DragonFlyBSD Kernel Audit
DF-3017 / verdict.json
← back to finding ↓ download raw
{
 "finding_id": "DF-3017",
 "status": "reproduced",
 "reproduced": 1,
 "impact": "panic",
 "confidence": "certain",
 "verdict": "Unprivileged local user races pty clone churn (open/close /dev/ptmx) against stat()/access() of the transient /dev/pts/N nodes; devfs_allocv()'s devfs_lock drop across getnewvnode()/vget() lets the concurrent devfs_freep() free the devfs_node and terminate its cdev, after which allocv writes node->v_node into freed memory and calls reference_dev() on the freed cdev -> sysref_get on an object in SRF_PUTAWAY teardown. Reproduced twice with identical panic (assertion in sysref_get, stack reference_dev <- v_associate_rdev <- devfs_allocv <- devfs_vop_nresolve); guest dropped to db> and went down both times. On this INVARIANTS kernel the observable is a reliable unpriv panic; the underlying primitive is a UAF write (sysref refcount resurrect on freed cdev memory, groomable via the attacker's own ptmx clones) plus UAF reads leaking freed/reused node fields into struct stat (same family via devfs_vop_getattr, which runs with no vnode lock from vn_stat()/naccess()). A full uid=0 chain was not developed in this run; the blocker documented in VERDICT.md is that the write is a fixed-offset refcount increment on an object being concurrently torn down, making deterministic single-shot exploitation research-scale. Fix validated: patched kernel (make nativekernel with fix.diff) survived the identical PoC for 8+ minutes (>2x baseline time-to-panic) with no panic.",
 "exploit_chain": "open(/dev/ptmx)+close churn (creates/destroys pts cdevs+nodes) || lstat(/dev/pts/N)+access() loop -> nresolve/allocv window vs devfs core destroy thread -> node freed + cdev sysref putaway -> reference_dev(freed cdev) = sysref_get on dying object [panic on INVARIANTS; silent refcount resurrect/double-free on production] ; secondary window: unlocked devfs_vop_getattr copies freed/reused node fields into struct stat returned to userspace (info leak); uid0 chain: not developed (refcount-resurrect on concurrently-freed same-cache object, needs precise sysref-thread timing - documented as hard blocker)",
 "evidence": [
  "findings/poc/DF-3017/panic.txt:46-55 - panic transcript run 1 (sysref_get SRF_PUTAWAY assertion, reference_dev <- v_associate_rdev <- devfs_allocv <- devfs_vop_nresolve)",
  "findings/poc/DF-3017/panic.run2.txt - identical panic run 2 (cpuid=5)",
  "findings/poc/DF-3017/df3017.c - 3-thread ptmx churn + 8-thread lstat/access racer, runs as nobody",
  "findings/poc/DF-3017/fix_build.log - patched build/install/boot + baseline-vs-patched summary",
  "findings/poc/DF-3022/wedge-console.txt - patched rerun console: no sysref panic; wedge is the separate DF-3022 lock-order bug",
  "findings/poc/DF-3017/fix.diff - git-apply-able interlock (DEVFS_ALLOCVINPROG/FREEWAIT handshake)"
 ],
 "kernel_refs": [
  "sys/vfs/devfs/devfs_vnops.c:474-521",
  "sys/vfs/devfs/devfs_vnops.c:548-623",
  "sys/vfs/devfs/devfs_core.c:303-390",
  "sys/vfs/devfs/devfs_core.c:424-535",
  "sys/vfs/devfs/devfs_core.c:1440-1459",
  "sys/kern/vfs_subr.c:1211-1223",
  "sys/kern/tty_pty.c:278-287",
  "sys/kern/vfs_vnops.c:833-840",
  "sys/kern/vfs_nlookup.c:1632-1646",
  "sys/kern/vfs_default.c:1228-1234"
 ],
 "poc_changes": "PoC authored fresh for this finding (no seed): 3 ptmx-churn threads + 8 lstat/access threads, anomaly detector for UAF-read leakage into struct stat; run detached in-guest (nohup) because the panic kills ssh mid-run.",
 "attempts": 5,
 "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64",
 "runtime_sec": 3000,
 "guest_dirty": 0,
 "build_cmd": "cc -O2 -pthread -o df3017 df3017.c",
 "run_cmd": "./df3017 300   (as nobody; baseline panic <=4min x2; patched 8min no panic)",
 "code_hash": "9f331aecf3d52379cccf60ace77e88a2489172030c1c29285010532cea3006bf",
 "notes": "Both baseline runs panicked the stock INVARIANTS kernel (#0, Jul 2 2026) before the 300s racer finished; time-to-panic ~1-4 min. The assert maps exactly to the audited lifetime hole: devfs_allocv drops devfs_lock with a raw node pointer and never revalidates against devfs_freep(). devfs_vop_getattr is the same hole without even the lock (unfixable with devfs_lock alone because naccess holds the ncp across VOP_GETATTR_LITE - proven by the v1-fix deadlock; documented residual). Guest reset with-src after validation.",
 "recommended_fix": "Interlock devfs_allocv()'s devfs_lock drops with a DEVFS_ALLOCVINPROG node flag that makes devfs_freep() defer the physical free (completed on allocv revalidation), and make devfs_vop_getattr() take devfs_lock(SHARED) + hold a cdev reference across node->d_dev use.",
 "fix_status": "fixed",
 "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Sat Sep 5 10:33:01 UTC 2026 (fix.diff v2: allocv/freep interlock only)",
 "fix_baseline_reproduced": 1,
 "fix_patched_reproduced": 0,
 "fix_verdict": "Baseline: identical unpriv racer panicked the stock INVARIANTS kernel twice (sysref_get SRF_PUTAWAY assertion via devfs_allocv<-devfs_vop_nresolve, <=4 min). Patched (fix.diff v2 = DEVFS_ALLOCVINPROG/FREEWAIT interlock around devfs_allocv devfs_lock drops + DEVFS_DESTROYED revalidation), kernel #1 Sep 5 10:33:01: same PoC ran with zero sysref panics for the full window (baseline died <=4min 2/2); at ~6 min the guest wedged via a SEPARATE pre-existing lock-order bug (DF-3022, ncp-under-nresolve vs cache_inval-under-devfs_lock) which stock also carries but which is normally masked by this panic. fix.diff v2 introduces no namecache locking, so the wedge is not a regression. A first fix attempt that additionally took devfs_lock inside devfs_vop_getattr DID deadlock (naccess holds the ncp across VOP_GETATTR_LITE) and was removed; getattr locking needs an upstream-grade redesign and stays documented residual risk.",
 "fix_evidence": [
  "findings/poc/DF-3017/run.patched.log",
  "findings/poc/DF-3017/fix.diff"
 ]
}