CVE-2026-20664
Overview
Background
- Resizable ArrayBuffer over wasm memory
- WebAssembly.Memory.toResizableBuffer() exposes growable wasm memory as an ArrayBuffer whose backing can move when the memory grows.
- Typed-array m_vector cache
- A JSArrayBufferView caches its buffer’s data pointer (plus byteOffset) in m_vector for fast indexed access.
- Incoming references
- An ArrayBuffer tracks the views referencing it; the fix uses this list to update each view after a grow.
- Memory grow / reallocation
- Growing wasm memory can move the backing store to a new base address, invalidating any cached pointer into the old store.
Root Cause Analysis
A WebAssembly.Memory can be exposed to JS as a resizable ArrayBuffer (toResizableBuffer), and typed-array views (JSArrayBufferView) over an ArrayBuffer cache the buffer’s backing data pointer in their m_vector field for fast element access. When the underlying wasm memory grows, its base data pointer can move (the memory is reallocated/remapped), and ArrayBufferContents is updated to the new base.
Pre-patch, growing the wasm memory did not refresh the cached m_vector pointers of the views attached to that buffer, so every typed array over the resizable buffer kept pointing at the OLD backing store – a stale/dangling data pointer. Reading or writing such a view after the grow accesses freed or moved memory: a use-after-free / out-of-bounds access.
The fix adds JSArrayBufferView::refreshVector(newData), which (when the view still hasVector()) recomputes m_vector as newData + byteOffsetRaw() and stores it without a write barrier; ArrayBuffer, after a wasm-memory grow, walks its incoming references (numberOfIncomingReferences/incomingReferenceAt), downcasts each to JSArrayBufferView, and calls refreshVector(newData) so every view is repointed at the new base, while ArrayBufferContents::refreshAfterWasmMemoryGrow sets m_data = memory->basePointer().
The restored invariant is that after a wasm memory grows, all typed-array views over its resizable buffer observe the buffer’s new base pointer rather than a stale one.
Attack Path
- Get a resizable buffer over wasm memory Create a WebAssembly.Memory with room to grow and obtain memory.toResizableBuffer(); allocate a typed array (e.g. Uint32Array) over it, caching the current data pointer.
- Grow the memory Call memory.grow(n), which moves the backing store to a new base pointer and frees/unmaps the old one.
- Leave views stale Pre-patch, the typed array’s cached m_vector still points at the old (freed/moved) backing store.
- Access via the stale view Read/write ta[i]; the access dereferences the stale data pointer – a use-after-free / out-of-bounds in WebContent, groomable by allocating over the freed region.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ArrayBuffer::setAssociatedWasmMemory / grow-refresh pathSource/JavaScriptCore/runtime/ArrayBuffer.cpp |
modified | After a wasm memory grow changes the data pointer, iterates incoming references and calls JSArrayBufferView::refreshVector(newData) on each view so cached vectors are repointed. |
JSArrayBufferView::refreshVectorSource/JavaScriptCore/runtime/JSArrayBufferViewInlines.h |
added | Recomputes m_vector as newData + byteOffsetRaw() (guarded by hasVector()) and stores it, so a view tracks the buffer's new backing base. |
ArrayBufferContents::refreshAfterWasmMemoryGrowSource/JavaScriptCore/runtime/ArrayBuffer.cpp |
modified | Sets m_data = memory->basePointer() so the contents reflect the post-grow base. |
JSArrayBufferView::refreshVector (decl)Source/JavaScriptCore/runtime/JSArrayBufferView.h |
modified | Declares the new refresh entry point. |
Files Changed
JSTests/wasm/stress/resizable-buffer-grow-view-refresh.jsSource/JavaScriptCore/runtime/ArrayBuffer.cppSource/JavaScriptCore/runtime/JSArrayBufferView.hSource/JavaScriptCore/runtime/JSArrayBufferViewInlines.h
Audit Directions
- Other buffer-base changesgrep JSC runtime for places that change ArrayBufferContents::m_data / basePointer (detach, transfer, resize, shared-memory remap) and confirm all incoming views are refreshed.
- Cached m_vector consumersAudit JSArrayBufferView fast paths that read m_vector without revalidating against the buffer after operations that can move backing store.
- Resizable/growable buffer APIsReview ArrayBuffer.prototype.resize, SharedArrayBuffer.grow, and toResizableBuffer paths for the same view-refresh requirement.