# Journey to Root, Episode I: The Maglev King

Maglev's BuildCheckSmi treats Smi-range HeapNumbers as Smis, eliding write barriers and causing a use-after-free in V8, chained to renderer RCE.

- Payout: $117,000
- Severity: high
- Program: Google
- Write-up by: Duc Phan
- Disclosed: 2026-07-17
- Write-up quality: 9/10
- Original: https://blog.calif.io/p/journey-to-root-episode-i-the-maglev
- Library page: https://bugbountylibrary.com/r/blog%3Ablog-calif-io-p-journey-to-root-episode-i-the-maglev
- Tags: area:os domain:browser impact:integrity impact:rce kind:research platform:research sev:high tech:chromium technique:chained technique:source-review vuln:info-disclosure vuln:memory-corruption vuln:rce

**Target:** V8 Maglev JIT compiler (maglev-graph-builder.cc BuildCheckSmi)
**Root cause:** BuildCheckSmi's constant shortcut uses Smi::IsValid on an int32 constant to conclude the value is a Smi, conflating range with type; a HeapNumber holding a Smi-range value passes and the write barrier is elided.
**Steps:**
1. Warm up a function storing a Smi into a top-level `var` (PropertyCell in kConstantType state) so Maglev compiles the store via GetSmiValue → BuildCheckSmi.
2. Add a top-level `let bT = 11` and store x into it before the var store, so the kConst ContextCell records 11 as x's checked_value alternative, letting TryGetInt32Constant fire and drop CheckSmi.
3. Create a kInt32 ContextCell by assigning a let a number outside Smi range, then store x into it before bT so GetInt32 emits CheckedNumberToInt32 and caches a kInt32 alternative on x, avoiding CheckedSmiUntag deopt.
4. Trigger the optimized function with a HeapNumber (e.g. `f64src[0]` = 11.0) so the store writes a heap pointer into the Old Space PropertyCell with no write barrier.
5. Force Minor GC to free the young HeapNumber while still referenced, yielding a dangling pointer / UAF for primitive construction.
**How found:** Source review of MaglevGraphBuilder::BuildCheckSmi; noticed the constant shortcut only checks Smi::IsValid (range) not type. Iterated on PoCs reading maglev graph traces and deopt reasons: naive store kept CheckSmi, adding kConst let store removed it but introduced CheckedSmiUntag deopt, then steering to CheckedNumberToInt32 via a kInt32 ContextCell cleared the remaining check.
**Exploitation:** Chain of 5 bugs: this Maglev UAF builds caged primitives in the renderer; a JSPI bug (faking the TerminateException object) escapes the V8 sandbox for code execution; an ExternalOneByteString legacy-feature info leak aids the escape; GPU-process info leak plus read/write primitive complete the chain. Total US$117,000 VRP payout.
**Escalation:** Renderer UAF → V8 sandbox escape via JSPI → renderer code execution; separate GPU-process leak + R/W primitive for GPU compromise (Windows and Linux).
**Insight:** In Maglev, a value's representation is a static compiler fact, not a runtime bit — so any helper that proves a constant by range (Smi::IsValid) rather than type is a representation-confusion candidate. Also: alternatives cached on a node (checked_value, int32) persist as facts for later uses, so you can plant a constant alternative via a kConst ContextCell store and a kInt32 alternative via a kInt32 ContextCell store to steer which conversion the compiler emits.

## Proof of concept (copied exactly from the original report)

**Naive PoC storing HeapNumber into PropertyCell**

```
// poc0.js
const f64src = new Float64Array(1);
f64src[0] = 11.0; // Get a HeapNumber

var cT = 0; cT = 1; // Get cT to be a Smi

function ct_only(x) {
 cT = x;
}

%PrepareFunctionForOptimization(ct_only);
ct_only(11);
ct_only(11);
%OptimizeMaglevOnNextCall(ct_only);
ct_only(f64src[0]);
```

**PoC with let cell planting constant alternative**

```
// poc1.js
const f64src = new Float64Array(1);
f64src[0] = 11.0;

let bT = 11;
var cT = 0;
cT = 1;

function bt_ct(x) {
 bT = x;
 cT = x;
}

%PrepareFunctionForOptimization(bt_ct);
bt_ct(11);
bt_ct(11);
%OptimizeMaglevOnNextCall(bt_ct);
bt_ct(f64src[0]);
```

