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)
PoC verification
Evidence pack
findings/poc/DF-5094 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_opsis notD_MPSAFE, so every vinum ioctl (including the wholevinum_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:83is pid-based; same-process threads are never ousted, and a busy daemon never re-checks); the daemon threads park in a non-PCATCHtsleep, so after the producers stop the process surviveskill -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.
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_testableNo 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
Confirmed kernel references
- sys/dev/raid/vinum/vinumdaemon.c:83
- sys/dev/raid/vinum/vinumdaemon.c:89-94
- sys/dev/raid/vinum/vinumdaemon.c:177
- sys/dev/raid/vinum/vinumdaemon.c:246
- sys/dev/raid/vinum/vinumioctl.c:296-301
- sys/dev/raid/vinum/vinum.c:58-70
- sys/dev/raid/vinum/vinum.c:459
- sys/kern/kern_device.c:570-577
- sys/dev/raid/vinum/vinuminterrupt.c:75
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.
No comments yet.