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

AF_UNIX SCM_RIGHTS raw kernel file-pointer leak via recvmsg(MSG_PEEK) β€” soreceive skips unp_externalize on the peek path

Field Value
ID DF-2702
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
CWE CWE-200 Exposure of Sensitive Information
File sys/kern/uipc_usrreq.c
Lines 1543-1633 (externalize), 1817-1825 (internalize); sink uipc_socket.c:1422-1425
Area kern
Confidence certain
Discovered 2026-08-30
Pass 2 (GLM 5.3 second pass)
Bucket kernleak
Reported pending
Known CVE none
CVE match novel

Summary

While queued in so_rcv, rights control mbufs carry raw kernel struct file * pointers (internalized by unp_internalize). Only unp_externalize converts pointers to fd numbers on the consume path, but soreceive()'s MSG_PEEK branch m_copym()s the control mbuf and returns the copy WITHOUT calling dom_externalize; kern_recvmsg then copyouts the raw pointers to the user's control buffer. Any unprivileged user reproduces with socketpair()+sendmsg(SCM_RIGHTS)+ recvmsg(MSG_PEEK) on SOCK_STREAM and SOCK_DGRAM. Externalizing the copy is not a valid fix (it would drop the in-flight refs of the still-queued original), so the fix returns no rights control on peek.

Threat model & preconditions

Local unprivileged info leak: discloses live kernel heap addresses of struct file objects (observed 0xfffff801xxxxxxxx, 0x100 apart in the same slab), defeating heap-address randomization and enabling reliable slab grooming/targeting for separate kernel heap-corruption bugs. Reachable by any user, 100% reliable.

Proof of concept

VERIFIED (findings/poc/DF-2702/peek_rights_leak.c): peek returns controllen=32 (CMSG_LEN(2*8), internalized) with slots 0xfffff801167ffd80/0xfffff801167ffe80 β€” stable across peeks; the subsequent real recvmsg returns controllen=24 with fds 6,7, proving the peek copy is the unreplaced internalized form. SOCK_DGRAM variant likewise. 4/4 runs kernel-range. Fix (skip rights control on peek; non-rights control like SCM_CREDS remains peekable) validated on an in-guest nativekernel build: peek controllen=0 for rights, SCM_CREDS peek intact, real fd passing intact.

soreceive() MSG_PEEK branch: do not return unreplaced SCM_RIGHTS control data to userland (see findings/poc/DF-2702/fix.diff).

Timeline

  • 2026-08-30 Discovered during pass-2 audit of uipc_usrreq.c (GLM 5.3); unpriv leak reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2702 Β· 13 files
FileTypeDescriptionSize
peek_rights_leak.c β€” 5.0 KB view raw
dgram_peek.c β€” 1.3 KB view raw
build.sh β€” 90 B view raw
run.sh β€” 53 B view raw
run.log β€” 1003 B view raw
run.2.log β€” 1016 B view raw
env.txt β€” 255 B view raw
fix.diff β€” 1.2 KB view raw
fix_build_excerpt.txt β€” 891 B view raw
fix_validation.log β€” 960 B view raw
VERDICT.md β€” 6.3 KB ↓ raw
manifest.json β€” 926 B view raw
verdict.json β€” 3.8 KB view raw
VERDICT.md
↓ download raw

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:

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

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel #1 (make nativekernel with fix.diff): PEEK returns controllen=0 for SCM_RIGHTS (baseline pointers 0xfffff801xxxxxxxx gone); SCM_CREDS peek still returns control (controllen=104, type=3); real recvmsg still externalizes fds correctly (controllen=24, fds 6,7). Bad behavior eliminated, no functional regression.

['fix_validation.log', 'fix_build_excerpt.txt', 'fix.diff']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Aug 31 02:02:55 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

["run.log: PEEK controllen=32, cmsg_len=32, slots 0xfffff801167ffd80 and 0xfffff801167ffe80 marked 'RAW KERNEL POINTER', identical across PEEK #0/#1; REAL RECV controllen=24 with fds 6,7", 'run.2.log: SOCK_DGRAM variant leaks 0xfffff801167e5500; fresh-process rerun leaks new pointers 0xfffff801167e5e80/... (4/4 kernel-range)', 'fix_validation.log: patched kernel #1 PEEK controllen=0 (no pointers), REAL recv controllen=24 fds 6,7; creds_peek: SCM_CREDS still peekable (controllen=104, type=3)', 'fix.diff: MSG_PEEK branch skips returning unreplaced SCM_RIGHTS control', 'VERDICT.md: full narrative + negative-space analysis of uipc_usrreq.c']

PoC changes

PoC authored fresh (no seed). Two-peek + real-recv design to prove internalized-vs-externalized cmsg_len delta (32 vs 24) and pointer stability; added SOCK_DGRAM and SCM_CREDS-post-fix variants.

Verified recommended fix

soreceive MSG_PEEK branch: do not return unreplaced SCM_RIGHTS control to userland (skip rights cmsgs on peek; externalize only on consume)

Verdict

recvmsg(MSG_PEEK) on an AF_UNIX socket with a queued SCM_RIGHTS message copies the control mbuf (m_copym) without running dom_externalize/unp_externalize, disclosing raw kernel struct file pointers to unprivileged users. Reproduced as uid=1001 on SOCK_STREAM (controllen=32=CMSG_LEN(2*8), slots 0xfffff801167ffd80/0xfffff801167ffe80, stable across peeks) and SOCK_DGRAM (0xfffff801167e5500), 4/4 runs kernel-range; real recvmsg of the same message externalizes to fds 6,7, proving the peek branch is the anomaly. Root cause uipc_socket.c:1422-1425 interacting with unp_internalize (uipc_usrreq.c:1817-1825) and unp_externalize (uipc_usrreq.c:1543). fix.diff validated on an in-guest nativekernel build (#1): peek returns controllen=0 for rights (leak gone), SCM_CREDS peek intact, real fd passing intact.