# DF-4797 — nata ad(4) teardown race: ad_strategy/ad_done vs ad_detach use-after-free / near-NULL panic

**File:** `sys/dev/disk/nata/ata-disk.c` (pass 2)
**Severity:** High (kernel memory corruption class / local DoS; teardown trigger is privileged or hypervisor-initiated)
**Confidence:** certain — reproduced 2/2 baseline, fatal trap 12

## What the bug is

`ad_detach()` (ata-disk.c:167-203) tears the disk down and `kfree()`s the
`ad_softc` **without any serialization against the data path**:

* `disk_invalidate()` → `dsgone()` frees `dp->d_slice` and NULLs it while
  bios that already entered `diskstrategy()` are still in `dscheck()`;
* `disk_destroy()` does not wait for in-flight strategy calls
  (`destroy_dev` only re-vectors the cdev ops; open file descriptors keep
  dispatching through them);
* `ata_fail_requests()` only drains requests already queued/running on the
  channel — a bio between `physio` and `ata_queue_request()` is missed;
* `device_set_ivars(dev, NULL)` + `kfree(adp)` then run while such a bio
  still executes `devstat_start_transaction(&adp->stats)`
  (ata-disk.c:360 — write into freed memory) and later `ad_done()` does
  `devstat_end_transaction_buf(&((struct ad_softc *)NULL)->stats, ...)`
  (ata-disk.c:368/376 — write to near-NULL).

This is the ad(4) twin of the nvme(4) teardown race DF-4646 (validated
fix), and the driver-level counterpart of the ata-queue drain issues
DF-4606/4607.

## Reproduction (in situ, on the audit guest)

The guest was booted with its system disk attached as a QEMU PIIX3 IDE
disk, so the root device is `ad0` on the nata stack:

    ad0: 30720MB <QEMU HARDDISK 2.5+> at ata0-master WDMA2
    ad0s1d on / (hammer2, local)

1. `./df4797_storm 16 20` — 16 threads hold `/dev/ad0` open and issue
   raw 512-byte preads in a tight loop.
2. `sysctl kern.df4797.run=1` — the df4797 KLD calls
   `device_detach(ad0)` (the exact PCI hot-unplug / kldunload path).

Observed baseline (twice, run.log / panic.txt):

    df4797: cycle 1/4 detaching ad0 (0xfffff8008bb62f20)...
    subdisk0: detached
    Fatal user address access from kernel mode from df4797_storm at ffffffff806972a8

    Fatal trap 12: page fault while in kernel mode
    cpuid = 5; lapic id = 5
    fault virtual address = 0xc
    fault code	= supervisor read data, page not present
    instruction pointer = 0x8:0xffffffff806972a8
    current process	= 906 (df4797_storm)
    Stopped at dscheck+0x48:	movl 0xc(%r13),%ecx
    db> trace
    dscheck() at dscheck+0x48
    diskstrategy() at diskstrategy+0x2a
    dev_dstrategy() at dev_dstrategy+0xb7
    physio.isra.0() at physio.isra.0+0xa5
    dev_dread() at dev_dread+0x55
    devfs_fo_read() at devfs_fo_read+0x100
    kern_preadv() at kern_preadv+0x1e5
    sys_extpread() at sys_extpread+0x7a
    syscall2() at syscall2+0x11e

The panicking thread is a storm thread whose pread entered the disk
layer while `ad_detach` freed `dp->d_slice` underneath it
(`dscheck(dev, bio, NULL)` reads `ssp->dss_nslices` at offset 0xc of the
NULL slices pointer).  The driver-level sinks
(`devstat_start_transaction` on the freed softc, `ad_done`'s NULL ivars)
sit on the same unserialized path.

## Fix

`fix.diff` (validated on a rebuilt kernel — see VERDICT.md):

* `ad_strategy()` takes a per-softc in-flight reference under the
  channel `queue_mtx` (fail with ENXIO when detaching);
* `ad_done()` drops it and wakes a detaching `ad_detach()`;
* `ad_detach()` sets `AD_F_DETACHING` + NULLs the ivars under the same
  lock and waits for the drain before any teardown;
* `ata_fail_requests()` runs *before* `disk_invalidate()/disk_destroy()`
  so failed biodone()s see intact disk-layer structures;
* `diskstrategy()` guards the `dscheck()` call against a NULL
  `dp->d_slice` (the reproduced panic site).

## Files

* `df4797_storm.c` — raw-I/O storm (userland)
* `df4797_detach.c` / `Makefile` — detach-trigger KLD
* `build.sh`, `run.sh` — exact commands
* `run.log`, `panic.txt` — full serial capture of the baseline panic
* `fix.diff` — the validated fix (git-apply-able against sys/)
* `VERDICT.md`, `manifest.json`, `verdict.json`
