wasm-emu: document compiled-store corruption as a nondeterministic race

E15/E16: runtime store-gating bisection plus repeated-run testing show the
0x4201e210 reader-open corruption is a race (identity-independent, config
alternates crash/pass), not a miscompiled TB. Blanket store-TCI suppresses it
in practice. Wire ?sthash and ?stcw/stcmask/stcval opt-in debug controls.
This commit is contained in:
2026-07-22 20:57:01 -04:00
parent fc2ec8a6ae
commit 9b8c6ba925
4 changed files with 58 additions and 3 deletions
+2 -2
View File
@@ -12,9 +12,9 @@ Opening `/Test/example.txt` through QEMU `vvfat` could corrupt coroutine/guest s
Opening a book panics **only in WASM** (`Invalid read ... reason: rejected` → Load access fault) inside `uzlib_uncompress` during the reader's font `prewarmCache` **if** compiled guest stores are enabled. Native QEMU from the same submodule renders fine.
Root cause (confirmed by full diff against pristine upstream): `wasm_tci_only_tb = true` in `tcg_out_qemu_st` is **upstream qemu-wasm's own design**. Upstream forces every store-containing TB to the TCI interpreter because its compiled restartable-block store path is not rewind-safe (a yielded softmmu helper replays the block; safe for loads, corrupting for stores). Font inflate is store-heavy, so that hot loop never JIT-promotes — a real but secondary perf ceiling.
Root cause (updated, E15/E16): the corruption is a **nondeterministic race**, not a deterministic miscompilation. Per-TB compiled store codegen is bit-identical to TCI (E10/E11, 300k+ comparisons). Runtime store-gating bisection showed the crash does not depend on any specific store TB's identity — only on enough compiled stores of *both* widths running — and the same fixed config alternates CRASH/PASS across runs. Prime suspect is shared-memory ordering / concurrency between the compiled store path and another agent touching guest RAM (IO-thread device models, dirty/notdirty handling, or the Asyncify store slow-path `DONE_FLAG`/`UNWINDING` re-entrancy). `wasm_tci_only_tb = true` in `tcg_out_qemu_st` (upstream qemu-wasm's blanket store-TCI) suppresses the race in practice: 0 corruption crashes over 4 clean runs (a 5th failure was an unrelated cold-boot watchdog flake).
Remaining fix is to make compiled stores rewind-safe and drop the blanket TCI — a substantial TCG-backend task, ~15 min per browser iteration, no native repro. Full analysis: `docs/wasm-inflate-panic.md`.
Remaining fix is to eliminate the race so compiled stores are safe and the blanket TCI can be dropped — a TCG-backend/threading task, ~15 min per browser iteration, no native repro. Opt-in debug tools: `?nostoretci` (all stores compiled, reproduces the race), `?sthash`, `?stcw/stcmask/stcval`. Full analysis: `docs/wasm-inflate-panic.md`.
## Resolved: `-icount` throttled book open ~4.7x
+53
View File
@@ -448,3 +448,56 @@ The narrowed fallback is a little faster when it works, but it is not reliable
enough: one run failed before controls enabled with an interrupt watchdog. The
default is restored to the upstream blanket store-TCI fallback. Keep the
32-bit-only result as a useful diagnostic, not a production fix.
## E15/E16: the corruption is a nondeterministic race, not a miscompiled TB
Added runtime-settable, opt-in store gating to isolate the culprit without
rebuilds:
- `?sthash=1|2` — compile one hash-parity half of *all* store TBs (both widths
present in the compiled half), interpret the other.
- `?stcw=<bytes>&stcmask=<m>&stcval=<v>` — keep every width compiled except the
selected width `stcw`, which compiles only when `(hash(pc) & m) == v`. Intended
for single-width binary search; logs `STBISECT compile pc=... w=...`.
### Attempted bisection and what it actually showed
- `sthash=1` (compile odd-hash half) -> crash; `sthash=2` (even half) -> pass.
Initial read: a specific pair of TBs.
- Width bisection contradicted that: with all other widths compiled, **both**
8-bit halves crash and **both** 32-bit halves crash. Identity of the compiled
store TBs does not matter; only that enough of them run.
### Decisive test: determinism
Re-running a single fixed config exposes nondeterminism:
```text
sthash=2 run 1 -> CRASH
sthash=2 run 2 -> PASS
sthash=2 run 3 -> CRASH
```
So the earlier single-run "pass" results (including the width fallbacks and the
first `sthash=2`) were probabilistic, not fixes. **The compiled-store corruption
is a nondeterministic race**, not a deterministic miscompilation of any specific
TB. E10/E11 (per-TB compiled == TCI, 300k+ comparisons) are fully consistent:
each TB is individually correct; the fault is emergent timing/ordering.
### Reliability of the production default
Blanket store-TCI over 5 runs: 4 clean reader-opens, 1 failure — and that 1 was
a **cold-boot interrupt-watchdog timeout** (`MEPC 0x4219a37e`), a separate
host-scheduling boot flake, not the `0x4201e210` corruption. So blanket
store-TCI shows 0 corruption crashes here; it suppresses the race in practice
but the underlying race remains.
### Revised suspect
Prime suspect is now shared-memory ordering / concurrency between the compiled
guest-store path and another agent touching guest RAM (IO/main thread device
models, dirty-bit/notdirty handling, or the Asyncify store slow-path
`DONE_FLAG`/`UNWINDING` re-entrancy flagged in E9), rather than store codegen.
The crash is a corrupted *pointer* (invalid read at `0x3FCB80xx`, region null),
consistent with a torn/stale store of a pointer-sized value under a race.
### Instrumentation status
`?sthash` and `?stcw/stcmask/stcval` are opt-in debug tools; the default build
path is unchanged (blanket store-TCI). The narrowed 32-bit-only fallback remains
rejected as a default because it only lowers, not removes, the race probability.
+2
View File
@@ -493,6 +493,8 @@ async function boot(file, variant, setStatus) {
} else if (params.has('tcgdiff2')) options._wasm_diff_enable2();
else if (params.has('tcgdiff')) options._wasm_diff_enable();
if (params.has('nostoretci')) options._wasm_disable_store_tci();
if (params.has('sthash')) options._wasm_set_store_tci_hash(Number(params.get('sthash')) || 1);
if (params.has('stcmask')) options._wasm_set_store_bisect(Number(params.get('stcw')) || 0, Number(params.get('stcmask')) >>> 0, (Number(params.get('stcval')) || 0) >>> 0);
options.FS.mkdirTree('/bios');
options.FS.mkdirTree('/var/tmp');
options.FS.writeFile('/bios/esp32c3-rom.bin', rom);