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.
This commit is contained in:
Yayoi-cs
2026-04-25 02:10:20 +09:00
committed by Andreas Kling
parent 5d69c6d2b7
commit 0b9636fadf
Notes: github-actions[bot] 2026-04-25 04:12:12 +00:00
6 changed files with 53 additions and 59 deletions

View File

@@ -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<Empty>())
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);

View File

@@ -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<ByteBuffer>();
}
// 25.2.2.2 IsSharedArrayBuffer ( obj ), https://tc39.es/ecma262/#sec-issharedarraybuffer
bool is_shared_array_buffer() const
{

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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)}`);
});
</script>