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.
Recommended fix
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
sys/vm/vm_object.hβstruct vm_object(kmalloc-backed).sys/kern/subr_sbuf.cβsbuf_printfsemantics.sys/vfs/procfs/procfs_map.c:245βuiomove_frombufcopies the sbuf to userland.
Timeline
- 2026-07-05 Discovered during automated audit.
- pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0922 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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'skprintfhas no equivalent gate, so the right fix is to stop emitting the pointer (or print0x0).
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
fixedVALIDATED 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)
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/
No comments yet.