From 0b9636fadfc9817149998c317a3d608049de55c5 Mon Sep 17 00:00:00 2001 From: Yayoi-cs Date: Sat, 25 Apr 2026 02:10:20 +0900 Subject: [PATCH] LibJS: Only cache TypedArray data pointers for owned buffers WebAssembly.Memory-backed ArrayBuffers wrap external ByteBuffer storage. When that memory grows, ByteBuffer::try_resize() may realloc the backing storage while old fixed-length buffer objects remain reachable from JS. TypedArrayBase cached m_data for all fixed-length buffers, and the asm interpreter fast path dereferenced that cached pointer directly. For wasm memory views this could leave a stale pointer behind across grow(). Restrict cached typed-array data pointers to fixed-length ArrayBuffers that own stable ByteBuffer storage. External/unowned buffers, including WebAssembly.Memory buffers, now keep m_data == nullptr and fall back to code that re-derives buffer().data() on each access. Add regressions for both the original shared-memory grow case and the second-grow stale-view case. --- Libraries/LibJS/Runtime/ArrayBuffer.cpp | 11 ---- Libraries/LibJS/Runtime/ArrayBuffer.h | 6 +- Libraries/LibJS/Runtime/TypedArray.h | 6 +- Libraries/LibWeb/WebAssembly/Memory.cpp | 2 - ...Assembly-Memory-grow-shared-stale-view.txt | 24 +++---- ...ssembly-Memory-grow-shared-stale-view.html | 63 ++++++++++--------- 6 files changed, 53 insertions(+), 59 deletions(-) diff --git a/Libraries/LibJS/Runtime/ArrayBuffer.cpp b/Libraries/LibJS/Runtime/ArrayBuffer.cpp index 03c3b49805a..15cc3a7b205 100644 --- a/Libraries/LibJS/Runtime/ArrayBuffer.cpp +++ b/Libraries/LibJS/Runtime/ArrayBuffer.cpp @@ -261,17 +261,6 @@ void ArrayBuffer::detach_buffer() m_data_block.byte_buffer = Empty {}; } -void ArrayBuffer::refresh_cached_typed_array_view_data_pointers() -{ - if (m_data_block.byte_buffer.has()) - return; - auto* new_base = buffer().data(); - for (auto& view : m_cached_views) { - if (view.viewed_array_buffer() == this) - view.set_cached_data_ptr(new_base + view.byte_offset()); - } -} - void ArrayBuffer::register_cached_typed_array_view(TypedArrayBase& view) { m_cached_views.set(view); diff --git a/Libraries/LibJS/Runtime/ArrayBuffer.h b/Libraries/LibJS/Runtime/ArrayBuffer.h index e90c7ee3186..d6490e187bb 100644 --- a/Libraries/LibJS/Runtime/ArrayBuffer.h +++ b/Libraries/LibJS/Runtime/ArrayBuffer.h @@ -99,7 +99,6 @@ public: void detach_buffer(); void register_cached_typed_array_view(TypedArrayBase&); - void refresh_cached_typed_array_view_data_pointers(); // 25.1.3.4 IsDetachedBuffer ( arrayBuffer ), https://tc39.es/ecma262/#sec-isdetachedbuffer bool is_detached() const @@ -122,6 +121,11 @@ public: return true; } + bool can_cache_typed_array_view_data_pointer() const + { + return !is_detached() && is_fixed_length() && m_data_block.byte_buffer.has(); + } + // 25.2.2.2 IsSharedArrayBuffer ( obj ), https://tc39.es/ecma262/#sec-issharedarraybuffer bool is_shared_array_buffer() const { diff --git a/Libraries/LibJS/Runtime/TypedArray.h b/Libraries/LibJS/Runtime/TypedArray.h index 3f1a95cdb88..2e53904a5fa 100644 --- a/Libraries/LibJS/Runtime/TypedArray.h +++ b/Libraries/LibJS/Runtime/TypedArray.h @@ -43,8 +43,8 @@ public: ArrayBuffer* viewed_array_buffer() const { return m_viewed_array_buffer; } // Cached raw pointer: viewed_array_buffer->buffer().data() + byte_offset. - // nullptr means "not cached, use slow path". This avoids chasing through - // ArrayBuffer -> DataBlock -> Variant -> ByteBuffer -> inline/outline on every access. + // nullptr means "not cached, use slow path". This is only safe for + // fixed-length ArrayBuffers that own stable backing storage. u8* cached_data_ptr() const { return m_data; } void set_cached_data_ptr(u8* ptr) { m_data = ptr; } @@ -91,7 +91,7 @@ protected: void update_cached_data_ptr() { - if (!m_viewed_array_buffer || m_viewed_array_buffer->is_detached() || !m_viewed_array_buffer->is_fixed_length()) { + if (!m_viewed_array_buffer || !m_viewed_array_buffer->can_cache_typed_array_view_data_pointer()) { m_data = nullptr; return; } diff --git a/Libraries/LibWeb/WebAssembly/Memory.cpp b/Libraries/LibWeb/WebAssembly/Memory.cpp index 1261315a72d..16c087160a9 100644 --- a/Libraries/LibWeb/WebAssembly/Memory.cpp +++ b/Libraries/LibWeb/WebAssembly/Memory.cpp @@ -216,8 +216,6 @@ void Memory::refresh_the_memory_buffer(JS::VM& vm, JS::Realm& realm, Wasm::Memor if (!buffer->is_shared_array_buffer()) { // 1. Perform ! DetachArrayBuffer(buffer, "WebAssembly.Memory"). MUST(JS::detach_array_buffer(vm, *buffer, JS::PrimitiveString::create(vm, "WebAssembly.Memory"_string))); - } else { - buffer->refresh_cached_typed_array_view_data_pointers(); } // 2. Let newBuffer be the result of creating a fixed length memory buffer from memaddr. diff --git a/Tests/LibWeb/Text/expected/WebAssembly-Memory-grow-shared-stale-view.txt b/Tests/LibWeb/Text/expected/WebAssembly-Memory-grow-shared-stale-view.txt index 8f199fa754c..1c609e6efd4 100644 --- a/Tests/LibWeb/Text/expected/WebAssembly-Memory-grow-shared-stale-view.txt +++ b/Tests/LibWeb/Text/expected/WebAssembly-Memory-grow-shared-stale-view.txt @@ -1,12 +1,12 @@ -newView[0x0]: 0xde -newView[0x1]: 0xad -newView[0x2]: 0xbe -newView[0x3]: 0xef -oldView[0x4]: 0xab -oldView[0x5]: 0xad -oldView[0x6]: 0x1d -oldView[0x7]: 0xea -newView[0x8]: 0xca -newView[0x9]: 0xfe -newView[0xa]: 0xba -newView[0xb]: 0xbe +firstView[0x0]: 0x11 +firstView[0x1]: 0x22 +firstView[0x2]: 0x33 +firstView[0x3]: 0x44 +thirdView[0x4]: 0x55 +thirdView[0x5]: 0x66 +thirdView[0x6]: 0x77 +thirdView[0x7]: 0x88 +firstView[0x8]: 0x99 +firstView[0x9]: 0xaa +firstView[0xa]: 0xbb +firstView[0xb]: 0xcc diff --git a/Tests/LibWeb/Text/input/WebAssembly-Memory-grow-shared-stale-view.html b/Tests/LibWeb/Text/input/WebAssembly-Memory-grow-shared-stale-view.html index 3989a52a300..cdd37978b79 100644 --- a/Tests/LibWeb/Text/input/WebAssembly-Memory-grow-shared-stale-view.html +++ b/Tests/LibWeb/Text/input/WebAssembly-Memory-grow-shared-stale-view.html @@ -4,40 +4,43 @@ test(() => { // regression test for the stale-view case on shared WebAssembly.Memory. // A typed array created from the old memory.buffer before grow() should still read and write the same underlying memory after the grow. - const mem = new WebAssembly.Memory({ initial: 1, maximum: 2, shared: true }); - const oldView = new Uint8Array(mem.buffer); - oldView[0x0] = 0xde; - oldView[0x1] = 0xad; - oldView[0x2] = 0xbe; - oldView[0x3] = 0xef; + const mem = new WebAssembly.Memory({ initial: 1, maximum: 1000, shared: true }); + const firstView = new Uint8Array(mem.buffer); mem.grow(1); + const secondView = new Uint8Array(mem.buffer); - const newView = new Uint8Array(mem.buffer); - // verify array created from the new memory.buffer after grow() reads the same underlying memory before the grow. - println(`newView[0x0]: 0x${newView[0x0].toString(16)}`); - println(`newView[0x1]: 0x${newView[0x1].toString(16)}`); - println(`newView[0x2]: 0x${newView[0x2].toString(16)}`); - println(`newView[0x3]: 0x${newView[0x3].toString(16)}`); + mem.grow(100); + const thirdView = new Uint8Array(mem.buffer); - // verify array created from the new memory.buffer before grow() reads the same underlying memory after the grow. - newView[0x4] = 0xab; - newView[0x5] = 0xad; - newView[0x6] = 0x1d; - newView[0x7] = 0xea; - println(`oldView[0x4]: 0x${oldView[0x4].toString(16)}`); - println(`oldView[0x5]: 0x${oldView[0x5].toString(16)}`); - println(`oldView[0x6]: 0x${oldView[0x6].toString(16)}`); - println(`oldView[0x7]: 0x${oldView[0x7].toString(16)}`); + // Writes through the newest view must be visible through the oldest view + thirdView[0x0] = 0x11; + thirdView[0x1] = 0x22; + thirdView[0x2] = 0x33; + thirdView[0x3] = 0x44; + println(`firstView[0x0]: 0x${firstView[0x0].toString(16)}`); + println(`firstView[0x1]: 0x${firstView[0x1].toString(16)}`); + println(`firstView[0x2]: 0x${firstView[0x2].toString(16)}`); + println(`firstView[0x3]: 0x${firstView[0x3].toString(16)}`); - // verify array created from the new memory.buffer before grow() writes the same underlying memory after the grow. - oldView[0x8] = 0xca; - oldView[0x9] = 0xfe; - oldView[0xa] = 0xba; - oldView[0xb] = 0xbe; - println(`newView[0x8]: 0x${newView[0x8].toString(16)}`); - println(`newView[0x9]: 0x${newView[0x9].toString(16)}`); - println(`newView[0xa]: 0x${newView[0xa].toString(16)}`); - println(`newView[0xb]: 0x${newView[0xb].toString(16)}`); + // Writes through the oldest view must be visible through the newest view. + firstView[0x4] = 0x55; + firstView[0x5] = 0x66; + firstView[0x6] = 0x77; + firstView[0x7] = 0x88; + println(`thirdView[0x4]: 0x${thirdView[0x4].toString(16)}`); + println(`thirdView[0x5]: 0x${thirdView[0x5].toString(16)}`); + println(`thirdView[0x6]: 0x${thirdView[0x6].toString(16)}`); + println(`thirdView[0x7]: 0x${thirdView[0x7].toString(16)}`); + + // And via the intermediate view. + secondView[0x8] = 0x99; + secondView[0x9] = 0xaa; + secondView[0xa] = 0xbb; + secondView[0xb] = 0xcc; + println(`firstView[0x8]: 0x${firstView[0x8].toString(16)}`); + println(`firstView[0x9]: 0x${firstView[0x9].toString(16)}`); + println(`firstView[0xa]: 0x${firstView[0xa].toString(16)}`); + println(`firstView[0xb]: 0x${firstView[0xb].toString(16)}`); });