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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2795 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
fixedPatched 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']
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.
No comments yet.