DF-2865 / verdict.json
{ "finding_id": "DF-2865", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "Reproduced on the 6-cpu stock INVARIANTS guest with a KLD harness that gives getpbuf_kva()/relpbuf() a per-subsystem reservation counter of cap 1 hammered by one kernel thread per CPU: the counter was observed NEGATIVE (vm.pbres_min=-1, vm.pbres_neg=2 in 10 s, console 'PBUFRES: counter NEGATIVE (-1)') and TWO pbufs were held simultaneously against the cap-1 reservation (vm.pbres_maxinflight=2), proving the <=0 gate at vm_pager.c:463 is not atomic with the decrement at :489-490 (per-bucket spinlocks give no cross-CPU exclusion for the counter). This is the allocator-side atomicity hole; it is distinct from known DF-0953 (release-side wake thresholds in relpbuf), though both manifest as unprivileged I/O latency (vfs_cluster.c mnt_pbuf_count / v_pbuf_count and swap_pager.c nsw_rcount are the real-world counters). fix.diff (atomic_cmpset_int reservation loop in all five allocators, buffer put back on lost race) validated on an in-guest rebuilt kernel #1: same harness shows neg=0, min=0, maxinflight=1 and clean unload.", "exploit_chain": "unpriv user: (1) generate concurrent clustered file I/O (reads/writes on any filesystem, vfs_cluster.c:924-927/1525-927 trypbuf_kva/getpbuf_kva against mnt_pbuf_count or v_pbuf_count) and/or force swap-in under memory pressure (swap_pager.c:1447 getpbuf_kva(&nsw_rcount)); (2) when such a counter sits near 0 under load, CPUs racing through the unlocked gate over-decrement it: the subsystem transiently exceeds its pbuf cap (over-consumption of the shared kva/raw pools, squeezing unrelated subsystems) and the counter dips negative, so getpbuf sleepers block on *pfreecnt<=0 while pbufs are actually free, extending stalls already made likely by DF-0953's wake thresholds; (3) self-correcting per release, so ceiling is local I/O-latency DoS, not memory corruption.", "evidence": [ "run.2.log: 'vm.pbres_neg: 2 / vm.pbres_min: -1 / vm.pbres_maxinflight: 2' + console 'PBUFRES: counter NEGATIVE (-1), cap=1, inflight=1'", "run.log: independent stock boot, same numbers; also shows 5 threads asleep on the reservation gate while vm.pbuf_kva_count=512 (pool 100% idle) - the known DF-0953 stall semantics this finding compounds", "run.patched.log (kernel #1 with fix.diff): 'vm.pbres_neg: 0 / vm.pbres_min: 0 / vm.pbres_maxinflight: 1', clean rescue+unload", "build.log / fix_build.log: module build and full nativekernel build logs" ], "kernel_refs": [ "sys/vm/vm_pager.c:417", "sys/vm/vm_pager.c:418", "sys/vm/vm_pager.c:444", "sys/vm/vm_pager.c:445", "sys/vm/vm_pager.c:463", "sys/vm/vm_pager.c:489", "sys/vm/vm_pager.c:490", "sys/vm/vm_pager.c:512", "sys/vm/vm_pager.c:538", "sys/vm/vm_pager.c:539", "sys/vm/vm_pager.c:570", "sys/vm/vm_pager.c:578", "sys/vm/vm_pager.c:602", "sys/vm/vm_pager.c:610", "sys/vm/swap_pager.c:1447", "sys/kern/vfs_cluster.c:924", "sys/kern/vfs_cluster.c:1525" ], "poc_changes": "PoC authored from scratch (no seed). Iterations: (1) volatile-qualified counters tripped -Werror=cast-qual in the module build -> plain int + atomics, matching kernel style; (2) first stock run wedged kldunload because cap=1 also exercises the known DF-953 wake-threshold stall (all sleepers gated, pbuf pool idle) -> added vm.pbres_bump rescue sysctl (pbuf_adjcount-style add+wakeup) for clean teardown; harness semantics unchanged.", "attempts": 3, "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": 75, "guest_dirty": 0, "build_cmd": "sh build.sh (KMOD=pbufres built in-guest against /usr/src/sys)", "run_cmd": "sh run.sh (kldload pbufres.ko; 10 s spin; sysctl vm.pbres_neg/min/maxinflight; sysctl vm.pbres_bump=100; kldunload pbufres)", "code_hash": "782a53b8abea0ab38509f1d54651f52d5a16badc44da66bb117042319dc268b1", "notes": "Both baseline boots (run.log, run.2.log) show identical stats; fix validation combined with the DF-2866 pagertab hunk in one nativekernel build (fix_build.log). Guest reset with-src afterwards, stock kernel #0 back, guest up. mnt_pbuf_count is additionally u_int type-punned through the int* API (sys/sys/mount.h:221) - benign on LE, noted for the record. vnode_pbuf_freecnt (vnode_pager.c:91) is written but never read anywhere - dead.", "recommended_fix": "Reserve the pfreecnt slot atomically at the point of dequeue: cmpxchg loop re-validating c>0 before c-1 (atomic_cmpset_int), put the buffer back on the free list and re-enter the gate when the reservation is lost; applied to getpbuf/getpbuf_kva/getpbuf_mem/trypbuf/trypbuf_kva - see fix.diff.", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Wed Sep 2 14:41:38 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Applied fix.diff to /usr/src in-guest (patch -p1, 6/6 hunks incl. the DF-2866 hunk), make -j6 nativekernel KERNCONF=X86_64_GENERIC + installkernel, rebooted into kernel #1. Exact same harness: baseline neg=2/min=-1/maxinflight=2; patched neg=0/min=0/maxinflight=1 with clean rescue/unload and normal ssh/dmesg operation. Reservation invariant holds; no regression seen.", "fix_evidence": [ "findings/poc/DF-2865/run.patched.log: 'vm.pbres_neg: 0 / vm.pbres_min: 0 / vm.pbres_maxinflight: 1' + 'PBUFRES: done. neg=0 min=0 maxinflight=1'", "findings/poc/DF-2865/fix_build.log: full nativekernel build log (BUILD-OK)", "findings/poc/DF-2865/fix.diff: git-apply-able (verified --check against pristine sys/)" ] } |