brintos

brintos / hardwarejs public Read only

0
0

dlopen service maps every host exception to -ENOMEM — real errors are undiagnosable from the guest #5

Closed dmitry opened this issue · 1 comment
D dmitry commented

The HWJS_PARK_DLOPEN service arm wraps serviceDlopen in a catch that returns -12 for any exception, so the guest's dlerror always reads Cannot allocate memory regardless of the actual failure. Bitten twice in one port (CPython dynamic split):

  1. WebAssembly.Instance(): Import #274 "env" "sync": imported function does not match the expected type — an import-type refusal — surfaced to the guest as ENOMEM; we found the real error only by instrumenting src/worker.mjs on the build host.
  2. A genuine browser-side allocation failure looks byte-identical to (1) from the guest.

The page console carries nothing either (the user-report post exists in current src/worker.mjs but its text never reaches the guest, and older bundles post the terminal error kind instead).

Suggestions: (a) map distinguishable classes to distinct errnos (type/link refusals → ENOEXEC/ELIBBAD, only real allocation failures → ENOMEM); (b) carry e.message somewhere guest-reachable (a dlerror side-channel or a guaranteed console.error), so a failing dlopen is diagnosable without host instrumentation.

jdbrinton jdbrinton commented

Fixed, verified end-to-end (guest dlerror() text included), and deployed to production.

Root cause

The HWJS_PARK_DLOPEN service arm in src/worker.mjs wrapped serviceDlopen in catch (e) { ... dlret = -12; } — every host exception, whatever its class, became -ENOMEM, and the guest had no channel for the real message at all (musl/glibc could only format the errno).

Fix (hardwarejs 7c16a44)

1. ClassificationclassifyDlopenFailure (exported from src/dynLoader.mjs, used by the service arm):

host failure errno class
WebAssembly.LinkError (import-type mismatch — your case 1) -ELIBBAD (80) link
WebAssembly.CompileError / not-wasm (bad magic) -ENOEXEC (8) compile
RangeError (engine grow OOM), the loader's never-silent mmap-OOM throw, shared-table exhaustion -ENOMEM (12) alloc
hwjs-dlopen: load-policy refusals (manifest skew, PIC holes, sp-fabrication, unresolved imports) -ELIBBAD (80) policy
anything unrecognized -EINVAL (22) unknown

Only genuine allocation failure keeps ENOMEM — your case 2 (and linux-in-the-browser#70's real OOM) still reads errno 12, now with the OOM text attached.

2. The dlerror side channel — new SYS_wasm_dlerror (1043, synchronous host intercept like dlsym; added to the asm/hwjs_dlopen.h shared ABI record): the service latches the real failure text on every failure path (classified throws AND the synchronous refusal errnos: ENOSYS/no-staging, static image, kernel staging errno), cleared at each SYS_wasm_dlopen latch. musl (src/ldso/wasm32/dlopen.c) and glibc (sysdeps/wasm32/dl-open.c) fetch it on a failed cookie and feed it to dlerror().

3. Host-side attribution — the user-report now carries class -> errno + full message, plus a guaranteed console.error copy, so a failing dlopen is diagnosable from the page console even without the guest half.

Before / after (guest stdout, real boots)

RED (pre-fix arm, import-type-mismatch .so):

dlopen failed: dlopen("/suspder2.so") failed (errno 12)

GREEN (final artifacts):

dlopen failed: dlopen("/suspder2.so") failed (errno 80): [link -> ELIBBAD] WebAssembly.Instance(): Import #17 "env" "read": imported function does not match the expected type

Simulated genuine OOM (HWJS_RED_DLOPEN_OOM=1, drives the real allocMemory throw):

dlopen failed: dlopen("/suspder2.so") failed (errno 12): [alloc -> ENOMEM] hwjs-dlopen: SYS_mmap for the .so data region (36B) returned 0 (never-silent OOM)

Gate

test/d1-dlopen-errno.test.ts — 8 unit classification arms + 2 e2e kernel boots: (a) d1-dlopen-ebad cpio ships a byte-patched import-type-mismatch .so (new distros/common/dlopen-fixtures/patch-import-type.mjs; GOOD manifest, still validates, LinkError at instantiate) asserting errno 80 + the verbatim engine text + the attributed host report; (b) the OOM knob asserting ENOMEM is preserved. The RED arm genuinely flips: run against the pre-fix catch-all, (a) fails with the observed errno 12 and (b) fails because the knob doesn't exist. Full d1-dlopen* + dlopen-loader* suites green (74 tests) on the rebuilt vmlinux + rebuilt musl fixtures.

SHAs

  • hardwarejs 7c16a44 (service + classifier + side channel + gate)
  • musl 33f83995 (dlopen fetches the host text; libc/musl-bc/fixture mains rebuilt)
  • glibc a2f08bf792e (_dl_open ditto; pool recompiled — elf_dl-open.bc verified to carry the 1043 call. Honest caveat: there is no glibc guest dlopen gate yet, so the glibc text half is compile-verified only; the errno half needs no guest rebuild and is live for existing binaries)
  • linux 9f2e14926374 (shared ABI record; vmlinux rebuilt)
  • superproject 704341e (gitlinks + the ebad fixture rig)

Deployed

make web && make web-sync + sst deploy --stage productionhttps://brintos.io/hardwarejs/worker.mjs and dynLoader.mjs verified on the live URLs to carry SYS_WASM_DLERROR/classifyDlopenFailure.

This un-masks the diagnostics behind linux-in-the-browser#70/#71/#73: #71's import-type refusal now surfaces its exact engine message in dlerror() (and on the page console), #70's genuine ENOMEM still reads ENOMEM (now with the allocation site named), and anything mis-attributed to memory pressure by the old collapse will now show its true class. Note existing guest binaries get the faithful errno immediately (host-side change); picking up the dlerror text requires a relink against the updated musl/glibc.

jdbrinton closed this as completed