CVE-2024-54508
Overview
Background
- BBQ JIT
- JavaScriptCore’s baseline wasm optimizing tier with its own register allocator.
- Register bindings
- m_gprBindings/m_fprBindings map physical registers to the live values they currently hold.
- consume / unbind
- Releases a value’s register binding so the allocator no longer assumes that register holds it.
- Unreachable region
- Wasm code after an unconditional branch/throw that is still validated and compiled, with special control handling.
Root Cause Analysis
In JavaScriptCore’s Wasm BBQ JIT, register allocation tracks which GPRs/FPRs are bound to live stack values via m_gprBindings/m_fprBindings. When compiling exception-handling catch blocks, the register-binding state must be reset consistently on entry to the catch and when finishing an unreachable region, or the JIT’s model of which registers hold which values diverges from reality.
Pre-patch, addCatch and addCatchAll did not unbind the currently-bound registers on entry, while addEndToUnreachable open-coded a loop that consumed all bindings. That asymmetry meant compiling a catch (especially in an unreachable context, per the compile-unreachable-catch test) left stale register bindings, so subsequent codegen could treat a register as holding a value it no longer does — a JIT register-state inconsistency that miscompiles and crashes.
The fix factors the binding-clearing into unbindAllRegisters() (consuming every non-none GPR/FPR binding) and calls it at the start of addCatch and addCatchAll as well as reusing it in addEndToUnreachable, so all three paths reset register state identically.
The restored invariant is that entering a catch handler or an unreachable end clears the register bindings, keeping the BBQ JIT’s register model consistent.
Attack Path
- Craft wasm with unreachable catch Build a module whose function has an exception-handling catch/catch_all reached in an unreachable code region.
- Compile via BBQ JIT Instantiation compiles the function; addCatch/addCatchAll run without clearing prior register bindings.
- Diverge register state Stale GPR/FPR bindings persist into the catch codegen, so a register is assumed to hold a value it no longer holds.
- Miscompile / corrupt The resulting code uses the wrong register contents, crashing or corrupting memory in the WebContent process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
BBQJIT::addCatch / addCatchAllSource/JavaScriptCore/wasm/WasmBBQJIT.cpp |
modified | Call unbindAllRegisters() on entry so catch handlers start from a clean register-binding state. |
BBQJIT::unbindAllRegisters (+ addEndToUnreachable)Source/JavaScriptCore/wasm/WasmBBQJIT.cpp |
added | Factors the consume-all-bindings loop into one helper reused by the unreachable-end path, unifying register-state reset. |
Audit Directions
- Other control-entry binding resetsgrep WasmBBQJIT for control-flow entries (loop/block/try/delegate) that should unbindAllRegisters() but hand-roll or omit it.
- Unreachable-region codegenAudit addEndToUnreachable and unreachable catch/delegate paths for register/stack-state assumptions.
- consume() coverageReview addArray*/addStruct*/call paths for values whose registers are not consumed, leaving stale bindings.