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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4760 Β· 22 files
FileTypeDescriptionSize
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_vinum wait loop), vinum.c:302-307 (MOD_UNLOAD), with sys/dev/raid/vinum/vinumdaemon.c:150-162 (daemon thread exits on the first daemonrq_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, vinumopen vinum.c:461-462) sets VF_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 vinum process parked in the VINUM_DAEMON ioctl) exits on the first daemonrq_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: kldunload wedges for as long as the holder likes, while holding the kernel linker lock β€” every kldload/kldunload/kldstat in 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; kldstat and vmstat -m blocked 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 logs
  • serial_5min_wedge.log β€” serial-console proof of the 300 s wedge + unload-at-close
  • fix.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 script
  • build.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 vinum hangs in state D / wchan vstop for the whole hold; devbuf grows ~50 leaked nodes/s; kldstat blocks; 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).
VERDICT.md
↓ download raw

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)

  • vinumopen sets VF_DAEMONOPEN when 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): while VF_STOPPING|VF_DAEMONOPEN are both set it queues one daemonrq_return per pass (~50-100/s, tsleep(...,1) at hz=100) and sleeps a tick.
  • queue_daemon_request (vinumdaemon.c:215-235) kmallocs a struct daemonq (M_DEVBUF) per call. With no kernel daemon loop alive (bare holder; or after the daemon already exited β€” it exits on the FIRST daemonrq_return, vinumdaemon.c:150-162), nothing ever drains the queue: every node leaks forever (teardown never frees daemonq; 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 every kldstat/kldload in the system blocks behind it (observed: kldstat and vmstat -m both hung during the 5-minute wedge).

Guest evidence (stock kernel, 2026-09-23)

  1. Caught in the loop (run-2, t=8 s of a 40 s hold): 1051 D2 vstop kldunload vinum β€” state D, wait channel vstop, the exact tsleep in free_vinum.
  2. Wedge duration == holder duration (run-4, 300 s hold): serial console shows the holder's open at 10:33:03 and vinum: unloaded at 10:38:03 = open+300 s to the second; no console progress in between; kldstat/vmstat -m hung system-wide during the window.
  3. 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).
  4. 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

fixed
baseline reproduced→ patch + rebuild →patched clean

vinum.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
↓ fix.diffper-fix-DF-4760

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.