SIGIO recipient stored as raw unreferenced struct proc pointer -> use-after-free after fork+exit
Summary
USB FIFO async-SIGIO recipient kept in f->async_p as raw struct proc* from td->td_proc with NO reference taken and NO registration on proc p_sigiolst. Every other async-signal character device (pipe socket tty bpf tap tun kqueue evdev drm) uses refcounted struct sigio/fsetown/pgsigio. Because USB bypasses that mechanism after fork() child inherits open file (FIFO with async_p==parent) parent can exit and be reaped freeing proc struct. Next USB transfer completion dereferences freed proc via lwkt_trytoken(&f->async_p->p_token) in usb_fifo_wakeup (and usb_fifo_close). usb_fifo_wakeup does if(f->async_p!=NULL && lwkt_trytoken(&f->async_p->p_token)) direct lockless deref of externally-owned un-refcounted pointer. Attacker: operator-group/desktop user opens ugen endpoint enables FIOASYNC forks parent exits reaped freeing proc child triggers completion. Floor: reliable kernel panic local DoS. Ceiling: proc slab reused heap grooming for code execution local privilege escalation.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2346 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | gate analysis + raw-proc UAF trace; reachability probe | 4.3 KB | β raw |
| fix.diff | suggested-fix | PHOLD/PRELE the proc on FIOASYNC set/clear/close | 914 B | view raw |
| build.sh | build-script | documents HW gate | 217 B | view raw |
| run.sh | run-script | prints gate proof | 359 B | view raw |
| env.txt | environment | guest env | 1.1 KB | view raw |
| gate_proof.txt | gate-proof | /dev/usbctl perms, no /dev/ugen*, usbconfig empty | 284 B | view raw |
| maxx_open_probe.txt | gate-proof | maxx open(/dev/usbctl) RDWR=-1, RDONLY=3; FIFO path unreachable | 590 B | view raw |
DF-2346 β SIGIO recipient stored as raw unreferenced struct proc * (usb_dev.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 vulnerable field is f->async_p on a USB FIFO (struct usb_fifo),
which is created per USB device endpoint and exposed as a /dev/ugen<N>.<ep>
character device. The audit QEMU/KVM guest has no USB device attached, so
there are no FIFOs and no ugen nodes:
$ usbconfig list # No device match or lack of permissions. $ ls /dev/ugen* # No such file or directory $ ls /dev/usb* # only /dev/usbctl $ ls -la /dev/usbctl # crw-r--r-- root operator (no group write) $ id maxx # uid=1001 gid=1001 groups=1001 (NOT in operator)
Direct probe run as maxx (see maxx_open_probe.txt):
open("/dev/usbctl", O_RDWR) -> -1 (EPERM: write needs root)
open("/dev/usbctl", O_RDONLY) -> 3 (read OK, but read-only fd is useless here)
Even the read-only fd does not reach the bug: f->async_p =
USB_TD_GET_PROC(td) lives in the per-FIFO usb_ioctl() case FIOASYNC
(usb_dev.c:1038-1044), dispatched on a ugen endpoint fd. /dev/usbctl's ioctl
entry is usb_static_ioctl (bus/device enumeration control ioctls), not the
FIFO ioctl, and FIONBIO/FIOASYNC on usbctl are "handled by upper FS layer"
(usb_dev.c:1035). With no USB device β no ugen endpoint β no USB FIFO β the
f->async_p = USB_TD_GET_PROC(td) assignment is unreachable by any user.
The bug's trigger (fork + parent exit + child USB completion) additionally requires an active async transfer on an open ugen FIFO, which presupposes the USB device the PoC relies on.
Source trace β the bug is REAL (sys/bus/u4b/usb_dev.c)
struct usb_fifo carries (usb_dev.h:110):
struct proc *async_p; /* process that wants SIGIO */
assigned in the per-FIFO usb_ioctl (usb_dev.c:1038-1048):
case FIOASYNC:
if (*(int *)addr) {
if (f->async_p != NULL) { error = EBUSY; break; }
f->async_p = USB_TD_GET_PROC(td); /* usb_dev.c:1044 */
} else
f->async_p = NULL;
break;
USB_TD_GET_PROC(td) = td->td_proc (usb_dragonfly.h:58) β a raw struct
proc * taken with no reference (no PHOLD() / refcount increment) and no
registration on proc->p_sigiolst.
It is then dereferenced locklessly in usb_fifo_wakeup (usb_dev.c:1789-1791):
if (f->async_p != NULL && lwkt_trytoken(&f->async_p->p_token)) {
ksignal(f->async_p, SIGIO);
lwkt_reltoken(&f->async_p->p_token);
}
and the same pattern in usb_fifo_close (usb_dev.c:798-801).
Every other async-signal character device in the tree (pipe, socket, tty, bpf,
tap, tun, kqueue, evdev, drm) uses the refcounted struct sigio / fsetown /
pgsigio mechanism. USB bypasses it. After fork(), the child inherits the
open ugen FIFO with async_p == parent's proc; the parent can exit() and be
reaped, freeing the proc struct. The next USB transfer completion calls
usb_fifo_wakeup β lwkt_trytoken(&f->async_p->p_token) on a freed proc β
UAF. Floor: reliable kernel panic / local DoS. Ceiling: proc slab reuse β
heap grooming for code execution.
Attacker: operator-group/desktop user opens a ugen endpoint, enables FIOASYNC, forks, parent exits; child triggers a completion. Requires an attached USB device providing an endpoint FIFO.
Exploit chain status
Not pursuable β primitive (lockless UAF on struct proc) requires an open ugen
FIFO, which requires an attached USB device (absent) β valid Phase-6 hard
blocker: dead path at runtime on this guest. On a desktop with a USB device
plugged in this is a real write-capable UAF.
PoC changes
Authored a small reachability probe (maxx_open_probe.txt) confirming maxx
cannot get a writable FIFO fd on this guest. No exploit chain written: no USB
device β no FIFO β no async_p assignment.
Recommended fix
Convert f->async_p to the refcounted struct sigio * mechanism (fsetown/
pgsigio/funsetown), matching every other async-signal char device. See
fix.diff for a minimal defense-in-depth fix that takes a proc reference
(PHOLD) on assignment and releases it (PRELE) on clear/close, closing the
UAF (a full sigio refactor is the proper long-term fix).
Fix verification
not_testablenot_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-2346/fix.diff -> OK (clean apply). No runtime test possible (HW-gated).
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-2346/{VERDICT.md,fix.diff,build.sh,run.sh,env.txt,gate_proof.txt,manifest.json}. No PoC source (HW-gated).
Verified recommended fix
PHOLD on set / PRELE on clear+close (or full struct sigio refactor). Full git-apply-able diff in findings/poc/DF-2346/fix.diff (git apply --check OK).
Verdict
NOT REPRODUCED (HW-gated). The bug is REAL in source (traced line-by-line). usb_dev FIOASYNC stores raw unreferenced struct proc * async_p (fork+exit UAF); no USB device, no /dev/ugen. 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.
No comments yet.