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
Files Changed
Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
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.
Patch
diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
index 32f08d29e1c1..80b9a3b50f0a 100644
--- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
+++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
@@ -3578,6 +3578,14 @@ void SpeculativeJIT::compilePutByValForIntTypedArray(Node* node, TypedArrayType
}
}
+ GPRReg scratch2GPR = InvalidGPRReg;
+#if USE(JSVALUE64)
+ if (node->arrayMode().mayBeResizableOrGrowableSharedTypedArray()) {
+ scratch2.emplace(this);
+ scratch2GPR = scratch2->gpr();
+ }
+#endif
+
bool result = getIntTypedArrayStoreOperand(
value, propertyReg,
#if USE(JSVALUE32_64)
@@ -3589,14 +3597,6 @@ void SpeculativeJIT::compilePutByValForIntTypedArray(Node* node, TypedArrayType
return;
}
- GPRReg scratch2GPR = InvalidGPRReg;
-#if USE(JSVALUE64)
- if (node->arrayMode().mayBeResizableOrGrowableSharedTypedArray()) {
- scratch2.emplace(this);
- scratch2GPR = scratch2->gpr();
- }
-#endif
-
GPRReg valueGPR = value.gpr();
GPRReg scratchGPR = scratch.gpr();
#if USE(JSVALUE32_64)