# DF-2397 — Unsynchronized AGP memory-list operations (sys/dev/agp/agp.c)

## Verdict: NOT REPRODUCED (HW-gated); REAL BUG IN SOURCE (defense-in-depth fix warranted)

## Hardware gate (why the PoC cannot run on this guest)

The AGP ioctls (`AGP_ALLOC_MEMORY`, `AGP_FREE_MEMORY`, `AGP_BIND_MEMORY`,
`AGP_UNBIND_MEMORY`) are served through `/dev/agpgart`, which is created only
when an AGP bridge driver (`agp`) attaches. The audit QEMU/KVM guest has no AGP
bridge:

```
$ ls /dev/agpgart 2>&1          # No such file or directory
$ pciconf -l | grep -iE "agp|0x0302|bridge"   # only i440fx hostbridge + PIIX ISA bridge
hostb0@pci0:0:0:0:  class=0x060000 chip=0x12378086   # i440fx (no AGP)
isab1@pci0:0:1:0:   class=0x060100 ...
$ kldstat | grep -i agp          # (none)
```

With `/dev/agpgart` absent there is no file descriptor on which to issue the
AGP ioctls, so the unsynchronized alloc/free/find/bind/unbind paths cannot be
raced by any user on this guest. AGP is a dead-bus technology absent from this
QEMU machine model.

## Source trace — the bug is REAL (sys/dev/agp/agp.c)

`sc->as_lock` is acquired **only** inside `agp_generic_bind_memory` (`agp.c:511`)
and `agp_generic_unbind_memory` (`agp.c:611`). Every other mutation of the
`sc->as_memory` TAILQ and the `as_allocated` / `as_nextid` accounting is
performed **without** the lock:

- `agp_generic_alloc_memory` `agp.c:474` `sc->as_nextid++`, `agp.c:481`
  `TAILQ_INSERT_TAIL(&sc->as_memory, ...)`, `agp.c:482` `sc->as_allocated += size`
  — no lock.
- `agp_generic_free_memory` `agp.c:495` `sc->as_allocated -= ...`,
  `agp.c:496` `TAILQ_REMOVE(&sc->as_memory, ...)` — no lock.
- `agp_find_memory` `agp.c:676-688` `TAILQ_FOREACH(&sc->as_memory, ...)` — no
  lock; returns a raw `struct agp_memory *` to its caller.
- `agp_deallocate_user` `agp.c:733` calls `agp_find_memory` then frees — no lock.
- `agp_bind_user` `agp.c:748` calls `agp_find_memory` (no lock) and passes the
  pointer to `AGP_BIND_MEMORY` which only acquires the lock **inside** bind:
  a classic TOCTOU window between the unlocked find and the locked bind lets a
  concurrent free (also unlocked) free the object out from under the binder.
- `agp_close` `agp.c:800` iterates `TAILQ_FIRST(&sc->as_memory)` across
  `AGP_FREE_MEMORY` calls with no lock.

Two concurrent ioctl callers (root — `/dev/agpgart` is operator/root on
DragonFly) can therefore corrupt the TAILQ and free a `struct agp_memory` while
another thread still holds/dereferences it → UAF / kernel memory corruption or
panic. The lock comment in the source states it only prevents re-entry to
`bind_body`, confirming the gap is by omission, not design.

Impact: concurrency race → TAILQ corruption / UAF; requires root-or-operator
access to `/dev/agpgart` plus an AGP bridge. Real bug; privilege boundary is
root↔kernel (hardening gap), not unpriv→root.

## Exploit chain status

Not pursuable — primitive is behind absent AGP hardware and is root-only even
on hardware (valid Phase-6 hard blocker: write reachable only from an already-
privileged `/dev/agpgart` context). No unprivileged escalation.

## PoC changes

None. `/dev/agpgart` absent; verified by source trace only.

## Recommended fix

Take `sc->as_lock` around alloc/free/find/deallocate/close as well as bind.
See `fix.diff` (matches finding proposal: serialize all `as_memory` list +
accounting mutations under `as_lock`).
