CVE-2026-106240
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
post_loop_type_src/maglev/maglev-ir.h |
modified | |
forsrc/maglev/maglev-loop-peeler.cc |
modified | |
whiletest/mjsunit/turbolev/regress-567177599.js |
modified |
Files Changed
src/maglev/maglev-ir.hsrc/maglev/maglev-loop-peeler.cctest/mjsunit/turbolev/regress-567177599.js
Patch
From 41d0351e57bb9282c5b2239e28865ac590c87e9d Mon Sep 17 00:00:00 2001
From: Darius Mercadier <dmercadier@chromium.org>
Date: Tue, 29 Sep 2026 13:00:10 +0200
Subject: [PATCH] [turbolev] Preserve phi type during loop peeling
Fixed: 567177599
Change-Id: I43e124b40eab9861516cc7247a9f3bd86b23d81b
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8486007
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Auto-Submit: Darius Mercadier <dmercadier@chromium.org>
Cr-Commit-Position: refs/heads/main@{#110155}
---
diff --git a/src/maglev/maglev-ir.h b/src/maglev/maglev-ir.h
index 90b50ef..ef6e647 100644
--- a/src/maglev/maglev-ir.h
+++ b/src/maglev/maglev-ir.h
@@ -10129,10 +10129,14 @@
// TODO(jgruber): More intuitive constructors, if possible.
Phi(uint64_t bitfield, MergePointInterpreterFrameState* merge_state,
interpreter::Register owner)
+ : Phi(bitfield, merge_state, owner, NodeType::kUnknown) {}
+
+ Phi(uint64_t bitfield, MergePointInterpreterFrameState* merge_state,
+ interpreter::Register owner, NodeType type)
: Base(bitfield),
owner_(owner),
merge_state_(merge_state),
- type_(NodeType::kUnknown),
+ type_(type),
post_loop_type_(NodeType::kUnknown) {
DCHECK_NOT_NULL(merge_state);
}
diff --git a/src/maglev/maglev-loop-peeler.cc b/src/maglev/maglev-loop-peeler.cc
index 7c5bd34..12edbec 100644
--- a/src/maglev/maglev-loop-peeler.cc
+++ b/src/maglev/maglev-loop-peeler.cc
@@ -711,8 +711,8 @@
for (Phi* phi : *block->phis()) {
DCHECK(!phi->is_exception_phi());
int input_count = phi->input_count();
- Phi* clone_phi = NodeBase::New<Phi>(zone(), input_count,
- dst_block->state(), phi->owner());
+ Phi* clone_phi = NodeBase::New<Phi>(
+ zone(), input_count, dst_block->state(), phi->owner(), phi->type());
for (int i = 0; i < input_count; ++i) {
clone_phi->set_input(i, Remap(ctx.value_map, phi->input(i).node()));
}
@@ -876,7 +876,7 @@
// what CloneBodySubgraph seeds into value_map for the header phis.
DCHECK_EQ(Remap(ctx.value_map, phi), phi->forward_edge());
Phi* pem_phi = NodeBase::New<Phi>(zone(), 2 * exit_edge_count, pem_state,
- phi->owner());
+ phi->owner(), phi->type());
for (int idx = 0; idx < exit_edge_count; ++idx) {
pem_phi->set_input(2 * idx, phi->forward_edge());
pem_phi->set_input(2 * idx + 1, phi);
@@ -1252,8 +1252,9 @@
// Create a new loop phi for each header phi and collect the rewires.
for (auto& [old_phi, pem_phi] : ctx.header_phi_to_pem_phi) {
- Phi* loop_phi = NodeBase::New<Phi>(
- zone(), kPhiInputCount, old_phi->merge_state(), old_phi->owner());
+ Phi* loop_phi =
+ NodeBase::New<Phi>(zone(), kPhiInputCount, old_phi->merge_state(),
+ old_phi->owner(), old_phi->type());
// Slot 0 takes the peeled clone of the back-edge value (via value_map);
// slot 1 the original back-edge value. If the back-edge value is itself a
// header phi (a recursive loop phi), slot 1 still points at the old phi
diff --git a/test/mjsunit/turbolev/regress-567177599.js b/test/mjsunit/turbolev/regress-567177599.js
new file mode 100644
index 0000000..9ed14c0
--- /dev/null
+++ b/test/mjsunit/turbolev/regress-567177599.js
@@ -0,0 +1,36 @@
+// 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 --turbolev
+
+var loop_value = 1.2;
+var exit_value = 2.5;
+loop_value = 1.4;
+exit_value = 1.5;
+
+const box = {value: 1.2};
+const input = {[Symbol.toPrimitive]() { return box.value; }};
+
+function store(n, input) {
+ let value = 1.1;
+ let sum = 0;
+ while (n-- > 0) {
+ sum += value;
+ value = +input;
+ loop_value = value;
+ }
+ exit_value = value;
+ return sum;
+}
+
+%PrepareFunctionForOptimization(store);
+store(3, input);
+
+%OptimizeFunctionOnNextCall(store);
+store(3, input);
+
+box.value = 42;
+store(3, input);
+assertEquals(42, exit_value);
+assertFalse(%IsSmi(exit_value));
Regression Test / PoC
diff --git a/test/mjsunit/turbolev/regress-567177599.js b/test/mjsunit/turbolev/regress-567177599.js
new file mode 100644
index 0000000..9ed14c0
--- /dev/null
+++ b/test/mjsunit/turbolev/regress-567177599.js
@@ -0,0 +1,36 @@
+// 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 --turbolev
+
+var loop_value = 1.2;
+var exit_value = 2.5;
+loop_value = 1.4;
+exit_value = 1.5;
+
+const box = {value: 1.2};
+const input = {[Symbol.toPrimitive]() { return box.value; }};
+
+function store(n, input) {
+ let value = 1.1;
+ let sum = 0;
+ while (n-- > 0) {
+ sum += value;
+ value = +input;
+ loop_value = value;
+ }
+ exit_value = value;
+ return sum;
+}
+
+%PrepareFunctionForOptimization(store);
+store(3, input);
+
+%OptimizeFunctionOnNextCall(store);
+store(3, input);
+
+box.value = 42;
+store(3, input);
+assertEquals(42, exit_value);
+assertFalse(%IsSmi(exit_value));
Original Bug Report
V8 Turbolev loop-phi type widening enables caged read/write
V8’s Turbolev compiler can store a tagged small integer into a global property cell whose optimized users expect a heap object. Attacker-controlled JavaScript can use the resulting type confusion to forge a double array and corrupt other heap objects. Chrome 154.0.8037.57 includes the flaw. The final impact is arbitrary byte read/write across the 4 GiB V8 pointer-compression cage.
Note: Turbolev must be enabled. This feature appears to be becoming default very very soon, as it is already enabled by default in Chrome-For-Testing 154.
Root cause
The script initializes global h and h2 with changing fractional numbers, then optimizes this loop:
function f(n, b) {
let x = 1.1, total = 0;
while (n-- > 0) {
total += x;
x = +b;
h = x;
}
h2 = x;
return total;
}
The globals have kConstantType property cells containing HeapNumbers. TryBuildPropertyCellStore checks the heap-object type before emitting the store. MergeLoopValue carries that checked type into the original loop phi. The final store to h2 relies on this guarantee.
Non-eager loop peeling creates cloned internal phis, peel-exit merge phis, and replacement loop phis without preserving the source phi’s static type:
Phi* loop_phi = NodeBase::New<Phi>(
zone(), kPhiInputCount, old_phi->merge_state(), old_phi->owner());
The new phi starts with an unknown type. Type recomputation can therefore widen it to include Smis. Phi representation selection then uses kCanonicalizeSmi where the original graph required kForceHeapNumber. Supplying the integer-valued HeapNumber 0x11111111 places Smi bits 0x22222222 in h2. A separately optimized array access treats those bits as a heap pointer and reads the map at cage base plus 0x22222221.
Commit 1f1f6c93385c introduced the faulty phi construction. The first affected V8 version is 15.1.70. Commit c6018c43ca5c enabled this peeling pass by default within Turbolev, starting in V8 15.2.104. The same missing type copies exist in upstream main.
Exploitability Analysis
Turbolev must be enabled. Chrome is rolling it out through the V8Turbolev field trial; the Chrome configuration selects V8Flag_turbolev for the enabled group. The ordinary-JavaScript PoC obtains caged read/write in default Chrome-for-Testing 154.0.8037.57 and in ordinary Chrome Stable 154.0.8037.57 with that feature enabled.
The payload places a forged JSArray header in a double-array allocation. The corrupted global points to that header, so optimized indexed accesses use its attacker-controlled elements pointer and length. Those accesses disclose an object’s compressed address and expand a Uint8Array into a byte view of the cage. The PoC reads a victim’s tagged property and changes its value from 1 to 42. Crossing the V8 heap-sandbox boundary requires a separate escape.
Proof of Concept
Run the crash input with standard release d8:
d8 --run-as-security-poc --turbolev --allow-natives-syntax turbolev-loop-phi-crash.js
d8 receives SIGSEGV. An ASan diagnostic run with --disable-in-process-stack-traces reports:
ERROR: AddressSanitizer: SEGV on unknown address 0x71b322222221
The signal is caused by a READ memory access.
#0 0x5aedadb00578 (<unknown module>)
#1 ... in Builtins_InterpreterEntryTrampoline
The caged-read/write PoC targets release d8 15.6.16 and Chrome 154.0.8037.57:
d8 --run-as-security-poc --turbolev turbolev-loop-phi-caged-rw.js
Expected output:
cageBytes=4294967296
before=1 after=42
Remediation
Preserve the source phi’s static type when creating the three kinds of replacement phi. v8-turbolev-loop-peel-phi-types.patch makes these assignments and adds a regression that checks the loop-exit value retains its HeapNumber representation. The patched build passes the regression, returns undefined for the crash input, and keeps the PoC’s byte view at its original length of 4096.
Disclaimer
This information is being shared by OpenAI solely for the purpose of improving security and reducing potential harm. This information is presented as-is. We make no representations or warranties, express or implied, as to the completeness, accuracy, or fitness for any particular purpose of the information. This includes, without limitation any suggestions or ideas presented on how to remedy or mitigate an identified vulnerability, including whether such suggestions or ideas would be effective and/or could have other negative impacts.
OpenAI disclaims any liability for direct or indirect damages arising from the reliance on, or use, misuse, or interpretation of this information. Any references to third-party systems, services, or entities are included solely for identification purposes and do not imply endorsement, responsibility, or attribution.
CREDIT INFORMATION
OpenAI Codex Security (amyb)
- https://chromium.googlesource.com/chromium/src/testing/+/d477a5baad6fd6b2a4e82d0bbd051a3ac6f7b58e/variations/fieldtrial_testing_config.json
- https://github.com/v8/v8/blob/15.1.70/src/maglev/maglev-loop-peeler.cc
- https://github.com/v8/v8/blob/671f7c27ac0403ceb8074a8dfbcc02cdd7369c3f/src/maglev/maglev-graph-builder.cc#L3238-L3261
- https://github.com/v8/v8/blob/671f7c27ac0403ceb8074a8dfbcc02cdd7369c3f/src/maglev/maglev-interpreter-frame-state.cc#L1052-L1073
- https://github.com/v8/v8/blob/671f7c27ac0403ceb8074a8dfbcc02cdd7369c3f/src/maglev/maglev-kna-processor.h#L134-L158
- https://github.com/v8/v8/blob/671f7c27ac0403ceb8074a8dfbcc02cdd7369c3f/src/maglev/maglev-loop-peeler.cc#L1253-L1266
- https://github.com/v8/v8/blob/671f7c27ac0403ceb8074a8dfbcc02cdd7369c3f/src/maglev/maglev-loop-peeler.cc#L709-L721
- https://github.com/v8/v8/blob/671f7c27ac0403ceb8074a8dfbcc02cdd7369c3f/src/maglev/maglev-loop-peeler.cc#L872-L886
- https://github.com/v8/v8/blob/671f7c27ac0403ceb8074a8dfbcc02cdd7369c3f/src/maglev/maglev-phi-representation-selector.cc#L1530-L1557
- https://github.com/v8/v8/blob/fe8f6737e1c06f8bf79c2bcf59fa6ba28b77ccb3/src/maglev/maglev-loop-peeler.cc#L1253-L1266
- https://github.com/v8/v8/commit/1f1f6c93385c84476df2a3d9ed4124a530a64156
- https://github.com/v8/v8/commit/c6018c43ca5cae3873428e79b64234de16c7691d