# DF-4543 — agp.c device-lifecycle: `/dev/agpgart` cdev outlives chipset detach; unit/minor mismatch in free path

## What the finding is (pass 2, new — not any of DF-1685/1686/1687/1688/2397/2398/2399/2400)

`sys/dev/agp/agp.c`:

1. **`agp_generic_attach` (agp.c:240-242)** creates the userland device with a
   **hardcoded minor 0 and name "agpgart"**, then stores the backing `device_t`
   in the cdev: `sc->as_devnode->si_drv1 = dev;`
2. **`agp_free_cdev` (agp.c:248-251)** removes it with
   `dev_ops_remove_minor(&agp_ops, device_get_unit(dev))`. That helper
   (`sys/kern/kern_device.c` → `devfs_destroy_dev_by_ops(ops, minor)`) matches
   cdevs by **minor**, not by unit. It is only correct for unit 0.
   For any agp device with unit != 0 the removal is a **no-op** — the
   `agpgart` node survives detach with `si_drv1` pointing at the detached
   (and soon freed) device. A second simultaneous AGP bridge also collides on
   the same name/minor (two `agpgart` cdevs, each overwriting its own
   `si_drv1`).
3. **Detach with the device open (any unit):** `destroy_dev` only unlinks the
   node and drops the link refs (`devfs_destroy_dev_worker`,
   `sys/vfs/devfs/devfs_core.c:1440-1463`). An open file's vnode holds its own
   cdev reference, so the cdev — and `si_drv1` — survives detach. After the
   driver finishes detaching, newbus frees the softc; the next
   `ioctl(2)/mmap(2)/close(2)` on the still-open fd runs
   `agp_ioctl` (agp.c:818-820) / `agp_mmap` (agp.c:857-858) /
   `agp_close` (agp.c:792-794) → `kdev->si_drv1` → `device_get_softc()` →
   **use-after-free** read+write on the freed softc (ioctls write `sc`
   fields: `as_state` at agp.c:653-655, TAILQ ops at agp.c:481/496/800).

## Trigger requirements (why this is Low)

- `/dev/agpgart` is `UID_ROOT, GID_WHEEL, 0600` (agp.c:241) — **root-only**
  (no devfs rule widens it; checked `etc/defaults/devfs.conf`).
- Detach requires either a kernel built with agp as a loadable module
  (stock `X86_64_GENERIC` compiles it in — `sys/config/X86_64_GENERIC:170` —
  so `kldunload` is impossible there) or ACPI/PCI hot-removal of the AGP
  bridge.

So: root-triggerable UAF, hardware/config-gated. Defense-in-depth value.

## Reproduce

- `./run.sh` — runs the liveness probe on the audit guest via
  `dfbsd-qemu/vm.sh run_root`.
- **Expected on this guest: nothing happens** — see `VERDICT.md`: the guest
  has no AGP hardware (QEMU bochs VGA, 7 PCI devices, no AGP caps, no
  `/dev/agpgart`), so the trigger surface does not exist to exercise. The
  probe output in `run.log` is the documented proof of hardware absence.

## Fix

`fix.diff` — make `agp_free_cdev` refuse teardown while a client is open
(returns EBUSY → detach fails, chip drivers propagate) and destroy the actual
stored devnode instead of guessing minor == unit.
