โฌข DragonFlyBSD Kernel Audit
โ† 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.

PoC verification

Evidence pack

findings/poc/DF-0016 ยท 11 files
FileTypeDescriptionSize
leak_kinfo.c trigger-source reads kern.proc.pid.<pid>, dumps kp_paddr/kp_fd/kl_wchan/kp_ktaddr + stability check 4.7 KB view raw
build.sh build-script cc -o leak_kinfo leak_kinfo.c 175 B view raw
run.sh run-script runs leak_kinfo as unprivileged user 304 B view raw
build.log build-log final successful build, full output 68 B view raw
run.log run-log decisive run: 24 kernel pointers leaked across 8 procs 2.4 KB view raw
run.2.log run-log 2nd run (variance): root-daemon addrs identical, self addr differs 2.4 KB view raw
leak_sample.txt leak-sample leaked slab/.text pointers + nm cross-ref (cron wchan -> nanowait) 3.7 KB view raw
VERDICT.md verdict full mechanism, reachability, proof, impact 5.1 KB โ†“ raw
README.md readme build/run/expected for humans 1.9 KB โ†“ raw
fix.diff suggested-fix git-apply-able: zero kp_paddr/kp_fd/kl_wchan/kp_ktaddr in kern_kinfo.c (libkvm caveat noted) 1.3 KB view raw
env.txt environment guest uname, cc version, sysctls 385 B view raw
README.md readme build/run/expected for humans
โ†“ 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() (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;          /* struct proc slab address    */
kp->kp_fd    = (uintptr_t)p->p_fd;    /* struct filedesc slab address*/
kl->kl_wchan = (uintptr_t)td_wchan;   /* wait-channel address        */
kp->kp_ktaddr= (uintptr_t)td;         /* kernel thread address       */

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)

pid 1      uid=0    comm=init
    kp_paddr   = 0xfffff80066807880   (struct proc slab)
    kp_fd      = 0xfffff80066840ec0   (filedesc slab)
    kl_wchan   = 0xfffff80066807880   (wait channel)
...
  pid 1 kp_paddr: 0xfffff80066807880 / 0xfffff80066807880 / 0xfffff80066807880  (STABLE)
result: 24 kernel pointers leaked across 8 processes
result: LEAK CONFIRMED (KASLR-defeat / slab-address primitive)

kp_paddr is the live slab address of the target's struct proc. Cross-ref: cron's kl_wchan = 0xffffffff8130f670 exactly matches the nm symbol nanowait (kernel .text) โ€” see leak_sample.txt. On a fixed kernel the fields are 0 and the program exits 2.

VERDICT.md verdict full mechanism, reachability, proof, impact
โ†“ download raw

DF-0016 โ€” kern.proc.* (kinfo_proc) exports unredacted kernel pointers (KASLR defeat)

Verdict

REPRODUCED โ€” the world-readable kern.proc.pid.<pid> 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 DragonFly master DEV v6.5.0.1712.g89e6a-DEVELOPMENT: as unprivileged maxx I read these for every root daemon; 24 kernel pointers leaked across 8 processes, including a .text-range wait-channel value that exactly matches a nm symbol.

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 fields live in user-visible struct kinfo_proc (sys/sys/kinfo.h:174 kp_paddr, :183 kp_fd, :237 kp_ktaddr). 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 (:2197-2201) are CTLFLAG_RD and sysctl reads are not framework-gated (kern_sysctl.c:1446-1450 gates writes only).

Proof

./leak_kinfo as maxx (uid 1001) reads kern.proc.pid.<pid> for self + root daemons and prints the pointer fields (excerpt; full in leak_sample.txt):

pid 1      uid=0    comm=init
    kp_paddr   = 0xfffff80066807880   (struct proc slab)
    kp_fd      = 0xfffff80066840ec0   (filedesc slab)
    kl_wchan   = 0xfffff80066807880   (wait channel)
pid 730    uid=0    comm=cron
    kp_paddr   = 0xfffff800ab144680 ; kp_fd = 0xfffff800ab16a540
    kl_wchan   = 0xffffffff8130f670   (kernel .text/.data!)
...
pid 1 kp_paddr: 0xfffff80066807880 / 0xfffff80066807880 / 0xfffff80066807880  (STABLE)
result: 24 kernel pointers leaked across 8 processes
result: LEAK CONFIRMED (KASLR-defeat / slab-address primitive)

Cross-referencing the leaked .text-range value against nm /boot/kernel/kernel: cron kl_wchan 0xffffffff8130f670 โ†’ nearest symbol nanowait.<clone>+0x0 (EXACT) โ€” a kernel .text/.data address, directly revealing the kernel base. The 0xfffff8xxxxxxxxxx values are DragonFly kernel KVA/malloc (slab) addresses; they are confirmed real (not garbage) because they are canonical upper-half kernel addresses, stable across repeated reads (pid 1 kp_paddr identical 3ร—), distinct per pid, and clustered in slab-sized groups (init/dhclient in 0xfffff8006680xxxx; hammer2/devd/syslogd/sshd/cron in 0xfffff800ab14xxxx). Root-daemon addresses are byte-identical between run.log and run.2.log (long-running procs don't relocate); the self (leak_kinfo) address differs run to run because each run is a new process โ€” itself confirming these are live slab allocations.

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.

PoC changes

The original leak_kinfo.c had two bugs: (1) missing #include <string.h> / <unistd.h> (compiler warnings), and (2) its looks_like_kaddr used >= 0xffffffff80000000 โ€” which is the kernel text range only and excludes the DragonFly direct-map/KVA heap region 0xfffff8xxxxxxxxxx where kp_paddr/kp_fd/kl_wchan live. The original therefore printed the leaked addresses but then reported "no kernel pointers observed" (RUN_EXIT=2) โ€” a false negative. The rewrite detects the full canonical kernel upper half (>= 0xffff800000000000), reads all four pointer fields, iterates self + several root daemons, adds a 3ร— stability check on pid 1's kp_paddr, and prints a clear LEAK CONFIRMED marker. Added build.sh/run.sh.

Matches the finding markdown's proposal (with the libkvm caveat). Authoritative fix.diff in this folder zeroes kp_paddr/kp_fd/kl_wchan/kp_ktaddr/ kp_lwp.kl_wchan in kern_kinfo.c before they reach userland. For libkvm (/dev/mem) consumers that need the real addresses, the addresses must instead be gated behind a root-only / /dev/mem-mediated path (those tools already require root), so unprivileged sysctl readers get zeroes.

Confirmed kernel references

Detail

Exploit chain

none (pure info-disclosure / KASLR-defeat + slab-layout primitive; no memory corruption to chain). It is the enabler that escalates the practical severity of a local heap/struct-proc corruption bug (notably DF-0013 kern.proc.args overflow) from DoS to reliable local privilege escalation by defeating KASLR and revealing the slab layout for grooming. No further primitive derivable from this read alone.

Evidence (decisive lines)

pid 1 uid=0 comm=init
    kp_paddr = 0xfffff80066807880 (struct proc slab)
    kp_fd    = 0xfffff80066840ec0 (filedesc slab)
    kl_wchan = 0xfffff80066807880 (wait channel)
pid 730 uid=0 comm=cron
    kl_wchan = 0xffffffff8130f670 (kernel .text/.data -> nm: nanowait EXACT)
=== stability check ===
pid 1 kp_paddr: 0xfffff80066807880 / ...880 / ...880  (STABLE = real struct proc addr)
result: 24 kernel pointers leaked across 8 processes
result: LEAK CONFIRMED (KASLR-defeat / slab-address primitive)
RUN_EXIT=0  (read as uid=1001 maxx)

PoC changes

Rewrote leak_kinfo.c: the original was a FALSE NEGATIVE -- it printed the leaked addresses but its looks_like_kaddr used >=0xffffffff80000000 (kernel TEXT range only), which EXCLUDES DragonFly's 0xfffff8xxxxxxxxxx direct-map/KVA heap region where kp_paddr/kp_fd/kl_wchan live, so it reported 'no kernel pointers observed' (RUN_EXIT=2) despite the leak. Also added missing string.h/unistd.h. New version detects the full canonical kernel upper half (>=0xffff800000000000), reads all four pointer fields, iterates self + root daemons, adds a 3x stability check on pid 1 kp_paddr, and prints a clear LEAK CONFIRMED marker. Added build.sh/run.sh. Full packs in findings/poc/DF-0016/ (VERDICT.md, leak_sample.txt w/ nm cross-ref, run.log+run.2.log, fix.diff).

Verified recommended fix

fix.diff (git apply --check OK) zeroes the leaking fields in sys/kern/kern_kinfo.c: kp_paddr (:128), kp_fd (:129), kl_wchan (:272), kp_ktaddr (:301), kp_lwp.kl_wchan (:321) before they reach userland. Matches finding markdown proposal. Caveat: libkvm consumers that walk allproc over /dev/kmem using kp_paddr need a migration path -- gate the real addresses behind a root-only / /dev/mem-mediated variant so unprivileged sysctl readers get zeroes while /dev/kmem tools (root-only) keep working; a more surgical alternative is to redact in the copyout path (kern_proc.c sysctl_out_proc) rather than in fill_*, preserving in-kernel/kvm use.

Verdict

REPRODUCED. fill_kinfo_proc (sys/kern/kern_kinfo.c:128-129) sets kp->kp_paddr=(uintptr_t)p and kp->kp_fd=(uintptr_t)p->p_fd; fill_kinfo_lwp (:272) sets kl->kl_wchan=(uintptr_t)td_wchan; fill_kinfo_proc_kthread (:301,:321) sets kp_ktaddr and kp_lwp.kl_wchan -- all raw kernel virtual addresses copied out unredacted by sysctl_out_proc (kern_proc.c:1603/1612/1633). Reachable as any user: sysctl_kern_proc for KERN_PROC_PID (kern_proc.c:1686-1694) checks only PRISON_CHECK (:1690), not p_trespass; the kern.proc.* nodes are CTLFLAG_RD and reads are not framework-gated (kern_sysctl.c:1446-1450). As unprivileged maxx I read kern.proc.pid. for every root daemon: 24 kernel pointers leaked across 8 processes. kp_paddr (struct proc slab) and kp_fd (filedesc slab) are in the 0xfffff8xxxxxxxxxx kernel KVA region; cron's kl_wchan=0xffffffff8130f670 is in kernel .text/.data and exactly matches nm symbol nanowait. -> direct KASLR base leak. Confirmed real (not garbage): canonical upper-half addresses, STABLE across 3 reads (pid 1 kp_paddr identical), distinct per-pid and slab-clustered. No master code change closes this.