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

msgsnd missing sequence-number revalidation after tsleep: stale sleeper injects into an IPC_RMID'd-and-reallocated queue (cross-user message injection)

Field Value
ID DF-2795
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:N
CWE CWE-367 / CWE-863
File sys/kern/sysv_msg.c
Lines 605 (recheck), 497 (entry check), 570 (unowned sleep)
Area kern
Confidence certain
Discovered 2026-08-31
Pass 2 (GLM 5.3 second pass)
Bucket privesc
Reported pending
Known CVE none
CVE match novel

Summary

sys_msgsnd validates the id's sequence number exactly once at entry. A sender that sleeps without owning MSG_LOCKED (only possible when another msgsnd is mid-copyin holding MSG_LOCKED β€” a blocking page fault releases msg_token) revalidates after tsleep with only msg_qbytes == 0, never the seq β€” unlike msgrcv (:977-978), proving oversight. IPC_RMID ignores MSG_LOCKED, so after RMID β†’ in-flight sender frees/unlocks β†’ another uid's msgget(IPC_PRIVATE) re-allocates the same slot (seq bumped), the stale sleeper sees qbytes!=0 and enqueues into the NEW queue under the OLD queue's IPC_W check.

Threat model & preconditions

Any unprivileged local user racing SysV message queues can inject attacker-chosen messages (arbitrary type + bytes) into another user's private mode-0600 message queue (data injection into a privileged service's command channel, cross-user integrity break); queue-budget consumption. No memory corruption (the recreated queue stays consistent) β€” ceiling is channel integrity, not uid=0. The DF-2677 IPC_RMID-family pattern in the msg flavor.

Proof of contest

VERIFIED on the stock guest (findings/poc/DF-2795/): attacker fills 39/40 slots; B's msgsnd body faults on a cold hammer2 page (~22ms holding MSG_LOCKED); 96 unowned senders sleep; IPC_RMID fires mid-fault; victim uid 1002's spinning msgget(IPC_PRIVATE, 0600) re-allocates the freed slot; a resuming sleeper enqueues type-0x2795 'PWNED-BY-DF-2795' β€” victim received it on its private queue (two independent baseline hits within 15-60s). Fix (mirror msgrcv's seq recheck into msgsnd's post-tsleep revalidation) in the pack.

See findings/poc/DF-2795/fix.diff (one-line).

Timeline

  • 2026-08-31 Discovered during pass-2 audit of sysv_msg.c (GLM 5.3); cross-user injection reproduced twice + fix prepared same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2795 Β· 10 files
FileTypeDescriptionSize
df2795.c β€” 9.5 KB view raw
victim.c β€” 2.6 KB view raw
run.log β€” 1.6 KB view raw
run.victim.log β€” 117 B view raw
hit.txt β€” 140 B view raw
run.fixed.log β€” 29.9 KB view raw
run.fixed.2.log β€” 23.7 KB view raw
fix.diff β€” 424 B view raw
kbuild.log β€” 5.6 MB ↓ download
kinst.log β€” 80.4 KB view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel (fix.diff, nativekernel+installkernel RC=0) survived 3524 identical race rounds (90s + 120s windows) with zero injections, versus baseline hits within <=50 rounds; 5619 formerly-injecting sleeper resumes now return EIDRM. Guest stayed up throughout.

["run.fixed.log: 'final: rounds=2020 real=1974 ... E22=166761 E82=1102' + NO-HIT", "run.fixed.2.log: 'final: rounds=1671 real=1550 ... E82=4517' + NO-HIT-2", 'kbuild.log / kinst.log: kernel build and install RC=0', 'fix.diff']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 1 10:51:33 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

unpriv uid A: (1) hold MSG_LOCKED via msgsnd whose body copyin faults on a cold hammer2 page (~22ms window); (2) 96 msgsnd sleepers pile up unowned behind the lock; (3) IPC_RMID the queue; (4) victim uid B's spinning msgget(IPC_PRIVATE,0600) re-allocates the freed slot microseconds later; (5) any sleeper that resumes after B's create injects attacker-chosen type+bytes into B's private queue. Victim receives and proves cross-user injection.

Evidence (decisive lines)

["hit.txt + run.victim.log: 'VICTIM uid 1002 RECEIVED INJECTED MESSAGE on its private queue qid=12845095 (slot 39): type=0x2795 PWNED-BY-DF2795' (original untouched baseline files)", 'run.log: baseline runs -- HIT at round 45 (~60s) and HIT within 15s; rc mix E22=3379/3854 EINVAL (fresh stale-id calls), E82=64/44 EIDRM (pre-recreate resumes), rc0 successes incl. injections', 'run.fixed.log / run.fixed.2.log: patched kernel, 1974+1550 rounds, NO hit, E82=1102+4517 EIDRM bounces', 'kbuild.log / kinst.log: make nativekernel + installkernel RC=0 on fix.diff kernel #1 (Tue Sep 1 10:51:33 UTC 2026)', 'VERDICT.md: full construction, ordering analysis, baseline-vs-patched comparison']

PoC changes

Four design iterations over the orchestrator's seed idea: (1) victim held the only free slot in a tight loop -> attacker msgget starved forever (real=0 over 525k tries); (2) shared-memory ctrl handshake (mmap'd word, fchmod 0666 to beat umask) so the victim waits for 'round queue created' before msgget-spinning; (3) fill-then-release-one to keep exactly one cycling slot; (4) libc msgsnd returns -1/errno, so the harness records -errno per thread, which exposed the E22/E82 split proving the mechanism. B's fault window made reliable with a 512MB hammer2 backing file, random offsets + posix_fadvise(DONTNEED), body starting in the last 8 bytes of a resident page (~22ms measured window).

Verified recommended fix

In sys_msgsnd's post-tsleep revalidation, also compare msqptr->msg_perm.seq against IPCID_TO_SEQ(uap->msqid) (mirroring sys_msgrcv) and return EIDRM on mismatch; validated in-guest.

Verdict

sys_msgsnd() checks the ipc sequence number only at entry (sysv_msg.c:497); after tsleep with we_own_it==0 (slept because another msgsnd held MSG_LOCKED mid-copyin), the post-wakeup revalidation (sysv_msg.c:605) rechecks only msg_qbytes, not the seq -- unlike msgrcv (sysv_msg.c:977-978). IPC_RMID ignores MSG_LOCKED (sysv_msg.c:254-282), so: RMID -> in-flight sender frees+unlocks -> other uid's msgget(IPC_PRIVATE) re-allocates the slot with a new seq -> the stale sleeper resumes, sees qbytes!=0, and enqueues into the NEW queue under the OLD queue's IPC_W check. Demonstrated on the stock guest: attacker uid 1001 injected type-0x2795 'PWNED-BY-DF2795' into uid 1002's IPC_PRIVATE mode-0600 queue within 15-60s (<=50 race rounds; two independent baseline hits, victim-side hit files preserved). No memory corruption (recreated queue state stays consistent; no panic reachable) -- ceiling is cross-user IPC channel integrity, not uid=0. Fix (seq recheck) validated in-guest: nativekernel rebuild, 3524 identical race rounds, zero injections, 5619 stale sleepers correctly returned EIDRM.