Chrome · V8 › Parser
CVE-2026-113192
Memory Corruption in V8 (Parser)
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Ftest/unittests/regress/regress-568831099-unittest.cc |
modified |
Files Changed
test/unittests/BUILD.gnsrc/parsing/parse-info.ccsrc/codegen/compiler.ccsrc/parsing/parser.ccsrc/objects/script.cctest/unittests/regress/regress-568831099-unittest.ccsrc/objects/shared-function-info.h
Patch
From 17f229f3ea0662f8fce3a77b1c1fcb2350748d7d Mon Sep 17 00:00:00 2001 From: Simon Zünd <[email protected]> Date: Tue, 06 Oct 2026 05:26:19 +0000 Subject: [PATCH] [parser] Restore parse restriction when reparsing Function constructor top-level Move the Script::CompilationKind::kFunctionConstructor check into UnoptimizedCompileFlags::SetFlagsForFunctionFromScript (guarded by is_toplevel()) so that top-level UnoptimizedCompileFlags::ForFunctionCompile also sets ONLY_SINGLE_FUNCTION_LITERAL. Also use parsing_dynamic_function_declaration_ for the eager compile hint in Parser::ParseFunctionLiteral, and check that syntax_kind and kind match when matching an existing SharedFunctionInfo to a FunctionLiteral. [email protected] Fixed: 568831099 Change-Id: Ie918dad4ee60ccbeb3d403e293d4802ba8123f2a Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8523457 Reviewed-by: Toon Verwaest <[email protected]> Commit-Queue: Simon Zünd <[email protected]> Cr-Commit-Position: refs/heads/main@{#110345} --- diff --git a/src/codegen/compiler.cc b/src/codegen/compiler.cc index c64c573..6e4bf13 100644 --- a/src/codegen/compiler.cc +++ b/src/codegen/compiler.cc @@ -737,6 +737,8 @@ void UpdateSharedFunctionFlagsAfterCompilation(FunctionLiteral* literal) { Tagged<SharedFunctionInfo> shared_info = *literal->shared_function_info(); DCHECK_EQ(shared_info->language_mode(), literal->language_mode()); + DCHECK_EQ(shared_info->syntax_kind(), literal->syntax_kind()); + DCHECK_EQ(shared_info->kind(), literal->kind()); // These fields are all initialised in ParseInfo from the SharedFunctionInfo, // and then set back on the literal after parse. Hence, they should already diff --git a/src/objects/script.cc b/src/objects/script.cc index 64c2f26..1a24f18 100644 --- a/src/objects/script.cc +++ b/src/objects/script.cc @@ -140,6 +140,8 @@ CHECK_EQ(result->EndPosition(), function_literal->end_position()); CHECK_EQ(result->function_literal_id(kRelaxedLoad), function_literal->function_literal_id()); + CHECK_EQ(result->syntax_kind(), function_literal->syntax_kind()); + CHECK_EQ(result->kind(), function_literal->kind()); function_literal->set_shared_function_info(result); return result; } diff --git a/src/objects/shared-function-info.h b/src/objects/shared-function-info.h index 8297f32..46ce635 100644 --- a/src/objects/shared-function-info.h +++ b/src/objects/shared-function-info.h @@ -828,13 +828,13 @@ void SetFunctionTokenPosition(int function_token_position, int start_position); - static void EnsureBytecodeArrayAvailable( + V8_EXPORT_PRIVATE static void EnsureBytecodeArrayAvailable( Isolate* isolate, Handle<SharedFunctionInfo> shared_info, IsCompiledScope* is_compiled_scope, CreateSourcePositions flag = CreateSourcePositions{false}); inline bool CanCollectSourcePosition(Isolate* isolate); - static void EnsureSourcePositionsAvailable( + V8_EXPORT_PRIVATE static void EnsureSourcePositionsAvailable( Isolate* isolate, DirectHandle<SharedFunctionInfo> shared_info); template <typename IsolateT> diff --git a/src/parsing/parse-info.cc b/src/parsing/parse-info.cc index 30db54d..f33708ab 100644 --- a/src/parsing/parse-info.cc +++ b/src/parsing/parse-info.cc @@ -74,10 +74,6 @@ : ScriptType::kClassic, v8_flags.lazy); flags.set_outer_language_mode(script->outer_language_mode()); - if (script->compilation_kind() == - Script::CompilationKind::kFunctionConstructor) { - flags.set_parse_restriction(ONLY_SINGLE_FUNCTION_LITERAL); - } flags.SetFlagsForFunctionFromScript(script); if (script->is_wrapped()) { flags.set_function_syntax_kind(FunctionSyntaxKind::kWrapped); @@ -158,6 +154,10 @@ set_block_coverage_enabled(block_coverage_enabled() && script->IsUserJavaScript()); + if (is_toplevel() && script->compilation_kind() == + Script::CompilationKind::kFunctionConstructor) { + set_parse_restriction(ONLY_SINGLE_FUNCTION_LITERAL); + } } ReusableUnoptimizedCompileState::ReusableUnoptimizedCompileState( diff --git a/src/parsing/parser.cc b/src/parsing/parser.cc index 05c5e161..c5898da 100644 --- a/src/parsing/parser.cc +++ b/src/parsing/parser.cc @@ -2835,13 +2835,11 @@ function_name = ast_value_factory()->empty_string(); } - // This is true if we get here through CreateDynamicFunction. - bool params_need_validation = parameters_end_pos_ != kNoSourcePosition; int compile_hint_position = peek_position(); FunctionLiteral::EagerCompileHint eager_compile_hint = function_state_->next_function_is_likely_called() || is_wrapped || - params_need_validation || + parsing_dynamic_function_declaration_ || (info()->flags().compile_hints_magic_enabled() && scanner()->SawMagicCommentCompileHintsAll()) || (info()->flags().compile_hints_per_function_magic_enabled() && diff --git a/test/unittests/regress/regress-568831099-unittest.cc b/test/unittests/regress/regress-568831099-unittest.cc new file mode 100644 index 0000000..26f523c --- /dev/null +++ b/test/unittests/regress/regress-568831099-unittest.cc @@ -0,0 +1,94 @@ +// Copyright 2026 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "src/api/api-inl.h" +#include "src/codegen/compiler.h" +#include "src/execution/isolate-inl.h" +#include "src/objects/feedback-vector-inl.h" +#include "src/objects/js-function-inl.h" +#include "src/objects/script-inl.h" +#include "src/objects/shared-function-info-inl.h" +#include "src/parsing/parse-info.h" +#include "test/unittests/test-utils.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace v8 { +namespace internal { + +using Regress568831099Test = TestWithContext; + +TEST_F(Regress568831099Test, ReparseFunctionConstructorToplevel) { + v8::HandleScope scope(isolate()); + + RunJS( + "var anonymous = 40;\n" + "var fn = new Function('a', 'if (a === 0) { anonymous; a.x } a.s = 1; " + "return a.s + anonymous;');\n" + "for (var i = 0; i < 20; i++) fn({s: 0});\n"); + + v8::Local<v8::Object> global = context()->Global(); + DirectHandle<JSFunction> fn = Cast<JSFunction>(v8::Utils::OpenDirectHandle( + *global->Get(context(), NewString("fn")).ToLocalChecked())); + + Handle<SharedFunctionInfo> inner_sfi(fn->shared(), i_isolate()); + DirectHandle<Script> script(Cast<Script>(inner_sfi->script()), i_isolate()); + EXPECT_EQ(script->compilation_kind(), + Script::CompilationKind::kFunctionConstructor); + EXPECT_EQ(inner_sfi->syntax_kind(), FunctionSyntaxKind::kAnonymousExpression); + + const int expected_slot_count = inner_sfi->feedback_metadata()->slot_count(); + ASSERT_TRUE(fn->has_feedback_vector()); + EXPECT_EQ(static_cast<int>(fn->feedback_vector()->length().value()), + expected_slot_count); + + Tagged<MaybeObject> maybe_toplevel = + script->infos()->get(kFunctionLiteralIdTopLevel); + Handle<SharedFunctionInfo> toplevel_sfi( + Cast<SharedFunctionInfo>(maybe_toplevel.GetHeapObjectAssumeWeak()), + i_isolate()); + ASSERT_TRUE(toplevel_sfi->is_toplevel()); + + // Top-level compiles for kFunctionConstructor scripts must restore the + // ONLY_SINGLE_FUNCTION_LITERAL parse restriction, while inner function + // compiles must not. + UnoptimizedCompileFlags toplevel_flags = + UnoptimizedCompileFlags::ForFunctionCompile(i_isolate(), *toplevel_sfi); + EXPECT_EQ(toplevel_flags.parse_restriction(), ONLY_SINGLE_FUNCTION_LITERAL); + + UnoptimizedCompileFlags inner_flags = + UnoptimizedCompileFlags::ForFunctionCompile(i_isolate(), *inner_sfi); + EXPECT_EQ(inner_flags.parse_restriction(), NO_PARSE_RESTRICTION); + + // Exercise source position collection on the top-level SFI while compiled. + SharedFunctionInfo::EnsureSourcePositionsAvailable(i_isolate(), toplevel_sfi); + + // Flush bytecode from both the top-level SFI and the inner function SFI, + // while `fn` keeps its FeedbackVector. + if (toplevel_sfi->CanDiscardCompiled()) { + SharedFunctionInfo::DiscardCompiled(i_isolate(), toplevel_sfi); + } + ASSERT_TRUE(inner_sfi->CanDiscardCompiled()); + SharedFunctionInfo::DiscardCompiled(i_isolate(), inner_sfi); + ASSERT_FALSE(toplevel_sfi->is_compiled()); + ASSERT_FALSE(inner_sfi->is_compiled()); + + // Recompiling the top-level SFI also eagerly recompiles the parenthesized + // `(function anonymous(...) { ... })` inner SFI. It must still be parsed as + // an anonymous dynamic function declaration rather than a named function + // expression. + IsCompiledScope is_compiled_scope; + SharedFunctionInfo::EnsureBytecodeArrayAvailable(i_isolate(), toplevel_sfi, + &is_compiled_scope, + CreateSourcePositions{true}); + EXPECT_TRUE(toplevel_sfi->is_compiled()); + EXPECT_TRUE(inner_sfi->is_compiled()); + EXPECT_EQ(inner_sfi->syntax_kind(), FunctionSyntaxKind::kAnonymousExpression); + EXPECT_EQ(inner_sfi->feedback_metadata()->slot_count(), expected_slot_count); + + v8::Local<v8::Value> result = RunJS("fn({s: 0})"); + EXPECT_EQ(result->Int32Value(context()).FromJust(), 41); +} + +} // namespace internal +} // namespace v8 diff --git a/test/unittests/BUILD.gn b/test/unittests/BUILD.gn index 316577f..fc1e850 100644 --- a/test/unittests/BUILD.gn +++ b/test/unittests/BUILD.gn @@ -508,6 +508,7 @@ "profiler/strings-storage-unittest.cc", "regexp/regexp-unittest.cc", "regress/regress-536271629-unittest.cc", + "regress/regress-568831099-unittest.cc", "regress/regress-crbug-1041240-unittest.cc", "regress/regress-crbug-1056054-unittest.cc", "regress/regress-crbug-938251-unittest.cc",
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/test/unittests/BUILD.gn b/test/unittests/BUILD.gn
index 316577f..fc1e850 100644
--- a/test/unittests/BUILD.gn
+++ b/test/unittests/BUILD.gn
@@ -508,6 +508,7 @@
"profiler/strings-storage-unittest.cc",
"regexp/regexp-unittest.cc",
"regress/regress-536271629-unittest.cc",
+ "regress/regress-568831099-unittest.cc",
"regress/regress-crbug-1041240-unittest.cc",
"regress/regress-crbug-1056054-unittest.cc",
"regress/regress-crbug-938251-unittest.cc",
diff --git a/test/unittests/regress/regress-568831099-unittest.cc b/test/unittests/regress/regress-568831099-unittest.cc
new file mode 100644
index 0000000..26f523c
--- /dev/null
+++ b/test/unittests/regress/regress-568831099-unittest.cc
@@ -0,0 +1,94 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/api/api-inl.h"
+#include "src/codegen/compiler.h"
+#include "src/execution/isolate-inl.h"
+#include "src/objects/feedback-vector-inl.h"
+#include "src/objects/js-function-inl.h"
+#include "src/objects/script-inl.h"
+#include "src/objects/shared-function-info-inl.h"
+#include "src/parsing/parse-info.h"
+#include "test/unittests/test-utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace v8 {
+namespace internal {
+
+using Regress568831099Test = TestWithContext;
+
+TEST_F(Regress568831099Test, ReparseFunctionConstructorToplevel) {
+ v8::HandleScope scope(isolate());
+
+ RunJS(
+ "var anonymous = 40;\n"
+ "var fn = new Function('a', 'if (a === 0) { anonymous; a.x } a.s = 1; "
+ "return a.s + anonymous;');\n"
+ "for (var i = 0; i < 20; i++) fn({s: 0});\n");
+
+ v8::Local<v8::Object> global = context()->Global();
+ DirectHandle<JSFunction> fn = Cast<JSFunction>(v8::Utils::OpenDirectHandle(
+ *global->Get(context(), NewString("fn")).ToLocalChecked()));
+
+ Handle<SharedFunctionInfo> inner_sfi(fn->shared(), i_isolate());
+ DirectHandle<Script> script(Cast<Script>(inner_sfi->script()), i_isolate());
+ EXPECT_EQ(script->compilation_kind(),
+ Script::CompilationKind::kFunctionConstructor);
+ EXPECT_EQ(inner_sfi->syntax_kind(), FunctionSyntaxKind::kAnonymousExpression);
+
+ const int expected_slot_count = inner_sfi->feedback_metadata()->slot_count();
+ ASSERT_TRUE(fn->has_feedback_vector());
+ EXPECT_EQ(static_cast<int>(fn->feedback_vector()->length().value()),
+ expected_slot_count);
+
+ Tagged<MaybeObject> maybe_toplevel =
+ script->infos()->get(kFunctionLiteralIdTopLevel);
+ Handle<SharedFunctionInfo> toplevel_sfi(
+ Cast<SharedFunctionInfo>(maybe_toplevel.GetHeapObjectAssumeWeak()),
+ i_isolate());
+ ASSERT_TRUE(toplevel_sfi->is_toplevel());
+
+ // Top-level compiles for kFunctionConstructor scripts must restore the
+ // ONLY_SINGLE_FUNCTION_LITERAL parse restriction, while inner function
+ // compiles must not.
+ UnoptimizedCompileFlags toplevel_flags =
+ UnoptimizedCompileFlags::ForFunctionCompile(i_isolate(), *toplevel_sfi);
+ EXPECT_EQ(toplevel_flags.parse_restriction(), ONLY_SINGLE_FUNCTION_LITERAL);
+
+ UnoptimizedCompileFlags inner_flags =
+ UnoptimizedCompileFlags::ForFunctionCompile(i_isolate(), *inner_sfi);
+ EXPECT_EQ(inner_flags.parse_restriction(), NO_PARSE_RESTRICTION);
+
+ // Exercise source position collection on the top-level SFI while compiled.
+ SharedFunctionInfo::EnsureSourcePositionsAvailable(i_isolate(), toplevel_sfi);
+
+ // Flush bytecode from both the top-level SFI and the inner function SFI,
+ // while `fn` keeps its FeedbackVector.
+ if (toplevel_sfi->CanDiscardCompiled()) {
+ SharedFunctionInfo::DiscardCompiled(i_isolate(), toplevel_sfi);
+ }
+ ASSERT_TRUE(inner_sfi->CanDiscardCompiled());
+ SharedFunctionInfo::DiscardCompiled(i_isolate(), inner_sfi);
+ ASSERT_FALSE(toplevel_sfi->is_compiled());
+ ASSERT_FALSE(inner_sfi->is_compiled());
+
+ // Recompiling the top-level SFI also eagerly recompiles the parenthesized
+ // `(function anonymous(...) { ... })` inner SFI. It must still be parsed as
+ // an anonymous dynamic function declaration rather than a named function
+ // expression.
+ IsCompiledScope is_compiled_scope;
+ SharedFunctionInfo::EnsureBytecodeArrayAvailable(i_isolate(), toplevel_sfi,
+ &is_compiled_scope,
+ CreateSourcePositions{true});
+ EXPECT_TRUE(toplevel_sfi->is_compiled());
+ EXPECT_TRUE(inner_sfi->is_compiled());
+ EXPECT_EQ(inner_sfi->syntax_kind(), FunctionSyntaxKind::kAnonymousExpression);
+ EXPECT_EQ(inner_sfi->feedback_metadata()->slot_count(), expected_slot_count);
+
+ v8::Local<v8::Value> result = RunJS("fn({s: 0})");
+ EXPECT_EQ(result->Int32Value(context()).FromJust(), 41);
+}
+
+} // namespace internal
+} // namespace v8
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page