DF-3120 / verdict.json
{ "finding_id": "DF-3120", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "Race double-execution of ufs_quotaoff proven on a QUOTA-enabled DragonFly 6.5-DEVELOPMENT x86_64 INVARIANTS kernel: two quotactl-capable threads passing the um_quotas[type]==NULLVP test at ufs_quota.c:503 both run the teardown. Clean baseline panicked <=45s with panic: VOP_STDCLOSE: BAD OPENCOUNT (stack vn_close<-ufs_quotaoff<-ufs_quotactl) proving the double vn_close at :519; an instrumented run of the same race trapped at crfree+0xc with fault address 0x0, the crfree(NOCRED) NULL-deref at :521 (NOCRED==(struct ucred *)0). QTF_CLOSING (:505) is never tested by quotaoff itself and vfs_busy (ufs_vfsops.c:141) is a shared pin, so nothing serializes. Both manifestations match the source analysis. Privilege gate: every path in needs caps_priv_check(SYSCAP_NOQUOTA_WR) (or unmount-side quotaoff, which vfs_busy/MNTK_UNMOUNT serializes), so this is privileged-admin kernel corruption/DoS (Medium), not an unprivileged escalation; realistic ceiling is a delegated quota-capable process turning its narrow capability into a vnode UAF. Stock X86_64_GENERIC has no options QUOTA (quotactl=EOPNOTSUPP), so a QUOTA kernel was built in-guest for verification. fix.diff (per-mount um_quota_lock; vp unlocked before lock in quotaon) was validated: identical PoC survived 300s/19M ops and a 120s confirmation with zero panics vs <=45s panic baseline.", "exploit_chain": "", "evidence": [ "findings/poc/DF-3120/panic.txt \u2014 baseline panic: panic: VOP_STDCLOSE: BAD OPENCOUNT, stack vop_stdclose<-vop_close<-vn_close<-ufs_quotaoff<-ufs_quotactl", "findings/poc/DF-3120/panic.diag.txt \u2014 instrumented run: DF3120-DIAG: race window entered (in-flight=1) then Fatal trap 12 ... fault virtual address = 0x0, Stopped at crfree+0xc: cmpq $0,(%rdi)", "findings/poc/DF-3120/run.log \u2014 patched kernel: SURVIVED ... EXITED_RC=0 over 300s (17.8M offs) + 120s confirmation, dmesg panic count 0", "findings/poc/DF-3120/fix.diff \u2014 validated fix (git-apply-able, patch(1) -p1 verified byte-identical to tested kernel source)", "findings/poc/DF-3120/env.txt, VERDICT.md, README.md" ], "kernel_refs": [ "sys/vfs/ufs/ufs_quota.c:503", "sys/vfs/ufs/ufs_quota.c:505", "sys/vfs/ufs/ufs_quota.c:519", "sys/vfs/ufs/ufs_quota.c:521", "sys/vfs/ufs/ufs_quota.c:522", "sys/vfs/ufs/ufs_quota.c:437", "sys/vfs/ufs/ufs_quota.c:448", "sys/vfs/ufs/ufs_quota.c:909", "sys/vfs/ufs/ufs_vfsops.c:141", "sys/sys/ucred.h:82", "sys/kern/vfs_lock.c:354" ], "poc_changes": "Fixed QCMD packing (kernel Q_* constants are pre-shifted: cmds = Q_cmd<<8 | type, e.g. 0x10000 for user-quota Q_QUOTAON \u2014 the seed sketchs semantics double-shifted correctly by accident but sanity tools used 0x100 and got EINVAL); added 8 rotating quota files and a distinct /qtmnt/ctrlN control path per thread (a shared control files namecache entry serializes the whole quotactl syscall and hides the race \u2014 with a shared path 1.5M quotactls produced zero window entries); unprivileged nobody children for Q_SYNC/Q_GETQUOTA/churn; children poll /tmp/QTSTOP (inherited flag never propagates to forked children); discovered stock kernel needs options QUOTA (X86_64_QUOTA built in-guest via make nativekernel).", "attempts": 4, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Sep 6 20:12:38 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_QUOTA x86_64 (baseline panic kernel: same tree #0 Sun Sep 6 18:19:17 UTC 2026)", "runtime_sec": 9600, "guest_dirty": 0, "build_cmd": "cc -O -pthread -o /root/qt_race /root/qt_race.c (guest kernels: cp X86_64_GENERIC X86_64_QUOTA + options QUOTA; make nativekernel && make installkernel KERNCONF=X86_64_QUOTA)", "run_cmd": "vnconfig -c /dev/vn0 /qt.img && newfs -m 0 /dev/vn0 && mount -t ufs /dev/vn0 /qtmnt && /root/qt_race 100", "code_hash": "1de2bc5175159a8b9366655e5e325fd9f631d208b17b064c5366ab0a9572c5b3", "notes": "Panic reproduced twice (two distinct manifestations: double vn_close via VOP_STDCLOSE BAD OPENCOUNT on clean baseline; crfree NULL-deref on instrumented kernel). No unprivileged trigger exists (privilege-checked entry), so no uid0 chain is possible for this bug; impact ceiling documented in VERDICT.md. Fix validation also caught and corrected a lock-order bug in my first fix iteration (vp lock held across um_quota_lock) \u2014 documented. Related-but-distinct from known DF-0847 (unlocked global dquot hash/free-list, unpriv-vs-unpriv) and DF-0846 (sequential cred/vp leak in quotaon): this finding is the mount-level quota state machine with a privileged race pair.", "recommended_fix": "Serialize ufs_quotaon/ufs_quotaoff with a per-mount lockmgr lock (um_quota_lock, lockinit in ffs_mountfs); wrap quotaoff body as ufs_quotaoff_locked so quotaons internal swap-call doesnt self-deadlock; unlock the quota-file vp before taking the lock.", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Sep 6 20:12:38 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_QUOTA x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Baseline panicked <=45s (VOP_STDCLOSE: BAD OPENCOUNT via ufs_quotaoff->vn_close); patched kernel survived the identical PoC for 300s (1.5M Q_QUOTAON + 17.8M Q_QUOTAOFF) plus a 120s confirmation run, exit 0, zero panics, mount healthy. Bad behavior gone.", "fix_evidence": [ "findings/poc/DF-3120/panic.txt (baseline panic)", "findings/poc/DF-3120/run.log (patched SURVIVED, RC=0, x2 runs)", "findings/poc/DF-3120/fix.diff (validated, patch -p1 byte-identical to tested source)" ] } |