CVE-2026-43716
Overview
Background
- Resizable buffer over wasm memory
- toResizableBuffer/useWasmMemoryToBufferAPIs exposes growable wasm memory as a resizable ArrayBuffer whose resize is routed to the memory.
- GC tracing / write barrier
- References between GC objects must be stored on GC-visible cells with a write barrier so the collector keeps the referent alive; a raw pointer on a non-GC object is invisible to GC.
- Native ArrayBuffer vs JSArrayBuffer
- ArrayBuffer is the native backing object; JSArrayBuffer is its GC-visible JS wrapper cell.
- HostResizeArrayBuffer
- The abstract operation resizing a buffer; for wasm-backed buffers it must delegate growth to the wasm memory.
Root Cause Analysis
When the useWasmMemoryToBufferAPIs feature exposes a WebAssembly.Memory as a resizable non-shared ArrayBuffer, the buffer and the wasm memory must stay associated so a JS-side resize is routed to the memory’s grow (which reallocates the backing store).
Pre-patch, the native ArrayBuffer held that association itself via m_associatedWasmMemory (set through ArrayBuffer::setAssociatedWasmMemory), and ArrayBuffer::resize, on a wasm-backed buffer, fetched RefPtrWasm::Memory from it and called memory->grow. Storing the buffer->memory link on the native ArrayBuffer (which is not a GC object and is not traced) meant the association was not GC-visible: after a garbage collection the link could go stale / the memory be reclaimed or left in an inconsistent state, and a later resize (the added test resizes a resizable buffer after GC) would operate on a dangling or wrong memory – a use-after-free / crash.
The fix moves the association onto the GC-visible JS wrapper: JSArrayBuffer gains m_associatedWasmMemoryWrapper with associatedWasmMemoryWrapper()/setAssociatedWasmMemoryWrapper(vm, wrapper) using a write barrier (.set(vm, this, wrapper)), so the JSWebAssemblyMemory is properly kept alive and traced by the collector; the native ArrayBuffer::setAssociatedWasmMemory is removed, and ArrayBuffer::resize now RELEASE_ASSERTs !isWasmMemory() (native resize must never touch a wasm memory – growth goes through the JS wrapper/memory path instead).
The restored invariant is that the buffer<->wasm-memory association is owned by a GC-traced wrapper so it cannot dangle across GC, and native ArrayBuffer resize is never applied to wasm memory.
Attack Path
- Expose wasm memory as a resizable buffer With useWasmMemoryToBufferAPIs, create a WebAssembly.Memory and obtain its resizable non-shared ArrayBuffer, associating buffer and memory.
- Drop strong references and GC Remove other references to the wasm memory / wrapper and force garbage collection, so the untraced native association can go stale.
- Resize after GC Call the resizable buffer’s resize/grow; pre-patch it dereferences the now-dangling associated Wasm::Memory.
- Use-after-free / crash The resize operates on freed or inconsistent memory state in the WebContent process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifSource/JavaScriptCore/runtime/ArrayBuffer.cpp |
modified | |
JSWebAssemblyMemorySource/JavaScriptCore/runtime/JSArrayBuffer.h |
modified | |
JSArrayBufferSource/JavaScriptCore/runtime/JSArrayBuffer.h |
modified | |
ifSource/JavaScriptCore/runtime/JSArrayBufferPrototype.cpp |
modified | |
LLIntOffsetsExtractorSource/JavaScriptCore/wasm/WasmMemory.h |
modified | |
MemorySource/JavaScriptCore/wasm/WasmMemory.h |
modified | |
forJSTests/wasm/stress/wasm-resizable-buffer-resize-after-gc.js |
modified |
Files Changed
JSTests/wasm/stress/wasm-resizable-buffer-resize-after-gc.jsSource/JavaScriptCore/runtime/ArrayBuffer.cppSource/JavaScriptCore/runtime/ArrayBuffer.hSource/JavaScriptCore/runtime/JSArrayBuffer.cppSource/JavaScriptCore/runtime/JSArrayBuffer.hSource/JavaScriptCore/runtime/JSArrayBufferPrototype.cppSource/JavaScriptCore/wasm/WasmMemory.hSource/JavaScriptCore/wasm/js/JSWebAssemblyMemory.cpp
Audit Directions
- Untraced cross-object pointersgrep JSC runtime for raw/RefPtr members on native (non-GC) objects that point at GC cells (wrappers, memories) and should instead live on the wrapper with a write barrier.
- Wasm memory <-> buffer pathsAudit resize/grow/detach/transfer for resizable and shared buffers backed by wasm memory to confirm associations survive GC and native fast paths are gated by isWasmMemory().
- resize-after-GCReview other resizable-buffer operations for correctness when the last non-GC reference is dropped and a collection intervenes.
Patch
diff --git a/Source/JavaScriptCore/runtime/ArrayBuffer.cpp b/Source/JavaScriptCore/runtime/ArrayBuffer.cpp
index 84a7fd6b4476..2a4dc89d8557 100644
--- a/Source/JavaScriptCore/runtime/ArrayBuffer.cpp
+++ b/Source/JavaScriptCore/runtime/ArrayBuffer.cpp
@@ -443,19 +443,6 @@ void ArrayBuffer::makeWasmMemory()
m_isWasmMemory = true;
}
-void ArrayBuffer::setAssociatedWasmMemory(Wasm::Memory* memory)
-{
- // The pointer from a buffer to a memory is only required when the buffer is resizable non-shared,
- // to direct a grow request to the memory (see ArrayBuffer::resize). In other scenarios
- // the pointer is not necessary and we should not be setting it to anything but a nullptr.
- ASSERT(isWasmMemory() && (isResizableNonShared() || !memory));
-#if ENABLE(WEBASSEMBLY)
- m_associatedWasmMemory = memory;
-#else
- UNUSED_PARAM(memory);
-#endif
-}
-
void ArrayBuffer::refreshAfterWasmMemoryGrow(Wasm::Memory* memory)
{
ASSERT(isWasmMemory());
@@ -553,10 +540,10 @@ Expected<int64_t, GrowFailReason> ArrayBuffer::grow(VM& vm, size_t newByteLength
return result;
}
-// Wasm JS API redefines the abstract operation HostResizeArrayBuffer as follows:
-// https://webassembly.github.io/threads/js-api/index.html#abstract-operation-hostresizearraybuffer
Expected<int64_t, GrowFailReason> ArrayBuffer::resize(VM& vm, size_t newByteLength)
{
+ RELEASE_ASSERT(!isWasmMemory());
+
auto memoryHandle = m_contents.m_memoryHandle;
if (!memoryHandle || m_contents.m_shared) [[unlikely]]
return makeUnexpected(GrowFailReason::GrowSharedUnavailable);
@@ -570,12 +557,6 @@ Expected<int64_t, GrowFailReason> ArrayBuffer::resize(VM& vm, size_t newByteLeng
return makeUnexpected(GrowFailReason::InvalidGrowSize);
deltaByteLength = static_cast<int64_t>(newByteLength) - static_cast<int64_t>(m_contents.m_sizeInBytes);
-#if ENABLE(WEBASSEMBLY)
- if (Options::useWasmMemoryToBufferAPIs()) {
- if (isWasmMemory() && (deltaByteLength < 0 || deltaByteLength % PageCount::pageSize))
- return makeUnexpected(GrowFailReason::InvalidGrowSize);
- }
-#endif
if (!deltaByteLength)
return 0;
@@ -587,17 +568,6 @@ Expected<int64_t, GrowFailReason> ArrayBuffer::resize(VM& vm, size_t newByteLeng
if (newPageCount != oldPageCount) {
ASSERT(memoryHandle->maximum() >= newPageCount);
-#if ENABLE(WEBASSEMBLY)
- if (Options::useWasmMemoryToBufferAPIs()) {
- // If this is currently associated with a Wasm memory, let the memory do the growing.
- // The memory will call back to our refreshAfterWasmMemoryGrow().
- RefPtr<Wasm::Memory> memory = m_associatedWasmMemory.get();
- if (memory) {
- std::ignore = memory->grow(vm, PageCount(newPageCount.pageCount() - oldPageCount.pageCount()));
- return deltaByteLength;
- }
- }
-#endif
size_t desiredSize = newPageCount.bytes();
RELEASE_ASSERT(desiredSize <= MAX_ARRAY_BUFFER_SIZE);
diff --git a/Source/JavaScriptCore/runtime/ArrayBuffer.h b/Source/JavaScriptCore/runtime/ArrayBuffer.h
index 19cdbde7d9c2..d74153fd0e39 100644
--- a/Source/JavaScriptCore/runtime/ArrayBuffer.h
+++ b/Source/JavaScriptCore/runtime/ArrayBuffer.h
@@ -287,7 +287,6 @@ public:
void NODELETE makeWasmMemory();
inline bool isWasmMemory();
- void NODELETE setAssociatedWasmMemory(Wasm::Memory*);
// When a resizable buffer is associated with a non-shared Wasm memory, this function is called by the memory's growthSuccessCallback.
void refreshAfterWasmMemoryGrow(Wasm::Memory*);
@@ -328,7 +327,6 @@ private:
public:
Weak<JSArrayBuffer> m_wrapper;
private:
- WeakPtr<Wasm::Memory> m_associatedWasmMemory;
Checked<unsigned> m_pinCount { 0 };
bool m_isWasmMemory { false };
// m_locked == true means that some API user fetched m_contents directly from a TypedArray object,
diff --git a/Source/JavaScriptCore/runtime/JSArrayBuffer.cpp b/Source/JavaScriptCore/runtime/JSArrayBuffer.cpp
index 03413453a597..3a05622c86ab 100644
--- a/Source/JavaScriptCore/runtime/JSArrayBuffer.cpp
+++ b/Source/JavaScriptCore/runtime/JSArrayBuffer.cpp
@@ -27,6 +27,9 @@
#include "JSArrayBuffer.h"
#include "JSCInlines.h"
+#if ENABLE(WEBASSEMBLY)
+#include "JSWebAssemblyMemory.h"
+#endif // ENABLE(WEBASSEMBLY)
#include "TypedArrayController.h"
namespace JSC {
@@ -82,5 +85,36 @@ size_t JSArrayBuffer::estimatedSize(JSCell* cell, VM& vm)
return Base::estimatedSize(cell, vm) + bufferEstimatedSize;
}
+#if ENABLE(WEBASSEMBLY)
+JSWebAssemblyMemory* JSArrayBuffer::associatedWasmMemoryWrapper() const
+{
+ return m_associatedWasmMemoryWrapper.get();
+}
+
+void JSArrayBuffer::setAssociatedWasmMemoryWrapper(VM& vm, JSWebAssemblyMemory* wrapper)
+{
+ ASSERT(impl()->isWasmMemory() && impl()->isResizableNonShared());
+ m_associatedWasmMemoryWrapper.set(vm, this, wrapper);
+}
+
+void JSArrayBuffer::clearAssociatedWasmMemoryWrapper()
+{
+ m_associatedWasmMemoryWrapper.clear();
+}
+#endif // ENABLE(WEBASSEMBLY)
+
+template<typename Visitor>
+void JSArrayBuffer::visitChildrenImpl(JSCell* cell, Visitor& visitor)
+{
+ auto* thisObject = uncheckedDowncast<JSArrayBuffer>(cell);
+ ASSERT_GC_OBJECT_INHERITS(thisObject, info());
+ Base::visitChildren(thisObject, visitor);
+#if ENABLE(WEBASSEMBLY)
+ visitor.append(thisObject->m_associatedWasmMemoryWrapper);
+#endif // ENABLE(WEBASSEMBLY)
+}
+
+DEFINE_VISIT_CHILDREN(JSArrayBuffer);
+
} // namespace JSC
diff --git a/Source/JavaScriptCore/runtime/JSArrayBuffer.h b/Source/JavaScriptCore/runtime/JSArrayBuffer.h
index 86ebc3c902a0..16aaee19929a 100644
--- a/Source/JavaScriptCore/runtime/JSArrayBuffer.h
+++ b/Source/JavaScriptCore/runtime/JSArrayBuffer.h
@@ -27,9 +27,14 @@
#include <JavaScriptCore/ArrayBuffer.h>
#include <JavaScriptCore/JSObject.h>
+#include <JavaScriptCore/WriteBarrier.h>
namespace JSC {
+#if ENABLE(WEBASSEMBLY)
+class JSWebAssemblyMemory;
+#endif // ENABLE(WEBASSEMBLY)
+
class JSArrayBuffer final : public JSNonFinalObject {
public:
using Base = JSNonFinalObject;
@@ -51,8 +56,16 @@ public:
JS_EXPORT_PRIVATE bool NODELETE isShared() const;
ArrayBufferSharingMode NODELETE sharingMode() const;
bool isResizableOrGrowableShared() const { return m_impl->isResizableOrGrowableShared(); }
-
+
+#if ENABLE(WEBASSEMBLY)
+ JSWebAssemblyMemory* associatedWasmMemoryWrapper() const;
+ void setAssociatedWasmMemoryWrapper(VM&, JSWebAssemblyMemory*);
+ void clearAssociatedWasmMemoryWrapper();
+#endif // ENABLE(WEBASSEMBLY)
+
DECLARE_EXPORT_INFO;
+
+ DECLARE_VISIT_CHILDREN;
// This is the default DOM unwrapping. It calls toUnsharedArrayBuffer().
static ArrayBuffer* toWrapped(VM&, JSValue);
@@ -67,6 +80,11 @@ private:
static size_t estimatedSize(JSCell*, VM&);
ArrayBuffer* m_impl;
+ // For resizable non-shared Wasm buffers, this points back to the owning JSWebAssemblyMemory so
+ // that ArrayBuffer.prototype.resize can delegate to Wasm::Memory::grow.
+#if ENABLE(WEBASSEMBLY)
+ WriteBarrier<JSWebAssemblyMemory> m_associatedWasmMemoryWrapper;
+#endif // ENABLE(WEBASSEMBLY)
};
inline ArrayBuffer* toPossiblySharedArrayBuffer(VM&, JSValue value)
diff --git a/Source/JavaScriptCore/runtime/JSArrayBufferPrototype.cpp b/Source/JavaScriptCore/runtime/JSArrayBufferPrototype.cpp
index 9fa1f22bceb9..66e2a880bb35 100644
--- a/Source/JavaScriptCore/runtime/JSArrayBufferPrototype.cpp
+++ b/Source/JavaScriptCore/runtime/JSArrayBufferPrototype.cpp
@@ -32,6 +32,11 @@
#include "JSCInlines.h"
#include <wtf/text/MakeString.h>
+#if ENABLE(WEBASSEMBLY)
+#include "JSWebAssemblyMemory.h"
+#include "WasmMemory.h"
+#endif
+
WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
namespace JSC {
@@ -292,6 +297,29 @@ JSC_DEFINE_HOST_FUNCTION(arrayBufferProtoFuncResize, (JSGlobalObject* globalObje
if (!std::isfinite(newLength) || newLength < 0)
return throwVMRangeError(globalObject, scope, "new length is out of range"_s);
size_t newByteLength = static_cast<size_t>(newLength);
+
+#if ENABLE(WEBASSEMBLY)
+ // Wasm JS API redefines the abstract operation HostResizeArrayBuffer as follows:
+ // https://webassembly.github.io/threads/js-api/index.html#abstract-operation-hostresizearraybuffer
+ //
+ // Further, WebAssembly-originated resizable ArrayBuffers must defer resizing to the backing
+ // WebAssembly memory for correct handling of refreshing bounds-checking memories.
+ if (auto* jsMemory = thisObject->associatedWasmMemoryWrapper()) {
+ size_t oldByteLength = thisObject->impl()->byteLength();
+ if (newByteLength < oldByteLength)
+ return throwVMRangeError(globalObject, scope, "Cannot shrink WebAssembly memory"_s);
+ if (newByteLength % PageCount::pageSize)
+ return throwVMRangeError(globalObject, scope, makeString("WebAssembly memory cannot be resized to new byte length "_s, newByteLength, " because it is not a multiple of "_s, PageCount::pageSize));
+ size_t delta = newByteLength - oldByteLength;
+ if (delta) {
+ auto result = jsMemory->memory().grow(vm, PageCount::fromBytes(delta));
+ if (!result)
+ return throwVMRangeError(globalObject, scope, makeString("ArrayBuffer resize failed with new byte length "_s, newByteLength));
+ }
+ return JSValue::encode(jsUndefined());
+ }
+#endif
+
if (!thisObject->impl()->resize(vm, newByteLength))
return throwVMRangeError(globalObject, scope, makeString("ArrayBuffer resize failed with new byte length "_s, newByteLength));
diff --git a/Source/JavaScriptCore/wasm/WasmMemory.h b/Source/JavaScriptCore/wasm/WasmMemory.h
index 06fac07b2015..d23f2115eb79 100644
--- a/Source/JavaScriptCore/wasm/WasmMemory.h
+++ b/Source/JavaScriptCore/wasm/WasmMemory.h
@@ -38,10 +38,9 @@
#include <wtf/CagedPtr.h>
#include <wtf/Expected.h>
#include <wtf/Function.h>
-#include <wtf/RefCountedAndCanMakeWeakPtr.h>
+#include <wtf/RefCounted.h>
#include <wtf/RefPtr.h>
#include <wtf/TZoneMalloc.h>
-#include <wtf/ThreadSafeWeakPtr.h>
#include <wtf/Vector.h>
namespace WTF {
@@ -53,7 +52,7 @@ namespace JSC {
class LLIntOffsetsExtractor;
namespace Wasm {
-class Memory final : public RefCountedAndCanMakeWeakPtr<Memory> {
+class Memory final : public RefCounted<Memory> {
WTF_MAKE_NONCOPYABLE(Memory);
WTF_MAKE_TZONE_ALLOCATED_EXPORT(Memory, JS_EXPORT_PRIVATE);
friend LLIntOffsetsExtractor;
@@ -95,8 +94,6 @@ public:
void registerInstance(JSWebAssemblyInstance&);
- void checkLifetime() { ASSERT(!refCountDebugger().deletionHasBegun()); }
-
static constexpr ptrdiff_t offsetOfHandle() { return OBJECT_OFFSETOF(Memory, m_handle); }
SharedArrayBufferContents* shared() const { return m_shared.get(); }
diff --git a/Source/JavaScriptCore/wasm/js/JSWebAssemblyMemory.cpp b/Source/JavaScriptCore/wasm/js/JSWebAssemblyMemory.cpp
index 461b9fe19b03..9bdabe628539 100644
--- a/Source/JavaScriptCore/wasm/js/JSWebAssemblyMemory.cpp
+++ b/Source/JavaScriptCore/wasm/js/JSWebAssemblyMemory.cpp
@@ -51,7 +51,6 @@ void JSWebAssemblyMemory::adopt(Ref<Wasm::Memory>&& memory)
{
m_memory.swap(memory);
ASSERT(m_memory->refCount() == 1);
- m_memory->checkLifetime();
}
Structure* JSWebAssemblyMemory::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
@@ -104,10 +103,10 @@ void JSWebAssemblyMemory::associateArrayBuffer(JSGlobalObject* globalObject, boo
m_buffer->makeShared();
}
m_buffer->makeWasmMemory();
- if (m_buffer->isResizableNonShared())
- m_buffer->setAssociatedWasmMemory(m_memory.ptr());
auto* arrayBuffer = JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(m_buffer->sharingMode()), m_buffer.get());
+ if (m_buffer->isResizableNonShared())
+ arrayBuffer->setAssociatedWasmMemoryWrapper(vm, this);
if (m_memory->sharingMode() == MemorySharingMode::Shared) {
objectConstructorFreeze(globalObject, arrayBuffer);
RETURN_IF_EXCEPTION(throwScope, void());
@@ -122,8 +121,9 @@ void JSWebAssemblyMemory::disassociateArrayBuffer(VM& vm)
ASSERT(m_buffer);
if (!m_buffer->isShared())
m_buffer->detach(vm);
- m_buffer->setAssociatedWasmMemory(nullptr);
m_buffer = nullptr;
+ if (auto* wrapper = m_bufferWrapper.get())
+ wrapper->clearAssociatedWasmMemoryWrapper();
m_bufferWrapper.clear();
}
@@ -312,9 +312,6 @@ void JSWebAssemblyMemory::growSuccessCallback(VM& vm, PageCount oldPageCount, Pa
}
}
-
- memory().checkLifetime();
-
vm.heap.reportExtraMemoryAllocated(this, newPageCount.bytes() - oldPageCount.bytes());
}
diff --git a/JSTests/wasm/stress/wasm-resizable-buffer-resize-after-gc.js b/JSTests/wasm/stress/wasm-resizable-buffer-resize-after-gc.js
new file mode 100644
index 000000000000..efa64566d316
--- /dev/null
+++ b/JSTests/wasm/stress/wasm-resizable-buffer-resize-after-gc.js
@@ -0,0 +1,40 @@
+// Resizing a Wasm-backed resizable ArrayBuffer should work
+// even after the originating WebAssembly.Memory is GC'd.
+
+function flushStackRoots(n) {
+ let a = [n, {}, n + 1, {}, {}];
+ if (n)
+ return flushStackRoots(n - 1);
+ return a;
+}
+
+for (let iter = 0; iter < 5; ++iter) {
+ let buf;
+ (function () {
+ let m = new WebAssembly.Memory({ initial: 1, maximum: 100 });
+ buf = m.toResizableBuffer();
+ })();
+ flushStackRoots(64);
+ fullGC();
+ flushStackRoots(64);
+ fullGC();
+
+ if (buf.byteLength !== 65536)
+ throw new Error("bad initial length: " + buf.byteLength);
+ if (!buf.resizable)
+ throw new Error("not resizable");
+
+ buf.resize(65536 * 100);
+
+ if (buf.byteLength !== 65536 * 100)
+ throw new Error("bad resized length: " + buf.byteLength);
+
+ let view = new Uint8Array(buf);
+ for (let i = 0; i < view.length; i += 4096) {
+ if (view[i] !== 0)
+ throw new Error("non-zero at " + i);
+ }
+ view[view.length - 1] = 0x42;
+ if (view[view.length - 1] !== 0x42)
+ throw new Error("write failed");
+}
Regression Test / PoC
diff --git a/JSTests/wasm/stress/wasm-resizable-buffer-resize-after-gc.js b/JSTests/wasm/stress/wasm-resizable-buffer-resize-after-gc.js
new file mode 100644
index 000000000000..efa64566d316
--- /dev/null
+++ b/JSTests/wasm/stress/wasm-resizable-buffer-resize-after-gc.js
@@ -0,0 +1,40 @@
+// Resizing a Wasm-backed resizable ArrayBuffer should work
+// even after the originating WebAssembly.Memory is GC'd.
+
+function flushStackRoots(n) {
+ let a = [n, {}, n + 1, {}, {}];
+ if (n)
+ return flushStackRoots(n - 1);
+ return a;
+}
+
+for (let iter = 0; iter < 5; ++iter) {
+ let buf;
+ (function () {
+ let m = new WebAssembly.Memory({ initial: 1, maximum: 100 });
+ buf = m.toResizableBuffer();
+ })();
+ flushStackRoots(64);
+ fullGC();
+ flushStackRoots(64);
+ fullGC();
+
+ if (buf.byteLength !== 65536)
+ throw new Error("bad initial length: " + buf.byteLength);
+ if (!buf.resizable)
+ throw new Error("not resizable");
+
+ buf.resize(65536 * 100);
+
+ if (buf.byteLength !== 65536 * 100)
+ throw new Error("bad resized length: " + buf.byteLength);
+
+ let view = new Uint8Array(buf);
+ for (let i = 0; i < view.length; i += 4096) {
+ if (view[i] !== 0)
+ throw new Error("non-zero at " + i);
+ }
+ view[view.length - 1] = 0x42;
+ if (view[view.length - 1] !== 0x42)
+ throw new Error("write failed");
+}