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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2557 Β· 13 files
FileTypeDescriptionSize
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
README.md readme bug, build/run, expected output
↓ download 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.

VERDICT.md verdict full narrative: mechanism, evidence, fix validation
↓ download raw

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)

  1. Trigger β€” an unprivileged user calls socketpair(AF_UNIX, SOCK_DGRAM), sets SO_PASSCRED on one end (sv[1]), sends a plain datagram (no SCM_CREDS ancillary data) from sv[0], and recvmsg() on sv[1] with a control-message buffer. Fully self-contained; no setup, no root.

  2. Primitive (root cause) β€” in sys/kern/uipc_usrreq.c, the SOCK_DGRAM send path checks the peer for SO_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) does memcpy(CMSG_DATA(cp), p, size) β€” copying the entire 84-byte struct cmsgcred (sizeof on x86_64) verbatim into the control mbuf, including the uninitialized tail.

  3. 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.

  1. Effect β€” kernel stack info leak β€” the receiver reads those 62 bytes via CMSG_DATA() on the received SCM_CREDS control message. Across runs the bytes vary (genuine stack residue, not deterministic), and many of them are recognizable kernel pointer fragments (e.g. 80e56440 00f8ffff little- 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Aug 8 17:51:27 UTC 2026 (make installkernel)

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 struct cmsgcred (no redefinition), socketpair(AF_UNIX,SOCK_DGRAM), setsockopt(SO_PASSCRED) on receiver, plain send() from sender, recvmsg() with control buffer, hexdumps the 84-byte struct and counts non-zero bytes in should-be-zero tail (2-byte padding after cmcred_ngroups + groups[ngroups..CMGROUP_MAX-1]). One compile-fix iteration (removed redefined struct clashing with system header). build.sh, run.sh, README.md, VERDICT.md, manifest.json, leak_sample.txt, fix.diff.

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.