# 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

```c
    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).
