# VERDICT — DF-2702 (recvmsg MSG_PEEK skips unp_externalize → kernel file-pointer leak)

**status: reproduced** (impact: leak, confidence: certain)

## Root cause

AF_UNIX SCM_RIGHTS travel in two forms:

* **internalized** — while queued in `so_rcv`: a 16-byte `cmsghdr` followed by
  N raw `struct file *` kernel pointers. Written by `unp_internalize()`
  (sys/kern/uipc_usrreq.c:1817-1825), which sets
  `cm->cmsg_len = CMSG_LEN(oldfds * sizeof(struct file *))`.
* **externalized** — when delivered: `unp_externalize()`
  (sys/kern/uipc_usrreq.c:1543-1633) replaces each pointer with an allocated
  fd number in the receiver's fd table and rewrites
  `cm->cmsg_len = CMSG_LEN(newfds * sizeof(int))` (uipc_usrreq.c:1629).

`soreceive()` runs the externalize step only on the **consume** path
(sys/kern/uipc_socket.c:1426-1437). The `MSG_PEEK` branch right above it
(sys/kern/uipc_socket.c:1422-1425) simply `m_copym()`s the control mbuf and
appends the copy to the `*controlp` chain:

```c
if (flags & MSG_PEEK) {
        if (controlp)
                *controlp = m_copym(m, 0, m->m_len, M_NOWAIT);
        m = m->m_next;      /* XXX race */
}
```

`kern_recvmsg()` (sys/kern/uipc_syscalls.c:1172-1189) then `copyout()`s
`m->m_len` bytes of that copy — i.e. the raw `struct file *` values — into the
user's control buffer. No externalize, no sanitization, no privileges needed.

(You cannot "just externalize the copy": `unp_externalize()` drops the
in-flight rights references (`unp_del_right()` + `fdrop()`,
uipc_usrreq.c:1666-1667) which the still-queued original message owns — that
would corrupt `f_msgcount`/`unp_rights`. Hence the fix returns no rights
control on peek; see fix.diff.)

## Reproduction (guest DragonFly 6.5-DEVELOPMENT #0, X86_64_GENERIC, uid=1001)

`./peek_rights_leak` (see run.log):

* sendmsg passes 2 fds over an `AF_UNIX` `SOCK_STREAM` socketpair with user
  `cmsg_len = CMSG_LEN(2*4) = 24`.
* PEEK #0 and PEEK #1 (same queued message) return `controllen=32`,
  `cmsg_len=32 = CMSG_LEN(2*8)`, and slots
  `0xfffff801167ffd80`, `0xfffff801167ffe80` — two kernel heap pointers
  (0x100 apart, same `struct file` slab zone). Identical across both peeks.
* REAL recvmsg of the same message returns `cmsg_len=24` and fds 6,7 — the
  consume path externalizes correctly, proving the peek copy is the anomaly.

`./dgram_peek` (run.2.log): SOCK_DGRAM leaks too:
`cmsg_len=24 = CMSG_LEN(1*8)`, `data0=0xfffff801167e5500`.

Across runs the leaked values always fall in the kernel virtual range
(`0xfffff801xxxxxxxx` on this kernel) and vary between processes — 4/4
observations matched kernel-memory pattern (leak criteria ≥3 runs satisfied).

## Impact

Unprivileged disclosure of live kernel heap addresses (`struct file`
allocations). Defeats heap-layout randomization, gives an attacker the file
zone base, and turns separate heap-corruption primitives into reliable
exploits (slab grooming targets). No KASLR on many DFly deployments makes the
*address* less secret, but the leak also discloses per-object placement and
requires zero privileges.

Severity: Medium (kernleak bucket).
CVSS: 3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N (5.5).

## Fix validation

`fix.diff` (sys/kern/uipc_socket.c, MSG_PEEK branch): do not hand unreplaced
SCM_RIGHTS control back on peek (non-rights control such as SCM_CREDS is still
peekable). Validated by in-guest `make nativekernel KERNCONF=X86_64_GENERIC`
build + install + reboot:

* baseline (stock #0 kernel): pointer leak as above (run.log, run.2.log)
* patched kernel: PEEK returns `controllen=0` (no control data, no kernel
  pointers); REAL recvmsg still returns fds 6,7 — fd passing fully functional
* see fix_validation.log, fix_build_excerpt.txt

## Negative space (what was checked and killed in uipc_usrreq.c itself)

* `unp_bind`/`unp_find_lockref` stack `buf[SOCK_MAXADDRLEN]` + `strncpy` with
  `sun_len`: `getsockaddr()` normalizes `sa->sa_len = len` and caps `len <=
  SOCK_MAXADDRLEN` (sys/kern/uipc_syscalls.c:1519-1528), so `namelen <= 253`
  and all copies stay in-bounds (uipc_usrreq.c:1141-1145, 2425-2431).
* `unp_pcblist` `bcopy(..., sun_len)` into `xu_addr`: the `xu_au`/`xu_cau`
  unions are 256 bytes (sys/sys/unpcb.h:115-124), `sun_len <= 255`. No
  overflow. (The uninit-stack aspect is known DF-2558.)
* control-mbuf forging: `kern_sendmsg` bounds `controllen` to
  `[sizeof(cmsghdr), MLEN]` and copies it into a single mbuf
  (sys/kern/uipc_syscalls.c:934-943); `unp_internalize` validates
  `cmsg_len ∈ [CMSG_LEN(0), m_len]` (uipc_usrreq.c:1723-1729, 1757-1759) and
  rewrites `m_len = CMSG_ALIGN(newlen)` (1810), so every kernel-side consumer
  (`unp_scan`, `unp_externalize`) sees a self-consistent first-cmsg. Trailing
  user bytes in the control buffer are never parsed by the kernel.
* unpcb lifetime vs. blocking token release: every syscall entry holds the
  fp (`holdsock`), and `soclose` (thus `uipc_detach` → `unp_drop` → kfree)
  runs only on the last `fdrop`, so a blocked `nlookup` inside
  `unp_find_lockref`/`unp_connect` cannot free the unpcb under the caller.
  `unp_reference()`/`unp_free()` pairs verified balanced on all paths
  (uipc_accept 368-371, uipc_peeraddr 518-521, uipc_rcvd 579-589, uipc_send
  671/717 and 758/792, unp_connect 1233/1278 + find_lockref/1284, unp_gc_process
  2127-2138).
* `unp_disconnect` peer-free race: freeing the peer requires disconnecting us
  first, which needs our pool token (uipc_usrreq.c:1365-1370, 2544-2548) —
  safe under token.
* `unp_gc` (new scheme): phase-1 DEAD marking can double-count
  `unp_unreachable` (uipc_usrreq.c:2114-2119 returns without UNPGC_SCANNED),
  but phase 2 re-validates `f_count == unp_msgcount` before collecting and
  the loop exits when no DEAD entries remain — overcount is benign (no
  OOB on `unref[]`, guarded by KASSERT at 2271-2279). Markers used for safe
  iteration; `SYSCTL_OUT`-blocking iteration in `unp_pcblist` is marker-safe.
* `cru2x()` bzeros the whole `xucred` (sys/kern/kern_prot.c:1237) — no leak
  via LOCAL_PEERCRED (uipc_usrreq.c:951).
* `soreceive` no-control-buffer receive of rights correctly disposes
  (`free_rights` → `dom_dispose`, sys/kern/uipc_socket.c:1608-1612).

Known findings not re-reported: DF-0010/2557 (uninit cmsgcred), DF-0011/2559
(sbcreatecontrol NULL panic), DF-2558 (pcblist stack leak), DF-2560
(ssb_appendcontrol failure rights leak), DF-2694 (sorflush UAF, receive paths).
