AHCI (SATA) Block Driver

Status: Design doc, non-normative (trails CLAUDE.md). Records the AHCI backend for block-driver, the modern-hardware disk path.

1. Why AHCI

The ATA PIO backend (docs/persistence.md §5) works in QEMU's legacy IDE but the T630's SSD is AHCI-only - its firmware exposes no legacy IDE, so a read-only probe of ports 0x1F0/0x170 returned status 0xFF (no drive) on real hardware. AHCI is the standard SATA interface: it works on the T630 and in QEMU (ich9-ahci), so it is the portable, modern path.

AHCI replaces only block-driver's bottom half - how sectors move. The block IPC (ReadBlock/WriteBlock), the on-disk format, mkfs, fs, and reboot survival all live above the block layer and are device-independent.

2. Shape of the driver (DMA + MMIO)

Unlike ATA PIO (port I/O), AHCI is MMIO + DMA, exactly like the USB drivers:

  • The kernel finds the AHCI controller in the PCI scan (class 0x01, subclass 0x06, progif 0x01), records its ABAR (BAR5) + BDF (pci.rs), and at spawn maps the ABAR (HBA registers) into block-driver and grants it a small physically-contiguous DMA arena (task/mod.rs, the same path as xhci/ehci). The driver takes the window with gs::driver::registers and the arena with gs::driver::arena, laid out in four named parts (docs/driver-library.md step 3).
  • Per-port DMA structures the driver builds in its arena: a command list (32 command headers), a received-FIS area, and per-command a command table (a command FIS + a PRDT - physical region descriptors pointing at the data buffer). A transfer = build a Register-Host-to-Device FIS (READ/WRITE DMA EXT), point the PRDT at the data buffer, set the command-issue bit, wait for completion, check the task-file for errors.

3. IOMMU (H1)

AHCI is DMA-capable, so on a machine with an IOMMU it should be confined to its arena like xhci (§6.4). The T630 has a working IOMMU. The driver was brought up in passthrough first (get it correct), on the "earned confinement" rule the USB drivers follow (docs/iommu.md).

It is still in passthrough, and that is now a decision rather than a stage. The T630's BIOS hands the SATA controller over carrying a stale firmware DMA pointer, and confining the device makes the controller's first access fault - the same quirk that keeps ehci in passthrough. The kernel states it at the policy site: kernel/src/task/mod.rs, the confine flag on HwClass::Pci (read by iommu_confine), "Not ehci + block-driver, which keep a stale firmware DMA pointer that confinement would fault, so they stay in passthrough". xhci and audio-driver are the only confined in-tree drivers (both confined on the T630; CLAUDE.md 6.4, note 2026-10-08); an installed driver package given a PCI device is spawned asking for confinement too (services/supervisor/src/installed.rs). So block-driver is trust-critical on every machine, IOMMU or not, exactly as §6.4 says an unconfined DMA driver must be.

4. Build steps (incremental, against QEMU ich9-ahci)

Developed behind the block-driver/ahci cargo feature so the ATA PIO tests stay green during the migration; becomes the default (retiring ATA PIO + the IDE probe) once read/write/fs/reboot are verified on it. Test: osdev test blockdev.

  • Step A - detect + HBA init. ✅ done. Map ABAR, enable AHCI mode (GHC.AE), read CAP/VS/PI, enumerate implemented ports, report which carry a SATA disk (DET=3, sig 0x00000101). Verified: 6 ports, disks on ports 0/1.
  • Step B - port init + IDENTIFY. ✅ done (osdev test blockdev, AHCI.B). Stop the port (clear ST/FRE, wait CR/FR), plant the command list + received-FIS base in the arena, restart (FRE then ST), then issue IDENTIFY DEVICE via a command header + H2D Register FIS + single-PRDT command table, wait on PxCI, and parse the result. Verified: model QEMU HARDDISK, 131072 sectors (64 MiB). The full DMA command path (command list / FIS / PRDT / command-issue / completion) now works.
  • Step C - read. ✅ done (osdev test blockdev, AHCI.C). READ DMA EXT (0x25) via issue() into the PRDT data buffer; block IPC ReadBlock restored. Verified: fs mounts the AHCI disk over IPC (fs: mounted).
  • Step D - write + integrate. ✅ done (AHCI.D). WRITE DMA EXT (0x35) + FLUSH EXT (0xEA); the block-IPC serve loop runs in the AHCI backend; fs spawns and the full file round-trip works over AHCI (fs: file round-trip OK). The whole filesystem stack now runs on AHCI. Harness: boot on legacy IDE, the persist disk ALONE on ich9-ahci (→ port 0), mirroring the T630 (SSD is the only SATA disk).
  • Step E - promote. ✅ done. Confinement NOT taken. AHCI is now the only backend (the ahci cargo feature is gone; ATA PIO + the hw_pio capability + the IDE probe are retired - the T630's SSD is AHCI-only). The confinement half of this step was not completed and is not pending: block-driver is spawned hwclass::pci(0x01_06_01, 5, false) (the block-driver row of the supervisor spawn table) and stays in IOMMU passthrough for the firmware stale-pointer reason in §3. Its DMA is arena-resident in practice, but nothing enforces that, so block-driver stays trust-critical on every machine (§6.4). Verified: osdev test blockdev 3/3 + identity 23/23; unsafe audit back to 413/27 (the hw_pio arch wrappers + capability/hw_pio.rs + SDK pio.rs removed).
  • Step F - port recovery (COMRESET). ✅ done (f069c85, hardware-found by the million-round soak). A soak that kills block-driver mid-command can leave the port stuck BSY (the kernel clears PCI bus-master on kill before the in-flight command drains), and the soft port-restart of Step B cannot clear it - only re-running the SATA OOB sequence does. port_comreset (ahci.rs) does what a cold boot does: idle the port keeping FIS-receive armed (so the device's post-reset initial D2H FIS is captured and clears PxTFD.BSY, AHCI 10.1.2), pulse PxSCTL.DET 1 -> hold -> 0, wait for the PHY (PxSSTS.DET == 3), clear PxSERR/PxIS, restart the engine. init_port runs it on every (re)start; recover_port (the Phase H per-I/O retry path) escalates to it when a soft recovery leaves the port wedged, so a running driver self-heals a wedged port with no restart. This is what lets fs's mount-on-truth get a truthful answer: block-driver's reply reflects a settled controller, never a wedged-BSY false 0 (Commandment VIII).
  • Step G - CI-stuck escalation. ✅ done (2026-07-09, hardware-found on the T630 by a rapid double kill all-services). Step F escalated to COMRESET only on a stuck PxTFD.BSY. But a command against a nonsense LBA (an fs mount that derived a backup-superblock LBA from a garbage capacity - see docs/persistence.md §6.16) can wedge the controller with the command-issue bit PxCI stuck set and BSY clear. A soft recovery (clearing PxSERR/PxIS) cannot clear an owned PxCI - only a port COMRESET does - so the BSY-only gate never fired and the driver retried into the same stuck state forever. recover_port now escalates on PxTFD.BSY != 0 || PxCI != 0, covering both wedge shapes. The fs-side half of the fix (rejecting the mis-shaped capacity reply, and a bounded failure-truth exit from the mount loop) is docs/persistence.md §6.16; together they complete Commandment VIII in the storage stack. The live CI-stuck condition is T630-only - QEMU's emulated AHCI never raises it.

5. Register cheat-sheet

HBA (from ABAR): CAP 0x00, GHC 0x04 (bit31 AE), IS 0x08, PI 0x0C, VS 0x10. Port n (base 0x100 + n·0x80): PxCLB 0x00, PxFB 0x08, PxIS 0x10, PxIE 0x14, PxCMD 0x18 (bit0 ST, bit4 FRE, bit14 FR, bit15 CR), PxTFD 0x20, PxSIG 0x24, PxSSTS 0x28 (DET in 3:0), PxSCTL 0x2C (DET in 3:0, the COMRESET control), PxSERR 0x30, PxCI 0x38 (command-issue bitmask).