# DF-2213 — PoC evidence pack (verify run, 2026-10-20)

Finding: `sys/dev/disk/nata/chipsets/ata-cyrix.c:98` —
`ata_cyrix_setmode()` programs the Cyrix 5530 PIO timing register with

```c
    ATA_OUTL(ch->r_io[ATA_BMCMD_PORT].res,
	     0x20 + (devno << 3), piotiming[mode & ATA_MODE_MASK]);
```

where `piotiming[]` has **five** elements (`ata-cyrix.c:67-68`) but the
ATA PIO mode constants start at `0x08` (`sys/sys/nata.h:203-207`:
`ATA_PIO0=0x08 .. ATA_PIO4=0x0c`) and `ATA_MODE_MASK` is `0x0f`
(`sys/sys/nata.h:200`).  For every real PIO mode the masked index is
therefore **8..12** — reading 3..7 elements (12..28 bytes) past the end
of the 20-byte table — and the garbage constant so read is written to
the controller PIO timing register.  In the shipped stock kernel the
elements at `piotiming[8..12]` are `dmatiming[2]` / a pad hole /
`udmatiming[0..2]` (`kernel_handler_disasm.txt`), so the standard boot
probe (setmode settles at `ATA_PIO4`) programs a **UDMA2 timing
(`0x00911030`) where a PIO4 timing (`0x00040010`) belongs**: wrong
setup/hold clocks on every PIO transfer — silent disk data-corruption
class.  Every sibling driver indexes the same shape of table with
`ata_mode2idx()` (`ata-pci.c:719-726`, the idiom that subtracts
`ATA_PIO0`; e.g. `ata-national.c:102`, the NatSemi clone of this very
driver — whose own table is 11 elements wide and padded for exactly
that idiom).

## How it was verified (production-handler KLD dispatch rig)

Hardware is gated (Cyrix 5530, `ATA_CYRIX_5530`), so the established
rig class was used (`dfrig_cyx.c`, same class as DF-2210 national,
DF-252x trm, DF-249x nata):

* the production `chipsets/ata-cyrix.c` is `#include`d **verbatim**
  into the KLD (`build.sh` pins sha256
  `29f548032b4104baeb4a3eb09009356bc80a0806323597580f5df3b6d041acd9`
  = the audited `sys/` tree; the only textual intervention renames the
  never-called `ata_cyrix_ident` to avoid a duplicate global with the
  built-in kernel copy — `ata_cyrix_setmode` itself is compiled
  byte-for-byte under its production name);
* the **running kernel's own** `ata_cyrix_setmode` (local symbol,
  `nm /boot/kernel/kernel` → `0xffffffff80319490` on the stock kernel)
  is dispatched by address — the shipped binary, zero recompilation;
* the fabricated state is exactly what a working Cyrix 5530 channel
  presents: `ctlr->r_res1` allocated (the `ata-cyrix.c:84` guard
  passes — *this* file guards its derefs, unlike the National clone of
  DF-2210), `ch->dma` present, disk present, PIO0..PIO4/UDMA2 identify
  data per scenario; the channel is in the `ATA_STALL_QUEUE` state with
  a `hw.begin_transaction` that reports the drive accepting SETXFER, so
  the kernel's own production `ata_limit_mode()`/`ata_controlcmd()`/
  `ata_queue_request()` run unmodified (synchronous CONTROL
  completion, `ata-queue.c:100-115`);
* the `ATA_OUTL` register writes dispatch through the real
  `bus_space_write_4()` onto a fabricated MEM-space bus tag + handle
  (`cpu/x86_64/include/bus_dma.h:404-412`: MEM tag ⇒
  `*(volatile u_int32_t *)(handle+offset) = value`), so the production
  timing writes land in the rig's 128-byte controller register file
  and are dumped — the garbage timing landing in the controller
  register, captured;
* the shipped kernel's `.rodata` `piotiming[]` base is recovered from
  the shipped handler's own disassembly (`build.sh` awk-extracts the
  absolute-indexed load `mov <base>(,%rax,4),%eax` of the PIO branch,
  `0xffffffff80d0cd20` stock) and the rig **cross-checks at runtime**
  that `[0..4]` equal the five source constants before every dump.

## Scenarios (`sysctl kern.dfrig_cyx.go=N`)

| go | handler | disk identify | request | stock kernel result |
|----|---------|---------------|---------|---------------------|
| 1  | running kernel own copy | PIO4/UDMA2 | ATA_PIO_MAX (boot-reinit first call, `ata-pci.c:863`) | settles PIO4 (0x0c) → **index 12 OOB** → reg 0x20 ← `0x00911030` (= `udmatiming[2]`, a UDMA2 timing); correct `piotiming[4]`=`0x00040010` |
| 2  | verbatim #included copy | PIO4/UDMA2 | ATA_PIO_MAX | identical to go=1 (byte-identity) |
| 3  | running kernel own copy | PIO0-only | ATA_PIO_MAX | settles PIO0 (0x08) → **index 8 OOB** → reg 0x20 ← `0x00002020` (= `dmatiming[2]`, a WDMA timing); correct `piotiming[0]`=`0x00009172` |
| 4  | full extracted call site (`ata-pci.c:855-866`) | PIO4/UDMA2 | ATA_UDMA2 (a UDMA disk at boot) | call 1: reg 0x20 ← `0x00911030` (OOB); call 2: reg 0x24 ← `0x00911030` (in-bounds `udmatiming[2]`, correct) — **both controller slots end up holding UDMA timings, the PIO one illegitimately** |
| 5  | running kernel own copy | WDMA2-only | ATA_WDMA2 | reg 0x24 ← `0x00002020` (in-bounds `dmatiming[2]`, **correct**) — the WDMA branch indexes correctly; the defect is specific to the PIO branch (line 98) |

Every OOB value is cross-checked against live kernel memory at
`piotiming + 4*index` (`OOB read CONFIRMED` lines in `run.log`).

## Build / run (guest, root)

```
cd /root/dfrig_cyx && sh build.sh     # KLD with verbatim production file
kldload ./dfrig_cyx.ko
KADDR=$(nm /boot/kernel/kernel | awk '/ t ata_cyrix_setmode$/{print $1}')
TBL=$(objdump -d --start-address=0x$KADDR --stop-address=$((0x$KADDR + 0x200)) \
      /boot/kernel/kernel | awk '$2=="8b" && $3=="04" && $4=="85" {b=$8$7$6$5}
                                END {print "0xffffffff" b}')
sysctl -w kern.dfrig_cyx.addr=0x$KADDR
sysctl -w kern.dfrig_cyx.tbl=$TBL
sysctl -w kern.dfrig_cyx.go=1         # ... 2 3 4 5
```

(run.sh does all of this; output lands on the console / dmesg.)

## Fix validation

`fix.diff` replaces the raw mask with the family idiom:
`piotiming[ata_mode2idx(mode)]` (matching `ata-national.c:102` and
every other sibling).  Single-fix kernel built with
`make -j6 nativekernel KERNCONF=X86_64_GENERIC` (`fix_build.log`,
kernel `#1: Tue Oct 20 22:39:26 UTC 2026`, sha256
`bfecc68ad512f8cbfbb2f1447b52411a17f435f891b47a21b1569c3a3abc4fc2`);
the exact same rig re-built from the patched source and re-run
(`fix_run.log`):

| register | stock kernel | fixed kernel |
|---|---|---|
| go1/go2/go4 PIO slot 0x20 | `0x00911030` (OOB UDMA2 constant) | **`0x00040010` = `piotiming[4]`** ✓ |
| go3 PIO slot 0x20 | `0x00002020` (OOB WDMA constant) | **`0x00009172` = `piotiming[0]`** ✓ |
| go4 UDMA slot 0x24 | `0x00911030` (in-bounds) | unchanged ✓ |
| go5 WDMA slot 0x24 | `0x00002020` (in-bounds) | unchanged ✓ |
| settled modes / SETXFER count / dma override 16+32768 | — | identical ✓ |

The fixed binary shows `callq ata_mode2idx` feeding the table load
(`fixed_handler_disasm.txt`), versus the stock `and $0xf,%eax`
(`kernel_handler_disasm.txt`).

Note on reading `fix_run.log`: the rig prints the *hypothetical* raw
mask index (`INDEX USED = mode & ATA_MODE_MASK`) as a static annotation
in both runs; on the fixed kernel the handler no longer uses it (the
register value and the disasm prove it), and the `(MISMATCH vs reg
0x20?!)` cross-check line is the *expected* fixed-kernel outcome — the
register no longer equals the OOB slot because it now carries the
in-bounds constant.

## Impact classification

`.rodata` OOB **read** whose value lands in a hardware register: no
kernel memory is disclosed to userspace (the value is a compile-time
constant of the shipped kernel), no write primitive beyond the fixed
controller register, no control flow influence — **not an info leak
and not a privesc** (the team-directive question is closed explicitly:
there is no route to uid=0).  The impact is a wrong PIO timing
programmed on real Cyrix 5530 hardware on every probe (setmode settles
≥ PIO0) — a data-integrity/reliability defect (silent disk corruption
class), persisting Low severity.

## Artifacts

* `dfrig_cyx.c` / `Makefile` / `build.sh` / `run.sh` — the rig
* `build.log` — full untrimmed baseline KLD build (checker sha lines)
* `build.patched.log` — rig rebuild from the patched source (checker
  notes the fixed sha)
* `run.log` — all five scenarios on the stock kernel (console/dmesg)
* `dmesg_full.txt` — full guest dmesg of the baseline session
* `kernel_handler_disasm.txt` — shipped handler disasm + `.rodata`
  window (tables and their absolute addresses)
* `fixed_handler_disasm.txt` — fixed handler disasm (`callq
  ata_mode2idx` feeding the PIO table load)
* `fix.diff` — git-apply-able one-line fix
* `fix_build.log` — full untrimmed fixed-kernel build
* `fix_run.log` — the five scenarios on the fixed kernel
* `env.txt` / `fix_env.txt` — guest environment (stock / fixed)
* `VERDICT.md` — full narrative
* `manifest.json` / `verdict.json` — machine-readable verdicts
