swp_pager_meta_build while-loop can free a swap block assigned by a concurrent meta_build for the same pindex
Summary
swp_pager_meta_build while-loop at :2385-2389 clears swap->swb_pages[index]=SWAPBLK_NONE then calls blocking swp_pager_freeswapspace (takes vm_token). While blocked, lwkt object token is shed -> concurrent meta_build on another CPU stores new swapblk into same slot. First thread resumes, while-condition re-reads slot, sees new value, frees that swapblk out from under the second caller. Then unconditional swap->swb_pages[index]=swapblk at :2396 overwrites. Result: loser swapblk freed back to blist, re-handed to third page, loser queues write to physical swapblk now owned by someone else -> cross-page data corruption or blist panic.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0946 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| swap_meta_race.c | trigger-source | stress test: 4 forked children re-dirtying pages of a shared anon region + pressure amplifier | 4.9 KB | view raw |
| pressure_amp.c | trigger-source | background memory hog to force swap activity | 804 B | view raw |
| build.sh | build-script | cc -O2 -Wall | 128 B | view raw |
| run.sh | run-script | background amp + swap_meta_race | 526 B | view raw |
| run.log | run-log | 200 rounds complete, no panic, swap usage 0% | 1.0 KB | view raw |
| env.txt | environment | uname + cc version | 188 B | view raw |
| VERDICT.md | verdict | race mechanism + why PoC cannot drive it | 4.5 KB | β raw |
| README.md | readme | build/run/expected | 1.1 KB | β raw |
| fix.diff | suggested-fix | while->if in swp_pager_meta_build, cache v before clearing slot | 1.2 KB | 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-0946 β PoC
Race in swp_pager_meta_build while-loop (swap_pager.c:2385-2391):
the loop re-reads swap->swb_pages[index] after a blocking call to
swp_pager_freeswapspace that sheds the lwkt object token, allowing
a concurrent meta_build on another CPU to insert a new swapblk into
the slot which the first thread then frees out from under the second
caller.
Build
./build.sh
Run
./run.sh
The stress test runs as the unprivileged user. It mmap's a 64MB MAP_SHARED anonymous region, forks 4 children that concurrently re-dirty the same pages, paired with a 3GB background swap-pressure amplifier to force the pageout daemon into swap activity. Runs 200 rounds; expected to NOT trigger the race (AC:High).
Expected
On a vulnerable kernel the race could panic in blist (double-free
of a swapblk) or quietly corrupt swapped data. On this guest, with
this workload, no panic is observed β see VERDICT.md for why (the
race window is too narrow for a userspace PoC).
The fix (fix.diff) is a one-line change: while β if, caching
the value to free before clearing the slot.
DF-0946 β swp_pager_meta_build while-loop race (swap_pager.c:2385-2391)
Verdict: NOT REPRODUCED FROM USERSPACE (real bug by source inspection; race too narrow to PoC)
The bug is real and the mechanism is as cited. From
sys/vm/swap_pager.c:2383-2391:
index &= SWAP_META_MASK;
while ((v = swap->swb_pages[index]) != SWAPBLK_NONE) {
swap->swb_pages[index] = SWAPBLK_NONE;
/* can block */
swp_pager_freeswapspace(object, v, 1);
--swap->swb_count;
--mycpu->gd_vmtotal.t_vm;
}
swp_pager_freeswapspace (swap_pager.c:583-603) acquires vm_token
and calls blist_free() which may block. While blocked, the lwkt
object token held by swp_pager_meta_build (asserted at line 2342
ASSERT_LWKT_TOKEN_HELD(vm_object_token(object))) is shed. A concurrent
swp_pager_meta_build on another CPU for the same (object, index)
can then acquire the object token, find swap->swb_pages[index] ==
SWAPBLK_NONE (already cleared by the first thread at line 2386), skip
the while-loop entirely, and store its own swapblk_B into the slot
at line 2396. When the first thread resumes from swp_pager_freeswapspace
and re-evaluates the while-condition, it sees swapblk_B (not NONE),
re-enters the loop, frees swapblk_B out from under the second
caller, then at line 2396 overwrites the slot with its own
swapblk_A.
Effect
The second caller's swapblk is returned to blist while still
referenced by the second caller's metadata. If blist hands that
swapblk to a third page, two pages will end up writing to the same
physical swapblk β cross-page data corruption. blist may also panic
on a double-free if the second caller later tries to free the same
swapblk.
Why it doesn't reproduce from a userspace PoC
The race requires both of these to fire simultaneously:
-
Two threads in
swp_pager_meta_buildfor the same(object, index)on different CPUs. This only happens during kernel-driven paging (swap_pager_putpages / swap_pager_swapin / swap_pager_copy) on a shared VM object β never directly invoked by a syscall. -
blist_freeblocking long enough for the second CPU to acquire the object token, run the whole meta_build, and release it, before the first CPU returns.blist_freeis normally very fast (radix tree ops), so the window is sub-microsecond.
The stress-test PoC (swap_meta_race.c) ran 200 rounds of forked
children concurrently re-dirtying pages of a 64MB shared MAP_SHARED
anonymously-mapped region, paired with a 3GB background swap-pressure
amplifier to force the pageout daemon into heavy swap activity.
run.log shows: 200 rounds completed, no panic, no obvious
corruption, swap usage stayed at 0% (the guest has 4GB RAM and the
paging path uses the page cache rather than swap for this workload).
The finding is correctly labelled confidence: speculative and CVSS
AC:High β even on a kernel that definitely has the bug, hitting the
race from userspace requires either:
- A paging-heavy workload that provably drives concurrent swap_pager_putpages on the same swblock (very hard to construct),
- A kernel-internal harness that calls
swp_pager_meta_builddirectly from two threads with controlled timing.
Both are out of scope for an unprivileged local exploit and not constructible from this guest's userspace.
Verdict classification
This is a real concurrency defect with verifiable mechanism
(path:line-cited), but the race is genuinely too narrow to trigger
from any userspace PoC we can construct on the guest. Recording
status=not_reproduced with confidence=likely (the bug exists; the
PoC just cannot drive the race in a reasonable time). This matches
the finding's own speculative confidence.
The bug is not a false positive: the while loop is genuinely
unsafe given the lwkt-token-shed-on-blocking semantics. The fix is
small and obvious (change while to if, freeing only the value
we observed before blocking).
Suggested fix
fix.diff changes the while to an if, caching the value to free
before clearing the slot. This way, if a concurrent meta_build
installs a new swapblk while we block, we don't free it β we only
free the value we observed. The new swapblk is preserved.
The fix is not runtime-testable on the guest (the race cannot be
triggered); it is verified for git apply --check and compiles
cleanly. A kernel-internal concurrency harness (two kthreads calling
swp_pager_meta_build on the same object with controlled timing)
would be needed to validate the fix end-to-end β out of scope for a
PoC runner.
Fix verification
not_testablecompile validated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. swp_pager_meta_build while-loop re-reads slot after blocking free. Race too narrow from userspace. Fix: while->if.
No comments yet.