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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2702 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 β 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-bytecmsghdrfollowed by N rawstruct file *kernel pointers. Written byunp_internalize()(sys/kern/uipc_usrreq.c:1817-1825), which setscm->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 rewritescm->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_UNIXSOCK_STREAMsocketpair with usercmsg_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 slots0xfffff801167ffd80,0xfffff801167ffe80β two kernel heap pointers (0x100 apart, samestruct fileslab zone). Identical across both peeks. - REAL recvmsg of the same message returns
cmsg_len=24and 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_lockrefstackbuf[SOCK_MAXADDRLEN]+strncpywithsun_len:getsockaddr()normalizessa->sa_len = lenand capslen <= SOCK_MAXADDRLEN(sys/kern/uipc_syscalls.c:1519-1528), sonamelen <= 253and all copies stay in-bounds (uipc_usrreq.c:1141-1145, 2425-2431).unp_pcblistbcopy(..., sun_len)intoxu_addr: thexu_au/xu_cauunions 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_sendmsgboundscontrollento[sizeof(cmsghdr), MLEN]and copies it into a single mbuf (sys/kern/uipc_syscalls.c:934-943);unp_internalizevalidatescmsg_len β [CMSG_LEN(0), m_len](uipc_usrreq.c:1723-1729, 1757-1759) and rewritesm_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), andsoclose(thusuipc_detachβunp_dropβ kfree) runs only on the lastfdrop, so a blockednlookupinsideunp_find_lockref/unp_connectcannot 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_disconnectpeer-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-countunp_unreachable(uipc_usrreq.c:2114-2119 returns without UNPGC_SCANNED), but phase 2 re-validatesf_count == unp_msgcountbefore collecting and the loop exits when no DEAD entries remain β overcount is benign (no OOB onunref[], guarded by KASSERT at 2271-2279). Markers used for safe iteration;SYSCTL_OUT-blocking iteration inunp_pcblistis marker-safe.cru2x()bzeros the wholexucred(sys/kern/kern_prot.c:1237) β no leak via LOCAL_PEERCRED (uipc_usrreq.c:951).soreceiveno-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
fixedPatched 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']
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.
No comments yet.