]> git.kaiwu.me - njs.git/commitdiff
Parser: fix "for" initializer parsing after a failed speculative parse
authorDmitry Volyntsev <xeioex@nginx.com>
Tue, 1 Sep 2026 01:29:41 +0000 (18:29 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Fri, 4 Sep 2026 20:56:37 +0000 (13:56 -0700)
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 <Expression>
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.

src/njs_parser.c
src/test/njs_unit_test.c

index c54cd55df329e2cf9337330a0a813b6a9a759b50..debace62c69316fe2dc3b99ba61b20b19ae9bdb9 100644 (file)
@@ -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);
     }
index 8ffd3a97b67ee9f628503566eb7f75b1ce6dca3b..639bb7a8c1a3c0b19e15077e087afc5f8c20bd6f 100644 (file)
@@ -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"