kinfo_proc (kern.proc.*) exports unredacted kernel pointers (KASLR defeat)
| Field | Value |
|---|---|
| ID | DF-0016 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-200 Exposure of Sensitive Information to an Unauthorized Actor |
| File | sys/kern/kern_proc.c (copyout path); sys/kern/kern_kinfo.c:128-129, 272, 301, 321 (assignment) |
| Lines | 1603-1648 |
| Area | kern |
| Confidence | likely |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
The kern.proc.* sysctls are world-readable and copy out the full
struct kinfo_proc. fill_kinfo_proc()/fill_kinfo_lwp() populate raw
kernel virtual addresses into user-visible fields β kp_paddr = (uintptr_t)p
(struct proc slab address), kp_fd = (uintptr_t)p->p_fd (filedesc slab
address), kl_wchan (wait-channel address), kp_ktaddr (kernel thread
address). These are copied out unredacted, so any unprivileged local user can
recover kernel heap/.text addresses β defeating KASLR and directly enabling
the slab-grooming needed to turn heap-corruption bugs (e.g. DF-0013) into
reliable privilege escalation.
Root cause
sys/kern/kern_kinfo.c:128-129:
kp->kp_paddr = (uintptr_t)p; /* struct proc slab address */
kp->kp_fd = (uintptr_t)p->p_fd; /* struct filedesc slab address*/
and at :272, :301, :321 the kl_wchan / kp_ktaddr fields. These
fields are written into kinfo_proc/kinfo_lwp which are then copied to
userland unredacted through the sysctl_out_proc() copyout path in
sys/kern/kern_proc.c:1603-1648. The kern.proc.* nodes (:2173-2201) are
CTLFLAG_RD (world-readable); sysctl reads are not privilege-gated.
This pattern is historically retained for libkvm/ps(1) compatibility
(walking the process list over /dev/mem via kp_paddr).
Threat model & preconditions
- Attacker position: any local unprivileged user.
- Privileges gained or impact: information disclosure β live slab
addresses of every
struct procandstruct filedesc, plus wait-channel / kernel-thread addresses. A reliable KASLR-bypass / slab-layout primitive. Most significantly, it escalates the practical severity of DF-0013 (thekern.proc.argsheap overflow) and any future slab-corruption bug from DoS to reliable code execution by defeating KASLR and revealing the slab layout. - Required config or capabilities: none; default kernel.
- Reachability:
sysctl kern.proc.pid.<pid>(and the otherkern.proc.*variants) as any user.
Proof of concept
PoC source: findings/poc/DF-0016/leak_kinfo.c
Build & run
cc -o leak_kinfo findings/poc/DF-0016/leak_kinfo.c ./leak_kinfo # as a non-root user
Expected output
pid <self> kp_paddr=0xffff... kp_fd=0xffff... result: LEAK CONFIRMED (KASLR-defeat / slab-address primitive)
Impact
Lowers the bar for exploiting any local kernel memory-corruption bug by defeating KASLR and revealing kernel heap layout. Particularly impactful in combination with DF-0013. Information disclosure only (addresses, not arbitrary memory); rated Low standalone.
Recommended fix
Stop exporting kernel addresses to unprivileged readers. Minimal change in
sys/kern/kern_kinfo.c:
--- a/sys/kern/kern_kinfo.c
+++ b/sys/kern/kern_kinfo.c
@@ -128,8 +128,8 @@
- kp->kp_paddr = (uintptr_t)p;
- kp->kp_fd = (uintptr_t)p->p_fd;
+ kp->kp_paddr = 0; /* do not leak kernel addresses to userland */
+ kp->kp_fd = 0;
@@ -272
- kl->kl_wchan = (uintptr_t)lwp->lwp_thread->td_wchan;
+ kl->kl_wchan = 0;
@@ -301
- kp->kp_ktaddr = (uintptr_t)td;
+ kp->kp_ktaddr = 0;
@@ -321
- kp->kp_lwp.kl_wchan = (uintptr_t)td->td_wchan;
+ kp->kp_lwp.kl_wchan = 0;
Caveat: libkvm consumers that rely on kp_paddr to walk the allproc
list over /dev/mem need a migration path β e.g. gate the real addresses
behind a kinfo_kvm flag or a root-only sysctl variant, so unprivileged
readers get zeroes while /dev/mem-based tools (which require root anyway)
still work. (This fix lives in kern_kinfo.c, which is the next file slated
for audit.)
References
sys/kern/kern_kinfo.c:128-129βkp_paddr/kp_fdraw-pointer assignment (verified).sys/kern/kern_kinfo.c:272,301,321βkl_wchan/kp_ktaddrassignments.sys/kern/kern_proc.c:1603-1648β copyout path (this file).- CWE-200 Exposure of Sensitive Information to an Unauthorized Actor.
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/kern_proc.c. - pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0016 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| leak_kinfo.c | trigger-source | unpriv kern.proc.* kinfo pointer-leak PoC (dynamic kern.proc.all tally) | 6.1 KB | view raw |
| build.sh | build-script | cc -o leak_kinfo leak_kinfo.c | 175 B | view raw |
| run.sh | run-script | ./leak_kinfo as unprivileged user; exit 0=leaked, 2=redacted | 334 B | view raw |
| README.md | readme | build/run/expected + before/after output | 2.9 KB | β raw |
| VERDICT.md | verdict | REPRODUCED + fix validated narrative with path:line refs | 7.5 KB | β raw |
| run.log | run-log | decisive baseline run on unpatched #0: 69 ptrs / 23 procs | 1.9 KB | view raw |
| run.2.log | run-log | prior-session variance run (hardcoded-pids variant) | 2.4 KB | view raw |
| leak_sample.txt | leak-sample | baseline leaked addresses + before/after contrast | 3.3 KB | view raw |
| fix.diff | suggested-fix | zero kp_paddr/kp_fd/kl_wchan/kp_ktaddr/kp_lwp.kl_wchan in kern_kinfo.c (validated) | 1.3 KB | view raw |
| fix_build.log | build-log | full nativekernel output for the single-fix build, NK_DONE rc=0 | 5.6 MB | β download |
| fix_run.log | run-log | patched #1 run: 0 ptrs / 23 procs, exit 2 (3x deterministic) | 2.0 KB | view raw |
| env.txt | environment | uname #0/#1, kernel sha256, cc version, sysctls, maxx identity | 671 B | view raw |
| build.log | build-log | kernel build log excerpt proving -Werror clean compile of patched source | 223 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0016 β PoC
leak_kinfo.c β unprivileged disclosure of kernel heap / struct proc
pointers via the world-readable kern.proc.* sysctl (kinfo_proc).
The bug
fill_kinfo_proc() / fill_kinfo_lwp() / fill_kinfo_proc_kthread()
(sys/kern/kern_kinfo.c:128-129, :272, :301, :321) fill user-visible
kinfo_proc / kinfo_lwp fields with raw kernel virtual addresses:
kp->kp_paddr = (uintptr_t)p; /* kern_kinfo.c:128 */
kp->kp_fd = (uintptr_t)p->p_fd; /* kern_kinfo.c:129 */
kl->kl_wchan = (uintptr_t)td_wchan; /* kern_kinfo.c:272 */
kp->kp_ktaddr = (uintptr_t)td; /* kern_kinfo.c:301 */
kp->kp_lwp.kl_wchan = (uintptr_t)td->td_wchan; /* kern_kinfo.c:321 */
These are copied out unredacted via sysctl_out_proc
(sys/kern/kern_proc.c:1603/1612/1633). sysctl_kern_proc for
KERN_PROC_PID only checks PRISON_CHECK (jail), not p_trespass
(kern_proc.c:1690), so an unprivileged user reads the kinfo_proc of any
pid (including every root daemon) and recovers those addresses β a
KASLR-bypass / slab-address primitive.
Build
cc -o leak_kinfo leak_kinfo.c # or: ./build.sh
Run
As an unprivileged user (e.g. maxx):
./leak_kinfo # or: ./run.sh
Expected output (bug present β unpatched #0)
== self ==
pid 850 uid=1001 comm=leak_kinfo
kp_paddr = 0xfffff80116e97780 (struct proc slab)
kp_fd = 0xfffff80116eceb40 (filedesc slab)
== pid 1 (init) ==
pid 1 uid=0 comm=init
kp_paddr = 0xfffff80089977280 (struct proc slab)
kp_fd = 0xfffff8008d2254c0 (filedesc slab)
kl_wchan = 0xfffff80089977280 (wait channel)
result: 69 kernel pointers leaked across 23 processes (of 23 total readable)
result: LEAK CONFIRMED (KASLR-defeat / slab-address primitive) [exit 0]
kp_paddr is the live slab address of the target's struct proc. The tally
is taken over every readable process via kern.proc.all, so it is robust
across boots (no hardcoded PIDs). On the prior-session boot a .text-range
wait channel was also leaked: cron kl_wchan 0xffffffff8130f670 β nm symbol
nanowait (exact) β directly revealing the kernel text base.
Expected output (bug fixed β single-fix kernel #1)
== pid 1 (init) ==
pid 1 uid=0 comm=init
kp_paddr = 0x0000000000000000
kp_fd = 0x0000000000000000
kl_wchan = 0x0000000000000000
kp_ktaddr = 0x0000000000000000
result: 0 kernel pointers leaked across 0 processes (of 23 total readable)
result: no kernel pointers observed (fields appear redacted) [exit 2]
The sysctl access path is unchanged (23 procs still readable), only the kernel
addresses are zeroed before copyout. See VERDICT.md for the full before/after
fix validation and fix.diff for the verified patch.
DF-0016 β kern.proc.* (kinfo_proc) exports unredacted kernel pointers (KASLR defeat)
Verdict
REPRODUCED + FIX VALIDATED. The world-readable kern.proc.* sysctl returns
the full struct kinfo_proc with unredacted kernel virtual addresses in
kp_paddr (struct proc slab), kp_fd (filedesc slab), kl_wchan (wait
channel) and kp_ktaddr (kernel thread). Confirmed on the unpatched
DragonFly master DEV 6.5-DEVELOPMENT #0 (Thu Jul 2 06:02:54 UTC 2026): as
unprivileged maxx (uid 1001, not in wheel) 69 kernel pointers leaked across
23 readable processes in a stable, deterministic tally. The standalone
fix.diff (zero those 5 fields in sys/kern/kern_kinfo.c before copyout) was
applied to /usr/src, built as a single-fix kernel #1, and booted: re-running
the same PoC yields 0 leaked pointers (all fields redacted) β the leak
is closed with no functional regression (the sysctl still returns all 23
processes; only the kernel addresses are zeroed).
Mechanism (confirmed by source + run)
fill_kinfo_proc (sys/kern/kern_kinfo.c:128-129):
kp->kp_paddr = (uintptr_t)p; /* struct proc slab address */
kp->kp_fd = (uintptr_t)p->p_fd; /* struct filedesc slab address */
fill_kinfo_lwp (kern_kinfo.c:272): kl->kl_wchan = (uintptr_t)lwp->lwp_thread->td_wchan;
fill_kinfo_proc_kthread (kern_kinfo.c:301): kp->kp_ktaddr = (uintptr_t)td;
and (kern_kinfo.c:321): kp->kp_lwp.kl_wchan = (uintptr_t)td->td_wchan;
These five (uintptr_t) casts are the only kernel-pointer assignments into
struct kinfo_proc/struct kinfo_lwp in kern_kinfo.c (verified by
grep -n "uintptr_t"). The fields live in user-visible struct kinfo_proc
(sys/sys/kinfo.h:174 kp_paddr, :183 kp_fd, :237 kp_ktaddr,
:149 kl_wchan). They are copied out unredacted by sysctl_out_proc
(sys/kern/kern_proc.c:1603 fill, :1612/:1633 SYSCTL_OUT(req, &ki, sizeof(ki))).
Reachability: sysctl_kern_proc for KERN_PROC_PID (kern_proc.c:1686-1694)
does pfind(name[0]) and only checks PRISON_CHECK(cr1, crcache) (:1690) β
no p_trespass. So an unprivileged user reads the kinfo_proc of any pid
(not just own). The kern.proc.* nodes are CTLFLAG_RD and sysctl reads are
not framework-gated (kern_sysctl.c:1446-1450 gates writes only).
Proof (baseline, unpatched #0)
./leak_kinfo as maxx (uid 1001) enumerates kern.proc.all and counts the
kernel-range pointer fields across every readable process (excerpt; full in
leak_sample.txt / run.log):
== self ==
pid 850 uid=1001 comm=leak_kinfo
kp_paddr = 0xfffff80116e97780 (struct proc slab)
kp_fd = 0xfffff80116eceb40 (filedesc slab)
== pid 1 (init) ==
pid 1 uid=0 comm=init
kp_paddr = 0xfffff80089977280 (struct proc slab)
kp_fd = 0xfffff8008d2254c0 (filedesc slab)
kl_wchan = 0xfffff80089977280 (wait channel)
result: 69 kernel pointers leaked across 23 processes (of 23 total readable)
result: LEAK CONFIRMED (KASLR-defeat / slab-address primitive) RUN_EXIT=0
3 baseline runs are identical (69 / 23 procs); root-daemon slab addresses are
byte-identical across all 3 (e.g. init kp_paddr 0xfffff80089977280 every
time β a real, fixed live kernel object), while the self (leak_kinfo)
kp_paddr differs run-to-run because each run is a new process β itself
confirming these are live slab allocations, not stack residue. In the prior
session a .text-range wait-channel was observed: cron kl_wchan
0xffffffff8130f670 β nearest nm symbol nanowait (exact), directly
revealing the kernel text base (KASLR defeat).
Fix validation (Phase 8)
fix.diff (sys/kern/kern_kinfo.c, this folder) zeroes all 5 pointer-cast
sites β kp_paddr, kp_fd, kl_wchan (lwp), kp_ktaddr, kp_lwp.kl_wchan
(kthread) β before they reach userland. This is complete coverage: grep
uintptr_t kern_kinfo.c shows these are the only 5 kernel-address assignments
into the copied-out struct.
| step | result |
|---|---|
vm.sh reset with-src β confirmed #0 unpatched |
6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 |
baseline PoC on #0 (as maxx) |
69 pointers / 23 procs, RUN_EXIT=0 (LEAK CONFIRMED) β reproduced |
cd /usr/src && patch -p1 < fix.diff |
Hunk #1..#4 succeeded at 125/269/298/318 β APPLIED clean |
make -j6 nativekernel KERNCONF=X86_64_GENERIC |
=== NK_DONE rc=0 === (no errors; full log fix_build.log) |
install kernel.stripped β /boot/kernel/kernel + reboot |
6.5-DEVELOPMENT #1: Thu Jul 2 19:25:22 UTC 2026 |
same PoC on patched #1 (as maxx) |
0 pointers / 0 procs, RUN_EXIT=2 (redacted) β 3Γ deterministic |
== pid 1 (init) == [patched #1]
kp_paddr = 0x0000000000000000 (REDACTED)
kp_fd = 0x0000000000000000 (REDACTED)
kl_wchan = 0x0000000000000000 (REDACTED)
kp_ktaddr = 0x0000000000000000 (REDACTED)
result: 0 kernel pointers leaked across 0 processes (of 23 total readable)
result: no kernel pointers observed (fields appear redacted) RUN_EXIT=2
before/after: baseline #0 leaked 69 kernel pointers / 23 procs β
single-fix #1 leaked 0 / 23 procs. The sysctl access path is unchanged
(23-24 processes still readable, PRISON_CHECK still the only gate), so there
is no functional regression β only the kernel addresses are redacted.
fix_status = fixed.
Impact
Information disclosure to any local unprivileged user: the live slab address of
every process's struct proc and struct filedesc, plus wait-channel /
kernel-thread addresses (sometimes a .text symbol). A reliable KASLR-bypass
and slab-layout primitive. Standalone it is info-disclosure (rated Low), but it
is the enabler that escalates the practical severity of any local heap /
struct-proc corruption bug (e.g. DF-0013) from DoS to reliable local privilege
escalation by defeating KASLR and revealing the slab layout for grooming. This
is not a memory-corruption class β there is no further exploit chain to develop
(no primitive beyond address disclosure).
PoC changes (this session)
- Rewrote
leak_kinfo.cto enumerate the whole process table viakern.proc.all(KERN_PROC_ALL) instead of hardcoding PIDs (68/285/328/...) that do not survive avm.sh reset. The tally is now robust across boots: it counts the 4 pointer fields (kp_paddr,kp_fd,kl_wchan,kp_ktaddr) across every readable process, plus keeps the detailed self + pid-1 dump and a 3Γ stability check on pid 1'skp_paddr. On the unpatched kernel the tally is large and deterministic; on the patched kernel it drops to exactly 0. run.log/leak_sample.txt/env.txtrefreshed with the boot-2 (#0/#1) addresses, before/after contrast, and the patched-kernel sha256.fix.diffunchanged from prior session β re-verified it applies cleanly (4/4 hunks) and fully closes the leak. Theindex 0000000..1111111line is a placeholder (fine forpatch -p1).
Recommended fix
Matches the finding markdown's proposal (with the libkvm caveat). The
authoritative fix.diff in this folder zeroes kp_paddr/kp_fd/
kl_wchan/kp_ktaddr/kp_lwp.kl_wchan in kern_kinfo.c before copyout. For
libkvm (/dev/mem//dev/kmem) consumers that need the real addresses (and
already require root), the addresses should additionally be gated behind a
root-only / /dev/mem-mediated path so unprivileged sysctl readers get zeroes
β but the simple zeroing alone closes the unprivileged leak and is what was
validated here.
Fix verification
fixedVALIDATED: the unpatched #0 baseline (6.5-DEVELOPMENT #0, Thu Jul 2 06:02:54 UTC 2026) leaked 69 kernel pointers across 23 procs (RUN_EXIT=0, 3x deterministic); the single-fix #1 kernel (same source + only fix.diff, built NK_DONE rc=0) leaked 0 pointers across 23 procs (RUN_EXIT=2, 3x deterministic) => fix closes the leak completely with no functional regression (sysctl access path unchanged, only kernel addresses redacted). fix_status=fixed.
baseline #0: 'result: 69 kernel pointers leaked across 23 processes (of 23 total readable)' / 'LEAK CONFIRMED' RUN_EXIT=0. patched #1: 'kp_paddr=0x0 kp_fd=0x0 kl_wchan=0x0 kp_ktaddr=0x0' / 'result: 0 kernel pointers leaked across 0 processes (of 23 total readable)' / 'no kernel pointers observed (fields appear redacted)' RUN_EXIT=2. build: '=== NK_DONE rc=0 ===' (no errors). patched kern.version '#1: Thu Jul 2 19:25:22 UTC 2026'.
Confirmed kernel references
Detail
Exploit chain
Info-disclosure (KASLR-defeat) class, not memory corruption: there is no corruption primitive to chain. The disclosed values are live slab addresses of every process's struct proc (kp_paddr) and struct filedesc (kp_fd) plus wait-channel / kernel-thread addresses (sometimes a .text symbol e.g. cron kl_wchan=0xffffffff8130f670 -> nm 'nanowait' in the prior session). This is a reliable KASLR-bypass + slab-layout primitive that escalates heap/struct-proc corruption bugs (e.g. DF-0013) from DoS to reliable LPE, but standalone it is address disclosure only.
Evidence (decisive lines)
baseline #0: 'pid 1 kp_paddr = 0xfffff80089977280 (struct proc slab); kp_fd = 0xfffff8008d2254c0; kl_wchan = 0xfffff80089977280' / 'result: 69 kernel pointers leaked across 23 processes' / 'LEAK CONFIRMED' RUN_EXIT=0 (3x identical). patched #1: 'pid 1 kp_paddr/kp_fd/kl_wchan/kp_ktaddr = 0x0 (REDACTED)' / 'result: 0 kernel pointers leaked across 0 processes (of 23 total readable)' / 'no kernel pointers observed (fields appear redacted)' RUN_EXIT=2 (3x identical). Full untrimmed logs: run.log / fix_run.log / fix_build.log.
PoC changes
Rewrote leak_kinfo.c to enumerate the whole process table via kern.proc.all (KERN_PROC_ALL) instead of hardcoding PIDs (68/285/328/...) which do not survive a vm.sh reset; the tally now counts the 4 pointer fields (kp_paddr/kp_fd/kl_wchan/kp_ktaddr) across EVERY readable process plus keeps the detailed self+pid1 dump and a 3x stability check on pid 1's kp_paddr. Result is robust across boots: large deterministic tally on the unpatched kernel, exactly 0 on the patched kernel. Refreshed run.log/leak_sample.txt/env.txt/README.md with the #0/#1 addresses and before/after contrast; fix.diff unchanged from prior session (re-verified applies 4/4 clean and fully closes the leak). Full evidence pack under findings/poc/DF-0016/.
Verified recommended fix
In sys/kern/kern_kinfo.c zero the five kernel-pointer fields before copyout: kp_paddr (:128), kp_fd (:129), kl_wchan (:272 and kthread variant :321), kp_ktaddr (:301) β these are all the (uintptr_t) casts into the copied-out struct (grep-verified). Validated on a built+booted single-fix #1 kernel: leak dropped 69->0 with no functional regression (23 procs still readable). Matches the finding markdown's proposal; for libkvm//dev/mem consumers that need real addresses (already root-only) gate them behind a root-only path so unprivileged sysctl readers still get zeros. Full git-apply-able diff in findings/poc/DF-0016/fix.diff.
Verdict
REPRODUCED + FIX VALIDATED. The world-readable kern.proc.* sysctl copies out struct kinfo_proc with unredacted kernel virtual addresses set by the five (uintptr_t) casts in kern_kinfo.c: kp_paddr=(uintptr_t)p (struct proc slab, :128), kp_fd=(uintptr_t)p->p_fd (filedesc slab, :129), kl_wchan=td_wchan (:272 / kthread variant :321), kp_ktaddr=(uintptr_t)td (:301); these are the ONLY kernel-pointer assignments into the copied-out struct (grep uintptr_t kern_kinfo.c == those 5 lines), copied out unredacted via sysctl_out_proc (kern_proc.c:1603/1612/1633) with only PRISON_CHECK and no p_trespass gate (:1690). On the unpatched #0 master DEV kernel (6.5-DEVELOPMENT #0, Thu Jul 2 06:02:54 UTC 2026), as unprivileged maxx (uid 1001, not in wheel) the PoC leaked 69 kernel pointers across all 23 readable processes, deterministically over 3 runs (init kp_paddr=0xfffff80089977280 byte-identical 3x; self kp_paddr varies run-to-run -> both confirm live slab allocations). The standalone fix.diff zeroes all 5 sites in kern_kinfo.c; applied cleanly (4/4 hunks) to /usr/src, built as a single-fix kernel #1 (NK_DONE rc=0), installed kernel.stripped->/boot/kernel/kernel and booted. Re-running the SAME PoC on #1 yields 0 leaked pointers (all fields 0) over 3 deterministic runs while 23 procs remain readable -> the leak is closed with NO functional regression (only kernel addresses redacted). fix_status=fixed.
No comments yet.