Skip to content

Commit f5c99c8

Browse files
authored
Merge pull request #827 from opentypejs/claude/fix-hinting-infinite-loop-Aokbh
Fix TrueType hinting VM infinite loop vulnerabilities
2 parents f95c049 + 0826408 commit f5c99c8

File tree

6 files changed

+670
-1
lines changed

6 files changed

+670
-1
lines changed

src/hintingtt.mjs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
*/
2828
import glyf from './tables/glyf.mjs';
2929

30+
// Safety limits to prevent denial-of-service from crafted fonts.
31+
const MAX_INSTRUCTIONS = 1000000; // max instructions per hinting session
32+
const MAX_CALL_DEPTH = 64; // max nested CALL/LOOPCALL depth
33+
const MAX_LOOP_COUNT = 10000; // max LOOPCALL iterations & SLOOP value
34+
3035
/*
3136
* turn on for intensive debugging.
3237
*/
@@ -593,6 +598,8 @@ Hinting.prototype.exec = function(glyph, ppem) {
593598

594599
fpgmState.funcs = [ ];
595600
fpgmState.font = font;
601+
fpgmState.instructionCount = 0;
602+
fpgmState.callDepth = 0;
596603

597604
if (DEBUG) {
598605
console.log('---EXEC FPGM---');
@@ -618,6 +625,8 @@ Hinting.prototype.exec = function(glyph, ppem) {
618625
new State('prep', font.tables.prep);
619626

620627
prepState.ppem = ppem;
628+
prepState.instructionCount = 0;
629+
prepState.callDepth = 0;
621630

622631
// Creates a copy of the cvt table
623632
// and scales it to the current ppem setting.
@@ -676,6 +685,8 @@ execGlyph = function(glyph, prepState) {
676685
State.prototype = prepState;
677686
if (!components) {
678687
state = new State('glyf', glyph.instructions);
688+
state.instructionCount = 0;
689+
state.callDepth = 0;
679690
if (DEBUG) {
680691
console.log('---EXEC GLYPH---');
681692
state.step = -1;
@@ -691,6 +702,8 @@ execGlyph = function(glyph, prepState) {
691702
const cg = font.glyphs.get(c.glyphIndex);
692703

693704
state = new State('glyf', cg.instructions);
705+
state.instructionCount = 0;
706+
state.callDepth = 0;
694707

695708
if (DEBUG) {
696709
console.log('---EXEC COMP ' + i + '---');
@@ -834,6 +847,11 @@ exec = function(state) {
834847
let ins;
835848

836849
for (state.ip = 0; state.ip < pLen; state.ip++) {
850+
if (++state.instructionCount > MAX_INSTRUCTIONS) {
851+
throw new Error(
852+
'Hinting instructions exceeded maximum of ' + MAX_INSTRUCTIONS
853+
);
854+
}
837855
if (DEBUG) state.step++;
838856
ins = instructionTable[prog[state.ip]];
839857

@@ -1230,6 +1248,10 @@ function SZPS(state) {
12301248
function SLOOP(state) {
12311249
state.loop = state.stack.pop();
12321250

1251+
if (state.loop > MAX_LOOP_COUNT) {
1252+
state.loop = MAX_LOOP_COUNT;
1253+
}
1254+
12331255
if (DEBUG) console.log(state.step, 'SLOOP[]', state.loop);
12341256
}
12351257

@@ -1349,10 +1371,16 @@ function DEPTH(state) {
13491371
function LOOPCALL(state) {
13501372
const stack = state.stack;
13511373
const fn = stack.pop();
1352-
const c = stack.pop();
1374+
let c = stack.pop();
1375+
1376+
if (c > MAX_LOOP_COUNT) c = MAX_LOOP_COUNT;
13531377

13541378
if (DEBUG) console.log(state.step, 'LOOPCALL[]', fn, c);
13551379

1380+
if (++state.callDepth > MAX_CALL_DEPTH) {
1381+
throw new Error('Hinting call depth exceeded maximum of ' + MAX_CALL_DEPTH);
1382+
}
1383+
13561384
// saves callers program
13571385
const cip = state.ip;
13581386
const cprog = state.prog;
@@ -1373,6 +1401,7 @@ function LOOPCALL(state) {
13731401
// restores the callers program
13741402
state.ip = cip;
13751403
state.prog = cprog;
1404+
state.callDepth--;
13761405
}
13771406

13781407
// CALL[] CALL function
@@ -1382,6 +1411,10 @@ function CALL(state) {
13821411

13831412
if (DEBUG) console.log(state.step, 'CALL[]', fn);
13841413

1414+
if (++state.callDepth > MAX_CALL_DEPTH) {
1415+
throw new Error('Hinting call depth exceeded maximum of ' + MAX_CALL_DEPTH);
1416+
}
1417+
13851418
// saves callers program
13861419
const cip = state.ip;
13871420
const cprog = state.prog;
@@ -1394,6 +1427,7 @@ function CALL(state) {
13941427
// restores the callers program
13951428
state.ip = cip;
13961429
state.prog = cprog;
1430+
state.callDepth--;
13971431

13981432
if (DEBUG) console.log(++state.step, 'returning from', fn);
13991433
}

test/fonts/HintingJMPRLoop.ttf

632 Bytes
Binary file not shown.
688 Bytes
Binary file not shown.
668 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)