Missing privilege check in /proc/<pid>/map exposes VM layout and mapped file paths of any process
| Field | Value |
|---|---|
| ID | DF-0921 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| CWE | CWE-862 Missing Authorization |
| File | sys/vfs/procfs/procfs_map.c |
| Lines | 56-79 |
| Area | vfs |
| Confidence | certain |
| Discovered | 2026-07-05 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
procfs_domap() performs no privilege/visibility check (no p_trespass, no
CHECKIO, no p_candebug equivalent) before dumping the entire VM map of
the target process. The only gate is PRISON_CHECK (jail boundary) applied
at lookup, and ps_showallprocs=1 by default (sys/kern/kern_proc.c:91)
lets any local user walk /proc/<any_pid>/ and open /map. Unlike the
Pmem node, Pmap is not subject to the CHECKIO/p_trespass test
that procfs_open() applies at sys/vfs/procfs/procfs_vnops.c:199 (only
the case Pmem: arm at :185-207 runs the check; the default: arm at
:209-211, which Pmap falls through into, does nothing).
Root cause
procfs_domap() is entered from procfs_rw() (sys/vfs/procfs/procfs_subr.c:346,
dispatched at :410-412) after pfs_pfind() only holds the proc and checks
P_POSTEXIT. procfs_domap() itself (sys/vfs/procfs/procfs_map.c:56-79)
goes straight to building the sbuf with no comparison of
curp->p_ucred against lp->lwp_proc->p_ucred:
int
procfs_domap(struct proc *curp, struct lwp *lp, struct pfsnode *pfs,
struct uio *uio)
{
struct proc *p = lp->lwp_proc;
ssize_t buflen = uio->uio_offset + uio->uio_resid;
...
if (uio->uio_offset < 0 || uio->uio_resid < 0 || buflen >= INT_MAX)
return EINVAL;
sb = sbuf_new(sb, NULL, buflen+1, 0);
...
}
Compare procfs_open() at sys/vfs/procfs/procfs_vnops.c:184-202 which does
enforce CHECKIO+p_trespass β but only inside the case Pmem: arm; the
default: arm (which Pmap falls through to at :209-210) does nothing.
The directory lookup gate at procfs_vnops.c:807-810 only restricts
visibility when ps_showallprocs==0 AND caller is non-root AND uids differ;
with the default ps_showallprocs=1 even that gate is inert.
Net effect: an unprivileged user can read the start/end addresses of every
mapping, the protection bits, and the resolved file path (vn_fullpath,
procfs_map.c:181) of an arbitrary process β including uid-0 daemons β
defeating that process's user-space ASLR (randomize_mmap,
sys/vm/vm_map.c:4386) and disclosing which files it has mapped.
Threat model & preconditions
- Attacker position: Local, unprivileged. Single-tenant or multi-user system, default config, procfs mounted (common on DragonFly).
- Privileges gained or impact: Full ASLR-layout disclosure of privileged processes (enabler for exploitation of any separate bug in a root daemon) plus disclosure of mapped file paths (information leak about a root process's loaded libraries / open image files).
- Required config or capabilities: None beyond "procfs is mounted".
Same-uid disclosure also applies when
ps_showallprocs=0(e.g. a multi-tenant web pool where many workers run as the same uid). - Reachability:
open("/proc/<victim_pid>/map", O_RDONLY); read();
Proof of concept
PoC source: findings/poc/DF-0921/leak_map.c
Build & run
cc -o leak_map leak_map.c ./leak_map /proc/$(pgrep -u 0 -o)/map # any root daemon pid
Expected output
0x800600000 0x800601000 resident -1 0x...0x... r-x COW NC default /bin/sh 0x800630000 0x800631000 ... ...
Full mapping table of the root process β start/end addresses, protection
bits, resolved paths of mapped files β is printed to the attacker's stdout
with no EPERM/ENOENT. The addresses are the victim's load addresses
(ASLR bypass) and the paths reveal the victim's mapped files.
Impact
- KASLR / process-ASLR defeat for privileged processes (enabler).
- Disclosure of mapped library paths of a root process (info leak).
- Read is by any local user against any pid in the same jail (or any pid at all if unjailed).
Recommended fix
Mirror the Pmem authorization into procfs_domap() (and ideally also
into the default:/Pmap arm of procfs_open). Apply at function entry,
before dropping p_token:
--- a/sys/vfs/procfs/procfs_map.c
+++ b/sys/vfs/procfs/procfs_map.c
@@ -68,6 +68,16 @@ procfs_domap(struct proc *curp, struct lwp *lp, struct pfsnode *pfs,
struct sbuf *sb = NULL;
unsigned int last_timestamp;
if (uio->uio_rw != UIO_READ)
return (EOPNOTSUPP);
+ /*
+ * The map file discloses the full VM address layout, mapped file
+ * paths, and kernel object pointers of the target. Gate cross-process
+ * access on the same policy used for /proc/pid/mem in procfs_open().
+ */
+ if (p != curp &&
+ (!CHECKIO(curp, p) || p_trespass(curp->p_ucred, p->p_ucred)))
+ return (EPERM);
+
error = 0;
Consider additionally folding the Pmap/Pstatus/Pcmdline/Prlimit
nodes into the same case that procfs_open() already protects for Pmem
at procfs_vnops.c:184-202, so the check is enforced at open() rather
than per-read.
References
sys/vfs/procfs/procfs_vnops.c:184-211βprocfs_openprivilege gate applied toPmemonly.sys/vfs/procfs/procfs_vnops.c:807-810β directory lookup visibility gate (inert whenps_showallprocs=1).sys/kern/kern_proc.c:91βps_showallprocsdefault.sys/kern/kern_prot.c:1023-1058βp_trespasssemantics.
Timeline
- 2026-07-05 Discovered during automated audit.
- pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0921 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| leak_map.c | trigger-source | minimal open+read PoC; drops to nobody if run as root | 1.0 KB | view raw |
| build.sh | build-script | cc -o leak_map leak_map.c | 161 B | view raw |
| run.sh | run-script | auto-picks a uid-0 daemon pid and dumps its /proc/<pid>/map | 655 B | view raw |
| README.md | readme | original PoC readme (build/run/expected) | 1.3 KB | β raw |
| VERDICT.md | verdict | full mechanism walkthrough + before/after evidence | 7.3 KB | β raw |
| fix.diff | suggested-fix | git-apply-able: add CHECKIO+p_trespass at procfs_domap entry | 696 B | view raw |
| run.log | run-log | baseline (#0): 3x leak of /proc/409/map + cross-target samples | 4.2 KB | view raw |
| fix_run.log | run-log | patched (#1): EPERM for syslogd/sshd/cron; own map still works | 966 B | view raw |
| fix_run2.log | run-log | patched (#1): exhaustive sweep β 7 uid-0 daemons all BLOCKED | 1.5 KB | view raw |
| fix_build.log | build-log | full make -j6 nativekernel output, rc=0 | 5.6 MB | β download |
| leak_sample.txt | leak-sample | raw leaked bytes from /proc/409/map (syslogd) on baseline | 2.7 KB | view raw |
| env.txt | environment | uname, cc version, security.ps_showallprocs=1, procfs mounted | 347 B | view raw |
| panic.txt | panic-signature | (none β info-leak finding; kernel did not crash) | 55 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-0921 β PoC: unprivileged read of /proc/<pid>/map
Goal
Demonstrate that an unprivileged local user can read the full VM map of an arbitrary process (including root daemons) β defeating ASLR of privileged processes and disclosing their mapped files β confirming DF-0921.
Build & run
cc -o leak_map leak_map.c
# Find a root daemon pid (sshd, syslogd, dhclient, etc.)
PID=$(ps -ax -o pid,uid,comm | awk '$2==0 && $3 ~ /ssh|syslog|cron/ {print $1; exit}')
./leak_map /proc/$PID/map
Expected output
[*] running as uid=65534 gid=65534 0x800600000 0x800601000 -1 -1 0xffff80002a3b4c00 r-x COW NC default /libexec/ld-elf.so.2 0x800630000 0x800631000 -1 -1 0xffff80002a3c0000 r-- NCOW NC vnode /lib/libc.so.8 ...
(The 5th column 0xffff... is DF-0922. The start/end addresses and the
mapped-file paths are the ASLR/layout leak that defines DF-0921.)
Notes
- If
procfsis not mounted:mount_procfs procfs /proc(root only). - On default DragonFly configs,
kern.ps_showallprocs=1(default) makes every pid visible in/proc/to every local user. - The dump contains load addresses and mapped library paths of a process
the reader does not own β the missing
p_trespass/CHECKIOcheck thatprocfs_open()applies forPmemis absent forPmap.
DF-0921 β Missing privilege check in /proc/<pid>/map (procfs Pmap)
| Field | Value |
|---|---|
| ID | DF-0921 |
| Verdict | REPRODUCED (info leak) |
| Impact | Process VM-layout + mapped-file-path disclosure to any local user |
| Confidence | certain |
| Kernel | DragonFly 6.5-DEVELOPMENT #0 (unpatched baseline) |
| Fix validated | YES on single-fix kernel #1 β EPERM returned |
Verdict
REPRODUCED β info leak. An unprivileged local user (maxx, uid 1001, not
in wheel) can read the complete VM map of any other user's process,
including uid-0 daemons (syslogd, sshd, cron, devd, dhclient), via
/proc/<pid>/map. The dump exposes:
- Start/end addresses of every VM mapping β defeats the victim's
user-space ASLR (
vm.randomize_mmap). - Protection bits (
r-x,rw-,r--). - Resolved file paths of every mapped vnode (
vn_fullpath) β discloses which executables, libraries and files the victim has mapped. - Kernel pointer in column 5 (
vm_objectaddress, e.g.0xfffff80116822c00) β that one is the adjacent DF-0922 leak, not this finding; it is mentioned only because it rides the same unchecked path.
The bug is not memory corruption; there is no escalation chain. Impact is honest info-leak: ASLR/layout defeat for privileged processes (enabler for a separate write primitive) and disclosure of mapped library/file paths.
Mechanism (every hop cited path:line)
- Reachability gate is inert under default config. The directory lookup
at
sys/vfs/procfs/procfs_vnops.c:809-811only restricts visibility whensecurity.ps_showallprocs==0and caller is non-root and uids differ. The guest shipssecurity.ps_showallprocs: 1(verified), so every pid is visible to every local user, and/proc/<pid>/walks succeed. - Open-time privilege gate covers
Pmemonly.procfs_open()atsys/vfs/procfs/procfs_vnops.c:184-211enforcesCHECKIO + p_trespassinside thecase Pmem:arm only (:199); thedefault:arm (:209-211, whichPmapfalls through into) is a no-op. procfs_domap()performs no privilege check at all. Atsys/vfs/procfs/procfs_map.c:56-79, the function jumps straight from theUIO_READ/bounds sanity checks intosbuf_new()and theRB_FOREACH(entry, vm_map_rb_tree, &map->rb_root)walk that emits every mapping's start/end/prot/path. There is noCHECKIO, nop_trespass, nopriv_check, no uid comparison anywhere on this path.- Sibling functions all check; this one is the outlier.
procfs_doregs(sys/vfs/procfs/procfs_regs.c:60),procfs_dofpregs(sys/vfs/procfs/procfs_fpregs.c:59),procfs_dodbregs(sys/vfs/procfs/procfs_dbregs.c:62),procfs_domem(sys/vfs/procfs/procfs_mem.c:190) andprocfs_doctl(sys/vfs/procfs/procfs_ctl.c:126) all begin withif (!CHECKIO(curp, p) || p_trespass(curp->p_ucred, p->p_ucred)) return EPERM;.procfs_domapis the onlydo*handler that omits it.
Evidence
Baseline (unpatched #0): maxx reads syslogd's full VM map
$ id uid=1001(maxx) gid=1001 groups=1001 $ ps -ax -o pid,uid,comm | awk '$2==0 && $3 ~ /syslog|sshd|cron|dhclient|devd/' 289 0 dhclient 329 0 devd 409 0 syslogd 721 0 sshd 741 0 cron $ ./leak_map /proc/409/map 0x0000000000400000 0x0000000000409000 -1 -1 0xfffff80116822c00 r-x 1 0 0x0000 COW NC vnode /usr/sbin/syslogd 0x0000000000609000 0x000000000060a000 -1 -1 0xfffff8011683bd40 rw- 1 0 0x0180 COW NC default - 0x000000000060a000 0x000000000060d000 -1 -1 0xfffff80116839180 rw- 1 0 0x0180 COW NNC default - 0x0000000800609000 0x000000080064a000 -1 -1 0xfffff80116821940 r-x 30 0 0x0000 COW NC vnode /libexec/ld-elf.so.2 ... 0x0000000800860000 0x0000000800871000 -1 -1 0xfffff80116824140 r-x 24 0 0x0000 COW NC vnode /lib/libutil.so.4 0x0000000800a73000 0x0000000800ba8000 -1 -1 0xfffff80116822200 r-x 30 0 0x0000 COW NC vnode /lib/libc.so.8 0x00007fffffdfe000 0x00007fffffdff000 -1 -1 0xfffff8011683a800 r-x 3 0 0x0180 COW NC default - <- stack
24 lines for syslogd, 19+ for sshd, cron, dhclient, devd β every uid-0 daemon
fully disclosed. Identical output across 3 runs (ASLR is OFF on this guest so
the leak is byte-stable; on a real system with vm.randomize_mmap=1 the same
read still returns the live runtime addresses).
After fix (single-fix kernel #1): same PoC, every uid-0 target β EPERM
$ uname -a | head -1
DragonFly ... 6.5-DEVELOPMENT #1: Sun Jul 12 05:20:21 UTC 2026 ...
$ for p in 285 299 329 409 703 723 842; do
./leak_map /proc/$p/map
done
read: Operation not permitted # dhclient (285)
read: Operation not permitted # dhclient (299)
read: Operation not permitted # devd (329)
read: Operation not permitted # syslogd(409)
read: Operation not permitted # sshd (703)
read: Operation not permitted # cron (723)
read: Operation not permitted # sshd-session (842)
# sanity: own uid's map still readable
$ ./leak_map /proc/$$/map | head -2
0x0000000000400000 0x0000000000472000 -1 -1 0xfffff801168216c0 r-x 1 0 0x0000 COW NC vnode /bin/sh
0x0000000000671000 0x0000000000674000 -1 -1 0xfffff8008fad47c0 rw- 1 0 0x0180 COW NC default -
The check is enforced symmetrically: cross-uid reads of the map return
EPERM, same-uid reads still succeed β the documented behavior of
CHECKIO + p_trespass.
Exploit chain
None β not a memory-corruption primitive. DF-0921 is a pure authorization bypass / info leak (CWE-862). The realistic impact ceiling is: full VM-layout disclosure of any local process (ASLR defeat for any separate write primitive the attacker has in a root daemon), plus disclosure of the mapped-file set of privileged processes. No write primitive is created by this bug alone.
Fix (validated)
Mirror the existing Pmem/Pregs/Pfpregs/Pdbregs/Pmem/Pctl
authorization into procfs_domap() at function entry. The fix.diff adds,
immediately after the existing UIO_READ check:
if (!CHECKIO(curp, p) || p_trespass(curp->p_ucred, p->p_ucred))
return (EPERM);
This is the same check procfs_doregs/procfs_dofpregs/procfs_dodbregs/
procfs_domem/procfs_doctl all use. Validated on a single-fix kernel:
baseline leaks the full map, patched kernel returns EPERM for every cross-uid
read while preserving same-uid access. Matches (and lands at the same site
as) the finding markdown's ## Recommended fix proposal.
PoC changes
No source changes were required to leak_map.c β it built and ran as written.
Added: build.sh, run.sh (runnable repro scripts), VERDICT.md, this file,
fix.diff, manifest.json, plus the full untrimmed logs (run.log,
fix_run.log, fix_run2.log, fix_build.log, leak_sample.txt, env.txt).
Reproduce
./build.sh ./run.sh # picks any uid-0 daemon pid automatically # or explicit: ./leak_map /proc/$(pgrep -u 0 syslogd)/map
- Unpatched (
#0): prints the victim's full VM map (start/end addresses, protection, mapped file paths, and a0xffff...kernel object pointer). - Patched (
#1):read: Operation not permitted.
Fix verification
fixedVALIDATED the fix: built make -j6 nativekernel KERNCONF=X86_64_GENERIC rc=0 on the with-src baseline after applying only fix.diff, installed kernel.stripped+kernel.debug, rebooted into #1 (kern.version Sun Jul 12 05:20:21 UTC 2026, sha256 f06a818dac76bf28c4d62fbe32da0c46a728c9ad4288769fdabc18fcbf0be644). On #1, the SAME ./leak_map PoC against 7 uid-0 daemons (dhclient x2, devd, syslogd, sshd, cron, sshd-session) returns 'read: Operation not permitted' for every one -- zero VM-map content leaked. Same-uid sanity (maxx reading /proc/$$/map) still works (19 lines including /bin/sh). Before/after contrast is clean and deterministic across the exhaustive sweep.
BEFORE (unpatched #0): ./leak_map /proc/409/map => 24 lines of syslogd VM map incl /usr/sbin/syslogd, /libexec/ld-elf.so.2, /lib/libc.so.8 + kernel pointer column. AFTER (patched #1): for p in 285 299 329 409 703 723 842; do ./leak_map /proc/$p/map; done => 'read: Operation not permitted' x7 (every uid-0 daemon BLOCKED, zero content leaked). SANITY (own map on #1): ./leak_map /proc/$$/map | head -2 => 0x0000000000400000 0x0000000000472000 ... r-x ... vnode /bin/sh (same-uid access preserved).
Confirmed kernel references
Detail
Exploit chain
none -- pure authorization bypass / info leak (CWE-862), not memory corruption. No write primitive is created by this bug alone. Realistic impact ceiling: complete VM-layout disclosure of any local process (defeats vm.randomize_mmap user-space ASLR for any separate write primitive the attacker may have in a root daemon) plus disclosure of the mapped-library/file set of privileged processes.
Evidence (decisive lines)
BASELINE (#0, maxx uid 1001 reading syslogd uid 0): $ ./leak_map /proc/409/map 0x0000000000400000 0x0000000000409000 -1 -1 0xfffff80116822c00 r-x 1 0 0x0000 COW NC vnode /usr/sbin/syslogd 0x0000000000609000 0x000000000060a000 -1 -1 0xfffff8011683bd40 rw- 1 0 0x0180 COW NC default - 0x0000000800609000 0x000000080064a000 -1 -1 0xfffff80116821940 r-x 30 0 0x0000 COW NC vnode /libexec/ld-elf.so.2 (24 lines total for syslogd; same for sshd/cron/dhclient/devd)
PoC changes
No source changes were required to leak_map.c -- it built and ran as written. Added to findings/poc/DF-0921/: build.sh + run.sh (runnable repro scripts), VERDICT.md (full mechanism + before/after evidence), fix.diff (git-apply-able CHECKIO+p_trespass guard at procfs_domap entry), manifest.json, and the full untrimmed logs (run.log, fix_run.log, fix_run2.log, fix_build.log, leak_sample.txt, env.txt, panic.txt).
Verified recommended fix
Add if (!CHECKIO(curp, p) || p_trespass(curp->p_ucred, p->p_ucred)) return (EPERM); at the entry of procfs_domap() in sys/vfs/procfs/procfs_map.c, immediately after the existing UIO_READ check (line 71) and before sbuf_new -- identical to the guard already used by procfs_doregs/fpregs/dbregs/mem/ctl. Matches (and lands at the same site as) the finding markdown's ## Recommended fix proposal. The full git-apply-able diff lives in findings/poc/DF-0921/fix.diff and was validated by building and booting a single-fix kernel.
Verdict
REPRODUCED -- info leak. procfs_domap() at sys/vfs/procfs/procfs_map.c:56-79 has NO privilege check (no CHECKIO, no p_trespass, no priv_check, no uid compare) before dumping the target's full VM map; the only open-time gate (procfs_open at sys/vfs/procfs/procfs_vnops.c:184-211) enforces CHECKIO+p_trespass inside the case Pmem: arm only, and Pmap falls through to the no-op default: arm. Verified as maxx (uid 1001, not in wheel) reading /proc/409/map (syslogd), /proc/721/map (sshd), /proc/741/map (cron) on the unpatched #0 kernel -- each returns the complete VM mapping table: start/end addresses (ASLR defeat), protection bits, and resolved file paths (vn_fullpath). procfs_doregs/fpregs/dbregs/mem/ctl all open with if (!CHECKIO(curp,p) || p_trespass(...)) return EPERM; -- procfs_domap is the lone outlier missing this check, confirming this is a real missing-authorization bug, not a false positive.
No comments yet.