free_vinum()'s daemon-stop loop wedges kldunload for the holder's chosen duration, permanently leaks one struct daemonq kmalloc per tick, and blocks kldload/kldstat system-wide via the linker lock
Summary
MOD_UNLOAD -> free_vinum wait loop queues daemonrq_return every tick while VF_DAEMONOPEN set; opening /dev/vinum/controld sets the flag without starting a daemon and the daemon loop exits after the FIRST return, so nothing drains the queue - every kmalloc'd daemonq leaks forever. Loop ends only when the holder closes. REPRODUCED on guest: 300s wedge (+14,600 leaked nodes, still leaked after unload; devbuf 5.10K->19.7K), kldstat/kldload blocked system-wide via linker lock, ~1.7MB/hr leak rate. Fix validated on rebuilt module: malicious holder -> EBUSY after ~10s bounded, legit daemon unload <1s zero leak (ask-once + bounded wait).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4760 Β· 22 files| File | Type | Description | Size | |
|---|---|---|---|---|
| hold_daemon.c | β | 1.8 KB | view raw | |
| run.sh | β | 283 B | view raw | |
| run2.sh | β | 1.6 KB | view raw | |
| run3.sh | β | 709 B | view raw | |
| run4.sh | β | 903 B | view raw | |
| fix2_run.sh | β | 2.2 KB | view raw | |
| df4760_run.log | β | 2.5 KB | view raw | |
| df4760_run2.log | β | 2.1 KB | view raw | |
| df4760_run3.log | β | 4.0 KB | view raw | |
| df4760_run4.log | β | 2.0 KB | view raw | |
| holder4.log | β | 144 B | view raw | |
| serial_5min_wedge.log | β | 988 B | view raw | |
| v2_holder.log | β | 143 B | view raw | |
| build.log | β | 122 B | view raw | |
| fix_build2.log | β | 975 B | view raw | |
| fix.diff | β | 2.5 KB | view raw | |
| fix_run.log | β | 2.8 KB | view raw | |
| env.txt | β | 484 B | view raw | |
| README.md | β | 4.1 KB | β raw | |
| VERDICT.md | β | 4.0 KB | β raw | |
| verdict.json | β | 3.8 KB | view raw | |
| manifest.json | β | 1.4 KB | view raw |
DF-4760 β vinum free_vinum() daemon-stop loop: unbounded per-tick kmalloc leak + kldunload wedge while /dev/vinum/controld is open (root-triggerable kernel DoS with system-wide lock blast radius)
- Files:
sys/dev/raid/vinum/vinum.c:215-219(free_vinumwait loop),vinum.c:302-307(MOD_UNLOAD), withsys/dev/raid/vinum/vinumdaemon.c:150-162(daemon thread exits on the firstdaemonrq_return, so at most one queued request is ever consumed) - Severity: LowβMedium demonstration (root-only trigger; permanent unbounded kernel heap leak + unload wedge + linker-lock DoS amplification)
- Bucket: base:raid / dos
What
while ((vinum_conf.flags & (VF_STOPPING | VF_DAEMONOPEN))
== (VF_STOPPING | VF_DAEMONOPEN)) { /* at least one daemon open */
queue_daemon_request(daemonrq_return, di); /* kmalloc EVERY pass */
tsleep(&vinumclose, 0, "vstop", 1); /* ~1 tick */
}
- Opening
/dev/vinum/controld(0600 root,vinumopenvinum.c:461-462) setsVF_DAEMONOPEN; the flag is refcount-free (any single close of a controld fd clears it, vinum.c:544-548, even if other fds are open). - Opening the device does not start the kernel-side daemon loop; and
the kernel daemon loop (running inside a userland
vinumprocess parked in theVINUM_DAEMONioctl) exits on the firstdaemonrq_returnβ so with a bare holder (no daemon), nothing ever drains the queue. - Each loop pass kmallocs a
struct daemonq(~24 B, M_DEVBUF, M_WAITOK) that is never freed: after teardown the queue head global is simply reset on the next module load. Leak is permanent and unbounded. - The loop only ends when the holder closes the fd:
kldunloadwedges for as long as the holder likes, while holding the kernel linker lock β everykldload/kldunload/kldstatin the system blocks behind it.
Reproduced (root on the DragonFly guest, stock kernel)
- Caught mid-wedge:
1051 D2 vstop kldunload vinum(ps at t=8 of run-2). - 300-second hold (run-4): "vinum: unloaded" logged exactly 300 s after
the holder opened (serial console); devbuf grew 5.10K β 19.7K
allocations (+14,600 leaked
struct daemonq, ~350 KB), still leaked after unload;kldstatandvmstat -mblocked system-wide during the wedge (run-4 log; linker lock held by the stuck unload thread). - Shorter holds (26-40 s) wedge identically (+1,850 / +1,180 leaked nodes).
Fix (validated)
fix.diff (this pack):
1. MOD_UNLOAD asks a live daemon to leave once (single
daemonrq_return), waits a bounded 5*hz, and returns EBUSY with
nothing torn down if the daemon device is still open;
2. free_vinum()'s loop replaced by queue-once + bounded wait.
Validated on a rebuilt module (same guest):
* malicious holder: kldunload β EBUSY after a bounded ~10 s, module
intact, devbuf +~10 allocations total (vs +14,600), clean unload after
the holder closes;
* legitimate daemon: kldunload β daemon asked once, exits and closes,
unload completes rc=0 in <1 s, zero leak.
Files
hold_daemon.cβ the holder (opens /dev/vinum/controld, sleeps, closes)run.sh/run2.sh/run3.sh/run4.shβ four baseline runs (run-4 is the 5-minute wedge)df4760_run*.log,holder4.logβ full untrimmed baseline logsserial_5min_wedge.logβ serial-console proof of the 300 s wedge + unload-at-closefix.diffβ combined vinum.c fix (DF-4760 hunks + the DF-4758 hunk)fix_run.logβ patched-module validation log (scenarios A and B)fix2_run.shβ validation scriptbuild.logβ holder build
Build / run (guest, root)
kldload vinum # fresh load, do NOT run any `vinum` command cc -O -o /tmp/hold_daemon /tmp/hold_daemon.c sh run4.sh # 300 s hold variant; or run.sh (40 s)
Expected
- Vulnerable:
kldunload vinumhangs in state D / wchanvstopfor the whole hold; devbuf grows ~50 leaked nodes/s;kldstatblocks; after the holder closes, unload completes and the leaked nodes stay leaked. - Patched: bounded wait then EBUSY (nothing torn down, ~1 node); clean unload once the daemon/holder is gone; legit-daemon unload path still works (ask-once, rc=0).
DF-4760 β VERDICT
REPRODUCED β status reproduced, impact dos, confidence certain.
Root-triggerable (open /dev/vinum/controld, 0600 root) kernel DoS:
kldunload vinum wedges for an attacker-chosen duration while leaking
kernel heap permanently, and blocks kldload/kldstat system-wide via the
linker lock.
Mechanism (path:line)
vinumopensetsVF_DAEMONOPENwhen a root process opens/dev/vinum/controld(vinum.c:461-462). No kernel daemon is started by the open.MOD_UNLOAD(vinum.c:302-307) βfree_vinum(0)(vinum.c:199) reaches the wait loop (vinum.c:215-219): whileVF_STOPPING|VF_DAEMONOPENare both set it queues onedaemonrq_returnper pass (~50-100/s,tsleep(...,1)at hz=100) and sleeps a tick.queue_daemon_request(vinumdaemon.c:215-235) kmallocs astruct daemonq(M_DEVBUF) per call. With no kernel daemon loop alive (bare holder; or after the daemon already exited β it exits on the FIRSTdaemonrq_return, vinumdaemon.c:150-162), nothing ever drains the queue: every node leaks forever (teardown never freesdaemonq; the global is just re-NULLed at next load, vinum.c:102-103).- The loop condition only clears when the holder closes the fd
(
vinumclose, vinum.c:544-548). Meanwhile the unloading thread holds the kernel linker lock, so everykldstat/kldloadin the system blocks behind it (observed:kldstatandvmstat -mboth hung during the 5-minute wedge).
Guest evidence (stock kernel, 2026-09-23)
- Caught in the loop (run-2, t=8 s of a 40 s hold):
1051 D2 vstop kldunload vinumβ state D, wait channelvstop, the exacttsleepinfree_vinum. - Wedge duration == holder duration (run-4, 300 s hold): serial
console shows the holder's open at 10:33:03 and
vinum: unloadedat 10:38:03 = open+300 s to the second; no console progress in between;kldstat/vmstat -mhung system-wide during the window. - Permanent unbounded leak: devbuf (M_DEVBUF) allocations
5.10K β 19.7K across the 300 s hold (+14,600
struct daemonq, ~350 KB at ~24 B/node β one per ~2 ticks), still leaked after the module finally unloaded; stair-step accumulation across successive runs (217K β 262K β 314K β 349K β 706K in-use). - Shorter holds (26 s / 40 s) wedge identically (logs in pack).
A holder that never closes (or a wedged/malicious root daemon) makes the
wedge and the leak unbounded in time: ~1.7 MB/hour of permanent kernel
heap loss plus indefinite kldload/kldstat unavailability system-wide.
Fix validation (performed in the same run)
fix.diff (this pack, v2):
1. MOD_UNLOAD: if VF_DAEMONOPEN is set, queue one
daemonrq_return, wait a bounded 5*hz ticks for the daemon to leave,
and return EBUSY with nothing torn down if it does not.
2. free_vinum: replace the per-tick queue+sleep loop with queue-once +
bounded wait.
Rebuilt vinum.ko from patched /usr/src (BUILD_RC=0), installed,
loaded, validated:
- A (malicious holder, no daemon):
kldunloadβ "Device busy" (rc=1) after a bounded ~10 s; module intact; devbuf +~10 allocations total (vs +14,600); no stuck kldunload; after the holder closes,kldunloadβ rc=0, clean unload. - B (legitimate daemon started by
vinum create):kldunloadβ the daemon is asked once, exits and closes, unload completes rc=0 in <1 s, zero leak ("vinum: unloaded" on console; daemon process gone).
An earlier fix attempt (v1: plain EBUSY pre-check) was refuted on the
guest β it made the legit-daemon unload path impossible β and was replaced
by the ask-first design above. Final verdict: fix_status=fixed,
fix_baseline_reproduced=1, fix_patched_reproduced=0.
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
fixedvinum.ko rebuilt from /usr/src patched with fix.diff v2 (make rc=0), installed, loaded. A) malicious holder: kldunload -> 'Device busy' rc=1 after a bounded ~10 s, module intact, devbuf +~10 allocations (baseline: 300 s wedge + 14,600 leaked nodes + system-wide kldstat blockage); after holder close, clean rc=0 unload. B) legit daemon: asked once, exits, unload rc=0 in <1 s, zero leak. Bad behavior gone on both paths.
findings/poc/DF-4760/fix_run.log (scenarios A and B with baseline comparisons); fix_build2.log; fix.diff in the same pack
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-4760/df4760_run2.log: '1051 D2 vstop kldunload vinum' caught mid-loop and devbuf 2.07K->3.92K; serial_5min_wedge.log: holder open 10:33:03 -> 'vinum: unloaded' 10:38:03 (=open+300s), devbuf 5.10K->19.7K (+14,600 leaked nodes); fix_run.log: patched module returns EBUSY after a bounded ~10 s with module intact and devbuf +~10, legit-daemon unload rc=0 <1 s
PoC changes
no seed; wrote hold_daemon.c (open/sleep/close holder) and four run scripts; run-1 initially showed an early loop exit caused by the leftover legit vinum daemon answering the first daemonrq_return -- fixed by using a fresh kldload with no vinum commands issued; ps visibility quirks on DF worked around with devbuf sampling + serial-console timestamps
Verified recommended fix
MOD_UNLOAD: queue a single daemonrq_return, wait a bounded 5*hz, return EBUSY (nothing torn down) if the daemon device is still open; make free_vinum's wait queue-once and bounded (see fix.diff)
Verdict
free_vinum()'s daemon-stop loop (vinum.c:215-219) queues one daemonrq_return per tick while VF_DAEMONOPEN is set, but nothing is guaranteed to drain the queue (opening /dev/vinum/controld starts no daemon, and the kernel daemon loop exits after the FIRST daemonrq_return), so kldunload with a root holder on the daemon device wedges for the holder's chosen duration, permanently leaks one struct daemonq kmalloc per pass, and blocks kldload/kldstat system-wide via the kernel linker lock. Reproduced on the stock-kernel guest across 4 runs; the 300 s hold wedged kldunload exactly 300 s (serial console) and leaked +14,600 nodes (~350 KB) that remain leaked after unload; kldstat and vmstat -m hung during the wedge. Root-triggerable (controld is 0600 root), hence DoS severity, not privesc.
No comments yet.