β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4599

VINUM_RESETCONFIG calls free_vinum() without lock_config(): frees all DRIVE/SD/PLEX/VOL arrays racing even the lock-holding VINUM_CREATE writer and every unlocked *CONFIG reader β†’ kernel heap UAF (read proven: panic; write/double-free windows on same interleaving)

Summary

The RESETCONFIG dispatch calls free_vinum(1) - which Frees all four global object arrays, bzeros vinum_conf, and reallocates - with no config lock; VINUM_CREATE holds lock_config precisely to serialize against this kind of mutation, so the creator's parse walks and the unlocked *CONFIG bcopy readers all race the array frees. REPRODUCED on guest (3-thread race, 2/3 runs): Fatal trap 12 in strcmp walking the freed VOL[] from the thread that HELD lock_config - DF-4478's fix does not cover free_vinum. Fix validated: patched kernel survived 2x240s at >=baseline load (also closes the DF-1741 fallthrough here). lock_config+setjmp-around-free_vinum+break fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4599 Β· 13 files
FileTypeDescriptionSize
resetconfig_race.c β€” 3.2 KB view raw
build.sh β€” 110 B view raw
run.sh β€” 118 B view raw
build.log β€” 219 B view raw
run.log β€” 1.2 KB view raw
panic.txt β€” 666 B view raw
fix.diff β€” 1.2 KB view raw
fix_build.log β€” 218 B view raw
fix_runs.log β€” 351 B view raw
env.txt β€” 150 B view raw
VERDICT.md β€” 4.5 KB ↓ raw
verdict.json β€” 4.9 KB view raw
manifest.json β€” 864 B view raw
VERDICT.md
↓ download raw

DF-4599 verdict β€” REPRODUCED (panic / kernel-heap UAF read; root-gated)

Root cause (code, certain)

  • vinumioctl() dispatches VINUM_RESETCONFIG at sys/dev/raid/vinum/vinumioctl.c:214-226: it checks vinum_inactive(0) (which itself takes and releases lock_config, vinum.c:179-187) and then calls free_vinum(1) without holding lock_config.
  • free_vinum() (sys/dev/raid/vinum/vinum.c:199-259) frees all four global object arrays β€” Free(DRIVE) :213, Free(SD) :228, Free(PLEX) :244, Free(VOL) :255 β€” bzero(&vinum_conf, …) and reallocates initial-size arrays via vinum_initconf() :258. It never takes the config lock.
  • Meanwhile VINUM_CREATE (vinumioctl.c:102-116) does hold lock_config() while parse_user_config β†’ find_volume()/get_empty_vol()/EXPAND walks and grows those same arrays (EXPAND β†’ expand_table() Free()s the old array, vinummemory.c:65-81). Every unlocked reader (VINUM_*CONFIG bcopy handlers, vinumioctl.c:118-178) is equally exposed.

So the lock that VINUM_CREATE relies on is simply ignored by the biggest destructor in the driver. Windows: (a) reader/writer walking an array freed by free_vinum (UAF read), (b) free_vinum's Free(VOL) racing EXPAND's Free(old) on the same chunk (double free), (c) parse_user_config writing into an array freed by free_vinum (UAF write).

How it was reproduced (stock kernel, kldload vinum, root)

3 threads hammering /dev/vinum/control: * creator: VINUM_CREATE ("volume vN", N monotonic β†’ constant EXPAND(VOL) churn under the lock) * resetter: VINUM_RESETCONFIG loop (free_vinum every iteration) * reader: VINUM_SDCONFIG loop (unlocked bcopy)

Run 2 of 3 (240 s) killed the guest: serial console shows Fatal trap 12: page fault while in kernel mode … supervisor read data, page not present … current process = 958 (resetconfig_race) … Stopped at strcmp+0xc (see panic.txt). strcmp on this path is find_volume()'s strcmp(VOL[i].name, name) walking the VOL[] array that the racing free_vinum had just freed β€” a freed/unmapped kernel-heap page fault (UAF read) in the process that held the config lock. Runs 1 (100 s) and 3 (240 s) survived: the race is probabilistic (the decisive free/realloc window is narrow relative to the destroy_dev churn inside free_vinum).

Impact

Root-gated (/dev/vinum/control 0600 + SYSCAP_RESTRICTEDROOT), so this is a post-root robustness / kernel-integrity bug: demonstrated kernel heap UAF read (panic); the same interleaving gives UAF-write (creator writing into freed arrays) and double-free windows. No unprivileged vector β€” the device cannot be opened by non-root. Realistic severity: Medium (matches DF-1739/DF-4478 precedent).

Fix validation (fix.diff)

fix.diff wraps free_vinum(1) in lock_config()/unlock_config() with a setjmp(command_fail) (free_vinum β†’ remove_drive β†’ save_config can throw_rude_remark under the lock), and adds the missing break that previously fell through into VINUM_SETSTATE (that fallthrough is DF-1741; leaving it while holding no lock would trade one bug for another, so the break is required by this fix too). Note vinum_inactive(0) must stay outside the lock (it takes the lock itself; nesting would deadlock).

  • Patched build: make nativekernel KERNCONF=X86_64_GENERIC rc=0, make installkernel rc=0 (kernel #1 Tue Sep 22 00:51:18 UTC 2026, vinum.ko rebuilt with the fix).
  • Patched runs: 2 Γ— 240 s at β‰₯ baseline load (1.51M+1.60M resetconfigs, 9.23M+3.61M sdconfigs, ~333K volume creations): guest survived both, no panic (fix_runs.log).
  • Baseline vs patched: 1 kill / 3 runs β†’ 0 kills / 2 runs at higher op counts. Fixed.

Negative results worth recording

  • bus_dmamap_unload/bus_dmamem_free NULL-guard their maps (sys/sys/bus_dma.h:312, busdma_machdep.c:553) β€” mpt's partial-init free path is safe.
  • expand_table bzeroes new slots, so VINUM_GETFREELIST on a drive_referenced slot (freelist_entries==0) is bounded β€” no new OOB there beyond known DF-1740.
  • VINUM_SDCONFIG/PLEXCONFIG/VOLCONFIG/DRIVECONFIG use unsigned compares on the index β€” negative indices are rejected (unlike the valid*/resetstats paths of DF-1739).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

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_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)
↓ fix.diffper-fix-DF-4599

Confirmed kernel references

Detail

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 (decisive lines)

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

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.

Verified 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)

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.