GuestHeap: host-side allocator over guest memory; remove the copy layer (ECO-416) - #43
Merged
Merged
Conversation
This was referenced Jul 24, 2026
Arshia001
force-pushed
the
napi-value-scope-rework
branch
from
July 28, 2026 14:11
293f8a6 to
4fff8bd
Compare
The bridge allocated guest memory by calling the guest's exported malloc (reentrant into the wasm guest, and with no free path — every allocation leaked). GuestHeap replaces that: it claims pages of the guest's linear memory by issuing memory.grow FROM THE HOST and manages allocations within the claimed ranges itself. Grows are serialized per memory and return the previous size, so host and guest (sbrk) claims interleave without overlap — the same protocol the WASIX dynamic linker relies on. Design points: - All allocator metadata is host-side (offset-allocator + a live-alloc side table). Guest memory holds payload bytes only, so a buggy or hostile guest can corrupt its own data but never allocator state; frees are validated against the side table (double-frees, fabricated and wrong-length pointers are rejected). - Shared memories grow through a cloned VMSharedMemory handle: no store needed, callable from any thread (required by the upcoming V8 array-buffer allocator routing, whose Free runs on GC threads). Non-shared memories (napi_wasmer conformance lane) are pre-funded at instantiation and refilled from import boundaries. - The base pointer must never move (external backing stores hold host pointers): verified at init via MemoryStyle::Static, re-checked after every grow (abort on violation). - Claimed bytes are charged to the WasmLinear budget pool (host-issued grows bypass BudgetedMemory's accounting) and uncharged on drop. All five malloc_fn call sites now use the heap (arraybuffer/buffer/ buffer_copy creators return napi_generic_failure on exhaustion, matching Node's OOM behavior), and the guest-malloc registration is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rs via finalizers (ECO-416) Phase 2 of the guest-heap work put bridge-side allocations in host-claimed guest memory. This routes the other — much larger — class of buffers there too: every backing store V8 allocates (Buffer.alloc & co in host JS) now comes from the GuestHeap, so guest and host share buffer bytes directly and the snapshot-copy coherence layer loses its last customer. - TrackingArrayBufferAllocator gains a guest-heap context (installed via unofficial_napi_env_create_options before the isolate exists, so even bootstrap-time backing stores are guest-backed). Allocate routes to the Rust hook (null => V8 RangeError; never falls back to host memory, which the guest could not address). Free uses a mixed-provenance protocol: the hook claims pointers by integer range (handles pre-ctx allocations from the legacy backing allocator). MaxAllocationSize is capped for the 32-bit guest space. - Guest-heap-routed allocations skip the external-memory budget charge: the claimed pages are already charged as wasm linear memory, which is the single source of truth for those bytes. - The allocator/finalizer contexts hold Weak<GuestHeap> plus copied integers, never an Arc: process-lifetime V8 globals are deliberately leaked (exit-race fix), and an Arc reachable from them would pin the instance's multi-GiB memory reservation forever. - Bridge-created buffers/arraybuffers now use finalized external variants whose V8 finalizer returns the allocation to the guest heap — fixing the historical permanent leak of every napi_create_buffer. - Weak no-op hook fallbacks keep the native edgejs build (no Rust host, no context installed) byte-for-byte on its existing path. js-native-api conformance: 34/34 with the full pipeline active. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With every backing store now living in guest linear memory (bridge allocations since the GuestHeap commit, V8/JS allocations since the allocator-routing commit), guest access to buffer bytes is exact base-relative translation with zero state. The snapshot-copy layer — per-frame guest copies, write-back flushes at callback/import unwinds, backing-store-token and handle-id caches — has no remaining customers, and its heuristics were the root cause of the string_decoder/zlib data-integrity cluster. Deleted: - NapiEnv: guest_data_ptrs (+ prune), guest_data_backing_stores, host_buffer_copies + frame stacks, and their types; - callback.rs: all flush machinery (the callback-invocation ctx swap stays — the trampoline needs it); - resolve_current/resolve_or_copy_host_data_to_guest and the remember_* helpers; - the snapshot/overwrite FFI pair and its C++ byte-span helpers. The info getters now translate exactly, with one narrow fallback: a "foreign" backing store that did not come from the array-buffer allocator (e.g. a view over a V8-side WebAssembly.Memory) gets a one-way READ-ONLY snapshot into a guest-heap allocation whose lifetime is tied to the JS value via napi_add_finalizer. Mutations through such a pointer do not propagate (they never reliably did); a rate-limited log makes any unexpected traffic visible. Also: unofficial_napi_free_buffer now actually frees (guest-heap validated), closing the profile/snapshot JSON copy leak. js-native-api conformance: 34/34. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ge (ECO-416) guest_napi_create_external_arraybuffer (and its buffer twin) computed the host backing-store address as `host_base + external_data as u64`, where external_data is the guest data pointer typed i32. wasm32 pointers are unsigned (0..4GiB); once the guest's own heap grows past 2GiB a pointer has bit 31 set, so `as u64` sign-extends it and the computed host address lands ~2GiB BELOW the linear-memory base. V8 then reads/writes that wild pointer — an out-of-bounds host access (SIGSEGV, and a co-tenancy hazard on Edge). Zero-extend instead (`as u32 as u64`). Reproduced with a buffer-churn that holds >2GiB of Buffer.alloc live (each is a guest-malloc'd external arraybuffer): host SIGSEGV in V8's TypedArray fill right at the 2GiB boundary before, clean run to >2.5GiB after. Pre-existing (the i32 signature predates the guest-heap work); surfaced now because dropped guest finalizers (ECO-415) let those buffers accumulate past 2GiB. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uption under WASIX threads) (ECO-416) WASIX worker threads share a single linear memory but each instantiates its own N-API env, so configure_instance built a SEPARATE GuestHeap per env over the SAME memory. Confirmed with etherpad: 6 heaps created over one base (0x..c00000) from 6 threads. Each heap had an identical ownership range (`owns()` = [base, base+4GiB)) but its own offset allocator, live table, and mutex — so ownership was ambiguous across heaps and the per-heap mutexes serialized nothing between them. Under etherpad's sustained multi-threaded load this corrupted the HOST glibc heap (`malloc_consolidate(): unaligned fastbin chunk detected`, SIGABRT). It was a Heisenbug — gdb and any added synchronization shifted the timing enough to hide it (which is itself how the multi-heap race was confirmed). Fix: a process-global registry keyed by the memory's base address hands every env on the same shared memory the same Arc<GuestHeap>. One allocator, one live table, one mutex now serialize all threads with unambiguous ownership; Weak refs let the heap drop when its last env goes. Non-shared memories (the single-threaded conformance lane) still get a fresh heap each time, avoiding base-key collisions across short-lived instances. Verified: etherpad 3x natively (no instrumentation, race live) — zero host corruption, now reaches only its pre-existing unclean-exit (unchanged, still skipped). js-native-api conformance 34/34; string-decoder / zlib / http2-respond-file / buffer still pass. The grow-from-host architecture is sound (dlmalloc copes with foreign memory.grow via add_segment); this was purely a host-side multi-heap concurrency bug. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Arshia001
force-pushed
the
napi-value-scope-rework
branch
from
July 28, 2026 14:52
4fff8bd to
4db81be
Compare
syrusakbary
reviewed
Aug 5, 2026
Comment on lines
+504
to
+513
| extern "C" __attribute__((weak)) void* napi_host_guest_heap_alloc( | ||
| const void* /*ctx*/, size_t /*length*/, int /*zero*/) { | ||
| return nullptr; | ||
| } | ||
| extern "C" __attribute__((weak)) int napi_host_guest_heap_free( | ||
| const void* /*ctx*/, void* /*data*/, size_t /*length*/) { | ||
| return 0; | ||
| } | ||
| extern "C" __attribute__((weak)) void napi_host_guest_heap_release( | ||
| void* /*ctx*/) {} |
Member
There was a problem hiding this comment.
I believe we can use the allocator exposed in the libc of wasm directly, that way we don't need 3 new extra functions (and we move that logic into the wasm program itself)
syrusakbary
approved these changes
Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack 4/4 (napi) — final; matches local state. Base:
napi-v8-imports-bridge-fixes.Replaces the snapshot-copy/write-back coherence layer (root cause of the string_decoder/zlib data-integrity cluster) with a host-side allocator over guest linear memory:
GuestHeap: claims pages via host-issuedmemory.grow, manages them withoffset-allocator(all metadata host-side)Results: wasix node suite 1625/0 (skip list 106→59), js-native-api conformance 34/34, native baseline green, ECO-414 loop 0/40.