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

Kernel heap pointer leaked to userspace via %p of vm_object in /proc/<pid>/map

Field Value
ID DF-0922
Status new
Severity Medium
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/vfs/procfs/procfs_map.c
Lines 208-216
Area vfs
Confidence certain
Discovered 2026-07-05
Reported pending
Known CVE none
CVE match dfly_specific

Summary

The format string passed to sbuf_printf() at procfs_map.c:208-214 contains a %p conversion that prints the address of the per-mapping vm_object (ba->object) into the /proc/<pid>/map output. Every emitted line therefore hands a kernel heap pointer to the reader. This is a direct KASLR / kernel-heap-layout bypass and is readable by any local user (and, per DF-0921, of any other process).

Root cause

At sys/vfs/procfs/procfs_map.c:208-216:

error = sbuf_printf(sb,
#if LONG_BIT == 64
      "0x%016lx 0x%016lx %d %d %p %s%s%s %d %d "
#else
      "0x%08lx 0x%08lx %d %d %p %s%s%s %d %d "
#endif
      "0x%04x %s%s %s %s\n",
        (u_long)e_start, (u_long)e_end,
        resident, -1, (ba ? ba->object : NULL),   /* <-- %p LEAK */
        ...);

The 5th conversion %p is satisfied by (ba ? ba->object : NULL) at :216, which is a struct vm_object * β€” a kernel heap address. The pointer is emitted verbatim to the sbuf and copied out by uiomove_frombuf() at procfs_map.c:245. There is no privilege gate and no %pK-style restriction. (vm_object structures live in the kernel heap; revealing their addresses defeats kernel heap ASLR and lets an attacker infer slab layout for heap-grooming against separate kernel bugs.)

Threat model & preconditions

  • Attacker position: Local, unprivileged, any user who can open /proc/<pid>/map (per DF-0921, on default config that is any local user for any process in the same jail).
  • Privileges gained or impact: KASLR / kernel-heap-layout bypass that materially aids exploitation of any other kernel memory-corruption bug.
  • Required config or capabilities: procfs mounted (common).
  • Reachability: cat /proc/self/map.

Proof of concept

PoC source: findings/poc/DF-0922/leak_kptr.sh

Build & run

sh leak_kptr.sh        # non-root

Expected output

0x800600000 0x800601000 -1 -1 0xffff80002a3b4c00 r-x COW NC default /lib/libc.so
0x800630000 0x800631000 -1 -1 0xffff80002a3b5800 r-- NCOW NC vnode /etc/...
...

The 5th whitespace-separated column is the printed vm_object pointer (e.g. 0xffff8000...). Repeating across mappings yields a set of live kernel heap addresses. Success = non-zero kernel-virtual addresses appear in column 5 of the user-visible output.

Impact

KASLR / kernel-heap-layout bypass that materially aids exploitation of any other kernel memory-corruption bug. No direct privilege gain.

Stop emitting the pointer. Preserve the column count for existing consumers (procstat/gdb/lsof) by printing a constant token instead:

--- a/sys/vfs/procfs/procfs_map.c
+++ b/sys/vfs/procfs/procfs_map.c
@@ -207,13 +207,13 @@
     */
    error = sbuf_printf(sb,
 #if LONG_BIT == 64
-         "0x%016lx 0x%016lx %d %d %p %s%s%s %d %d "
+         "0x%016lx 0x%016lx %d %d 0x0 %s%s%s %d %d "
 #else
-         "0x%08lx 0x%08lx %d %d %p %s%s%s %d %d "
+         "0x%08lx 0x%08lx %d %d 0x0 %s%s%s %d %d "
 #endif
          "0x%04x %s%s %s %s\n",
            (u_long)e_start, (u_long)e_end,
-           resident, -1, (ba ? ba->object : NULL),
+           resident, -1,
            (e_prot & VM_PROT_READ) ? "r" : "-",

If a debug consumer genuinely needs the pointer, gate it behind priv_check_cred(curp->p_ucred, ...) and print 0x0 otherwise β€” but the default must not leak.

References

Timeline

  • 2026-07-05 Discovered during automated audit.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0922 Β· 14 files
FileTypeDescriptionSize
leak_kptr.sh trigger-source extracts column 5 of /proc/<pid>/map (the %p of vm_object) 475 B view raw
run.sh run-script runnable wrapper: build + run + classify leak count 791 B view raw
build.sh build-script no-op (pure shell PoC, nothing to compile) 194 B view raw
fix.diff suggested-fix git-apply-able: replace %p with literal 0x0, drop ba->object arg 678 B view raw
run.log run-log baseline #0 reproduction, full output with leaked pointers 1.2 KB view raw
fix_run.log run-log patched #1 re-run, full output showing 0x0 (no leak) 986 B view raw
fix_build.log build-log single-fix kernel build (nativekernel, full output) 5.6 MB ↓ download
leak_sample.txt leak-sample 3-run variance sample showing pointers drift (live heap) 941 B view raw
env.txt environment uname, procfs mount, sysctl context 383 B view raw
VERDICT.md verdict full narrative: mechanism, repro, fix, validation 4.9 KB ↓ raw
README.md readme original PoC readme 1.1 KB ↓ raw
manifest.json manifest this catalog 2.9 KB 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 original PoC readme
↓ download raw

DF-0922 β€” PoC: kernel heap pointer leak via %p in /proc/<pid>/map

Goal

Demonstrate that the format string at procfs_map.c:208-216 leaks the vm_object kernel pointer via %p to userland β€” column 5 of the /proc/<pid>/map output.

Build & run

sh leak_kptr.sh                  # default: /proc/self/map
sh leak_kptr.sh /proc/<pid>/map  # any pid (combined with DF-0921)

Expected output

[*] reading /proc/self/map as uid=65534

0xffff80002a3b4c00
0xffff80002a3c0000
0xffff80002a3c5400
0xffff80002a3d1000
...

A set of distinct kernel-virtual addresses (0xffff...) appears. These are struct vm_object * addresses β€” direct kernel heap pointers, usable for KASLR defeat and slab-layout inference in subsequent kernel exploitation.

Notes

  • Even if DF-0921 is fixed (gated to self / privileged access), the leak persists for any process reading its own map.
  • %pK (Linux) was the historical fix for this class of leak; DragonFly's kprintf has no equivalent gate, so the right fix is to stop emitting the pointer (or print 0x0).
VERDICT.md verdict full narrative: mechanism, repro, fix, validation
↓ download raw

DF-0922 β€” VERDICT

Verdict: REPRODUCED β†’ FIXED

DF-0922 is a real kernel heap-pointer info leak via %p of the vm_object pointer in /proc/<pid>/map output. The leak is reachable by any local user reading their own map (no privilege needed). The supplied fix.diff removes the %p conversion and was validated on a built-and-booted single-fix kernel: 0 kernel pointers leaked after the fix, vs 12–16 on baseline.

Mechanism (confirmed line-by-line)

At sys/vfs/procfs/procfs_map.c:208-223 the per-map-entry format string passed to sbuf_printf() contains a %p conversion (line 210 for 64-bit, line 212 for 32-bit):

error = sbuf_printf(sb,
#if LONG_BIT == 64
      "0x%016lx 0x%016lx %d %d %p %s%s%s %d %d "        /* line 210 */
#else
      "0x%08lx 0x%08lx %d %d %p %s%s%s %d %d "          /* line 212 */
#endif
      "0x%04x %s%s %s %s\n",
      (u_long)e_start, (u_long)e_end,
      resident, -1, (ba ? ba->object : NULL),            /* line 216: %p arg */
      ...);

The 5th conversion %p is satisfied by (ba ? ba->object : NULL) at procfs_map.c:216 β€” a struct vm_object *, i.e. a kernel heap address. That pointer is emitted verbatim into the sbuf and copied back to userland by uiomove_frombuf() at procfs_map.c:245. There is no %pK-style privilege gate; the entire procfs_domap path has no privilege check on the content of the output (the cross-user access check is a separate bug, DF-0921).

Reproduction (unpatched #0 baseline)

$ uname -a
DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026 ... x86_64
$ id
uid=1001(maxx) gid=1001(maxx) groups=1001(maxx)
$ head -2 /proc/self/map
0x0000000000400000 0x0000000000403000 -1 -1 0xfffff80116837240 r-x 2 0 0x0000 COW NC vnode /bin/cat
0x0000000000602000 0x0000000000603000 -1 -1 0xfffff80116837240 rw- 2 0 0 0x0000 COW NNC vnode /bin/cat
                                          ^^^^^^^^^^^^^^^^^^^^
                                          leaked vm_object kernel pointer

Three independent runs each emit 12 distinct 0xffff... addresses that vary between runs (live heap, not cosmetic) β€” see leak_sample.txt:

run1: 0xfffff80116821940, 0xfffff801170b1d40, 0xfffff801170b3f00, ...
run2: 0xfffff80116821940, 0xfffff801170b0e40, 0xfffff801170b2380, ...
run3: 0xfffff80116821940, 0xfffff801168252c0, 0xfffff80116825900, ...

(The 4 stable addresses are shared kernel objects like libc/ld-elf.so vnodes; the rest drift run-to-run as fresh vm_objects are allocated.)

Impact

Info leak β€” KASLR / kernel-heap-layout defeat. No direct privilege gain, no memory corruption. The leaked set of vm_object addresses lets an attacker: - defeat KASLR (kernel heap is at fixed offsets on this guest, but on a KASLR-hardened kernel this would defeat randomization); - infer slab-zone layout for heap-grooming in service of a separate memory-corruption primitive.

Not exploitable on its own β€” no write primitive, no control flow. This is the "classic %p leak" pattern (Linux fixed it with %pK).

Fix

fix.diff replaces the %p conversion with the literal token 0x0 in both the 64-bit and 32-bit format strings, and drops the now-unused (ba ? ba->object : NULL) argument from the variadic list. This: - preserves the column count (existing procstat/gdb/lsof consumers keep parsing the same field positions); - emits a constant 0x0 instead of the kernel pointer; - removes one variadic argument so the format/argument count still matches.

A debug consumer that genuinely needs the pointer should be gated behind priv_check_cred() and print 0x0 otherwise β€” out of scope for this minimal fix. This fix matches the finding markdown's ## Recommended fix proposal.

Fix validation (Phase 8)

Built a single-fix kernel with make -j6 nativekernel KERNCONF=X86_64_GENERIC (warm obj, applied only this fix.diff), installed /boot/kernel/kernel + /boot/kernel/kernel.debug, rebooted into #1, re-ran the SAME PoC:

kernel kern.version column-5 sample 0xffff count
baseline (unpatched) #0 Thu Jul 2 0xfffff80116837240 12–16
patched (this fix) #1 Sun Jul 12 0x0 0

Deterministic across 3 runs on the patched kernel. The leak is gone and no new panic or regression was introduced. fix_status: fixed.

Files

file desc
leak_kptr.sh trigger: extracts column 5 of /proc/<pid>/map
run.sh runnable wrapper that builds + runs + classifies
build.sh no-op (pure shell PoC)
fix.diff git-apply-able fix (replaces %p with 0x0)
run.log baseline #0 reproduction, full output
fix_run.log patched #1 re-run, full output
fix_build.log single-fix kernel build log
leak_sample.txt 3-run variance sample
env.txt guest environment
manifest.json artifact catalog

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix: leak_kptr.sh on the unpatched #0 baseline leaks 12-16 distinct 0xfffff8... kernel heap pointers in column 5 of /proc/self/map (e.g. 0xfffff80116837240); on the single-fix #1 kernel (this fix.diff applied, rebuilt with make -j6 nativekernel, installed to /boot/kernel/kernel, rebooted) the SAME PoC emits literal 0x0 in column 5 and zero 0xffff pointers across 3 deterministic runs => fix closes the leak with no regression.

baseline #0: 0x...400000 0x...403000 -1 -1 0xfffff80116837240 r-x ... /bin/cat  (12-16 kptr/run, grep -c ^0xffff = 16)
patched #1: 0x...400000 0x...403000 -1 -1 0x0 r-x ... /bin/cat  (grep -c ^0xffff = 0, x3 runs deterministic)
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 06:03:10 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (sha256 a1c9aeebfac8701f4589ef84f32bd1a7010773966ae4b9e9943d99566f909ff4)

Confirmed kernel references

Detail

Exploit chain

none (pure info leak; no write primitive, no memory corruption). Impact ceiling: KASLR / kernel-heap-layout defeat that materially aids exploitation of any separate kernel memory-corruption bug by revealing vm_object slab addresses. On this guest KASLR is already off so the leak is informational; on a KASLR-hardened kernel it would defeat heap randomization and enable slab-layout inference for grooming. No escalation chain exists for this class.

Evidence (decisive lines)

baseline #0 /proc/self/map (maxx, uid 1001):
0x0000000000400000 0x0000000000403000 -1 -1 0xfffff80116837240 r-x 2 0 0x0000 COW NC vnode /bin/cat
0x0000000800602000 0x0000000800643000 -1 -1 0xfffff80116821940 r-x 32 0 0x0000 COW NC vnode /libexec/ld-elf.so.2
[*] 12-16 distinct 0xffff... pointers per run, varying across runs (run1: 0xfffff801170b1d40; run2: 0xfffff801170b0e40; run3: 0xfffff801168252c0)

PoC changes

Added run.sh (runnable wrapper that counts 0xffff pointers and classifies leak/no-leak), build.sh (no-op for the pure-shell PoC), VERDICT.md, manifest.json, fix.diff, env.txt, leak_sample.txt (3-run variance), run.log (baseline repro), fix_run.log (patched re-run), fix_build.log (single-fix kernel build). Original leak_kptr.sh and README.md left unchanged.

Verified recommended fix

Replace the %p conversion with the literal token 0x0 in both the 64-bit (procfs_map.c:210) and 32-bit (procfs_map.c:212) format strings, and drop the now-unused (ba ? ba->object : NULL) argument at procfs_map.c:216. This preserves the column count for procstat/gdb/lsof consumers while emitting a constant 0x0 instead of the kernel pointer. Matches the finding markdown's ## Recommended fix proposal. Full git-apply-able diff in findings/poc/DF-0922/fix.diff (applies cleanly, validated on a built-and-booted kernel).

Verdict

REPRODUCED. The bug is real and exactly as claimed: procfs_map.c:210/212 has a %p conversion satisfied by (ba ? ba->object : NULL) at procfs_map.c:216, a struct vm_object * kernel heap address, emitted verbatim into the /proc//map sbuf and copied to userland by uiomove_frombuf at procfs_map.c:245. Confirmed as unprivileged user maxx (uid 1001) reading /proc/self/map: column 5 contains 12-16 distinct 0xfffff8... kernel heap pointers per run, and the addresses drift across runs (leak_sample.txt) proving they are live heap, not cosmetic. No privilege check gates the output content (the cross-user access check is the separate DF-0921).