From 0f42d04eb600aed0acf4b53a42068db3236dcfc3 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sat, 29 Aug 2026 03:13:33 +0200 Subject: [PATCH] fix: guard parser stack bounds Wasm inputs nested beyond 2,048 entries wrote past fixed parser stacks, which could terminate Node instead of returning a parse error. Fixes: https://github.com/nodejs/cjs-module-lexer/issues/81 --- lexer.js | 30 +- lib/lexer.wasm | Bin 22167 -> 22244 bytes lib/lexer.wat | 772 ++++++++++++++++++++++++++----------------------- src/lexer.c | 40 ++- test/_unit.js | 63 ++++ 5 files changed, 529 insertions(+), 376 deletions(-) diff --git a/lexer.js b/lexer.js index 1614fd9..664b729 100755 --- a/lexer.js +++ b/lexer.js @@ -1,4 +1,5 @@ let source, pos, end; +const STACK_DEPTH = 2048; let openTokenDepth, templateDepth, lastTokenPos, @@ -37,6 +38,20 @@ const Import = 0; const ExportAssign = 1; const ExportStar = 2; +/** @param {number} tokenPos */ +function pushOpenToken (tokenPos) { + if (openTokenDepth === STACK_DEPTH) + throw new Error('Maximum nesting depth exceeded.'); + openTokenPosStack[openTokenDepth++] = tokenPos; +} + +function pushTemplate () { + if (templateStackDepth === STACK_DEPTH || openTokenDepth === STACK_DEPTH) + throw new Error('Maximum nesting depth exceeded.'); + templateStack[templateStackDepth++] = templateDepth; + templateDepth = ++openTokenDepth; +} + function parseCJS (source, name = '@') { resetState(); try { @@ -125,7 +140,7 @@ function parseSource (cjsSource) { pos += 23; if (source.charCodeAt(pos) === 40/*(*/) { pos++; - openTokenPosStack[openTokenDepth++] = lastTokenPos; + pushOpenToken(lastTokenPos); if (tryParseRequire(Import) && keywordStart(startPos)) { tryBacktrackAddStarExportBinding(startPos - 1); } @@ -136,7 +151,7 @@ function parseSource (cjsSource) { if (source.startsWith('Star', pos)) pos += 4; if (source.charCodeAt(pos) === 40/*(*/) { - openTokenPosStack[openTokenDepth++] = lastTokenPos; + pushOpenToken(lastTokenPos); if (source.charCodeAt(pos + 1) === 114/*r*/) { pos++; tryParseRequire(ExportStar); @@ -170,7 +185,7 @@ function parseSource (cjsSource) { tryParseObjectDefineOrKeys(openTokenDepth === 0); break; case 40/*(*/: - openTokenPosStack[openTokenDepth++] = lastTokenPos; + pushOpenToken(lastTokenPos); break; case 41/*)*/: if (openTokenDepth === 0) @@ -178,9 +193,9 @@ function parseSource (cjsSource) { openTokenDepth--; break; case 123/*{*/: - openClassPosStack[openTokenDepth] = nextBraceIsClass; + pushOpenToken(lastTokenPos); + openClassPosStack[openTokenDepth - 1] = nextBraceIsClass; nextBraceIsClass = false; - openTokenPosStack[openTokenDepth++] = lastTokenPos; break; case 125/*}*/: if (openTokenDepth === 0) @@ -1324,7 +1339,7 @@ function throwIfImportStatement () { switch (ch) { // dynamic import case 40/*(*/: - openTokenPosStack[openTokenDepth++] = startPos; + pushOpenToken(startPos); return; // import.meta case 46/*.*/: @@ -1382,8 +1397,7 @@ function templateString () { const ch = source.charCodeAt(pos); if (ch === 36/*$*/ && source.charCodeAt(pos + 1) === 123/*{*/) { pos++; - templateStack[templateStackDepth++] = templateDepth; - templateDepth = ++openTokenDepth; + pushTemplate(); return; } if (ch === 96/*`*/) diff --git a/lib/lexer.wasm b/lib/lexer.wasm index 3ff889cb0472640070395de659f0584fbac54dff..577fbedf0bccaf6b96f8df6ee2b63371cfa0d937 100755 GIT binary patch delta 959 zcmY*YO=}ZT6uoaIlVZTyV z6N?pJI zj~)(6?EZ}P_@aD%>Xp1zc_OE(#ZDB*+1QEWn9-8~ZZ#4cPxgE_bn{e+gvu-jeItAd2kBI+%kS36p~uIwY)Ha z(4hlgt}^w^SSlsam6z&Q&W5A5UqEK6=3Sr!R9i4?3lgi&eB1*SDugcOnmAq{-_&RO z0UvUF#mQ)UG|XxvG|W-wyr*G4g(A$iJRe_>Aa)Kw_#+kX0Y53(N>&M`+T=nq%WLDoCZK)C;t#lL^MWspI0vXTJq z@7p(UW0iH4z8U}t)Jhc)V;rrJR#|ZQpt(5s)4b})YYVsM$k&9?X3tq*HSo?lm~Ua8 kY@?4r?Yk1mxK`6(h-J6O%J%TtN_jA$gp+;5Bw>L@Wb$kiQ^sP4Gvd z%@nDGi1mqZjfF)l1w~C`s~8LMSBwgRjc?~}SQm!(&ExyNdGqb;_St5iJsxMiU{3g4 z4<^{Xy7LD0qvJ31`usyZ-577HIQC;Nj$=lv1l(CnT%7Ahl_;oCCyvKPy}!_nTpkPH zL`YjwQh5a}=~LZ@Wm(m?8#64{{l;d1H@6{Ukm_qEF7~P%Vag?62TPrlQ6E1M9}}9mThnJ5g{G z{9?N)?pip5A1s{3_ZHR*-+AJ;rUI8T%~RF)7gib5-pQTbfg)z^l~w*xt|t0-6tgdt za+ULc@PP_#@Zl(Um+DtKc8r^3DIGeG?n=Ybg;aS-MhPg73O?Op`KmjR0*a1a$*!M` zMzTLZVdmm}pg)kyn+yF-4+1IPR})`YsNg^i84or)F49rthwqBZ3gwrH@p$nx2~-7jcq_$=DnV4 WPlgm!!sw} { + const stackDepth = 2048; + const expressionNesting = [ + ['(', ')'], + ['import(', ')'], + ['class A{a=', '}'], + ['`${', '}`'] + ]; + + /** + * @param {number} depth + */ + function mixedSource (depth) { + const braceDepth = Math.floor(depth / 5); + const expressionDepth = depth - braceDepth; + const openings = new Array(expressionDepth); + const closings = new Array(expressionDepth); + for (let index = 0; index < expressionDepth; index++) { + const [opening, closing] = expressionNesting[index % expressionNesting.length]; + openings[index] = opening; + closings[expressionDepth - index - 1] = closing; + } + return '{'.repeat(braceDepth) + openings.join('') + '0' + closings.join('') + + ';' + '}'.repeat(braceDepth); + } + + const sources = [ + [ + '('.repeat(stackDepth) + '0' + ')'.repeat(stackDepth), + '('.repeat(stackDepth + 1) + '0' + ')'.repeat(stackDepth + 1) + ], + [ + '{'.repeat(stackDepth) + '0;' + '}'.repeat(stackDepth), + '{'.repeat(stackDepth + 1) + '0;' + '}'.repeat(stackDepth + 1) + ], + [ + '`${'.repeat(stackDepth) + '0' + '}`'.repeat(stackDepth), + '`${'.repeat(stackDepth + 1) + '0' + '}`'.repeat(stackDepth + 1) + ], + [ + 'class A{a='.repeat(stackDepth) + '0' + '}'.repeat(stackDepth), + 'class A{a='.repeat(stackDepth + 1) + '0' + '}'.repeat(stackDepth + 1) + ], + [ + 'import('.repeat(stackDepth) + '"x"' + ')'.repeat(stackDepth), + 'import('.repeat(stackDepth + 1) + '"x"' + ')'.repeat(stackDepth + 1) + ], + [ + mixedSource(stackDepth), + mixedSource(stackDepth + 1) + ] + ]; + const expectedError = process.env.WASM || process.env.WASM_SYNC + ? /^Error: Parse error @/ + : /^Error: Maximum nesting depth exceeded\./; + + for (const [accepted, rejected] of sources) { + assert.deepStrictEqual(parse(accepted), { exports: [], reexports: [] }); + assert.throws(() => parse(rejected), expectedError); + assert.deepStrictEqual(parse('exports.after = 1'), { exports: ['after'], reexports: [] }); + } + }); + test('Division / Regex ambiguity', () => { const source = ` /as)df/; x();