# DF-2491 — Divide-by-zero panic in CHS-mode `ata_tf_write` from device-controlled geometry

`sys/dev/disk/nata/ata-lowlevel.c:771-790` — verified on the audit QEMU guest.

## What the finding is

The CHS sub-path of `ata_tf_write()` takes `heads`/`sectors` straight from the
device-supplied ATA IDENTIFY words with **no zero check** and then executes

```
ata-lowlevel.c:783   ATA_IDX_OUTB(ch, ATA_SECTOR, (request->u.ata.lba % sectors)+1);
ata-lowlevel.c:785   ATA_IDX_OUTB(ch, ATA_CYL_LSB, lba / (sectors * heads));
ata-lowlevel.c:790   DRIVE head field = ((lba % (sectors*heads)) / sectors) & 0xf
```

`sectors==0` → x86 #DE at :783 (`lba % sectors`); `heads==0` → #DE at :785.
The sibling consumer of the *same* words, `ad_get_geometry()`
(`ata-disk.c:469-470`), guards with `&& current_heads && current_sectors`;
the taskfile writer does not. A device that forces CHS mode —
`ad_get_geometry` sets `ATA_D_USE_CHS` whenever `!ad_version(version_major)`
or `!lbasize` (`ata-disk.c:484-486`; `ad_version(0)==0`) — and reports a zero
geometry word panics the kernel on the **first I/O**, including the geom
partition-table probe.

**Threat model:** malicious/compromised ATA device (or any attacker who can
answer IDENTIFY — e.g. USB-UAS bridge firmware, fabricated channel behind a
hypervisor, malicious drive firmware) at first disk I/O. Local-denial /
malicious-device class (AV:P), same class as DF-2489.

## Files in this pack

| file | what it is |
|---|---|
| `dfrig2491.c` | the verification rig (KLD): verbatim `#include` of production `ata-lowlevel.c` (sha256-gated, 5 globals renamed) **plus** dispatch of the running kernel's OWN static `ata_tf_write` through the kernel's own `ata_controlcmd → ata_queue_request → ata_begin_transaction → ata_generic_command → hw.tf_write` chain, **plus** a full production funnel (kernel's own `ata_identify`/`ad_probe`/`ad_attach` with a crafted IDENTIFY, then first I/O) |
| `Makefile`, `build.sh`, `run.sh`, `cycle.sh` | exact build/run machinery |
| `build.log` | stock-tree rig build (checker sha equality + full compiler output) |
| `build.fixed-source.log` | rig build against the FIXED `/usr/src` (checker notes the sha change) |
| `run_control.log` | stock-kernel controls: go1 (CHS sane), go4 (LBA), go6 (verbatim copy), go8 (full funnel) — all pass, taskfile registers verified |
| `panic.go2_kernel_sectors0.txt` | **FATAL trap 18**, kernel's own `ata_tf_write+0x1f0`, `divq %r10,%eax` — IDENTIFY current_sectors=0 (:783) |
| `panic.go3_kernel_heads0.txt` | **FATAL trap 18**, `ata_tf_write+0x218`, `divq %rdi` — current_heads=0 (:785) |
| `panic.go5_verbatim_sectors0.txt` | **FATAL trap 18** in the module's verbatim #included copy (IP inside `dfrig2491.ko`) — byte-identity |
| `panic.go7_funnel_sectors0.txt` | **FATAL trap 18** — FULL production funnel: crafted IDENTIFY → kernel's own `ad_attach` SURVIVES (`ad0: 0MB <DF2491 ZEROGEOM DISK>`) → first I/O → trap at :783 |
| `panic.go9_funnel_heads0.txt` | **FATAL trap 18** — full funnel, current_heads=0 → :785 |
| `panic.go10_kernel_raw_sectors0.txt` | **FATAL trap 18** — raw-words path (no word-53 bit), raw sectors=0 → :783 |
| `ata_tf_write_trap_disasm.txt` | guest `objdump` of the stock kernel around both trap IPs; pins `+0x1f0` to the `(lba%sectors)+1` div (:783) and `+0x218` to the `lba/(sectors*heads)` div (:785) |
| `fix_build.log` | full fixed-kernel rebuild log (`make nativekernel` after `fix.diff` in guest `/usr/src`) |
| `fix_run.log` | **A/B validation on the fixed kernel**: every previously-fatal scenario SURVIVES with clamped/raw-fallback CHS registers; controls bit-identical |
| `fix.diff` | the verified fix (git-apply-able, never applied to the read-only `sys/`) |
| `env.txt` | guest uname / kernel sha256 / rig details |
| `manifest.json`, `verdict.json` | machine-readable verdict |

## Reproduce (host, from repo root)

```sh
# one fatal scenario per boot (2, 3, 5, 7, 9, 10); controls (1, 4, 6, 8) anytime
sh findings/poc/DF-2491/cycle.sh 7      # full-funnel :783 panic on stock kernel
dfbsd-qemu/vm.sh log 60                 # Fatal trap 18 ... ata_tf_write+0x1f0
```

Fix validation (already performed, logs in pack): apply `fix.diff` in the
guest `/usr/src`, `make nativekernel && make installkernel`, reboot, rerun
`run.sh 2 3 5 7 9 10` — all return `rc=0`, no panic, clamped CHS taskfile.

## Success criterion

Stock kernel + crafted device geometry in CHS mode ⇒ `Fatal trap 18: integer
divide fault` at `ata_tf_write+0x1f0`/`+0x218` (= `ata-lowlevel.c:783`/`:785`)
on first I/O. Fixed kernel ⇒ I/O proceeds with sane-clamped CHS taskfile;
sane-CHS and LBA devices bit-identical before/after the fix.
