# DF-0923 — VERDICT

## Verdict
**REPRODUCED — reliable kernel panic (DoS) from an unprivileged user.** The
use-after-free of `vm_map` (embedded in `vmspace`) cached in `procfs_domap()`
is real, deterministic, and triggered by an unprivileged local user
(`maxx`, uid 1001). **Fix validated** on a single-fix kernel (Phase 8 passed:
panic gone). Escalation to `uid=0` is **not** achievable: the primitive is a
read-dominant UAF of a dedicated-slab object (`vmspace_cache`); see
"Exploit chain / escalation" below.

## The bug (confirmed path:line)

`procfs_domap()` in `sys/vfs/procfs/procfs_map.c`:

| Line | Code | Problem |
|---|---|---|
| `65` | `vm_map_t map = &p->p_vmspace->vm_map;` | caches `map` **without** `vmspace_hold()`. Compare `procfs_rwmem()` at `procfs_mem.c:84-94` which does `vmspace_hold(vm)` before caching. |
| `86` | `vm_map_lock_read(map);` | only `lockmgr(&map->lock, LK_SHARED)` (`vm_map.h:484`) — does NOT touch `vm_holdcnt`. |
| `87` | `lwkt_reltoken(&p->p_token);` | the target's proc token is gone for the whole scan. |
| `142-143` | `last_timestamp = map->timestamp; vm_map_unlock(map);` | **per-iteration lock drop** — opens the race window. |
| `175-178, 216` | `ba->object->flags`, `ba->object->ref_count`, `ba->object` | stale `ba` dereferenced while unlocked. |
| `230` | `vm_map_lock_read(map);` | **re-locks the cached `map`** — UAF if the vmspace was freed. |
| `89 / RB_FOREACH` | `vm_map_rb_tree_RB_NEXT` advances `entry` | walks freed/invalid RB nodes — **this is where it faults**. |

A concurrent `execve()` on the target calls `vmspace_exec()`
(`sys/vm/vm_map.c:4298`) which does `vmspace_rel(oldvmspace)` at `:4330`.
With no hold/ref held by `procfs_domap`, the old vmspace (and its embedded
`vm_map`, including `map->lock` and the RB tree) is freed
(`objcache_put(vmspace_cache, vm)` at `vm_map.c:543`). The reader's
re-lock/iterate at `:230`/`:89` then operates on freed memory => UAF => panic.
`exit()` is also sufficient: `kern_exit.c:432-433` releases `p_token` around
`vmspace_relexit(vm)`, so the vmspace can be freed even while a reader holds
`p_token`.

## Reproduction

PoC: `race_map.c` — parent forks a victim that tight-loops
`execve(self,"--victim")` (each exec replaces/frees the vmspace) and N readers
that tight-loop `open()`+`read()` of `/proc/<victim>/map` (entering
`procfs_domap`). 4 KB read buffer (deliberately small — see comment in the
source — to avoid tripping an unrelated `sbuf` malloc-limit issue that would
mask the UAF).

Result on default GENERIC `#0` (INVARIANTS ON): **deterministic kernel panic
within seconds**, every run, identical signature:
```
Fatal user address access from kernel mode from race_map at ffffffff809a2540
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x8
current process = <reader pid>
Stopped at vm_map_rb_tree_RB_NEXT: movq 0x8(%rdi),%rax   <- RB_FOREACH through freed vm_map
```
Reproduced 3× (4 readers/25 s and small-buffer variants). Guest down each time.

## Exploit chain / escalation (Phase 6)

**Outcome: blocked by a valid hard blocker — read-only UAF of a dedicated-slab
object; no attacker-controlled write to a victim is derivable. Impact = panic.**

- **Primitive:** the reader's operations on the freed/stale `map` are
  read-dominant: `ba->object` reads (`:175-178,216`), `map->timestamp` read
  (`:142,236`), `vm_map_lookup_entry`/RB walk (`:238`, `:89`). The only *write*
  to freed memory is `vm_map_lock_read(map)` at `:230` = `lockmgr LK_SHARED`,
  which increments the lockmgr shared-count at a **fixed offset**
  (`offsetof(struct vm_map, lock)` within the vmspace-embedded map) by a
  **fixed delta** (+1). It is not attacker-controlled in value or target.
- **Slab:** vmspace uses a **dedicated objcache** (`vmspace_cache`,
  `vm_map.c:121,233`). A freed vmspace is returned to `vmspace_cache` and is
  reused **only** by `vmspace_alloc()` — never by `struct file`/`ucred`/a
  function-pointer object. Cross-type slab grooming into the freed slot is
  therefore **impossible**.
- **Consequence of the lone write:** when the freed slot is reused by a new
  vmspace (the only possible reuser), `map->lock` is a *valid* lockmgr lock —
  the reader's `LK_SHARED` just takes a shared lock on a stranger's valid
  vmspace (logic confusion + a cross-process map info-leak into the reader's
  sbuf). No corruption; certainly no controlled write to a victim object.
- **No chain attempted is justified** because there is no write primitive to
  convert. This is Phase 6 valid hard blocker #1 ("genuinely read-only").
- Secondary impact: a cross-process info leak of another user's `/proc/<pid>/map`
  contents (kernel pointers, vnode paths) if the reader catches a slot reused
  by a victim's vmspace — bounded, not controllable, lower severity than the
  DoS.

## The fix (fix.diff — validated)

Mirror `procfs_rwmem()`: `vmspace_hold(vm)` before caching `map`,
`vmspace_drop(vm)` after the scan. Two DragonFly-specific constraints shaped
the exact patch:

1. **lwkt tokens are a strict LIFO stack** (`sys/kern/lwkt_token.c:828-857`,
   assertion at `:842-853`: `lwkt_reltoken` pops the top ref and requires it to
   match). `vmspace_hold()` acquires `vm_map.token` on top of `p_token`, so
   `vm_map.token` must be released *first*. Therefore `p_token` is held for the
   entire function (the original `lwkt_reltoken(&p->p_token)` at `:87` and the
   re-acquire at `:248` are removed), and `vmspace_hold`/`vmspace_drop` nest
   cleanly inside `p_token`'s scope.
2. `p_token` alone does **not** pin the vmspace: `kern_exit.c:432-433` releases
   `p_token` around `vmspace_relexit()`. The `vmspace_hold` (which bumps
   `vm_holdcnt`) is what actually prevents termination.
3. The vmspace-validity EFAULT checks (`SIDL`/`SZOMB`, `P_WEXIT`/`P_INEXEC`,
   `vmspace_getrefs<0`, mirroring `procfs_rwmem:85-88`) are placed **before**
   `sb = sbuf_new()` so the EFAULT paths do not leak the sbuf (an earlier fix
   iteration that put them after `sbuf_new` leaked the sbuf on every exec and
   tripped `sbuf: malloc limit exceeded`).

`#include <vm/vm_extern.h>` is added (declares `vmspace_hold`/`vmspace_drop`/
`vmspace_getrefs`; already included by `procfs_mem.c`).

This fix **supersedes** the finding markdown's initial proposal (which
suggested `vmspace_hold`/`vmspace_drop` without addressing the LIFO token
order, the `p_token`-held-throughout requirement, or the sbuf-leak hazard).

## Phase 8 — fix validation

| Kernel | PoC | Result |
|---|---|---|
| `#0` unpatched baseline (Jul 2) | `./race_map 25 4` | PANIC `vm_map_rb_tree_RB_NEXT` within seconds (×3) |
| `#1` single-fix (Jul 7 12:07, sha `db4fb534…`) | `./race_map 90 6` | **survived 90 s, RUN_EXIT=0, guest up, no panic** |
| `#1` single-fix | `./race_map 60 8` | **survived 60 s, RUN_EXIT=0, guest up, no panic** |

Clean before/after => `fix_status = fixed`. (Two intermediate fix iterations
failed first: v1 tripped `lwkt_reltoken: illegal release` (LIFO); v2 tripped
`sbuf: malloc limit exceeded` (leaked sbuf). v3 = shipped `fix.diff`.)
