Uninitialized struct cmsgcred leaks kernel stack via synthesized SCM_CREDS (SO_PASSCRED)
| Field | Value |
|---|---|
| ID | DF-0010 |
| 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-908 Use of Uninitialized Resource; CWE-200 Exposure of Sensitive Information |
| File | sys/kern/uipc_usrreq.c |
| Lines | 683, 695-697, 1734-1744 |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
In uipc_send()'s AF_UNIX SOCK_DGRAM path, when the receiver has
SO_PASSCRED set and the sender did not include an SCM_CREDS control
message, the kernel synthesizes one from an uninitialized on-stack
struct cmsgcred cred. sbcreatecontrol() copies sizeof(cred) bytes of that
stack garbage into the mbuf, then unp_internalize() (SCM_CREDS branch) only
fills cmcred_pid/uid/euid/gid/ngroups and groups[0..ngroups-1].
The tail groups[ngroups..CMGROUP_MAX-1] (up to 15*4 = 60 bytes) retain
whatever was on the kernel stack and are delivered verbatim to the receiver
via recvmsg().
Root cause
sys/kern/uipc_usrreq.c:683 declares struct cmsgcred cred; with no
initializer. sys/kern/uipc_usrreq.c:695-696 calls
sbcreatecontrol(&cred, sizeof(cred), SCM_CREDS, SOL_SOCKET), which
(uipc_sockbuf.c) copies the full sizeof(cred) bytes โ including the
uninitialized tail โ into the control mbuf. unp_internalize then, in the
SCM_CREDS branch (uipc_usrreq.c:1734-1744), fills only:
cmcred->cmcred_pid = p->p_pid;
cmcred->cmcred_uid = ... cr_ruid;
cmcred->cmcred_gid = ... cr_rgid;
cmcred->cmcred_euid = ... cr_uid;
cmcred->cmcred_ngroups = MIN(...cr_ngroups, CMGROUP_MAX);
for (i = 0; i < cmcred->cmcred_ngroups; i++)
cmcred->cmcred_groups[i] = ... cr_groups[i];
It never writes groups[ngroups..CMGROUP_MAX-1]. CMGROUP_MAX == 16
(sys/sys/socket.h:422) and struct cmsgcred.cmcred_groups[16]
(sys/sys/socket.h:437), so up to (16 - ngroups) * 4 bytes of kernel stack
are delivered to the receiver.
Threat model & preconditions
- Attacker position: any local unprivileged user.
- Privileges gained or impact: information disclosure. Each message leaks up to ~60 bytes of kernel stack whose contents depend on prior frames (pointer fragments, etc.). A samplable KASLR / stack-residue oracle that lowers the bar for exploiting a separate kernel bug. Not a direct LPE.
- Required config or capabilities: none; default kernel.
- Reachability:
socketpair(AF_UNIX, SOCK_DGRAM),setsockopt(SO_PASSCRED)on one end,sendplain data from the other (noSCM_CREDS),recvmsgon the marked end. Fully self-reachable (sender == receiver).
Proof of concept
PoC source: findings/poc/DF-0010/leak_cmsgcred.c
Build & run
cc -o leak_cmsgcred findings/poc/DF-0010/leak_cmsgcred.c ./leak_cmsgcred # as a non-root user
Expected output
sample 0: pid=... uid=... ... ngroups=1
UNFILLED groups [1..15] = { 0x<..> 0x<..> ... }
result: LEAK CONFIRMED (kernel-stack bytes in unfilled groups)
Impact
Low-impact kernel-memory info leak, samplable in a tight loop. Useful as a KASLR/heap-grooming oracle ingredient. Rated Low.
Recommended fix
Zero the synthesized cred:
--- a/sys/kern/uipc_usrreq.c
+++ b/sys/kern/uipc_usrreq.c
@@ -680,7 +680,7 @@
if (so2->so_options & SO_PASSCRED) {
struct mbuf **mp;
struct cmsghdr *cm;
- struct cmsgcred cred;
+ struct cmsgcred cred = {};
struct mbuf *ncon;
References
sys/kern/uipc_usrreq.c:683โ uninitializedcred.sys/kern/uipc_usrreq.c:1734-1744โ partial field fill (tailgroupsunwritten).sys/sys/socket.h:422,437โCMGROUP_MAX,cmcred_groups[16].- CWE-908 Use of Uninitialized Resource.
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/uipc_usrreq.c. - pending Reported to DragonFlyBSD security contact.
PoC verification
Evidence pack
findings/poc/DF-0010 ยท 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| leak_cmsgcred.c | trigger-source | minimal AF_UNIX SOCK_DGRAM SO_PASSCRED uninit-leak trigger | 3.1 KB | view raw |
| README.md | readme | build/run/expected + root-cause | 1.4 KB | โ raw |
| VERDICT.md | verdict | full REPRODUCED narrative: trigger->primitive->effect with path:line | 4.5 KB | โ raw |
| build.sh | repro-script | cc -o leak_cmsgcred leak_cmsgcred.c | 122 B | view raw |
| run.sh | repro-script | ./leak_cmsgcred | 226 B | view raw |
| build.log | build-log | final successful build, full output | 13 B | view raw |
| run.log | run-log | decisive single run, full output | 1.2 KB | view raw |
| leak_sample.txt | leak-sample | 3 stress runs showing varying leaked kernel-stack bytes incl. KASLR pointers | 3.7 KB | view raw |
| env.txt | environment | uname, cc version, kern.ipc sysctls | 307 B | view raw |
| fix.diff | suggested-fix | git-apply-able: bzero(&cred,...) at uipc_usrreq.c:683 to zero the synthesized cmsgcred | 316 B | view raw |
DF-0010 โ PoC
leak_cmsgcred.c โ unprivileged leak of kernel-stack bytes via the
uninitialized struct cmsgcred synthesized by SO_PASSCRED.
The issue
uipc_send() (sys/kern/uipc_usrreq.c:680-700), AF_UNIX SOCK_DGRAM,
receiver has SO_PASSCRED and sender sent no SCM_CREDS:
struct cmsgcred cred; /* :683 uninitialized */
...
ncon = sbcreatecontrol(&cred, sizeof(cred), SCM_CREDS, SOL_SOCKET); /* :695 */
unp_internalize(ncon, msg->send.nm_td);
sbcreatecontrol copies sizeof(cred) bytes of stack garbage into the mbuf;
unp_internalize SCM_CREDS (uipc_usrreq.c:1734-1744) only fills
cmcred_pid/uid/euid/gid/ngroups and groups[0..ngroups-1]. The tail
groups[ngroups..CMGROUP_MAX-1] (up to 15*4 = 60 bytes) retain the leaked
kernel-stack bytes and are delivered to the receiver via recvmsg.
Build
cc -o leak_cmsgcred findings/poc/DF-0010/leak_cmsgcred.c
Run
As an unprivileged user:
./leak_cmsgcred
Expected output (bug present)
sample 0: pid=... uid=... ... ngroups=1
filled groups [0..0] = { <gid> }
UNFILLED groups [1..15] = { 0x<..> 0x<..> ... } <-- leaked kernel stack
result: LEAK CONFIRMED (kernel-stack bytes in unfilled groups)
The unfilled group words vary across runs and are kernel-stack residue (pointer fragments, etc.) โ a samplable KASLR/stack-residue oracle. On a fixed kernel they read as zero.
DF-0010 โ VERDICT
Verdict: REPRODUCED (uninitialized-kernel-stack info leak via SO_PASSCRED
synthesis). Impact: info leak, up to ~60 bytes of kernel stack per call
including canonical kernel-virtual pointers (KASLR/stack-residue oracle).
Confidence: certain.
Mechanism (trigger โ primitive โ effect)
-
Trigger (unprivileged, self-reachable). An
AF_UNIXSOCK_DGRAMsocketpair; the receiver end setsSO_PASSCRED; the peer sends a plain datagram with noSCM_CREDScontrol message.send()โsosendโuipc_send. -
Synthesis from an uninitialized on-stack struct. In
uipc_send,sys/kern/uipc_usrreq.c:683declaresc struct cmsgcred cred; /* NO initializer */inside theif (so2->so_options & SO_PASSCRED)block (:680). When the loop at:687-693finds no pre-existingSCM_CREDScmsg (ncon == NULL), the kernel synthesizes one at:694-699:c ncon = sbcreatecontrol(&cred, sizeof(cred), SCM_CREDS, SOL_SOCKET); unp_internalize(ncon, msg->send.nm_td); *mp = ncon; -
Primitive: full-struct copy of stack garbage.
sbcreatecontrol(sys/kern/uipc_sockbuf.c:585-604) doesmemcpy(CMSG_DATA(cp), p, size)withsize = sizeof(struct cmsgcred) = 84(:598) โ it copies all 84 bytes of the uninitialized on-stackcredinto the control mbuf. -
Partial fill leaves the tail unwritten.
unp_internalize'sSCM_CREDSbranch (sys/kern/uipc_usrreq.c:1734-1744) only writes: -cmcred_pid,cmcred_uid,cmcred_gid,cmcred_euid,cmcred_ngroups(:1736-1741) -cmcred_groups[0..ngroups-1](:1742-1743)
It never writes groups[ngroups..CMGROUP_MAX-1] (nor the 2-byte pad after
cmcred_ngroups). struct cmsgcred (sys/sys/socket.h:431-438) is
__pid_t + uid_t*3 + short + pad + gid_t[CMGROUP_MAX] with CMGROUP_MAX =
16 (sys/sys/socket.h:422), so up to (16 - ngroups)*4 + 2 bytes of
kernel stack are delivered verbatim to the receiver via recvmsg.
- Effect: kernel-stack residue leaked to peer. For
maxx(ngroups=1),groups[1..15](60 bytes) plus 2 pad bytes are leaked. Across runs the leaked words vary (different stack residue) and contain canonical kernel-virtual pointers such as0xfffff8006687a5a0,0xfffff800ab9d5688,0xfffff800aba73688โ directly defeating KASLR and providing a samplable stack-residue / heap-grooming oracle for a separate kernel bug. No integrity/availability impact (pure info leak).
Evidence (decisive)
leak_sample.txt holds three independent runs (PIDs 838 / 933 / 938 โฆ on
different boots). Representative (PID 838, run 1):
sample 0: pid=838 uid=1001 euid=1001 gid=1001 ngroups=1
filled groups [0..0] = { 1001 }
UNFILLED groups [1..15] = { 0x00000000 0x4060d100 0xfffff800 0xaba73688
0xfffff800 0x80656a1e 0xffffffff 0x00000005 0x00000000 0x4060d100
0xfffff800 0xaba736a8 0xfffff800 0x00000000 0x00000000 }
The 0xfffff800........ words are canonical kernel-virtual addresses (the
kernel text/data range). They differ across PIDs/runs, proving genuine
uninitialized-memory content rather than cosmetic output. result: LEAK
CONFIRMED on every run; exit code 0. run.log is the decisive single run.
PoC changes
The seeded leak_cmsgcred.c was already correct and compiled/run unchanged
(no source edits were needed). It builds clean with cc -o leak_cmsgcred
leak_cmsgcred.c and reproduces on the first run.
Exploit chain
Not a memory-corruption class โ pure info leak. No further primitive is derivable beyond the samplable ~60-byte kernel-stack oracle (useful as a KASLR/heap-grooming ingredient for a separate bug). Ceiling impact = Low info disclosure, matching the finding's rating.
Recommended fix
fix.diff (git-apply-able, git apply --check verified) inserts
bzero(&cred, sizeof(cred)); immediately after the declaration block at
sys/kern/uipc_usrreq.c:683-684, zeroing all 84 bytes (including the pad and
the unfilled groups[ngroups..15]) before sbcreatecontrol copies them.
This matches the finding markdown's ## Recommended fix intent (zero the
synthesized cred) and additionally covers the 2-byte inter-field padding
that = {} initialization would not guarantee to clear. Alternative
defense-in-depth would be to zero the whole cmcred inside unp_internalize
before the partial fill at :1734-1744, but the declaration-site fix is the
minimal targeted change at the root location the finding cites.
Confirmed kernel references
Detail
Exploit chain
none -- pure info-leak class (no memory corruption). Ceiling impact = samplable ~60-byte/call kernel-stack oracle (KASLR/heap-grooming ingredient for a separate bug); no integrity/availability impact, no further primitive derivable.
Evidence (decisive lines)
sample 0: pid=853 uid=1001 euid=1001 gid=1001 ngroups=1
filled groups [0..0] = { 1001 }
UNFILLED groups [1..15] = { 0x00000000 0x405a1080 0xfffff800 0x668787a0 0xfffff800 0x00000148 0x00000000 0x00000005 0x00000000 0x405a1080 0xfffff800 0xab9496a8 0xfffff800 0x00000000 0x00000000 }
... sample 2: ... 0xfffff800 0x806bd635 0xffffffff ... 0xab4e6c88 0xfffff800 ...
result: LEAK CONFIRMED (kernel-stack bytes in unfilled groups)
RUN_EXIT=0 (0xfffff800........ = canonical kernel-virtual ptrs; vary across runs)
PoC changes
The seeded leak_cmsgcred.c was already correct -- no source edits needed; it built clean and reproduced on the first run. Added build.sh/run.sh repro scripts, captured build.log/run.log/leak_sample.txt (3 stress runs)/env.txt, wrote VERDICT.md + manifest.json, and authored fix.diff (bzero(&cred,...) at uipc_usrreq.c:683).
Verified recommended fix
fix.diff (git-apply-able, git apply --check verified) inserts bzero(&cred, sizeof(cred)); right after the declaration block at sys/kern/uipc_usrreq.c:683-684, zeroing all 84 bytes (including the inter-field pad and the unfilled groups[ngroups..15]) before sbcreatecontrol copies them. Matches the finding markdown's ## Recommended fix intent (zero the synthesized cred) and additionally covers the 2-byte padding that = {} would not guarantee to clear. Full diff in findings/poc/DF-0010/fix.diff.
Verdict
REPRODUCED. The bug is real: uipc_send (sys/kern/uipc_usrreq.c:683) declares struct cmsgcred cred; with no initializer in the SOCK_DGRAM SO_PASSCRED synthesis block; sbcreatecontrol (uipc_sockbuf.c:598) memcpy's all 84 bytes of that stack garbage into the control mbuf; unp_internalize's SCM_CREDS branch (uipc_usrreq.c:1734-1744) only fills cmcred_pid/uid/gid/euid/ngroups and groups[0..ngroups-1], leaving the 2-byte pad after ngroups and groups[ngroups..CMGROUP_MAX-1] (CMGROUP_MAX=16, sys/sys/socket.h:422) uninitialized. Confirmed empirically: an unprivileged socketpair(AF_UNIX,SOCK_DGRAM) + SO_PASSCRED + plain send() leaks ~60 bytes of kernel stack to the receiver, including canonical kernel-virtual pointers (0xfffff800668787a0, 0xfffff800ab9496a8, ...) that vary across PIDs/runs (838/853/933/938) -- a genuine uninitialized-memory info leak / KASLR+stack-residue oracle, not cosmetic output. Full 3-run variance proof in findings/poc/DF-0010/leak_sample.txt.