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.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0010 Β· 15 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 + 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 |
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, 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)
-
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) doesmemcpy(CMSG_DATA(cp), p, size)withsize = sizeof(struct cmsgcred)β 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-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.
- 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 as0xfffff800........,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
vm.sh reset with-src-> clean source + warm obj + unpatched#0. Baseline re-confirmed leak (above).- Applied ONLY
fix.diffto/usr/srcviapatch -p1 --forward-> hunk #1 succeeded at line 683; verifiedbzero(&cred, sizeof(cred));now present atuipc_usrreq.c:686. - Built single-fix kernel:
cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC(background,</dev/null) ->=== NK_DONE rc=0 ===infix_build.log(35294 lines). Onlyuipc_usrreq.o+ link re-ran (incremental warm obj). - Installed the freshly-stripped kernel over the bare loader name:
cp .../kernel.stripped /boot/kernel/kernel(+.debug),sync./boot/kernel/kernelsha256 changed5dc83dac...->75d02b7b.... - Rebooted (
vm.sh down && up);sysctl kern.version->6.5-DEVELOPMENT #1: Thu Jul 2 18:16:59 UTC 2026(the#1build), 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.
Recommended fix
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
fixedVALIDATED 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 ===
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.
No comments yet.