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

Kernel stack info leak via unp_pcblist sysctl (net.local.{dgram stream seqpacket}.pcblist)

Summary

pcblist sysctl handler walks all AF_UNIX PCBs and for each emits struct xunpcb xu on stack without initializer. Only xu_len xu_unpp xu_unp(full bcopy) partial fill of xu_addr/xu_caddr(sun_len bytes of 256-byte unions) and xu_socket written. Trailing bytes of two 256-byte unions plus xu_alignment_hack(8 bytes) uninitialized kernel stack. SYSCTL_OUT copies entire struct to userspace. CTLFLAG_RD readable without privilege. Per PCB up to ~520 bytes kernel stack leaked. SYSCTL_OUT drops token to block so stack slot reused accumulates fresh residue.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2558 Β· 16 files
FileTypeDescriptionSize
poc.c trigger-source AF_UNIX socket plant + pcblist sysctl read + leak-byte counter 9.6 KB view raw
build.sh build-script cc -O2 -o poc poc.c 141 B view raw
run.sh run-script ./poc 3 121 B view raw
build.log build-log unpatched-kernel PoC build output 84 B view raw
run.log run-log unpatched baseline run 1 (LEAK CONFIRMED, 12375 bytes) 14.7 KB view raw
run.2.log run-log unpatched baseline run 2 (12049 bytes) 14.1 KB view raw
run.3.log run-log unpatched baseline run 3 (11268 bytes) 14.2 KB view raw
leak_sample.txt leak-sample raw leaked hex + run-to-run variance 1.9 KB view raw
fix.diff suggested-fix bzero(&xu, sizeof(xu)) in unp_pcblist loop 416 B view raw
fix_build.log build-log full single-fix kernel build output (nativekernel) 5.6 MB ↓ download
fix_run.log run-log patched #1 kernel run 1 (NO LEAK, 0 bytes) 2.3 KB view raw
fix_run.2.log run-log patched #1 kernel run 2 (NO LEAK, 0 bytes) 2.3 KB view raw
fix_run.3.log run-log patched #1 kernel run 3 (NO LEAK, 0 bytes) 2.3 KB view raw
env.txt environment uname, cc version, sysctls, sysctl readability 438 B view raw
README.md readme bug, impact, build/run, expected output 2.1 KB ↓ raw
VERDICT.md verdict full narrative: mechanism, evidence, fix, fix-validation 7.4 KB ↓ raw
README.md readme bug, impact, build/run, expected output
↓ download raw

DF-2558 β€” Kernel stack info leak via the AF_UNIX pcblist sysctl

Bug

sys/kern/uipc_usrreq.c:1465 β€” unp_pcblist() (the handler behind net.local.{dgram,stream,seqpacket}.pcblist) declares

struct xunpcb xu;          /* ON THE STACK, NO INITIALIZER, 912 B */

and only fills xu_len, xu_unpp, a partial bcopy of unp_addr/conn addr (≀ sun_len bytes of two 256-byte unions), the full xu_unp, and xu_socket. The trailing bytes of xu_addr/xu_caddr past sun_len plus the 8-byte xu_alignment_hack trailer (sys/sys/unpcb.h:126, never written) stay as raw kernel-stack residue. SYSCTL_OUT(req,&xu, sizeof xu) then copies the whole struct verbatim to userspace (uipc_usrreq.c:1498).

The pcblist sysctl nodes are CTLFLAG_RD with no privilege check (uipc_usrreq.c:1511-1519), so any unprivileged local user can read them.

Impact

Kernel stack info leak (CWE-908 / CWE-200). Up to ~510 bytes per PCB of uninitialized kernel-stack residue β€” including a canonical kernel-virtual pointer in xu_alignment_hack every record β€” are readable by any local unprivileged user, in a tight loop. Useful as a KASLR / stack-residue oracle for a separate kernel exploit. No write primitive β€” the leak itself is the finding (Medium).

Build & run

./build.sh          # cc -O2 -o poc poc.c
./run.sh            # ./poc 3   (unprivileged)

Expected output (unpatched, bug present)

A per-record breakdown in which xu_addr tail / xu_caddr / xu_alignment_hack carry non-zero kernel-stack residue that varies between samples (e.g. xu_alignment_hack (8 B): 90870e81 ffffffff = 0xffffffff810e8790), and the LEAK CONFIRMED summary line. Typical leak: ~300 non-zero bytes/record out of ~500 possible; ~12k non-zero bytes over 3 sysctl reads.

Expected output (patched, bug gone)

The same regions are all 0x00 and the program prints NO LEAK (0 leaked non-zero bytes).

Reproduction environment

DragonFly 6.5-DEVELOPMENT master DEV, X86_64_GENERIC (INVARIANTS ON), with-src snapshot. Fully unprivileged: socket+bind AF_UNIX, sysctlnametomib, sysctl(2). No setup, no root, no special config.

VERDICT.md verdict full narrative: mechanism, evidence, fix, fix-validation
↓ download raw

DF-2558 β€” VERDICT

Verdict

REPRODUCED β€” uninitialized kernel-stack info leak via the AF_UNIX pcblist sysctl handler (unp_pcblist in sys/kern/uipc_usrreq.c). The sysctl is world-readable (CTLFLAG_RD, no privilege check), so any unprivileged local user can sample it. Fix VALIDATED on a built-and- booted single-fix kernel: the leak is gone (0 leaked bytes across 3Γ—3 runs).

Mechanism (trigger β†’ primitive β†’ effect)

  1. Trigger β€” an unprivileged user creates a few AF_UNIX sockets (so the pcblist contains records) and reads one of the world-readable pcblist sysctl nodes: - net.local.dgram.pcblist - net.local.stream.pcblist - net.local.seqpacket.pcblist (sys/kern/uipc_usrreq.c:1511-1519). These are SYSCTL_PROC(...,CTLFLAG_RD,...) with no priv_check/suser gate; netstat/fstat rely on them.

  2. Primitive (root cause) β€” unp_pcblist() (sys/kern/uipc_usrreq.c:1464-1502) walks every PCB in the head list and, for each one, declares the 912-byte response record on the stack with no initializer: c /* uipc_usrreq.c:1465 */ while ((unp = TAILQ_NEXT(marker, unp_link)) != NULL && i < n) { struct xunpcb xu; /* <-- UNINITIALIZED on-stack, 912 B */ ... It then writes only: - xu.xu_len = sizeof(xu) (uipc_usrreq.c:1475) - xu.xu_unpp = unp (uipc_usrreq.c:1476) - bcopy(unp->unp_addr, &xu.xu_addr, sun_len) partial fill of a 256-byte union (uipc_usrreq.c:1486) - bcopy(conn->unp_addr, &xu.xu_caddr, conn_sun_len) partial fill of a 256-byte union (uipc_usrreq.c:1490) - bcopy(unp, &xu.xu_unp, sizeof(*unp)) full struct unpcb copy (uipc_usrreq.c:1494) - sotoxsocket(so, &xu.xu_socket) full xsocket fill (uipc_usrreq.c:1495)

Three regions of the struct are never written and stay as raw stack: - the tail of xu.xu_addr union past sun_len (up to 256 B) - the tail of xu.xu_caddr union past conn_sun_len (up to 256 B; for an unconnected socket, the entire 256 B since nothing is copied) - xu_alignment_hack (8 B trailer at the end of struct xunpcb, sys/sys/unpcb.h:126) β€” never written by the handler.

  1. Effect β€” kernel stack info leak β€” SYSCTL_OUT(req, &xu, sizeof(xu)) (uipc_usrreq.c:1498) copies the whole 912-byte struct verbatim to userspace, including the uninitialized tail. The comment on line 1497 ("This could block and temporarily release unp_token") means the stack slot is reused between records, so each record carries fresh residue.

Per record up to ~510 bytes of pure kernel-stack residue leak; for a bound, unconnected dgram socket (sun_lenβ‰ˆ18) the measured leak is ~290–340 non-zero bytes. xu_alignment_hack reliably leaks the canonical kernel-virtual pointer 0xffffffff810e8790 in every record of every run (CWE-908 uninitialized-stack disclosure / CWE-200 info exposure).

Reproduction evidence (unpatched #0 baseline)

run.log, run.2.log, run.3.log β€” three standalone runs of ./poc 3 (3 sysctl reads each), excerpt:

=== iter 0: 10944 bytes / 12 records (recsize=912) ===
  rec 0 (xu_unpp=0xfffff8008edf0660 sun_path="/var/run/log"):
    xu_alignment_hack            (8 B): 90870e81 ffffffff
    >>> rec 0 leaked = 329 / 506 possible
  ...
==== SUMMARY over 3 iters, 39 records: 12375 leaked non-zero bytes (of 18972 possible) ====
result: LEAK CONFIRMED (kernel-stack residue in xunpcb via pcblist sysctl)

Run-to-run totals vary (11268, 12049, 12375) β€” genuine stack-residue variation, not deterministic struct contents. The residue contains many 0xfffff8??_???????? and 0xffffffff_???????? qwords (recognizable kernel pointer fragments β†’ KASLR-defeat / stack-residue oracle).

Why it is NOT a write primitive / no escalation chain

unp_pcblist is a read-only sysctl handler: it only copies bytes OUT to userspace and writes no attacker-controlled data into the kernel. There is no write/UAF/double-free/type-confusion path derivable from it. Read-only info leak is a valid hard blocker for an escalation chain (Phase 6 valid blocker #1: "primitive is genuinely read-only"). Impact ceiling: KASLR-defeat / kernel-stack-residue oracle that would aid a separate write-capable bug. The leak itself is the finding (Medium).

Fix

fix.diff β€” zero-initialize the on-stack record before filling it:

    while ((unp = TAILQ_NEXT(marker, unp_link)) != NULL && i < n) {
        struct xunpcb xu;
+       bzero(&xu, sizeof(xu));

        TAILQ_REMOVE(&head->list, marker, unp_link);

This is a one-line, root-cause fix that closes every uninitialized region (xu_addr/xu_caddr tails + xu_alignment_hack) with a single bzero. It is the same pattern used to fix the sibling DF-2557 (SO_PASSCRED cmsgcred) leak, and matches the finding markdown's bzero/={} intent. git apply --check -p1 passes.

Fix validation (Phase 8)

Kernel kern.version sha256 (/boot/kernel/kernel) Result
baseline (unpatched) 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (snapshot baseline) LEAK β€” 11268–12375 non-zero bytes / 3 iters
single-fix 6.5-DEVELOPMENT #1: Sat Aug 8 23:50:52 UTC 2026 4ae87806a10a1d54cbd206c6fdb54812677d7d9b33ff84e416bac112e1647a1f NO LEAK β€” 0 non-zero bytes across 3Γ—3 runs

The single-fix kernel was built with make -j6 nativekernel KERNCONF=X86_64_GENERIC from the patched /usr/src, installed with make installkernel KERNCONF=X86_64_GENERIC, and booted. The kern.version #N suffix bumped #0 β†’ #1 with today's build timestamp, confirming the patched kernel is the one running.

On the patched kernel the same PoC now prints, for every record:

  rec 0 leaked = 0 / 506 (clean) (al=14 cl=0)
  ...
==== SUMMARY over 3 iters, 36 records: 0 leaked non-zero bytes (of 18060 possible) ====
result: NO LEAK (xunpcb trailing bytes all zero - bug not present / fixed)

xu_alignment_hack is now 00000000 00000000 (the bzero killed the leak). fix_status = fixed.

PoC changes

Authored the PoC from scratch (poc.c) β€” the findings/poc/DF-2558/ folder was empty. The PoC: plants 8 bound AF_UNIX SOCK_DGRAM sockets (so the dgram pcblist has records whose xu_caddr/xu_alignment_hack are pure uninitialized stack), reads net.local.dgram.pcblist via sysctlnametomib+sysctl(2), walks each 912-byte struct xunpcb record, and counts non-zero bytes in the three leak regions (xu_addr tail, the whole/remaining xu_caddr union, and xu_alignment_hack). Uses the real <sys/unpcb.h> struct (sizeof=912 on x86_64) with offsetof for the region bounds. Reports per-record and per-iteration totals; exits 0 with LEAK CONFIRMED if any residue is found, else exits 1 with NO LEAK.

Files

File Purpose
poc.c trigger PoC (unprivileged)
build.sh / run.sh exact build & run
build.log unpatched-kernel build output
run.log / run.2.log / run.3.log unpatched baseline runs (3Γ— variance)
fix_run.log / fix_run.2.log / fix_run.3.log patched-kernel runs (determinism)
fix_build.log full single-fix kernel build output
leak_sample.txt raw leaked hex + variance across baseline runs
env.txt guest environment (uname, cc, sysctls, readability)
fix.diff git-apply-able fix
manifest.json artifact catalog

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. PoC leaks kernel-stack residue on unpatched #0 baseline (11268-12375 non-zero bytes over 3 sysctl reads, incl. kernel pointer 0xffffffff810e8790 in xu_alignment_hack every record) and does NOT leak on single-fix #1 kernel built 'make -j6 nativekernel' from patched /usr/src, 'make installkernel', booted (kern.version #N bumped #0->#1). On patched kernel same PoC reports 0 leaked non-zero bytes across 3x3 runs => bzero closes the bug.

baseline (#0 unpatched): rec 0: xu_alignment_hack (8 B): 90870e81 ffffffff, leaked = 329/506; SUMMARY over 3 iters 39 records: 12375 leaked non-zero bytes (of 18972); LEAK CONFIRMED. patched (#1 single-fix): rec 0 leaked = 0/506 (clean); SUMMARY over 3 iters 36 records: 0 leaked non-zero bytes (of 18060); NO LEAK (deterministic across fix_run.log/.2/.3).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Aug 8 23:50:52 UTC 2026 (sha256 /boot/kernel/kernel = 4ae87806a10a1d54cbd206c6fdb54812677d7d9b33ff84e416bac112e1647a1f)

Confirmed kernel references

Detail

Exploit chain

none β€” read-only info leak. unp_pcblist is a pure sysctl read handler: only copies bytes OUT to userspace, writes no attacker-controlled data into kernel, so no write/UAF/double-free/type-confusion primitive derivable. Valid Phase-6 hard blocker (primitive genuinely read-only). Impact ceiling: samplable KASLR-defeat / kernel-stack-residue oracle (every record leaks canonical kernel-virtual pointer via xu_alignment_hack plus ~300 B of pointer-laden residue) that would aid a separate write-capable bug. The leak itself is the finding (Medium).

Evidence (decisive lines)

BEFORE (unpatched #0): iter 0: 10944 bytes / 12 records (recsize=912); rec 0 (xu_unpp=0xfffff8008edf0660 sun_path='/var/run/log'): xu_addr.sun_len=14 -> tail[238..480) non-zero=154; xu_caddr.sun_len=236 -> leak[480..736) non-zero=167; xu_alignment_hack -> [904..912) non-zero=8; xu_alignment_hack (8 B): 90870e81 ffffffff; rec 0 leaked = 329 / 506. SUMMARY over 3 iters, 39 records: 12375 leaked non-zero bytes (of 18972). result: LEAK CONFIRMED. AFTER (patched #1): 0 leaked non-zero bytes, result: NO LEAK.

PoC changes

Authored PoC from scratch (dir empty). poc.c plants 8 bound AF_UNIX SOCK_DGRAM sockets so dgram pcblist carries records whose xu_caddr/xu_alignment_hack are pure uninitialized stack, reads net.local.dgram.pcblist via sysctlnametomib+sysctl(2), walks each 912-byte struct xunpcb record (real struct, sizeof=912), counts non-zero bytes in three leak regions (xu_addr tail past sun_len, whole/remaining xu_caddr union, xu_alignment_hack) via offsetof. Exits 0 + 'LEAK CONFIRMED' if residue found. Two compile fixes: UNIX_PATH_MAX -> sizeof(sun_path), added for offsetof. fix.diff (bzero(&xu,sizeof(xu)) in loop, git apply --check passes), VERDICT.md, README.md, manifest.json, build.sh, run.sh.

Verified recommended fix

In sys/kern/uipc_usrreq.c:1465, zero-initialize the on-stack record before filling it: add 'bzero(&xu, sizeof(xu));' immediately after 'struct xunpcb xu;'. Single line closes every uninitialized region (xu_addr/xu_caddr tails + 8-byte xu_alignment_hack trailer). Supersedes finding markdown's bzero/={} intent β€” verified, line-accurate, git-apply-able form (same pattern as sibling DF-2557 fix). Full diff in findings/poc/DF-2558/fix.diff.

Verdict

REPRODUCED. The unp_pcblist() sysctl handler (sys/kern/uipc_usrreq.c:1464-1502) declares struct xunpcb xu; (912 B on x86_64) on the stack with NO initializer (uipc_usrreq.c:1465) and only partially fills it: xu_len/xu_unpp (1475-1476), partial bcopy of unp_addr/conn-addr into two 256-byte unions (sun_len bytes only, 1486/1490), full xu_unp bcopy (1494), and xu_socket (1495). Trailing bytes of xu_addr/xu_caddr past sun_len plus the 8-byte xu_alignment_hack trailer (sys/sys/unpcb.h:126, NEVER written) stay as raw kernel stack, and SYSCTL_OUT copies whole struct to userspace (1498). The three pcblist sysctl nodes are CTLFLAG_RD with no privilege gate (1511-1519), confirmed world-readable by unprivileged maxx. PoC planted 8 bound AF_UNIX dgram sockets, read net.local.dgram.pcblist, counted non-zero bytes in leak regions: 11268-12375 leaked bytes over 3 sysctl reads, with xu_alignment_hack reading 0xffffffff810e8790 (kernel pointer) in every record of every run. Run-to-run variance confirms genuine uninitialized stack residue (CWE-908/CWE-200).