# rsdos Debug Notes — heap/cells corruption family (v0.5.7) Handoff state: **HEAD = `ab633ce`**; the mystery below is SOLVED and the verify rework is committed on top (see git log). ## ✅ ROOT CAUSE + FIX (this session) — ExitBootServices retry wipes the free list **Root cause (confirmed by QEMU simulation + OptiPlex screen markers):** 1. `capture_memory_map` (main.rs, pre-EBS) → `init_heap` builds the free list from the raw map; `map_key` stashed. 2. `gop::init` → `SetMode` — on real hardware SetMode may allocate/reallocate memory, making `map_key` stale — then allocates the live display shadow (first heap alloc, `mem::forget`'d) and `vt::init` builds the cells grids. 3. `ExitBootServices(image_handle, map_key)` **fails** (stale key) → retry `refresh_and_exit` → `capture_memory_map` → **`init_heap` REBUILT the free list from the raw map, wiping every pre-EBS live allocation's state** — the shadow's 1.8 MB span and the vt grids became FREE while still live. 4. Post-EBS allocations carved **inside the live shadow** (the Opt's vt marker String at exactly `heap+0x443e0`, the family-A crash ptr); the display renders into the shadow → clobbers those objects → **zeroed back-pointers** (family A `ptr=0x404443e0 block=0x0`) and **gray text pixels** (family B `0xaaaaaa00aaaaaa` = 2× `0x00AAAAAA` = PALETTE[7] fg-7, NOT a firmware scrub). **Fix (committed):** `refresh_and_exit` now re-captures the map and returns a FRESH EBS key WITHOUT rebuilding the free list (`refresh_map_key`, the free list is the authoritative live-allocation state). Verified in QEMU with a forced EBS failure: `verify: ok`, `probe free=false`, `flhead in_shadow=false` — identical to the no-failure boot. Test hook: `memory::SIMULATE_EBS_FAILURE` (one-shot, defaults off). **OptiPlex markers that exposed it:** `probe@+0x443e0 free=YES`, `w0=203a7476 w1=78302063` (= "vt: c 0x" — the freed marker String), `blk=0x404443c0 sz=0x770`, `flhead=0x404443c0 in-shadow=YES`, `shadow =0x40400000 freed=no`, `fb=0xe0000000 in-heap=no`. ## TL;DR - Real-hardware (OptiPlex, no serial) boots crash in the shell with heap corruption. Two **distinct** bug families: - **(A) scroll-crash family**: in-heap `ptr=0x404443e0` with zeroed header. - **(B) 0xAA family**: `0xaaaaaa00aaaaaa` = two `0x00AAAAAA` u32s. - **CORRECTION (this session): `0x00AAAAAA` is NOT a UEFI firmware scrub. It is the VGA GRAY text colour: `rgb(170,170,170)` packs to `(170<<16)|(170<<8)|170 = 0x00AAAAAA`, which is `PALETTE[7]` = fg 7 — the vt's default foreground.** Every rendered glyph pixel is a `0x00AAAAAA` word in the shadow; the shadow's background is `0x00000000` (bg 0). - Family B `0xaaaaaa00aaaaaa` = **two adjacent gray TEXT pixels** — the memory read as heap held rendered text ⇒ it was **inside the live shadow** (see root cause above). - Family A zeros = the shadow's **black background** at the same span. - ⇒ BOTH families = the allocator handed out memory inside the live shadow after the EBS-retry free-list rebuild. - The pre-verify `0x70020` "mystery" was a debug artifact, not corruption. `BLANK = {ch:32, fg:7, bg:0}` = raw `0x70020`, and `vec![BLANK; rows*cols]` fills the whole grid with it at init. QEMU proves both grids are 100% BLANK at shell entry. (Note: 0x70020 ≠ 0x00AAAAAA — the two "fills" are cleanly distinguishable; the family-A/B dump content is TEXT/bg, not BLANK cells.) ## The two confirmed bugs 1. **(A) scroll-crash family** — heap allocator returns an in-heap block whose header is zeroed (`ptr=0x404443e0`). Dropping it panics in the guard. 2. **(B) 0xAA family** — freed garbage pointer `0xaaaaaa00aaaaaa`; the two `0x00AAAAAA` words are **gray text pixels** (PALETTE[7]) — the memory at that address was rendered shadow content. Something handed the allocator a pointer that was never a live allocation (or a live-shadow block that leaked into the free list). ## What we've fixed / proven already - **IST=1 stack collision → ROOT CAUSE of ACTIVE_VOL corruption**: vec 14 ran on `PF_STACK`, a 4 KiB `.bss` static overlapping FAT `ACTIVE_VOL`. Fixed in `gdt.rs`: heap-allocated 32 KiB IST stacks. (Done, earlier.) - **AHCI/DMA EXONERATED**: user removed all disks; crashes persisted. - **All QEMU repro attempts were clean** (mem/klog/tail-crash) until the content probe upgrade — which tripped the *content check only* on a healthy boot for the reason above (expectation bug + break bug). Fixed: the check now expects `blank_raw` (0x70020), records all four probe values, and adds a full-grid nz/blank summary line. QEMU now reports `verify: ok` on every boot. - **Clean heap geometry (OptiPlex)**: heap = `0x40400000..0xc7e34000` (~2.2 GB), boot stack ~1 MiB above heap end. - **Shadow geometry (QEMU)**: shadow = first alloc at `heap+0x20` (`0x1980020`), 480000 words → span `[heap+0x20, heap+0x1D4C20)`; cells data at `heap+0x1D4C40`; VecDatas at ~`heap+0x1D8630`. - **Shadow-rejection hypothesis (strengthened by QEMU forced-reject test)**: on the OptiPlex the shadow alloc may have been rejected (or its block freed), leaving that span free + text/zero-filled. `0x40444970 = heap+0x44970` is *inside* the would-be shadow span. Forcing rejection in QEMU reproduces family A exactly: `probe heap+0x443e0 IN free blk` with all-zero content. - **Original #GP resolved**: crashed image was RELEASE; drop of `Parsed` struct; cause = real memory corruption (laytest excluded compiler bug). - **Stale PDB trap**: fresh PDB re-resolved shell-entry #GP → `rsnix::vt::put_byte+0x16a` (`mov %ax,(%rdx,%rcx,4)`) — u16 char at 4-byte stride into cells buffer via garbage ptr from `r9=0x405d8630`. bt chain: `shell::run → GopWriter::write_str → fmt::Formatter::pad_integral → klog::LineBuf::write_str → core::fmt::write → Display(u32)`. - **Crash decoded**: the dropped object in the last boot crash was a 126-byte String (`cap 0x7e`) = the vt verify klog line (19-char prefix + 106-char text + `\n`); `ptr=0x40444970` is STALE (inside the would-be shadow span, back-pointer never written). ## Allocator layout (verified, `memory.rs`) - `HEADER_SIZE=16`, `MINI_SIZE=8`, `MIN_BLOCK=32`, `MIN_ALIGN=16` - payload = `align_up(block+24, align)` - split: rewrites rem header + `free_list_insert` + back-pointer at `payload-8` - `back_pointer_ok(ptr)` validates `[ptr-8]`; guard v0.5.7 (`0187b80`) reports + halts on failure; `corrupt` QEMU hook verified. ## vt grid facts - `put_cell(r,c,ch)` sets `ch` + fg/bg from statics `SGR_FG=7`/`SGR_BG=0`. - Cell raw encoding: `ch | fg<<16 | bg<<24`; BLANK = 0; cursor-inverse blank = `0x07000000` (bg and fg swapped). **`0x70020` is NOT a cursor-inverse cell** — it's a literal space with fg=7 (ch=0x20, fg=7, bg=0). - `flush()` diffs cells vs rendered; cursor pass writes inverse colors into both FB and `rendered`. - `term_println(s)` = `gop_print!` + blank line + explicit `serial::write_line`. Display mirror to serial is NOT automatic for `gop_println!`. - Boot order after vt init: shadow → boot markers → memory self-test → page allocator + self-tests → paging adopt + self-test → sched init → rtc/pci/ahci/fat/fs → **cursor spawn** → **`debug_verify()`** → `shell::run()` (first banner `rs-dos v0.5.7 …`, then `=====`, then crash-log read). ## Current committed work (post-`ab633ce`) - `gop::boot_marker_at(msg, row)` + `gop::text_rows()` + `gop::bottom_row(off)` — shadow marker now on the LAST text row, vt verify marker one above it (both survive the banner, prompt, and crash.log replay). - `debug_verify()` fixed: compares probes against `blank_raw` (0x70020), not zero; records all four probe values (no early break); probes BOTH cells and rendered; adds a full-grid `grids c nz=… blank=… r nz=… blank=…` summary (serial). Marker format: `vt: c {ptr:#x} cap={cap:#x} ok|GARBAGE | r {ptr:#x} ok|GARBAGE` at `bottom_row(1)`. - QEMU verified: `verify: ok` every boot; screendump shows the banner (gray), prompt, and both bottom markers. test-msg + test-fault PASS; debug and release builds 0 warnings. ## QEMU reproduction of the content-check failure (RESOLVED) - **0x00AAAAAA scan verdict (this session)**: `shadow scan: 11333w/480000` hits are all at **glyph positions of on-screen text** (fg=7 gray pixels). The "scrub" reading at the crash offset (`probe mem: 00000000 00aaaaaa 00aaaaaa 00000000 …`) is the heapdbg output line **rendered at that exact shadow location** — the probe reads its own text. The shadow far rows (r300, r500) read 0 ⇒ init zeroing is correct and nothing over-writes untouched shadow rows in QEMU. FB pixel at (171,1) = (170,170,170) GRAY = 0x00AAAAAA — shadow and FB agree; there is NO shadow/FB desync. - The shadow scan is still a useful health signal: untouched far rows must stay 0; text-row hits are expected (that's what the shadow holds). - Serial showed `probes c=[0x70020,0x0,0x0,0x0] r=[0x70020,0x0,0x0,0x0]` — this was **misread** as "a single stray cell". The full-grid dump (3700 reads) showed `nz=3700 blank=3700` in both grids: **every cell is BLANK (0x70020), the grids are healthy**. The probe array's 1–3 slots were zero-initialized leftovers from the early-`break` bug. - Memory dump proves grids are fully healthy on QEMU: cells block base `0x1b54c20` `{size=0x39f0, free=0}`, back-ptr written; rendered block base `0x1b58650`; both grids contain the shell banner post-draw. **No heap corruption on QEMU.** - QEMU verify geometry: cells ptr `0x1b54c40` cap `0xe74` len `0xe74`; rendered ptr `0x1b58670`. - ⇒ `CONTENT-GARBAGE` was never evidence of OptiPlex corruption — the check was comparing against the wrong expectation. Now: probes compare against `blank_raw` (0x70020); every probe value is logged; a `grids c nz=… blank=…` summary is the definitive health signal. ## The pre-verify `0x70020` at grid(0,0) — MYSTERY SOLVED **There was no writer.** `0x70020` is the grid's own init fill: `BLANK = Cell{ch:32, fg:7, bg:0}` encodes as `0x70020`, and `vt::init` builds both grids with `vec![BLANK; rows*cols]`. The old content check expected all-zero cells (wrong), and the probe loop's early `break` made probes 1–3 look like zeros. Nothing writes a pre-banner space; the shell banner (gray VGA-7 text, rows 0–1) and the `>` prompt (row 2) are the only vt writes before the markers. Also learned while verifying with a screendump: - The shell banner occupies grid rows **0 and 1** (motd + `=====` line), and the prompt sits at row 2 — so a marker at FB row 1 (the old shadow marker) is **clobbered** by the banner, and row 2's first glyphs get eaten by the prompt + cursor. Markers whose values matter now live at the **bottom** (`gop::bottom_row(0)` = last row, `bottom_row(1)` = second-to-last), which also survive a stale crash.log replay (~20 lines at grid rows 2+). - Banner text renders VGA gray `0xAAAAAA` (fg 7), boot markers white `0xFFFFFF` — a white-only pixel matcher misses the banner. QEMU screendump decode of the bottom rows: `vt: c 0x1b54c40 cap=0xe74 ok | r 0x1b58670 ok` and `gop: shadow 0x1980020 1.83 MiB`. **Excluded candidates (all still true, now moot):** cursor task (FB/shadow only), flush cursor-inverse pass (0x07000000 ≠ 0x70020), boot markers (direct FB), memory self-test, shell banner (runs after verify), backspace (BLANK). ## OptiPlex flash — what to look for (the linchpin) **FIRST OPTIPLEX REPORT (this session, previous image):** ``` heapdbg: probe@+0x443e0 free=YES w0=203a7476 w1=78302063 heapdbg: shadow=0x40400000 freed=no ``` - `w0=0x203a7476 w1=0x78302063` = LE ASCII **"vt: c 0x"** — the leftover bytes of the *vt verify marker String* (`debug_verify`'s `alloc::format!("vt: c {:#x} …")`), which was **allocated at exactly `heap+0x443e0`** (the historical family-A crash ptr) and freed before the marker ran. ⇒ on the Opt the heap's first-fit hands out **`heap+0x443e0` — INSIDE the live shadow span [`heap+0x20`, `heap+0x1D4C20`)** — while `shadow=0x40400000 freed=no` (the shadow Vec's block header claims allocated, forget'd). - QEMU control: same marker prints `probe free=false … flhead=0x1b706d0 in_shadow=false` — the probe address is NOT in the free list. - ⇒ The free list contains the live shadow span on the Opt. The two new boot lines (`fb … in-heap=YES/no`, `flhead … in-shadow=YES/no`) plus the probe's containing `blk=…` tell us whether the overlap is the whole heap block (shadow carve never happened / init_heap after alloc), the fb inside the heap (display clobbers), or a mid-span block. New image puts on screen (markers are **bottom-anchored** now — they survive the banner rows 0–1, the prompt row 2, AND a stale crash.log replay at rows 2+; use `gop::bottom_row(off)` to place any future ones): 1. **Last row (bottom_row(0))**: `heapdbg: probe@+0x443e0 free=YES|no w0=… w1=… blk={base} sz={size}` — crash offset free? content (zeros=shadow bg, `00aaaaaa`=gray text, ASCII=leftover string bytes)? and the **base/size of the free block containing it** (0x40400000 = whole-heap block → the shadow carve never happened; inside the shadow span → overlap; a normal small block → ordinary freed allocation). 2. **Second-to-last row (bottom_row(1))**: `heapdbg: shadow={ptr| -} freed=YES|no` — the shadow block base + header free flag. 3. **Third-to-last (bottom_row(2))**: `heapdbg: fb={base} size={size} in-heap=YES|no` — is the framebuffer inside the heap range (display clobbers heap)? 4. **Fourth-to-last (bottom_row(3))**: `heapdbg: flhead={base} size={size} in-shadow=YES|no` — does the free-list head overlap the shadow span? 5. Also: `vt: c {ptr} cap=… ok|GARBAGE | r {ptr} ok` marker (was bottom_row(1), now overwritten by the shadow line) — the cells ptr value is the linchpin for a stale-VecData theory: equals `0x40444970`? 6. If it crashes: the full guard band (`hdr:` + all four `mem:` lines, 16 stack words) plus the bottom markers if still visible. Serial-side (QEMU only): the boot heapdbg values are mirrored to serial (`heapdbg: probe free=… shadow=… freed=… fb_in_heap=… flhead=… in_shadow=…`) and captured in the klog ring — a healthy OptiPlex boot can also dump them with the `klog` shell command. ## Build/flash/test loop (traps) - Build 0 warnings, edition 2024, `no_std`, `x86_64-unknown-uefi`, workspace v0.5.7. Zero new deps. - Real-hardware boot chain: rsboot at `EFI/BOOT/BOOTX64.EFI` → kernel at `bzImage`. QEMU dev loop boots kernel as BOOTX64.EFI. - CRASHLOG stick must be FAT32; `mdir` grep gotcha: match `crash` case-insensitively. - **Stale qemu holds flock** → `pkill -9 -f 'qemu[-]system'` in a SEPARATE command; `rm -f esp/NvVars` between boots. - Use repo `scripts/` infra: test-msg/test-fault/test-mouse; monitor socket; HMP `mouse_move`. - Fault-context serial rule: never `serial_println!` in ISR/fault code; use `serial::write_raw`; guard report must not allocate (stack `FmtBuf`). - Marker on row 0 gets overwritten by the shell MOTD — use `boot_marker_at(msg, row>0)`. - Stale PDBs misname symbols — re-resolve with a fresh PDB per build (PDB math is decimal); `r9` = struct base with Vec layout `{ptr@0, cap@8, len@16}`. ## Next steps (priority order) 1. **Flash the fixed image to the OptiPlex** (BOOTX64.EFI in `esp/`). Expected bottom rows now: `probe@+0x443e0 free=no` (was YES), `flhead=… in-shadow=no` (was YES), `vt: … ok`. Run the old repro (`klog -t 2`, `ls`, `help`) — should no longer crash. 2. If the Opt still misbehaves: relay the four bottom lines again — the fix only holds if the free list no longer overlaps the shadow. 3. Reattach the window-manager milestone once confirmed. (QEMU control arm is green with and without a forced EBS failure: `verify: ok`, probe NOT free, shadow base NOT in free list, full shell sequence clean. `0x00AAAAAA` = gray text colour; shadow far rows stay 0.) --- ## 🔄 EHCI driver debug — in progress (`rsnix/src/ehci.rs`) ### Background The USB mouse attaches to the **EHCI** (USB2) controllers on the real OptiPlex (`00:1a.0`, `00:1d.0`), NOT the xHCI (`00:14.0` = USB3 only). A full EHCI driver was written in `rsnix/src/ehci.rs` (~26 KB), wired into `lib.rs`, `main.rs`, `cursor.rs`, `shell/src/lib.rs` (`usb` cmd). ### What works - PCI discovery, MMIO probe, HC reset, async+periodic list init. - Port reset + device detection: FS device found on port 0. - `ctl_xfer` builds SETUP/DATA/STATUS qtd chain; async list is circular (head → xfer_qh → head). - QEMU EHCI bench: async schedule processes our control QH correctly. ### The bug: QTD DMA reads return all-zeros The async schedule fetches our XFER_QH at `0x01b6e7c0` and reads its `next_qtd` field correctly (`0x01b6e7f0`). Then `ADVANCEQUEUE` sets `q->qtdaddr = 0x01b6e7f0`. But `FETCHQTD` reads from `0x01b6e7e0` (**16 bytes BEFORE** the expected QTD address) and gets all-zeros. ``` FETCHQH @ 0x01b6e7c0: next_qtd=0x01b6e7f0 ← correct ADVANCEQUEUE → qtdaddr=0x01b6e7f0 ← correct FETCHQTD @ 0x01b6e7e0: all zeros ← WRONG address + zeros ``` The QH's buffer pointer area (offset 28–44) is at `0x01b6e7dc–0x01b6e7ec`. `0x01b6e7e0` falls inside the QH's `bufptr[1]` field — which we zeroed. So the DMA is reading from inside our own QH structure, not the QTD that follows it. ### Root cause hypothesis: DMA address ≠ CPU address `dma_alloc` uses `alloc::alloc::alloc()` (the Rust global heap allocator). This returns a **virtual address**. On UEFI, the page tables from `adopt_tables()` copy the firmware's identity map — so virt == phys for most RAM. BUT: 1. The heap pages allocated by the page allocator may have been remapped or the identity map may not cover all heap pages. 2. The EHCI controller does DMA to/from the address we write to `ASYNCLISTADDR`. If the heap allocation's physical address differs from the virtual address the CPU sees, DMA reads will fetch zeros (unmapped or wrong page). **Evidence**: CPU can read back `*st.add(2) = 0x80e80` (the SETUP token) via `read_volatile`, but QEMU's EHCI DMA engine reads 0 from the same address. The CPU sees the data; the DMA engine does not. ### What's NOT the problem - **USBCMD bits**: Fixed (PSE=bit4, ASE=bit5, RUN=bit0). - **Async list linkage**: Fixed (head→xfer→head circular, Q_TYPE_QH tags). - **QH/QTD struct layout**: Matches QEMU's `EHCIqh`/`EHCIqtd` exactly. - **Periodic schedule interference**: Disabled (no PSE). OVMF's stale periodic entries caused crashes; now avoided. - **Build cache**: Full `cargo clean` done; binary matches source. - **Identity mapping**: `adopt_tables` copies UEFI page tables — should be identity-mapped. But the evidence says otherwise for our DMA buffers. ### Debug QEMU command ```bash qemu-system-x86_64 \ -bios /usr/share/edk2/x64/OVMF.4m.fd \ -drive file=fat:rw:esp,format=raw \ -serial file:/tmp/rsdos_q.log \ -monitor unix:/tmp/rsdos_mon.sock,server,nowait \ -m 256M -machine q35 -cpu qemu64 -no-reboot \ -device ich9-usb-ehci1 -device usb-mouse \ -trace "usb_ehci*" >/tmp/rsdos_trace.txt 2>&1 ``` ### Key QEMU EHCI facts - **Register layout**: `CAPLEN +0, HCSPARAMS +4, HCCPARAMS +8`; operational base = `base + CAPLEN`. `USBCMD=+0, USBSTS=+4, ASYNCLISTADDR=+0x18, CONFIGFLAG=+0x40, PORTSC#1=+0x44`. - **USBCMD bits**: `RUNSTOP=bit0, HCRESET=bit1, FLS=bits2-3 (2-bit), PSE=bit4, ASE=bit5, IAAD=bit6, ITC=bits16-23`. - **QEMU async BH**: triggered by USBCMD writes (bit-change detection) and frame timer. `ehci_work_bh` processes periodic frames first, then advances async schedule. - **QEMU strips FLS** if guest sets it (FCR register). - **Port reset**: deassert PR; PED stays 0 for FS (no HS speed mask). - **`ehci_find_queue_by_qh`** reuses queue objects; `q->seen > 1` stops re-processing the same QH (reclamation detection). ### Key QEMU EHCI structs (from `h110.h` / `hw/usb/hcd-ehci.h`) ```c // QH: 12 dwords (48 bytes) typedef struct EHCIqh { uint32_t next; // dword0: horizontal link pointer uint32_t epchar; // dword1: dev(0-6), i(7), ep(8-10), eps(12-13), dtc(14), h(15), mps(16-26) uint32_t epcap; // dword2 uint32_t current_qtd; // dword3 uint32_t next_qtd; // dword4 uint32_t altnext_qtd; // dword5 uint32_t token; // dword6: tbytes(16-30), cerr(10-11), pid(8-9), ioc(16), active(7), halt(6) uint32_t bufptr[5]; // dword7-11 } EHCIqh; // QTD: 8 dwords (32 bytes) typedef struct EHCIqtd { uint32_t next; // dword0 uint32_t altnext; // dword1 uint32_t token; // dword2: tbytes(16-30), cpage(12-15), cerr(10-11), pid(8-9), ioc(16), active(7) uint32_t bufptr[5]; // dword3-7 } EHCIqtd; #define NLPTR_GET(x) ((x) & ~0xf) #define NLPTR_TBIT(x) ((x) & 1) #define NLPTR_TYPE_GET(x) (((x) >> 1) & 3) #define Q_TYPE_QH 1 // NLPTR_TYPE value for QH links ``` ### What to try next 1. **Allocate DMA buffers from a known-safe region**: Instead of `alloc::alloc`, use a dedicated DMA pool at a known physical address (e.g., the first page allocator pages, or a statically allocated buffer). Verify the physical address matches the virtual address by reading the PML4/PDPT/PD/PT entries for the buffer's page. 2. **Add a page-table walk diagnostic**: Walk the 4-level page tables for the DMA buffer address and print the physical address. If virt ≠ phys, that's the bug. Add this to `ehci.rs` init or to `paging.rs`. 3. **Use the page allocator directly** (which returns physical addresses that are identity-mapped by construction) instead of the heap allocator. `alloc_pages(n)` in `memory.rs` returns a `*mut u8` — verify this is identity-mapped by checking a known page table entry. 4. **Check if the heap allocator's pages lose identity mapping**: The heap is built from the UEFI memory map. `adopt_tables` copies the firmware page tables. But if the heap allocator later allocates pages that were NOT in the firmware's map (e.g., from a free region), those pages might not have identity-mapped PTEs. 5. **Simpler test**: Allocate a small buffer (e.g., 64 bytes) with `dma_alloc`, write a known pattern, and check if the EHCI controller can DMA-read it. If even a small buffer fails, the problem is fundamental (identity mapping broken for heap pages). ### Key code locations - `rsnix/src/ehci.rs:751` — `dma_alloc()` (uses `alloc::alloc`) - `rsnix/src/ehci.rs:420` — `init()` (sets up ASYNCLISTADDR, head QH, XFER_QH) - `rsnix/src/ehci.rs:230` — `ctl_xfer()` (builds QH + QTD chain, enables schedule) - `rsnix/src/paging.rs:48` — `adopt_tables()` (copies UEFI page tables) - `rsnix/src/memory.rs` — `alloc_pages()` (page allocator) --- ## ✅ v0.5.10 — USB HID mouse driver (polled xHCI, `rsnix/src/usb.rs`) Handoff: HEAD = `v0.5.10` (workspace Cargo.toml + boot banner). Adds a polled xHCI controller driver (mouse-only) announced via `usb: xHCI at ` during boot; `cursor.rs` polls `usb::poll_mouse().or_else(ps2)` so a USB mouse supersedes PS/2. New shell cmd: `usb` (diagnose), `usb rescan`. ### How it works - Bring up controller (`OPC=0`, set `RCS`+`CRR`, `CNR`→0), discover a mouse asynchronously from the cursor task (`maybe_discover`), reset the port, enable slot, address device, read device + config descriptors, find the HID boot-mouse IN endpoint, SET_CONFIGURATION, configure EP, arm an IN TRB and drain transfer-completions into `ps2::MouseEvent`. - Kernel is identity-mapped + PIC-only (no MSI): DMA is plain kernel heap pointers; no IRQ — keep one IN report armed and drain on each poll. ### QEMU bring-up observations / gotchas that bit us - **MaxPorts = bits 31:24 of `HCSPARAMS1`**, not 16:23 (`MaxIntrs`). Using the wrong nibble silently yielded 0 ports ("ports=0", every PORTSC read 0). - **Operational registers** (xHCI >=1.1, matches QEMU/Linux): `CRCR=0x18`, `DCBAAP=0x30`, `CONFIG=0x38`. Wrong offsets made Enable-Slot never complete (only Port-Status-Change events → "Enable-Slot never completed"). - **`issue_cmd` must pass the input-context pointer** in TRB parameter (never drop it); Address-Device failed with code 5 (`CC_TRB_ERROR`) until `code5issue_cmd_ptr(d3, ptr)` passed `INCTX`, and - **Input Context is DW0=Drop, DW1=Add** (reverse of what looks natural). QEMU validates `ictl[0]==0 && ictl[1]==0x3`, else `CC_TRB_ERROR`. - **HID walker: `bInterfaceClass` is `+5` in the interface descriptor**, not `+2` (`cfg[i+5]`). A reverse-engineering of the mouse's config dump (`09 04 00 00 01 03 01 02 00 … 07 05 81 03 04 00 07`) made it obvious. - QEMU monitor **`mouse_button` help ("1=L 2=M 4=R") is wrong for the usb-mouse path**: the value is a direct HID bitmask — `1`=bit0 L, `2`=bit1 R, `4`=bit2 M (matches a real boot-mouse report byte). Bump monitors semantics: transport `mouse_move dx dy`; button via `mouse_button 1|2|4`. - A long-lived QEMU session sometimes stopped forwarding `mouse_move` after heavy use; a fresh relaunch always revived it — not a driver bug. - Diagnose cmd `usb` prints `xhci mmio=… mapped=… ports=… mouse=… slot=…` plus per-port `PORTS` dwords and a `hid ep-index=… mps=… armed=true` line. ### Boot log (good path) ``` usb: xHCI at 00:03.0 usb: controller up (8 ports) — mouse discovery deferred to cursor task usb: port 4 connect (speed 3) usb: dev 0627:0001 class=0x0 mps0=64 usb: HID mouse IN ep mps=4 interval=7 euid=3 usb: HID boot mouse armed (slot 1) ``` ### Known limitations - Mouse-only, no HID descriptor protocol selection (`SET_PROTOCOL` boot), no high-rate intervals, no keyboard/tablet, no hot-unplug handling, no MSI. ### EHCI (usb2) bring-up — QEMU quirks that bit us (all now fixed) - **QTDs must be 32-byte aligned.** QEMU's `NLPTR_GET(x) = x & 0xffffffe0` masks the LOW 5 bits, so a qtd at QH+0x30 (e.g. `0x01b6e7f0`) gets rounded down to `0x01b6e7e0` and the transfer executes at the wrong address (seen as a 16-byte jump in the FETCHQTD trace). Control qtds now live at QH+0x40/0x60/0x80; the HID report qtd at HID_QH+0x40. - **Unlink must self-loop WITH the QH type bit**: `link_of(head)` (addr|0x02). A bare `head as u32` self-loop is parsed by QEMU as an iTD → "non queue head request in async schedule" → HC reset. - **DATA qtd altnext must be `1` (terminate)**, not 0. On a short IN (e.g. config descriptor < 256 bytes) TBYTES stays != 0 so ADVANCEQUEUE follows altnext; QEMU treats altnext=0 as a *valid* address → jump to 0. - **Periodic HID QH must set the EP number (epchar bits 8-11).** The QH was built with devaddr|EPS|MPS but EP=0, so every periodic IN went to the control endpoint: the first transfer "completed" oddly, then the qtd HALTed every frame (`usb_ehci_qtd_bits` showed `halt 1`, packet on ep 0). Fix: `| ((epnum & 0x0f) << 8)`. - **Idle periodic qtd with correct EP just NAKs** (device has no report) — that is correct; don't mistake "no reports" for a broken schedule. - Headless QEMU 11 **cannot inject input into `usb-mouse`**: it has no `display=` property (only usb-tablet/usb-kbd do), so it is never bound to a display and `input-send-event`/`send-key` events (traced as `con -1`) never reach it. First-report delivery was verified with `usb-kbd` + `send-key` instead (b2 carried the real HID keycode, e.g. `0x04` for 'a'). - **Controller halt+reset before init** is required to clear OVMF's stale async schedule left in the controller. ### EHCI boot log (good path, usb-mouse) ``` ehci: USB2 EHCI at 00:04.0 ehci: mmio=0x81060000 caplen=0x20 ports=6 64-bit-caps=false ehci: controller up (6 ports) > ehci: port 0 resolved (full/low — TT path) ehci: dev 2706:0100 class=0x0 mps0=64 ehci: HID mouse IN ep1(0x81) mps=4 interval=7 ehci: HID boot mouse armed (device 1) ``` Periodic schedule: 2200+ EXECUTEs/frame with 0 qtd halts; idle mouse NAKs. --- ## 🔌 USB real-hardware status (OptiPlex 7010 / Q77) — v0.6.x rework ### Why USB "straight out did not work" 1. **EHCI was disabled in the shipped build.** `ehci.rs` had `static DEBUG_MATRIX: bool = true` (since `be868a4`): every boot ran the one-shot transfer-variant bisection matrix and then `return false` before enumerating. No committed build had ever brought an EHCI device up. 2. **No hub support.** On 7-series PCH the EHCI root port presents the **integrated Rate Matching Hub** (8087:0024, device class 9); FS/LS mice and keyboards sit *behind* it. The driver read the RMH's descriptors, found no HID endpoint and gave up. Split transactions were hardcoded to hub address 0 + root port (the QEMU topology). 3. **Only the first EHCI was initialized** (`find_ehci()` returned the first match). The 7010 has two: `00:1a.0` and `00:1d.0`. 4. **No `SET_PROTOCOL(boot)`** on EHCI/xHCI; UHCI asked for report protocol (`wValue=1`). Report protocol layouts are device-defined. 5. **EHCI DMA had no <4 GiB bound** (32-bit DMA pointers), while the page bitmap spans all RAM. A buffer above 4 GiB silently breaks transfers. 6. **xHCI**: no scratchpad buffers (HCSPARAMS2), `MaxSlotsEn=1`, and DMA from the heap allocator rather than physical pages. ### What the drivers do now - `ehci.rs`: both controllers; hub class requests (power / reset / status); TT splits with the hub address + child port; HID mouse **and** keyboard; boot protocol; per-device periodic QH (chained into the frame list); DMA from `alloc_pages_range(16 MiB, 4 GiB)`; 2 ms recovery after `SET_ADDRESS`. - `usb.rs` (xHCI): scratchpad buffers, `MaxSlotsEn = min(MaxSlots, 4)`, per-slot EP0 + interrupt rings, page-allocator DMA, boot protocol, mouse + keyboard. - `hid.rs`: shared HID boot-keyboard decoding (one scancode FIFO feeding `ps2::feed_scancode`), used by UHCI, EHCI and xHCI. - First-report lines are logged (`ehci: mouse report …`, `usb: kbd report …`) and the `usb` shell command lists controller(s) and device(s) with armed/fails/reports counters. ### OptiPlex 7010 — expected serial log (success path) ``` ehci: 00:1a.0 mmio=… ports=… 64-bit-caps=… ehci: USB legacy handed over by BIOS ehci: controller up (… ports) ehci: port N connected (high-speed) ehci: dev 8087:0024 class=0x9 … addr=1 ← rate matching hub ehci: hub addr=1 ports=8 ehci: hub port K: full-speed device (split) ehci: dev …:… class=0x0 … addr=2 sp=0 (split) ehci: HID mouse armed (addr 2 ep0x81 mps … ) ehci: periodic schedule enabled (1 device(s) on ctrl 0) ehci: mouse report dx=… dy=… b=0x… ``` Then the same on `00:1d.0` for the keyboard, and `> ` followed by typed keys. Failure signatures to report: - `ehci: no EHCI controller` — PCI probe/class mismatch (should not happen). - `ehci: USB legacy claim timed out` — BIOS still owns the controller. - `ehci: hub descriptor failed` / `hub port K reset timeout` — hub class request stage; note the code. - `ehci: get-dev-desc failed (addr 0 sp 0 tt Some(…))` — split transaction did not reach the child; note the TT tuple. - `ehci: no HID mouse/keyboard endpoint in config` — descriptor walk. - `ehci: xfer fail stage=…` — transfer engine; the QR band has the state. ### QEMU coverage (make test-usb) - EHCI root-port FS mouse/keyboard (split path), two devices chained. - xHCI mouse/keyboard on two slots. - Keyboard report → scancode → WM → shell echo is asserted in both cases. - **Not coverable in QEMU**: an EHCI high-speed hub (`usb-hub` is FS-only and refuses a HS EHCI root port), so the RMH code path is hardware-only. - **QEMU EHCI split limit**: for full/low-speed devices behind root-port split transactions QEMU delivers only the *first* report (125 NAKs after it, even though the qTD is re-armed). The re-arm shape is the standard one (`qh->next_qtd = qtd`, qTD re-activated; the HC owns the status/toggle overlay), so `test-usb` asserts one key end-to-end on EHCI and a full command on xHCI. ### Known gaps - EHCI: hub support is one level deep; no hotplug once a device of that kind is armed; no isochronous/other transfer types. - xHCI: no hub support; no USB3 warm-reset subtleties beyond the current port-reset sequence. - AHCI/PS2 behaviour unchanged (PS/2 remains the fallback input path). ### OptiPlex 7010 — first hardware QR decode The first hardware run booted to the shell with no USB input and displayed the EHCI failure QR: ``` EHCI fail stage=4 t=20ms sp=0 dev=0 in=true len=18 cmd=00000020 sts=0000b013 prtsc0=0x00001005 hp=0x00003000 qh1=48084000 qh2=40800000 nqtd=012080c0 setup=00080e80 data=80120d80 stat=80008c80 ``` - `sts` bit4 = **HSE (Host System Error)** plus HCH: the controller aborted the very first DMA pass after 20 ms, so this is a bus/DMA fault, not a protocol error. - `cmd=0x20`: RUNSTOP self-cleared, only ASE remains. - `qh1/qh2/qTD tokens` are structurally correct: control QH, DTC, MPS 8, split hub 0 / port 1, qTD active with TBYTES. - `prtsc0` PED=0: full-speed device on the root port (companion/TT path). **Root cause: 64-bit addressing capability.** The PCH EHCI advertises HCCPARAMS bit0, which extends every QH with `bufptr_hi[5]` (68 B) and every qTD with `bufptr_hi[5]` (52 B). The driver packed them 32-bit-style (48/32 B), so the HC read the *next qTD* as the overlay's high buffer pointer and DMA'd to a bogus 64-bit address -> HSE. QEMU reports `caps64=false`, so only hardware exposed this. **Fix:** always reserve the 64-bit layout (32-bit controllers ignore the tail): HID qTD at QH+0x80; control qTDs at +0x100/+0x140/+0x180 and the setup packet at +0x1C0. The failure QR now prints `c64=` and the DMA bases (async/xfer/setup/buf) so the next run is self-describing. ### OptiPlex 7010 — second hardware QR (after the 64-bit layout fix) ``` EHCI fail stage=1 t=20ms sp=0 dev=0 in=true len=18 c64=1 cmd=00000021 sts=00008003 prtsc0=0x00001005 hp=0x00003000 qh1=48084000 qh2=40800000 nqtd=012081c0 setup=00080e42 data=80120d80 stat=80008c80 dma a=01208000 x=01208080 s=01208240 b=0120b000 ``` - `sts=0x8003`: USBINT|USBERRINT|ASS — **no HSE**, the 64-bit QH/qTD layout is fixed and the controller runs. - `stage=1`: the **SETUP qTD halted**, `setup=0x00080e42` -> TBYTES 0, Halted, C_ERR still 3, no XactErr/Babble/DBE (a split that got no valid response). - `prtsc0=0x1005`: device connected + powered, **PED=0** (port never enabled), speed field reads full-speed. On Intel PCH the USB2 physical ports sit behind the integrated Rate Matching Hub, so a healthy root port shows the hub as a **high-speed** device (`PED=1`). PED=0 with a connected device means the port did not enable, and a full/low-speed split has no valid TT to talk to -> SETUP halts. Follow-up hardening in the driver: - 100 ms power-up settle (RMHs are slow) and stale port-change bits cleared before the reset walk - one reset retry after a settle - explicit log line when a connected port stays disabled - low-speed retry for a failed full-speed device (the PORTSC speed field is only trustworthy once the port is enabled) - persistent bottom-row summary: per controller `64b=`/`leg=` handoff result, every connected port's PORTSC, and the HID device count — hardware runs without serial now show the whole picture, not just the first QR ### OptiPlex — "dies at the prompt" (third run) Same QR (stage=1, PED=0), but now the machine never reaches the shell. Two boot-time hazards, both introduced/exposed by the hardware diagnostics: 1. **Count-based MMIO waits.** `reset_port` polled PED for 2,000,000 iterations and the controller reset used 10,000,000-iteration loops. On real silicon each iteration is an uncached MMIO read; with the new reset retry that is seconds per port. The boot-phase watchdog panics when task 0 keeps a fixed stack frame for 750 ticks (7.5 s), and its panic band is the only visible trace (serial carries the rest). 2. **Automatic re-scan every second.** While no HID was found the cursor task re-ran the entire EHCI scan (including port resets), so a rate matching hub never got a chance to settle. Fixes: all EHCI waits are wall-clock bounded (halt/reset/run 500 ms, PED 200 ms, control transfer 500 ms), a per-controller 3 s port-scan budget, no automatic re-scan (hotplug is explicit via `usb rescan`), a 300 ms power-up settle, and the boot watchdog window raised to 20 s. The xHCI controller also gets a persistent bottom-row summary. QEMU `make test-usb` is green again (the 2 M-iteration PED poll had also eaten the 2 s scan budget and skipped the keyboard).