swap_pager_getpage() 20-second timeout diagnostic dereferences the completed-and-released pbuf (use-after-free read in console path)
| Field | Value |
|---|---|
| ID | DF-2701 |
| Status | new |
| Severity | Info |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L |
| CWE | CWE-672 / CWE-416 |
| File | sys/vm/swap_pager.c |
| Lines | 1494-1496 (invalidation), 1527-1535 (derefs), 2099-2107 (relpbuf) |
| Area | vm |
| Confidence | likely |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
swap_pager_getpage() declares bp invalid after vn_strategy() because
swp_pager_async_iodone() releases it via relpbuf() on completion. Yet
when the hz*20 tsleep times out, the diagnostic kprintf dereferences
bio->bio_offset and bp->b_bcount. If the stalled I/O completes in
the window between the timeout wakeup and the dereference, the pbuf has
been freed to the pbuf zone and may already be reused β the kprintf
then reads and prints freed/reused kernel memory. Impact confined to a
console/dmesg diagnostic (values only); triggering additionally
requires a swap read stalled >20s plus a microsecond-scale race.
Recommended fix
Snapshot the two values before dispatch and print the locals (diff in the row above / findings/poc/DF-2701/).
Timeline
- 2026-08-30 Discovered during pass-2 audit of swap_pager.c (GLM 5.3); negative-result validation (400 swap-alloc records) run live.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2701 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.5 KB | β raw | |
| VERDICT.md | β | 4.0 KB | β raw | |
| verdict.json | β | 2.5 KB | view raw | |
| hog.c | β | 621 B | view raw | |
| swap_allocs.txt | β | 8.7 KB | view raw |
DF-2701 β swap_pager_getpage() timeout diagnostic dereferences a pbuf that completion may already have freed
Status: untested (Info severity β below the verification threshold; mechanism is certain, trigger needs a >20 s stalled swap I/O)
Build / run (of the accompanying negative-result validation, which WAS run)
# on the guest, as root: sysctl vm.report_swap_allocs=400 cc -O2 -o /tmp/hog hog.c && /tmp/hog # ~5 GB touch storm on a 4 GB box dmesg | grep swap_alloc > swap_allocs.txt # 400 (blk, n) allocation records
Expected (validation, observed): 400 allocations, 268 at non-16-aligned offsets (heavy fragmentation), 0 runs crossing a SWB_DMMAX stripe boundary, 40 runs ending exactly on a stripe boundary (live fire of known DF-0948's trigger), all runs confined to interleave slot 0 (single active swap device).
The finding itself (not reproduced; Info)
sys/vm/swap_pager.c:1486-1490 states bp is invalid after vn_strategy().
sys/vm/swap_pager.c:1527-1535 violates that invariant: when the 20 s
tsleep() times out, the kprintf dereferences bio->bio_offset and
bp->b_bcount β if the I/O completes in the window between timeout wakeup and
the kprintf, swp_pager_async_iodone() has already relpbuf()'d the bp and it
may have been reused by any other swap I/O. UAF-read confined to a console
diagnostic.
Fix sketch
Save bio_offset and b_bcount into locals before vn_strategy() (alongside
blk/i) and print those.
DF-2701 VERDICT β untested (Info)
What was found
swap_pager_getpage() (sys/vm/swap_pager.c) dispatches its read with
vn_strategy(swapdev_vp, bio) at :1496 and its own comment at :1486-1490
declares bp invalid from that point ("bp cannot be considered valid after this
point because we automatically release it on completion"). Nevertheless, when
the 20-second tsleep() at :1527 times out, the diagnostic kprintf at
:1528-1535 dereferences bio->bio_offset (:1533) and bp->b_bcount
(:1534). Completion (swp_pager_async_iodone β relpbuf, :2099-2107) may run
in the window between the timeout wakeup and the dereference, freeing the pbuf
back to the pbuf zone where it can be immediately reused by any other swap
I/O. The deref therefore reads freed/reused kernel memory and prints it to the
console. This is a UAF read only; no control flow depends on the values.
Why Info and not higher: the trigger requires a swap read stalled for >20 s (dead/hung disk, NFS swap hiccup) and completion landing inside a microsecond-scale race window; the impact is bogus numbers in a console message. No path to disclosure (console/dmesg), corruption, or control flow.
Why not verified: per contract, Low/Info hardening findings are not PoC'd, and manufacturing a 20 s-stalled swap device plus winning the race window is not a meaningful use of the single-tenant guest. Mechanism is certain from source; rating "likely" only for the untested race window.
What was verified instead (negative-result validation for this pass)
The pass-2's central analytical claim β that blist allocations can NEVER cross a SWB_DMMAX(64-page) stripe, because BLIST_BMAP_RADIX == 64 == SWB_DMMAX and blst_leaf_alloc() serves runs from a single 64-aligned leaf β was validated live on the guest:
sysctl vm.report_swap_allocs=400(swap_pager.c:1704-1707), then a ~5 GB anonymous touch storm (hog.c) on the 4 GB guest forced real pageout to the 4 GB swap device.- 400
swap_alloc %08jx,%drecords captured (swap_allocs.txt): - 268 allocations at non-16-aligned offsets (real fragmentation),
- 0 runs with blk%64 + n > 64 (no stripe crossing),
- 40 runs with blk%64 + n == 64 β the exact-boundary case where the
putpages stripe trim (:1713-1717) fires with
n - j == 0: known DF-0948's trigger observed live 40 times (cross-evidence, not a new finding), - all 400 runs inside interleave slot 0 (single active device), consistent with the swaponvp interleave geometry.
- Source proof of the geometry: sys/kern/subr_blist.c
blst_meta_allocrequirescount <= child radix(panics otherwise) and serves the entire run from one child; children partition[blk, blk+radix)consecutively from 0; leaves therefore cover 64-aligned linear spans == stripes.
This killed the strongest candidate of the pass (cross-device sw_nused
mis-accounting via stripe-crossing allocations β permanent swapoff EINTR):
swapacctspace()'s single-device assumption (sys/vm/vm_swap.c:592-601) is
upheld by blist geometry, not luck.
Environment
DragonFly dfbsd 6.5-DEVELOPMENT #0 (X86_64_GENERIC), 4 GB RAM, 4 GB /dev/vbd0s1b swap, stock INVARIANTS kernel, guest up and clean (hog exited, swap use back to 0).
Recommended fix
--- a/sys/vm/swap_pager.c
+++ b/sys/vm/swap_pager.c
@@ -1490,6 +1490,8 @@ swap_pager_getpage(vm_object_t object, vm_pindex_t pindex,
bp->b_cmd = BUF_CMD_READ;
BUF_KERNPROC(bp);
+ /* snapshot for the timeout diagnostic: bp is invalid after strategy */
+ off_t save_offset = bio->bio_offset;
+ long save_bcount = bp->b_bcount;
vn_strategy(swapdev_vp, bio);
@@ -1528,9 +1530,8 @@ swap_pager_getpage(vm_object_t object, vm_pindex_t pindex,
kprintf(
"swap_pager: indefinite wait buffer: "
" bp %p offset: %lld, size: %ld "
" m=%p busy=%08x flags=%08x\n",
bp,
- (long long)bio->bio_offset,
- (long)bp->b_bcount,
+ (long long)save_offset,
+ (long)save_bcount,
mreq, mreq->busy_count, mreq->flags);
(Diff authored post-analysis; never applied to sys/.)
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['sys/vm/swap_pager.c:1486-1490 (bp invalid after vn_strategy)', 'sys/vm/swap_pager.c:1514-1537 (timeout loop)', 'sys/vm/swap_pager.c:1527-1535 (stale bp/bio deref in kprintf)', 'sys/vm/swap_pager.c:2099-2108 (relpbuf on completion)', 'findings/poc/DF-2701/swap_allocs.txt (400 swap_alloc records: 0 stripe crossings under heavy fragmentation; 40 exact-boundary DF-0948 trigger fires)']
PoC changes
none; finding left untested (Info). hog.c/swap_allocs.txt are the negative-result validation artifacts reused from DF-2672's storm pattern.
Verified recommended fix
Snapshot bio_offset/b_bcount into locals before vn_strategy() and print the locals in the timeout diagnostic.
Verdict
Info-severity use-after-free READ confined to the 20-second-timeout diagnostic kprintf in swap_pager_getpage(): after vn_strategy() the bp must be considered freed (relpbuf in swp_pager_async_iodone), yet the timeout path dereferences bio->bio_offset and bp->b_bcount. If completion races the timeout wakeup, freed/reused pbuf memory is read and printed to console. Not verified: triggering requires a swap I/O stalled >20 s and the impact is console output only. The same run's guest validation (findings/poc/DF-2701/swap_allocs.txt, 400 allocations) proved the pass-2 central negative claim: blist never returns runs crossing a SWB_DMMAX stripe (leaf radix 64 == SWB_DMMAX, source-proven in sys/kern/subr_blist.c blst_meta_alloc/blst_leaf_alloc), which killed the candidate cross-device sw_nused mis-accounting finding, and captured 40 live fires of known DF-0948's exact-stripe-boundary trim trigger.
No comments yet.