Vega VEGA / Bug List / CVE-2026-6307
Finding · Chrome · V8 TurboFan · JS-to-Wasm lazy deoptimization · Jun 27, 2026
CVE-2026-6307

JS-to-Wasm FrameState merging breaks V8 sandbox boundaries

src/compiler/frame-states.cc · FrameStateFunctionInfo::operator==
Affects Chrome 106-146 Chrome 147 before 147.0.7727.101 Chrome 148 beta V8 TurboFan V8 heap sandbox
Disclosure timeline
  1. Reported
    Mar 29, 2026
  2. Acknowledged
    Mar 30, 2026
  3. Fixed
    Mar 31, 2026
  4. Released
    Apr 07, 2026
  5. Disclosed
    Jun 27, 2026

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 externref and i64 return values during lazy deoptimization
  • Renderer impact: addrof and fakeobj primitives 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: () -> externref
i64 continuation: () -> i64
^ return type not compared

When 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:

  1. Put a target object in an externref global and cause that call result to be reconstructed as i64. The full tagged pointer is exposed as a BigInt, giving addrof.
  2. Put an attacker-controlled i64 in a Wasm global and cause that result to be reconstructed as externref. V8 treats the integer as a tagged object pointer, giving fakeobj.

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.

Read the deep-dive

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.

Related findings

See all ↗
CVE-2026-5865CNA 8.8

Maglev: incorrect phi untagging can lead to exploitable write barrier omission

type-confusion
Issue 494914816

Maglev: type confusion in [REDACTED] can lead to arbitrary code execution

type-confusion
Issue 493534950

Maglev: type confusion in [REDACTED] can lead to arbitrary code execution

type-confusion
Vega · AI security research

Find bugs in your code. Before anyone else does.

Sign up