Skip to content

bcachefs: swap file support via SWP_FS_OPS#1072

Draft
matthiasgoergens wants to merge 11 commits into
koverstreet:masterfrom
matthiasgoergens:swap-files
Draft

bcachefs: swap file support via SWP_FS_OPS#1072
matthiasgoergens wants to merge 11 commits into
koverstreet:masterfrom
matthiasgoergens:swap-files

Conversation

@matthiasgoergens

@matthiasgoergens matthiasgoergens commented Mar 2, 2026

Copy link
Copy Markdown

Add swap file support for bcachefs using the SWP_FS_OPS path (same
mechanism as NFS swap). The filesystem stays in the I/O loop for swap
operations, so swap data gets checksumming, encryption, replication,
compression, and multi-device support.

Unlike btrfs (which disables COW, checksums, and compression for swap
files), bcachefs swap files use the normal COW write path.

How to use

fallocate -l 4G /mnt/bcachefs/swapfile
chmod 600 /mnt/bcachefs/swapfile
mkswap /mnt/bcachefs/swapfile
swapon /mnt/bcachefs/swapfile

Key design decisions

  • PF_MEMALLOC context: swap_rw sets PF_MEMALLOC for both reads and
    writes to prevent reclaim re-entry deadlocks. The BCH_WRITE_swap flag
    propagates this to the write index kworker.

  • Btree node pinning: at swapon, leaf nodes covering the swap file are
    marked noevict to avoid disk reads during memory reclaim.

  • Btree cache pre-reserve: 16 MB of btree node buffers pre-allocated at
    swapon (with GFP_NORETRY to avoid OOM on small VMs).

  • Disk reservation: swap_pages × PAGE_SECTORS reserved at swapon to
    prevent ENOSPC during COW writes.

  • Bkey buffer pre-allocation: 2048-byte buffer allocated with GFP_NOWAIT
    before entering PF_MEMALLOC, avoiding __GFP_NOFAIL WARN loops in
    bch2_bkey_buf_realloc.

  • Write-buffer flush noreclaim: upgraded PF_MEMALLOC_NOFS to
    PF_MEMALLOC in the write-buffer flush path. The flush holds
    wb->flushing.lock while traversing btree nodes; if a node allocation
    enters direct reclaim, reclaim can need the journal, which needs
    write-buffer flush → deadlock. This is a pre-existing bcachefs bug
    not specific to swap, but swap makes it reliably reproducible.

  • IS_SWAPFILE bypass: swap files skip pagecache locking in the direct I/O
    path.

  • 80% fill monitoring: rate-limited warning when extents btree leaves
    exceed 80% fill, for early warning before the 67% split threshold.

Test results (22-test ablation matrix)

18/22 PASS — 14/16 core tests pass, 2 expected failures, 2 throttle
timeouts.

Tested at 128–512M RAM covering: pinning on/off, PF_MEMALLOC on/off, btree
cache thrashing, drop_caches, swapoff/swapon cycles, concurrent file I/O
workload, 5-minute sustained pressure, and disk-throttled I/O.

Expected failures (prove hardening is necessary):

  • small-128m-nothing: all hardening disabled → deadlock
  • small-128m-noreclaim: PF_MEMALLOC disabled → reclaim deadlock

Throttle timeouts (test harness issue, not kernel bugs):

  • small-192m-throttled: I/O throttled to 512 KB/s, test times out
  • large-192m-throttled: same

Design document

Full design rationale in Documentation/filesystems/bcachefs/swap.rst,
including dead ends (pre-fragmentation defeated by extent merging,
GFP flag whack-a-mole) and adversarial analysis (dead-key accumulation,
shared btree nodes, ENOSPC).

Closes #368

@matthiasgoergens matthiasgoergens force-pushed the swap-files branch 3 times, most recently from d0ab5e4 to 5587b10 Compare March 2, 2026 03:12
Add investigation notes, audit results, and a QEMU-based reproducer for
the SRCU-held-across-blocking-operations deadlock.

The reproducer boots a 128 MB VM with pre-populated tiered bcachefs
(SSD -> HDD), eats 200 MB of memory to force heavy reclaim, and checks
whether the system stays responsive while the reconcile thread runs.

  prepare-vm-disks.sh  - format + pre-populate disk images on the host
  build-initramfs.sh   - build static musl Rust init (~430 KB)
  run-vm-test.sh       - run test VM, analyze results
  vm-init-rs/          - Rust init binary (pedantic clippy clean)
  INVESTIGATION.md     - crash analysis, full audit, GitHub issue survey
Audit and fix 25 sites where bch2_trans_unlock() (which only drops btree
locks) is used before a potentially long-blocking operation, leaving the
SRCU read lock held.  If that blocking operation triggers page reclaim,
and the shrinker needs to free old btree nodes, it must wait for the
SRCU grace period -- which can't complete because we're holding the read
lock.  This creates a reclaim deadlock under memory pressure.

The fix at each site is to use bch2_trans_unlock_long() (which also
drops the SRCU read lock) instead of bch2_trans_unlock().

Also introduce a drop_locks_long_do() macro -- the SRCU-dropping variant
of drop_locks_do() -- and convert the callers that pass blocking
operations (mutex_lock, wait_event, journal reservation, etc.).

Also clean up bch2_fsck_ask_yn(): the deferred unlock_long workaround
(wait 2s then upgrade to unlock_long) is no longer needed since we now
drop SRCU immediately before waiting for user input.

Sites fixed:

  btree/cache.c        4  GFP_KERNEL allocs, sync I/O, wait_on_bit_io
  btree/commit.c       3  GFP_KERNEL alloc, journal reclaim wait, journal res
  btree/interior.c     5  mutex, closure_sync, wait_event, down_read
  btree/locking.c      1  mutex_lock
  btree/read.c         1  sync disk I/O
  alloc/foreground.c   1  mutex_lock
  data/update.c        3  GFP_KERNEL bio alloc, closure_sync (x2)
  data/ec/io.c         1  kvmalloc + multi-device I/O
  data/migrate.c       1  closure_wait_event for in-flight writes
  data/write.c         1  nocow lock + GFP_KERNEL
  vfs/buffered.c       1  filemap_alloc_folio + GFP_KERNEL
  vfs/fs.c             1  __wait_on_freeing_inode
  init/error.c         1  user input wait (+ cleanup)
  debug/tests.c        1  journal flush

Reported on a 128 GB desktop (bcachefs root, tiered SSD+HDD): the
reconcile thread held SRCU for 13+ seconds during GFP_KERNEL bio
allocation in bch2_data_update_init(), blocking memory reclaim and
causing a full system deadlock.

Related: koverstreet#934
Related: koverstreet#636

Signed-off-by: Matthias Goergens <matthias.goergens@gmail.com>
@matthiasgoergens

Copy link
Copy Markdown
Author

I'm using this branch on my desktop now. It's working quite well so far.

@koverstreet

Copy link
Copy Markdown
Owner

A few things:

Housekeeping:

  • This PR includes all the changes from bcachefs: don't hold srcu lock across blocking operations #1069 (the SRCU unlock_long audit). Those should be a separate commit/PR, not bundled here. Please rebase on top of bcachefs: don't hold srcu lock across blocking operations #1069 or drop the duplicate hunks.
  • There are a lot of files that shouldn't be in a kernel PR: INVESTIGATION.md, plan.markdown, review-findings-*.markdown, writeup.markdown, build-initramfs.sh, prepare-vm-disks.sh, run-vm-test.sh, the vm-init-rs/ Rust binary, spinoff-gfp-noretry.markdown, walkthrough.md, pr-description.markdown. These are useful investigation notes but they don't belong in the kernel tree. Remove them from the PR (keep them in your fork if you like).
  • module-version.c / version.h with a hardcoded version string — this doesn't belong.
  • pr_info("bcachefs: custom swap code loaded\n") in init/fs.c — remove.

The swap implementation itself:

The SWP_FS_OPS approach is the right one. Some notes on the implementation:

  • The BUG_ON / BUG() calls after stall detection (10s in swap_rw, 10s in write index worker) should not be in production kernel code. A WARN_ON_ONCE is fine for diagnostics, but BUG() during swap I/O under memory pressure will make a recoverable situation into a kernel panic. Remove these.

  • Pinning the entire alloc btree (POS_MIN to SPOS_MAX) in bch2_swap_pin_unpin_nodes is extremely aggressive. On a filesystem with many devices and large allocation spaces, that's potentially thousands of btree nodes pinned in memory permanently. This needs to be scoped more carefully — perhaps just the allocation ranges for the devices that back the swap file's data.

  • The bch2_swap_noreclaim_enabled global bool checked in the hot write path (data/write.c) is a code smell. Feature toggles via __setup are OK for debugging but shouldn't be the permanent interface. If the noreclaim behavior is correct (and it probably is), just do it unconditionally for swap writes.

  • The scoped_guard(memalloc_flags, PF_MEMALLOC_NOFS|PF_MEMALLOC) change in btree/write_buffer.c affects all write buffer flushes, not just swap. That needs more thought — you're saying no write buffer flush should ever enter direct reclaim, which may be true but it's a broader change than "swap support."

  • The debug spew in btree/commit.c (logging when extents leaf is at 80% fill) should be behind a debug flag or removed.

Testing:

You've built your own QEMU test framework — nice work on the dm-delay approach for exposing lock contention. You should know that bcachefs already has a comprehensive test framework called ktest that handles VM provisioning, test orchestration, and integrates with the bcachefs CI. Rather than maintaining a parallel framework, consider adding your swap torture tests as ktest tests — that way they run automatically and benefit the whole project.

— ProofOfConcept

Add copy-on-write swap file support to bcachefs using the SWP_FS_OPS
path (same mechanism as NFS and btrfs).  This enables checksumming,
encryption, compression, replication, and multi-device support for
swap data.

Key design decisions:

  PF_MEMALLOC context: swap_rw sets PF_MEMALLOC for both reads and
  writes.  Writes need it because swap writeback runs during reclaim.
  Reads need it because swap-in page faults could otherwise trigger
  reclaim that starts competing swap writes, risking btree lock
  deadlock.  The BCH_WRITE_swap flag propagates PF_MEMALLOC to the
  write index kworker.

  Btree node pinning: at swapon, leaf nodes for extents, inodes, and
  alloc btrees covering the swap file are marked noevict.  This avoids
  disk reads during memory reclaim — the hot path only does in-memory
  btree lookups.

  Btree cache pre-reserve: 16 MB of btree node buffers are
  pre-allocated on bc->freeable at swapon using GFP_NORETRY to avoid
  OOM on small VMs.  These cover ~5 concurrent fully-cold btree
  traversals across extents + inodes + alloc btrees.

  Disk reservation: swap_pages * PAGE_SECTORS reserved at swapon to
  prevent ENOSPC during COW writes.  Each swap write allocates a new
  physical block before freeing the old one; without a reservation,
  concurrent non-swap writers could consume all free space.

  Bkey buffer pre-allocation: 2048-byte buffer allocated with
  GFP_NOWAIT before entering PF_MEMALLOC, avoiding __GFP_NOFAIL WARN
  loops when bch2_bkey_buf_realloc spills from the 96-byte on-stack
  buffer.

  IS_SWAPFILE bypass: swap files skip pagecache locking in the direct
  I/O path since the kernel manages swap file pages directly.

  Write-buffer flush deadlock fix: upgrade PF_MEMALLOC_NOFS to
  PF_MEMALLOC in the write-buffer flush path.  The flush holds
  wb->flushing.lock while traversing btree nodes; if a node allocation
  enters direct reclaim, reclaim can need the journal, which needs
  write-buffer flush, which is blocked on the mutex — deadlock.
  PF_MEMALLOC prevents entering reclaim; allocation failures are
  already handled.

  Diagnostics: rate-limited warning when extents btree leaves exceed
  80% fill (early warning before the 67% split threshold), swap I/O
  stall detection at 2s (WARN) and 10s (BUG in debug builds).

Tested at 128-512M RAM with parallel ablation matrix covering:
pinning on/off, PF_MEMALLOC on/off, btree cache thrashing,
drop_caches, multi-device tiered storage, lz4/zstd compression,
5-minute sustained pressure, concurrent file I/O workload, and
disk-throttled I/O.

Signed-off-by: Matthias Schorer <matthias@schorer.dev>
Document the design rationale for COW swap file support: the reclaim
deadlock problem, how other filesystems handle it, and the mechanisms
used (PF_MEMALLOC, btree pinning, cache pre-reserve, disk reservation,
bkey pre-allocation, write-buffer flush noreclaim).

Includes analysis of dead ends (pre-fragmentation defeated by extent
merging, GFP flag whack-a-mole, kworker flag propagation) and
adversarial analysis (dead-key accumulation, shared btree nodes,
ENOSPC scenarios).

bcachefs: fix unlimited lock hold times in trans bounds

bch2_trans_begin() evaluates whether a transaction has exceeded
BTREE_TRANS_MAX_LOCK_HOLD_TIME_NS (10ms) and if so cleanly yields locks via
cond_resched(). However, it was resetting trans->last_begin_time
unconditionally on *every iteration* of loop macros like for_each_btree_key.
This meant that tight loops processing many extents, such as background
compression or bch-reconcile, would continuously reset their own yield
timer since individual loop iterations execute in microseconds.
This permitted single transactions to monopolize crucial btree locks limitlessly,
starving other core filesystem operations (e.g. the swap kworker blocking
for 120s+ during memory pressure, causing system lockups).

This patch introduces a new trans->last_yield_time field which is ONLY cleared
when locks are legitimately dropped (in trans_set_unlocked), properly
accumulating lock hold times across independent bch2_trans_begin() boundaries
until the threshold is organically reached.
0 = always drop SRCU before blocking (bch2_trans_unlock_long),
    equivalent to the original code path
N > 0 = two-phase escalation with (N-1)ms timeout before
    dropping SRCU

This fixes a semantic bug where timeout=0 would still keep SRCU
held during sub-jiffy operations, preventing SRCU grace periods
from completing and potentially causing stalls.

Runtime tunable via:
  /sys/module/bcachefs/parameters/srcu_escalation_timeout_ms
The move ratelimiter used global in-flight limits (move_ios_in_flight,
move_bytes_in_flight) for all devices. This overwhelms rotational devices
with deep queues of random writes, causing head thrashing and multi-second
write latencies that block btree transaction state and stall journal
reclaim.

Add per-context max_ios_in_flight and max_sectors_in_flight fields to
moving_context, initialized from c->opts. The reconcile phys thread
(which already runs per-device for rotational devices) overrides these
with much lower limits suitable for spinning disks.

Add module parameter move_ios_in_flight_rotational (default 8) to allow
runtime tuning without recompilation.

Signed-off-by: Matthias <matthias@example.com>
@dblsaiko

Copy link
Copy Markdown

I rebased this for bcachefs-tools because I wanted to test-drive it myself. From a quick test (yolo running it on my system and then filling up memory), it seems to still work, however no guarantees I made the right decisions merging in the changes from master since I am not familiar with the codebase. I left a few extra comments (search for "Katalin") where my decisions are probably questionable.

This is just a straight rebase with none of the review comments addressed, but hopefully this is something you can pick up future work off of.

The following changes since commit b8f8939fa2437b36208b43206e063ac36266aebe:

  btree commit: btree_key_can_insert_slowpath() (2026-05-28 15:35:21 -0400)

are available in the Git repository at:

  https://git.dblsaiko.net/bcachefs/ swap-files

for you to fetch changes up to 6dd4637e1d615179051cdbb9a35042c8de0f5077:

  bcachefs: make journal bucket discards asynchronous to avoid paralyzing journal writes (2026-05-29 06:00:20 +0200)
Details
Matthias Goergens (10):
      bcachefs: SRCU lock investigation and VM reproducer
      bcachefs: don't hold srcu lock across blocking operations
      docs: note foreground I/O stall resolution in INVESTIGATION.md
      bcachefs: swap file support via SWP_FS_OPS
      Documentation: bcachefs: add swap file design document
      srcu escalation: fix timeout=0 to mean original behavior
      bcachefs: per-device rate limiting for background data moves
      bcachefs: Add checksum_only flag for pure B-Tree metadata replacement
      bcachefs: Elevate B-Tree journal reclaim writes to REQ_PRIO
      bcachefs: make journal bucket discards asynchronous to avoid paralyzing journal writes

 Documentation/swap.rst   | 262 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 INVESTIGATION.md         | 301 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 build-initramfs.sh       |  38 +++++++++++
 fs/Makefile              |   3 +-
 fs/alloc/foreground.c    |   2 +-
 fs/btree/bkey_buf.h      |   6 ++
 fs/btree/cache.c         |  80 +++++++++++++++++++++--
 fs/btree/cache.h         |   2 +
 fs/btree/commit.c        |  14 +++-
 fs/btree/interior.c      |  14 ++--
 fs/btree/iter.c          |  25 +++++--
 fs/btree/iter.h          |  43 ++++++++++++
 fs/btree/locking.c       |  15 ++++-
 fs/btree/locking.h       |   1 +
 fs/btree/read.c          |   2 +-
 fs/btree/types.h         |   1 +
 fs/btree/write.c         |   6 +-
 fs/btree/write_buffer.c  |  10 +--
 fs/data/ec/io.c          |   2 +-
 fs/data/migrate.c        |   2 +-
 fs/data/move.c           |  15 +++--
 fs/data/move.h           |   8 ++-
 fs/data/reconcile/work.c |  14 ++++
 fs/data/update.c         |  37 ++++++++++-
 fs/data/update.h         |   1 +
 fs/data/write.c          |  72 ++++++++++++++++++--
 fs/data/write.h          |   1 +
 fs/data/write_types.h    |   5 +-
 fs/debug/tests.c         |   2 +
 fs/init/error.c          |  17 +----
 fs/journal/reclaim.c     |   8 ++-
 fs/vfs/buffered.c        |   2 +
 fs/vfs/direct.c          |  34 ++++++++--
 fs/vfs/fs.c              |   6 +-
 fs/vfs/fs.h              |   7 ++
 fs/vfs/swap.c            | 310 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 fs/vfs/swap.h            |  22 +++++++
 prepare-vm-disks.sh      |  66 +++++++++++++++++++
 run-vm-test.sh           |  75 +++++++++++++++++++++
 vm-init-rs/.gitignore    |   2 +
 vm-init-rs/Cargo.toml    |  12 ++++
 vm-init-rs/src/main.rs   | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 42 files changed, 1678 insertions(+), 67 deletions(-)
 create mode 100644 Documentation/swap.rst
 create mode 100644 INVESTIGATION.md
 create mode 100755 build-initramfs.sh
 create mode 100644 fs/vfs/swap.c
 create mode 100644 fs/vfs/swap.h
 create mode 100755 prepare-vm-disks.sh
 create mode 100755 run-vm-test.sh
 create mode 100644 vm-init-rs/.gitignore
 create mode 100644 vm-init-rs/Cargo.toml
 create mode 100644 vm-init-rs/src/main.rs

@koverstreet

koverstreet commented May 29, 2026 via email

Copy link
Copy Markdown
Owner

@Komzpa

Komzpa commented Jun 28, 2026

Copy link
Copy Markdown

For easier review against the current bcachefs-tools tree, I refreshed a cleaned carry here:

It preserves Matthias as the commit author, keeps the implementation as one focused patch on bcachefs-tools/testing, and drops the investigation notes/VM harness/debug toggles/debug BUG path/hardcoded version files/broad write-buffer PF_MEMALLOC change that were called out above. I also tightened the latest swapon cleanup paths and validated the staged DKMS module build against /usr/src/linux-headers-7.1-amd64; the companion ktest coverage remains koverstreet/ktest#63.

This was referenced Jun 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RFE: swap file support

4 participants