Description
A very fast program. Find the vulnerability and exploit it to read the flag.
Setup
Download the V8 build and the challenge patch/diff.
wget https://mercury.picoctf.net/static/.../d8wget https://mercury.picoctf.net/static/.../turboflan.diffchmod +x d8Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Read the V8 patch and identify the missing deopt guardObservationI noticed the challenge ships a turboflan.diff alongside the d8 binary, which suggested the vulnerability was introduced by a code change; reading the patch and locating the removed DeoptimizeIfNot calls in LowerCheckMaps() would reveal exactly what protection was stripped.Open the diff and find the change inside src/compiler/effect-control-linearizer.cc, specifically the LowerCheckMaps() function. The patch removes calls to DeoptimizeIfNot(DeoptimizeReason::kWrongMap, ...). Normally, when TurboFan's JIT encounters an object whose map (hidden class) differs from what it observed during profiling, it bails out and re-interprets the code in the interpreter (deoptimizes). With that guard removed, the JIT keeps running its specialized, optimized path even when the actual object type has changed, leading to type confusion.bashless turboflan.diffbash# Find the LowerCheckMaps function in effect-control-linearizer.ccbash# The removed lines look like:bash# DeoptimizeIfNot(DeoptimizeReason::kWrongMap, ...);What didn't work first
Tried: Search the diff for 'CheckMaps' instead of 'DeoptimizeIfNot' to find the removed guard.
The diff does reference CheckMaps nodes, but those are the high-level IR nodes that still exist in the graph. The actual runtime bailout is expressed as DeoptimizeIfNot calls inside LowerCheckMaps(). Searching for the node name finds the correct function but not the critical removed lines - you need to look for the DeoptimizeIfNot removal specifically to understand what protection was stripped.
Tried: Look for the bug in the TurboFan typer (typer.cc or type-narrowing passes) rather than the effect-control linearizer.
Typer bugs (integer range confusion, incorrect type widening) produce a different class of V8 exploit where the compiler miscomputes a value's possible range and eliminates bounds checks. This diff removes a map-check guard in the linearizer, which is a hidden-class type confusion - the typer still sees the correct types, but the runtime guard that enforces them is gone. Auditing typer.cc will find nothing relevant.
Learn more
V8 Turbofan, not C heap."Turboflan" is a pun on Turbofan, V8's optimizing JIT compiler for JavaScript. The vulnerability lives in JS-level reasoning, not in glibc heap structures.
Why LowerCheckMaps matters: During JIT compilation, Turbofan inserts CheckMaps nodes to verify that an object's map (hidden class) matches the type the compiler assumed during optimization. The Effect Control Linearizer lowers these high-level nodes into concrete machine-level guards. When the DeoptimizeIfNot calls are removed from LowerCheckMaps(), those guards disappear and the compiled code blindly assumes the original map is still valid, even after the caller switches to a different object type.
The type confusion this enables: By calling an optimized function first with a float64 array (so the JIT specializes for floats) and then with a pointer-compressed object array (which has different element size and layout), the missing map check lets the JIT treat object pointers as raw 64-bit floats. Reading an element from the object array via the float-specialized path returns raw pointer bits as a double, leaking heap addresses. Writing via the same confused path corrupts an adjacent field, giving an arbitrary-read/write primitive.
See real-world bug patterns for adjacent JS sandbox-escape ideas.
Step 2
Trigger JIT compilation and force the bugObservationI noticed the missing guard only exists in the Turbofan-compiled path, not in the interpreter or baseline tier, which meant the exploit could only fire after the target function had been optimized; a warmup loop followed by %OptimizeFunctionOnNextCall was the standard way to force that tier-up before passing the type-confusing input.Standard recipe: write a small attack function, force optimization via repeated calls (or %OptimizeFunctionOnNextCall in d8 with --allow-natives-syntax), then call once with an input the typer mispredicts.bash# Run d8 with native syntax to force optimization: ./d8 --allow-natives-syntax exploit.jsbash# Sketch of a typer-confusion exploit (specifics depend on the diff): # function leak(x) { return arr[x]; } # for (let i = 0; i < 0x10000; i++) leak(0); # %OptimizeFunctionOnNextCall(leak); # leak(badValue); // typer thinks in-range, runtime is out-of-range -> OOB readExpected output
picoCTF{...}What didn't work first
Tried: Call the attack function with the bad value immediately without warming it up first, skipping the loop of 0x10000 calls.
V8 only JIT-compiles a function after it has been called enough times to gather profiling feedback (the tier-up threshold). Without the warmup loop the function runs in the interpreter or Maglev baseline tier, both of which still have valid map checks in place. The type confusion only manifests in the Turbofan-compiled code where the DeoptimizeIfNot guard has been removed, so skipping the warmup means the bug is never triggered.
Tried: Use %OptimizeFunctionOnNextCall without passing --allow-natives-syntax to d8.
Percent-prefixed intrinsics like %OptimizeFunctionOnNextCall are gated behind the --allow-natives-syntax flag at the engine level. Without it, V8 treats the percent identifier as a syntax error and aborts before running any code. The exploit script will not execute at all, not just fail silently, so the flag must be passed on the d8 command line.
Learn more
From OOB read to flag. A typical chain: corrupt a JSArray's elements pointer (or its length) to fabricate a fake array that addresses arbitrary memory; build addrOf/read/write primitives; locate
%FunctionPrototype%code object; flip a writable but executable page; drop shellcode; call.readFile("flag.txt")in d8 is often available too, sidestepping shellcode entirely.
Flag
Reveal flag
picoCTF{Good_job!_Now_go_find_a_real_v8_cve!_...}
Turboflan is a V8 Turbofan JIT type-confusion challenge triggered by a missing deoptimization guard in LowerCheckMaps(), not a typer-range bug or a glibc heap bug. Exploit details depend on the exact diff shipped, so treat this page as the framing rather than a turn-key exploit.