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

Unbounded vinum daemon instantiation with pid-blind lazy abdication: N concurrent daemons, unkillable P_SYSTEM processes, non-unloadable module; the plausible multi-consumer daemonq double-free is refuted on current DragonFly only by incidental MP-token serialization

Summary

No single-instance check + a pid-based abdication that never ousts same-process or busy daemons - demonstrated on guest: 3 concurrent daemons, host survives kill -9 in D-state, module pinned. The dequeue double-free PoC ran 46.5M raced elements with ZERO corruption (the honest refutation): memory safety rests on the undocumented non-MPSAFE cdevsw MP-token property - a D_MPSAFE conversion turns the same PoC into a heap double-free. Single-instance + PCATCH + real queue-lock fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5094 Β· 13 files
FileTypeDescriptionSize
README.md β€” 1.6 KB ↓ raw
VERDICT.md β€” 4.1 KB ↓ raw
df5094_multidaemon.c β€” 4.2 KB view raw
build.sh β€” 126 B view raw
run.sh β€” 95 B view raw
build.log β€” 391 B view raw
run.log β€” 735 B view raw
run.2.log β€” 660 B view raw
env.txt β€” 592 B view raw
dmesg.txt β€” 718 B view raw
threadstate.txt β€” 1.1 KB view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 4.7 KB view raw

DF-5094 β€” vinum: unbounded daemon instantiation + pid-blind abdication

(multi-consumer double-free hypothesis REFUTED by MP-token serialization)

Build / Run (root on the DragonFly guest)

kldload vinum
cc -O2 -pthread -o /tmp/df5094 /tmp/df5094_multidaemon.c   # = build.sh
/tmp/df5094 90                                              # = run.sh

What to expect

  • Corruption hypothesis (double Free of one daemonq element by two concurrent daemons): does NOT reproduce. Two runs pushed 27,798,967 + 18,707,189 elements through 3 concurrent daemon threads; no panic, no slab messages, guest healthy. Root cause of the refutation: vinum_ops is not D_MPSAFE, so every vinum ioctl (including the whole vinum_daemon() loop) runs under the MP token (kern_device.c:570-577), and every daemonq producer holds the same token β€” the queue is fully serialized despite the crit-only guard. See VERDICT.md.
  • Residual defect (reproduces): N daemons run concurrently (abdication at vinumdaemon.c:83 is pid-based; same-process threads are never ousted, and a busy daemon never re-checks); the daemon threads park in a non-PCATCH tsleep, so after the producers stop the process survives kill -9 (D-state), and the module cannot be unloaded.

Files

df5094_multidaemon.c β€” 3 daemon threads (VINUM_DAEMON ioctl) + 4 producer threads (VINUM_SAVECONFIG ioctl storm). run.log/run.2.log β€” decisive runs. threadstate.txt β€” live wchan capture (2Γ— vinum, 1Γ— mp_token) + kill -9. dmesg.txt, env.txt, build.log β€” environment and compiler evidence.

VERDICT.md
↓ download raw

DF-5094 β€” vinum: unbounded daemon instantiation, pid-blind abdication; the

plausible multi-consumer double-free is REFUTED on current DragonFly (MP token)

What was claimed and what held

Claim investigated (pass-2 extension of DF-2103, which itself says "No mem corruption"): VINUM_DAEMON (vinumioctl.c:296) starts vinum_daemon() with no single-instance check; the "abdication" guard (vinumdaemon.c:83) compares curproc->p_pid with the daemonpid stamp and only runs after tsleep returns β€” so threads of one process (same pid) can never oust each other, and a busy daemon never re-checks. With the dequeue at vinumdaemon.c:89-94 guarded only by crit sections (no cross-CPU serialization of the queue itself), two concurrent daemons should double-dequeue one struct daemonq element and Free() it twice (vinumdaemon.c:177) β†’ kernel heap double-free.

What is real (certain, demonstrated on the guest): * any number of concurrent daemons can be instantiated β€” 3 ran at once; * abdication is pid-based and lazy: same-process daemons are NEVER ousted; * the daemons are permanent: P_SYSTEM + non-PCATCH tsleep β†’ the host process becomes unkillable (kill -9 leaves a D-state process β€” captured in threadstate.txt), aggravating the DF-4760 stop-wedge.

What is refuted (certain, demonstrated on the guest): * the memory corruption. Every path that touches daemonq executes while holding the MP token: - consumers: vinum_daemon() runs inside the VINUM_DAEMON/VINUM_FINDDAEMON ioctl β†’ dev_doperate_ops() takes get_mplock() because vinum_ops is NOT D_MPSAFE (vinum.c:58-70, kern_device.c:570-577). tsleep releases the token while asleep and reacquires it before returning, so the whole daemon loop body (dequeue, switch, Free) is token-serialized. - producers: save_config() from ioctls (token via the ioctl), the daemonrq_ioerror producer complete_rqe() explicitly get_mplock() (vinuminterrupt.c:75), daemonrq_closedrive from set_sd_state ioctls, and the daemonrq_return stop element from free_vinum() (module-event path). So the crit-only queue protection is redundant-but-safe in the current tree: the MP token is the real lock.

Empirical: two adversarial runs (3 daemon threads + 4 VINUM_SAVECONFIG producer threads on a 6-CPU guest) pushed 27,798,967 + 18,707,189 β‰ˆ 46.5M elements through the concurrent consumers with zero panics, zero slab/freelist corruption messages, guest healthy afterwards (run.log, run.2.log). Live thread states show the serialization directly: two daemons asleep on wchan vinum, the third blocked on mp_token (threadstate.txt).

Reproduce (root on the DragonFly guest)

kldload vinum
cc -O2 -pthread -o /tmp/df5094 /tmp/df5094_multidaemon.c   # or ./build.sh
/tmp/df5094 90                                               # or ./run.sh

Expected on this kernel: 3 concurrent daemons consume the storm cleanly, the process becomes unkillable at exit, no panic β€” i.e. the corruption claim does NOT reproduce; the unbounded-daemon / unkillable-process behavior DOES.

Notes

  • Note for DF-2103: the same MP-token invariant calls into question pass-1's "cross-CPU races lose/leak requests" impact on the current tree for the token-holding producers; the lost-wakeup liveness half of DF-2103 (check-then-tsleep with no interlock, vinumdaemon.c:74-88) remains valid β€” token serialization does not fix lost wakeups.
  • If vinum is ever converted to D_MPSAFE (or the daemon moved out of the ioctl context) without replacing crit sections with a real spinlock/mutex, this becomes the double-free described above β€” the PoC here is then the trigger.

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

No fix validation performed: the memory-corruption claim did not reproduce on the baseline (MP token serializes all daemonq access), so there is no observable bad behavior to flip; the residual unbounded-daemon/unkillable-process defect is robustness-level and root-gated. fix.diff intentionally omitted per the false-positive convention.

VERDICT.md 'What is refuted' section; run.log/run.2.log show no baseline corruption to compare a patched kernel against
per-fix-DF-5094

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log / run.2.log (46.5M raced elements, RC=0, guest up both runs); threadstate.txt (wchan capture: 2x 'vinum' sleepers + 1x 'mp_token' blocker inside the same process; kill -9 leaves PID 1316 in D1E; kldstat shows vinum.ko non-removable); VERDICT.md (full MP-token reasoning with path:line); dmesg.txt (no allocator complaints, only benign DIOCGPART rejections)

PoC changes

authored from scratch (no runnable seed): correct superdev node /dev/vinum/control (production name; /dev/vinum/Control is the VINUMDEBUG twin), ioctl letter L='F' (vinumio.h:39), VINUM_DAEMON=_IOC(0,'F',92,0) no-arg ioctl, VINUM_SAVECONFIG=_IOW('F',72,int) as the fast queue producer, VINUM_SETDAEMON noupdate to keep elements no-ops, and deliberately same-process daemon threads so the pid-based abdication check can never oust any of them

Verified recommended fix

Refuse VINUM_DAEMON/VINUM_FINDDAEMON when a daemon is already alive: track the daemon by its thread pointer (not pid) cleared on every exit path, reject re-entry with EBUSY, and make the daemon tsleep PCATCH-aware or wake it on process exit so its host process is killable; if D_MPSAFE is ever adopted, replace the crit sections around daemonq/dqend/intqp with a real spinlock.

Verdict

The multi-consumer heap double-free hypothesis (two concurrent vinum daemons double-dequeueing one daemonq element and Free()ing it twice at vinumdaemon.c:177) is REFUTED on current DragonFly: vinum_ops is not D_MPSAFE (vinum.c:58-70), so dev_doperate_ops() wraps every vinum ioctl in get_mplock() (kern_device.c:570-577); the daemon lives inside its VINUM_DAEMON ioctl and every daemonq producer (complete_rqe at vinuminterrupt.c:75, config ioctls, free_vinum) holds the same MP token, so dequeue/append/Free are token-serialized despite the crit-only guard. Two adversarial runs (3 daemon threads + 4 producer threads, 6-CPU guest) pushed 27.8M + 18.7M elements with zero corruption and a healthy guest; a live wchan capture shows two daemons asleep on 'vinum' and the third blocked on 'mp_token'. What IS real and demonstrated: VINUM_DAEMON spawns daemons unboundedly (vinumioctl.c:296), abdication is pid-based and lazy (vinumdaemon.c:83 β€” same-process threads are never ousted, a busy daemon never re-checks), and the parked daemon threads make the host process survive kill -9 in D state (captured), aggravating the DF-4760 stop-wedge. If vinum ever adopts D_MPSAFE or the daemon moves out of ioctl context without adding a real queue lock, this exact PoC becomes the double-free trigger.