β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2983

phys_pager_putpages() panics: vm_fault wiring path creates queue-managed pages in OBJT_PHYS objects (SysV shm + mlock + memory pressure β†’ kernel panic)

Field Value
ID DF-2983
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H
CWE CWE-617 / CWE-667
File sys/vm/phys_pager.c
Lines 114, 99 (invariant split: vm_fault.c:383-385, 2327)
Area vm
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:vm
Reported pending
Known CVE none
CVE match novel

Summary

phys_pager_putpages() executes panic("phys_pager_putpage called") on the assumption that all pages in an OBJT_PHYS object are PG_UNQUEUED/unmanaged — an invariant only phys_pager_getpage() establishes (:99). The vm_fault wiring path violates it: TRYPAGER() is false for any fault with VM_FAULT_WIRE_MASK, so mlock/mlockall-driven faults on not-yet-resident SysV shm pages (OBJT_PHYS, kern.ipc.shm_use_phys=1 default) never call the pager; the page is vm_page_alloc'd and zero-filled at vm_fault.c:2327 WITHOUT PG_UNQUEUED — a queue-managed page in a phys object. After write + munlock/exit it is queued, and under memory pressure the pagedaemon launders it — vm_pageout treats any non-SWAP/non-DEFAULT object as launderable, vm_pager_put_pages → panic. Any process with SYSCAP_RESTRICTEDROOT that legally uses the SHM+mlock pattern (root-run databases, chrome-style shm daemons) crashes the whole kernel as soon as the pagedaemon launders — complete availability loss from ordinary documented syscall usage, no attacker-supplied data needed. Not reachable by uid≠0 on stock DF (mlock priv-gated; the pmap_wired_count RLIMIT branch is dead code), capping at Low — but enabling rlimit-gated unprivileged mlock would immediately convert this into an unprivileged kernel panic (then High).

Proof of contest

VERIFIED (findings/poc/DF-2983/): root: 512MB shmget β†’ shmat β†’ mlock β†’ memset β†’ munlock + 1.5GB PG_UNQUEUED hog + 3GB anonymous eater β†’ 'panic: phys_pager_putpage called', stack phys_pager_putpages←vm_pageout_flush←vm_pageout_clean_helper←vm_pageout_page← vm_pageout_thread, guest down (~30s of pressure). fix.diff validated (PG_UNQUEUED set in vm_fault's zero-fill branch for OBJT_PHYS + defensive VM_PAGER_FAIL putpages): rebuilt kernel survived identical PoC under harsher pressure (free=9608 pages, 2.7GB swapped). No userβ†’root route β€” ceiling is availability.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of phys_pager.c (GLM 5.3); panic reproduced + fix validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2983 Β· 14 files
FileTypeDescriptionSize
df2983.c β€” 4.1 KB view raw
eater.c β€” 848 B view raw
build.sh β€” 173 B view raw
run.sh β€” 419 B view raw
build.log β€” 62 B view raw
run.log β€” 427 B view raw
run_fixed.log β€” 778 B view raw
build_fix_kernel.log β€” 229 B view raw
panic.txt β€” 2.2 KB view raw
env.txt β€” 466 B view raw
fix.diff β€” 1.3 KB view raw
verdict.json β€” 5.7 KB view raw
VERDICT.md β€” 5.9 KB ↓ raw
README.md β€” 4.5 KB ↓ raw

DF-2983 β€” phys_pager_putpages panic: wiring faults create queue-managed pages in OBJT_PHYS objects

What

phys_pager_putpages() unconditionally executes panic("phys_pager_putpage called") (sys/vm/phys_pager.c:114). The pager's design assumption is that every page in an OBJT_PHYS object is PG_UNQUEUED (unmanaged, never on a page queue) because phys_pager_getpage() sets that flag (phys_pager.c:99).

That assumption is violated by the page-fault wiring path: TRYPAGER() is false for any fault carrying VM_FAULT_WIRE_MASK (sys/vm/vm_fault.c:383-385), so a wiring fault on a not-yet-resident page of an OBJT_PHYS object never calls the pager; the page is allocated by vm_page_alloc() (no PG_UNQUEUED) and zero-filled at vm_fault.c:2327 β€” a queue-managed page in a phys object.

When such a page is later unwired (munlock / process exit: vm_fault_unwire() β†’ vm_page_unwire(m, 1), vm_fault.c:2694 β†’ vm_page.c:3297-3315) it lands on the active queue, dirty after any write. Under memory pressure the pagedaemon launders it:

vm_pageout_scan_inactive: type != OBJT_SWAP && != OBJT_DEFAULT  -> swap_pageouts_ok=1
    (vm_pageout.c:1186-1189 β€” the code assumes "not anonymous" == "vnode")
vm_pageout_page() -> vm_pageout_clean_helper()  (PG_UNQUEUED check at :329 passes)
    -> vm_pageout_flush() -> vm_pager_put_pages() (vm_pageout.c:509)
    -> phys_pager_putpages() -> panic  (phys_pager.c:114)

Reproduced stack (serial console):

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")

Attack surface / privilege

The only in-tree producer of OBJT_PHYS objects is SysV shm with kern.ipc.shm_use_phys=1 (the default, sys/kern/sysv_shm.c:124,540-542).

Trigger privilege: on DragonFly mlock(2)/mlockall(2) are gated by caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT) (sys/vm/vm_mmap.c:1030 and :1059; the #ifdef pmap_wired_count RLIMIT branch is dead code β€” no platform defines pmap_wired_count). The reproducer therefore runs as root: any privileged process that legally uses SHM + mlock (databases, chrome-style shm + mlock daemons) crashes the kernel as soon as the pagedaemon launders. Not reachable by uid!=0 on a stock system today β€” but it is a live landmine: the vm_mmap.c #ifdef shows an rlimit-gated unprivileged mlock was intended, and enabling it turns this into an unprivileged panic instantly.

Impact

Kernel panic β€” full system crash (local availability), triggered by ordinary documented syscall usage (no bug in the caller). Severity: Low on stock DF (requires SYSCAP_RESTRICTEDROOT to wire), certain reproducibility.

Reproduce (baseline)

Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC), 4 GB RAM + 4 GB swap.

./build.sh                                  # cc -O2 -o df2983 df2983.c
# as root (mlock requires SYSCAP_RESTRICTEDROOT on DF):
./run.sh                                    # stage A: shmget 512MB, mlock, write,
                                            #          munlock -> dirty managed pages
                                            # stage B: 1.5GB unreclaimable phys hog
                                            # stage C: anonymous eater (eat3g.c)
# ~1-2 minutes under memory pressure:
#   panic: phys_pager_putpage called        (see panic.txt)

Note: the original df2983.c embeds the anon eater (512 MB); the decisive run used the external eater.c (3 GB) started after the main program finished staging, because 2.5 GB of hog alone did not push the pagedaemon into laundering fast enough.

Fix

fix.diff (two hunks):

  1. Root cause β€” sys/vm/vm_fault.c: in the no-backing-object zero-fill branch, mirror phys_pager_getpage() for OBJT_PHYS pages: set PG_UNQUEUED so wiring faults stop producing queue-managed pages in phys objects. This also preserves SHM data semantics (phys pages then never get reclaimed/zeroed by the daemon).
  2. Defense in depth β€” sys/vm/phys_pager.c: phys_pager_putpages() returns VM_PAGER_FAIL for every page instead of panicking (pages stay dirty+resident; contents preserved).

Validated: with the fix applied (nativekernel rebuild, same guest), the identical PoC + 3 GB eater ran to completion with the guest staying up; re-run of the eater confirmed sustained laundering pressure with no panic. See VERDICT.md.

VERDICT.md
↓ download raw

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).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

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.

['run_fixed.log', 'build_fix_kernel.log', 'fix.diff']
↓ fix.diffDragonFly 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

Confirmed kernel references

Detail

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 (decisive lines)

["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']

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.

Verified 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.

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.