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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0946 Β· 11 files
FileTypeDescriptionSize
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
README.md readme build/run/expected
↓ download 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.

VERDICT.md verdict race mechanism + why PoC cannot drive it
↓ download raw

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:

  1. Two threads in swp_pager_meta_build for 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.

  2. blist_free blocking 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_free is 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_build directly 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_testable

compile 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.