DF-3017 / verdict.json
{ "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" ] } |