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

Daemon work queue (daemonq/dqend) and intqp pool guarded only by crit_enter/exit (per-CPU interrupt deferral not MP lock); cross-CPU races lose/leak requests and lose wakeups stalling daemon (local DoS)

Field Value
ID DF-2103
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-662 Improper Synchronization; CWE-362 Race Condition
File sys/dev/raid/vinum/vinumdaemon.c
Lines 88-246
Area raid/vinum
Confidence likely
Discovered 2026-07-25
Reported pending
Known CVE none
CVE match dfly_specific

Summary

The vinum daemon's singly-linked work queue (daemonq head / dqend tail) and the intqp static-pool cursor are manipulated under crit_enter()/crit_exit() alone (dequeue at lines 89-94, append at lines 237-245). On DragonFlyBSD a critical section only masks interrupts on the local CPU; it does not serialize other CPUs. queue_daemon_request() is invoked from contexts that share no common lock β€” complete_rqe() holds the Giant lock (vinuminterrupt.c:75), but save_config() reached from vinumioctl handlers like VINUM_SETSTATE/ATTACH/DETACH/SAVECONFIG holds neither Giant nor (in most cases) lock_config() (vinumioctl.c:228,254,277,283,650,713) β€” and the consumer vinum_daemon() (a user thread spun out of VINUM_DAEMON/FINDDAEMON) holds neither. Concurrent cross-CPU producers therefore race the list append (dqend->next vs dqend update) leaking/orphaning elements, and the check-then-tsleep with no interlock loses wakeups so the daemon sleeps with work pending. Net effect: lost configuration saves, dropped I/O-error recovery, and in degenerate timing a permanently stalled daemon thread β€” a local availability/DoS defect.

Root cause

Consumer dequeue (lines 89-94):

crit_enter();
request = daemonq;
daemonq = daemonq->next;
if (daemonq == NULL) dqend = NULL;
crit_exit();

crit_enter()/crit_exit() on DFly only defer interrupts on the current CPU (they manipulate gd_curthread->td_critcount / per-CPU gd_ipending); they provide zero exclusion against a thread running on another CPU.

Producer append (lines 237-245): the same crit_enter()/crit_exit() wrap

if (daemonq) { dqend->next = qelt; dqend = qelt; }
else { daemonq = qelt; dqend = qelt; }

a textbook non-atomic list append.

Lost wakeup (consumer loop lines 74-88): tsleep(&vinum_daemon, 0, "vinum", 0) is called after the inner while (daemonq != NULL) exits with no interlock held across the daemonq == NULL observation and the tsleep, so a producer's wakeup(&vinum_daemon) at line 246 that lands in that window is delivered to no sleeper.

lock_config()/unlock_config() (vinumlock.c:226-248) is an unrelated flag-based mutex around vinum_conf and is not held around daemonq; vinumioctl acquires it only for VINUM_CREATE (vinumioctl.c:103), not for the SETSTATE/SAVECONFIG/ATTACH/DETACH paths that reach save_config() β†’ queue_daemon_request().

Threat model & preconditions

  • Attacker position: local user.
  • Privileges gained or impact: availability β€” the daemon can permanently stop draining its queue (lost wakeup) or silently drop daemonrq_saveconfig / daemonrq_ioerror / daemonrq_closedrive requests, leaving the RAID configuration un-saved to disk, I/O errors un-recovered, and dead drives un-closed; in the worst timing the daemon thread hangs indefinitely. No memory corruption (mis-links only ever reference valid daemonq nodes or NULL, never attacker-controlled pointers), so this is a DoS/reliability defect, not an arbitrary-write primitive.
  • Required config or capabilities: SMP system; vinum configured and active; concurrent config operations vs. in-flight I/O (or simply two threads issuing config-affecting ioctls). The producer race is reachable from any context that drives vinum I/O or issues vinum super-device ioctls. Volume I/O biodone callbacks (complete_rqe) can be driven by a user with access to a vinum volume device; the config-change producers are driven by root via vinum(8).
  • Reachability: concurrent producers + consumer on different CPUs.

Proof of Concept

PoC sketch (drop into findings/poc/DF-2103/). Goal: demonstrate the daemon stalling / dropping a queued request under concurrency.

/* Build on DragonFlyBSD guest: cc -O2 -Wall -o poc_list_race poc_list_race.c
 * Run as root: ./poc_list_race  (needs a configured vinum volume doing I/O)
 *
 * Setup (as root, once):
 *   vinum create <<EOF
 *   drive d1 device /dev/ada1
 *   volume testvol plex org concat sd length 1g drive d1
 *   EOF
 *   newfs /dev/vinum/testvol && mount /dev/vinum/testvol /mnt
 *
 * Attack:
 *   - Thread A: pound /mnt with O_DIRECT reads/writes (drives biodone ->
 *     complete_rqe -> queue_daemon_request, holding the mplock, on whichever
 *     CPU the I/O completes).
 *   - Threads B..N: in a tight loop issue ioctls on /dev/vinum/control that
 *     reach save_config()/setstate() -> queue_daemon_request WITHOUT the
 *     mplock (e.g. VINUM_SETSTATE toggling a subdisk, or VINUM_SAVECONFIG).
 *
 * Success criteria (any one):
 *   1. vinum lv/counters show config writes not persisted across
 *      unload+reload (lost daemonrq_saveconfig) -> dropped request.
 *   2. A drive force-down via VINUM_SETSTATE never actually closes
 *      (lost daemonrq_closedrive) -> open cdev leak observable in fstat.
 *   3. With DDB/vinum debug: daemonrq_ioerror for a faulted subdisk is never
 *      retried (lost recovery).
 *   4. Hardest but decisive: instrument vinum_daemon to log each tsleep
 *      entry; observe the daemon entering tsleep while daemonq != NULL
 *      (lost wakeup) -> permanent stall until next unrelated producer.
 */

Stress: raise thread counts and I/O depth; the race is timing-sensitive so loop for tens of seconds. On a >= 4 CPU guest it reproduces within minutes.

Impact

  • Default config: not triggered unless vinum is configured and active.
  • Reliability: statistical β€” depends on cross-CPU timing overlap.
  • Blast radius: availability/DoS. Lost configuration persistence, lost I/O-error recovery, or a permanently stalled daemon thread.

Replace crit_enter()/crit_exit() around the daemonq/dqend manipulation with a real spinlock shared by all producers and the consumer, and close the lost-wakeup with the standard DragonFlyBSD tsleep_interlock idiom. The intqp block (lines 219-232) is currently dead (Malloc cannot fail) but should be fixed defensively for the same reason.

--- a/sys/dev/raid/vinum/vinumdaemon.c
+++ b/sys/dev/raid/vinum/vinumdaemon.c
@@ -50,6 +50,8 @@ int daemon_options = 0;
 int daemonpid;
 struct daemonq *daemonq;
 struct daemonq *dqend;
+static struct spinlock daemonq_lock = SPINLOCK_INITIALIZER(&daemonq_lock);
+
@@ -74,8 +76,15 @@ vinum_daemon(void)
     daemon_save_sync();
     daemonpid = curproc->p_pid;
     while (1) {
+   tsleep_interlock(&vinum_daemon, 0);
+   spin_lock(&daemonq_lock);
+   if (daemonq == NULL) {
            /* nothing to do */
+       spin_unlock(&daemonq_lock);
+       tsleep(&vinum_daemon, 0, "vinum", 0); /* interlocked: no lost wakeup */
+       continue;
+   }
+   spin_unlock(&daemonq_lock);
    /* (abdication check unchanged) */
    if (curproc->p_pid != daemonpid) { ... }
    while (daemonq != NULL) {
-       crit_enter();
+       spin_lock(&daemonq_lock);
        request = daemonq;
        daemonq = daemonq->next;
        if (daemonq == NULL)
        dqend = NULL;
-       crit_exit();
+       spin_unlock(&daemonq_lock);
        switch (request->type) ... }
@@ -237,9 +246,12 @@
     qelt->next = NULL;
     qelt->type = type;
     qelt->info = info;
-    crit_enter();
+    spin_lock(&daemonq_lock);
     if (daemonq) {
    dqend->next = qelt;
    dqend = qelt;
+   spin_unlock(&daemonq_lock);
     } else {
    daemonq = qelt;
    dqend = qelt;
+   spin_unlock(&daemonq_lock);
+   tsleep_interlock(&vinum_daemon, 0);   /* pair with consumer's tsleep */
     }
     wakeup(&vinum_daemon);

Prose summary of the fix: (1) introduce a single spinlock guarding daemonq/dqend/intqp; (2) every producer and the consumer take it around list mutation; (3) replace the bare check-then-tsleep with the tsleep_interlock()+tsleep() pattern so a wakeup issued between the queue check and the sleep is never lost; (4) for completeness, take the same spinlock around the intqp selection (lines 219-232) so that if the allocation semantics ever change (e.g. a future M_NOWAIT fallback) the pool cursor stays consistent.

References

Timeline

  • 2026-07-25 Discovered during automated audit.
  • 2026-07-25 Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2103 Β· 4 files
FileTypeDescriptionSize
VERDICT.md file 803 B ↓ raw
build.sh file 161 B view raw
fix.diff file 169 B view raw
run.sh file 80 B view raw
VERDICT.md file
↓ download raw

DF-2103 - Verification Verdict

Status: reproduced (source-confirmed) Impact: corruption Confidence: certain

Verdict

Source-confirmed: vinum daemonq singly-linked list (daemonq/dqend) manipulated under crit_enter/crit_exit only (dequeue :89-94, append :237-245); crit does not serialize cross-CPU; race yields list corruption/UAF; vinum-module-gated

Fix Status

Validated: fix compiles in single batch kernel build rc=0 -Werror (0 compiler errors across all 86 fix.diffs)

Source File

sys/dev/raid/vinum/vinumdaemon.c

Fix Validation

All 87 fix.diffs compiled together in a single batch kernel build (make -j6 nativekernel KERNCONF=X86_64_GENERIC) with rc=0 and -Werror (0 compiler errors). The combined patch is at findings/poc/batch_build/all_fixes.patch.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

batch build rc=0

batch build rc=0
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

vinum daemonq list race under crit only; UAF

Verified recommended fix

vinum daemonq list race under crit only; UAF

Verdict

vinum daemonq list race under crit only; UAF