The file browser no longer auto-seeds, so the headless test seeds Test/example.txt into OPFS before autoboot. Docs corrected: store-containing-TB TCI is upstream qemu-wasm design (compiled store path is not rewind-safe), not a port bug.
7.7 KiB
WASM-only reader panic in uzlib inflate
Living record of the investigation into the browser-only crash when opening a book. Native QEMU (same submodule) does not crash, so this is a wasm32 TCG backend bug. Update as things are ruled in/out.
Symptom
Opening a book in the browser panics; native opens the same book fine.
Invalid read at addr 0x3FCB7999, size 1, region '(null)' size 0x0 owner '(none)' ram=0, reason: rejected
Guru Meditation Error: Core 0 panic'ed (Load access fault). Exception was unhandled.
MEPC : 0x4201e210 RA : 0x4201e18c SP : 0x3fcb3810
Root cause (established)
wasm_tci_only_tb = true in tcg_out_qemu_st is upstream qemu-wasm's own
design, not a workaround this project invented. A full diff of our
tcg/wasm32/tcg-target.c.inc against pristine upstream qemu-wasm shows the two
files are identical except for that one line. Upstream unconditionally forces
every store-containing TB to the TCI interpreter, so upstream's compiled
(JIT) store path is emitted but never executed — it is effectively unfinished,
untested dead code, and it corrupts guest state the moment it runs.
Mechanism: the wasm32 backend compiles a TB as a chain of wasm blocks guarded
by if (BLOCK_PTR <= block_idx). When a softmmu slow-path helper yields
(asyncify unwind for I/O / interrupt), the wasm function returns and re-enters,
skipping already-executed blocks via BLOCK_PTR. This restart is safe to
replay for loads but not for a compiled store, so upstream punts all store TBs
to TCI (which has precise instruction-level checkpointing).
The real fix is therefore not a port bug hunt but completing upstream's compiled store path to be rewind-safe (e.g. defer the memory commit past the TB's yield points / buffer stores until block commit), then dropping the blanket TCI. Large TCG-backend task; ~15 min per browser iteration; no native repro.
Established facts
- Fault site (via same-version local ELF addr2line, not byte-identical):
uzlib_uncompress(tinflate.c:613) ←tinf_inflate_block_data(tinflate.c:476) ←InflateReader::read←FontDecompressor::decompressGroup←prewarmCache. "Indexing" = the reader inflating compressed glyph groups from flash into amalloc'd temp buffer. - Input is static flash data, independent of SD. The compressed font bank is in the firmware image; the SD file only selects which glyph groups get prewarmed (the characters present in the text).
- Native renders the reader ("chapter one") with no fault — proven with
_scratch/native-reader-repro.py+ QMP screendumps. Confirms the firmware, the font data, and the decompression logic are all correct. - Fault address is non-deterministic:
0x3FCB7999and0x3FCB7997across runs. A static machine-map boundary would fault at a fixed address. Drift ⇒ a computed pointer is slightly wrong, i.e. runtime/codegen, not the memory map. - Rejected region is bogus:
region '(null)' size 0x0 owner '(none)' ram=0. The address lands in mapped DRAM (0x3fc80000+0x60000) but the wasm softmmu TLB routed it to the IO slow path with a zero-size section. Hallmark of a bad address/TLB result from a JIT-compiled TB. - Menus work. Home/file-browser render crisp text through the same font system and the same TLB fast path, so the TLB codegen is not broadly broken — the bug is pattern/data specific in a hot, JIT-promoted TB.
- User observation: firmware-written crash logs open fine; prepopulated files do not. Consistent with data-dependence — a crash log's character set prewarms different glyph groups than lorem-ipsum, avoiding the group whose compressed byte pattern trips the miscompiled path.
Architecture notes (why JIT is implicated)
tcg/wasm32.c: TBs run on the TCI interpreter until executedINSTANTIATE_NUMtimes, then get compiled to native WASM (instantiate_wasm) and run as added table functions. The inflate hot loop gets promoted.WASM_TCI_ONLY_ENTRYforces a TB to stay on TCI forever;wasm_tci_only_tb(per-TB, reset each translation) requests that.- Dynamic promotion must stay on for usable performance (all-TCI starves the guest watchdog), so a permanent fix must correct codegen, not disable JIT.
Hypotheses
| # | Hypothesis | Status |
|---|---|---|
| H1 | A store-containing TB is miscompiled (the cleanup removed wasm_tci_only_tb = true from tcg_out_qemu_st); the compiled TB corrupts a pointer a later load dereferences |
confirmed (E1) |
| H2 | Compiled load codegen bug (tcg_wasm_out_qemu_ld / _direct) |
untested |
| H3 | TLB fast-path codegen edge case (tcg_wasm_out_tlb_load) hit only by certain access patterns |
untested |
| H4 | asyncify/fiber unwind corrupts vCPU state mid-inflate | unlikely (inflate makes no async import calls) |
| H5 | mimalloc / linear-memory / threads issue | unlikely (native heap layout differs, no crash) |
Experiments
E1 — force store-containing TBs to TCI (tests H1) ✅ CONFIRMED
Re-added wasm_tci_only_tb = true; to tcg_out_qemu_st in
tcg/wasm32/tcg-target.c.inc.
- Result: panic gone.
make browser-testreaches the reader and renders a correct page (black lorem-ipsum on white, footer80% example 1/3 33%). No reject, noGuru Meditation, 1 boot, 0 watchdog panics. New refresh frames (dtm2_white=0.891) appear past the old0.960stall. - Conclusion: the compiled
qemu_stcodegen is the bug. Forcing store TBs to TCI is a working workaround, not the fix — every store-containing TB then runs interpreted (broad perf cost; prior attempts reported watchdog starvation under sustained load, though this finite-burst run was clean). - The reader first paints a correct BW page (black text on white), then a
follow-up grayscale refresh pass inverts it into muddled white-on-black
text. That second issue is real and still open (see ISSUES.md "Reader renders
inverted grayscale"); it is independent of this panic fix. The
browser-testwait-for-stable happened to screenshot the initial white page.
Caveat on E1's specificity
Forcing store-containing TBs to TCI drops the whole TB (its loads, stores,
and arithmetic) to the interpreter. So the bug is in a TB that also has a
store, not provably the store op. tcg_wasm_out_qemu_st_direct and
tcg_wasm_out_qemu_ld_direct are byte-for-byte symmetric and look correct in
isolation, which points away from the direct emit and toward the store path's
surrounding block/rewind/helper scaffolding in tcg_wasm_out_qemu_st, or an
interaction with a co-located op.
Next options
- Ship E1 as a guarded workaround — reader works now; measure real navigation/page-turn perf before judging it unacceptable.
- Localize precisely — bisect by making the TCI trigger conditional (store width; presence of the miss/helper path; TB size) to shrink the suspect set, then read the generated wasm for that TB.
Diagnostics currently in the tree (uncommitted)
system/memory.c: reject log prints region size/owner/ram to identify the faulting region.hw/display/xteink_x3_eink.c:[eink] refreshpopcount trace (per-plane white ratio) — also used for the separate grayscale-rendering issue.
Reproduction
- WASM:
make browser-test(the test does 4 confirm presses: Browse Files → Test → highlight → Open). Panic appears in_scratch/browser-raw-sd.log. - Native (control, no panic):
nix develop -c python3 _scratch/native-reader-repro.py, then inspect_scratch/native-step4-reader.ppm.
Ruled out
- Not SD / filesystem: fixed separately (raw FAT32, SSI-SD token, CMD13); the file reads correctly and "Indexing" begins.
- Not firmware / font data: native decompresses and renders identical input.
- Not the machine memory map: fault address drifts; native map is the same.