swapoff_one mutates global swapblist without vm_token, racing with concurrent page-out (blist corruption / UAF on resize)
| Field | Value |
|---|---|
| ID | DF-0945 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:C/C:H/I:H/A:H |
| CWE | CWE-662 Improper Synchronization; CWE-416 Use After Free |
| File | sys/vm/swap_pager.c (root cause in sys/vm/vm_swap.c) |
| Lines | swap_pager.c:536,537,587,599; vm_swap.c:538-544,572-577 |
| Area | vm |
| Confidence | certain |
| Discovered | 2026-07-05 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
Every blist mutation performed by the swap pager proper is guarded by
vm_token β swp_pager_getswapspace acquires vm_token at
swap_pager.c:536 before blist_allocat, and
swp_pager_freeswapspace at swap_pager.c:587 before blist_free.
sys/vm/vm_swap.c:swaponvp explicitly takes vm_token with the
comment "needed for vm_swap_size and blist" (vm_swap.c:263).
swapoff_one (vm_swap.c:441) only acquires swap_mtx and then
performs blist_fill (vm_swap.c:542), blist_destroy
(vm_swap.c:572), and blist_resize (vm_swap.c:577) on the same
shared swapblist with no vm_token. Concurrent swap allocation/free
from the page daemon or any swap-cache strategy call therefore races,
corrupting the radix tree and β in the resize path β using a pointer to
a tree that has just been kfree()'d.
Root cause
swapoff_one at sys/vm/vm_swap.c:538-544 executes:
sp->sw_flags |= SW_CLOSING;
for (dvbase = SWB_DMMAX; dvbase < aligned_nblks; dvbase += SWB_DMMAX) {
blk = min(aligned_nblks - dvbase, SWB_DMMAX);
vsbase = index * SWB_DMMAX + dvbase * nswdev;
vm_swap_size -= blist_fill(swapblist, vsbase, blk);
vm_swap_max -= blk;
}
and at vm_swap.c:572-577:
blist_destroy(swapblist);
swapblist = NULL;
// or
blist_resize(&swapblist, nswap, 0);
while holding only swap_mtx. swp_pager_getswapspace
(swap_pager.c:536-567) and swp_pager_freeswapspace
(swap_pager.c:583-603) hold only vm_token when calling
blist_allocat/blist_free. Neither swap_mtx nor vm_token nests
the other on this path, so the two serializations are independent and
the blist radix tree (no internal locking β confirmed by reading
subr_blist.c) is modified by two CPUs simultaneously.
The worst manifestation is blist_resize (subr_blist.c:320-338),
which does *pbl = newbl; blst_copy(...); blist_destroy(save); β a
concurrent blist_allocat that cached the old swapblist pointer will
dereference swapblist->bl_root after blist_destroy() has
kfree()'d it (UAF), and a concurrent alloc reading swapblist after
the pointer swap but before blst_copy completes will see a
half-initialized tree.
The asymmetry with swaponvp (which does take vm_token at
vm_swap.c:263) shows this is an oversight, not a design choice.
Threat model & preconditions
- Attacker position: Root issues
swapoff(2)(gated bySYSCAP_RESTRICTEDROOTatvm_swap.c:410) while the system is under memory pressure. The memory-pressure side β heavy anonymous paging plus swap-cache writes β can be driven hard by an unprivileged user (malloc a large anonymous region, frob lots of tmpfs files, etc.). - Privileges gained or impact: The corruption modes are concrete:
1. Double-allocation β
blist_allocatreturns a block thatblist_fillis concurrently marking filled, giving two pages the same swap block, so each process's swap-in reads the other's data (cross-process info leak / data corruption). 2. Lost-update inbl_radix/bl_free/bm_bighintleading to either exhaustion panics ("freeing free block" atsubr_blist.c:564/ "freeing already free blocks" atsubr_blist.c:642) or silent overflow of the allocator. 3. UAF reads through the destroyed old tree, leaking kernel heap contents into swap-block numbers that can then be read back. - Required config or capabilities: Root for
swapoff; the pager stressor is unprivileged. - Reachability:
swapoff /dev/<dev>while pager is active.
Proof of concept
PoC source: findings/poc/DF-0945/
Build & run
# 1. Pager stressor (unprivileged):
cc -O2 -o stress stress.c
./stress & # run several instances
# 2. Root toggles swap:
while true; do
swapon /dev/ada0s1b
sleep 0.1
swapoff /dev/ada0s1b
done
Expected output
Any one of:
- Kernel panic with "freeing free block" or "freeing already free
blocks" from
subr_blist.c(concurrent fill-vs-free). - Kernel panic dereferencing a destroyed
bl_root(UAF inblist_resizewindow β often manifests as a NULL or garbage pmap deref, or an "allocation too large" panic fromsubr_blist.c:762reading corruptbm_bighint). - Silent corruption β two processes that
malloc+dirty+read distinct anonymous regions observe each other's contents after a pageout/pagein cycle (verify by writing distinct patterns0xAAvs0xBBand checking for cross-contamination).
Increase reproducibility of the resize-window UAF by using a single
swap device and toggling swapon/swapoff on it (the destroy path at
vm_swap.c:572-573 races more tightly than resize).
Impact
- Cross-process info leak / data corruption (two pages, same swap block).
- Kernel panic via blist corruption.
- UAF reads of destroyed blist nodes leaking kernel heap into swap block numbers.
Recommended fix
Acquire vm_token around all blist mutations in swapoff_one,
matching swaponvp's protocol. Do not take vm_token across
swap_pager_swapoff() itself (it internally drops/reacquires tokens
via vm_fault_object_page and would deadlock or serialize
excessively); take it only around the blist mutations.
--- a/sys/vm/vm_swap.c
+++ b/sys/vm/vm_swap.c
@@ -535,6 +535,7 @@ swapoff_one(int index)
/*
* Prevent further allocations on this device
*/
+ lwkt_gettoken(&vm_token);
sp->sw_flags |= SW_CLOSING;
for (dvbase = SWB_DMMAX; dvbase < aligned_nblks; dvbase += SWB_DMMAX) {
blk = min(aligned_nblks - dvbase, SWB_DMMAX);
vsbase = index * SWB_DMMAX + dvbase * nswdev;
vm_swap_size -= blist_fill(swapblist, vsbase, blk);
vm_swap_max -= blk;
}
+ lwkt_reltoken(&vm_token);
/*
* Page in the contents of the device and close it.
@@ -563,6 +564,7 @@ swapoff_one(int index)
nswap = aligned_nblks * nswdev;
+ lwkt_gettoken(&vm_token);
if (nswap == 0) {
blist_destroy(swapblist);
swapblist = NULL;
vrele(swapdev_vp);
swapdev_vp = NULL;
} else {
blist_resize(&swapblist, nswap, 0);
}
+ lwkt_reltoken(&vm_token);
mtx_unlock(&swap_mtx);
return (0);
An alternative (and stronger) fix is to make vm_token the single lock
for swapblist and require it in both swaponvp and swapoff_one,
dropping the reliance on swap_mtx for blist protection.
References
sys/vm/vm_swap.c:263,388βswaponvptakesvm_tokenaround all blist ops (the correct pattern).sys/vm/swap_pager.c:536,587β swap pager ops holdvm_tokenfor blist mutations.sys/kern/subr_blist.c:320-338,564,642βblist_resizeUAF and "freeing free block" panics.
Timeline
- 2026-07-05 Discovered during automated audit.
- pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0945 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| stress.c | trigger-source | unprivileged pager stressor (slow-dirty + random-access thrash) | 2.6 KB | view raw |
| swap_toggle.sh | trigger-source | root-side swapoff/swapon loop on small secondary device | 664 B | view raw |
| run_poc.sh | trigger-source | two-device race orchestrator with cyclic pressure | 3.1 KB | view raw |
| exp3.sh | trigger-source | focused experiment with per-iteration swapoff rc tracking | 1.8 KB | view raw |
| build.sh | build-script | cc -O2 -o stress stress.c | 98 B | view raw |
| run.sh | run-script | root entry point for the race | 556 B | view raw |
| VERDICT.md | verdict | full analysis: mechanism, evidence, fix validation | 7.6 KB | β raw |
| README.md | readme | summary and reproduce instructions | 2.9 KB | β raw |
| fix.diff | suggested-fix | acquire vm_token in sys_swapoff before swap_mtx (matches swaponvp) | 871 B | view raw |
| run_baseline.log | run-log | unpatched #0 kernel: 200 iters, 6 false swap-full msgs, swap_anon grew 198931->530006 | 2.5 KB | view raw |
| fix_run.log | run-log | patched #1 kernel: 200 iters, 0 false swap-full msgs, swap stable | 1.8 KB | view raw |
| boot_run1.log | panic-signature | serial console from baseline race showing swap-full messages | 42.0 KB | view raw |
| panic.txt | panic-signature | extracted swap_pager_getswapspace swap-full messages | 293 B | view raw |
| fix_build.log | build-log | full single-fix kernel build output (rc=0) | 5.6 MB | β download |
| env.txt | environment | uname, swap config, sysctl state | 860 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0945 β swapoff_one blist race (missing vm_token)
Summary
swapoff_one() (sys/vm/vm_swap.c:441) mutates the global swapblist radix
tree via blist_fill/blist_destroy/blist_resize while holding only
swap_mtx. The swap pager (swp_pager_getswapspace/swp_pager_freeswapspace)
mutates the SAME tree via blist_allocat/blist_free while holding only
vm_token. Neither lock nests the other β concurrent radix-tree mutation β
tree corruption. swaponvp() correctly acquires vm_token before swap_mtx,
but sys_swapoff/swapoff_one do not acquire vm_token at all.
Impact
- DoS (panic/OOM): corrupted blist β false "swap full" β OOM killer, or
panic("freeing free block")on double-free. Confirmed live. - Potential cross-process info leak/corruption: double-allocation of swap blocks could map two processes' pages to the same block. Not demonstrated in this run (requires longer race or INVARIANTS-OFF kernel).
Threat model
Root initiates swapoff while an unprivileged user drives pager pressure.
The direct trigger is root-gated, but the corruption can cross privilege
boundaries if swap blocks are double-allocated across processes.
How to reproduce
Build (as maxx)
cc -O2 -o stress stress.c
Run (as root)
# Orchestrated race: creates vn1 (8MB secondary swap), launches stressor,
# waits for swap activity, then toggles vn1 swapoff/swapon 200 times.
sh run_poc.sh /dev/vbd0s1b 8 1 3250 120
# Or the focused experiment with per-iteration tracking:
sh exp3.sh 8 3250 200
Expected output (bug present, unpatched #0 kernel)
The serial console (dfbsd-qemu/boot.log) shows:
swap_pager_getswapspace: swap full allocating 16 pages
despite vm.swap_free showing ~2 GB free. This is the blist corruption
signature β blist_allocat returned SWAPBLK_NONE because the tree was
corrupted by concurrent blist_fill/blist_resize from swapoff_one.
Expected output (FIXED, patched #1 kernel)
Zero "swap full" messages during 200 toggle iterations. vm.swap_anon_use
remains stable. System stable throughout.
Files
| File | Description |
|---|---|
stress.c |
Unprivileged pager stressor (slow-dirty + random-access thrash) |
swap_toggle.sh |
Root-side swapoff/swapon loop |
run_poc.sh |
Full orchestrator (two-device race with cyclic pressure) |
exp3.sh |
Focused experiment with per-iteration swapoff rc tracking |
build.sh |
Build script |
run.sh |
Run script (root entry point) |
fix.diff |
Standalone git-apply-able fix |
VERDICT.md |
Full analysis and verdict |
run_baseline.log |
Unpatched #0 kernel race output (200 iters, 6 "swap full") |
fix_run.log |
Patched #1 kernel race output (200 iters, 0 "swap full") |
boot_run1.log |
Serial console from the baseline race |
panic.txt |
Swap-full messages extracted from boot.log |
fix_build.log |
Full single-fix kernel build output |
env.txt |
Guest environment |
DF-0945 β swapoff_one mutates global swapblist without vm_token
Verdict
REPRODUCED β locking race confirmed by code analysis and live observation of blist tree corruption (false "swap full" errors during concurrent swapoff/pager activity). Fix validated on a single-fix kernel (#1): the false "swap full" errors are eliminated at the same workload that produced them on the unpatched kernel (#0).
Mechanism (trigger β primitive β effect)
The locking bug (root cause). The global swapblist radix tree (a
blist_t in sys/kern/subr_blist.c) tracks free/allocated swap blocks. It is
mutated by two code paths under different, non-nesting locks:
-
swapoff_one()(sys/vm/vm_swap.c:441β582) β called bysys_swapoff()(root-only viacaps_priv_check_self(SYSCAP_RESTRICTEDROOT)at:410).sys_swapoffacquiresswap_mtxat:414and callsswapoff_oneat:432.swapoff_onethen re-acquiresswap_mtx(recursive,:451) and calls: -blist_fill(swapblist, ...)at:542β marks blocks as off-limits -blist_destroy(swapblist)at:572β frees the entire tree -blist_resize(&swapblist, ...)at:577β rebuilds the tree None of these are protected byvm_token. -
The swap pager β
swp_pager_getswapspace()(sys/vm/swap_pager.c:532) acquiresvm_tokenat:536and callsblist_allocat(swapblist, ...)at:537.swp_pager_freeswapspace()(:583) acquiresvm_tokenat:587and callsblist_free(swapblist, ...)at:599. These run on any CPU whenever the page daemon swaps a page out/in.
swaponvp() (vm_swap.c:248) correctly acquires vm_token at :263
BEFORE swap_mtx at :264 β establishing the lock order vm_token β swap_mtx.
But sys_swapoff/swapoff_one acquire only swap_mtx, never vm_token. The
two locks do not nest: swap_mtx does not prevent the pager (holding
vm_token) from concurrently mutating the tree, and vm_token does not prevent
swapoff_one (holding swap_mtx) from mutating it.
The primitive. Concurrent mutation of a shared radix tree without mutual
exclusion. blist_fill/blist_resize modify tree nodes (bitmaps, bighint
fields, child pointers) while blist_allocat/blist_free traverse and modify
the same nodes. Torn reads/writes produce:
- Corrupted bighint β blist_allocat skips subtrees that have free space
β returns SWAPBLK_NONE despite ample free swap β false "swap full" error.
- Corrupted bitmap β same block allocated twice β double-free panic
("freeing free block" at subr_blist.c:564).
- UAF in blist_resize β *pbl = newbl at subr_blist.c:326 swaps the
global pointer; blist_destroy(save) at :337 frees the old tree. If the
pager read the old pointer before the swap and dereferences it after the free
β UAF.
Live observation (the evidence). On the unpatched #0 kernel, a controlled
race (root toggling a small 8 MB secondary swap device via swapoff/swapon
200 times while an unprivileged user drives ~3.25 GB of anonymous paging
pressure) produced:
swap_pager_getswapspace: swap full allocating 16 pages
despite ~2 GB of swap being free (vm.swap_free was 520490 pages β 2 GB).
The swap_pager_getswapspace message (swap_pager.c:549) is printed when
blist_allocat returns SWAPBLK_NONE β i.e., the tree was corrupted such that
the allocator could not find free blocks. Concurrently, vm.swap_anon_use grew
from 198931 β 530006 pages during the toggle phase (the pager was struggling to
allocate swap due to the corrupted tree). All 200 swapoff iterations
succeeded (rc=0), confirming swapoff_one reached the blist_fill/
blist_resize code path each time.
On the patched #1 kernel (same workload: 3250 MB, 200 iterations), zero
false "swap full" messages appeared, and vm.swap_anon_use remained stable
(~48000 pages throughout).
Exploit chain (Phase 6 assessment)
Threat model. The race requires root to initiate swapoff while an
unprivileged user drives memory pressure. The direct trigger is root-gated.
However, the corruption outcome (double-allocation of swap blocks β two
processes' pages mapped to the same swap block) could cross a privilege
boundary: if the unprivileged user's swapped-out page collides with another
user's swapped page, that's cross-process data corruption / info leak.
On GENERIC (INVARIANTS ON). The blist_free code has unconditional panics
(panic("freeing free block") at subr_blist.c:564, panic("freeing
already...") at :642) that fire on double-free. These catch the
double-allocation outcome as a DoS panic. The more common manifestation (this
run) is corrupted bighint β false "swap full" β cascading OOM. Both are DoS.
Cross-process info leak (outcome 1). For a silent double-allocation (no
KASSERT trip) to produce a cross-process info leak, the bitmap corruption
would need to clear a bit that should be set, allowing blist_allocat to
return an already-allocated block. The corrupted block would then be shared by
two swap pages from different processes. When one process reads its page, it
gets the other process's data. This is theoretically achievable but was not
demonstrated in this run β the bighint corruption (false "swap full")
dominated over bitmap corruption (double-allocation). Demonstrating the
cross-process leak would require either: (a) running the race for much longer
to hit a bitmap corruption, or (b) using the noinv kernel (INVARIANTS OFF)
where the panic checks don't fire, allowing the corruption to accumulate
silently. This is labeled impact: dos (panic/OOM) with a noted potential for
cross-process corruption.
uid0 escalation. Not pursued β the primitive is radix-tree corruption in
the swap allocator, not a kernel heap/stack write that can be groomed into a
credential overwrite. The realistic impact ceiling is DoS (panic or OOM) and
potential cross-process info leak/corruption, not direct uid=0.
PoC changes
stress.cβ completely rewritten. Original seeded version used small (256 MB) chunks with fastmemset, which did not trigger any swap activity on this 4 GB-RAM guest (DragonFly's page daemon didn't react before the pages were freed). Rewritten to allocate a large (~3.25 GB) anonymous region and dirty it SLOWLY (sequential, ~1 page/Β΅s with periodicusleep), giving the page daemon time to push pages to swap. Then random-access thrashing drives continuousblist_allocat/blist_free.swap_toggle.shβ fixed device name from FreeBSD/dev/ada0s1bto DragonFly/dev/vbd0s1b. Changed to toggle a SMALL secondary device (vn1, 8 MB vnode-backed file) instead of the main device β the main device'sswapoffneeds to page in gigabytes of data and returnsENOMEMunder pressure, never reachingblist_fill. The small device'sswapoffsucceeds (reachesblist_fill/blist_resize) while the pager races on the same global tree.run_poc.shβ new orchestrator: creates the secondary swap device viavnconfig/swapon, launches the stressor asmaxx, waits for swap activity, then togglesvn1in a tight loop withswapoffrc tracking.exp3.shβ focused single-experiment script with per-iterationswapoffreturn-code tracking and swap-state logging to/root/(persistent across OOM).
How to reproduce
# On the guest as root:
sh /root/run_poc.sh /dev/vbd0s1b 8 1 3250 120
# Or the focused experiment:
sh /root/exp3.sh 8 3250 200
Then check the serial console (dfbsd-qemu/boot.log) for:
swap_pager_getswapspace: swap full allocating 16 pages
appearing despite vm.swap_free showing ~2 GB free. This is the blist
corruption signature.
Fix verification
fixedVALIDATED the fix: the unpatched #0 baseline produced 6 false 'swap_pager_getswapspace: swap full allocating 16 pages' messages during 200 swapoff/swapon iterations with 3250MB pager pressure (blist tree corruption from concurrent mutation, ~2GB swap was free), and vm.swap_anon_use grew 198931->530006 (pager struggling). The single-fix #1 kernel (lwkt_gettoken(&vm_token) added to sys_swapoff) produced ZERO such messages at the same workload, and vm.swap_anon_use remained stable (~48000 pages). The fix eliminates the concurrent blist mutation by ensuring swapoff_one holds vm_token, matching swaponvp's lock order.
baseline #0 (unpatched): swap_anon_use 198931->530006, 6x 'swap full allocating 16 pages', swap_free depleted 851565->520490. patched #1 (fixed): swap_anon_use 49822->47950 (stable), 0x 'swap full' messages, swap_free stable 1000674->1002546.
Confirmed kernel references
- sys/vm/vm_swap.c:414
- sys/vm/vm_swap.c:432
- sys/vm/vm_swap.c:441
- sys/vm/vm_swap.c:451
- sys/vm/vm_swap.c:542
- sys/vm/vm_swap.c:572
- sys/vm/vm_swap.c:577
- sys/vm/vm_swap.c:263
- sys/vm/swap_pager.c:532
- sys/vm/swap_pager.c:536
- sys/vm/swap_pager.c:537
- sys/vm/swap_pager.c:549
- sys/vm/swap_pager.c:583
- sys/vm/swap_pager.c:587
- sys/vm/swap_pager.c:599
- sys/kern/subr_blist.c:294
- sys/kern/subr_blist.c:321
- sys/kern/subr_blist.c:326
- sys/kern/subr_blist.c:337
- sys/kern/subr_blist.c:564
Detail
Exploit chain
The primitive is concurrent radix-tree mutation of the global swapblist (not a kernel heap/stack write), so there is no slab-grooming -> credential-overwrite path to uid=0. The realistic impact is DoS: (1) corrupted bighint -> false 'swap full' -> cascading OOM (confirmed live), and (2) potential 'panic(freeing free block)' at subr_blist.c:564 if the race causes a double-allocation followed by a double-free (not observed in this run, but the code path exists). A cross-process info leak (double-allocation mapping two processes' pages to the same swap block) is theoretically possible if bitmap corruption occurs instead of bighint corruption, but was not demonstrated. No escalation chain file was written because the primitive class (radix-tree corruption in the swap allocator) does not lend itself to credential forgery.
Evidence (decisive lines)
UNPATCHED #0 baseline (200 swapoff/swapon iterations, 3250MB pager pressure): PRE-TOGGLE: swap_anon_use=198931 swap_free=851565 free=12007. POST-TOGGLE: swap_anon_use=530006 swap_free=520490 free=10800. boot.log: swap_pager_getswapspace: swap full allocating 16 pages (x4), swap_pager: out of swap space (x2) [false swap-full despite ~2GB free swap = blist tree corruption]. PATCHED #1 kernel (same workload, 200 iterations): PRE-TOGGLE: swap_anon_use=49822 swap_free=1000674 free=14274. POST-TOGGLE: swap_anon_use=47950 swap_free=1002546 free=15315. boot.log: 0 new swap-full messages [fix eliminates the concurrent mutation].
PoC changes
Completely rewrote stress.c: the seeded version used small 256MB chunks with fast memset, which triggered ZERO swap activity on the 4GB-RAM guest. Rewritten to allocate ~3.25GB and dirty it SLOWLY (sequential, ~1 page/us with usleep), giving the page daemon time to push pages to swap, then random-access thrash. Fixed swap_toggle.sh device name from FreeBSD /dev/ada0s1b to DragonFly /dev/vbd0s1b. Changed the toggle target from the main 4GB device (whose swapoff returns ENOMEM under pressure, never reaching blist_fill) to a small 8MB vnode-backed secondary device (vn1) whose swapoff succeeds while still calling blist_fill/blist_resize on the global tree. New run_poc.sh orchestrator.
Verified recommended fix
Acquire vm_token in sys_swapoff() BEFORE swap_mtx (matching swaponvp()'s lock order at vm_swap.c:263-264), and release it after mtx_unlock in the done: label. This ensures swapoff_one() (called from sys_swapoff) holds vm_token when it calls blist_fill/blist_destroy/blist_resize on swapblist, serializing against the pager's blist_allocat/blist_free which also hold vm_token. The fix is a 2-line change (lwkt_gettoken + lwkt_reltoken) with a documenting comment. Supersedes finding proposal. Full diff in findings/poc/DF-0945/fix.diff.
Verdict
REPRODUCED. The locking race is confirmed by line-by-line code analysis: swapoff_one() (sys/vm/vm_swap.c:441-582) holds only swap_mtx while calling blist_fill (:542), blist_destroy (:572), blist_resize (:577) on the global swapblist radix tree. The pager (swp_pager_getswapspace at swap_pager.c:532/:536 and swp_pager_freeswapspace at :583/:587) holds only vm_token while calling blist_allocat/blist_free on the SAME tree. Neither lock nests the other (swaponvp at vm_swap.c:263 correctly takes vm_token THEN swap_mtx; sys_swapoff at :414 takes only swap_mtx). Live reproduction on the unpatched #0 kernel: root toggled a small 8MB secondary swap device 200 times (all 200 swapoff reached blist_fill/blist_resize, rc=0) while unprivileged user maxx drove 3.25GB of anonymous paging pressure. During the toggle phase, 'swap_pager_getswapspace: swap full allocating 16 pages' appeared in the serial console DESPITE ~2GB of swap being free (vm.swap_free=520490 pages). This false-positive swap exhaustion is the signature of blist tree corruption from concurrent radix-tree mutation: blist_allocat returned SWAPBLK_NONE because bighint fields were corrupted by the racing blist_fill/blist_resize. Concurrently vm.swap_anon_use grew 198931->530006 (pager struggling with the corrupted tree).
No comments yet.