# DF-2489 — VERDICT

**Finding:** `sys/dev/disk/nata/ata-disk.c:147` and `:540` —
`ad_get_geometry()` copies `adp->heads`/`adp->sectors` raw from the
device-supplied ATA IDENTIFY words (word 3 / word 6, else-branch at
:476-480 — the `current_*` branch at :469-475 is guarded, the raw branch
is not); a device reporting heads=0 or sectors=0 makes `ad_attach()`
compute `total_secs/(heads*sectors)` → x86 #DE, **Fatal trap 18:
integer divide fault**, at attach (boot or hot-plug). The identical
divisor is re-evaluated in the bootverbose path of `ad_describe():540`.

**Verdict: REPRODUCED (panic, 4 variants, incl. the shipped kernel
binary), then FIX VALIDATED.** Panic-class DoS at device attach —
**not** an escalation (closed explicitly below).

## 1. Root cause confirmed in source

* `ata-disk.c:477-478` (`ad_get_geometry` else-branch): raw copy of
  `param.heads`/`param.sectors` (u_int16_t IDENTIFY words 3/6) into the
  `u_int8_t` ad_softc fields — no lower bound. The branch is taken
  whenever word 53 (`atavalid`) lacks `ATA_FLAG_54_58` or
  `current_heads`/`current_sectors` are 0.
* Nothing upstream validates the geometry: `ata_getparam()`
  (`ata-all.c:590-663`) copies IDENTIFY verbatim into `atadev->param`
  (le16toh + string bswap only; sole check = printable model byte),
  `ata_identify()` (`ata-all.c:683-732`) creates the child and calls
  `bus_generic_attach` → `ad_probe` (config word only) → `ad_attach`.
* `ad_attach:147`: `info.d_ncylinders =
  adp->total_secs/(adp->heads*adp->sectors)` — divisor `heads*sectors`
  == 0 → #DE. `ad_describe:540` (bootverbose): identical expression.
* With a nonzero `lba_size` (words 60/61) the dividend is nonzero
  (`:489-490` replaces total_secs) — a crafted device trivially arranges
  `X/0`; `0/0` traps identically.

## 2. Rig (nata verbatim-interposition class — DF-2210/DF-249x shape)

`dfrig2489.c` KLD, **boot-loaded** via `/boot/loader.conf` (runtime
kldload GPFs on this INVARIANTS guest):

* Production `sys/dev/disk/nata/ata-disk.c` **#included verbatim**
  (module plumbing neutralized; unused `ad_devclass` renamed;
  `build.sh` sha256-pins the file to the audited tree,
  `8773a2df...` == guest `/usr/src`).  The rig carries byte-identical
  `ad_attach`/`ad_describe`/`ad_get_geometry` under their production
  names.
* **Full production funnel (go1/2/3):** the kernel's own global
  `ata_identify()` runs on a fabricated `struct ata_channel` (real
  `"ata"` devclass, `ATA_IDLE`, `dma=NULL` PIO-only — legitimate per
  `ata-dma.c:153`, production `ata_queue_init()`). The crafted
  512-byte IDENTIFY is served by a `hw.begin_transaction` stub through
  the REAL `ata_queue_request`/`ata_start`/taskqueue machinery and
  parsed by the REAL `ata_getparam` (le16toh/bswap/btrim); REAL newbus
  child (`device_add_child` on the devclass-carrying channel), REAL
  `ad_probe`, and — the point — the **RUNNING KERNEL'S OWN
  `ad_attach`** (`nm`: `t ad_attach` = `0xffffffff80328d30`).
* ata_if setmode/locking on the channel = the interface's own DEFAULT
  implementations (`ata_if.m` CODE blocks).  Scenarios run in a rig
  worker thread **holding the MP token** — the boot-attach context:
  the built-in atapicam driver's `identify()` adds a child to every
  probed channel and its `xpt_bus_register()` asserts the MP token
  (`cam_sim.c:104`), which boot-time attach holds (first rig iteration
  without the MP lock hit that INVARIANTS assert — a rig-context
  artifact, fixed by mirroring the boot context, not by altering any
  production path).
* Two early rig iterations wedged when the funnel was fired from the
  sysctl handler thread itself (attach-time sysctl/devfs registration
  from inside a live sysctl handler); moving dispatch to the dedicated
  worker thread resolved it.  No production code was changed in
  either step.

## 3. Baseline results (stock kernel `#0 Thu Jul 2 06:02:54 UTC 2026`,
sha256 `5dc83dac...`)

| scenario | input | result |
|---|---|---|
| go1 CONTROL full funnel | IDENTIFY 16383/16/63, word53=0x0006, LBA 0x2000000 | clean attach: `ad0: 16384MB <DF2489 SANE GEOMETRY FW000001> at ata0-master PIO4` + bootverbose `ad0: 33554432 sectors [33288C/16H/63S] 16 sectors/interrupt 1 depth queue`, ataraid metadata scan served (4 reads, "check1 failed"), RETURNED |
| go2 full funnel | **word 3 heads = 0** | real getparam parse → kernel own ad_attach → **Fatal trap 18: integer divide fault, IP 0xffffffff80328f7b = `ad_attach+0x24b`, `Stopped at ad_attach+0x24b: divq %rsi,%eax`** (panic_go2_funnel_heads0.txt) |
| go3 full funnel | **word 6 sectors = 0** | identical trap (panic_go3_funnel_sectors0.txt) |
| go4 verbatim copy | heads=0 post-parse state | identical `divq` trap in the rig-compiled production copy (panic_go4_verbatim_attach.txt) — byte-identity |
| go5 verbatim copy describe | ad_softc heads=0, bootverbose | `48MB <...>` MB line prints, then **trap 18 at `ad_describe+0x1bf: divq`** — the :540 site (panic_go5_describe_bootverbose.txt) |

Instruction↔line attribution is airtight (`ad_attach_stock_disasm.txt`):
trap IP is `movslq %ecx,%rsi; div %rsi` — u64 `total_secs` divided by
the 32-bit `heads*sectors` product, result stored then
`device_get_unit`/`device_add_child(dev,"subdisk",...)` = line 151;
i.e. exactly `ata-disk.c:147`.  In the wild the :540 site is shadowed
by :147 (attach precedes describe, same divisor) but a :147-only fix
would leave every bootverbose boot panicking — the fix must (and does)
cover both.

## 4. Reachability honesty

Trigger: malicious/buggy ATA/SATA device, bridge dongle or VM disk
firmware reporting IDENTIFY word 3 = 0 and/or word 6 = 0 with word 53
not marking words 54-58 valid.  Words 3/6 are retired in the modern ATA
spec — real devices report legacy 16/63 — which is exactly why nothing
sanity-checks them and a crafted device is unchallenged.  Attack vector
is physical device attach (AV:P; the CVSS AV:L in the finding record is
generous — no unprivileged local user path exists on this guest: the
nata channels here are boot-time PCI devices, and `vfs.usermount`
irrelevant).  Impact ceiling: **kernel panic at attach** (boot DoS /
hot-plug DoS).  NOT an escalation — closed explicitly: the only
attacker-controlled values at the faulting instruction are the divisor
(0) and dividend; a #DE raises trap 18, stops all CPUs, drops to DDB —
no memory corruption, no controlled write, no route to uid=0.

## 5. Fix validation (single-fix kernel)

`fix.diff` clamps the geometry at its single source in
`ad_get_geometry` (heads→16 / sectors→63 when 0; `current_*` branch
already guarded; total_secs still driven by LBA sizes).  Applied to the
guest `/usr/src`, `make -j6 nativekernel + installkernel`
(fix_build.log: patched ata-disk.c compiled `-Werror` clean; kernel #1
Mon Oct 19 18:19:19 UTC 2026, sha256 `7823c0ca...`), rig rebuilt from
the PATCHED source (checker notes sha `0145d54b...`), same scenarios
re-run (fix_run.log):

| scenario | stock kernel | fixed kernel |
|---|---|---|
| go1 control sane | clean, `[33288C/16H/63S]` describe | **identical output, byte-for-byte** |
| go2 funnel heads=0 | **Fatal trap 18 `ad_attach+0x24b`** | **clean attach**: `ad1: 16384MB <DF2489 ZEROGEOM DISK FW000001> at ata0-master PIO4` + `ad1: 33554432 sectors [33288C/16H/63S] ...` — clamped geometry, :147 AND bootverbose :540 both executed safely, RAID scan ran, scenario DONE, guest alive |
| go4 verbatim copy heads=0 | `divq` trap | no trap 18; attach proceeds through :147 and the full subdisk RAID scan (a later trap 12 in `device_doattach` is a rig-fabrication artifact — the go4 fake bsd_device lacks a sysctl_ctx, which the stock kernel never reached because it trapped first; the real-funnel go2 child is make_device'd and completes 100% cleanly) |

Fixed-binary disassembly (`ad_attach_fixed_disasm.txt`) shows the clamp
in the shipped text: `test %dil,%dil; movb $0x10,0x8(%rax)` /
`test %sil,%sil; movb $0x3f,0x9(%rax)` (ad_softc heads/sectors at
+0x8/+0x9) ahead of the (still present, now-safe) `div`.

Baseline reproduced = 1 (four variants), patched reproduced = 0.
Fix status: **fixed**.

## 6. Honesty notes / limitations

* No physical ATA device exists under this QEMU (root is virtio-blk);
  the rig replaces only the hardware-facing seams (channel device
  graph, IDENTIFY/data/CONTROL answers, the ata_if DEFAULT
  setmode/locking).  For go1/2/3 every instruction from the IDENTIFY
  bytes through `ata_getparam`/`ata_identify`/newbus/`ad_probe`/
  `ad_attach` to the faulting `divq` is the shipped kernel binary.
* The panic fires in the rig worker thread on the guest; on a real
  machine the identical fault fires from the boot attach (or hot-plug
  reinit) funnel — the same first `ad_attach` call with the same
  device-supplied divisor.
* `ad_describe`'s `unknown:` nameunit in go5 is the fake device's
  nameunit; cosmetic, rig-only.
* Related-but-distinct filed findings: DF-1883 (same sites, Low),
  DF-1604/DF-1738/DF-2491 (other zero-geometry divisors in
  XPT_CALC_GEOMETRY / ata-lowlevel CHS), DF-2496 (RAID geometry class,
  whose site-guard fix style this fix's source-clamp complements),
  DF-4798 (u16→u8 truncation in the same raw copy — the clamp also
  neutralizes its 0x0100→0 heads-trap corner).

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
