No system-wide or per-user POSIX mqueue accounting: queues and their queued messages persist after the last close with no enumeration β unprivileged, unbounded, permanent-until-reboot kernel heap exhaustion
| Field | Value |
|---|---|
| ID | DF-2781 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-770 Allocation of Resources Without Limits |
| File | sys/kern/sys_mqueue.c |
| Lines | 71-76 (only limits), 391-399 (destroy gated on MQ_UNLINK) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
mq_close_fop() destroys a queue only when MQ_UNLINK is set; queued messages are kmalloc'd (M_MQBUF) and survive the creator's exit. The only limits are per-process open count and per-queue geometry β nothing bounds the system-wide queue count or queued bytes, and there is no way to enumerate queues, so memory stranded under withheld names is unreclaimable short of reboot. Demonstrated: 96.4 MB stranded then retained after process exit; sustained stranding drove the kernel to ENOMEM on mq_send/mq_open at ~390 MB on a 4 GB guest with 2.48 GB user memory free, with zero reclaim progress. Linux ships RLIMIT_MSGQUEUE + fs.mqueue.* sysctls for exactly this attack class.
Proof of contest
VERIFIED on the stock guest (findings/poc/DF-2781/strand.c):
./strand brief β 96.4 MB persists after exit; ./strand exhaust β
"mq_send #280: Cannot allocate memory" then mq_open ENOMEM at ~390 MB,
permanent. Fix (kern.mqueue.mq_max_queues sysctl, default 256,
maintained counter enforced at creation) validated with sysctl=8 β
strander stops after exactly 8 queues.
Recommended fix
See findings/poc/DF-2781/fix.diff; upstream should also consider per-uid byte limits (RLIMIT_MSGQUEUE model) and an enumeration/cleanup interface.
Timeline
- 2026-08-31 Discovered during pass-2 audit of sys_mqueue.c (GLM 5.3); permanent exhaustion reproduced unpriv same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2781 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| strand.c | β | 3.0 KB | view raw | |
| build.sh | β | 36 B | view raw | |
| run.sh | β | 291 B | view raw | |
| build.log | β | 151 B | view raw | |
| run.log | β | 412 B | view raw | |
| run.exhaust.log | β | 5.1 KB | view raw | |
| fix_validation.log | β | 490 B | view raw | |
| VERDICT.md | β | 2.4 KB | β raw | |
| verdict.json | β | 3.5 KB | view raw | |
| fix.diff | β | 1.6 KB | view raw | |
| env.txt | β | 119 B | view raw |
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)
- Persistence:
./strand briefstrands 96.4 MB (12 queues Γ 512 msgs Γ 16 KB); after the strander EXITED,vmstat -mshowsmqueues ... 97.5Mstill allocated (run.log) β the memory belongs to orphaned queues; POSIX semantics keep them (and their messages) alive untilmq_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. - Exhaustion:
./strand exhaustran until the KERNEL began failing allocations:mq_send #280: Cannot allocate memory, thenmq_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);vmstatstill 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 exited 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).
Fix verification
fixedWith kern.mqueue.mq_max_queues=8, strand exhaust stops after exactly 8 queues / 64.2 MB (mq_open ENOMEM at #9) and a second strander cannot create one queue; sysctl restored to 256. First fix iteration (cap check before lock acquisition, goto exit with mq==NULL) panicked the kernel - rejected, corrected placement; that incident surfaced DF-2782.
fix_validation.log; fixiter1 analysis in VERDICT.md
Confirmed kernel references
Detail
Exploit chain
loop: mq_open(unique, {maxmsg=512, msgsize=16352}) -> send 512 x 16 KB messages (8.4 MB/queue) -> close (queue+messages persist) -> exit strander -> kernel retains everything; repeat with new names until kmalloc ENOMEM; no reclamation path without the (withheld) names.
Evidence (decisive lines)
["run.log: 96.4 MB stranded then 'mqueues 7.54K 97.5M' AFTER process exit", "run.exhaust.log: 'mq_send #280: Cannot allocate memory' + 'mq_open: Cannot allocate memory' at ~390 MB; zero progress across 2.5 min of retries", 'fix_validation.log: with kern.mqueue.mq_max_queues=8 the strander stops after exactly 8 queues / 64.2 MB']
PoC changes
msgsize lowered 16384->16352 (the send-path cap includes the 32-byte mq_msg header, so attr-accepted 16384 always EMSGSIZEs on send); brief/exhaust modes; byte accounting includes header+slack.
Verified recommended fix
Add system-wide (and ideally per-uid) queue-count and queued-byte caps as sysctls (kern.mqueue.mq_max_queues in fix.diff, default 256); upstream should also consider an enumeration/cleanup interface.
Verdict
Unprivileged persistent kernel-memory exhaustion proven: 12 queues x 512 msgs x 16 KB = 96.4 MB remained allocated (vmstat -m: mqueues 97.5M) after the strander exited - queues and messages survive last close unless unlinked (sys_mqueue.c:391-399) and cannot be enumerated; sustained stranding drove the kernel to ENOMEM on mq_send/mq_open (M_WAITOK kmalloc failing) at ~390 MB on the 4 GB guest with 2.48 GB user memory still free, and retry loops made zero further progress - the ceiling is kernel-heap-side and the consumption is permanent until reboot. No system-wide or per-user queue/byte accounting exists (only per-process open count and per-queue geometry, sys_mqueue.c:71-76); Linux ships RLIMIT_MSGQUEUE + fs.mqueue.* for exactly this attack class.
No comments yet.