CVE-2026-14383
Overview
Background
- `for-of` iteration
- The JavaScript language construct that repeatedly calls an iterator’s
next()method, each call producing a result with avalueand adoneflag. - `ForOfNext`
- The V8 bytecode/builtin that drives one step of a
for-ofloop, historically writing bothvalueanddoneinto a pair of adjacent interpreter registers. - Deopt continuation
- A builtin such as
ForOfNextResultDeoptContinuationthat resumes execution and reproduces the semantics of an optimized operation after the optimizing compiler bails out (deoptimizes) mid-operation. - The hole (`TheHoleConstant`)
- A special internal V8 sentinel value, distinct from
undefined, used to signal “no value here”; it must never leak into user-visible JavaScript.
Root Cause Analysis
The for-of machinery returned two independent values, value and done, and the deopt continuations ForOfNextResultDeoptContinuation, ForOfNextValueEagerDeoptContinuation, and the ForOfNextLoadValue* continuations wrote both of them back into a caller-supplied register pair via StoreValueAndDoneInRegisterPair. Because the continuation received a raw value_done_reg register index and manually poked parent_frame_pointer at computed value_offset and done_offset using StoreFullTaggedNoWriteBarrier, the invariant that the two register slots and their contents stay consistent with the interpreter’s expectation was fragile: on the deopt paths the value/done pair could be written incoherently, so consumers could read a done/value combination the loop never actually produced.
The fix collapses the two return values into one: when done is true the builtin now returns TheHoleConstant() (via Return) instead of separately storing undefined plus a done flag, and when not done it returns the real value, letting the hole itself signal termination. This removes the hand-rolled dual-slot frame writes entirely (the StoreValueAndDoneInRegisterPair helper is deleted), eliminating the class of inconsistent register-pair states and the associated builtin/baseline calling-convention mismatch.
value and done) written to a manually addressed register pair, which allowed an inconsistent value/done state on deopt continuation paths. The fix uses a single return value and encodes “done” as TheHoleConstant, so a coherent value can no longer be split apart or desynchronized.Attack Path
- Enter an optimized `for-of` loop
Craft JavaScript with a
for-ofloop over an object whose iteratornext()returns attacker-controlled result objects, and get it optimized by the compiler. - Force a deoptimization mid-step
Trigger an eager or lazy deopt exactly during the
ForOfNextoperation (e.g. via a side-effectingdoneorvaluegetter), routing execution intoForOfNextResultDeoptContinuationorForOfNextValueEagerDeoptContinuation. - Exploit the register-pair write
Rely on
StoreValueAndDoneInRegisterPairwritingvalueanddoneinto the manually computedvalue_offset/done_offsetslots of the parent frame, producing avalue/donecombination inconsistent with what the interpreter expects. - Consume the desynchronized state Let the loop body observe the mismatched register pair, causing V8 to treat a not-really-done iteration as done (or vice versa) and mis-handle the loop variable.
- Escalate the logic error Leverage the resulting incorrect value/type assumptions downstream to corrupt engine state or bypass expected iteration invariants.
Impact Assessment
for-of iteration, obtaining incorrect or inconsistent iterator results that can violate engine invariants and serve as a primitive toward further corruption. It requires only that the victim execute attacker-controlled JavaScript and that a deoptimization occur during a for-of step, with no special permissions. The bug is confined to the V8 engine within the renderer process.Changed Functions
| Function | Change | Notes |
|---|---|---|
TF_BUILTINsrc/builtins/builtins-iterator-gen.cc |
modified |
Files Changed
src/baseline/baseline-compiler.ccsrc/builtins/builtins-iterator-gen.ccsrc/builtins/builtins-iterator-gen.hsrc/builtins/iterator.tq
Audit Directions
- Multi-value register writes on deopt pathsReview other deopt continuations that manually compute frame offsets and write multiple tagged slots (e.g. via
StoreFullTaggedNoWriteBarrieronLoadParentFramePointer), since they can desynchronize related values. - Sentinel-vs-undefined conventionsAudit builtins that switch between returning
TheHoleConstant/UndefinedConstantto encode state flags, checking that the hole never escapes to user-visible JavaScript and that consumers agree on the convention. - Baseline/builtin calling-convention driftVerify that callers like
BaselineCompiler::VisitForOfNextand their target builtins agree on argument and output-register contracts, since a mismatch in who owns which slot can silently corrupt interpreter state.
Patch
From 02a725e2a0762f39266fdac2ba2869c71af37ed2 Mon Sep 17 00:00:00 2001 From: Marja Hölttä <[email protected]> Date: Tue, 12 May 2026 11:25:04 +0200 Subject: [PATCH] [for-of] Use a single return value Technically we return two values, "value" and "done". But when "done" is true, there's no value. We can thus only return "value" and use the hole to signal that done is true. This also fixes a bunch of open bugs. Fixed: 492410546 Bug: 408061015 Change-Id: I2757aff95743d53e785fb9386e0b7018c8324005 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7826150 Reviewed-by: Leszek Swirski <[email protected]> Commit-Queue: Marja Hölttä <[email protected]> Cr-Commit-Position: refs/heads/main@{#107253} --- diff --git a/src/baseline/baseline-compiler.cc b/src/baseline/baseline-compiler.cc index 28e4166..bdf8087 100644 --- a/src/baseline/baseline-compiler.cc +++ b/src/baseline/baseline-compiler.cc @@ -2661,14 +2661,10 @@ Register next = scratch_scope.AcquireScratch(); __ LoadRegister(object, RegisterOperand(0)); __ LoadRegister(next, RegisterOperand(1)); - // Pass the output register slot as an argument, so that the builtin - // is responsible for writing into the slots. - Register out_reg_address = scratch_scope.AcquireScratch(); - basm_.RegisterFrameAddress(RegisterOperand(2), out_reg_address); + CallBuiltin<Builtin::kForOfNextBaseline>(object, // object next, // next - out_reg_address, // out_reg - FeedbackSlotAsSmi(3)); // call_slot + FeedbackSlotAsSmi(2)); // call_slot } void BaselineCompiler::VisitGetIterator() { diff --git a/src/builtins/builtins-iterator-gen.cc b/src/builtins/builtins-iterator-gen.cc index bf08cb0..d567daf 100644 --- a/src/builtins/builtins-iterator-gen.cc +++ b/src/builtins/builtins-iterator-gen.cc @@ -524,63 +524,32 @@ Return(iterator); } -void IteratorBuiltinsAssembler::StoreValueAndDoneInRegisterPair( - TNode<Context> context, TNode<Object> value, TNode<Object> done, - TNode<Smi> value_done_reg) { - // ForOfNext writes to a register pair. Since this is an eager deopt - // continuation, we need to manually update the registers in the parent - // interpreted frame. - TNode<IntPtrT> reg_index = SmiToIntPtr(value_done_reg); - TNode<IntPtrT> base_offset = - IntPtrConstant(InterpreterFrameConstants::kRegisterFileFromFp); - TNode<IntPtrT> value_offset = - IntPtrSub(base_offset, TimesSystemPointerSize(reg_index)); - TNode<IntPtrT> done_offset = IntPtrSub( - base_offset, - TimesSystemPointerSize(IntPtrAdd(reg_index, IntPtrConstant(1)))); - - auto parent_frame_pointer = LoadParentFramePointer(); - StoreFullTaggedNoWriteBarrier(parent_frame_pointer, value_offset, value); - StoreFullTaggedNoWriteBarrier(parent_frame_pointer, done_offset, done); -} - -// ForOfNextResultDeoptContinuation is used for the lazy deoptimization on -// next() call and the eager deoptimization of loading done value. TF_BUILTIN(ForOfNextResultDeoptContinuation, IteratorBuiltinsAssembler) { auto context = Parameter<Context>(Descriptor::kContext); - auto value_done_reg = Parameter<Smi>(Descriptor::kValueDoneReg); auto result_object = Parameter<Object>(Descriptor::kResultObject); Label is_jsreceiver(this), if_notjsreceiver(this, Label::kDeferred); BranchIfJSReceiver(result_object, &is_jsreceiver, &if_notjsreceiver); BIND(&is_jsreceiver); - TVARIABLE(Object, var_value); - TVARIABLE(Object, var_done); - - Label if_done(this), if_not_done(this), end(this, &var_value); - var_done = + TNode<Object> var_done = GetProperty(context, CAST(result_object), factory()->done_string()); - BranchIfToBooleanIsTrue(var_done.value(), &if_done, &if_not_done); + + Label if_done(this), if_not_done(this); + BranchIfToBooleanIsTrue(var_done, &if_done, &if_not_done); BIND(&if_done); { - var_value = UndefinedConstant(); - Goto(&end); + Return(TheHoleConstant()); } BIND(&if_not_done); { - var_value = + TNode<Object> value = GetProperty(context, CAST(result_object), factory()->value_string()); - Goto(&end); + Return(value); } - BIND(&end); - StoreValueAndDoneInRegisterPair(context, var_value.value(), var_done.value(), - value_done_reg); - Return(var_value.value()); - BIND(&if_notjsreceiver); CallRuntime(Runtime::kThrowIteratorResultNotAnObject, context, result_object); Unreachable(); @@ -590,51 +559,36 @@ auto context = Parameter<Context>(Descriptor::kContext); auto result_object = Parameter<Object>(Descriptor::kResultObject); auto done = Parameter<Object>(Descriptor::kDone); - auto value_done_reg = Parameter<Smi>(Descriptor::kValueDoneReg); - TVARIABLE(Object, var_value); - Label if_done(this), if_not_done(this), end(this, &var_value); + Label if_done(this), if_not_done(this); BranchIfToBooleanIsTrue(done, &if_done, &if_not_done); BIND(&if_done); { - var_value = UndefinedConstant(); - Goto(&end); + Return(TheHoleConstant()); } BIND(&if_not_done); { - var_value = + TNode<Object> value = GetProperty(context, CAST(result_object), factory()->value_string()); - Goto(&end); + Return(value); } - - BIND(&end); - StoreValueAndDoneInRegisterPair(context, var_value.value(), done, - value_done_reg); - Return(var_value.value()); } TF_BUILTIN(ForOfNextLoadValueEagerDeoptContinuation, IteratorBuiltinsAssembler) { auto context = Parameter<Context>(Descriptor::kContext); auto result_object = Parameter<Object>(Descriptor::kResultObject); - auto value_done_reg = Parameter<Smi>(Descriptor::kValueDoneReg); TNode<Object> value = GetProperty(context, CAST(result_object), factory()->value_string()); - StoreValueAndDoneInRegisterPair(context, value, FalseConstant(), - value_done_reg); Return(value); } TF_BUILTIN(ForOfNextLoadValueLazyDeoptContinuation, IteratorBuiltinsAssembler) { - auto context = Parameter<Context>(Descriptor::kContext); auto value = Parameter<Object>(Descriptor::kValue); - auto value_done_reg = Parameter<Smi>(Descriptor::kValueDoneReg); - StoreValueAndDoneInRegisterPair(context, value, FalseConstant(), - value_done_reg); Return(value); } diff --git a/src/builtins/builtins-iterator-gen.h b/src/builtins/builtins-iterator-gen.h index 4aee1bf..21f79ca 100644 --- a/src/builtins/builtins-iterator-gen.h +++ b/src/builtins/builtins-iterator-gen.h @@ -97,9 +97,7 @@ TNode<JSArray> FastIterableToList(TNode<Context> context, TNode<JSAny> iterable, Label* slow); - void StoreValueAndDoneInRegisterPair(TNode<Context> context, - TNode<Object> value, TNode<Object> done, - TNode<Smi> value_done_reg); + void StoreRegister(TNode<Smi> reg, TNode<Object> value); }; } // namespace internal diff --git a/src/builtins/iterator.tq b/src/builtins/iterator.tq index 87ad17e..537a84f 100644 --- a/src/builtins/iterator.tq +++ b/src/builtins/iterator.tq @@ -129,40 +129,22 @@ return CreateAsyncFromSyncIterator(context, syncIterator); } -struct ForOfNextResult { - value: Object; - done: Object; -} -
Regression Test / PoC
diff --git a/test/mjsunit/es6/for-of-array-iterator-optimization-maglev-lazy-next-call-done-deopt.js b/test/mjsunit/es6/for-of-array-iterator-optimization-maglev-lazy-next-call-done-deopt.js
new file mode 100644
index 0000000..126a35e
--- /dev/null
+++ b/test/mjsunit/es6/for-of-array-iterator-optimization-maglev-lazy-next-call-done-deopt.js
@@ -0,0 +1,50 @@
+// 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 --maglev --no-stress-maglev
+// Flags: --for-of-optimization
+
+function lazy_next() {
+ this.count++;
+ if (this.count === 4) {
+ // Trigger a lazy deopt in the caller frame upon return!
+ %DeoptimizeFunction(testForOfLazyDeoptNextCall);
+ }
+ if (this.count === 4) {
+ return {done: true};
+ }
+ return {value: this.count * 10, done: false}; // Sums to 60
+}
+// Keep next() unoptimized to ensure a clean call/return boundary
+// that correctly exercises the lazy deopt continuation.
+%NeverOptimizeFunction(lazy_next);
+
+const iterable = {
+ [Symbol.iterator]() {
+ return {
+ count: 0,
+ next: lazy_next
+ };
+ }
+};
+
+function testForOfLazyDeoptNextCall() {
+ let sum = 0;
+ for (const x of iterable) {
+ sum += x;
+ }
+ return sum;
+}
+
+%PrepareFunctionForOptimization(testForOfLazyDeoptNextCall);
+testForOfLazyDeoptNextCall();
+testForOfLazyDeoptNextCall();
+
+%OptimizeMaglevOnNextCall(testForOfLazyDeoptNextCall);
+testForOfLazyDeoptNextCall();
+
+const result = testForOfLazyDeoptNextCall();
+
+assertUnoptimized(testForOfLazyDeoptNextCall);
+assertEquals(60, result);
diff --git a/test/mjsunit/regress/regress-492410546.js b/test/mjsunit/regress/regress-492410546.js
new file mode 100644
index 0000000..b3ab96a
--- /dev/null
+++ b/test/mjsunit/regress/regress-492410546.js
@@ -0,0 +1,35 @@
+// 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 --for-of-optimization --turbolev-future --turbolev
+
+const resultWithGetter = {};
+Object.defineProperty(resultWithGetter, 'value', {
+ get: function () { }, // effectively returns undefined
+});
+
+const myIterable = {
+ [Symbol.iterator]() {
+ return {
+ next() {
+ return Object.create(resultWithGetter);
+ }
+ };
+ }
+};
+
+function foo() {
+ try {
+ for (const v of myIterable) {
+ String.prototype.endsWith.apply();
+ }
+ } catch (e) {}
+}
+%PrepareFunctionForOptimization(foo);
+
+// Both calls are needed to repro this bug.
+foo();
+foo();
+%OptimizeFunctionOnNextCall(foo);
+foo();
diff --git a/test/unittests/interpreter/bytecode-array-builder-unittest.cc b/test/unittests/interpreter/bytecode-array-builder-unittest.cc
index 2597754..4a775c4 100644
--- a/test/unittests/interpreter/bytecode-array-builder-unittest.cc
+++ b/test/unittests/interpreter/bytecode-array-builder-unittest.cc
@@ -253,7 +253,7 @@
// Emit Iterator-protocol operations
builder.GetIterator(reg, load_slot.ToInt(), call_slot.ToInt());
- builder.ForOfNext(reg, reg, pair, 1);
+ builder.ForOfNext(reg, reg, 1);
// Emit load / store lookup slots.
builder.LoadLookupSlot(name, TypeofMode::kNotInside)
diff --git a/test/unittests/interpreter/bytecode_expectations/ForOfOptimization.golden b/test/unittests/interpreter/bytecode_expectations/ForOfOptimization.golden
index b46c6dc..0a1e388 100644
--- a/test/unittests/interpreter/bytecode_expectations/ForOfOptimization.golden
+++ b/test/unittests/interpreter/bytecode_expectations/ForOfOptimization.golden
@@ -12,7 +12,7 @@
"
frame size: 13
parameter count: 1
-bytecode array length: 107
+bytecode array length: 110
bytecodes: [
/* 48 S> */ B(CreateArrayLiteral), U8(0:ARRAY_BOILERPLATE_DESCRIPTION_TYPE), FBV(0), Flag8(0x25),
B(Star4),
@@ -27,13 +27,17 @@
B(Mov), R(context), R(9),
B(LdaTrue),
B(Star5),
- /* 43 s> */ B(ForOfNext), R(3), R(2), R(4), FBV(7),
- B(Ldar), R(5),
- B(JumpIfToBooleanTrue), U8(14),
- /* 43 S> */ B(Mov), R(4), R(1),
+ /* 43 s> */ B(ForOfNext), R(3), R(2), FBV(7),
+ B(Star4),
+ B(LdaTheHole),
+ B(TestReferenceEqual), R(4),
+ B(JumpIfTrue), U8(16),
+ B(LdaFalse),
+ /* 43 S> */ B(Star5),
+ B(Mov), R(4), R(1),
/* 43 S> */ B(Mov), R(1), R(0),
B(Ldar), R(0),
- /* 34 E> */ B(JumpLoop), U8(19), I8(0), FBV(13),
+ /* 34 E> */ B(JumpLoop), U8(22), I8(0), FBV(13),
B(LdaSmi), I8(-1),
B(Star7),
B(Star6),
@@ -77,8 +81,8 @@
INTERNALIZED_ONE_BYTE_STRING_TYPE ["return"],
]
handlers: [
- [22, 45, 51],
- [64, 83, 85],
+ [22, 48, 54],
+ [67, 86, 88],
]
---
@@ -87,7 +91,7 @@
"
frame size: 13
parameter count: 1
-bytecode array length: 117
+bytecode array length: 120
bytecodes: [
/* 58 S> */ B(CreateArrayLiteral), U8(0:ARRAY_BOILERPLATE_DESCRIPTION_TYPE), FBV(0), Flag8(0x25),
B(Star3),
@@ -106,13 +110,17 @@
B(Mov), R(context), R(9),
B(LdaTrue),
B(Star5),
- /* 43 s> */ B(ForOfNext), R(3), R(2), R(4), FBV(11),
- B(Ldar), R(5),
- B(JumpIfToBooleanTrue), U8(14),
- /* 43 S> */ B(Mov), R(4), R(1),
+ /* 43 s> */ B(ForOfNext), R(3), R(2), FBV(11),
+ B(Star4),
+ B(LdaTheHole),
+ B(TestReferenceEqual), R(4),
+ B(JumpIfTrue), U8(16),
+ B(LdaFalse),
+ /* 43 S> */ B(Star5),
+ B(Mov), R(4), R(1),
/* 43 S> */ B(Mov), R(1), R(0),
B(Ldar), R(0),
- /* 34 E> */ B(JumpLoop), U8(19), I8(0), FBV(17),
+ /* 34 E> */ B(JumpLoop), U8(22), I8(0), FBV(17),
B(LdaSmi), I8(-1),
B(Star7),
B(Star6),
@@ -157,8 +165,8 @@
INTERNALIZED_ONE_BYTE_STRING_TYPE ["return"],
]
handlers: [
- [32, 55, 61],
- [74, 93, 95],
+ [32, 58, 64],
+ [77, 96, 98],
]
---
@@ -167,7 +175,7 @@
"
frame size: 13
parameter count: 1
-bytecode array length: 117
+bytecode array length: 120
bytecodes: [
/* 58 S> */ B(CreateArrayLiteral), U8(0:ARRAY_BOILERPLATE_DESCRIPTION_TYPE), FBV(0), Flag8(0x25),
B(Star3),
@@ -186,13 +194,17 @@
B(Mov), R(context), R(9),
B(LdaTrue),
B(Star5),
- /* 43 s> */ B(ForOfNext), R(3), R(2), R(4), FBV(11),
- B(Ldar), R(5),
- B(JumpIfToBooleanTrue), U8(14),
- /* 43 S> */ B(Mov), R(4), R(1),
+ /* 43 s> */ B(ForOfNext), R(3), R(2), FBV(11),
+ B(Star4),
+ B(LdaTheHole),
+ B(TestReferenceEqual), R(4),
+ B(JumpIfTrue), U8(16),
+ B(LdaFalse),
+ /* 43 S> */ B(Star5),
+ B(Mov), R(4), R(1),
/* 43 S> */ B(Mov), R(1), R(0),
B(Ldar), R(0),
- /* 34 E> */ B(JumpLoop), U8(19), I8(0), FBV(17),
+ /* 34 E> */ B(JumpLoop), U8(22), I8(0), FBV(17),
B(LdaSmi), I8(-1),
B(Star7),
B(Star6),
@@ -237,7 +249,7 @@
INTERNALIZED_ONE_BYTE_STRING_TYPE ["return"],
]
handlers: [
- [32, 55, 61],
- [74, 93, 95],
+ [32, 58, 64],
+ [77, 96, 98],
]
Original Bug Report
DCHECK failure in (maglev_value) != nullptr in turbolev-graph-builder.cc
Detailed Report: https://clusterfuzz.com/testcase?key=6662523457568768
Fuzzer: ochang_js_fuzzer Job Type: linux32_d8_dbg Platform Id: linux
Crash Type: DCHECK failure Crash Address: Crash State: (maglev_value) != nullptr in turbolev-graph-builder.cc
Sanitizer: address (ASAN)
Regressed: https://clusterfuzz.com/revisions?job=linux32_d8_dbg&range=105738:105739
Reproducer Testcase: https://clusterfuzz.com/download?testcase_id=6662523457568768
Issue filed automatically.
To reproduce this, please build the target in this report and run it against the reproducer testcase. Please use the GN arguments provided at bottom of this report when building the binary.
If you have trouble reproducing, please also export the environment variables listed under “[Environment]” in the crash stacktrace.
If you have any feedback on reproducing test cases, let us know at https://forms.gle/Yh3qCYFveHj6E5jz5 so we can improve.