Externrefs a guest holds are borrowed pointers. An import's result was
kept only until the outermost call returned, which was before the export
call converted its results to JavaScript, so a result created by an
import was freed before conversion. A table or global slot released the
reference it replaced at once, although the guest may still hold it in a
local after reading the slot. The kept references are now released after
the outermost caller has converted its results (scr_wasm_leave), and a
table or global store during a call keeps the reference it replaces until
then.
Checked memories (explicit bounds checks: sanitized builds, Windows)
grew with realloc, which can move the storage that memory.buffer and its
views alias. They now reserve the address space for their largest size
(mmap on POSIX hosts, VirtualAlloc on Windows) and commit pages as they
grow, so the base never moves. Hosts without address-space reservation
grow in place only until a buffer has been handed out; after that,
growth that would move the storage fails.
Tests: webassembly-externref-lifetimes.mjs (import results through an
export, table and global references replaced by the guest and by
JavaScript), webassembly-memory-growth.test.ts (views written after
growth from JavaScript and from the guest), and translate.test.ts cases
for memargs naming memory 0 explicitly and for local.tee operand types,
which b3bcbaaf7 made consistent with validation. Under SCRIPTC_SAN=1 the
two runtime tests report heap-use-after-free without these changes.
Merges main; internal/compatibility/generated/node-v24-backlog.json is
regenerated with pnpm node-compat.
new WebAssembly.Memory, Table and Global construct the runtime objects an
instance exports, with Node 24's descriptor order, defaults, conversions
and messages (V8's EnforceUint32 errors, the v128 and shared-memory
errors; a valid shared memory is refused with a TypeError until threads),
so either kind can be imported. Imports of memories, tables and globals
now check limits, types and mutability with V8's LinkError messages, and
exports keep V8's identity (imported objects re-export as themselves, one
object per memory and table). The classes called without new throw Node's
TypeError.
WebAssembly.Module.imports, exports and customSections read the
descriptor (ABI v2). Custom sections are carried only by programs that
call customSections. Every function a module references reaches
JavaScript as a callable function: the translator emits one call thunk
per signature, so table.get and funcref globals return the module's
unexported functions too, and exported functions can be stored in tables
and globals from any instance. Discovery also finds .so WebAssembly
shared libraries at resolved paths.
Fixes to W3/W4 found on the way: externref Table.grow fills with
undefined; Memory.grow/Table index errors and funcref messages match V8;
an invalid funcref global import is a LinkError; global.valueOf() on a
WebAssembly.Global-typed receiver answers the value; explicit-bounds
memories keep the blocks grow replaced, so old ArrayBuffers never
dangle.
This unblocks quickjs-wasi's extension loader: quickjs-wasi with its url,
encoding, headers and structured-clone extensions runs through its own
glue identically to Node, and pi codemode's static coverage goes from 83%
to 84% with no WebAssembly blocker left.
Evidence: differential corpus programs for the API (webassembly-jsapi),
a 261-test port of WPT wasm/jsapi (webassembly-wpt-jsapi), quickjs-wasi's
dist/extensions.js run unmodified against dylink-shaped modules
(webassembly-dylink), and the 2.0 spec suite's modules that import the
spectest memory and table (new webassembly-spec-imports group plus the
memory and table groups); all match Node in the plain and sanitized
lanes. The zero-cost gate leaves every non-WebAssembly program
byte-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A build now translates every WebAssembly module whose bytes it can see
(W2's discovery) into LLVM IR with a TypeScript translator
(packages/compiler/src/wasm/module.ts, translate.ts) and appends it to the
program module, so the existing LLVM helper optimizes, partitions and
caches it with the program. The program also emits scr_wasm_registry, a
portable table of module descriptors (scr_wasm_abi.h). At run time,
WebAssembly.compile, instantiate, validate, new Module and new Instance
find the translated module by the SHA-256 of its bytes; bytes the build did
not see reject with a CompileError. There is no interpreter and no JIT.
The runtime (scr_wasm.c, link-gated by the new `wasm` feature, handle tags
inside W0's headroom) implements the JavaScript API once, over
descriptors, with Node's semantics and messages: exports (functions,
memories, tables, globals), imports (functions, immutable globals, and
memories, tables and globals other instances export), every trap as
WebAssembly.RuntimeError, the CompileError, LinkError and RuntimeError
classes, compile and instantiate settling on a later turn, and
globalThis.WebAssembly as a value. On 64-bit Linux and macOS, memories sit
in 8 GiB guard reservations and a SIGSEGV/SIGBUS handler turns faults into
traps or stack exhaustion; async fibers get a guard page on first use.
Sanitizer builds and Windows check bounds and the stack explicitly. A
WebAssembly.Module crosses to workers through W0's transfer hook (W4).
W1's refusals become lowerings; the Memory, Table and Global constructors,
Module.imports/exports/customSections and the streaming functions still
refuse with SC2020. The globals: WebAssembly compatibility row moves to
partial.
Evidence: differential corpus programs for the API, TypeScript use and the
codemode worker shape, plus five programs executing about 9,300 commands
of the WebAssembly 2.0 spec suite; all match Node in the plain and
sanitized lanes. quickjs-wasi runs through its own glue, matching Node
(apart from a pre-existing non-wasm fence) and on par with or faster than
V8. The zero-cost gate leaves every non-wasm program byte-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>