CVE-2024-44308
Overview
Background
- DFG SpeculativeJIT
- JavaScriptCore’s speculative optimizing JIT that emits machine code for typed-array stores.
- Scratch register (scratch2)
- A temporary GPR the emitted code needs; it must be reserved before other operands claim registers so it cannot alias.
- Resizable/growable typed array
- Typed arrays over resizable ArrayBuffers / growable SharedArrayBuffers needing extra length handling.
Root Cause Analysis
This fixes a DFG JIT register-allocation ordering bug in JavaScriptCore’s typed-array store fast path. In SpeculativeJIT::compilePutByValForIntTypedArray, on JSVALUE64 a scratch2 GPR is reserved only when the array mode may be a resizable-or-growable-shared typed array (mayBeResizableOrGrowableSharedTypedArray()), where it is needed for the extra length/bounds handling those arrays require.
Before the fix, scratch2 was allocated AFTER getIntTypedArrayStoreOperand(…) had already assigned value/property/scratch registers; the fix moves the scratch2 reservation to BEFORE that call. The violated invariant is that all scratch registers a code path needs must be reserved before other operands claim registers, so they cannot alias. Allocating scratch2 late means it could collide with a register already in use for the store, so for resizable/growable typed arrays the generated code used a clobbered register in the bounds check or store-address computation, producing an out-of-bounds write in the JIT-compiled store. That is a powerful memory-corruption primitive, consistent with the critical ‘arbitrary code execution’ impact and Apple’s note of active exploitation on Intel Macs. Reordering the allocation guarantees scratch2 is a distinct register. INFERENCE: the exact clobbered register/instruction is not printed by the diff; the commit establishes the allocation-order change on the resizable-typed-array path.
Attack Path
- Create a resizable/growable typed array JS allocates a typed array backed by a resizable ArrayBuffer or growable SharedArrayBuffer so arrayMode().mayBeResizableOrGrowableSharedTypedArray() holds.
- Trigger DFG compilation of a store Hot code repeatedly writes into the typed array so the DFG compiles compilePutByValForIntTypedArray for it.
- Miscompiled store uses a clobbered register With scratch2 reserved too late, the emitted bounds/address computation aliases a live register on the resizable path.
- Out-of-bounds write The store writes outside the typed array’s backing store, corrupting adjacent heap memory.
- Escalate to code execution With the heap groomed, the OOB write corrupts an adjacent object to build an exploitation primitive and achieve arbitrary code execution (standard escalation; actively exploited on Intel Macs per the advisory).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
SpeculativeJIT::compilePutByValForIntTypedArraySource/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp |
modified | Moves the JSVALUE64 scratch2 GPR reservation for resizable/growable-shared typed arrays to before getIntTypedArrayStoreOperand(), preventing the scratch register from aliasing operands and mis-computing the store on that path. |
Audit Directions
- Scratch allocation ordering in the JITsAudit DFG/FTL emitters where scratch/tmp registers are reserved after operand materialization; look for early-return paths between reservation and use.
- Resizable/growable typed-array code pathsReview mayBeResizableOrGrowableSharedTypedArray branches for correct bounds/length register usage.