β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2346

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2346 Β· 7 files
FileTypeDescriptionSize
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
VERDICT.md verdict gate analysis + raw-proc UAF trace; reachability probe
↓ download 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.

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_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-2346/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-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.