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)
PoC verification
Evidence pack
findings/poc/DF-2983 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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):
- Root cause β sys/vm/vm_fault.c: in the no-backing-object zero-fill branch,
mirror
phys_pager_getpage()for OBJT_PHYS pages: setPG_UNQUEUEDso 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). - Defense in depth β sys/vm/phys_pager.c:
phys_pager_putpages()returnsVM_PAGER_FAILfor 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.
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 defaultkern.ipc.shm_use_phys=1) βmlock()(wiring faults) β write (dirty) βmunlock()(pages queued) β 1.5 GB second shm segment as unreclaimable hog β externaleater.c(3 GB anonymous) to drive the pagedaemon into laundering.
Attempt log
uid=1001run:mlockβ EPERM. Traced to sys/vm/vm_mmap.c:1030 βcaps_priv_check_td(td, SYSCAP_RESTRICTEDROOT); the#ifdef pmap_wired_countRLIMIT branch is dead code (no platform definespmap_wired_count). mlock is privileged on all DF platforms β PoC must run as root.- Root run v1: second shmget(2 GB) β ENOMEM: run-1's leaked segment still held
shm_committedbudget. Fixed byipcrm+ shrinking hog to 1.5 GB. - Root run v2 (2 GB total hog, no anon eater): 240 s, no panic β pressure insufficient to force laundering (free ~750 MB).
- 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)
shmget()β OBJT_PHYS object (sys/kern/sysv_shm.c:540-542; defaultshm_use_phys=1at sysv_shm.c:124).mlock()βvm_fault_wire(user_wire=TRUE)βvm_fault(..., VM_FAULT_USER_WIRE)(sys/vm/vm_fault.c:2593-2632).TRYPAGER()is false wheneverVM_FAULT_WIRE_MASKis set (sys/vm/vm_fault.c:383-385) βphys_pager_getpage()never runs β page allocated byvm_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).- 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. - Memory pressure: active scan deactivates dirty page (vm_pageout.c:1605-1616);
inactive scan sets
swap_pageouts_ok=1for any object that is not OBJT_SWAP/OBJT_DEFAULT (vm_pageout.c:1184-1196 β assumes non-anon β vnode);vm_pageout_clean_helperPG_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->sizevs 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
fixedfix.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']
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.
No comments yet.