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)
PoC verification
Evidence pack
findings/poc/DF-4599 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
DF-4599 verdict β REPRODUCED (panic / kernel-heap UAF read; root-gated)
Root cause (code, certain)
vinumioctl()dispatchesVINUM_RESETCONFIGatsys/dev/raid/vinum/vinumioctl.c:214-226: it checksvinum_inactive(0)(which itself takes and releaseslock_config,vinum.c:179-187) and then callsfree_vinum(1)without holdinglock_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 viavinum_initconf():258. It never takes the config lock.- Meanwhile
VINUM_CREATE(vinumioctl.c:102-116) does holdlock_config()whileparse_user_configβfind_volume()/get_empty_vol()/EXPANDwalks and grows those same arrays (EXPANDβexpand_table()Free()s the old array,vinummemory.c:65-81). Every unlocked reader (VINUM_*CONFIGbcopy 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_GENERICrc=0,make installkernelrc=0 (kernel #1 Tue Sep 22 00:51:18 UTC 2026,vinum.korebuilt 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_freeNULL-guard their maps (sys/sys/bus_dma.h:312,busdma_machdep.c:553) β mpt's partial-init free path is safe.expand_tablebzeroes new slots, soVINUM_GETFREELISTon adrive_referencedslot (freelist_entries==0) is bounded β no new OOB there beyond known DF-1740.VINUM_SDCONFIG/PLEXCONFIG/VOLCONFIG/DRIVECONFIGuse unsigned compares on the index β negative indices are rejected (unlike thevalid*/resetstatspaths 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
fixedfix.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)
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.
No comments yet.