Date: March 6, 2026
Reproducer:
for(function(){r({/a/;0;1)1Root cause is a parser scope leak during speculative for (...) head parsing,
not a primary generator bug.
./build/njs for_fuzz.js-> ASan SEGV innjs_scope_value()(src/njs_scope.h:81in current tree), reached vianjs_vmcode_operand(src/njs_vmcode.c) while executing generated bytecode.- Crash case disassembly includes
REGEXP 0103 .... - Valid baseline
for(/a/;0;1)1yieldsREGEXP 0123 ...and does not crash.
0103 encodes a local-level temp in this context, while top-level execution is
not expected to reference such leaked local scope slots.
Key states:
njs_parser_iteration_statement_for_map()(src/njs_parser.c:5974) default branch pushes:- fence:
njs_parser_for_expression_map_reparsewithoptional=0(src/njs_parser.c:6080) - continue:
njs_parser_for_expression_map_continuewithoptional=1(src/njs_parser.c:6086)
- fence:
- Parsing LHS enters
functionexpression path. njs_parser_function_expression()opens function scope (njs_parser_scope_begin(... NJS_SCOPE_FUNCTION ...)atsrc/njs_parser.c:7468).- Scope close is done later only in
njs_parser_function_lambda_body_after()(njs_parser_scope_end()atsrc/njs_parser.c:8274), and that continuation was pushed optional. - On malformed input, parser falls into
njs_parser_reject()(src/njs_parser.c:537), which pops optional entries until fence and does not unwind scope. - Therefore function scope opened at 7468 survives incorrectly.
- Fence handler
njs_parser_for_expression_map_reparse()runs; whenparser->node == NULLit reparses vianjs_parser_expression(src/njs_parser.c:5812-5817). - New nodes created during reparse capture leaked scope because
njs_parser_node_new()setsnode->scope = parser->scope(src/njs_parser.h:184). - Generator later allocates temp indexes from that wrong owning function scope
(
njs_generate_temp_index_get(),src/njs_generator.c:6287+). - Runtime dereferences invalid level slot path and crashes.
Important correction: the leaked scope is the speculative function-expression
scope, not the block scope opened for entering for (...).
Relevant grammar intent (ECMAScript):
for ( Expression? ; Expression? ; Expression? ) Statementfor ( LeftHandSideExpression in Expression ) Statementfor ( var/let/const ... in Expression ) Statement- and
for...ofcounterparts (njs currently rejectsofhere).
Disambiguation requirement: parser may speculatively try candidates, but rejected candidates must not leak semantic state (scope/binding/AST ownership) into the accepted parse.
So invalid input must end as syntax error only; producing executable AST/bytecode is non-compliant behavior.
Redesign for head parsing to remove reparse/reject speculation for this
grammar point.
Use deterministic dispatch with context flag allow_in = false for classic
for init parsing, and do not parse one branch then roll back to another.
After consuming for (:
- If next token is
;:- parse classic form with empty init.
- Else if next token is
var|let|const:- parse declaration list once.
- if next token is
in(orofwhen supported), parsefor-in/of. - if next token is
;, parse classicfor. - otherwise syntax error.
- Else:
- parse init expression once with
allow_in = false. - then inspect delimiter:
;-> classicfor.in-> parsefor-inwith LHS validity checks.of-> current njs behavior (not supported) or futurefor-of.- otherwise syntax error.
- parse init expression once with
Add/propagate a context bit in expression parser path:
njs_parser_expression_ctx(..., allow_in)or equivalent.- relational parsing must not consume
inwhenallow_in == false.
This removes the need for for-head reparse handlers and the optional-fence
mechanism in this path.
- No rollback state to leak (scope/bindings/targets).
- Closer to ECMAScript disambiguation model for
forheads. - Easier to reason about and fuzz: one parse path per token sequence.
- Eliminates this crash class in
forparsing instead of patching symptoms.
Generator is missing a guaranteed invariant, not syntax data:
- every executable node must carry scope provenance from the final accepted parse path.
Without that contract, generator cannot distinguish valid from leaked-scope nodes and may emit indexes with wrong level/type.
Parser:
src/njs_parser.c:537njs_parser_reject()src/njs_parser.c:5805njs_parser_for_expression_map_reparse()src/njs_parser.c:5974njs_parser_iteration_statement_for_map()src/njs_parser.c:7468function-scope open innjs_parser_function_expression()src/njs_parser.c:8274function-scope close innjs_parser_function_lambda_body_after()src/njs_parser.h:184scope capture innjs_parser_node_new()
Generator/runtime:
src/njs_generator.c:6287njs_generate_temp_index_get()src/njs_scope.h:81njs_scope_value()src/njs_vmcode.coperand resolution path (njs_vmcode_operand)
for(function(){r({/a/;0;1)1for(async function(){r({/a/;0;1)1for(function(){r({/a/;0;1);1for(function(){r({/a/;0;1)11for(function(){r({/a/;0;1)1;
Expected: syntax error only, no crash.
These currently produce syntax errors and are useful for coverage, but do not prove this crash mechanism by themselves:
for(function(){function(){/a/;0;1)1for(()=>{r({/a/;0;1)1for(function(){if(1){/a/;0;1)1
- The bug is parser reject/backtracking scope corruption.
- Generator/VM crash is downstream from leaked scope provenance in AST nodes.
- Best path: replace speculative
forhead parsing with a deterministicallow_in = falsedesign and keep regression corpus for malformedforheads.