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)
PoC verification
Evidence pack
findings/poc/DF-2558 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
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)
-
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 areSYSCTL_PROC(...,CTLFLAG_RD,...)with nopriv_check/susergate;netstat/fstatrely on them. -
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.
- 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
fixedVALIDATED. 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).
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
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).
No comments yet.