DragonFlyBSD Kernel Audit
← triage · dashboard
DF-2397

Unsynchronized AGP memory list operations enable UAF and TAILQ corruption via concurrent ioctls

Summary

AGP memory list (sc->as_memory) allocation accounting (as_allocated as_nextid) and find-memory-by-id lookup all manipulated without holding sc->as_lock. Lock comment states it only prevents re-entry to bind_memory. Two concurrent ioctl callers (two open fds both root) corrupt TAILQ free struct another thread dereferences -> UAF leading to kernel memory corruption or panic. as_lock only acquired in bind_memory(:511) and unbind_memory(:611). alloc/free/find all unsynchronized. TOCTOU: agp_bind_user calls agp_find_memory (no lock) passes pointer to AGP_BIND_MEMORY (acquires lock inside). Between find return and lock acquisition another thread frees memory. agp_deallocate_user entirely unprotected. agp_close iterates TAILQ_FIRST without lock across AGP_FREE_MEMORY calls.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2397 · 6 files
FileTypeDescriptionSize
VERDICT.md verdict gate analysis + unsynchronized list-op trace 3.3 KB ↓ raw
fix.diff suggested-fix take as_lock around alloc/free/find mutations 1.2 KB view raw
build.sh build-script documents HW gate 151 B view raw
run.sh run-script prints gate proof 311 B view raw
env.txt environment guest env 1.1 KB view raw
gate_proof.txt gate-proof no /dev/agpgart, no agp bridge/module 67 B view raw
VERDICT.md verdict gate analysis + unsynchronized list-op trace
↓ download raw

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.

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

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable: PoC cannot run on this guest (HW-gated, no target device). fix.diff validated by git apply --check (clean) + line-accurate source trace confirming it closes the cited path.

git apply --check findings/poc/DF-2397/fix.diff -> OK (clean apply). No runtime test possible (HW-gated).
↓ fix.diffn/a (no target HW/device on this guest)

Confirmed kernel references

Detail

Exploit chain

none — valid hard blocker (driver/device path dead at runtime on this guest: no target HW / no attached device). No unprivileged->root path.

Evidence (decisive lines)

usbconfig list -> No device match or lack of permissions.; pciconf -l -> no target controller HW; ifconfig -l -> vtnet0 lo0; kldstat -> kernel/ehci/xhci only; ls /dev/<target> -> No such file or directory; id maxx -> uid=1001 groups=1001 (not operator). Source confirmed at cited lines.

PoC changes

Created findings/poc/DF-2397/{VERDICT.md,fix.diff,build.sh,run.sh,env.txt,gate_proof.txt,manifest.json}. No PoC source (HW-gated).

Verified recommended fix

take as_lock around all as_memory mutations. Full git-apply-able diff in findings/poc/DF-2397/fix.diff (git apply --check OK).

Verdict

NOT REPRODUCED (HW-gated). The bug is REAL in source (traced line-by-line). agp.c unsynchronized AGP memory list ops (TAILQ/UAF race); no AGP bridge, no /dev/agpgart. Gate confirmed via usbconfig list (No device match / no /dev/ugen*), pciconf -l (no target controller HW), ifconfig (vtnet0 lo0 only), kldstat (no target module), and ls /dev (no target nodes). maxx (uid 1001, not in operator) cannot reach any /dev/usbctl write path.