From: Dmitry Volyntsev Date: Tue, 1 Sep 2026 01:29:41 +0000 (-0700) Subject: Parser: fix "for" initializer parsing after a failed speculative parse X-Git-Tag: 1.0.2~90 X-Git-Url: https://git.kaiwu.me/%7B@url%7D?a=commitdiff_plain;h=55f72dc3defe3eba610d9e0304ab544d17652545;p=njs.git Parser: fix "for" initializer parsing after a failed speculative parse Previously, the "for" initializer was parsed speculatively as a LeftHandSideExpression, and njs_parser_for_expression_map_reparse() resumed parsing whenever that parse produced no node. njs_parser_reject() rewinds the state stack only, so the resume continued from a later token with an abandoned function scope still on parser->scope, and generated bytecode with a scope that is never instantiated at run time. The fix is to remove that landing state together with its frame, so a rejected speculative parse reports a SyntaxError, and to dispatch the initializer starts that cannot begin a LeftHandSideExpression to directly. Routing "await" through that dispatch also applies [~In] to it, so a bare "in" in an await initializer is now a SyntaxError. Found by OSS-Fuzz. --- diff --git a/src/njs_parser.c b/src/njs_parser.c index c54cd55d..debace62 100644 --- a/src/njs_parser.c +++ b/src/njs_parser.c @@ -329,9 +329,6 @@ static njs_int_t njs_parser_iteration_statement_for(njs_parser_t *parser, static njs_int_t njs_parser_for_expression_map_continue( njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current); -static njs_int_t njs_parser_for_expression_map_reparse( - njs_parser_t *parser, njs_lexer_token_t *token, - njs_queue_link_t *current); static njs_int_t njs_parser_expression_continue_op(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current); static njs_int_t njs_parser_expression_continue_assign_comma( @@ -5801,26 +5798,6 @@ njs_parser_iteration_statement_for(njs_parser_t *parser, } -static njs_int_t -njs_parser_for_expression_map_reparse(njs_parser_t *parser, - njs_lexer_token_t *token, njs_queue_link_t *current) -{ - if (parser->ret != NJS_OK && parser->node != NULL) { - return njs_parser_failed(parser); - } - - if (parser->node == NULL) { - njs_lexer_in_fail_set(parser->lexer, 1); - - njs_parser_next(parser, njs_parser_expression); - - return NJS_OK; - } - - return njs_parser_stack_pop(parser); -} - - static njs_int_t njs_parser_for_expression_map_continue(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current) @@ -6047,11 +6024,6 @@ njs_parser_iteration_statement_for_map(njs_parser_t *parser, goto expression_after; - case NJS_TOKEN_AWAIT: - njs_parser_next(parser, njs_parser_expression); - - goto expression_after; - default: ret = njs_parser_match_arrow_expression(parser, token); if (ret == NJS_OK) { @@ -6062,6 +6034,30 @@ njs_parser_iteration_statement_for_map(njs_parser_t *parser, return NJS_ERROR; } + /* The speculative parse below has no way back, see the log. */ + + switch (token->type) { + case NJS_TOKEN_DELETE: + case NJS_TOKEN_VOID: + case NJS_TOKEN_TYPEOF: + case NJS_TOKEN_AWAIT: + case NJS_TOKEN_ADDITION: + case NJS_TOKEN_SUBTRACTION: + case NJS_TOKEN_BITWISE_NOT: + case NJS_TOKEN_LOGICAL_NOT: + case NJS_TOKEN_INCREMENT: + case NJS_TOKEN_DECREMENT: + njs_lexer_in_fail_set(parser->lexer, 1); + + parser->target = NULL; + njs_parser_next(parser, njs_parser_expression); + + goto expression_after; + + default: + break; + } + parser->target = NULL; njs_parser_next(parser, njs_parser_left_hand_side_expression); @@ -6077,12 +6073,6 @@ njs_parser_iteration_statement_for_map(njs_parser_t *parser, *text = token->text; - ret = njs_parser_after(parser, current, text, 0, - njs_parser_for_expression_map_reparse); - if (ret != NJS_OK) { - return NJS_ERROR; - } - return njs_parser_after(parser, current, text, 1, njs_parser_for_expression_map_continue); } diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index 8ffd3a97..639bb7a8 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -3488,6 +3488,12 @@ static njs_unit_test_t njs_test[] = { njs_str("for (in + j;;) {}"), njs_str("SyntaxError: Unexpected token \"in\"") }, + { njs_str("for (var x = [1 in {}];;) break;"), + njs_str("undefined") }, + + { njs_str("for (var x = true ? 1 in {} : 0;;) break;"), + njs_str("undefined") }, + { njs_str("for (true ? 0 in {}: 0; false; ) ;"), njs_str("undefined") }, @@ -3530,6 +3536,39 @@ static njs_unit_test_t njs_test[] = { njs_str("for(i;;)for(-new+3;;)break;"), njs_str("SyntaxError: Unexpected token \"+\"") }, + { njs_str("for(function(){r({/a/;0;1)1"), + njs_str("SyntaxError: Unexpected token \"/\"") }, + + { njs_str("for(a(function(){r({/a/;0;1)1"), + njs_str("SyntaxError: Unexpected token \"/\"") }, + + { njs_str("for(async function(){r({/a/;0;1)1"), + njs_str("SyntaxError: Unexpected token \"/\"") }, + + { njs_str("for({/a/;0;1)1"), + njs_str("SyntaxError: Unexpected token \"/\"") }, + + { njs_str("for(function f(){}.x in {a:1}); 1"), + njs_str("1") }, + + { njs_str("var o={}; for(function(){return o}().x in {a:1,b:2}); o.x"), + njs_str("b") }, + + { njs_str("async function f(){for(await p in o;;)break;} f()"), + njs_str("SyntaxError: Invalid left-hand side in for-loop") }, + + { njs_str("var a=0; for(-a;;)break; a"), + njs_str("0") }, + + { njs_str("var a=0; for(typeof a;;)break; a"), + njs_str("0") }, + + { njs_str("var a=0; for(++a;;)break; a"), + njs_str("1") }, + + { njs_str("var a={b:0}; for(delete a.b;;)break; a.b"), + njs_str("undefined") }, + /* switch. */ { njs_str("switch"), @@ -21754,6 +21793,16 @@ static njs_unit_test_t njs_externals_test[] = "f().then($r.retval)"), njs_str("X:4") }, + { njs_str("async function f() {" + " var r = [];" + " for (var k in await Promise.resolve({a:1, b:2})) {" + " r.push(k);" + " }" + " return r.join(':');" + "}" + "f().then($r.retval)"), + njs_str("a:b") }, + { njs_str("async function f() {" " return ((...a) => a[1] + ':' + a[2] + ':' + a[0].length)" " `a${await Promise.resolve(2)}b"