unp_pcblist sysctl exports raw kernel pointers (unpcb/socket/sockaddr/vnode addresses + the AF_UNIX connection graph) to unprivileged users
Summary
net.local.{stream,dgram,seqpacket}.pcblist (CTLFLAG_RD, no privilege gate) returns a struct xunpcb per AF_UNIX socket containing xu_unpp (kernel unpcb address), a verbatim bcopy of struct unpcb (unp_socket, unp_conn, unp_addr, unp_vnode, unp_rvnode, unp_fp, list links) and xso_so/so_pcb from sotoxsocket β an unprivileged map of kernel heap objects plus the system-wide socket connection graph. Verified uid=1001 3/3: 24 kernel-range pointers per snapshot, so_pcb==xu_unpp and xso_so==unp_socket self-invariants hold per record, mutual unp_conn==peer.xu_unpp for the PoC's socketpair, unp_addr/unp_vnode/unp_rvnode disclosed for a bound SOCK_DGRAM. Distinct root cause from DF-2558 (uninitialized stack padding in the same output β zeroing cannot fix this). This is the disclosure that converts unreliable heap grooming into deterministic slab targeting for companion AF_UNIX bugs (e.g. DF-2702's raw struct file* leak) and defeats future KASLR. Pointer-scrub-for-unprivileged-readers fix proposed.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5816 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.2 KB | β raw | |
| VERDICT.md | β | 7.1 KB | β raw | |
| pcblist_ptrleak.c | β | 11.0 KB | view raw | |
| build.sh | β | 123 B | view raw | |
| run.sh | β | 136 B | view raw | |
| build.log | β | 519 B | view raw | |
| run.log | β | 5.0 KB | view raw | |
| fix_validation.log | β | 1.5 KB | view raw | |
| fix_build_excerpt.txt | β | 925 B | view raw | |
| fix.diff | β | 1.7 KB | view raw | |
| env.txt | β | 773 B | view raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 5.3 KB | view raw |
DF-5816 β unp_pcblist sysctl exports raw kernel pointers to unprivileged users
sys/kern/uipc_usrreq.c:unp_pcblist() (1432-1509) fills a stack
struct xunpcb xu for every AF_UNIX socket and SYSCTL_OUT()s it to any
reader of the world-readable net.local.{stream,dgram,seqpacket}.pcblist
sysctl nodes (1511-1519). Three of the exports are live kernel pointers:
xu.xu_unpp = unpβ kernel unpcb address (1476)bcopy(unp, &xu.xu_unp, sizeof(*unp))(1494) β copies the wholestruct unpcb, exportingunp_socket,unp_conn,unp_addr,unp_vnode,unp_rvnode,unp_fp,unp_refs,unp_reflink,unp_linkpointerssotoxsocket()(1495) β exportsxso_soandso_pcb(sys/kern/uipc_socket2.c:827-831)
Distinct from DF-2558 (uninitialized stack padding in the same sysctl output): this finding is the deliberate export of pointer values, which zeroing the buffer cannot fix.
Build
cc -O2 -Wall -o pcblist_ptrleak pcblist_ptrleak.c
(or sh build.sh)
Run (unprivileged)
sh run.sh # 3 runs, prints id + uname
Expected output (stock kernel)
global: 24 non-NULL exported pointers, 0 outside kernel range,
so_pcb==xu_unpp held for all 4 records
socketpair(): 2 new stream records
record A: ... unp_conn = 0xfffff8011788b110 == B.xu_unpp
record B: ... unp_conn = 0xfffff8011788b2b0 == A.xu_unpp
bound dgram: unp_addr 0xfffff8008d8011c0 KERNEL-RANGE
unp_vnode 0xfffff801170aa680 KERNEL-RANGE
unp_rvnode 0xfffff8008f520f00 KERNEL-RANGE
LEAK CONFIRMED: unprivileged uid=1001 obtained live kernel pointers ...
Success criterion: LEAK CONFIRMED with all printed pointers in the
kernel range (β₯ 0xffff000000000000), the kernel self-consistency checks
xu_socket.so_pcb == xu_unpp and xu_socket.xso_so == xu_unp.unp_socket
holding per record, and the mutual unp_conn == peer.xu_unpp correlation
for the socketpair the program itself created.
On a kernel with fix.diff applied (pointer export gated on
SYSCAP_RESTRICTEDROOT), the same binary prints
global: 0 non-NULL exported pointers and fails the correlation steps β
that absence is the fixed behavior (see fix_validation.log).
DF-5816 β VERDICT
REPRODUCED (leak), 3/3 runs, then fix validated in-guest.
What was tested
sys/kern/uipc_usrreq.c:unp_pcblist() walks every AF_UNIX unpcb under
unp_token and copies a struct xunpcb out through SYSCTL_OUT to any
reader of net.local.stream.pcblist / net.local.dgram.pcblist /
net.local.seqpacket.pcblist (uipc_usrreq.c:1432-1519). The record
contains, as initialized data (not padding):
xu_unppβ kernel address of thestruct unpcb(uipc_usrreq.c:1476)- the entire
struct unpcbbcopy'd verbatim (uipc_usrreq.c:1494), i.e.unp_socket,unp_conn,unp_addr,unp_vnode,unp_rvnode,unp_fp,unp_refs,unp_reflink,unp_linkkernel pointers (layout: sys/sys/unpcb.h:68-85) xso_so(struct socket *) andso_pcbfromsotoxsocket(sys/kern/uipc_socket2.c:827-831, called at uipc_usrreq.c:1495)
There is no privilege gate on the nodes (SYSCTL_PROC ... CTLFLAG_RD, uipc_usrreq.c:1511-1519) and no pointer redaction anywhere in the path.
How it was proven (uid 1001, stock kernel #0, see run.log)
- Kernel-range check β every non-NULL pointer field of every record
sits at β₯ 0xffff000000000000 (
0 outside kernel range, 24 non-NULL pointers per snapshot), 3/3 runs. - Liveness / not garbage β for every record the kernel's own
invariants hold in the exported data:
xu_socket.so_pcb == xu_unppandxu_socket.xso_so == xu_unp.unp_socket(so->so_pcbis the unpcb). Random stack garbage cannot satisfy both per record. - Heap-object graph disclosure β the program creates a
socketpair(AF_LOCAL, SOCK_STREAM)between two pcblist snapshots and identifies the two new records; they are mutually connected in the exported data:A.unp_conn == B.xu_unppandB.unp_conn == A.xu_unpp(e.g. 0xfffff8011788b2b0 <-> 0xfffff8011788b110). - Object-kind coverage β for a bound SOCK_DGRAM the program creates,
the record leaks
unp_addr(kmalloc'd sockaddr in kmem_map, e.g. 0xfffff8008d8011c0),unp_vnode(the bound socket's vnode, e.g. 0xfffff801170aa680) andunp_rvnode; the record is proven to be ours by the sun_path echoed insidexu_addr.
The mirror structs in the PoC are validated at runtime: every record's
xu_len (set by the kernel to sizeof(struct xunpcb)) equals the mirror
size (912 bytes on amd64), so a layout drift aborts instead of misparsing.
Impact ceiling
Info leak, not corruption β no uid=0 chain applicable. Ceiling: a complete
unprivileged map of kernel heap objects (unpcb, socket, sockaddr, vnode
addresses) plus the connection graph between all AF_UNIX sockets on the
system, including other users'/daemons' sockets. On this guest (no KASLR)
the values are even stable across runs. This is exactly the class of
disclosure that converts unreliable heap-grooming into deterministic
exploitation of the adjacent AF_UNIX memory bugs in this audit
(e.g. DF-2702's struct file * leak): unpcb (M_UNPCB, 208 B) and
socket allocations become addressable targets.
Fix validation (mandatory-class: N/A β leak, not corruption; done anyway)
fix.diff(authored after verification, never applied to the repo'ssys/): gate the pointer fields oncaps_priv_check_td(req->td, SYSCAP_RESTRICTEDROOT), scrubbingxu_unpp, allxu_unp.*pointers andxso_so/so_pcbfor unprivileged readers. Address payloads (xu_addr/xu_caddr) and all counters/state are unaffected.- Applied inside the guest
/usr/src(patch -p1, all hunks clean),make -j6 nativekernel KERNCONF=X86_64_GENERICβ KBUILD_OK,make installkernel, reboot intoDragonFly dfbsd 6.5-DEVELOPMENT #1: Tue Oct 13 04:30:17 UTC 2026. - Exact same PoC as uid 1001 on the patched kernel (fix_validation.log,
3 runs):
global: 0 non-NULL exported pointers, 0 outside kernel range; socketpair/dgram correlation steps can no longer find pointers β the leak is gone. - As root on the patched kernel:
global: 24 non-NULL exported pointersβ privileged consumers (netstat) keep full functionality. - Guest reset to the clean
with-srcsnapshot afterwards.
Negative space (classes hunted and killed in pass 3)
sun_len-driven overflows inunp_bind/unp_find_lockref(uipc_usrreq.c:1141-1145, 2425-2431):getsockaddr()forcessa->sa_len = lenand caps len at SOCK_MAXADDRLEN (uipc_syscalls.c:1513-1527), sostrncpy(buf, sun_path, namelen)is an exact fit; pcblistbcopy(..., sun_len)cannot exceed the 256-bytexu_au/xu_cauunions (sys/sys/unpcb.h:115-124).- Un-internalized SCM_RIGHTS reaching
unp_externalize: control is always a single MT_CONTROL mbuf β€ MLEN (uipc_syscalls.c:933-943);unp_internalizeconverts the first cmsghdr and truncatesm_len(uipc_usrreq.c:1809-1810); externalize/scan parse only the first cmsghdr of an mbuf (uipc_usrreq.c:1548-1554, 2344-2358) β a fake rights cmsg packed behind an SCM_CREDS first cmsg is never interpreted as pointers. MCLGET(control, M_WAITOK)silent failure inunp_internalize(uipc_usrreq.c:1772-1780):objcache_getwith pure M_WAITOK blocks and retries rather than returning NULL (kern_objcache.c objcache_get), so the post-MCLGET pointer writes stay inside the attached cluster.- Close/shutdown cascades vs in-flight sends: a sender's
unp_referencekeeps the peer unpcb (and thereby the socket:sofreeassertsso_pcb == NULLbefore final free, uipc_socket.c:380-415) alive across blocking token releases;so_rcvssb token serializes append vs sorflush. - accept/connect races:
sonewconn_faddrqueues so3 onso_incompunder the head token withSS_ASSERTINPROGuntil connect completes (uipc_socket2.c:440-477), so thesoabort_directfailure path (uipc_usrreq.c:1273-1275) cannot race accept(); unpcb refcount chain around sonewconn's kept ref is balanced (no double unp_free). - GC mark/sweep races: the sweep re-validates
fp->f_count == unp_msgcountper socket after every blocking window (uipc_usrreq.c:2266-2268), so rights externalized/discarded between mark and sweep de-schedule the candidate instead of UAF; deferredunp_discard(2371-2396, 2556-2571) holds one real reference per queued right, so the deferred fdrops cannot fire on freedstruct file. unp_externalizeerror paths (uipc_usrreq.c:1562-1616) close exactly the installed fds and discard+NULL the remaining pointers before the mbuf is handed to the caller, which frees it with plainm_freem(uipc_syscalls.c:1202-1203) β no double discard, no NULL unp_discard.uipc_connect2writesunp_peercredbefore the ISATTACHED checks (uipc_usrreq.c:1312-1318) β sloppy but unreachable-with-NULL (socketpair creates both pcbs before either fd exists).
Known findings DF-0010/0011/2557/2558/2559/2560/2702 were not re-tested here; this run's subject was the pointer export only.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedPatched kernel #1 (fix.diff applied to guest /usr/src, make nativekernel + installkernel + reboot): uid=1001 pcblist now exports 0 non-NULL pointers (baseline: 24 kernel-range pointers + socketpair graph); records still enumerate with intact xu_len and address payloads; root still sees all 24 pointers so privileged consumers (netstat) are unaffected. Bad behavior eliminated, no functional regression.
['fix_validation.log', 'fix_build_excerpt.txt', 'fix.diff']
Confirmed kernel references
Detail
Evidence (decisive lines)
["run.log: 'global: 24 non-NULL exported pointers, 0 outside kernel range, so_pcb==xu_unpp held for all 4 records' in all 3 runs (uid=1001)", "run.log: socketpair graph 'record A: unp_conn = 0xfffff8011788b110 == B.xu_unpp' / 'record B: unp_conn = 0xfffff8011788b2b0 == A.xu_unpp'", "run.log: bound dgram 'unp_addr 0xfffff8008d8011c0 KERNEL-RANGE', 'unp_vnode 0xfffff801170aa680 KERNEL-RANGE', 'unp_rvnode 0xfffff8008f520f00 KERNEL-RANGE', path prefix match proves the record is the PoC's own socket", "run.log: mirror self-validation 'xu_len == 912 == sizeof(mirror)' per record (layout drift would abort)", "fix_validation.log: patched kernel #1, uid=1001, 'global: 0 non-NULL exported pointers' in all 3 runs; correlation steps find no pointers (leak gone)", 'fix_build_excerpt.txt: patch hunks 1-4 clean, nativekernel KBUILD_OK, installkernel, uname #1: Tue Oct 13 04:30:17 UTC 2026; root still sees 24 pointers on patched kernel']
PoC changes
PoC authored fresh (no seed). Fixes during bring-up: DF sysctlbyname takes 5 args; sun_path comparison changed to strncmp-prefix because the kernel bcopy copies sun_len bytes with no NUL terminator. Mirror structs validated at runtime via the kernel-set xu_len field.
Verified recommended fix
unp_pcblist: scrub xu_unpp, xu_unp pointer fields and xu_socket.xso_so/so_pcb for readers failing caps_priv_check_td(req->td, SYSCAP_RESTRICTEDROOT) (keep address payloads and counters)
Verdict
unp_pcblist (sys/kern/uipc_usrreq.c:1432-1509) exports, via the world-readable net.local.{stream,dgram,seqpacket}.pcblist sysctls (1511-1519), a full struct xunpcb per AF_UNIX socket whose contents include live kernel pointers: xu_unpp (1476), a verbatim bcopy of struct unpcb (1494) carrying unp_socket/unp_conn/unp_addr/unp_vnode/unp_rvnode/unp_fp/list links, and xso_so/so_pcb from sotoxsocket (1495, sys/kern/uipc_socket2.c:827-831). Reproduced as uid=1001 on the stock guest, 3/3 runs: 24 non-NULL kernel-range pointers per snapshot, kernel self-invariants so_pcb==xu_unpp and xso_so==unp_socket hold in the exported data (proving live pointers, not garbage), a socketpair created by the PoC shows mutual unp_conn==peer.xu_unpp (e.g. 0xfffff8011788b2b0 <-> 0xfffff8011788b110), and a bound SOCK_DGRAM record discloses unp_addr (kmem sockaddr, 0xfffff8008d8011c0), unp_vnode and unp_rvnode. Impact ceiling: unprivileged mapping of kernel heap objects plus the system-wide AF_UNIX connection graph -- the exact disclosure that makes heap grooming deterministic for the companion AF_UNIX bugs (DF-2702 file-pointer leak). Distinct from DF-2558 (uninitialized stack padding in the same output; zeroing cannot fix this). fix.diff (privilege-gate via caps_priv_check_td/SYSCAP_RESTRICTEDROOT, scrub pointer fields for non-root) validated in-guest: patched kernel #1 shows 0 non-NULL exported pointers for uid=1001 while root still sees 24 (netstat functionality retained); guest reset clean afterwards.
No comments yet.