# VERDICT — DF-2834 (unbounded vmspace_create kernel-memory exhaustion)

**status: reproduced / impact: dos / confidence: certain**

Guest: DragonFly 6.5-DEVELOPMENT #0 (stock INVARIANTS kernel,
X86_64_GENERIC), QEMU/KVM 4GB / 6 vcpu. All runs 2026-10-20 UTC.

## 1. Reachability and the no-cap claim

`sys_vmspace_create` (sys/vm/vm_vmspace.c:146-201) is a plain syscall
(SYS #486) with no privilege check of any kind. The ONLY gate is
`vkernel_enable` at sys/vm/vm_vmspace.c:155 (`vm.vkernel_enable`, root
sysctl, default 0). With it set to 1 — the documented operating state of
any machine that hosts virtual kernels — any local user can create
vmspaces without limit:

- statically: no rlimit (`lim_*`), no `priv_check`, no counter, no sysctl
  cap anywhere in the function; nothing else in the kernel counts
  `p_vkernel` vmspaces;
- empirically: 120,000 live vmspaces in one unprivileged process
  (uid 65534), zero failures, 6.9 seconds (run.log, PRE3..120000 block).
  For contrast, mmap entries per proc are capped (`vm.max_proc_mmap`
  = 1000000); no analogous knob existed for vmspaces.

## 2. Per-iteration cost, line-accurate

Each successful create, held until `vmspace_destroy` or process exit:

| cost | where |
|---|---|
| `kmalloc(sizeof(struct vmspace_entry))` | sys/vm/vm_vmspace.c:182 |
| `vmspace_alloc()` objcache object (`sizeof(struct vmspace)`) | sys/vm/vm_vmspace.c:183 → sys/vm/vm_map.c:309-340; cache at vm_map.c:233 |
| 8192 B pageable kernel KVA (`kmem_alloc_pageable(kernel_map, PAGE_SIZE*2, VM_SUBSYS_PML4)`) | sys/platform/pc64/x86_64/pmap.c:2368-2370 |
| 1 **wired** PML4 `vm_page` (`pmap_allocpte(pmap_pml4_pindex())` → `vm_page_wire(m)`) | pmap.c:2376-2380 → pmap.c:2757 |
| `pmap_pinit2()` — **no-op on x86_64** | pmap.c:2509-2512 |

The finding text listed `pmap_pinit2` among the cost contributors; on
x86_64 it is an empty function (refinement, not a contradiction — the
substance of the cost chain is confirmed).

## 3. Measured growth (stock kernel)

Three runs, ids `0x10000000+i`, sampling `vm.stats.vm.v_wire_count`,
`vm.kvm_free`, `vm.stats.vm.v_free_count`:

| run | creates | Δwire | pages/create | Δkvm_free | B/create | time |
|---|---|---|---|---|---|---|
| 1 (cold) | 20,000 | +47,526 | **2.376** | 195,035,136 | 9,752 | 1.543 s |
| 2 (warm) | 20,000 | +46,066 | 2.303* | 12,618,048* | — | 1.214 s |
| 3 | 120,000 | +284,878 | **2.374** | 970,981,376 | ~9,999 | 6.915 s |

(*) warm run reused objcache-retained objects (see §5); wire slope stays
~2.37 pages/create. Growth is linear (run 1 vs run 3 slope delta 0.1%).
Rate: **17,350 creates/s** sustained.

120,000 live vmspaces = 431,414 wired pages observed mid-run
(~1.68 GB wired held by one unprivileged process). Guest stayed healthy
(load 0.18 after).

## 4. Exhaustion extrapolation — NOT executed (single-tenant guest)

- total physical pages: 4,257,873,920 / 4096 = **1,039,519**
- quiescent wirable pool (wire 143,045 + free 849,484) = 992,529 pages
- creates needed: 992,529 / 2.374 ≈ **418,000**
- time: 418,000 / 17,350 ≈ **24 seconds** for one unprivileged process
  to wire the entire 4 GB of this guest (pageout cannot help: wired pages
  are unswappable).

Failure mode at the wall: `vm_page_alloc(VM_ALLOC_NORMAL)` inside
`pmap_allocpte` (pmap.c:2751-2756) fails → `vm_wait(0)` loop; because the
exhaustion is in WIRED memory the wait can never be satisfied, and every
`M_WAITOK` allocation system-wide blocks → hard hang, no clean error.
The path offers no error return before that point (`kmalloc(M_WAITOK)` at
vm_vmspace.c:165/:182, `vmspace_alloc(M_WAITOK)`). This is the
panic-free-but-wedge class: RUN timeout + unresponsive guest would be the
observed artifact if executed; deliberately not driven to that state on
the shared audit guest per policy — the linear rate + totals above are
the honest proof of the exhaustion primitive.

## 5. Post-exit reclamation (hygiene + not-a-permanent-leak)

Process exit (`vkernel_exit` sys/vm/vm_vmspace.c:773-805 →
`rb_vmspace_delete` → `vmspace_entry_delete`) reclaims live vmspace
memory: run 3 dropped wire from 431,414 → 154,535 within 3 s of exit.
Some memory stays CACHED (objcache magazines retain vmspace objects with
their pmap — see vm_map.c:244 comment; `vmstat -m` showed 390 MB M_VKERNEL
retained right after exit) — run 2 proved this cache is REUSED, not
leaked (Δkvm_free ≈ 0 for the first 15,000 warm creates). The DoS
primitive is the LIVE pinned set, which the attacker controls the
duration of.

## 6. Fix validation (A/B on patched kernel)

Patch: `fix.diff` — adds `nvmspaces` to `struct vkernel_proc`
(sys/sys/vkernel.h), `vm.max_vkernel_vmspaces` sysctl (default 64),
cap checked+incremented under `vkp->token` in the same critical section
as the RB insert (mirroring the EEXIST unwind), decremented in
`vmspace_entry_delete` under the token. Also the DF-2835 EINVAL guards.

Built with `make nativekernel KERNCONF=X86_64_GENERIC` in the guest
scratch /usr/src (6.1 min, warning-free), installed, rebooted into
`#1: Tue Oct 20 11:55:22 UTC 2026`. sysctl `vm.max_vkernel_vmspaces: 64`
present.

| leg | result |
|---|---|
| [A] unpriv `vmsp_grow 100000 -u` | `FAILED iter=64 rc=-1 errno=59 (ETOOMANYREFS)` — clean error, wire flat at baseline |
| [B] cap arithmetic (`vmsp_cap`, uid 1001) | create #65 → ETOOMANYREFS; destroy → 0; create again → 0 (slot freed) |
| [C] legitimate use unchanged | root raises cap to 100000 → unpriv creates 5000 in 313 ms, all succeed, reclaimed on exit |
| [D] per-process cap | two concurrent filler processes EACH hit #65 independently |

Legitimate vkernel workloads (vkernel userland creates one vmspace per
emulated process; typical counts are tens) sit far below 64; operators
running huge vkernels raise one sysctl. Not boot-tested end-to-end
(vkernel-in-vkernel out of scope for this guest) — reasoned from
vmspace_create call sites.

Baseline vs patched: stock = unbounded linear growth, no error;
patched = clean ETOOMANYREFS at the cap, legitimate paths intact.

## 7. Team directive: privesc?

NOT privesc. There is no uid=0 route — the primitive is resource
exhaustion (impact dos). It is gated on `vm.vkernel_enable=1`, a root
tunable that defaults to 0, so default-config machines return EOPNOTSUPP
to unprivileged callers. On vkernel-hosting machines (tunable = 1, the
supported configuration) any local unprivileged user can wedge the box in
seconds. Medium severity is the correct rating; the finding stands as
filed.

## 8. Guest hygiene

`vm.vkernel_enable` restored to 0 immediately after each stock-kernel run
and after the fixed-kernel legs; guest `vm.sh reset with-src` performed
at the end (patched kernel replaced by the clean snapshot kernel);
guest_dirty = 0.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
