vm.max_launder sysctl accepts negative values: per-queue launder budget goes negative, dirty-page laundering is silently and permanently disabled, and any unprivileged memory workload then livelocks the pagedaemon (deterministic whole-machine freeze until power-cycle)
Summary
vm_max_launder (SYSCTL_INT RW, root) is divided and re-capped in vm_pageout_scan_inactive without any sign check: for vm_max_launder <= -16 the cap branch is always true (negative RHS) and overwrites the <=1 floor, yielding a NEGATIVE budget β vm_pageout_page's laundering branch (*max_launderp > 0) is then unreachable and the pagedaemon can never again clean a dirty page. Once free+cache is exhausted, avail_shortage never retires, every allocation blocks in vm_wait, the once-per-minute Continuous paging warning fires forever, remote administration is lost until hard reset. REPRODUCED 2/2 on the guest: sysctl vm.max_launder=-32 then a 60s anon-touch hog as uid 1001 froze the machine (swap pageouts frozen at 205 vs 39047 in the stock control; ssh dead; power-cycle recovery). Severity capped Low (enabling write needs root). Clamp-at-use + fixed cap comparison git-apply-clean.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5815 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df5815_hog.c | β | 1.8 KB | view raw | |
| build.sh | β | 94 B | view raw | |
| run.sh | β | 141 B | view raw | |
| observe.sh | β | 424 B | view raw | |
| observe_baseline_stock.log | β | 630 B | view raw | |
| observe_attack.log | β | 808 B | view raw | |
| panic_wedge_console.txt | β | 2.3 KB | view raw | |
| attack2_console.txt | β | 2.3 KB | view raw | |
| control_vs_attack.txt | β | 1.6 KB | view raw | |
| fix.diff | β | 867 B | view raw | |
| verdict.json | β | 4.2 KB | view raw | |
| README.md | β | 3.3 KB | β raw |
DF-5815 β vm.max_launder accepts negative values: laundering silently disabled, pagedaemon livelock (reproduced DoS)
Finding: vm.max_launder (SYSCTL_INT, CTLFLAG_RW, root) is used
without any range validation in vm_pageout_scan_inactive()
(sys/vm/vm_pageout.c:828-836):
max_launder = (long)vm_max_launder / PQ_L2_SIZE; /* negative for vm<0 */
if (pass) max_launder *= 2;
max_launder = (max_launder + MAXSCAN_DIVIDER - 1) / MAXSCAN_DIVIDER;
if (max_launder <= 1) max_launder = 1;
if (max_launder >= vm_max_launder / 16) /* ALWAYS true when */
max_launder = vm_max_launder / 16 + 1; /* vm_max_launder < 0 */
For vm_max_launder <= -16 the final cap overwrites the floor and the
per-queue launder budget goes negative, so vm_pageout_page()'s branch
} else if (*max_launderp > 0) { /* sys/vm/vm_pageout.c:1171 */
is never taken: no dirty page is ever paged out again. The pagedaemon
can then never rebuild free+cache from dirty memory; it spins forever
(avail_shortage never retires, pass grows, "Continuous paging"
warning once per minute), every allocation that hits vm_wait() blocks
forever, and the machine wedges until rebooted (the sysctl is
boot-reset, but a wedged box needs a hard reset).
Reproduced (2/2 deterministic), impact=dos
Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC), 4 GB RAM,
8 GB swap, HAMMER2 root. Hog = unprivileged (maxx, uid 1001).
Control (stock vm.max_launder: 4076)
df5815_hog 3400 60(3.4 GB anon dirty hog for 60 s): swap pager pageouts 39047, hog completed in 61 s ("hog done"), ssh alive throughout, zero "Continuous paging" console lines,dmesg | grep -c "Continuous paging"β 0.
Attack (sysctl vm.max_launder=-32 as root)
- Run #1 (3400 MB hog): swap pager pageouts frozen at 205 (nothing
laundered),
pagedaemon: WARNING! Continuous paging for 1 minutesprinted 5Γ (once per minute, indefinitely), ssh dead ("Connection timed out during banner exchange") for 6+ minutes, guest unrecoverable β hard reset. Console captured inpanic_wedge_console.txt. - Run #2 (deliberate, same params): sysctl set 03:38:48, hog started
03:38:49; by ~03:41 ssh dead and 3Γ "Continuous paging" on the serial
console (
attack2_console.txt) β hard reset.
Intermediate observation (2900 MB hog, -32): system idles at 80 MB free with daemon counters frozen (no wakeups, nothing laundered) β the wedge requires free+cache exhaustion, then nothing recovers.
Threat
Root must misconfigure one tunable (or a management agent feeding it a bad value); afterwards any unprivileged user's ordinary memory usage freezes the whole machine (denial of service against all tenants), and remote administration is lost until power-cycle. Severity Low (requires root for the write) but deterministic and total.
Files
df5815_hog.cβ unprivileged anon hog (build.sh/run.sh)observe.shβ guest-side counter/vmstat snapshot helperobserve_baseline_stock.log/observe_attack.logβ captured snapshotspanic_wedge_console.txtβ run #1 serial console (5Γ warning, wedge)attack2_console.txtβ run #2 serial consolecontrol_vs_attack.txtβ side-by-side summaryfix.diffβ clampvm_max_launderat the use site (applies clean)verdict.json/manifest.json
Fix verification
not_testablefix.diff (negative clamp + cap-condition fix) applies clean to sys/vm/vm_pageout.c and provably removes the negative-budget path by construction; kernel fix-build not performed (non-corruption Low/DoS finding; guest kernel rebuild ~40min not justified per contract). Baseline (misconfig+wedge) reproduced 2/2 before the fix.
findings/poc/DF-5815/fix.diff (git apply --check clean)
Confirmed kernel references
Detail
Exploit chain
root (or a management agent) writes sysctl vm.max_launder=-32 -> per-queue launder budget negative -> pagedaemon can never clean dirty pages -> unprivileged user runs ordinary memory hog -> free+cache exhausts -> all allocations block in vm_wait forever -> console floods 'Continuous paging' once/min -> ssh/remote admin dead -> hard reset required. No privilege escalation path (governor state only).
Evidence (decisive lines)
['findings/poc/DF-5815/control_vs_attack.txt β side-by-side: stock=39047 swap pageouts/hog completes; -32=frozen laundering + wedge', "findings/poc/DF-5815/panic_wedge_console.txt β run #1 serial console: 5x 'pagedaemon: WARNING! Continuous paging for 1 minutes'", 'findings/poc/DF-5815/attack2_console.txt β run #2 (deliberate): 3x warning, ssh dead by t+170s', 'findings/poc/DF-5815/observe_attack.log β swap pageouts frozen at 205 with -32 while fre sits at 80MB', 'sys/vm/vm_pageout.c:828-836 β the unvalidated-division + cap inversion', "sys/vm/vm_pageout.c:1171 β '*max_launderp > 0' branch never taken"]
PoC changes
none β the hog is a plain anonymous-memory touch loop; the decisive parameter (3400MB on 3.4GB usermem) was found by escalation from 2900MB (which left enough free+cache headroom for the daemon to idle at target).
Verified recommended fix
Clamp vm_max_launder at the use site (negative -> 0) and make the cap conditional; see fix.diff.
Verdict
Deterministically reproduced (2/2 runs). With root-set vm.max_launder=-32, vm_pageout_scan_inactive()'s budget math (sys/vm/vm_pageout.c:828-836) yields a NEGATIVE per-queue max_launder (-32/64=0 -> floor 1 -> cap test 1 >= -2 always true -> -2+1 = -1), so vm_pageout_page()'s laundering branch (:1171, '*max_launderp > 0') is permanently unreachable: dirty pages are never paged out. An unprivileged 3.4GB anonymous hog (uid 1001) then exhausts free+cache; the pagedaemon spins forever ('pagedaemon: WARNING! Continuous paging for 1 minutes' every minute on the serial console, 5x in run #1 / 3x in run #2), all vm_wait() sleepers block, ssh dies with 'Connection timed out during banner exchange', and only a hard reset recovers (both runs ended in vm.sh reset). Identical hog with stock vm.max_launder=4076: 39047 swap pager pageouts, hog completes in 61s, ssh alive, zero warnings. Severity Low only because the enabling write requires root; after the misconfiguration any unprivileged user freezes the machine and remote administration is lost until power-cycle.
No comments yet.