CVE-2026-43663
Overview
Background
- DFG constant folding
- A JavaScriptCore optimizing-JIT phase that replaces operations whose inputs are provably constant with precomputed results, sometimes embedding object-derived pointers (like a typed array’s backing store) directly into machine code.
- Resizable ArrayBuffer / growable SharedArrayBuffer
- ArrayBuffer variants whose byte length can change at runtime via resize()/grow(), which may cause the engine to reallocate the underlying backing store to a new memory address.
- Backing store (vector) pointer
- The raw pointer to a typed array’s element storage; for non-resizable buffers it is stable, but for resizable/growable-shared buffers it is invalidated when the store is reallocated.
- isResizableOrGrowableShared()
- The JSArrayBufferView predicate the patch uses to detect views whose buffer may be reallocated, gating the folding decision.
- WebAssembly.Memory.grow()
- A JS API that increases a Wasm memory’s size and, when it cannot extend in place, reallocates the backing store—one of the operations that invalidates a folded pointer.
Root Cause Analysis
The DFG constant-folding phase (DFGConstantFoldingPhase.cpp) walks the IR and tries to strength-reduce/eliminate operations on JSArrayBufferViews whose identity is known at compile time. In the branch shown, once the phase has proven it is dealing with a concrete typed-array view it calls m_interpreter.execute(indexInBlock) and sets eliminated = true, folding the operation and, as part of that, baking view-derived state (notably the backing store / vector pointer of the typed array) into the compiled code as a constant. The invariant this relies on is that a typed array’s backing buffer, once observed, does not move for the lifetime of the folded code. That invariant holds for ordinary ArrayBuffers, but it is FALSE for views over resizable ArrayBuffers and growable SharedArrayBuffers: resize() and WebAssembly.Memory.grow() may reallocate the backing store, leaving any previously-folded vector pointer dangling.
The patch adds an early break: if (view->isResizableOrGrowableShared()) { break; } so the phase refuses to fold operations on such views, restoring the invariant by simply not embedding a pointer that can go stale. The added JSTests case demonstrates the path: it grooms the WebAssembly memory pool, creates a WebAssembly.Memory with a resizable buffer, builds a Float64Array over it, warms up trigger() writing view[0] until the DFG compiles it (folding the store against the current backing store), then calls memory.grow(1) to reallocate and finally trigger(1.1) writes through the now-stale folded pointer. The exact node type being folded (a GetByVal/PutByVal or a GetTypedArrayLength/StorePointer style operation) is not literally printed in the diff, so the precise folded value is an inference; what the diff establishes is that folding operations on resizable/growable-shared views was unsound and is now suppressed.
Attack Path
- Allocate a resizable backing store From JS, create a WebAssembly.Memory with a small initial and larger maximum and obtain a resizable ArrayBuffer via memory.toResizableBuffer() (or a resizable ArrayBuffer directly), then build a typed array view (Float64Array) over it.
- Groom memory so growth relocates Pre-allocate many large WebAssembly.Memory objects (as the test does) so that when the target memory is grown its backing store cannot be extended in place and must be reallocated to a new address.
- Warm up the accessor to trigger DFG compilation Call a function that indexes the view (e.g. trigger(val){ view[0]=val; }) in a tight loop (~10000 iterations) so the DFG tiers it up and the constant-folding phase bakes the current backing-store pointer into the compiled code.
- Reallocate the backing store Call memory.grow(1) (or buffer.resize()), which reallocates the backing store to a new address and frees/repurposes the old one, invalidating the folded pointer.
- Re-enter the compiled code Call the compiled accessor again (trigger(1.1)); the JIT code dereferences the stale folded vector pointer, reading/writing memory that is no longer the live buffer, producing an out-of-bounds or use-after-free access and typically a crash.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ConstantFoldingPhase per-node folding loop (foldConstants)Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp |
modified | Adds an early break when the involved typed-array view isResizableOrGrowableShared(), before m_interpreter.execute()/eliminated=true, so the phase no longer folds/embeds backing-store state for views whose buffer can be reallocated. Exact enclosing method name not shown in the diff (hunk context is 'private:'). |
Files Changed
JSTests/stress/resizable-array-constant-folding.jsSource/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
Audit Directions
- Same phase, other foldable typed-array operationsIn DFGConstantFoldingPhase.cpp, review every branch that calls m_interpreter.execute()/sets eliminated for typed-array or ArrayBuffer nodes and confirm each is now guarded by isResizableOrGrowableShared(); grep for ‘isResizableOrGrowableShared’, ‘vector()’, ‘butterfly’, and typed-array node cases.
- Other JIT phases that embed backing-store pointersAudit DFGAbstractInterpreter, DFG/FTL strength reduction and lowering (e.g. GetByVal/PutByVal/GetIndexedPropertyStorage handling) for places that capture a view’s vector/length as a constant; look for CheckArray/GetIndexedPropertyStorage without a resizable-buffer guard.
- Auto-length and length assumptionsSearch across JSC for code that caches typed-array length or storage across side-effecting calls and does not account for resizable buffers; grep for ‘isResizable’, ‘isGrowableShared’, ‘byteLength’, ’lengthTrackingAutoLength’ near cached-pointer or cached-length logic.
- Watchpoint/invalidation coverage for resizeVerify that resize()/grow() paths correctly invalidate any structure/watchpoint-based assumptions the JIT relies on; grep for the resize implementations (ArrayBuffer::resize, Wasm memory grow) and cross-reference with JIT watchpoint registration for array-buffer views.