Use-after-free in devfs_clone β clone handler freed during lock release window before nhandler call
Summary
devfs_clone :2325-2326 chandler matched under SHARED devfs_lock. :2333 lockmgr(&devfs_lock,LK_RELEASE) lock dropped. :2334 devfs_config() sends SYNC to core thread which processes pending DEVFS_CHANDLER_DEL that frees chandler via devfs_chandler_del_worker :1598-1620 TAILQ_REMOVE+kfree. :2341 error=(chandler->nhandler)(&ap) derefs freed M_DEVFS memory = UAF on function pointer. TAILQ_FOREACH continuation :2322 via continue :2344 also accesses chandler->link on freed memory. Race window between lock release and function pointer call. Trigger: unprivileged user opens clonable device (/dev/dsp /dev/ptmx) racing concurrent clone handler removal (USB audio detach destroy_autoclone_dev). Physical device removal by local attacker triggers detach no root needed. Function pointer nhandler if heap-groomed = RIP control arbitrary kernel code exec. Fix: copy clone_fn=chandler->nhandler while locked use clone_fn after unlock.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0774 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| uaf_stress.c | trigger-source | concurrent open/kldunload stress harness | 3.8 KB | view raw |
| build.sh | build-script | cc -O2 -o uaf_stress uaf_stress.c | 159 B | view raw |
| run.sh | run-script | kldload if_tap; ./uaf_stress 15 | 265 B | view raw |
| fix.diff | suggested-fix | copy nhandler while locked + remove devfs_config + tap unload reorder | 2.8 KB | view raw |
| VERDICT.md | verdict | full analysis: mechanism, reproduction, impact, fix validation | 6.6 KB | β raw |
| README.md | readme | build/run/expected summary | 1.2 KB | β raw |
| panic.txt | panic-signature | Fatal trap 12 page fault in tapcreate+0x11a | 862 B | view raw |
| run.log | run-log | unpatched kernel run output with panic | 1.7 KB | view raw |
| fix_run.log | run-log | patched kernel run output (still crashes β module-data race) | 1.3 KB | view raw |
| fix_build.log | build-log | single-fix kernel build output (rc=0) | 5.6 MB | β download |
| env.txt | environment | uname, cc version, sysctls, kldstat | 536 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0774 β devfs_clone Use-After-Free
Finding
Use-after-free in devfs_clone() (sys/vfs/devfs/devfs_core.c:2314β2351):
clone handler freed during lock-release window before nhandler callback.
Build
cc -O2 -o uaf_stress uaf_stress.c
Run
# MUST run as root: /dev/tap is 0600, kldunload requires root
kldload if_tap
./uaf_stress 15
Expected behavior (bug present)
Kernel panic β Fatal trap 12: page fault in tapcreate+0x11a.
Guest goes down (DDB on serial console).
Reproduces within ~12 seconds of stress.
Preconditions
- Root access (for kldunload + /dev/tap open)
- OR physical device removal of a USB autoclone device (e.g. /dev/dsp)
- Unprivileged users CANNOT trigger this on a default guest
Files
uaf_stress.cβ concurrent open/kldunload stress harnessbuild.sh/run.shβ exact build/run commandsfix.diffβ git-apply-able fix (devfs_core.c + if_tap.c)VERDICT.mdβ full analysispanic.txtβ kernel panic signature from serial consolerun.logβ unpatched kernel run outputfix_run.logβ patched kernel run outputfix_build.logβ patched kernel build logenv.txtβ guest environment
DF-0774 β Verdict: REPRODUCED (panic / DoS)
Summary
The UAF described in DF-0774 is REAL and confirmed by code-level trace and runtime panic.
The bug is in devfs_clone() (sys/vfs/devfs/devfs_core.c:2314β2351): the function
releases devfs_lock (line 2333) and then dereferences chandler->nhandler (line 2341)
without holding the lock or a reference to the chandler. A concurrent
devfs_clone_handler_del() (from driver detach / module unload) can free the chandler
during this window, making the dereference a use-after-free on M_DEVFS slab memory.
Mechanism (confirmed by line-by-line source trace)
-
Entry (locked):
devfs_spec_open()(devfs_vnops.c:901) acquiresdevfs_lockSHARED and callsdevfs_clone(). -
Match (locked):
devfs_clone:2322β2326βTAILQ_FOREACHiteratesdevfs_chandler_list; at line 2325β2326, a handler is matched by name andchandler->nhandleris checked non-NULL. -
Lock release (UAF window opens): Line 2333:
lockmgr(&devfs_lock, LK_RELEASE)β the SHARED lock is dropped entirely. -
SYNC synchronization: Line 2334:
devfs_config()sends aDEVFS_SYNCmessage to the devfs core thread (devfs_msg_core:1264β1271). The core thread processes ALL pending messages in FIFO order before replying. If aDEVFS_CHANDLER_DELwas queued (by a concurrentdevfs_clone_handler_del()atdevfs_core.c:893β900), the core thread processes it insidedevfs_msg_exec:1289(which acquiresdevfs_lockEXCLUSIVE) β callsdevfs_chandler_del_worker:1598β1620βTAILQ_REMOVE+kfree(chandler->name)+kfree(chandler). -
UAF dereference: Line 2341:
error = (chandler->nhandler)(&ap)β thechandlerpointer now points to freedM_DEVFSmemory. Becausedebug.use_weird_array=0by default (kern_slaballoc.c:232), freed slab chunks are NOT poisoned withWEIRD_ADDR(0xdeadc0de), so thenhandlerfield retains its old value (a valid function pointer). The call succeeds and enters the clone callback (e.g.tapclone). -
Crash: The clone callback (tapclone β tapcreate) accesses module-global data (
DEVFS_CLONE_BITMAP(tap),tap_ops,tap_listhead) thatkldunloadis concurrently tearing down (destroy_autoclone_devβ bitmap uninit,destroy_dev,dev_ops_remove_all). This causes a page fault insidetapcreate+0x11a(fault address 0x25 β a NULL+offset dereference on freed/corrupted module data).
Reproduction
PoC: uaf_stress.c β races 8 threads opening /dev/tap in a tight loop
against a parent doing kldunload(2)/kldload(2) of if_tap.ko.
Preconditions:
- MUST run as root (/dev/tap is mode 0600 root:wheel; kldunload requires root).
- OR: physical device removal of a USB autoclone device (e.g. USB audio /dev/dsp)
β a local attacker with physical access can trigger destroy_autoclone_dev
without root.
Unprivileged reachability: NOT triggerable by an unprivileged user on this
guest. All callers of destroy_autoclone_dev / devfs_clone_handler_del require
either root (kldunload, ifconfig destroy, vnconfig) or physical device
removal. No USB autoclone devices exist on the QEMU guest.
Panic signature (unpatched #0 kernel):
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x25 instruction pointer = 0x8:0xffffffff82600afa Stopped at tapcreate+0x11a: movl -0x1510310(%rip),%eax
Reproduced reliably within ~12 seconds of stress.
Impact
- From root: kernel panic (DoS) β reliable.
- From unprivileged user (no physical access): NOT triggerable.
- From local user with physical access (USB device removal): kernel panic (DoS). Potential RIP control if the freed chandler's slab chunk is reclaimed with attacker-controlled data (function pointer overwrite β with no SMAP/SMEP, redirect to userspace shellcode). However, this requires physical device access, which is a constrained threat model.
- Severity: Medium (consistent with finding).
Exploit Chain Assessment
The primitive is a UAF on a small slab object (struct devfs_clone_handler in
M_DEVFS, ~24 bytes β likely kmalloc-32 bucket). The nhandler field (a
function pointer) is at offset ~9 in the struct. If the freed chunk is reclaimed
with attacker-controlled data, nhandler can be overwritten to achieve RIP control.
Valid hard blocker for unprivileged escalation: The trigger path
(devfs_clone_handler_del) is reachable ONLY from root (kldunload) or physical
device removal. There is no unprivileged syscall surface that causes clone
handler removal. Therefore, unprivileged β root escalation is NOT possible
on this guest. This is a legitimate blocker per Phase 6: "the write is
reachable only from an already-root context."
The bug remains a real DoS / potential privilege boundary issue for deployments where local users have physical access to autoclone devices (USB audio, etc.).
PoC Changes
- Wrote
uaf_stress.cfrom scratch (no prior PoC existed). The harness forks 8 children that open/close/dev/tapin tight loops while the parent cycleskldunload(2)/kldload(2)ofif_tap.ko. - Wrote
fix.diffaddressing bothdevfs_core.c(copynhandlerto local, removedevfs_config(), restart scan on error) andif_tap.c(reorder unload to remove clone handler before destroying ops).
Fix Validation
The fix (fix.diff) correctly addresses the chandler struct UAF: nhandler is
copied into clone_fn while the lock is held, and the local copy is used after
unlock. The goto again prevents dereferencing chandler->link on freed memory.
However, the PoC STILL triggers a panic on the patched kernel β at the SAME
location (tapcreate+0x11a). This is because the crash's proximate cause is
the module-data teardown race (tapclone accessing freed tap_ops/bitmap
during kldunload), not the chandler struct deref per se. Without slab poisoning
(use_weird_array=0), the unpatched code reads the same valid nhandler value
from freed memory, so both patched and unpatched kernels call tapclone and hit
the module-data crash.
Evidence that the chandler UAF specifically exists: with debug.use_weird_array=1
(slab poisoning), the unpatched kernel does NOT crash β poisoning corrupts the
freed chandler's namlen field, causing the name-length check at line 2323 to
fail, so the handler is skipped entirely. Without poisoning, the check passes
and the UAF manifests.
A complete fix requires module reference counting: devfs_clone should hold
a reference to the module that registered the clone handler, preventing module
unload while the callback is in progress. This is beyond a single-file diff.
Fix verification
fix_failedfix_failed: the fix.diff correctly addresses the chandler struct UAF (nhandler copied to local clone_fn while locked, never dereffed after free), confirmed by static analysis. However, the PoC STILL triggers a panic on the patched kernel at the SAME location (tapcreate+0x11a) because the crash's proximate cause is a RELATED module-data teardown race: tapclone() accesses freed module globals (DEVFS_CLONE_BITMAP, tap_ops) during concurrent kldunload, which is not prevented by copying the function pointer. Without slab poisoning (use_weird_array=0 default), the unpatched code reads the same valid nhandler from freed memory, making the observable crash identical on both kernels. A complete fix requires module reference counting (hold a ref to the registering module across the clone callback), which is beyond a single-file diff. Baseline: Fatal trap 12 page fault tapcreate+0x11a on unpatched #0 within 12s. Patched: same crash on #1 within 12s.
BASELINE (#0 unpatched): Fatal trap 12: page fault while in kernel mode / fault virtual address = 0x25 / Stopped at tapcreate+0x11a: movl -0x1510310(%rip),%eax -- within 12s of stress. PATCHED (#1 with fix.diff): Fatal trap 12: page fault while in kernel mode / Stopped at tapcreate+0x11a: movl -0x1510310(%rip),%eax -- same crash within 12s. NOTE: The chandler struct UAF IS addressed (nhandler is read from locked valid memory, not freed memory). The remaining crash is from the module-data teardown race. With debug.use_weird_array=1 (slab poisoning), the unpatched kernel does NOT crash -- poisoning corrupts namlen, the handler match fails, proving the chandler UAF exists and is the mechanism the fix targets.
Confirmed kernel references
- sys/vfs/devfs/devfs_core.c:2322
- sys/vfs/devfs/devfs_core.c:2333
- sys/vfs/devfs/devfs_core.c:2334
- sys/vfs/devfs/devfs_core.c:2341
- sys/vfs/devfs/devfs_core.c:1598
- sys/vfs/devfs/devfs_core.c:1613
- sys/vfs/devfs/devfs_core.c:1615
- sys/vfs/devfs/devfs_core.c:2684
- sys/vfs/devfs/devfs_core.c:1289
- sys/vfs/devfs/devfs_vnops.c:901
- sys/kern/kern_conf.c:435
- sys/kern/kern_slaballoc.c:232
Detail
Exploit chain
Valid hard blocker for unprivileged escalation: the trigger path (devfs_clone_handler_del) is reachable ONLY from root (kldunload, ifconfig destroy, vnconfig) or physical device removal (USB autoclone device unplug). There is no unprivileged syscall surface that causes clone handler removal on this guest -- all callers of destroy_autoclone_dev require root or physical access. Therefore unpriv->root escalation is NOT possible; this is root->kernel or physical-access->kernel. The primitive itself (UAF on ~24-byte struct devfs_clone_handler in M_DEVFS, containing function pointer nhandler at offset ~9) COULD yield RIP control if the freed slab chunk were reclaimed with attacker-controlled data, and with no SMAP/SMEP the forged pointer could jump to userspace shellcode. But the trigger precondition (root/physical) makes this a DoS/hardening finding, not an unprivileged privesc. No escalation chain developed due to the root-only trigger path -- this is a legitimate Phase 6 hard blocker.
Evidence (decisive lines)
malloc_uninit: 1152 bytes of 'tap' still allocated on cpu 6 Fatal user address access from kernel mode from uaf_stress at ffffffff82600afa Fatal trap 12: page fault while in kernel mode fault virtual address = 0x25 instruction pointer = 0x8:0xffffffff82600afa Stopped at tapcreate+0x11a: movl -0x1510310(%rip),%eax db>
PoC changes
Wrote uaf_stress.c from scratch (no prior PoC existed). The harness forks 8 children that open/close /dev/tap in tight loops while the parent cycles kldunload(2)/kldload(2) of if_tap.ko. Wrote fix.diff addressing both devfs_core.c (copy nhandler to local clone_fn while locked at line 2326, remove devfs_config() call at 2334 that forces core-thread SYNC, use goto again instead of continue to avoid chandler->link deref) and if_tap.c (reorder MOD_UNLOAD to call destroy_autoclone_dev before dev_ops_remove_all).
Verified recommended fix
The fix.diff makes three changes: (1) In devfs_core.c devfs_clone(): copy chandler->nhandler into local clone_fn while devfs_lock is held (line 2326), then call clone_fn(&ap) after lock release instead of chandler->nhandler -- prevents the M_DEVFS UAF. (2) Remove the devfs_config() call at line 2334 -- this SYNC forces the core thread to drain pending CHANDLER_DEL messages during the callback window, which is the mechanism that makes the free observable. (3) In if_tap.c tapmodevent(MOD_UNLOAD): move destroy_autoclone_dev before dev_ops_remove_all so no new clone callbacks can fire after handler removal. Matches the finding proposal ('copy clone_fn=chandler->nhandler while locked, use clone_fn after unlock') with additional hardening. NOTE: a complete fix also requires module reference counting to prevent module unload while a clone callback is in progress -- documented in VERDICT.md as a known limitation. The full git-apply-able diff lives in findings/poc/DF-0774/fix.diff.
Verdict
REPRODUCED. The UAF described in DF-0774 is REAL and confirmed by both line-by-line source trace and runtime panic. devfs_clone() (sys/vfs/devfs/devfs_core.c:2314-2351) releases devfs_lock at line 2333, then calls devfs_config() at 2334 which sends a SYNC to the devfs core thread (devfs_msg_core:1264). The core thread processes pending DEVFS_CHANDLER_DEL messages under EXCLUSIVE lock (devfs_msg_exec:1289), calling devfs_chandler_del_worker:1598-1620 which does TAILQ_REMOVE + kfree(chandler). Back in devfs_clone, line 2341 dereferences chandler->nhandler on the freed M_DEVFS memory -- a use-after-free on a function pointer. Confirmed by: (1) kernel panic 'Fatal trap 12: page fault in tapcreate+0x11a' reproduced reliably within ~12s of stress; (2) enabling debug.use_weird_array=1 (slab poisoning) on the unpatched kernel PREVENTS the crash -- poisoning corrupts the freed chandler's namlen field, causing the name-match check at line 2323 to fail, so the handler is skipped entirely, proving the chandler struct is being freed and re-accessed. The PoC triggers via racing open('/dev/tap') against kldunload if_tap.
No comments yet.