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

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, send plain data from the other (no SCM_CREDS), recvmsg on 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.

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

Timeline

  • 2026-06-29 Discovered during automated file-by-file audit of sys/kern/uipc_usrreq.c.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0010 Β· 15 files
FileTypeDescriptionSize
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 + FIX VALIDATED narrative with path:line 7.3 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 PoC compile, full output 13 B view raw
baseline_run.log run-log decisive leak run on unpatched #0 kernel (LEAK CONFIRMED) 1.2 KB view raw
run.log run-log prior 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/kern.version for #0 and #1, source refs 1.3 KB view raw
fix.diff suggested-fix git-apply-able: bzero(&cred,sizeof(cred)) at uipc_usrreq.c:686 (validated: builds + closes leak on #1) 316 B view raw
fix_build.log fix-build-log full nativekernel output for the single-fix build, NK_DONE rc=0 (35294 lines) 5.6 MB ↓ download
fix_run.log fix-run-log 3 PoC runs on patched #1 kernel: all unfilled groups 0x00000000, exit 2 3.6 KB 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 + root-cause
↓ download 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.

VERDICT.md verdict full REPRODUCED + FIX VALIDATED narrative with path:line
↓ download raw

DF-0010 β€” VERDICT

Verdict: REPRODUCED, FIX VALIDATED. Uninitialized-kernel-stack info leak via SO_PASSCRED synthesis is real on the unpatched #0 master DEV kernel, and is fully closed by the single-line bzero(&cred, sizeof(cred)) fix in fix.diff, confirmed by a clean before/after on a built-and-booted single-fix kernel.

  • Impact: info leak, up to ~60 bytes of kernel stack per call including canonical kernel-virtual pointers (KASLR / stack-residue oracle). Low.
  • Confidence: certain.
  • fix_status: fixed.

Mechanism (trigger -> primitive -> effect)

  1. Trigger (unprivileged, self-reachable). An AF_UNIX SOCK_DGRAM socketpair; the receiver end sets SO_PASSCRED; the peer sends a plain datagram with no SCM_CREDS control message. send() -> sosend -> uipc_send.

  2. Synthesis from an uninitialized on-stack struct. In uipc_send, sys/kern/uipc_usrreq.c:683 declares c struct cmsgcred cred; /* NO initializer */ inside the if (so2->so_options & SO_PASSCRED) block (:680). When the loop at :687-693 finds no pre-existing SCM_CREDS cmsg (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;

  3. Primitive: full-struct copy of stack garbage. sbcreatecontrol (sys/kern/uipc_sockbuf.c) does memcpy(CMSG_DATA(cp), p, size) with size = sizeof(struct cmsgcred) β€” it copies all 84 bytes of the uninitialized on-stack cred into the control mbuf.

  4. Partial fill leaves the tail unwritten. unp_internalize's SCM_CREDS branch (sys/kern/uipc_usrreq.c:1734-1744) only writes: - cmcred_pid, cmcred_uid, cmcred_gid, cmcred_euid, cmcred_ngroups - cmcred_groups[0..ngroups-1]

It never writes groups[ngroups..CMGROUP_MAX-1] (nor the 2-byte pad after cmcred_ngroups). struct cmsgcred is __pid_t + uid_t*3 + short + pad + gid_t[CMGROUP_MAX] with CMGROUP_MAX = 16, so up to (16-ngroups)*4 + 2 bytes of kernel stack are delivered verbatim to the receiver via recvmsg.

  1. 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 as 0xfffff800........, 0xfffff801........ β€” directly defeating KASLR and providing a samplable stack-residue / heap-grooming oracle for a separate kernel bug. No integrity/availability impact (pure info leak).

Baseline reproduction (unpatched #0)

baseline_run.log is the decisive single run on DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026. Representative (sample 0, PID 901):

sample 0: pid=901 uid=1001 euid=1001 gid=1001 ngroups=1
  filled groups [0..0] = { 1001 }
  UNFILLED groups [1..15] = { 0x00000000 0x4063e400 0xfffff800 0x18335688 0xfffff801 0x8065730e 0xffffffff 0x00000005 0x00000000 0x4063e400 0xfffff800 0x183356a8 0xfffff801 0x00000000 0x00000000 }

result: LEAK CONFIRMED (kernel-stack bytes in unfilled groups)
RUN_EXIT=0

The 0xfffff800........ / 0xfffff801........ words are canonical kernel-virtual addresses; they differ across samples/runs, proving genuine uninitialized-memory content rather than cosmetic output. leak_sample.txt holds the prior 3-run variance proof.

Fix validation (Phase 8)

Fix (fix.diff): one line, at the declaration site the finding cites β€” insert bzero(&cred, sizeof(cred)); immediately after the declaration block (sys/kern/uipc_usrreq.c:683-684), so all 84 bytes of the synthesized struct cmsgcred (including the inter-field pad and the unfilled groups[ngroups..15]) are zeroed before sbcreatecontrol copies them.

Procedure

  1. vm.sh reset with-src -> clean source + warm obj + unpatched #0. Baseline re-confirmed leak (above).
  2. Applied ONLY fix.diff to /usr/src via patch -p1 --forward -> hunk #1 succeeded at line 683; verified bzero(&cred, sizeof(cred)); now present at uipc_usrreq.c:686.
  3. Built single-fix kernel: cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC (background, </dev/null) -> === NK_DONE rc=0 === in fix_build.log (35294 lines). Only uipc_usrreq.o + link re-ran (incremental warm obj).
  4. Installed the freshly-stripped kernel over the bare loader name: cp .../kernel.stripped /boot/kernel/kernel (+ .debug), sync. /boot/kernel/kernel sha256 changed 5dc83dac... -> 75d02b7b....
  5. Rebooted (vm.sh down && up); sysctl kern.version -> 6.5-DEVELOPMENT #1: Thu Jul 2 18:16:59 UTC 2026 (the #1 build), sha matches the installed binary.

After (patched #1) β€” fix_run.log, 3 runs / 12 samples

sample 0: pid=829 uid=1001 euid=1001 gid=1001 ngroups=1
  filled groups [0..0] = { 1001 }
  UNFILLED groups [1..15] = { 0x00000000 0x00000000 0x00000000 0x00000000
    0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000
    0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 }

result: no residue this run
RUN_EXIT=2          (PoC's "no leak" exit code)

Every unfilled group word is 0x00000000 across all 3 runs β€” the leak is gone, deterministically. cmcred_pid/uid/euid/gid/groups[0] (the legitimately filled fields) are still correct, so the fix has no functional regression.

Before/after contrast

kernel unfilled groups[1..15] result exit
#0 varying kernel ptrs 0xfffff8.. LEAK CONFIRMED 0
#1 all 0x00000000 no residue 2

Conclusion: fix.diff closes the bug (fix_status: fixed). The guest was reset to the with-src (unpatched #0) baseline afterward.

PoC changes

No changes to leak_cmsgcred.c (it was already correct and reproduced on the first run). The only additions to the evidence pack are the fix-validation artifacts: baseline_run.log, fix_build.log, fix_run.log, updated env.txt, this updated VERDICT.md, and an updated manifest.json. fix.diff is unchanged from the prior session (it applied cleanly and compiled).

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.

fix.diff (git-apply-able, patch -p1 verified to apply and compile, and validated on a booted kernel) 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 guarantees the 2-byte inter-field padding is cleared (which = {} initialization would not promise portably). 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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix: leak_cmsgcred on unpatched #0 (6.5-DEVELOPMENT #0) leaked varying kernel-stack bytes incl. canonical 0xfffff8.. pointers in cmcred_groups[1..15] (LEAK CONFIRMED, exit 0); the SAME PoC on the single-fix #1 kernel (bzero(&cred,sizeof(cred)) at uipc_usrreq.c:686) produced all-zero unfilled groups across 3 deterministic runs (no residue, exit 2) while the legit credential fields stayed correct. Build rc=0; /boot/kernel/kernel sha 5dc83dac..->75d02b7b.. confirms the swap. Fix closes the bug with no regression.

BASELINE #0: UNFILLED[1..15]={0x00000000 0x4063e400 0xfffff800 0x18335688 0xfffff801 0x8065730e ...} -> 'result: LEAK CONFIRMED' RUN_EXIT=0 | PATCHED #1: UNFILLED[1..15]={0x00000000 x15} -> 'result: no residue this run' RUN_EXIT=2 (x3 runs). fix_build: === NK_DONE rc=0 ===
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 18:16:59 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC

Confirmed kernel references

Detail

Exploit chain

unpriv kernel stack leak via uninitialized cmsgcred on SO_PASSCRED recv β€” pure info leak (not memory corruption). Ceiling = samplable ~60-byte kernel-stack/KASLR oracle ingredient for a separate bug; no LPE derivable. Matches Low severity.

Evidence (decisive lines)

BASELINE #0 (baseline_run.log, pid 901): UNFILLED groups[1..15] = { 0x00000000 0x4063e400 0xfffff800 0x18335688 0xfffff801 0x8065730e 0xffffffff 0x00000005 ... } -> 'LEAK CONFIRMED' RUN_EXIT=0. PATCHED #1 (fix_run.log, 3 runs/12 samples): UNFILLED groups[1..15] = all 0x00000000 -> 'no residue this run' RUN_EXIT=2 (PoC's no-leak code).

PoC changes

leak_cmsgcred.c unchanged (reproduced first try). Only fix-validation artifacts added to findings/poc/DF-0010/: baseline_run.log (decisive #0 leak run), fix_build.log (full 35294-line nativekernel output, NK_DONE rc=0), fix_run.log (3 #1 runs all-zero), refreshed env.txt (both #0 and #1 kern.version + sha), updated VERDICT.md (REPRODUCED+FIX VALIDATED narrative), updated manifest.json. fix.diff unchanged (applied cleanly + compiled).

Verified recommended fix

In sys/kern/uipc_usrreq.c, immediately after the declaration block at line 683-684, insert bzero(&cred, sizeof(cred)); so all 84 bytes of the synthesized struct cmsgcred (including the inter-field pad and unfilled groups[ngroups..15]) are zeroed before sbcreatecontrol copies them. Matches the finding markdown's Recommended fix intent (zero the synthesized cred) and additionally guarantees the 2-byte padding is cleared; validated to build and fully close the leak on a booted #1 kernel. Full git-apply-able diff in findings/poc/DF-0010/fix.diff.

Verdict

REPRODUCED + FIX VALIDATED. On the unpatched master DEV #0 kernel, the synthesized struct cmsgcred on the AF_UNIX SOCK_DGRAM SO_PASSCRED path leaks ~60 bytes of uninitialized kernel stack per call: uipc_usrreq.c:683 declares struct cmsgcred cred; with no initializer, uipc_usrreq.c:695 sbcreatecontrol() copies all 84 stack bytes into the control mbuf, and unp_internalize (uipc_usrreq.c:1734-1744) only fills pid/uid/euid/gid/ngroups+groups[0..ngroups-1], leaving groups[ngroups..15] (and a 2-byte pad) as verbatim kernel-stack residue. The leaked words vary across samples and include canonical kernel-virtual pointers (0xfffff800../0xfffff801..), defeating KASLR / serving as a samplable oracle. The single-line fix.diff (bzero(&cred,sizeof(cred)) at uipc_usrreq.c:686) was applied with patch -p1, built as a single-fix kernel (nativekernel rc=0), installed over the bare /boot/kernel/kernel, and booted as #1: on the patched kernel all unfilled groups read 0x00000000 across 3 deterministic runs while the legitimately-filled credential fields remain correct, so the leak is closed with no functional regression.