Local reproduction and weaponization of the V8 JS-to-Wasm deoptimization type
confusion described by Nebula, taken to a xcalc popped from the renderer of Chrome for Testing 146.0.7680.165
(the bug is fixed in 147.0.7727.101).
All payloads are benign (a calculator / exit(0x42) / jmp $).
Everything runs against a deliberately outdated browser in a local lab
do not point it at anyone else.
Demo
https://github.com/user-attachments/assets/d450f064-ba52-4f39-a88b-367f31217143
The bug
JSToWasmFrameStateFunctionInfo's equality operator omits the Wasm signature
comparison, so CSE/GVN merges frame states that differ only in signature. A lazy
deopt then materializes a Wasm i64 as a tagged externref (and vice-versa),
giving a full 64-bit addrof/fakeobj whose pointers are not cage-compressed
(Wasm refs use the tagged register representation, returned via kReturnRegister0).
Everything below was verified live against 146.0.7680.165. Per-load numbers are
measured hit-rates over repeated runs (the confusion is GC-sensitive; each page
relaunches on a dirty build). No run ever produced a false positive.
Flag-free primitives — no --allow-natives-syntax
Most V8 bug PoCs force optimization with %OptimizeFunctionOnNextCall, a test-only
intrinsic gated behind --allow-natives-syntax that no real victim enables. These
drive the bug with no flags at all, by shaping the warmup feedback so the deopt
confusion fires naturally. Every address is leaked at runtime — nothing is hardcoded.
Run them headless with no flags.
| File | Proves | Verified |
|---|---|---|
flag-free/00-reachability-flagfree.html |
triggers the confused state; fires on vulnerable 146, not on patched 147 | 6/15 per load |
flag-free/01-primitives-flagfree.html |
addrof / fakeobj (fakeobj(addrof(o))===o) |
8/12 |
flag-free/02-store-flagfree.html |
out-of-cage store lands a chosen value in a chosen object | 12/12 |
flag-free/03-arw-flagfree.html |
in-cage read64 / write64 (fake PACKED_DOUBLE array) |
2/12 (flaky, retry) |
Visible RCE — no-ASLR lab build
rce-no-aslr/ takes it to code execution: an optimized JS function g stages
execve shellcode as JIT double-literals (movabs constants, each ending in
eb rel8 to chain the next), the confused store patches a nop; jmp into g's code,
and calling g() slides RIP into the shellcode.
| File | Proves | Verified |
|---|---|---|
rce-no-aslr/exploit-calc.html |
pops a real xcalc window via execve from the renderer |
pops in ~1s on a clean slate |
rce-no-aslr/exploit-exit.html |
renderer runs the injected shellcode to exit(66); #jmp spins RIP inside it |
exit(66) 8/8 (strace) |
This build uses --allow-natives-syntax (%Optimize pins g's JIT payload so the
deopt-GC can't flush it) and --no-memory-protection-keys (so the confused store can
patch the JIT page — V8's PKU marks it read-only otherwise). ASLR is off
(randomize_va_space=0) so the addresses are deterministic
(A_MAP = 0x5555bbc0128a). The renderer also needs --disable-seccomp-filter-sandbox --no-zygote, because seccomp otherwise blocks the shellcode's execve.
Run it
Native x86_64 Linux, ASLR off, Chrome 146 on PATH, an X display on :0 (Xvfb
or real X). rce-no-aslr/run.sh sets all the flags and serves over http.
cd rce-no-aslr
./run.sh calc # pop a real xcalc over http://127.0.0.1:8080, on :0
./run.sh exit # strace-confirms the renderer's exit(66)
./run.sh jmp # gdb-confirms RIP inside the injected shellcode
./demo.sh calc # clean-slate + auto-retry, full-size headful browser (for recording)
./demo.sh msgbox # same, but the pop is a branded "0xSha :: pwned" box
pocmode.sh calc|msgbox switches what the RCE's execve("/usr/bin/xcalc") launches (a
real calculator vs the branded box). The flag-free primitives just need a browser:
chrome --headless=new --no-sandbox http://127.0.0.1:8080/flag-free/01-primitives-flagfree.html
Layout
flag-free/ the no-flags contribution (NO --allow-natives-syntax)
00-reachability trigger + patch differential (vuln 146 vs patched 147)
01-primitives addrof / fakeobj
02-store out-of-cage store
03-arw in-cage read64 / write64
rce-no-aslr/ the visible RCE (deterministic, no-ASLR lab build)
exploit-calc.html / exploit-exit.html
run.sh, demo.sh, pocmode.sh, xcalc-msgbox.sh
Bug and root-cause writeup: Nebula Security (linked above). Flag-free trigger, primitives, and the visible works in this repo are reproductions/extensions verified in a local lab.
Comments