Skip to content

bcachefs: snapshot-inject: deleting a snapshotted subvolume, and an interrupted collapse under container layers - #144

Open
ticpu wants to merge 7 commits into
koverstreet:masterfrom
ticpu:snapshot-collapse-repro
Open

ticpu wants to merge 7 commits into
koverstreet:masterfrom
ticpu:snapshot-collapse-repro

Conversation

@ticpu

@ticpu ticpu commented Oct 8, 2026

Copy link
Copy Markdown

Two groups of snapshot-inject tests, both grown from the same filesystem: a podman graph root whose layers were snapshotted flat into a new subvolume and then kept in use. Every test here is a repair oracle in the file's usual contract (fsck -y must detect, fsck -n must then be clean, data in the snapshots the test didn't destroy must survive) and fails until the repair converges. Results below are against bcachefs-tools master 289ae7df3, kernel 7.3-rc5, in-kernel offline fsck.

Deleting an original that has been snapshotted (bcachefs-tools#1109)

test_deleted_original_after_snapshots is three commands and no injection: create a subvolume, snapshot it twice, delete it. The deleted subvolume's root inode keeps a version at the node that became interior, naming the deleted subvolume, and nothing removes that key: the node has two live children, so the sweep leaves it alone. On the next fsck, check_inodes takes the stale bi_subvol for damage (inode_bi_subvol_missing, since v1.39.1, when the leaf gate on that block went), strips bi_subvol and bi_parent_subvol, and the version is now an orphaned directory at an interior snapshot. check_unreachable_inodes reattaches it, and the lost+found it resolves for that snapshot is the one inside that very inode, the root of every snapshot in the family: reattached at (loop at <inum>:<snapshot>)/lost+found/<inum>.

On v1.39.7 that is where it stops: the reattach's child fixup whiteouts the new dirent in the snapshot roots' leaves, fsck converges, and every snapshot of the family now carries a lost+found naming the tree's own root. The test fails on that, by asserting the root inum is named by no dirent.

On master it no longer converges at all. check_directory_structure had been a no-op since v1.38.4 (51bf7dc36 kept POS_MIN as the start of a reverse walk; fixed by 57bae0829), which is why the field runs printed it done in 0 seconds. Now it finds the loop, and its repair removes the backpointer and reattaches the inode into lost+found, which is the lost+found inside the loop. A second loop follows, then inode points to dirent that does not point back on the lost+found inode itself, check_path_loop(): error removing dirent ENOENT_dirent_doesnt_match_inode, and recovery fails with exit 8. The second fsck does the same. A filesystem fsck cannot complete on, from deleting a subvolume that had been snapshotted.

test_deleted_original_after_snapshots_repointed is the field report's state. There the 204 cycling subvolume roots have backpointers naming the reattach dirent rather than their DT_SUBVOL names, which is what bch2_reattach_inode()'s child fixup writes when the descendant versions reach it without a backpointer: it points them at the new dirent instead of whiteouting it in their snapshot. The test runs the organic setup and one repair, then models that outcome with kvdb (drop the two whiteouts, point both roots at the reattach dirent) and asserts convergence. Result on master, before the same loop abort: inode_points_to_missing_dirent and inode_dir_missing_backpointer alternating on both roots, inode_wrong_backpointer, the dirent retyped dir to subvol, dirent_not_visible_in_parent_subvol, the subvolume's fs_path_parent rewritten to its sibling and the root's bi_parent_subvol with it. That is the report: a 2-cycle between check_inodes, which resolves a subvolume root's backpointer through its parent subvolume's leaf and finds nothing there, and check_dirents, which walks the dirent's descendant versions and puts the backpointer back. What put the field's roots in front of that fixup arm without backpointers is not reproduced here; with current code check_unreachable_inodes reaches the leaf versions first and reattaches them as subvol-N before the ancestor's fixup runs.

walk_subvols is fixed on the way: ls -d snap-* other exits 2 when other is absent, and under pipefail the substitution takes the ERR trap, so capture_state died in every test that doesn't build the standard tree.

Interrupted collapse under a container layer tree

The podman bcachefs graph driver snapshots each layer from its parent and replaces files by unlink and create, so a base-image inode is whited out in some layers and alive in others. test_interrupted_collapse_replaced_file builds that shape, interrupts snapshot deletion mid-collapse with snapshot_delete_bail_before_inodes (CONFIG_BCACHEFS_DEBUG), and asserts fsck finishes the collapse without reporting key in missing inode for extents the surviving layers still read; the field report had fsck delete them and the files read back as zeros. _twice adds a second interrupted round that moves the collapse terminal, _churn spreads snapshot ids past the is_ancestor bitmap with 150 create/delete cycles on the kept layer, as in the field, and _churn_userspace runs the userspace fsck the field used, with BCACHEFS_KERNEL_ONLY unset since it silently overrides -K. interrupt_snapshot_deletion() is factored out of test_interrupted_deletion.

Two small runner commits ride along: ktest: find virtiofsd off $PATH and testrunner: only mount the nix store on nix hosts.

Full logs of the runs quoted above are available on request.

ticpu and others added 7 commits October 8, 2026 11:46
Debian ships virtiofsd in /usr/libexec and Arch in /usr/lib; neither is
on $PATH, so start_vm failed on both. Resolve it there too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er tree

Reproducer for a field report: podman's bcachefs graph driver snapshots
each layer from its parent and replaces files by unlink and create, so a
base-image inode is whited out in some layers and alive in others. After
snapshot deletion was interrupted mid-collapse, fsck reported
key_in_missing_inode for those files and deleted extents that surviving
layers still read.

Two variants: one interrupted round, and a second round that moves the
collapse terminal. Snapshot deletion interruption is factored into
interrupt_snapshot_deletion(), shared with test_interrupted_deletion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
root_image symlinks /nix to /host/nix, which dangles on a host without
a store; mkdir -p /nix/store then fails under errexit and the VM never
runs the test. Mount only when the host exports a store, and let a
failed mount surface instead of being swallowed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd userspace fsck

The field report's snapshot ids were far apart, putting is_ancestor on
skiplists, and it ran userspace fsck; the first variants covered neither.
Container churn on the kept layer spreads ids past the 128-id bitmap, and
the userspace variant unsets BCACHEFS_KERNEL_ONLY, which silently
overrides fsck -K.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…shots

bcachefs-tools issue #1109. Snapshot a subvolume twice, delete the
original: its root inode's version at the now-interior node keeps
naming the deleted subvolume. check_inode strips bi_subvol
(inode_bi_subvol_missing), the version becomes an orphaned directory
and gets reattached into the lost+found inside itself, and from then
on check_inodes and check_dirents undo each other's backpointer repair
on every run.

Repair-oracle test: fails until fsck converges.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ointed

deleted_original_after_snapshots converges: the reattach's child fixup
whiteouts the new dirent in the snapshot roots' leaves. The field report
has the roots' backpointers naming that dirent instead, which the same
fixup writes when the roots had no backpointer. Model that outcome on
the repaired image and assert convergence; fails until check_inodes and
check_dirents agree on it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ls -d exits 2 when "other" is absent, and under pipefail the command
substitution takes the ERR trap, so capture_state dies in every test
that doesn't build the standard tree. The names are fixed: glob them
and let the directory test skip what's missing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ticpu
ticpu force-pushed the snapshot-collapse-repro branch from ae6803f to 81ad16c Compare October 8, 2026 15:46
@ticpu
ticpu marked this pull request as ready for review October 8, 2026 15:55
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.

1 participant