summaryrefslogtreecommitdiffstats
path: root/DEBUG_NOTES.md
blob: b72a979fbfd448d6bcdc87bd8f09a00c533c85d8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
# 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
<bus.dev.fn>` 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).