DragonFlyBSD Kernel Audit
DF-2983 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-2983",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "panic",
  "confidence": "certain",
  "verdict": "phys_pager_putpages() panics ('phys_pager_putpage called', phys_pager.c:114) when the pagedaemon launders a queue-managed dirty page of an OBJT_PHYS (SysV shm, kern.ipc.shm_use_phys=1 default) object. Such pages are created because wiring faults (mlock/mlockall -> vm_fault with VM_FAULT_WIRE_MASK) bypass the pager entirely (TRYPAGER false, vm_fault.c:383-385), so the zero-fill branch (vm_fault.c:2327) leaves the page without the PG_UNQUEUED flag that phys_pager_getpage() (phys_pager.c:99) would have set; after munlock/exit the page is queued (vm_fault.c:2694, vm_page.c:3297) and vm_pageout's non-anon laundering gate (vm_pageout.c:1186-1189) hands it to the pager. Reproduced as a kernel panic on the stock guest: shmget 512MB -> mlock -> write -> munlock -> 1.5GB phys hog + 3GB anonymous eater; panic stack phys_pager_putpages <- vm_pageout_flush <- vm_pageout_clean_helper <- vm_pageout_page <- vm_pageout_thread. On stock DF the mlock step requires SYSCAP_RESTRICTEDROOT (vm_mmap.c:1030; the pmap_wired_count RLIMIT branch is dead code on every platform), so the trigger is privileged today - a legal SHM+mlock pattern by any root daemon crashes the kernel under memory pressure; severity held at Low because of that gate, but the kernel-level defect and panic are certain and the dead #ifdef shows an rlimit-gated unprivileged mlock was intended, which would instantly make this an unprivileged panic.",
  "exploit_chain": "shmget(IPC_PRIVATE,512MB) [OBJT_PHYS, default shm_use_phys=1] -> mlock (wiring faults, pager bypassed, no PG_UNQUEUED) -> write (dirty) -> munlock (vm_page_unwire(m,1) -> active queue) -> memory pressure via phys-object hog + 3GB anonymous eater -> pagedaemon inactive scan: swap_pageouts_ok=1 for non-SWAP/DEFAULT object -> vm_pageout_page -> vm_pageout_clean_helper -> vm_pageout_flush -> vm_pager_put_pages -> panic('phys_pager_putpage called'); guest down ~30s after pressure onset. No path past the panic to corruption (panic fires before any pager I/O), so the ceiling is availability.",
  "evidence": [
    "panic.txt - serial console: 'panic: phys_pager_putpage called' with the vm_pageout_thread->...->phys_pager_putpages stack",
    "run.log - baseline PoC staging output (root): mlock/write/munlock of 512MB shm + 1.5GB hog",
    "env.txt - guest uname, shm sysctls (shm_use_phys=1), swapinfo, RLIMIT_MEMLOCK",
    "run_fixed.log - patched kernel (#1 Sep 4): identical PoC + 3GB eater, free=9608 pages, swap 65% used (pagedaemon laundering hard), guest STILL-UP, no panic",
    "fix.diff - vm_fault.c zero-fill branch sets PG_UNQUEUED for OBJT_PHYS (root cause) + phys_pager.c putpages returns VM_PAGER_FAIL instead of panic (defense in depth)",
    "build_fix_kernel.log - nativekernel build completed BUILD_RC=0 on patched sources"
  ],
  "kernel_refs": [
    "sys/vm/phys_pager.c:114",
    "sys/vm/phys_pager.c:99",
    "sys/vm/vm_fault.c:383",
    "sys/vm/vm_fault.c:2327",
    "sys/vm/vm_fault.c:2631",
    "sys/vm/vm_fault.c:2694",
    "sys/vm/vm_pageout.c:1186",
    "sys/vm/vm_pageout.c:329",
    "sys/vm/vm_pageout.c:509",
    "sys/vm/vm_page.c:3297",
    "sys/kern/sysv_shm.c:124",
    "sys/kern/sysv_shm.c:540",
    "sys/vm/vm_mmap.c:1030"
  ],
  "poc_changes": "Seed sketch had no mlock-privilege awareness: DF gates mlock to SYSCAP_RESTRICTEDROOT (dead #ifdef pmap_wired_count), so the PoC runs as root; second shmget had to shrink 2GB->1.5GB because a leaked run-1 segment held shm_committed budget (ipcrm cleanup); 240s of shm-only pressure was insufficient - added external 3GB anonymous eater.c which produced the panic within ~30s.",
  "attempts": 4,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64",
  "runtime_sec": 300,
  "guest_dirty": 0,
  "build_cmd": "cc -O2 -Wall -o df2983 df2983.c (guest, gcc 8.3 DragonFly)",
  "run_cmd": "./df2983  (as root; then ./eater for 3GB anonymous pressure)",
  "code_hash": "e178de031a66510e134d1e79535a38db8a0002f5de6bd785e8b6c57a7636a293",
  "notes": "Trigger requires SYSCAP_RESTRICTEDROOT on stock DF (no platform defines pmap_wired_count), hence Low severity despite certain reproducibility; enabling rlimit-based unpriv mlock would make it an unprivileged panic. Zeroing discipline verified clean everywhere (no DF-2944-family leak): getpage zero-fills (phys_pager.c:97), wire-fault path zero-fills (vm_fault.c:2327), shm prealloc uses VM_ALLOC_ZERO + getpage. phys_pager_haspage TRUE-beyond-size is vestigial (no in-tree OBJT_PHYS caller). alloc arithmetic safe under caller range checks. Baseline kernel #0 Jul 2 vs fixed kernel #1 Sep 4 16:23 (uname -v); baseline reproduced, patched not, under equal-or-harsher laundering pressure (2.7GB swapped).",
  "recommended_fix": "Set PG_UNQUEUED on OBJT_PHYS pages created by the vm_fault wiring-path zero-fill branch (mirroring phys_pager_getpage), and make phys_pager_putpages return VM_PAGER_FAIL per page instead of panicking.",
  "fix_status": "fixed",
  "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Fri Sep  4 16:23:25 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "fix.diff applied to clean /usr/src (patch -p1), nativekernel + installkernel, reboot into #1 Sep 4 16:23. Identical PoC + identical 3GB eater: free dropped to 9608 pages, 2.7GB swapped (pagedaemon laundering actively), no panic, guest stayed up for the full window and after. Baseline behavior eliminated.",
  "fix_evidence": ["run_fixed.log", "build_fix_kernel.log", "fix.diff"]
}