brintos

brintos / hwjs-cc public Read only

0
0

HwjsCoroutinize refuses VLA-shaped merge sorts (msort_r, reason=vla) — any app linking a glibc-style qsort kills the whole-program link #2

Closed dmitry opened this issue · 1 comment
D dmitry commented

Summary

The coroutinize plugin (provenance f96cd9a5) refuses functions that allocate a VLA (reason=vla), and the classic glibc merge-sort implementation msort_r is exactly such a function (char tmp[...] sized at runtime). At the whole-program link vantage the refusal is fatal ("live ABI chimera"). Practical consequence: any application that carries a copy of glibc's msort — which is common in the wild — cannot link on the coroutinize arm.

Observed while porting Midnight Commander (BRINTOS-32): GLib 2.56 bundles gqsort.c (a verbatim copy of old glibc msort); its msort_r was refused and killed the mc link. Earlier in the same loop the same function also surfaced as a refused frame on a conservative static setjmp-unwind path (setjmp-2b-unwind-through-non-coroutinizable-frame).

Reproduction

Whole-program wasmcc-glibc link of any object set containing GLib 2.56's gqsort.o (or another copy of glibc msort), with the plugin at f96cd9a5:

HwjsCoroutinize: REFUSE msort_r reason=vla
FATAL: ... whole-program link aborted

Workaround in use (recipe-side)

Drop the offending object from the link (llvm-ar x, remove gqsort.o) and provide a coroutinizable, no-VLA replacement — an in-place heapsort implementing qsort / qsort_r (GNU 3-arg) / __qsort_r / g_qsort_with_data. Shipped in our build overlay (EmcraftSystems/brintos-build-overlay, branch BRINTOS-32-mc-glibc, glibc/apply_mc_glibc.py); mc links and runs with it (browser-verified).

Suggested direction

Either teach the plugin to coroutinize bounded VLA frames (spill the VLA to the coroutine frame / heap), or ship a no-VLA qsort family in the platform libc pool so ported code stops tripping on bundled msort copies.

jdbrinton jdbrinton commented

Fixed in F159 — the pass now LOWERS the bundled-glibc-msort VLA shape (no more reason=vla)

Root cause — why the pass refused

The classic glibc merge sort msort_r (bundled verbatim in the wild: GLib 2.56 gqsort.c, the Midnight Commander wall) allocates its merge temp as a VLA (dynamic alloca), stores it into the local msort_param, and merges through memcpy while the user comparator — which can suspend on this substrate — is dispatched indirectly.

At the whole-program glibc vantage memcpy is defined, and glibc's generic C memcpy (string/memcpy.c + sysdeps/generic/memcopy.h + wordcopy.c) converts both pointer args to integers up front (dstp = (long) dstpp), byte-copies through raw (unmasked) inttoptr, and hands the address-integers onward as integer arguments to the defined _wordcopy_fwd_* helpers. Every prior ptrtoint recognizer rightly failed on this: Q5/Q2/G-ptr-confined ban inttoptr; F134 requires an align-DOWN mask before the inttoptr and has no integer-call-arg form; F130 blesses only raw syscall args. So the deep leaf ptrtoint ptr %1 inside @memcpy refused msort_r with:

[hwjs-coro] REFUSE fn=msort_r reason=vla/dynamic-alloca-address-escape-opaque-sink ...
wasmld: FATAL (HWJS-3 coro arm): ... a refused def is a live ABI chimera. Stop-and-report.

and every glibc-carrying qsort/msort user died at the whole-program link.

What F159 does — and why it is frame-safe across suspends

F159 (pass/coroutinize/CoroutinizePass.cpp, ~lines 3513–3535 and 3678–3868; recognizer ptrtointWordcopyReaddress / wordcopyChainOk) is a classification-precision recognizer in the escape walk's ptrtoint arm — mechanism-based, no name table, so any bundled copy of the idiom in any package lowers identically. The integer chain from the region ptrtoint is followed through non-publishing scalar derivations (binop / trunc / *ext / select / phi / freeze / pure-int intrinsics, with the F156 shift-amount and F157 GEP-index terminals), accepting two new forms the prior recognizers refuse:

  • (i) a plain (unmasked) inttoptr reconstitution — the reconstituted pointer rejoins the main escape walk as an ordinary region derivation, so loads/stores through it are in-place linear-memory accesses and a downstream publish/host-sink still refuses (the never-silent boundary is inherited, not re-implemented); and
  • (ii) a chain integer passed as an integer argument of a direct call to a defined, non-vararg function (the _wordcopy_fwd_aligned(dstp, srcp, n) shape) — descended under the same rules, inner inttoptrs classified by a fresh callee-mode escape walk (Opaque / returned-region → fail).

Anything else still fails the chain — a store of the integer, a ret of the integer, a declared/indirect/vararg callee — and the region falls to the loud, counted Opaque catch-all (fail-safe; fixture controls on all three).

Frame-safety: the VLA merge temp DS-LOWERs to a frame-resident region (the same DS6 lowering as any coroutinized alloca), so the merge-temp storage lives in the coroutine frame and survives every suspend of the comparator. The address only ever leaves pointer form into (a) integers confined to non-publishing scalar arithmetic inside defined, module-local glibc code, and (b) pointers reconstituted from those integers that rejoin the ordinary walk and are used solely for in-place accesses to delta=0-copied linear memory. Same delta=0 witnessed-sound in-MEMORY bar as F130/F134 — classification-precision only, no emission/ABI change.

Now lowered vs. still loudly refused

  • Lowered: a dynamic-alloca region merged through the glibc integer-laundering memcpy/_wordcopy idiom (msort_r's p.t), including the s>32 indirect lane with pointer entries living inside the region.
  • Still loudly refused (unchanged): a genuine escape of the dynamic-alloca address to an opaque/host sink outside delta=0 linear memory — a store of the address-integer, a ret of it, or a publish through the reconstituted pointer. The never-silent wall is intact.

RED → GREEN evidence

Direct reproduction of the wall (pre-fix plugin, hwjs-cc 7d47bb1, no knobs):

[hwjs-coro] REFUSE fn=msort_r reason=vla/dynamic-alloca-address-escape-opaque-sink escaped=1 opaque_escape=1 ... suspends=4
wasmld: FATAL (HWJS-3 coro arm): ... a refused def is a live ABI chimera. Stop-and-report.

New superproject gate run-hwjscc2-gqsort-gate.mjs (verbatim GLib 2.56 gqsort.c fixture) — 9/9 on the final F159 plugin:

  • GREEN: whole-program wasmcc-glibc link SUCCEEDS; DS-LOWER fn=msort_r lowered=1/1; msort_r NOT refused.
  • RUNTIME: SORT OK, exit 0 — both the direct (s=4) and the s>32 indirect (pointers-in-region) msort lanes sort correctly with the comparator parking on every compare (66 parks); the VLA merge temp survives every park. CONTROL (no-park) also sorts.
  • RED (genuinely flipping): HWJS_CORO_NO_WORDCOPY_READDRESS=1 re-exposes the exact ticket refusal (reason=vla/dynamic-alloca-address-escape-opaque-sink) + the "live ABI chimera" fatal — verified behaviour-identical to a 7d47bb1-built plugin.

Also green on the final re-derived plugin (PLUGIN-PROVENANCE commit=60d95279c880 dirty=0): provenance gate 6/6, coroutinize pass suite 177 pass / 0 fail (7 environmental skips).

All distros green (one lineage forward together)

Kernel + all four distros rebuilt on the F159 plugin lineage; in the real glibc distro link the merge-sort family (msort_with_tmp, __qsort_r, qsort_r_mergesort, indirect_msort_with_tmp, qsort, bsearch) coroutinizes cleanly (refused=0):

  • linux-6.12-glibc-bash-coreutils — green (vmlinux.wasm 16,484,338 bytes)
  • linux-6.12-musl-bash-coreutils — green
  • linux-6.12-musl-busybox — green
  • wabuntu-24.04 — green (composition distro)

Substrate regressions clean on the final artifacts — no regressions from F159: musl malloc-churn (#67) GREEN + RED flips; glibc malloc-fatal (#73) GREEN + both RED arms flip; hardwarejs r6 + dlopen M1/M2-dlclose/M2-fork/M3-dispatch/gates (23 vitest tests) pass.

All three live machines deployed (additive): brintos/linux-6.12-{glibc-bash-coreutils,musl-bash-coreutils,musl-busybox}.

SHAs

  • Pass: brintos/hwjs-cc @ 60d95279c880ce9d9ec7fcc9800c5388e3b140b2 (F159), pushed to joel-dev (7d47bb1..60d9527).
  • Superproject: brintos/linux-in-the-browser @ 9bbdfbd (advances the toolchains/hwjs-cc gitlink 7d47bb1 → 60d95279 + adds the gqsort gate), pushed to joel-dev (8ef0121..9bbdfbd).

The recipe-side workaround (dropping gqsort.o and shipping a no-VLA heapsort) is no longer required — the bundled glibc msort links and runs as-is.

Closing as completed.

jdbrinton closed this as completed