sp->sw_nblks is a 32-bit int but swaponvp() stores a 64-bit block count β swap devices >= 2^31 pages (8 TiB) truncate to negative/garbage bounds while vm_swap_size still counts the full size
| Field | Value |
|---|---|
| ID | DF-2880 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-197 Integer Overflow or Wraparound |
| File | sys/vm/vm_swap.c |
| Lines | 344, 367 (struct conf.h:181) |
| Area | vm |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:vm |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
struct swdevt.sw_nblks is int while aligned_nblks is swblk_t=int64_t;
the assignment silently truncates. The :344 guard only bounds nblks by
BLIST_MAXBLKS/nswdev = 2^59 pages β no guard for real hardware. An β₯8TiB
swap device (trivially creatable, incl. sparse NFS file) makes sw_nblks
negative/wrapped: swapdev_strategy then rejects all I/O to the device
while nswap/vm_swap_size/vm_swap_max still count petabytes of
"available" swap β kernel overcommits against a device that can never
take a page, degrading into swap-full/OOM-kill churn; blist_create for
nswapβ₯2^33 additionally attempts a multi-GB M_WAITOK kmem allocation.
Root-only and β₯8TiB, hence Low; no OOB enabled (truncation only makes
the bound more restrictive). Fix: widen sw_nblks/sw_nused to swblk_t or
reject nblks > INT_MAX pages at registration.
Timeline
- 2026-09-02 Discovered during pass-2 audit of vm_swap.c (GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2880 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| run.log | β | 1.1 KB | view raw | |
| memhog.c | β | 384 B | view raw | |
| fix.diff | β | 459 B | view raw |
Fix verification
fixedint64 sw_nblks: 8TiB swapon accounts fully and vn0s0 accepts pageouts with ZERO EINVAL console spam under the same memory pressure that killed the stock kernel (baseline: endless error 22 pageout failures).
df2880_ab2.log: no I/O error lines; watermarks 85941283/128911925
Confirmed kernel references
Detail
Exploit chain
8TiB swap file -> swapon accepted (ineffective BLIST bound) -> sw_nblks int truncates to INT_MIN -> swapdev_strategy EINVALs every pageout (error-22 spam) while vm.swap_size counts 8TiB -> swap-death wedge
Evidence (decisive lines)
df2880_baseline.log: error-22 spam + Used=0 on vn0s0 under pressure + 8TiB accounting; df2880_ab2.log: patched kernel zero I/O errors
PoC changes
swapon on a regular file refused (Block device required) - used vnconfig on a sparse file instead
Verified recommended fix
--- sys/sys/conf.h 2026-06-29 12:51:19.000000000 +0000
Verdict
Reproduced with an 8TiB sparse swap file (vnconfig + swapon) on the stock kernel. struct swdevt.sw_nblks is int (sys/sys/conf.h:181) while aligned_nblks is swblk_t (int64); the :344 BLIST_MAXBLKS/nswdev check is ineffective because BLIST_MAXBLKS with 64-bit swblk_t is 0x2000000000000000 - the 8TiB device (2^31 pages) was accepted and the global blist resized in ~33s. sw_nblks truncated to INT_MIN: pstat displays the size as |INT_MIN|x8 pages unsigned, vm.swap_size counted the full 8TiB (2148532096 pages - overcommit against a device that can never take a page), and under memory pressure swapdev_strategy rejected EVERY pageout to the device with EINVAL - the serial console filled with thousands of swap_pager: I/O error - pageout failed; ... error 22 lines and the guest wedged in swap-death (pageout spinning on rejected I/O) while the real 4GB device took pages. Fix A/B on the patched kernel (int64 sw_nblks): the same 8TiB swapon produced full accounting (watermarks 85941283/128911925), ZERO I/O-error lines under the same pressure, and vn0s0 accepted pageouts (the guest thrashed normally on file-backed swap instead of wedging on rejected I/O).
No comments yet.