msgctl(IPC_STAT) leaks kernel heap pointers (msg_first/msg_last) and uninitialized padding to any local user
| Field | Value |
|---|---|
| ID | DF-0050 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-200 Exposure of Sensitive Information; CWE-909 Missing Initialization of Resource |
| File | sys/kern/sysv_msg.c |
| Lines | 324 (IPC_STAT copyout); struct in sys/sys/msg.h:68-84 |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
sys_msgctl(IPC_STAT) does copyout(msqptr, user_msqptr, sizeof(struct
msqid_ds)) with no field sanitization. struct msqid_ds (sys/sys/msg.h)
contains struct msg *msg_first (:70) and *msg_last (:71) β populated
kernel heap pointers β plus msg_pad1..msg_pad4 (:78,:80,:82,:83) that are
never zeroed (the boot-time msqids kmalloc at sysv_msg.c:130 omits
M_ZERO). Any local user who creates a queue (msgget(IPC_PRIVATE)) and sends
one message can read the kernel address of the msg header (and possibly
early-boot heap residue in the pad fields), defeating kernel heap ASLR / KASLR
and aiding exploitation of a separate memory-corruption bug.
Root cause
eval = copyout(msqptr, user_msqptr, sizeof(struct msqid_ds)); /* verbatim */
msg_first/msg_last are assigned at msgsnd enqueue (:762-768) / msgget
init (:427-428); the pad fields are never written.
Threat model & preconditions
- Attacker position: any local unprivileged user.
- Privileges gained or impact: information disclosure β a live kernel heap
pointer (the
struct msgaddress in theM_MSGslab) plus possible boot-time heap residue. A reliable KASLR / heap-ASLR bypass that converts a separate unreliable kernel memory-corruption bug into a reliable exploit. No direct code execution. - Required config or capabilities: none; default kernel.
- Reachability:
msgget(IPC_PRIVATE)+msgsnd+msgctl(IPC_STAT).
Proof of concept
PoC source: findings/poc/DF-0050/msg_leak.c
Build & run (unprivileged)
cc -o msg_leak findings/poc/DF-0050/msg_leak.c ./msg_leak
Expected output
Kernel heap addresses for msg_first/msg_last.
Impact
Unprivileged kernel-heap-pointer disclosure (KASLR/heap-ASLR bypass / exploit enabler). Same class as DF-0006/DF-0009/DF-0016/DF-0025. Rated Low (info-leak).
Recommended fix
Sanitize the pointer/pad fields in a local copy before copyout:
--- a/sys/kern/sysv_msg.c
+++ b/sys/kern/sysv_msg.c
@@ -321,7 +321,17 @@
}
- eval = copyout(msqptr, user_msqptr, sizeof(struct msqid_ds));
+ {
+ struct msqid_ds msqout = *msqptr;
+ msqout.msg_first = NULL;
+ msqout.msg_last = NULL;
+ msqout.msg_pad1 = 0;
+ msqout.msg_pad2 = 0;
+ msqout.msg_pad3 = 0;
+ bzero(msqout.msg_pad4, sizeof(msqout.msg_pad4));
+ eval = copyout(&msqout, user_msqptr, sizeof(msqout));
+ }
References
sys/kern/sysv_msg.c:324β verbatim IPC_STAT copyout.sys/sys/msg.h:70-71,78-83βmsg_first/msg_last/msg_pad*.- CWE-200; CWE-909.
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/sysv_msg.c. - pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0050 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| msg_leak.c | trigger-source | msgget+msgsnd+msgctl(IPC_STAT) leak PoC (header-comment compile-fixed, pad fields dumped) | 2.6 KB | view raw |
| build.sh | build-script | cc -o msg_leak msg_leak.c | 184 B | view raw |
| run.sh | run-script | ./msg_leak as unprivileged user | 264 B | view raw |
| build.log | build-log | final successful build, full output | 66 B | view raw |
| run.1.log | run-log | decisive unpatched run #1 (msg_first=0xfffff8008e0ea980) | 422 B | view raw |
| run.2.log | run-log | unpatched run #2 | 422 B | view raw |
| run.3.log | run-log | unpatched run #3 | 422 B | view raw |
| leak_sample.txt | leak-sample | leaked kernel heap pointers across runs + 2-msg variant | 1.4 KB | view raw |
| env.txt | environment | uname, kern.version, cc version, sysv msg config | 442 B | view raw |
| VERDICT.md | verdict | full narrative + fix validation before/after | 4.0 KB | β raw |
| README.md | readme | build/run/expected | 801 B | β raw |
| fix.diff | suggested-fix | git-apply-able fix: zero msg_first/msg_last/msg_pad* before copyout | 533 B | view raw |
| fix_build.log | build-log | full patched-kernel build log (rc=0) | 5.6 MB | β download |
| fix_run.log | run-log | patched-kernel run: msg_first=0x0 (leak gone) | 268 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0050 β PoC
msg_leak.c β unprivileged leak of kernel heap pointers (msg_first/
msg_last) + uninitialized padding via msgctl(IPC_STAT).
The bug
sys_msgctl IPC_STAT (sys/kern/sysv_msg.c:324) does
copyout(msqptr, user_msqptr, sizeof(struct msqid_ds)) with no sanitization.
struct msqid_ds (sys/sys/msg.h:68-84) carries struct msg *msg_first
(:70) and *msg_last (:71) (live kernel heap pointers) and msg_pad1..4
(:78,:80,:82,:83) that are never zeroed (boot kmalloc at sysv_msg.c:130
without M_ZERO).
Build & run (unprivileged)
cc -o msg_leak findings/poc/DF-0050/msg_leak.c ./msg_leak
Expected output (bug present)
msg_first = 0xffff<...> msg_last = 0xffff<...> -> kernel heap address disclosed to uid=1000 (KASLR/ASLR bypass)
DF-0050 β VERDICT
Verdict: REPRODUCED (info leak) β and the fix is VALIDATED.
The bug
sys_msgctl(IPC_STAT) at sys/kern/sysv_msg.c:324 does a verbatim
copyout(msqptr, user_msqptr, sizeof(struct msqid_ds)). struct msqid_ds
(sys/sys/msg.h:68-84) carries two kernel-internal pointers,
struct msg *msg_first (:70) and *msg_last (:71), that are populated
with live kernel heap addresses by msgsnd at sysv_msg.c:761-768. After a
queue has any message queued, an unprivileged msgctl(IPC_STAT) returns
those kernel pointers to userspace.
The boot-time msqids array is allocated without M_ZERO
(sysv_msg.c:130), and msgget (:419-436) initializes everything except
msg_pad1..msg_pad4 (msg.h:78,80,82,83); the pad fields are never written
and may carry boot-time heap residue. (In this guest they happened to read
zero, but the spec lets them leak.)
Threat model
Any local unprivileged user: msgget(IPC_PRIVATE) + msgsnd + msgctl(IPC_STAT).
No privilege, no special config, default GENERIC kernel (6.5-DEVELOPMENT #0).
Reproduction evidence (unpatched #0 baseline)
Single-message queue, 3 runs (deterministic β the struct msg headers are
allocated once at boot from a fixed pool, so the same slot is reused):
msg_first = 0xfffff8008e0ea980 <- live kernel heap pointer (M_MSG slab) msg_last = 0xfffff8008e0ea980
Two-message queue (proves BOTH ends of the list leak; addresses differ):
first = 0xfffff8008e0ea998 last = 0xfffff8008e0ea980 same=0 first = 0xfffff8008e0ea980 last = 0xfffff8008e0ea998 same=0
The 0xfffff800... upper 16 bits confirm these are in the DragonFlyBSD
kernel-map range (KVA). Cross-checked with kernel symbols (msghdrs at
0xffffffff8131dc88 BSS; the leaked values index into the heap array it
points at). This is a reliable KASLR / kernel-heap-ASLR bypass.
Impact
Info leak only β no memory corruption, no escalation chain possible. The
primitive is a single kernel heap pointer (struct msg *) per call, plus
possibly boot-time heap residue in the pad fields. Useful as an exploit
enabler to convert a separate unreliable kernel memory-corruption bug into
a reliable one. Rated Low (CWE-200 / CWE-909).
The fix
Sanitize the kernel-internal fields in a local copy before copyout:
struct msqid_ds msqout = *msqptr;
msqout.msg_first = NULL;
msqout.msg_last = NULL;
msqout.msg_pad1 = 0;
msqout.msg_pad2 = 0;
msqout.msg_pad3 = 0;
bzero(msqout.msg_pad4, sizeof(msqout.msg_pad4));
eval = copyout(&msqout, user_msqptr, sizeof(msqout));
The finding markdown's ## Recommended fix proposal matches this exactly
(no supersession). The standalone git apply-able diff is in fix.diff.
Fix validation (Phase 8)
Built a single-fix kernel (make -j6 nativekernel KERNCONF=X86_64_GENERIC,
rc=0) from the with-src base + this one diff, installed it as
/boot/kernel/kernel, rebooted, confirmed kern.version bumped to
#1: Mon Jul 13 00:07:02 UTC 2026.
Before (unpatched #0): msg_first = 0xfffff8008e0ea980
After (patched #1): msg_first = 0x0
Confirmed deterministically across 3 single-msg runs AND the 2-message queue (both ends now NULL). The fix closes the leak completely. The kernel also still functions normally (msgsnd/msgctl succeed, exit 0).
PoC changes
The shipped msg_leak.c had nested /* ... /* ... */ */ block comments
that broke the compiler (inner /* terminated the outer comment early,
stray ' became a token). Rewrote the header comment to use parenthesized
field descriptions instead of inner /* ... */ blocks, added #include
<unistd.h> / <stdint.h> / <sys/types.h>, hardened the build with
explicit IPC_RMID cleanup on error paths, and added pad-field dumps plus
a LEAK: classifier that recognizes the 0xffff... kernel-map signature.
Build: cc -o msg_leak msg_leak.c. No runtime/setup changes.
Reproduce
./build.sh && ./run.sh # as unprivileged maxx # buggy: msg_first = 0xfffff800... # fixed: msg_first = 0x0
Fix verification
fixedVALIDATED: baseline msg_first=0xfffff8008e0ea980; patched msg_first=0x0. x3 runs.
BEFORE #0: msg_first=0xfffff8008e0ea980. AFTER #1: msg_first=0x0 (NULL).
Confirmed kernel references
Detail
Exploit chain
none (pure info leak). No memory corruption. Exploit enabler only.
Evidence (decisive lines)
baseline #0: msg_first=0xfffff8008e0ea980 (kernel heap ptr). patched #1: msg_first=0x0 (NULL).
PoC changes
Fixed nested block comment compile bug in msg_leak.c. Added build.sh, run.sh, VERDICT.md, manifest.json, fix.diff, full logs.
Verified recommended fix
In sys_msgctl(IPC_STAT) at sysv_msg.c:324, copy to local struct, NULL out msg_first/msg_last, zero msg_pad1..4, then copyout the local. Matches finding markdown. Full git-apply-able diff in findings/poc/DF-0050/fix.diff.
Verdict
REPRODUCED. sys_msgctl(IPC_STAT) at sysv_msg.c:324 does verbatim copyout(msqptr,...) with no field sanitization. struct msqid_ds carries struct msg *msg_first/msg_last which msgsnd populates with live kernel heap addresses. As unprivileged uid=1001: msgget+msgsnd+msgctl(IPC_STAT) deterministically returns msg_first=0xfffff8008e0ea980 (kernel heap pointer). KASLR/heap-ASLR bypass.
No comments yet.