Skip to content

Instantly share code, notes, and snippets.

@xeioex
Created March 6, 2026 06:55
Show Gist options
  • Select an option

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

Select an option

Save xeioex/f98ee1e44302dd811eff11e3064b7db4 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) Correct fix approach

3.1 Immediate targeted fix (for this bug)

In for LHS speculative path, capture pre-LHS scope and restore it in reparse fence before calling njs_parser_expression.

Practical implementation:

  • Replace bare text payload passed to reparse fence with a tiny struct:
    • original token text (for existing error reporting)
    • saved njs_parser_scope_t *saved_scope
  • In njs_parser_for_expression_map_reparse(), when parser->node == NULL, restore parser->scope = saved_scope before reparse.

This closes the immediate OSS-fuzz crash path with minimal risk.

3.2 Systemic fix (recommended)

Make reject/fence unwinding scope-aware globally.

Concrete model:

  • Extend njs_parser_stack_entry_t with njs_parser_scope_t *scope_snapshot.
  • _njs_parser_after() stores current parser->scope in each entry.
  • In njs_parser_reject():
    • pop optional entries as today,
    • on first non-optional fence, restore parser->scope from that fence entry snapshot before switching state/target.

This gives deterministic scope rollback for all speculative parse fences.

3.3 Defense-in-depth (secondary)

  • Add debug/asan assertions in generator/runtime to fail safely when scope/type invariants are broken.
  • Add AST shape guard for FOR before codegen to reject malformed synthetic container nodes rather than executing them.

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: land targeted fix now, then land systemic reject-scope snapshot rollback 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