# VERDICT — DF-2781: no system-wide mqueue cap → persistent unpriv kmem exhaustion

**Status: REPRODUCED** · impact: dos · confidence: certain

## What was proven on the guest (stock kernel, build #0, 4 GB RAM)

1. **Persistence**: `./strand brief` strands 96.4 MB (12 queues × 512 msgs ×
   16 KB); after the strander EXITED, `vmstat -m` shows `mqueues ... 97.5M`
   still allocated (run.log) — the memory belongs to orphaned queues; POSIX
   semantics keep them (and their messages) alive until `mq_unlink`
   (`sys_mqueue.c:391-399`), and there is no enumeration interface, so
   queues whose names are withheld can never be reclaimed short of reboot.
2. **Exhaustion**: `./strand exhaust` ran until the KERNEL began failing
   allocations: `mq_send #280: Cannot allocate memory`, then
   `mq_open: Cannot allocate memory` (M_WAITOK kmalloc returning NULL) at
   ~390 MB stranded (run.exhaust.log). 2.5 min of retry loops made zero
   progress (ceiling reached); `vmstat` still showed 2.48 GB of userland
   memory free — the failure is kernel-heap-side. Basic system ops still
   worked at that point (cc, file create), but the mqueue subsystem was
   permanently dead and 390 MB permanently consumed on a 4 GB machine.

Each queue may legally hold 512 × ~16.4 KB ≈ 8.4 MB; a single process may
open 512 such queues (~4.3 GB), exit, and repeat with new names. Linux
mitigates exactly this attack class with RLIMIT_MSGQUEUE + fs.mqueue.*
sysctls; DragonFly has only per-process open count and per-queue geometry
caps (`sys_mqueue.c:71-76`).

## Fix validation (patched kernel, build #1 04:57)

`fix.diff` adds `kern.mqueue.mq_max_queues` (default 256, root-tunable),
enforced at creation, with a maintained global counter (decremented at both
LIST_REMOVE sites).

Validation with the sysctl set to 8 (fix_validation.log): the same
`strand exhaust` stops after **exactly 8 queues / 64.2 MB**
(`mq_open: Cannot allocate memory` at #9), vmstat confirms 64.0 M held, and
a second strander cannot create even one queue. Sysctl restored to 256.

Note: a first fix iteration placed the cap check before the mq_mtx
acquisition and `goto exit`ed with `mq == NULL`, tripping the shared exit
path (`lockmgr` on a NULL-derived address) — that iteration was rejected and
the check moved; the incident also surfaced the latent stock bug filed as
DF-2782.

fix_status: **fixed** (fix_baseline_reproduced=1, fix_patched_reproduced=0).
