Skip to content

Revive & harden the V8-imports WASIX lane (ECO-414 + bridge fixes) - #42

Merged
Arshia001 merged 7 commits into
napi-scope-bound-lifetimesfrom
napi-v8-imports-bridge-fixes
Aug 7, 2026
Merged

Revive & harden the V8-imports WASIX lane (ECO-414 + bridge fixes)#42
Arshia001 merged 7 commits into
napi-scope-bound-lifetimesfrom
napi-v8-imports-bridge-fixes

Conversation

@Arshia001

Copy link
Copy Markdown
Member

Stack 3/4 (napi). Base: napi-scope-bound-lifetimes. → next: napi-value-scope-rework.

The bridge fixes that made the V8-imports (imports-provider) WASIX lane boot and run again, plus the ECO-414 crash fix:

  • js_source ABI drift fix (header struct-ptr vs split Rust import — born inconsistent in napi 5669f74)
  • weak fallbacks for the Rust budget allocator hooks (native edgejs links no-ops)
  • napi_wrap: stop dropping the caller's ref request through the bridge (fixes the "Illegal invocation" bootstrap break)
  • ECO-414: leak process-lifetime V8 runtime globals to fix the exit-vs-teardown double-free/SIGSEGV
  • external_buffer ABI data-path fix (a host-copy coherence attempt was added then reverted, keeping only the ABI fix)

⚠️ Stacked chain, merge bottom-up; not yet mergeable. Fixes ECO-414; relates to ECO-415/416.

Arshia001 and others added 7 commits July 28, 2026 14:51
The value-limit/budget work pulls dependency versions that need rustc
1.94; the wasmer workspace already pins it, but standalone builds
(cargo-standalone.sh, used by the edgejs harness) picked up whatever
rustup default was active.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The guest header passes unofficial_napi_js_source by pointer (one wasm
param), but the four Rust import handlers expected text/bytecode split
into two params, so any edgejs.wasm built since the struct API landed
failed instantiation with an import-type mismatch. Read the struct from
guest memory instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The native edgejs build links unofficial_napi.cc without the Rust host,
so the extern budget-hook symbols were unresolved. The hook pointer is
never installed in that configuration, so weak no-op definitions are
never called; napi_wasmer's strong Rust exports override them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The guest-side import handler discards finalize_cb (a wasm function
pointer the host cannot invoke) but still forwarded ref_out to the
host napi_wrap, which per N-API semantics rejects a ref request without
a finalizer (napi_invalid_arg). The object was silently never wrapped,
so every subsequent method's napi_unwrap failed and edgejs bindings
threw "TypeError: Illegal invocation" - first hit during bootstrap by
the signal wrap created for process.stdout, breaking every V8-imports
guest since the check landed (2026-05-14).

Wrap without a ref instead, and mint the caller's ref separately as a
weak (refcount-0) reference, matching what napi_wrap would return.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h (ECO-414, ECO-416)

The wasmer CLI (and any embedder) may call exit() as soon as the guest's
exit code is known, which runs C++ static destructors on the main thread
while WASIX env teardown (NapiEnv drop -> snapi_bridge_unofficial_release_env
-> Isolate::Delete) is still running on a tokio thread. The SharedRuntime
static destructor destroyed EdgeV8Platform (and with it the fallback
platform's PageAllocator and TracingController), turning the concurrent
isolate teardown into a use-after-free.

Observed as:
- SIGSEGV in BoundedPageAllocator::FreePages during
  PagePool::ReleaseOnTearDown (the cage allocator's underlying platform
  PageAllocator was sitting on a glibc tcache freelist), and
- SIGSEGV in TraceEventHelper::GetTracingController during
  Heap::StartTearDown (freed platform vtable), and
- the ECO-414 "double free or corruption (fasttop)" abort in the static
  destructor chain itself -- all the same race from different sides.

Fix: make the runtime singleton and every isolate/env registry map a
heap allocation that is intentionally never freed, so exit-time
destructors can no longer pull the platform out from under a live
teardown. The OS reclaims everything at _exit; this matches the
standard V8 embedder rule of avoiding exit-time destructors.

Verified: test-http2-respond-file-fd host segfault 4/6 runs before ->
0/10 after (wasmer CLI --experimental-napi lane); ECO-414 conformance
repro (napi_wasmer array_test x40) 0 crashes, previously ~1 in 5-30.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t-copy coherence (ECO-416)

Two root causes behind the wasix-lane data-integrity cluster (http2
respondWithFD serving garbage, string_decoder losing buffered state,
zlib/manifest truncation symptoms):

1. napi_create_external_buffer parameter order. The C ABI is
   (env, length, data, ...) — unlike napi_create_external_arraybuffer,
   which is (env, data, length, ...). The Rust import read (data, length),
   so every guest external-buffer creation built a host Buffer whose data
   pointer was the LENGTH value (an address in the wasm data segment — the
   embedded builtin catalog, hence deterministic lexer.js bytes on the
   wire) and whose length was the guest POINTER (~100MB). This corrupted
   every consumer of the StreamPipe fd path.

2. Host-allocated buffer copy coherence. Guest access to host-allocated
   buffers goes through per-frame guest copies with write-back, and three
   defects could lose or resurrect data:
   - flush wrote back EVERY copy, so a stale unmodified copy flushed late
     clobbered newer host-side writes (write-backs now compare against
     the bytes captured at copy creation and skip unmodified copies);
   - cache-hit reuse of an existing copy never registered a flush
     obligation, so mutations through a reused copy were never written
     back (hits now adopt the copy into the current frame, refreshing it
     from live host bytes on first per-frame use);
   - host->guest callback returns did not flush, deferring write-back to
     the outermost import unwind — far too late for JS that reads the
     buffer right after the callback (e.g. StringDecoder's state checks);
     the callback trampoline now flushes copies created during the call;
   - the mid-execution coherency flush dropped all copy entries while
     active frames still held pointers into them, orphaning later
     mutations; it now writes back and re-bases entries instead.

Also removes the long-lived backing-store-token registrations for
guest-memory-backed externals (exact base-relative translation resolves
them statelessly) — raw-pointer tokens recycle after GC and permanent
entries could alias unrelated buffers.

Verified on the edgejs V8-imports wasix lane: test-http2-respond-file{,-fd,
-range,-fd-range}, test-string-decoder{,-end} now pass (previously
garbage/truncation + NGHTTP2_PROTOCOL_ERROR); js-native-api conformance
34/34 clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ABI fix (ECO-416)

The copy-coherence changes from the previous commit (pristine-compare
write-back skip, cache-hit adoption, per-callback flush) fixed the
string_decoder state-loss bug and unlocked ~35 skipped wasix tests, but
regressed the async zlib pipeline: draining copy entries at callback
boundaries orphans output buffers that zlib worker threads fill after the
creating callback returns (test-zlib-from-string/from-gzip/dictionary/
flush and friends failed, some with segfaults, none of them previously
skipped). Iterations on the flush policy (rebase instead of drain,
modified-only drain) traded one cluster for the other, so the coherence
work is reverted to the original semantics until it can be redesigned
around explicit copy lifetimes (tracked in ECO-416; the write-up on the
issue has the full analysis).

What stays:
- the napi_create_external_buffer parameter-order fix — the C ABI is
  (env, length, data, ...), unlike napi_create_external_arraybuffer's
  (env, data, length, ...); the import read (data, length), corrupting
  every StreamPipe payload into catalog bytes with a bogus ~100MB length.

Verified: zlib suite back to baseline (from-string/from-gzip/dictionary/
flush/brotli-from-brotli/random-byte-pipes all pass); the fd/sendfile
respond-file tests that don't depend on the string_decoder bug now pass
(respond-file-range, respond-file-fd-range, respond-file-compat,
respond-file-with-pipe, generic-streams-sendfile).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Arshia001
Arshia001 force-pushed the napi-v8-imports-bridge-fixes branch from 3ebc068 to ed95533 Compare July 28, 2026 14:52
@Arshia001
Arshia001 merged commit e4b04d7 into main Aug 7, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants