---
title: "CVE-2026-6307 — JS-to-Wasm FrameState merging breaks V8 sandbox boundaries"
description: "JS-to-Wasm FrameState merging breaks V8 sandbox boundaries (src/compiler/frame-states.cc · FrameStateFunctionInfo::operator==)"
url: https://nebusec.ai/buglist/CVE-2026-6307/
published: 2026-06-27T00:00:00.000Z
---

CVE-2026-6307 · Critical

# JS-to-Wasm FrameState merging breaks V8 sandbox boundaries

Chrome · V8 TurboFan · JS-to-Wasm lazy deoptimization · Published Jun 27, 2026

src/compiler/frame-states.cc · FrameStateFunctionInfo::operator==

* Fixed
* Type confusion
* Remote

Affects · Chrome 106-146 · Chrome 147 before 147.0.7727.101 · Chrome 148 beta · V8 TurboFan · V8 heap sandbox

[View reproducer→](#reproducer) · [Research deep-dive](https://nebusec.ai/research/v8-cve-2026-6307-writeup.md)

Artifacts · [reproducer](#reproducer) · [exploit.md](#exploit) · [patch.diff](#patch)

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 `FrameState`s 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:

```cpp
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:

```text
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](https://nebusec.ai/research/v8-cve-2026-6307-writeup.md)

```js
  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 →](https://nebusec.ai/research/v8-cve-2026-6307-writeup.md)

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

```diff
// 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

NebuSec 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 findings →](https://nebusec.ai/buglist/)

[CVE-2026-5865](https://nebusec.ai/buglist/CVE-2026-5865.md)

[Maglev: incorrect phi untagging can lead to exploitable write barrier omission](https://nebusec.ai/buglist/CVE-2026-5865.md)

[Type-confusion · Score 8.8 · Read](https://nebusec.ai/buglist/CVE-2026-5865.md)

Issue 494914816

Maglev: stale post-inline Smi phi type can lead to exploitable write barrier omission

Type-confusion

Issue 493534950

Maglev: unsound node replacement when inlining can lead to exploitable write barrier omission

Write-barrier-omission

Find bugs in your code.

Before anyone else does.

[Book a demo](https://nebusec.ai/book-a-demo/)
