High chrome Memory Corruption 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in V8
DescriptionMemory corruption in V8
ComponentV8
Sub-componentCompiler
Bug ClassMemory Corruption
Tracker569659194
Fix commitaf9e5c36d892 (v8/v8) +153/-12
CISA KEVNot listed
CreditedXinyang Ge
Disclosed2026-10-15

Changed Functions

FunctionChangeNotes
for
test/mjsunit/compiler/regress-569659194-3.js
modified

Files Changed

  • test/mjsunit/compiler/regress-569659194-3.js
  • src/compiler/operator-properties.cc
  • test/mjsunit/compiler/regress-569659194-1.js
  • src/compiler/js-generic-lowering.cc
  • test/mjsunit/compiler/regress-569659194-2.js
  • src/compiler/js-operator.cc
  • src/compiler/js-intrinsic-lowering.cc
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.