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

4. **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:
```diff
-			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 |
