# VERDICT — DF-2839

**Status: reproduced (impact: dos — unprivileged kernel heap exhaustion via
orphaned M_UPMAP allocations). Confidence: certain (for the leak);
the freed-page stray-PTE edge is speculative.**

## What the finding is

`user_kernel_mapping()` (`sys/kern/kern_memio.c:801-824`) — the
`UKSMAPOP_FAULT` handler for `/dev/lpmap` (minor 7, mode 0666, open to any
user) — reads `lp->lwp_lpmap` and, when NULL, calls `lwp_usermap(lp, -1)`
(kern_memio.c:813) **without any lifetime synchronization against the lwp
leaving the map** other than the vm_map read lock held by `vm_fault()`.

The teardown side, `lwp_userunmap()` (`sys/kern/kern_proc.c:1356-1384`),
runs from `lwp_exit()` (`sys/kern/kern_exit.c:688`) and:

1. `lwkt_gettoken(&lp->lwp_token)` (kern_proc.c:1363) — *released whenever
   the thread sleeps* (LWKT tokens are re-acquired by the scheduler,
   `lwkt_getalltokens()` in `lwkt_switch`),
2. saves and NULLs `lp->lwp_lpmap` (1365-1366),
3. drains every lpmap backing with `vm_map_remove()` (1374) — this *sleeps*
   for the map write lock, for milliseconds under mapping churn, **with
   LWP_MP_WEXIT not yet set** (it is only set after `lwp_userunmap()`
   returns, kern_exit.c:695),
4. frees the saved lpmap (1381) — the only `lwp_lpmap` free site in the
   kernel (verified by grep).

A page fault on a mapping created by the exiting thread (any other thread
of the same process — the map is process-wide) that lands in the drain
window:

- `lp->lwp_lpmap` reads NULL (step 2 already ran),
- `lwp_usermap()` acquires `lwp_token` (free — the exiter released it to
  sleep), passes the `LWP_MP_WEXIT` guard (kern_proc.c:1326 — flag not set
  yet), and **installs a brand-new lpmap** into the exiting lwp,
- the fault even succeeds (`pmap_kextract` of the fresh page, kern_memio.c:821),
- the exiter wakes, finishes `lwp_userunmap()` having already saved the OLD
  pointer, and the LWP is reaped with the NEW allocation still installed.

**Nothing ever frees it: 4096 bytes of M_UPMAP kernel heap per hit,
triggerable by an unprivileged user at will.**

## How it was proven (run.3.log, leak_sample.txt)

Stock kernel `DragonFly dfbsd 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC
2026 X86_64_GENERIC`, unprivileged uid 1001, `/dev/lpmap` crw-rw-rw-:

    upmap  15.4K / 61.4M   (residual of earlier probe runs)
    +45 s  22.4K / 89.5M   (+7.0K allocs, +28.1 MB)
    +45 s  29.2K / 117M    (+6.8K allocs, +27.6 MB)
    ps ax | wc -l == 196 at every sample

Monotonic growth across two independent runs with constant live-process
count = orphaned allocations, ~150 pages/s from four unprivileged
processes. M_UPMAP is capped at 390M on this guest; sustained racing
exhausts the cap in minutes and permanently consumes kernel heap (local
DoS; each wedged page also pins a kernel page table mapping).

## Why this is not the deadlock I first suspected (negative results)

v1/v2 (`run.log`, `run.2.log`) probed an AB-BA deadlock
(map-read-lock held while `lwkt_gettoken(lwp_token)` vs
`lwp_token`-held `vm_map_remove`). It cannot deadlock: DFly LWKT tokens
are per-thread and are **re-acquired by the scheduler when the thread is
rescheduled** (`lwkt_getalltokens`, sys/kern/lwkt_thread.c:707/755), so the
exiting thread does not hold `lwp_token` while sleeping in the drain. The
lockmgr is also reader/excl fair (`lockmgr_shared`, sys/kern/kern_lock.c:107:
new shared acquisitions block behind `LKC_EXREQ`). Both probes ran 90 s
with zero stalls. The leak above is what the same interleaving actually
produces.

## Impact ceiling (speculative, source-proven only)

Two narrow edges of the same unsynchronized window:

- the fault's `pmap_enter()` (vm_fault.c:577) can land after the drain's
  `pmap_remove()` of the entry range, leaving a stray user PTE to a page
  freed at kern_proc.c:1381 (RW window onto recycled kernel heap) — needs
  the faulting thread to stall between callback return and `pmap_enter`
  across the whole drain: not demonstrated;
- with a *real* lpmap (victim faults one page first), the pre-NULL window
  allows `pmap_kextract` of the page freed at 1381 — same stall caveat.

Reported as Medium/dos on the strength of the demonstrated leak; the
memcorrupt edges are documented for triage, not claimed.

## Fix validation (fix.diff — verified on this guest)

`LWP_MP_WEXIT` is now set in `lwp_exit()` *before* `lwp_userunmap()`
(kern_exit.c). This is airtight for the leak: `lwp_usermap()`'s check is a
token-serialized check-and-store evaluated *after* its (sleeping) kmalloc,
and `lwp_userunmap()`'s save happens under the same token — so any store
that slips in before the save is freed by the drain, and any store after
the save is refused by the flag. It also closes the stray-`pmap_enter`
edge, because faults in the window now fail with EINVAL (allocation
refused) instead of succeeding.

Rebuilt with `make -j6 nativekernel` + `make installkernel`, rebooted;
identical PoC workload ran twice with zero M_UPMAP growth
(see verdict.json `fix_*` fields and fix run log).

## Kernel references

- sys/kern/kern_memio.c:801-824 (user_kernel_mapping, minor 7)
- sys/kern/kern_memio.c:813 (lwp_usermap call, unsynchronized)
- sys/kern/kern_proc.c:1318-1345 (lwp_usermap, WEXIT guard)
- sys/kern/kern_proc.c:1356-1384 (lwp_userunmap, save/drain/free)
- sys/kern/kern_exit.c:688,695 (WEXIT set after lwp_userunmap — the bug)
- sys/kern/kern_exit.c:1269 (proc_userunmap at reap — upmap side ordered safely)
- sys/vm/vm_fault.c:562-580, 467 (uksmap fault under map read lock)
- sys/vm/vm_map.c:1660-1665 (aux_info = curthread->td_lwp at mmap)
