Summary
CVE-2026-6307 can achieve all of the following on its own:
- Gain arbitrary memory read/write primitive with 100% success rate, without any spraying trick
- Achieve V8 heap sandbox escape and RCE on its own, without any other bug
- Found in Chrome 106, across 4 years
CVE-2026-6307 is a TurboFan metadata bug in V8’s JS-to-Wasm lazy
deoptimization path. Two inlined JS-to-Wasm continuation FrameStates with
different Wasm return signatures can be treated as equal, letting common
subexpression elimination merge metadata that should stay distinct.
Once optimized code later deoptimizes, V8 can materialize a Wasm return value
with the wrong type. In practice, the bug confuses externref and i64,
yielding both a full tagged-pointer leak and a way to treat attacker-controlled
64-bit integers as JavaScript object references.
Impact & exploitability
The attacker model is remote JavaScript execution in Chrome. A malicious page can build two Wasm getter paths with identical parameter lists but different return types, warm a JavaScript property access until TurboFan inlines both JS-to-Wasm wrappers, then trigger lazy deoptimization after one of the continuation states has been incorrectly shared.
The resulting primitive crosses two boundaries at once:
- Attacker model: remote webpage / renderer-context JavaScript
- Primitive: confused materialization of
externrefandi64return values during lazy deoptimization - Renderer impact:
addrofandfakeobjprimitives from tagged-pointer leak and forged object reference - Sandbox impact: forged references can point outside the V8 heap sandbox, enabling writes through optimized in-object property stores
- End result: renderer RCE, with a V8-sandbox escape primitive from the same underlying bug
Root cause
The equality operator for FrameStateFunctionInfo compares the base-class
fields used by several frame-state kinds:
return lhs.type() == rhs.type() && lhs.parameter_count() == rhs.parameter_count() && lhs.max_arguments() == rhs.max_arguments() && lhs.local_count() == rhs.local_count() && lhs.shared_info().equals(rhs.shared_info()) && lhs.bytecode_array().equals(rhs.bytecode_array());JSToWasmFrameStateFunctionInfo extends that base class with a Wasm
signature_. That signature decides how a JS-to-Wasm lazy-deopt return value
is reconstructed: an i64 return is boxed as a BigInt, while an externref
return is treated as a tagged JavaScript reference.
Because the equality operator accepted base-class references and never compared
the derived signature_, two different continuation states could compare
equal:
externref continuation: () -> externrefi64 continuation: () -> i64 ^ return type not comparedWhen TurboFan inlines two Wasm getters into one optimized JavaScript function,
common subexpression elimination can merge those FrameState nodes. Nothing
breaks on the normal optimized path. The failure appears only when execution
takes a lazy deoptimization exit after a JS-to-Wasm call returns.
At that point, the deoptimizer trusts the serialized return kind from the
surviving FrameState. If the actual call returned an externref but the
merged metadata says i64, the tagged pointer bits become a BigInt. If the
actual call returned an i64 but the metadata says externref, attacker-chosen
integer bits become a JavaScript object reference.
Reproducer
The trigger shape is two Wasm functions with identical parameter lists and different return types, exposed as JavaScript property getters:
read full writeup and reproducer
let arm_deopt = false;
function LeakI64() {} function LeakRef() {}
const exports_ = makeInstance(() => { if (arm_deopt) { LeakRef.prototype.deopt_marker = 1; } });
Object.defineProperty(LeakI64.prototype, 'x', {get: exports_.rl, configurable: true}); Object.defineProperty(LeakRef.prototype, 'x', {get: exports_.rr, configurable: true});
function foo(o) { return o.x; }Warm function with instances of both prototypes so TurboFan specializes both receiver shapes and inlines both JS-to-Wasm wrappers. The imported Wasm callback mutates one prototype after optimization, invalidating the optimized code while the caller is suspended below the JS-to-Wasm call. The transition then occurs as a lazy deoptimization when the Wasm call returns.
Exploit
The exploit first turns the return-kind confusion into the standard V8 building blocks:
- Put a target object in an
externrefglobal and cause that call result to be reconstructed asi64. The full tagged pointer is exposed as aBigInt, givingaddrof. - Put an attacker-controlled
i64in a Wasm global and cause that result to be reconstructed asexternref. V8 treats the integer as a tagged object pointer, givingfakeobj.
The same fakeobj direction is what makes the bug pierce the V8 heap sandbox.
The forged reference is materialized from a full 64-bit integer, so it can point
outside the sandbox region. If the fake object has a valid-looking map and
properties layout, an optimized in-object property store such as r.p = v
writes relative to that forged pointer.
The public write-up uses this to target JIT memory. Constants are staged in JIT-compiled code, then a property-store write patches nearby instructions and redirects execution into attacker-controlled bytes.
The full write-up covers the TurboFan FrameState merge, lazy deoptimization mechanics, addrof/fakeobj construction, and the sandbox escape, JIT-memory write technique.
Patch
The fix makes the equality comparison account for the derived JS-to-Wasm
continuation metadata. Two JSToWasmFrameStateFunctionInfo objects with
different Wasm signatures must not compare equal:
// JSToWasmFrameStateFunctionInfo has an additional signature_ field.// Two frame states with different wasm signatures must not compare equal,// otherwise CSE/GVN can merge them and the deoptimizer will use the wrong// signature to materialize the continuation frame.if (lhs.type() == FrameStateType::kJSToWasmBuiltinContinuation && rhs.type() == FrameStateType::kJSToWasmBuiltinContinuation) { if (static_cast<const JSToWasmFrameStateFunctionInfo&>(lhs).signature() != static_cast<const JSToWasmFrameStateFunctionInfo&>(rhs).signature()) { return false; }}Chrome released the fix in 147.0.7727.101 on Apr 7, 2026.
Mitigation
Update Chrome to a fixed build. The disclosure states that the bug was
introduced in Chrome 106, affected released Chrome versions before the fixed
Chrome 147 build, and also affected Chrome 148 beta at the time of report. The
fixed stable release is Chrome 147.0.7727.101.
There is no meaningful site-level workaround for end users. The vulnerable surface is crafted JavaScript running in the renderer, so practical mitigation is browser update deployment and avoiding affected builds for untrusted browsing.
Timeline & credit
Nebula Security reported the bug to Google on Mar 29, 2026. Google
acknowledged it on Mar 30, identified the root cause and completed the fix on
Mar 31, and released the fix on Apr 7 in Chrome 147.0.7727.101. The public
Longinus deep-dive was published on Jun 27, 2026.