# DF-2983 — VERDICT

**Status: reproduced** (kernel panic) · **Impact: panic (local kernel DoS)** ·
**Confidence: certain** · Trigger privilege on stock DF: `SYSCAP_RESTRICTEDROOT` (mlock).

## What was run

- Guest: DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026
  (X86_64_GENERIC), 4 GB RAM, 4 GB swap, stock INVARIANTS kernel.
- PoC: `df2983.c` — SysV shm (OBJT_PHYS via default `kern.ipc.shm_use_phys=1`) →
  `mlock()` (wiring faults) → write (dirty) → `munlock()` (pages queued) →
  1.5 GB second shm segment as unreclaimable hog → external `eater.c` (3 GB
  anonymous) to drive the pagedaemon into laundering.

## Attempt log

1. `uid=1001` run: `mlock` → **EPERM**. Traced to sys/vm/vm_mmap.c:1030 —
   `caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT)`; the `#ifdef pmap_wired_count`
   RLIMIT branch is dead code (no platform defines `pmap_wired_count`). mlock is
   privileged on all DF platforms → PoC must run as root.
2. Root run v1: second shmget(2 GB) → ENOMEM: run-1's leaked segment still held
   `shm_committed` budget. Fixed by `ipcrm` + shrinking hog to 1.5 GB.
3. Root run v2 (2 GB total hog, no anon eater): 240 s, no panic — pressure
   insufficient to force laundering (free ~750 MB).
4. **Decisive run**: v2 staging plus `eater.c` (3 GB anonymous). Free fell to
   ~45 K pages; within ~30 s: ssh died, `vm.sh status` ⇒ **down**. Serial
   console (`panic.txt`):

   ```
   panic: phys_pager_putpage called
   cpuid = 3
   phys_pager_putpages() at phys_pager_putpages+0x12
   vm_pageout_flush() at vm_pageout_flush+0x16d
   vm_pageout_clean_helper() at vm_pageout_clean_helper+0x358
   vm_pageout_page() at vm_pageout_page+0x46b
   vm_pageout_thread() at vm_pageout_thread+0x1fc8
   Debugger("panic")
   ```

   Exactly the source-predicted chain.

## Why it happens (path:line)

1. `shmget()` → OBJT_PHYS object (sys/kern/sysv_shm.c:540-542; default
   `shm_use_phys=1` at sysv_shm.c:124).
2. `mlock()` → `vm_fault_wire(user_wire=TRUE)` → `vm_fault(...,
   VM_FAULT_USER_WIRE)` (sys/vm/vm_fault.c:2593-2632).
3. `TRYPAGER()` is false whenever `VM_FAULT_WIRE_MASK` is set
   (sys/vm/vm_fault.c:383-385) → `phys_pager_getpage()` never runs → page
   allocated by `vm_page_alloc()` (vm_fault.c:2036-2043, no PG_UNQUEUED) and
   zero-filled at vm_fault.c:2327 → **queue-managed page in a phys object**
   (phys_pager.c:99's PG_UNQUEUED is the invariant, set only in getpage).
4. write → dirty; `munlock()`/exit → `vm_fault_unwire()` → `vm_page_unwire(m,1)`
   (vm_fault.c:2690-2696) → `vm_page_activate()` (vm_page.c:3297-3315) → page
   on the active queue.
5. Memory pressure: active scan deactivates dirty page (vm_pageout.c:1605-1616);
   inactive scan sets `swap_pageouts_ok=1` for any object that is *not*
   OBJT_SWAP/OBJT_DEFAULT (vm_pageout.c:1184-1196 — assumes non-anon ⇒ vnode);
   `vm_pageout_clean_helper` PG_UNQUEUED guard passes (vm_pageout.c:329);
   `vm_pageout_flush()` → `vm_pager_put_pages()` (vm_pageout.c:509) →
   `phys_pager_putpages()` → `panic()` (sys/vm/phys_pager.c:114).

The primitive is memory-safety-adjacent but terminates in a clean `panic()` —
no corruption primitive exists past the panic; exploitation ceiling is
availability. DF's `default_pager_putpages` (→ swap_pager) shows the graceful
alternative; phys_pager is the only pager that panics.

## Privilege note (why severity is Low, not High)

The only unprivileged-relevant producer of OBJT_PHYS objects is SysV shm, but
the *wire-fault* trigger requires `mlock`, which DF gates to
`SYSCAP_RESTRICTEDROOT` (vm_mmap.c:1030, :1059). No unprivileged producer of
`VM_FAULT_WIRE_MASK` faults on user maps exists in-tree (swept: mlock,
mlockall, MAP_WIREFUTURE in mmap/sbrk/vm_map_fork — all originate from
priv-gated mlockall). So: any privileged process using the completely legal
SHM+mlock pattern crashes the kernel under memory pressure; a jailed root
cannot (syscap not granted). If DF ever enables rlimit-based unprivileged
mlock (the dead `#ifdef pmap_wired_count` scaffolding suggests intent), this
becomes an unprivileged panic immediately.

## Fix validation (fix.diff)

- `vm.sh reset with-src`, applied fix.diff (2 hunks: vm_fault.c zero-fill
  branch sets PG_UNQUEUED for OBJT_PHYS pages — root cause; phys_pager.c
  putpages returns VM_PAGER_FAIL per page instead of panic — defense in depth).
- `make nativekernel KERNCONF=X86_64_GENERIC` + `make installkernel`, reboot
  into DragonFly 6.5-DEVELOPMENT **#1** Fri Sep 4 16:23:25 UTC 2026.
- Re-ran the identical PoC on the patched kernel, then the identical 3 GB
  eater: free fell to **9,608 pages** (37 MB), swap 65 % used (2.7 GB laundered
  by the pagedaemon — the exact machinery that panicked the baseline), guest
  stayed **up**, eater alive (`run_fixed.log`).
- fix_status: **fixed** (baseline panic gone under equal-or-harsher pressure).

## Negative results (classes considered and killed for this file)

- Uninitialized-memory leak (DF-2944 family): killed — every path that creates
  a phys-object page zero-fills it (`phys_pager_getpage` → vm_page_zero_fill,
  phys_pager.c:97; wire-fault zero-fill vm_fault.c:2327; prealloc grab uses
  VM_ALLOC_ZERO then getpage zero-fills, sysv_shm.c:582-587).
- phys_pager_alloc arithmetic (phys_pager.c:67-73): callers range-check
  (`uap->size` vs shmmin/shmmax, sysv_shm.c:505; shmall commit check :510);
  foff is always 0; no overflow reachable.
- phys_pager_haspage == TRUE beyond object size (phys_pager.c:131-135): no
  in-tree caller reaches it for OBJT_PHYS (vm_fault no longer does readahead
  clustering; only drm linux_shmem/ttm call vm_pager_has_page, on other object
  types). Vestigial.
- getpage NULL page / pindex-beyond-size: callers pre-check
  (vm_fault.c:2015, sysv_shm.c:585).
- dealloc: asserts only; page freeing via vm_object_terminate handles
  PG_UNQUEUED pages (RB-tree indexed); swblocks impossible on phys objects.
- Refcount/lock issues: pager holds no per-object state beyond the object
  itself (handle forced NULL, phys_pager.c:72).
