# 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`:

```c
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.
