wasm-emu: rule out store redispatch path; document data-race conclusion (E17)

?yieldtrace shows the crash occurs with zero compiled-TB DONE_FLAG resume
re-entries, so it is not the store block-replay path. Combined with
nondeterminism and identity-independence, the corruption is a guest-RAM data
race with concurrent device/timer callbacks. Blanket store-TCI stays default.
This commit is contained in:
2026-07-22 21:43:12 -04:00
parent 9b8c6ba925
commit c608b65ef5
3 changed files with 38 additions and 1 deletions
+36
View File
@@ -501,3 +501,39 @@ consistent with a torn/stale store of a pointer-sized value under a race.
`?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.
## E17: not the store block-replay/redispatch path
Added opt-in `?yieldtrace`: the dispatch loop counts compiled-TB "yield/resume"
re-entries, defined as a compiled TB returning non-zero with the *same* `tb_ptr`
and `do_init == 0` (the `DONE_FLAG`-driven redispatch/resume path; chains set
`do_init = 1`, completion sets `tb_ptr = 0`).
Result over three crashing `?nostoretci` runs: **all crashed at `0x4201e210`,
zero yield re-entries**. So the store-specific block-replay-via-redispatch
mechanism (upstream's stated reason for blanket store-TCI) does not fire during
this workload. That path is not the trigger here.
Caveat: Asyncify-based unwinds (coroutine yields) restore the whole C stack and
are transparent to this counter, so a coroutine-mediated replay is not excluded.
But the store `DONE_FLAG` redispatch path is definitively ruled out.
### Consolidated conclusion
The `0x4201e210` corruption is:
- nondeterministic (same config crashes/passes across runs),
- independent of which specific store TBs are compiled,
- gated only by having enough compiled stores of both widths (a timing knob),
- not the store `DONE_FLAG` redispatch/replay path.
The consistent remaining explanation is a **data race on guest RAM** between the
CPU worker (compiled `i64.storeN`) and another agent that runs concurrently —
main-thread `QEMUTimer`/device-model callbacks (SPI/SD/GDMA/cache) reading or
writing guest DRAM. TCI's C store path masks it by changing timing/serialization,
not by being semantically different. This matches upstream qemu-wasm shipping
blanket store-TCI rather than a targeted fix.
A real fix is a threading-correctness project: ensure guest-RAM accesses from
device/timer callbacks are properly serialized against vCPU stores (BQL/memory
barriers), or run device memory access on the vCPU thread. Opt-in repro/debug
controls now available: `?nostoretci`, `?sthash`, `?stcw/stcmask/stcval`,
`?yieldtrace`. Production default remains blanket store-TCI.
+1
View File
@@ -493,6 +493,7 @@ 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('yieldtrace')) options._wasm_enable_yield_trace();
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');