Skip to content

Instantly share code, notes, and snippets.

@xeioex
Created March 6, 2026 07:00
Show Gist options
  • Select an option

  • Save xeioex/fa65ef8396c4151a8a3081d871bb70f3 to your computer and use it in GitHub Desktop.

Select an option

Save xeioex/fa65ef8396c4151a8a3081d871bb70f3 to your computer and use it in GitHub Desktop.

NJS for_fuzz.js Investigation (Codex, Refined)

Date: March 6, 2026

Reproducer:

for(function(){r({/a/;0;1)1

1) Root cause

Root cause is a parser scope leak during speculative for (...) head parsing, not a primary generator bug.

Crash signature

  • ./build/njs for_fuzz.js -> ASan SEGV in njs_scope_value() (src/njs_scope.h:81 in current tree), reached via njs_vmcode_operand (src/njs_vmcode.c) while executing generated bytecode.
  • Crash case disassembly includes REGEXP 0103 ....
  • Valid baseline for(/a/;0;1)1 yields REGEXP 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.

Exact parser failure chain

Key states:

  • njs_parser_iteration_statement_for_map() (src/njs_parser.c:5974) default branch pushes:
    • fence: njs_parser_for_expression_map_reparse with optional=0 (src/njs_parser.c:6080)
    • continue: njs_parser_for_expression_map_continue with optional=1 (src/njs_parser.c:6086)
  • Parsing LHS enters function expression path.
  • njs_parser_function_expression() opens function scope (njs_parser_scope_begin(... NJS_SCOPE_FUNCTION ...) at src/njs_parser.c:7468).
  • Scope close is done later only in njs_parser_function_lambda_body_after() (njs_parser_scope_end() at src/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; when parser->node == NULL it reparses via njs_parser_expression (src/njs_parser.c:5812-5817).
  • New nodes created during reparse capture leaked scope because njs_parser_node_new() sets node->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 (...).

2) Spec mapping for for (...)

Relevant grammar intent (ECMAScript):

  • for ( Expression? ; Expression? ; Expression? ) Statement
  • for ( LeftHandSideExpression in Expression ) Statement
  • for ( var/let/const ... in Expression ) Statement
  • and for...of counterparts (njs currently rejects of here).

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.

3) Solution proposition: non-speculative for parser

Redesign for head parsing to remove reparse/reject speculation for this grammar point.

3.1 Core idea

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.

3.2 Proposed parse flow

After consuming for (:

  1. If next token is ;:
    • parse classic form with empty init.
  2. Else if next token is var|let|const:
    • parse declaration list once.
    • if next token is in (or of when supported), parse for-in/of.
    • if next token is ;, parse classic for.
    • otherwise syntax error.
  3. Else:
    • parse init expression once with allow_in = false.
    • then inspect delimiter:
      • ; -> classic for.
      • in -> parse for-in with LHS validity checks.
      • of -> current njs behavior (not supported) or future for-of.
      • otherwise syntax error.

3.3 Parser API adjustment

Add/propagate a context bit in expression parser path:

  • njs_parser_expression_ctx(..., allow_in) or equivalent.
  • relational parsing must not consume in when allow_in == false.

This removes the need for for-head reparse handlers and the optional-fence mechanism in this path.

3.4 Why this is better

  • No rollback state to leak (scope/bindings/targets).
  • Closer to ECMAScript disambiguation model for for heads.
  • Easier to reason about and fuzz: one parse path per token sequence.
  • Eliminates this crash class in for parsing instead of patching symptoms.

4) Generator-side "missing info"

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.

5) Code hotspots

Parser:

  • src/njs_parser.c:537 njs_parser_reject()
  • src/njs_parser.c:5805 njs_parser_for_expression_map_reparse()
  • src/njs_parser.c:5974 njs_parser_iteration_statement_for_map()
  • src/njs_parser.c:7468 function-scope open in njs_parser_function_expression()
  • src/njs_parser.c:8274 function-scope close in njs_parser_function_lambda_body_after()
  • src/njs_parser.h:184 scope capture in njs_parser_node_new()

Generator/runtime:

  • src/njs_generator.c:6287 njs_generate_temp_index_get()
  • src/njs_scope.h:81 njs_scope_value()
  • src/njs_vmcode.c operand resolution path (njs_vmcode_operand)

6) Regression tests

Confirmed crash repros (must be fixed)

  • for(function(){r({/a/;0;1)1
  • for(async function(){r({/a/;0;1)1
  • for(function(){r({/a/;0;1);1
  • for(function(){r({/a/;0;1)11
  • for(function(){r({/a/;0;1)1;

Expected: syntax error only, no crash.

Exploratory/non-crash variants (keep separate)

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)1
  • for(()=>{r({/a/;0;1)1
  • for(function(){if(1){/a/;0;1)1

7) Bottom line

  • The bug is parser reject/backtracking scope corruption.
  • Generator/VM crash is downstream from leaked scope provenance in AST nodes.
  • Best path: replace speculative for head parsing with a deterministic allow_in = false design and keep regression corpus for malformed for heads.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment