btrfs: resumed snapshot deletion resets drop_progress to zero
Kernel fix, fstests regression test, reproducers and test logs.
The kernel patch was posted to linux-btrfs@vger.kernel.org (thread). The fstests test (btrfs/355) was posted separately to fstests@vger.kernel.org (thread). Neither is merged.
The bug
Deleting a subvolume is done in the background by btrfs_drop_snapshot(), which saves its position (drop_progress, drop_level) in the root item so it can resume after an unmount.
- On resume, the saved key is copied only into
wc->update_progress.wc->drop_progressandwc->drop_levelstay zero. - If a commit or unmount happens before the resumed run drops its first reference, the root item gets drop key
(0 0 0)level 0. - The next mount restarts from the first slot. Blocks the earlier run already released, and still used by a snapshot, look exclusively owned, and the drop tries to free them again:
unable to find ref byte nr ... parent 0 root N owner L offset 0 BTRFS: error ... in __btrfs_free_extent: errno=-117 Filesystem corrupted - The transaction aborts. In the test runs,
btrfs checkthen reported errors and reads under the snapshot returned EIO.
Introduced by aea6f028d01d ("btrfs: save drop_progress if we drop refs at all", v5.1-rc1). Still present in v7.4-rc1.
The fix
Seed wc->drop_progress and wc->drop_level from the root item on resume, so a checkpoint before the first drop rewrites the same resume point:
btrfs_disk_key_to_cpu(&key, &root_item->drop_progress);
memcpy(&wc->update_progress, &key,
sizeof(wc->update_progress));
+ /* keep the resume point until this run drops something itself */
+ memcpy(&wc->drop_progress, &key, sizeof(wc->drop_progress));
level = btrfs_root_drop_level(root_item);
BUG_ON(level == 0);
+ wc->drop_level = level;
Files
- 0001-btrfs-keep-drop_progress-when-resuming-snapshot-deletion.patch
- Kernel fix,
git format-patchagainst v7.3.1. Applies to v7.0.14 through v7.4-rc1. - 0001-btrfs-test-that-a-resumed-snapshot-deletion-keeps-it.patch
- fstests regression test btrfs/355, against fstests master. Plain files: fstests/355, fstests/355.out.
- repro-simple2.sh
- Minimal reproducer, ~70 s. Prints
BUG: ...and exits 1 on the abort. Needs mkfiles.py. - repro-simple.sh
- Same sequence with checks after each step: drop key, dmesg,
btrfs check --readonly, snapshot content. Exit 0 bug, 1 correct, 2 inconclusive. Needs mkfiles.py and manifest.py. - SUMMARY.md
- Write-up: results, environment, reproducers, limits.
- logs/
- Per-run logs, listed in logs/index.html: ab/ (reproducers), btrfs-355/, fstests-quick/, dmesg-abort-lines.txt.
- README.txt, sha256sums
- Plain-text version of this page; checksums (
sha256sum -c sha256sums).
Reproducing
Use a throwaway VM. The scripts only touch an image file they create, but the bug aborts a transaction in the running kernel.
sudo ./repro-simple2.sh /var/tmp/work # btrfs-progs, dmsetup, python3
sudo ./repro-simple.sh /var/tmp/work
Sequence: 2 GB loop image with dm-delay, mkfs.btrfs -n 4096, subvolume S1 with 100k files (level 3 tree), read-only snapshot S2, delete S1, unmount after the first root child is dropped, mount and unmount right away, then let the drop finish.
fstests: copy fstests/355* to tests/btrfs/ (or apply the patch), then:
./check btrfs/355
btrfs/355 needs no dm-delay: 30k files, checks the drop key after each of five quick resumes, then S2's content and that S1 is gone. About 15 s.
Results
Ubuntu 26.10 VM, kernel 7.3.0-9-generic (v7.3-rc6 based). Self-built modules come from the matching Ubuntu source with the same toolchain as stock, and differ only by the patch.
repro-simple.sh, 3 runs per module
| Module | Drop key zeroed | Abort | btrfs check | Snapshot S2 |
|---|---|---|---|---|
| stock | 3/3 | 3/3 | errors 3/3 | EIO 3/3 |
| self-built, unpatched | 3/3 | 3/3 | errors 3/3 | EIO 3/3 |
| self-built, patched | 0/3 | 0/3 | clean 3/3 | intact 3/3 |
repro-simple2.sh: abort 3/3 on stock and unpatched, 0/3 patched. With the patch the drop key kept the resume point over 20 mount/unmount cycles and the next mount finished the drop.
fstests btrfs/355 (final version)
| Storage | Stock | Unpatched | Patched | Time |
|---|---|---|---|---|
| SATA SSD, loop files on ext4 | 3/3 fail | 3/3 fail | 3/3 pass | 15–17 s |
| loop files on tmpfs | – | 3/3 fail | 3/3 pass | 11–13 s |
Each failing run: drop key 0 after the first resume, abort in dmesg, S2 content changed, btrfs check errors.
fstests -g quick: 765 of 766 executed tests pass with and without the patch. generic/574 (fs-verity, 1K blocks) fails in both, unrelated.
Limits
- One machine and one kernel. Upstream v7.3.1 was built with the patch but not run.
- btrfs/355 depends on timing. It was checked on an older SATA SSD and on tmpfs, not on HDD or a much faster CPU. A missed window gives not run or a false pass, never a false fail.