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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5816 Β· 13 files
FileTypeDescriptionSize
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 whole struct unpcb, exporting unp_socket, unp_conn, unp_addr, unp_vnode, unp_rvnode, unp_fp, unp_refs, unp_reflink, unp_link pointers
  • sotoxsocket() (1495) β€” exports xso_so and so_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).

VERDICT.md
↓ download raw

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 the struct unpcb (uipc_usrreq.c:1476)
  • the entire struct unpcb bcopy'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_link kernel pointers (layout: sys/sys/unpcb.h:68-85)
  • xso_so (struct socket *) and so_pcb from sotoxsocket (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)

  1. 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.
  2. Liveness / not garbage β€” for every record the kernel's own invariants hold in the exported data: xu_socket.so_pcb == xu_unpp and xu_socket.xso_so == xu_unp.unp_socket (so->so_pcb is the unpcb). Random stack garbage cannot satisfy both per record.
  3. 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_unpp and B.unp_conn == A.xu_unpp (e.g. 0xfffff8011788b2b0 <-> 0xfffff8011788b110).
  4. 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) and unp_rvnode; the record is proven to be ours by the sun_path echoed inside xu_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's sys/): gate the pointer fields on caps_priv_check_td(req->td, SYSCAP_RESTRICTEDROOT), scrubbing xu_unpp, all xu_unp.* pointers and xso_so/so_pcb for 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 into DragonFly 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-src snapshot afterwards.

Negative space (classes hunted and killed in pass 3)

  • sun_len-driven overflows in unp_bind/unp_find_lockref (uipc_usrreq.c:1141-1145, 2425-2431): getsockaddr() forces sa->sa_len = len and caps len at SOCK_MAXADDRLEN (uipc_syscalls.c:1513-1527), so strncpy(buf, sun_path, namelen) is an exact fit; pcblist bcopy(..., sun_len) cannot exceed the 256-byte xu_au/xu_cau unions (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_internalize converts the first cmsghdr and truncates m_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 in unp_internalize (uipc_usrreq.c:1772-1780): objcache_get with 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_reference keeps the peer unpcb (and thereby the socket: sofree asserts so_pcb == NULL before final free, uipc_socket.c:380-415) alive across blocking token releases; so_rcv ssb token serializes append vs sorflush.
  • accept/connect races: sonewconn_faddr queues so3 on so_incomp under the head token with SS_ASSERTINPROG until connect completes (uipc_socket2.c:440-477), so the soabort_direct failure 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_msgcount per 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; deferred unp_discard (2371-2396, 2556-2571) holds one real reference per queued right, so the deferred fdrops cannot fire on freed struct file.
  • unp_externalize error 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 plain m_freem (uipc_syscalls.c:1202-1203) β€” no double discard, no NULL unp_discard.
  • uipc_connect2 writes unp_peercred before 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched 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']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Tue Oct 13 04:30:17 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.