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

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

sys/kern/sysv_msg.c:324:

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 msg address in the M_MSG slab) 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).

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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0050 Β· 16 files
FileTypeDescriptionSize
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
README.md readme build/run/expected
↓ download 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)
VERDICT.md verdict full narrative + fix validation before/after
↓ download raw

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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline msg_first=0xfffff8008e0ea980; patched msg_first=0x0. x3 runs.

BEFORE #0: msg_first=0xfffff8008e0ea980. AFTER #1: msg_first=0x0 (NULL).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Mon Jul 13 00:07:02 UTC 2026

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.