brintos

brintos / hwjs-cc public Read only

0
0

HwjsCoroutinize: static merged-module glibc REFUSEs vasnprintf (SJLJ) + fpcast phase-order (xz/openssl) that dynamic linking accepts #4

Closed dmitry opened this issue · 1 comment
D dmitry commented

Summary

On the glibc arm, static merged-module links of common packages fail in the HwjsCoroutinize pass, while the same packages build clean when linked dynamically. Three failures, all on the yocto glibc arm (MACHINE=brintos-wasm32, static-only distro, hwjs-cc pin e861f8f):

1. coreutilsvasnprintf, at the link of ~12 binaries (users, [, nice, base64, stty, df, timeout, hostid, …):

[hwjs-coro] REFUSE fn=vasnprintf reason=vla/dynamic-alloca-address-escape-opaque-sink
  escaped=1 opaque_escape=1 dynalloca_total=1 suspends=146
[hwjs-coro] REFUSE fn=vasnprintf reason=setjmp-2b-unwind-through-non-coroutinizable-frame
  (... RED: HWJS_CORO_SJLJ_NO_UNWIND_PATH_CHECK)

vasnprintf's dynamic-alloca address escapes to an opaque host sink → the pass leaves it native → but it sits on a __libc_siglongjmp unwind path in the merged static module, so the SJLJ gate refuses the whole cross-frame lowering.

2. xzlzmadec link: [hwjs-coro] FPCAST PHASE-ORDER VIOLATION: a triggered fpcast site survives as an indirect call at the classification vantage (report_fatal_error).

3. opensslapps/openssl link: the same FPCAST PHASE-ORDER VIOLATION (dump shows glibc msort_with_tmp/qsort fpcast sites).

Why this is a static-merge artifact, not a source bug

  • The gnulib config is byte-identical glibc-vs-musl for every vasnprintf-relevant define (empty diff on NEED_PRINTF_*, ALLOCA, SNPRINTF, DCHAR, SIGSETJMP, WCHAR_T_IS_*), and musl static builds all three clean.
  • BRINTOS-3 built xz, openssl and 106 coreutils binaries clean on the glibc arm — dynamically (shared glibc.so), on pin f96cd9a, a 2-commit descendant of the failing e861f8f that carries the same HWJS_CORO_SJLJ_NO_UNWIND_PATH_CHECK gate.

So the difference is purely the link model: dynamic keeps __libc_siglongjmp in the shared library and each function in its own module (no single-module cross-frame path); the static merge pulls vasnprintf and siglongjmp into one module and the gate fires. The pass refuses static-linked glibc code it accepts dynamically.

Reproduction

Yocto glibc arm (meta-brintos, MACHINE=brintos-wasm32, TCLIBC=glibc, static distro), hwjs-cc e861f8f:

bitbake coreutils   # vasnprintf SJLJ refuse at the binary links
bitbake xz          # lzmadec FPCAST phase-order
bitbake openssl     # apps/openssl FPCAST phase-order

All three link clean on the musl arm and in a dynamic glibc build.

Candidate direction

DS6-lower the residual VLA/dynamic-alloca address escape (or permit a native frame on a setjmp/longjmp unwind path) in the static merged module; or this is subsumed once dynamic glibc linking (shared libc.so) is available on the yocto arm.

Impact / workaround

Blocks coreutils, xz and openssl on the static glibc arm — i.e. the deb-packaged glibc distro (dpkg needs real coreutils). Current workaround: ship BusyBox (core-image-minimal-brintos) and defer coreutils to this fix.

jdbrinton jdbrinton commented

Resolved at F161 (hwjs-cc 06665f7) — two distinct defects, both pure static-merge artifacts, both root-caused, fixed, and witnessed on the real packages

Root cause 1 — xz lzmadec / apps/openssl: the fpcast devirt's own lowering manufactured its evidence

FPCAST PHASE-ORDER VIOLATION was not a phase-order bug at all — it was evidence pollution by the pass's own devirt arms:

  • lowerFpcastSite rewrites a triggered indirect dispatch into the equality-guarded compare-chain whose arms are DIRECT calls to every sig-compatible address-taken candidate (the completeness contract).
  • The five fpcast evidence walks that cross a call boundary backward (fpcastRootGlobal / fpcastRootObjects / fpcastResolveThreaded / fpcastResolveActual FollowArgs + buildFpcastRegistry's helper-call scan) treated those synthetic arms as genuine callers.
  • At whole-program STATIC vantage, every static main carries glibc's __run_exit_handlers cxa site (Fvii, registered call_fini Fvi). Lowering it emits arms direct-calling every Fvii-compatible app function with actuals rooted in the exit-handler list. The assertFpcastLoweredBeforeClassify re-collect then walked Argument→caller-actual through an arm, rooted liblzma lzma_next_end's allocator->free field (byte offset 4 — coinciding with glibc's exit_function fn slot) at @__exit_funcs, manufactured call_fini as sig-mismatched evidence, re-TRIGGERED the previously-sound site, and the guard fataled the link. openssl: same class through the msort_with_tmp/qsort sites.
  • Why dynamic passed: the per-module split keeps glibc's dispatch sites and the app's address-taken universe in different modules — no arm inside glibc.so ever calls an app function. The failure only exists when the static merge assembles both in one module and the pass's own lowering bridges them.

Witnessed 1:1 on the real xz-5.4.5 lzmadec merged module: the RED knob re-manufactures exactly

[hwjs-coro] FPCAST-SURVIVOR fn=lzma_next_end site_sig=Fvii cands=16 evidence={call_fini:Fvi } site: tail call void %16(ptr noundef %20, ptr noundef %10)
fatal error: error in backend: [hwjs-coro] FPCAST PHASE-ORDER VIOLATION: ...

Fix (F161): every lowering-emitted call (arms + the F117 consult arm) carries !hwjs.fpcast.arm provenance metadata; the five caller-crossing walks skip arm calls. This is exact PRE-lowering analysis parity — an indirect caller was never followed either — so no site's evidence shrinks below its collect-#1 classification, and a genuinely-unenumerated target still lands on the chain's LOUD abort default (never a silent misdispatch). The violation path now NAMES every survivor (FPCAST-SURVIVOR fn=… evidence={…}) before the fatal. RED knob HWJS_CORO_NO_FPCAST_ARM_PROVENANCE (registered in wasmld's RED-knob guard).

Root cause 2 — coreutils vasnprintf (SJLJ): retired by F159, root-caused and gated here

Reproduced byte-exact (suspends=146) on gnulib vasnprintf.c recompiled with -DHAVE_ALLOCA=1 against this ticket's pin e861f8f: vasnprintf allocas its printf-directive scratch (buf = alloca(buf_neededlength)) and hands the address into glibc as the snprintf FORMAT pointer; the escape walk bottomed in glibc's integer-laundered printf/word-copy internals (ptrtoint both args, raw-inttoptr byte loops, address-integers as int args into defined _wordcopy_* helpers) → REFUSE vla/dynamic-alloca-address-escape-opaque-sinkvasnprintf stays NATIVE → and because the static merge puts __libc_siglongjmp in-module with vasnprintf on a sigsetjmp/siglongjmp cross-frame unwind path, the SJLJ seam fataled the whole link (setjmp-2b-unwind-through-non-coroutinizable-frame). The two refusal lines in this ticket are one causal chain, not two bugs. Dynamic passed because glibc.so keeps __libc_siglongjmp out of the app module — no in-module cross-frame path is ever assembled.

F159's integer-laundered word-copy readdressing (landed for brintos/hwjs-cc#2) follows exactly that chain, so the directive alloca now DS-LOWERs (1/1) to a frame-resident region, vasnprintf coroutinizes, and the SJLJ seam never fires. RED knob HWJS_CORO_NO_WORDCOPY_READDRESS re-manufactures both exact ticket refusals.

(Why the in-repo glibc distro never hit it: its coreutils config takes the malloc arm — HAVE_ALLOCA undef. The yocto arm's HAVE_ALLOCA=1 is the trigger; byte-identical gnulib defines glibc-vs-musl, as the ticket established.)

Verdict: two distinct defects (shared only the vantage)

Both are whole-program-merge phenomena the per-module dynamic vantage never assembles, but mechanically independent: (1) devirt-arm evidence pollution in the fpcast walks; (2) an escape-classification gap whose refusal cascaded into the SJLJ closure. Each has its own fix, its own flipping RED knob, and its own gate.

RED → GREEN evidence (all on the FINAL plugin, provenance commit=06665f7 dirty=0)

Real packages (link + RUN, static, whole-program glibc merge):

  • xz-5.4.5 lzmadec static link: GREEN — coroutinized=415 refused=0; RED (HWJS_CORO_NO_FPCAST_ARM_PROVENANCE=1) fatals with the exact ticket message + the new FPCAST-SURVIVOR fn=lzma_next_end attribution. RUN: decodes a real .lzma stream byte-exact under the −517-parking harness (1 park/1 resume) and in a no-park control.
  • xz-5.4.5 xz static binary: full compress → decompress round-trip byte-exact across parks.
  • openssl-3.0.13 apps/openssl static link: GREEN (pre-F161: the exact same FPCAST PHASE-ORDER fatal). RUN: openssl version prints "OpenSSL 3.0.13" (exit 0 across a park), openssl dgst -sha256 matches host hash byte-exact (028069dd…), openssl list -digest-commands sorts correctly (direct qsort/msort fpcast exercise).
  • coreutils nice + [ static links with gnulib vasnprintf recompiled -DHAVE_ALLOCA=1 (the ticket's package shape): GREEN with DS-LOWER fn=rpl_vasnprintf lowered=1/1, and [ RUNS ([ a = a ] → 0, [ a = b ] → 1).

Committed non-vacuous gates:

  • pass/coroutinize/fpcast_arm_provenance.mjs + fixtures/fpcast_arm.c (hwjs-cc, in run-coro.sh): GREEN triggered+lowered+no survivor, RUNTIME through the adapted arm, RED flips to the exact fatal. 8/8.
  • distros/linux-6.12-glibc-bash-coreutils/tools/run-hwjscc4-static-glibc-gate.mjs + tools/hwjscc4-static/{vasn_fx.c,fpcast_glibc_fx.c} (superproject): both shapes static-link (WASMLD_DYNAMIC_MAIN=0/MODULE=0, the yocto posture) AND RUN across parks + siglongjmp; both RED arms flip to the exact pre-fix refusals; plus the rig-conditional real-coreutils leg. 20/20 GREEN.

No-regression: hwjs-cc battery 178 pass / 7 skip / 0 fail; kernel force-recompiled per #84 (genuinely-new-plugin objects) and all three distros rebuilt green on the F161 lineage; substrate/glibc distro gates green (#73 malloc-fatal 8/8, #2 gqsort, #3 gstrerror all GREEN; hardwarejs vitest no regression — the 14 env-gated failures are identical pre/post-F161, needing the absent binaryen fixture-assembler). parked_abi/drive_abi/ds_region_abi unchanged — metadata-only provenance, no wire change, no new import.

Live: F161 is pass-only (no TS/hardwarejs runtime change), so no web/sst deploy; all three live distro machines (brintos/linux-6.12-{glibc-bash-coreutils,musl-bash-coreutils,musl-busybox}) were rebuilt on the F161 lineage and deployed additively (server-durable, no deletions).

SHAs: hwjs-cc 06665f7 (F161, pushed to joel-dev), superproject fb5318e (gitlink advance + the #4 gate, pushed to joel-dev).

Both static-merged-module walls the ticket reported — the coreutils vasnprintf SJLJ refusal and the xz/openssl fpcast phase-order violation — now link AND run correctly on the final artifacts. Closing as completed.

jdbrinton closed this as completed