← All reports

WASM multi-memory grow refreshes only the memory that changed

Growing one shared memory refreshed every other memory in the instance.

Component: JSC WebAssembly memory | 4002a93

WASM multi-memory lets a module import several linear memories at once, mixing shared: true (SharedArrayBuffer-backed, reachable from multiple threads and workers) with non-shared thread-local ones. Each JSWebAssemblyInstance caches a per-memory {base, boundsCheckingSize} pair so JIT-generated bounds checks can be fast integer compares rather than dereferences of live objects. BufferMemoryHandle tracks every instance — every "anchor" — that imported a given memory across all threads, so a grow() on a shared memory can notify each consumer to refresh.

Growing a shared WebAssembly.Memory walked every anchor and called updateCachedMemories(), which refreshed that instance's entire memory cache — including non-shared sibling memories owned by other threads. That raced those threads' own Memory::grow(), which frees the old handle when swapping buffers, so the refresh could read a freed BufferMemoryHandle. The fix replaces the blanket refresh with updateMatchingCachedMemoriesConcurrently(), which skips any memory slot whose Wasm::Memory::shared() pointer is not identical to the specific SharedArrayBufferContents that grew.

The racy read populated the {base, boundsCheckingSize} pair that JIT-compiled WASM code trusts unconditionally, so a lost race could inject stale or freed values into the data gating every load and store. That is what makes this a memory-safety fix rather than a threading tidy-up: the corrupted state is not incidental, it is the bounds-check input itself.

The forward-facing pattern is a notification fan-out that refreshes more state than the event actually invalidated. Anywhere a cross-thread callback iterates a registry of anchors, subscribers or observers and then re-reads all of a target's cached state, the extra slots may be owned by threads that share no lock with the notifier. Audit the other anchors() consumers and any per-instance cache refresh reachable from a shared-object mutation for the same over-broad refresh shape — the tell is a per-target callback whose body loops over a container rather than touching the single slot the event names.