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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5815 Β· 12 files
FileTypeDescriptionSize
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 minutes printed 5Γ— (once per minute, indefinitely), ssh dead ("Connection timed out during banner exchange") for 6+ minutes, guest unrecoverable β†’ hard reset. Console captured in panic_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 helper
  • observe_baseline_stock.log / observe_attack.log β€” captured snapshots
  • panic_wedge_console.txt β€” run #1 serial console (5Γ— warning, wedge)
  • attack2_console.txt β€” run #2 serial console
  • control_vs_attack.txt β€” side-by-side summary
  • fix.diff β€” clamp vm_max_launder at the use site (applies clean)
  • verdict.json / manifest.json

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-5815

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.