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

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 proc and struct filedesc, plus wait-channel / kernel-thread addresses. A reliable KASLR-bypass / slab-layout primitive. Most significantly, it escalates the practical severity of DF-0013 (the kern.proc.args heap 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 other kern.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.

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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0016 Β· 15 files
FileTypeDescriptionSize
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
README.md readme build/run/expected + before/after output
↓ download 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.

VERDICT.md verdict REPRODUCED + fix validated narrative with path:line refs
↓ download raw

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)

  1. Rewrote leak_kinfo.c to enumerate the whole process table via kern.proc.all (KERN_PROC_ALL) instead of hardcoding PIDs (68/285/328/...) that do not survive a vm.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's kp_paddr. On the unpatched kernel the tally is large and deterministic; on the patched kernel it drops to exactly 0.
  2. run.log / leak_sample.txt / env.txt refreshed with the boot-2 (#0/ #1) addresses, before/after contrast, and the patched-kernel sha256.
  3. fix.diff unchanged from prior session β€” re-verified it applies cleanly (4/4 hunks) and fully closes the leak. The index 0000000..1111111 line is a placeholder (fine for patch -p1).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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'.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 19:25:22 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (sha256 0968423828d08aee0dad955f470218cda550801dcda17d620c048b9e566b46d4)

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.