[2] Use-after-free after growing a resizable buffer on a WebAssembly memory
Severity: High | Component: JSC runtime — ArrayBuffer/JSArrayBufferView | 6b357f3
Rated High because the observable effect is a typed-array view dereferencing freed Wasm-memory pages after a grow, the regression test demonstrates reclamation by an unrelated allocation, and escalation to a controlled R/W primitive is projected with confidence 0.9 from the cached-pointer model — bounded only by attacker control over BoundsChecking heap layout.
Source/JavaScriptCore/runtime/ArrayBuffer.cpp
void ArrayBuffer::refreshAfterWasmMemoryGrow(Wasm::Memory* memory)
{
ASSERT(isWasmMemory());
+
+ void* oldData = m_contents.data();
m_contents.refreshAfterWasmMemoryGrow(memory);
+ void* newData = m_contents.data();
+ if (newData == oldData)
+ return;
+
+ for (size_t i = numberOfIncomingReferences(); i--;) {
+ JSCell* cell = incomingReferenceAt(i);
+ auto* view = dynamicDowncast<JSArrayBufferView>(cell);
+ if (view)
+ view->refreshVector(newData);
+ }
}
void ArrayBufferContents::refreshAfterWasmMemoryGrow(Wasm::Memory* memory)
{
ASSERT(isResizableNonShared());
m_memoryHandle = memory->handle();
+ m_data = memory->basePointer();
m_sizeInBytes = m_memoryHandle->size();
Source/JavaScriptCore/runtime/JSArrayBufferViewInlines.h
+inline void JSArrayBufferView::refreshVector(void* newData)
+{
+ if (hasVector()) {
+ void* newVectorPtr = static_cast<uint8_t*>(newData) + byteOffsetRaw();
+ m_vector.setWithoutBarrier(newVectorPtr);
+ }
+}
JSTests/wasm/stress/resizable-buffer-grow-view-refresh.js
+const memory = new WebAssembly.Memory({ initial: 1, maximum: 10 });
+const buffer = memory.toResizableBuffer();
+const ta = new Uint32Array(buffer);
+ta[0] = 0xCAFEBABE;
+memory.grow(1);
+for (let i = 0; i < 100; i++) {
+ const arr = new ArrayBuffer(65536);
+ new Uint32Array(arr).fill(0xDEADBEEF);
+}
+assert.eq(ta[0], 0xCAFEBABE);
Patch Details
Two coordinated changes inside the refreshAfterWasmMemoryGrow family. ArrayBufferContents::refreshAfterWasmMemoryGrow now also assigns m_data = memory->basePointer() — previously it only refreshed m_memoryHandle and m_sizeInBytes, leaving m_data stranded at the old base. ArrayBuffer::refreshAfterWasmMemoryGrow now snapshots the old data() pointer, runs the contents refresh, and — if the base actually moved — walks numberOfIncomingReferences() / incomingReferenceAt(i), downcasts each cell to JSArrayBufferView, and invokes a new refreshVector(newData) method. refreshVector recomputes the view's cached m_vector as newData + byteOffsetRaw() (gated on hasVector() because incoming-reference edges persist across view detachment).
Stale cached pointer to a relocatable backing store: a derived data pointer is computed once at view construction and not refreshed when the underlying buffer is reallocated.
Background
WebAssembly.Memory({initial, maximum}) reserves a Wasm linear memory; memory.toResizableBuffer() exposes that memory as a JS ArrayBuffer whose length grows when memory.grow() is called. In BoundsChecking memory mode — a Wasm implementation strategy where bounds are enforced in software rather than via virtual-memory traps — growing can require allocating a fresh backing region and copying contents, so the base pointer changes. A JSArrayBufferView (the C++ object behind Uint32Array, DataView, etc.) caches a direct pointer (m_vector) into the buffer's storage so element access does not need to reload buffer->data() each time; this pointer equals buffer base + byteOffset at the moment the view is constructed. WebKit's GC tracks an "incoming references" set per cell (numberOfIncomingReferences/incomingReferenceAt), which is what the new walk uses to find dependent views.
Analysis
Before this commit, a BoundsChecking grow that relocated the backing store left two pointers dangling: ArrayBufferContents::m_data (never assigned the new base inside refreshAfterWasmMemoryGrow) and every JSArrayBufferView::m_vector (never re-derived). The introduction of refreshAfterWasmMemoryGrow showed the developers recognised the relocation event existed — they just missed two of the cached pointers it needed to fan out to.
Aaa Aaaaaaaaaa Aaaa Aa Aaa Aaaaaaa Aaaa Aaaaaaaaaaaa Aaa Aaaaaa Aaaa Aaaa Aaaa Aaaaaaaaaaaaaaaaaaaa Aaaaaaaa Aaaaaaaa a Aaaaaaaaa Aaaaaa Aaaa a Aaaaaaaaaaa Aaaaa Aaaa Aaaaaaaaaaaaaaaa Aa Aaaaa Aaaaaaaaaaa Aaaa Aaaaa Aa Aaa Aaaaaaaaaaaaa Aaaaaaaaaaa Aa Aaaaaaa Aaa Aaaaa Aaaaaa Aaaaaaa Aaaaaaaaaaaaaaaaa Aaaaaaaaaaaa Aaaaa Aaaaaaa Aaaa Aaa Aaaaa Aaaaaaa Aaaaaaaaaa Aaaa a Aaaaaa Aaaaaaaaaaa Aa Aaaaaaa Aaaaaaaaaaaa Aaaaa
a Aaaaaaaaaa Aaaaaaaa Aaaaa Aaaaaaa Aaaaaaaaaaaa Aaaa Aa Aaaaa Aaaaa Aa Aaaaaaa Aaaa Aaaaaaaaaaaaaaaaaaa Aaaaaa Aaaa Aaaa Aa Aaaaa Aaaaaaa Aaa Aaaaaaaa Aaaaaaaaaaa Aaaaa Aaa Aaaaa Aaaaaa Aa Aaaaaaaaaaaaaa Aaaaa Aaaaa Aaaaaaaaaaa Aa Aaaaaaa Aaaaaaaaaaaaa Aa Aaa Aaaa Aaaa Aaaaaaaaaa a Aaaaaaaaaa Aaaaa Aaaaa Aaa Aaaaaaaa a Aaaaaaaaaaa Aaaaaa Aaaa Aaa Aaaaaaaa Aa Aaaaaaa Aaaaaaaaaa a a Aaaaaaaa Aa Aaaaaaaa Aaa Aaaaaaaaa Aaaaaa Aaa Aaaaaaaaaa Aa Aaaa Aaaaaa Aaa Aaaaaaaa Aaaaaaaaa Aaa Aaaaaaaaaaa Aaaaaa Aaaa Aa Aa Aaaaaaaaaaa Aaaa Aaaaaaa Aaaaaaaaaaa Aaaaaaa Aa Aaaaaaaaaaa Aaaaaaa Aa Aaa Aaaaaaaaaaaaaa Aaaa Aaaaaaaaaa
Aaaa Aaaaaaaaaaaaa Aaaaaaa Aaaaaa Aaaaaa Aaaaaa Aaa Aaaaaaaaaa Aaaaaaaa Aaa Aaaaaaaaa Aaaa a Aaaaaaaaaaa Aaaaaa Aaaaaa Aaaaaaaaaa Aaaaaaaaaaaa Aaaaa Aaaaaaaaaaaaaaaa Aaaaaa Aaa Aaaaaa Aaaaaaaa Aaa Aaaaaaaaaa Aaaa Aaaaaa Aaaa Aaa Aaaaaaaaaa a Aaaaaaaa Aaaaaaa Aaaaaa Aaaaa Aaaaa Aa Aaaaaaaa Aa Aaaaa Aaa Aaaaaaaaa
Aaaaaaaaa Aaa Aaaaa Aaaaaaaaaaa Aaaaaaaaa Aa Aaa Aaaaa Aaaaaaaaaaaaaa Aaaaa a Aaaaaaa Aaaa Aaa Aaaaaaaa Aaaaaa Aa Aa Aaaaaaaaaaaaa Aa Aaaaa Aaaaa Aaa Aaaaaaa Aaaaaaa Aaaaaaaaaaa Aaaa Aa Aaaaaaaaaaaa Aaaaa Aaaa Aaaaa Aaa Aaaaaaaaaaaa Aaaaaa Aa Aaa Aaaa Aaaaaaa a Aa Aaaaaaaa Aaaa Aaa Aaaaaa Aaaaaaaaa Aaaaaa Aaaa Aaaaaaaa Aaaaaaa Aa Aaa Aaaaa Aaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aa Aaaaaaaaaaaa Aaaaaaaaa Aa Aaa Aaaaaa
🔒The lifetime and relocation implications of cached buffer pointers across WebAssembly memory growth are analyzed in depth, with a step-by-step walkthrough of the regression test's exploitation pattern.
Subscribe to read more
Audit directions
a Aaaaaaaa Aaaaaaa Aaaaaaaa Aaaa a Aaaaaaa Aaaaa Aaaa Aaa Aa Aaaaaaaaaaa Aa Aa Aaaaaaaaa Aaaaaaaa Aaaaa Aaaa Aa Aaa Aaaa Aaaaaaaaa Aaaaaaaaaaaaaaaaaaaaa Aa Aaaaaaaa Aaaaa a Aaaaaaaaaaa Aaaa Aaa Aaaaaa Aaa Aaaaaa Aa Aaaaaaa Aaaaaa Aa Aaaaaaaa Aaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaa Aaa Aaa Aaaaaaa Aaaaaaaaaaaa Aaaa Aaaa Aaaaa a Aaaaaaaaaaa Aaaa Aaaaaaa Aaaa Aaaaaaa Aaaaaaa Aaaaa Aaaaaa Aaaa Aa Aaaaaaaaaaa Aa Aaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
a Aaaaaaaaaaaa Aaaaaaa Aaaa a Aaaaaa Aaaaaaa Aaaaaaaaaa Aaaaaaaaaaaaaaa Aaaa Aaaaaaa Aa Aaaaa Aaa Aaa Aaaaaa Aaaaaa Aaaaaaa Aaaaaa Aaaaaa Aaa Aaaa Aa Aaaa a Aaaaaaaa Aaa Aaaaaa Aaa Aaaa a Aaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaa Aaaaaaaaaaaaaaa Aaaa Aaaaaa Aa Aaaaaaaaaaaaaaaaaaaaa Aaaaaaa Aaaa Aaa Aaaaaaaaaaa Aaaa Aaa Aaaa Aaaaa Aaaaaaa Aaaaa Aaaaaa
a Aaaaaaaaaaaaaaaaaaaa Aaaaaaa Aaaaaaaa Aa Aaaaaaaaaaaaaa Aaaa Aaaaaaaaa Aaaa Aaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaa Aa Aaaaaaaaa Aaaaa Aaaa Aaaa Aaaaaaa a Aaaaaa Aaaaaa Aaaaa Aaaaa Aaaa Aaa Aaaa Aaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaa Aaa Aaa Aaaaaaaa Aaa Aaa Aaaa Aaaaaaa Aaa Aaaaaaaa Aaaa a Aaaaaaaaaaaaaaaaaaaa Aaaaaa a Aaaa Aaaaaaaaa
a Aaaaaaaaaaaaaaaaaaaa Aaaaa Aa a Aaaaaaa Aaaaaaaaaaaa Aaaa Aaaaaaa Aaaaa Aaaa Aa Aaaaa Aaaaaaaa Aaaaaaaaa Aa Aaaaaaaaaaaaaaaaaa Aaaa Aa Aaa Aaaaaaa Aaaaa Aaaaaaa Aaaaaaaa Aaaaaaaaa a Aaaaaaaaaaaa Aaaaaa Aaa Aaaaa Aaaa Aaa Aaaaaaa a Aaaaa Aaaaaaaaaaa Aaaa Aaaaaaaa Aaaaaaa Aaaa Aaaaa Aaaa Aaaaa Aaa Aa Aaaaaaa Aa Aaa Aaa Aaaaaaa Aaaa Aaa Aaaaa Aaaaa Aaa Aaaaa Aaaaa
🔒Multiple reusable audit patterns identified for cached-pointer/relocation bugs across JSC, with concrete starting points spanning the typed-array, Wasm memory, and JIT inline-cache surfaces.
Subscribe to read more