DF-2626 / verdict.json
{ "finding_id": "DF-2626", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "CONFIRMED on the stock guest (root fs hammer2): hammer2_read_file() (hammer2_vnops.c:926-985) discards uiomovebp()'s return at :983; on a user-page fault at byte 0 uiomove() (kern_subr.c:148-153) returns EFAULT without advancing uio_resid/uio_offset, so the loop predicate at :926 can never change and the thread spins in kernel mode forever holding the vnode lock SH (vfs_vnops.c:751) and ip->truncate_lock SH (:922). Empirically: an unprivileged read() of any readable hammer2 file into a PROT_NONE page pins one CPU at 100% system (STAT R, wchan '-') and the process SURVIVES two kill -9 attempts โ unkillable. readlink() is an equal vector (:790). Six readers on six distinct hammer2 files starved all 6 CPUs: ssh dead, serial console silent, no panic โ full system DoS requiring power cycle. tmpfs control returns EFAULT immediately (console: 'tmpfs_read uiomove error 14'), isolating the defect to hammer2's read loop. fix.diff (propagate the error, bqrelse preserved, break) was rebuilt in-guest (nativekernel) and validated: the identical PoC binary returns -1/EFAULT in 0.00 s on the patched kernel, with 50/50 good/bad read regression and the 6-file wedge pattern leaving 0 spinners and 98.9% idle. fix_status=fixed.", "exploit_chain": "", "evidence": [ "run.log: pid 988 uid 1001 STAT R2 100.0% CPU wchan '-' โ '>>> STILL ALIVE after kill -9 <<<' twice; top 'CPU2 99.85% df2626poc'", "serial_wedge2.log: serial console ends at '=== DF-2626 WEDGE2 ... ncpu=6' then total silence; wedge2.log + vm.sh status => down (all-CPU starvation, no panic)", "readlink_stock.log + serial_readlink_stock.log: single unprivileged readlink() wedged the stock box within ~60 s (ssh banner timeout, console silent)", "wedge1.log: tmpfs control โ same PoC on tmpfs returns -1/EFAULT instantly, kernel console prints 'tmpfs_read uiomove error 14'", "fix_run.log: patched kernel #1 (20:29:47), identical binary md5 โ 'read returned -1 errno=14' in 0.00 real, POC_EXIT=0", "fix_regress.log: good_reads=50 efault_reads=50; 'spinners left: 0'; CPU 98.9% idle after the 6-distinct-file pattern" ], "kernel_refs": [ "sys/vfs/hammer2/hammer2_vnops.c:983", "sys/vfs/hammer2/hammer2_vnops.c:926", "sys/vfs/hammer2/hammer2_vnops.c:922", "sys/vfs/hammer2/hammer2_vnops.c:986", "sys/vfs/hammer2/hammer2_vnops.c:790", "sys/kern/kern_subr.c:148", "sys/kern/vfs_vnops.c:751" ], "poc_changes": "Replaced seed trigger with deterministic fault-at-byte-0: mmap(PROT_NONE, MAP_FIXED) at 0x10000000 instead of a raw bad pointer ((void*)0x10000 can be mapped). Added readlink variant (same loop via hammer2_vop_readlink). Fixed the guest harness for DragonFly: no setsid(1)/procstat(1) on DF; su -m must not carry csh redirect syntax; backgrounded jobs need </dev/null to release the ssh pipe; all evidence teed to /dev/console to survive ssh death.", "attempts": 6, "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": 2580, "guest_dirty": 0, "build_cmd": "cc -O2 -Wall -o poc poc.c && cc -O2 -Wall -o readlink_poc readlink_poc.c", "run_cmd": "./poc /etc/rc (as unprivileged uid 1001, cwd on hammer2 root fs; ./readlink_poc likewise)", "code_hash": "0eaf49ab2c29b7c5cbbc95158b7f72cf641896565e07303c0d8bc5bf850e0472", "notes": "DoS only (no memory corruption โ no escalation chain applicable; primary objective (1) N/A by class, objective (2) met with full characterization). Per-CPU unkillable spin per read()/readlink() call; distinct files => one CPU each; all CPUs => silent starvation (no panic; needs hard reset; even root cannot kill offenders). Same-file readers interleave in getblk (100% sys total, box limping) โ distinct files remove the last sleep point. Write path at hammer2_vnops.c:1144 already checked the same error โ the read loop was the miss. In-guest fix_build.log was lost to the final snapshot revert; fix_build.log preserves verbatim poll excerpts (flagged). Guest left clean: final vm.sh reset with-src, stock kernel #0, no spinners.", "recommended_fix": "In hammer2_read_file(): capture the uiomovebp() result and break on error โ 'error = uiomovebp(bp, bp->b_data + loff, n, uio); bqrelse(bp); if (error) break;' (see fix.diff).", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Fri Aug 28 20:29:47 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff applied to guest /usr/src (patch -p1, hunk at :980), kernel rebuilt with make -j6 nativekernel KERNCONF=X86_64_GENERIC + make installkernel (hammer2_vnops.c compiled clean under -Werror), rebooted into kernel #1 (Fri Aug 28 20:29:47 UTC 2026). Baseline on stock #0: livelock reproduced (unkillable 100%-CPU spin; 6-file pattern wedges the box). Patched #1: the IDENTICAL PoC binary (md5 ed5f88f7...) returns -1/EFAULT in 0.00 s with exit 0 (fix_run.log), readlink variant likewise (fix_readlink.log), and regressions pass โ 50/50 good reads of /etc/rc unchanged, 50/50 faulting reads return EFAULT, the 6-distinct-file wedge pattern leaves 0 spinning processes and the box 98.9% idle (fix_regress.log). The previously-observed bad behavior is GONE with no read-path regressions: fix_status=fixed.", "fix_evidence": "fix_build.log (patch applied at :980; hammer2_vnops.o -Werror clean; BUILD_INSTALL_OK; kernel #1 20:29:47); fix_run.log (-1/EFAULT, 0.00 real, POC_EXIT=0); fix_readlink.log; fix_regress.log (good_reads=50 efault_reads=50, spinners left: 0, 98.9% idle)" } |