Kernel stack info leak via SO_PASSCRED auto-created SCM_CREDS on AF_UNIX SOCK_DGRAM
Summary
When receiving AF_UNIX datagram socket has SO_PASSCRED set and sender omits SCM_CREDS uipc_send synthesizes one from uninitialized on-stack struct cmsgcred cred. sbcreatecontrol copies whole struct (80 bytes) into mbuf then unp_internalize only fills pid/uid/euid/gid/ngroups and groups[0..ngroups-1]. 2 bytes padding plus cmcred_groups[ngroups..CMGROUP_MAX-1] retain kernel stack copied verbatim to receiver by recvmsg. Any unprivileged local user SO_PASSCRED requires no privilege. Up to 66 bytes kernel stack per datagram KASLR bypass stack-content disclosure repeatable indefinitely.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2557 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| leak_cmsgcred.c | trigger-source | unprivileged AF_UNIX SOCK_DGRAM SO_PASSCRED SCM_CREDS synthesis leak PoC | 5.3 KB | view raw |
| build.sh | build-script | cc -O2 -o leak_cmsgcred leak_cmsgcred.c | 191 B | view raw |
| run.sh | run-script | ./leak_cmsgcred 3 | 136 B | view raw |
| README.md | readme | bug, build/run, expected output | 2.0 KB | β raw |
| VERDICT.md | verdict | full narrative: mechanism, evidence, fix validation | 6.3 KB | β raw |
| run.log | run-log | unpatched #0 baseline, 3 samples, LEAK CONFIRMED | 1.7 KB | view raw |
| run.6.log | run-log | unpatched #0 baseline, 6 samples, 291 leaked bytes | 3.1 KB | view raw |
| fix_run.log | run-log | patched #1 kernel, 3 samples, 0 leaked bytes | 1.6 KB | view raw |
| fix_run.6.log | run-log | patched #1 kernel, 6 samples, 0 leaked bytes | 477 B | view raw |
| leak_sample.txt | leak-sample | raw hex of received cmsgcred across baseline runs | 2.6 KB | view raw |
| fix.diff | suggested-fix | bzero(&cred, sizeof(cred)) before sbcreatecontrol copies it | 353 B | view raw |
| fix_build.log | build-log | full single-fix nativekernel build output, rc=0 | 5.6 MB | β download |
| env.txt | environment | uname, kern.version, cc, id, /boot/kernel/kernel | 465 B | view raw |
DF-2557 β SO_PASSCRED SCM_CREDS synthesis uninitialized-stack info leak
Bug
sys/kern/uipc_usrreq.c:683 declares struct cmsgcred cred; with no
initializer. When an AF_UNIX SOCK_DGRAM receiver has SO_PASSCRED set
and the sender sends a datagram with no SCM_CREDS ancillary data,
uipc_send synthesizes one (lines 694β699):
ncon = sbcreatecontrol(&cred, sizeof(cred), SCM_CREDS, SOL_SOCKET);
unp_internalize(ncon, msg->send.nm_td);
sbcreatecontrol copies the full sizeof(cred) (80 bytes on x86_64) β
including the uninitialized tail β into the control mbuf.
unp_internalize (lines 1734β1744) then fills only:
- cmcred_pid, cmcred_uid, cmcred_euid, cmcred_gid, cmcred_ngroups
- cmcred_groups[0 .. ngroups-1]
Leaving the 2 bytes of padding after the short cmcred_ngroups and the
groups [ngroups .. CMGROUP_MAX-1] (up to 60 bytes for a typical 1-group
user) as raw kernel-stack residue, delivered verbatim to the receiving
socket.
Impact
Kernel stack info leak (CWE-908/CWE-200). Up to 62 bytes per datagram of uninitialized kernel-stack residue are made readable by any local unprivileged user, in a tight loop. Useful as a KASLR / stack-residue oracle ingredient for a separate kernel exploit. No write primitive β the leak itself is the finding.
Build & run
./build.sh # cc -O2 -o leak_cmsgcred leak_cmsgcred.c ./run.sh # ./leak_cmsgcred 3 (unprivileged)
Expected output (unpatched, bug present)
A hex dump of the received 80-byte struct cmsgcred in which the padding
bytes after cmcred_ngroups and the tail groups[ngroups..CMGROUP_MAX-1]
are non-zero kernel-stack residue that varies between samples; the
LEAK CONFIRMED summary line.
Expected output (patched, bug gone)
The same bytes are all 0x00 and the program prints NO LEAK.
Reproduction environment
DragonFly 6.5-DEVELOPMENT master DEV, X86_64_GENERIC (INVARIANTS ON),
with-src snapshot. Fully unprivileged: socketpair, setsockopt,
send, recvmsg. No setup, no root, no special config.
DF-2557 β VERDICT
Verdict
REPRODUCED β uninitialized kernel-stack info leak via synthesized
SCM_CREDS on AF_UNIX SOCK_DATAGRAM + SO_PASSCRED. Fix VALIDATED on a
built-and-booted single-fix kernel: the leak is gone.
Mechanism (trigger β primitive β effect)
-
Trigger β an unprivileged user calls
socketpair(AF_UNIX, SOCK_DGRAM), setsSO_PASSCREDon one end (sv[1]), sends a plain datagram (noSCM_CREDSancillary data) fromsv[0], andrecvmsg()onsv[1]with a control-message buffer. Fully self-contained; no setup, no root. -
Primitive (root cause) β in
sys/kern/uipc_usrreq.c, the SOCK_DGRAM send path checks the peer forSO_PASSCRED(uipc_usrreq.c:680). If the peer wants creds and the sender did not attach any, the kernel synthesizes them:c /* uipc_usrreq.c:683 */ struct cmsgcred cred; /* UNINITIALIZED on-stack */ ... /* uipc_usrreq.c:695-697 */ ncon = sbcreatecontrol(&cred, sizeof(cred), SCM_CREDS, SOL_SOCKET); unp_internalize(ncon, msg->send.nm_td);sbcreatecontrol()(sys/kern/uipc_sockbuf.c:598) doesmemcpy(CMSG_DATA(cp), p, size)β copying the entire 84-bytestruct cmsgcred(sizeof on x86_64) verbatim into the control mbuf, including the uninitialized tail. -
Partial fill β
unp_internalize()(uipc_usrreq.c:1734-1744) then writes only: -cmcred_pid,cmcred_uid,cmcred_euid,cmcred_gid,cmcred_ngroups-cmcred_groups[0 .. ngroups-1]
It never touches the 2 bytes of padding after the short cmcred_ngroups
(struct alignment to gid_t) nor the groups[ngroups .. CMGROUP_MAX-1]
tail. For a typical single-group unprivileged user (ngroups=1) that is
2 + 15*4 = 62 bytes of stack residue per datagram, samplable in a tight
loop.
- Effect β kernel stack info leak β the receiver reads those 62 bytes
via
CMSG_DATA()on the receivedSCM_CREDScontrol message. Across runs the bytes vary (genuine stack residue, not deterministic), and many of them are recognizable kernel pointer fragments (e.g.80e56440 00f8fffflittle- endian =0xfffff8004064e580, a canonical x86_64 kernel-virtual address). This is a samplable KASLR / stack-residue oracle for the price of a datagram. No write primitive β pure info leak (CWE-908 / CWE-200).
Reproduction evidence (unpatched #0 baseline)
run.log (3 samples) and run.6.log (6 samples), excerpt:
=== sample 0: ... ngroups=1 ===
full 80-byte cmsgcred (as received) (84 bytes):
85030000 e9030000 e9030000 e9030000 0100ffff e9030000 49010000 80e56440
00f8ffff 88163518 01f8ffff 0e736580 ffffffff 05000000 00000000 80e56440
00f8ffff a836ff16 01f8ffff 00000000 00000000
padding bytes [18..20): ffff
tail groups[1..16) [24..84) non-zero bytes: 34
>>> sample 0 leaked-non-zero-bytes = 36 / 62 possible
...
==== SUMMARY over 6 samples: 291 leaked non-zero bytes (of 372 possible) ====
result: LEAK CONFIRMED (kernel-stack residue in synthesized SCM_CREDS)
Bytes 18-19 (padding after cmcred_ngroups) read ffff β uninitialized.
Tail groups carry varying kernel-stack residue including 0xfffff8??_????????
kernel pointer fragments. ~36β54 non-zero leaked bytes per sample out of 62
possible.
Why it is NOT a write primitive / no escalation chain
The bug exposes kernel stack bytes to a receiver; it never writes attacker- controlled data into the kernel. Read-only info leak is a valid hard blocker for an escalation chain (Phase 6 valid blocker #1: "primitive is genuinely read-only"). Impact ceiling: KASLR-defeat / stack-residue oracle that would aid a separate write-capable bug. Rated Medium for the info disclosure itself.
Fix
fix.diff β zero-initialize the synthesized cred before
sbcreatecontrol() copies it into the mbuf:
- struct cmsgcred cred;
+ struct cmsgcred cred;
+ bzero(&cred, sizeof(cred));
This is a one-line, root-cause fix. It matches (and is functionally
identical to) the finding markdown's struct cmsgcred cred = {}; proposal
in DF-0010 (DF-2557 is a re-file of the same bug at Medium severity); the
bzero form was chosen for explicitness and to avoid any C syntax edge cases
with = {} on older gcc.
Fix validation (Phase 8)
| Kernel | kern.version |
Result |
|---|---|---|
| baseline (unpatched) | 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 |
LEAK β 127β291 non-zero bytes (of 186β372 possible) |
| single-fix | 6.5-DEVELOPMENT #1: Sat Aug 8 17:51:27 UTC 2026 |
NO LEAK β 0 non-zero bytes across 3 + 6 samples |
The single-fix kernel was built with make -j6 nativekernel
KERNCONF=X86_64_GENERIC from the patched /usr/src, installed with
make installkernel (full debug kernel, schg flag β same as a real
admin install), and booted. The kern.version #N suffix bumped
#0 β #1 with today's build timestamp, confirming the patched kernel
is the one running.
On the patched kernel the same PoC now prints:
padding bytes [18..20): 0000
tail groups[1..16) [24..84) non-zero bytes: 0
>>> sample 0 leaked-non-zero-bytes = 0 / 62 possible
==== SUMMARY over 6 samples: 0 leaked non-zero bytes (of 372 possible) ====
result: NO LEAK (struct is fully zeroed - bug not present)
Padding bytes 18-19 now read 0000 (the bzero killed the leak).
fix_status = fixed.
PoC changes
Authored the PoC from scratch (leak_cmsgcred.c) β the findings/poc/DF-2557/
folder was empty. The PoC uses the system <sys/socket.h> struct cmsgcred
(no redefinition), socketpair(AF_UNIX, SOCK_DGRAM), setsockopt(SO_PASSCRED)
on the receiver, a plain send() from the sender, recvmsg() with a control
buffer, and reports both a full hex dump and a counted "should-be-zero"
tail (padding + groups[ngroups..CMGROUP_MAX-1]).
Files
| File | Purpose |
|---|---|
leak_cmsgcred.c |
trigger PoC (unprivileged) |
build.sh / run.sh |
exact build & run |
build.log (via run.log) / run.log / run.6.log |
unpatched baseline runs |
fix_run.log / fix_run.6.log |
patched-kernel runs |
fix_build.log |
full single-fix kernel build output |
leak_sample.txt |
raw leaked hex across baseline runs |
env.txt |
guest environment |
fix.diff |
git-apply-able fix |
manifest.json |
artifact catalog |
Fix verification
fixedVALIDATED. Same PoC leaks 36-54 non-zero bytes of kernel stack residue per sample (of 62 possible, including 0xfffff8??_???????? kernel pointer fragments) on unpatched #0 baseline (kern.version Thu Jul 2 06:02:54), and leaks ZERO bytes (all padding + tail groups 0x00) on single-fix #1 kernel (make -j6 nativekernel + make installkernel, kern.version Sat Aug 8 17:51:27). kern.version #N suffix bumped #0 -> #1. Fix closes the bug.
baseline #0: padding bytes [18..20): ffff / tail groups[1..16) non-zero=41 / sample0 leaked=43/62 / SUMMARY(6)=291 leaked of 372. patched #1: padding bytes [18..20): 0000 / tail groups non-zero=0 / sample0 leaked=0/62 / SUMMARY(6)=0 leaked of 372 / result: NO LEAK.
Confirmed kernel references
Detail
Exploit chain
Read-only info leak (no write primitive). Valid Phase-6 hard blocker (primitive genuinely read-only): no escalation chain derivable. Impact ceiling = samplable KASLR/stack-residue oracle (62 bytes/datagram, tight loop, contains kernel pointer fragments) that aids a separate write-capable bug. No exploit.c/chain.c because no corruption to convert.
Evidence (decisive lines)
BASELINE #0: padding bytes [18..20): ffff; tail groups[1..16) non-zero=41; sample0 leaked=43/62. Hexdump: 0100ffff e9030000 49010000 80e56440 00f8ffff 88163518 01f8ffff (0xfffff800_4064e580 kernel pointer visible). SUMMARY over 6 samples: 291 leaked non-zero bytes (of 372). PATCHED #1: padding [18..20): 0000; tail non-zero=0; sample0 leaked=0/62; SUMMARY over 6 samples: 0 leaked non-zero bytes. NO LEAK.
PoC changes
Authored leak_cmsgcred.c from scratch (dir empty). Uses system
Verified recommended fix
In sys/kern/uipc_usrreq.c at the SOCK_DGRAM SO_PASSCRED synthesis block, zero-initialize the on-stack struct before sbcreatecontrol copies it: change 'struct cmsgcred cred;' (line 683) to 'struct cmsgcred cred; bzero(&cred, sizeof(cred));'. Root-cause fix (no stack bytes can leak). Functionally matches finding markdown's 'struct cmsgcred cred = {};' proposal (DF-0010 is the same bug re-filed at Medium); bzero form chosen for explicitness. Full git-apply-able diff in findings/poc/DF-2557/fix.diff.
Verdict
REPRODUCED. In uipc_send's AF_UNIX SOCK_DGRAM SO_PASSCRED synthesis path, sys/kern/uipc_usrreq.c:683 declares struct cmsgcred cred; uninitialized; sbcreatecontrol (uipc_sockbuf.c:598) memcpy's the full 84-byte struct into the control mbuf, and unp_internalize (uipc_usrreq.c:1734-1744) only fills pid/uid/euid/gid/ngroups/groups[0..ngroups-1]. The 2 bytes of padding after the short cmcred_ngroups plus groups[ngroups..CMGROUP_MAX-1] (62 bytes for a 1-group user) remain as raw kernel-stack residue and are delivered to the receiver. Confirmed by unprivileged socketpair+SO_PASSCRED+send+recvmsg PoC: 36-54 non-zero leaked bytes of 62 per sample, varying across runs, including kernel pointer fragments like 0xfffff8004064e580. The 2-byte padding reads 0xffff uninitialized.
No comments yet.