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

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).

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

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-0921 Β· 15 files
FileTypeDescriptionSize
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
README.md readme original PoC readme (build/run/expected)
↓ download 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 procfs is 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/CHECKIO check that procfs_open() applies for Pmem is absent for Pmap.
VERDICT.md verdict full mechanism walkthrough + before/after evidence
↓ download raw

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:

  1. Start/end addresses of every VM mapping β†’ defeats the victim's user-space ASLR (vm.randomize_mmap).
  2. Protection bits (r-x, rw-, r--).
  3. Resolved file paths of every mapped vnode (vn_fullpath) β†’ discloses which executables, libraries and files the victim has mapped.
  4. Kernel pointer in column 5 (vm_object address, 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)

  1. Reachability gate is inert under default config. The directory lookup at sys/vfs/procfs/procfs_vnops.c:809-811 only restricts visibility when security.ps_showallprocs==0 and caller is non-root and uids differ. The guest ships security.ps_showallprocs: 1 (verified), so every pid is visible to every local user, and /proc/<pid>/ walks succeed.
  2. Open-time privilege gate covers Pmem only. procfs_open() at sys/vfs/procfs/procfs_vnops.c:184-211 enforces CHECKIO + p_trespass inside the case Pmem: arm only (:199); the default: arm (:209-211, which Pmap falls through into) is a no-op.
  3. procfs_domap() performs no privilege check at all. At sys/vfs/procfs/procfs_map.c:56-79, the function jumps straight from the UIO_READ/bounds sanity checks into sbuf_new() and the RB_FOREACH(entry, vm_map_rb_tree, &map->rb_root) walk that emits every mapping's start/end/prot/path. There is no CHECKIO, no p_trespass, no priv_check, no uid comparison anywhere on this path.
  4. 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) and procfs_doctl (sys/vfs/procfs/procfs_ctl.c:126) all begin with if (!CHECKIO(curp, p) || p_trespass(curp->p_ucred, p->p_ucred)) return EPERM;. procfs_domap is the only do* 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 a 0xffff... kernel object pointer).
  • Patched (#1): read: Operation not permitted.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 05:20:21 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.