brintos

brintos / hwjs-cc public Read only

0
0

mcview (GNU mc 4.8.33 in viewer mode) traps at g_locale_from_utf8 from unix_error_string.resume — coroutinized frame parks under a native caller (set-over-set violation) #3

Closed dmitry opened this issue · 1 comment
D dmitry commented

Summary

On the glibc wasm32 machine, the mc binary in viewer mode (mcview FILE, i.e. mc -v) traps unconditionally on startup: RuntimeError: unreachable at g_locale_from_utf8, reached from a resumed (coroutinized) unix_error_string frame. The user-runtime then reports a hard invariant violation — set-over-set: parked slot not empty — a coroutinized frame tried to park while a previous park slot was still occupied, because the call chain reaching it passes through a native (non-coroutinizable) frame. The machine dies (blank screen).

The same binary in file-manager mode (mc) runs fine, so this is not a broken build of mc — it is a coroutinize-runtime limitation exposed by mcview's init path. It is the same class as the msort_r reason=vla refusal in #2: a function the plugin must coroutinize cannot legally park given the native frame on the live stack.

Environment

  • Machine: dmitry/linux-6.12-glibc-bash-coreutils (wasm32, glibc pool, dynamic-main).
  • mc: GNU Midnight Commander 4.8.33, built via the glibc overlay (BRINTOS-32).
  • Reproduced in-browser on brintos.io (headless Chrome), 2026-07-10.

Reproduction

Unconditional — independent of the file's type or size:

printf 'alpha\nbeta\ngamma\n' > /tmp/t.txt
TERM=ansi exec /usr/bin/mcview /tmp/t.txt

(exec only because the site's forked children cannot read the tty — a separate kernel issue, linux#7 — unrelated to this trap.) Also reproduced on a 26-line rootfs file (/root/selftest_mc.sh). Every run: blank screen, machine halts.

Trap (browser console)

hardwarejs: user[1] ERROR: unreachable
RuntimeError: unreachable
    at mc.g_locale_from_utf8        (wasm-function[836])
    at mc.unix_error_string.resume  (wasm-function[786])
    at mc.hwjs_resume ... at mc.hwjs_drive_loop ... at mc._start
hwjs-user-rt VIOLATION: set-over-set: parked slot not empty at a new park
    (a coroutinized frame parked under a NATIVE caller and the witness reached the next park)

Evidence localizing the trigger

Pointing MC_DATADIR at a writable, empty directory stops the crash:

mkdir -p /tmp/mc
MC_DATADIR=/tmp/mc TERM=ansi exec /usr/bin/mcview /tmp/t.txt   # viewer opens, no trap

With MC_DATADIR pointed away from the installed tree (/usr/share/mc, /etc/mc), mcview skips loading those files (skin, syntax, charset, sfs.ini — the last is even warned missing) and falls back to its built-in skin; the trap does not fire. So the trap is provoked while mcview reads one of its data/config files from the read-only cloud rootfs during init (most likely the skin .ini, whose string handling reaches g_locale_from_utf8), on a stack that has already been coroutinized past a native frame.

This is not a usable workaround: with MC_DATADIR redirected there is no skin, and the built-in fallback renders unreadably (blank) under this terminal. It only localizes the fault to the coroutinize runtime, not the mc recipe.

unix_error_string (mc lib/util.c) formats an errno string and calls g_locale_from_utf8 to charset-convert it; g_locale_from_utf8g_convert → iconv, which yields. It is a legitimate coroutinizable function. The defect is that the runtime cannot honor the park when a native frame is interposed between it and its coroutine parent.

Suggested direction

Same as #2 — either teach the plugin/runtime to coroutinize/park correctly when a native frame is interposed (spill/relocate as needed), or give the charset-conversion path (g_convert/iconv) a non-yielding fast path so it does not require a park in this context (e.g. identity / small-buffer conversions). Until then any glibc app whose error/skin path reaches g_locale_from_utf8 on a coroutinized-under-native stack traps the same way.

No candidate fix attached: this is a toolchain (coroutinize) defect, not fixable in the mc build recipe; the MC_DATADIR observation degrades rendering and is diagnostic only.

jdbrinton jdbrinton commented

Fixed in F160 (12dc436) — pass layer. The exclusion ledger's #11 "precisely-scoped" claim was the bug.

Root cause (from your shipped mc binary's disassembly, indices matching your trap frames exactly)

Not a publication gap and not a runtime accounting bug — and, decisively, g_locale_from_utf8 was NOT native: in /usr/bin/mc on dmitry/linux-6.12-glibc-bash-coreutils, function 836 (g_locale_from_utf8) and 786 (unix_error_string.resume) both exist with full .resume/.destroy siblings. The trap at 836 is the inlined __hwjs_set_parked set-over-set witness inside g_locale_from_utf8's flat ramp (it prints the violation via the inlined linux.syscall write, then hits its unreachable) — 836 is the victim; the culprit is one call earlier.

unix_error_string.resume state 0 reads, verbatim:

[0x1523344]==0 ?          ; inlined bracket-entry clear-assert — slot verified EMPTY
[0x1523872] = 0           ; the folded sync-arm's owner-reset, HOISTED ABOVE the call
call 454                  ; g_strerror — WHICH IS COROUTINIZED (455/.resume, 456/.destroy)
call 836 (retval, -1,0,0,0)  ; g_locale_from_utf8(<the g_strerror RAMP's return>, ...)

There is no binding take / xfer_descend / park on the g_strerror edge, while the g_locale_from_utf8 edge right beside it has the full bracket (owner-check against 1211, take, __hwjs_xfer_descend, state store). That residue is the signature of a post-pass -O2 fold, not a pass skip: GLib declares const gchar *g_strerror (gint) G_GNUC_CONST in gstrfuncs.h, so memory(none) rides the coroutinized definition and every header-borne call-site copy. The optimizer proved __hwjs_parked unchanged across the call, folded the post-call parked-check against the dominating clear-assert, and DCE'd the whole take/DESCEND arm. On the flat drive every ramp parks unconditionally at its initial suspend, so at runtime: g_strerror's ramp set the slot and returned its frame handle, unix_error_string ran on with the handle as the gchar* (handle-as-value corruption — your "unconditional, independent of the file" observation is exactly the initial-suspend park needing no I/O), and g_locale_from_utf8's ramp hit the never-silent set-over-set. This is the documented F133 exclusion of the F124 memory-effect clear (EXCLUSION-LEDGER #11) — whose "cost is precisely scoped to musl ls / | wc -wc" claim this ticket disproves: the class is every G_GNUC_CONST/pure transitively-suspending function in every GLib/glibc app (the fresh glibc-arm rebuild strips 72 such lies in the glibc closure alone: __strdup, _IO_setb, __getpagesize, regex internals, …).

Ruled out: publication gap (both functions coroutinized; the bracket was emitted — its folded residue is in the artifact); runtime accounting (the witness fired correctly, naming the orphaned handle; in the minimal repro the orphan's owner-word decodes to the g_strerror-analogue's self id).

Fix — F160, one owner, the F85-noreturn-strip twin

HwjsCoroutinize now runs a MEMORY-EFFECT LIE STRIP by default, at the same canonicalization vantage and with the same keying as the F85 noreturn strip (hwjs-suspends attr + Marked + the F150 xmod-import analog — never conversion status, so per-TU/published-decl builds are covered): marked-suspender defs AND decls are stripped to MemoryEffects::unknown() (+speculatable dropped — a speculated ramp call would create+park a frame no path consumes); direct call sites are swept module-wide through edgeCallee (aliases resolve, F87/F89); classified indirect may-suspend sites drop the call-site attr. This subsumes/retires the old opt-in F124 per-def clear; HWJS_CORO_MEMEFFECT_CLEAR is now the default and a no-op; the fix-disabling RED knob is HWJS_CORO_NO_MEMEFFECT_CLEAR (registered in wasmld's config attestation). Handled shapes: header-const/pure decls (this ticket's g_strerror), same-TU pure defs (the old wc btoc32), published decls in per-TU kernel builds, xmod suspend imports, alias-reached call sites, classified indirect sites.

RED → GREEN (non-vacuous, flipping)

Committed gate distros/linux-6.12-glibc-bash-coreutils/tools/run-hwjscc3-gstrerror-gate.mjs (+ gstrerror-const/ fixture) builds the exact mc shape — a __attribute__((const)) transitively-suspending callee published via a header from its own TU, called by a unix_error_string-shaped coroutinized caller that feeds the result into a second suspender — against the real whole-program glibc pool:

  • RED (proven against the true pre-fix F159 plugin 60d9527, and re-proven via the RED knob on F160): dies unconditionally (no would-block arm needed) with the EXACT ticket signature — hwjs-user-rt VIOLATION: set-over-set: parked slot not empty at a new park (orphaned handle … owner-word=<my_strerror's self id> …), RuntimeError: unreachable at the second callee's ramp, reached from <caller>.resume ← hwjs_resume ← hwjs_drive_loop ← _start.
  • GREEN (F160 default): MEMEFFECT-STRIP fn=my_strerror logged at the link, the callee's REAL string (not a frame handle) crosses the edge, correct output, exit 0, slot balanced across 3 genuine −517 parks, zero violations. 11/11 checks. pure_suspend_loop.mjs rewritten to the flipped default (IR-level: default ⇒ stripped, RED knob ⇒ memory(read) retained).

All-distros / regressions / live

One lineage (12dc436, dirty=0): provenance gate 6/6; pass suite 177/0 (7 dependency-skips, unchanged); kernel fully recompiled (884 TUs — forced, since Kbuild doesn't key objects on the plugin) with zero kernel-side strips and a byte-size-identical vmlinux (the F133 hang surface is untouched by construction); all three linux-* distros rebuilt green. Regressions on final artifacts: r6 4/4, full d1-dlopen family (M1–M3, dlopen70 churn, d43 reopen-fork, gates 7/7), glibc malloc-fatal #73, musl malloc-churn + regexp-churn #67, gqsort #2 (9/9), F158 dtor gate (in-suite). Live headless-Chromium (the ledger's own re-inclusion condition for the F133 exclusion): bash GREEN (boot→prompt 1.3 s, the suspected-hang scenario), busybox GREEN, dlopen70 GREEN. All three machines redeployed (brintos/linux-6.12-*).

SHAs

  • hwjs-cc F160: 12dc436 (joel-dev)
  • superproject advance: ffacc1d (joel-dev)

One honest remainder

This is a compile-time fix: the already-miscompiled /usr/bin/mc on dmitry/linux-6.12-glibc-bash-coreutils stays broken until mc(+GLib) is relinked with the F160 toolchain (the deployed lineage now produces a correct bracket for this shape — witnessed by the gate above, which is the faithful minimal mcview path per the closure criterion). No mc/GLib recipe exists in the superproject (BRINTOS-32 is your build), so please relink with the advanced pin; happy to assist if you point me at the recipe.

jdbrinton closed this as completed