# DF-4874 VERDICT — afd_detach() in-flight teardown race

## Status: untested (hardware absent from guest); root cause proven by line trace + reproduced twin DF-4797

## What the finding is

`afd_detach()` (sys/dev/disk/nata/atapi-fd.c:134-156) tears the driver down in
an order that leaves two independent windows for use-after-free:

1. **Disk-layer race (shared with DF-4797).** `disk_invalidate(&fdp->disk)`
   (atapi-fd.c:144) → `dsgone()` (sys/kern/subr_diskslice.c:348) `kfree()`s the
   slices struct and NULLs `dp->d_slice` while a concurrent raw read/write is
   inside `diskstrategy()` (sys/kern/subr_disk.c:1246) → `dscheck(dev, bio,
   NULL)` dereferences `ssp->dss_nslices` at offset 0xc of NULL → fatal trap 12.
   Open fds on the cdev keep the old ops vector alive across `disk_destroy()`,
   so post-detach I/O keeps entering `afd_strategy()` too.

2. **Driver-level UAF.** `ata_fail_requests(dev)` (atapi-fd.c:148;
   sys/dev/disk/nata/ata-queue.c:565-605) pulls the channel-queued and running
   requests and — *after releasing queue_mtx/state_mtx* — hands each to
   `ata_finish()` (ata-queue.c:518-537), which normally defers to
   `taskqueue_swi_mp`. The completion callback `afd_done()` therefore runs
   asynchronously, *after* `afd_detach()` has already done
   `device_set_ivars(dev, NULL)` (atapi-fd.c:153) and `kfree(fdp)` (:154).
   `afd_done()` then executes `fdp = device_get_ivars(request->dev)` → NULL
   (atapi-fd.c:321) and `devstat_end_transaction_buf(&fdp->stats, bbp)`
   (atapi-fd.c:329) — a write to `NULL + offsetof(struct afd_softc, stats)` —
   and `biodone(bp)` (:330) on a bio whose disk layer is already destroyed.
   The same window exists for a request that completes via the HW interrupt
   racing the detach.

`afd_strategy()` itself also runs lock-free against detach:
`devstat_start_transaction(&fdp->stats)` (atapi-fd.c:313) can write into freed
memory, exactly like `ad_strategy()` did for DF-4797 (ata-disk.c:360).

## Why this is a distinct finding (not a DF-1944/DF-2507/DF-4797 re-report)

* DF-1944/DF-2507 are the **afd_reinit()** path (atapi-fd.c:167-182): softc
  freed *without* `disk_destroy()` on a channel reinit, orphaning the cdev.
  This finding is the **afd_detach()** path — different entry, different
  trigger (device_detach / hot-unplug / kldunload), and the softc is freed
  *after* a teardown sequence that still fails to drain in-flight I/O.
* DF-4797 is the same shape in **ata-disk.c** (`ad`), where it was REPRODUCED
  2/2 on this guest (fatal trap 12 at `dscheck+0x48`, fault address 0xc) and
  whose fix was validated on a rebuilt kernel. The nata pass-set convention is
  per-file findings; atapi-fd.c carries its own copy of the defect.

## Verification attempt (what was run, and the blocker)

Trigger infrastructure was ported from the proven DF-4797 pack to the `afd`
devclass and **built successfully on the audit guest** (build.log: KLD
`df4874.ko` + userland `df4874_storm`, cc 8.3, kernel sources in /usr/src):

```
$ kldload ./df4874.ko && sysctl kern.df4874.run=1
df4874: afd0 device_t is NULL
$ ./df4874_storm 4 3
storm: done, total_io=0
```

The `afd` devclass exists (the driver is compiled into X86_64_GENERIC,
`device afd`), but there is no `afd` device: the afd driver only probes ATAPI
*direct-access* devices (atapi-fd.c:90-91), and QEMU 7.2 cannot emulate one
(only `ide-hd`/`ide-cd` exist on the IDE bus — checked with
`qemu-system-x86_64 -device help`). So the race cannot be instantiated on this
guest: **untested, hard blocker documented above**, not a refutation.

Corroboration: the shared disk-layer mechanism (subr_disk.c `dscheck` vs
`dsgone`) plus the identical nata teardown ordering were proven live on this
exact kernel build by DF-4797's in-situ reproduction (2/2 fatal trap 12).

## Threat model

* Attacker needs an open descriptor on `/dev/afd0` (raw floppy device;
  typically group `operator`, same exposure DF-4797 had on `/dev/ad0`) to hold
  the cdev alive and storm `afd_strategy()`.
* The detach side is the standard PCI hot-unplug / `devctl detach` /
  kldunload teardown (privileged or hypervisor-initiated) — same threat model
  as DF-4797 / DF-4646 (nvme twin).
* Impact: kernel UAF write (`devstat_*` on freed softc) + NULL-deref panic;
  realistic ceiling is memory corruption in the disk/stat structures, not a
  demonstrated uid=0 chain.

## Recommended fix

Mirror the validated DF-4797 fix in atapi-fd.c: refuse detach while the disk
is open (`disk.d_opencount`), set a DETACHING flag under `ch->queue_mtx`
before NULLing ivars, take a per-softc in-flight reference in `afd_strategy()`
dropped in `afd_done()`, drain before `kfree`, and move `ata_fail_requests()`
*before* `disk_invalidate()/disk_destroy()` so biodone()s land while the disk
layer is intact. See fix.diff.

## Fix validation

fix_status: not_testable — the baseline cannot be triggered on the audit guest
(no ATAPI direct-access device can be attached under QEMU 7.2), so a
patched-kernel A/B run is impossible here. fix.diff is a direct port of the
DF-4797 fix whose own patched-kernel validation PASSED on this guest
(run.fixed.log in findings/poc/DF-4797/).

## 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.
