← All reports

[JSC] SerializedScriptValue is not carrying memory64 flag

JSC WebAssemblyTypeConfusion

Component: JSC WebAssembly | 3e351c7

Source/WebCore/bindings/js/SerializedScriptValue.cpp

+ write(memory->memory().addressType().is64Bit());
...
+ bool isMemory64;
+ if (!read(isMemory64)) { fail(); return JSValue(); }
+ JSC::Wasm::AddressType addressType { isMemory64 };
...
- memory = Wasm::Memory::create(contents.releaseNonNull(), result->memory().addressType(), WTF::move(handler));
+ memory = Wasm::Memory::create(contents.releaseNonNull(), addressType, WTF::move(handler));
...
if (m_internals->wasmMemoryHandlesArray) {
- for (auto& content : *m_internals->wasmMemoryHandlesArray)
- cost += content->sizeInBytes(std::memory_order_relaxed);
+ for (auto& content : *m_internals->wasmMemoryHandlesArray) {
+ if (content)
+ cost += content->sizeInBytes(std::memory_order_relaxed);
+ }
}

WebAssembly.Memory objects can be marked shared and passed between browsing contexts and workers via postMessage. The underlying buffer contents are genuinely shared, but each side reconstructs its own WebAssembly.Memory wrapper through structured cloning — the CloneSerializer/CloneDeserializer pair in SerializedScriptValue.cpp. The memory64 proposal lets a memory use 64-bit (i64) addressing instead of the default 32-bit, and that address type governs how bounds, grow() arguments, and pointer arithmetic into the shared buffer are interpreted, so it must survive the clone intact.

This commit makes the serializer write the flag — write(memory->memory().addressType().is64Bit()) — and the deserializer read it back into a JSC::Wasm::AddressType that is passed to Wasm::Memory::create directly. Previously the serializer never wrote the flag at all, and the deserializer (along with the analogous receiveBroadcast path in jsc.cpp) read addressType() off a freshly-created JSWebAssemblyMemory before adopt() had been called — i.e. before it had any real address type to report. The memory-cost accounting loop also gains a null check on each entry of wasmMemoryHandlesArray.

A memory64 shared WebAssembly.Memory could deserialize on the receiving side as an i32-addressed memory, leaving two agents disagreeing about how to address one shared buffer. Address-type confusion across an agent boundary is exactly the shape that turns into bounds-check divergence: one side computes offsets and limits under 32-bit assumptions while the other operates the same backing store as 64-bit. The fix restores the invariant that both agents reconstruct the same address type from the wire.

The forward-facing pattern is type metadata that lives beside the payload rather than in it, and therefore has to be re-derived on the receiving side. Narrow: re-read dumpDerivedTerminal, readDerivedTerminal and computeMemoryCost in SerializedScriptValue.cpp for any remaining site that calls addressType() — or any other accessor on a Wasm object — before adopt() has run, since the object is only partially constructed at that point; the tell is an accessor call on a JS* wrapper created within the same function that has not yet been handed its backing object. Wider: audit the other Wasm types that carry type metadata separate from their backing storage — tables (element type, limits), globals (value type and mutability), tags, and shared structs — for whether the structured clone path serializes every field the receiving side needs to reconstruct an identically-typed object, rather than defaulting anything. The shape to look for is a write(...) sequence in the serializer with fewer fields than the corresponding create(...) signature in the deserializer. Widest: the same class covers any cross-agent or cross-process serializer where one side reconstructs a typed object from a subset of its fields and fills the rest with defaults — the audit question to carry is "if this field is omitted from the wire, what value does the receiver assume, and does the sender ever disagree?"