DF-4599 / verdict.json
{ "finding_id": "DF-4599", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "VINUM_RESETCONFIG (vinumioctl.c:214-226) calls free_vinum(1) (vinum.c:199-259) which frees ALL vinum object arrays (DRIVE/SD/PLEX/VOL) without ever taking lock_config(), so it races even the lock-holding VINUM_CREATE writer and every unlocked *CONFIG reader. Reproduced on the stock guest kernel: 3-thread race (volume-creator via VINUM_CREATE with constant EXPAND(VOL) churn under the lock, VINUM_RESETCONFIG hammer, VINUM_SDCONFIG reader) killed the guest in run 2 of 3 with Fatal trap 12 ... supervisor read data, page not present ... Stopped at strcmp+0xc, current process = resetconfig_race - find_volume()s strcmp walking the VOL[] array freed out from under it by free_vinum. UAF-read proven; UAF-write (parse writing into freed arrays) and double-free (free_vinum Free vs EXPAND Free of same chunk) windows exist on the same interleaving. Root-gated: /dev/vinum/control is 0600 root:wheel plus caps_priv_check(SYSCAP_RESTRICTEDROOT) at open, so no unprivileged vector; severity is post-root kernel-integrity/robustness (Medium). Fix (lock_config + setjmp around free_vinum, plus the missing break so it no longer falls into VINUM_SETSTATE) built via nativekernel+installkernel and validated: patched kernel survived 2x240s at >= baseline load (1.5M+1.6M resetconfigs, 9.2M+3.6M sdconfigs) with no panic.", "exploit_chain": "root opens /dev/vinum/control (0600 root:wheel, SYSCAP_RESTRICTEDROOT); thread A: ioctl(VINUM_CREATE, volume vN) loop - parse_user_config/find_volume walks and EXPAND-grows VOL[] while holding lock_config; thread B: ioctl(VINUM_RESETCONFIG) loop - free_vinum Free(VOL)+bzero(vinum_conf)+vinum_initconf realloc, ignoring the lock; thread C: ioctl(VINUM_SDCONFIG) unlocked bcopy reader. Interleaving B-free vs A-walk => kernel heap UAF read (observed: page-fault panic in strcmp); B-free vs A-EXPAND-free => double free; B-free vs A-write => UAF write. Ceiling: kernel heap corruption from a root-only device - denial of service / kernel memory corruption behind root, not an unprivileged escalation.", "evidence": "panic.txt (Fatal trap 12, strcmp+0xc, process 958 = PoC, fault VA 0xfffff80119a18000 page-not-present); run.log (baseline: kill on run 2/3, survive runs 1+3); fix_runs.log (patched: 2x240s survived, 9.23M/3.61M reads + 1.51M/1.60M resets); fix_build.log (nativekernel rc=0); VERDICT.md for the full narrative", "kernel_refs": [ "sys/dev/raid/vinum/vinumioctl.c:214", "sys/dev/raid/vinum/vinum.c:199", "sys/dev/raid/vinum/vinum.c:213", "sys/dev/raid/vinum/vinum.c:255", "sys/dev/raid/vinum/vinummemory.c:65", "sys/dev/raid/vinum/vinumlock.c:227" ], "poc_changes": "v1 (single volume rv0 + RESETCONFIG + SDCONFIG) survived 90s: tokenize() stops at newline so only the first config line is parsed per ioctl and the arrays never grew, leaving only a tiny realloc window. v2 creates volume vN with monotonic N so every 4th ioctl triggers EXPAND(VOL) (Malloc/bcopy/Free of the old array) under lock_config, and adds a plex xN creator thread for PLEX churn; v2 killed the guest in 1 of 3 runs.", "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": 1140, "guest_dirty": 0, "build_cmd": "cc -O2 -o /root/resetconfig_race /root/resetconfig_race.c -lpthread", "run_cmd": "kldload vinum; /root/resetconfig_race 240", "code_hash": "8ff2f104b02a7e9b5f1bbdfffb48fec198600c281bbc3da0013902d391e1f315", "notes": "Race is probabilistic (decisive window is a few instructions wide against destroy_dev-dominated free_vinum): 1 kill / 3 baseline runs at 100-240s each. Distinct from DF-4478: the panic occurred in the thread HOLDING lock_config - DF-4478s fix (locking SETSTATE/ATTACH/DETACH) does not cover free_vinum. The fixs added break also resolves the DF-1741 RESETCONFIG->SETSTATE fallthrough (required: falling into setstate while releasing the lock would be a new unlocked path). vinum_inactive(0) stays outside the lock to avoid self-deadlock (it takes lock_config internally).", "recommended_fix": "Wrap free_vinum(1) in lock_config()/unlock_config() with setjmp(command_fail) throw-safety in the VINUM_RESETCONFIG case and add the missing break before VINUM_SETSTATE (see fix.diff)", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff (lock_config + setjmp around free_vinum, missing break added) applied to guest /usr/src, nativekernel + installkernel rc=0, rebooted into kernel #1 Sep 22 00:51:18; same 3-thread PoC survived 2x240s runs at >= baseline op counts with guest up and no panic.", "fix_evidence": "fix_runs.log (run1 survived 240s without a panic explicit; run2 all worker-exit counters printed, process exited, guest stayed up); fix_build.log (kernel build completed rc=0); env.txt (patched-kernel uname #1)" } |