Skip to content

rollback: support named-subvolume btrfs layouts (ROLLBACK_METHOD)#1133

Open
mxsb wants to merge 8 commits into
openSUSE:masterfrom
mxsb:mxsb/configurable-rollback-method
Open

rollback: support named-subvolume btrfs layouts (ROLLBACK_METHOD)#1133
mxsb wants to merge 8 commits into
openSUSE:masterfrom
mxsb:mxsb/configurable-rollback-method

Conversation

@mxsb
Copy link
Copy Markdown

@mxsb mxsb commented May 15, 2026

On systems where fstab mounts the root filesystem with an explicit subvol= option (e.g. subvol=@root), snapper rollback currently has no effect on the next boot. The BTRFS_IOC_DEFAULT_SUBVOL ioctl changes the default subvolume ID, but when the kernel mounts by name the default ID is ignored.

This affects systems using named-subvolume layouts (Fedora, Ubuntu, many manual btrfs setups) and is the root cause behind #722, #365, #159, and #1011.

Changes

This PR adds a ROLLBACK_METHOD config key with three values:

  • auto (default) — detects from /proc/mounts whether root uses a named subvol= option and picks the appropriate method; no manual configuration needed on most systems
  • set-default — existing behavior, unchanged
  • subvol-rename — atomically swaps the named root subvolume using renameat2(RENAME_EXCHANGE) (Linux 3.17+)

The subvol-rename sequence:

  1. Read-write copy of the rollback target created as @root.incoming
  2. renameat2(RENAME_EXCHANGE) atomically swaps @root.incoming@root — no window where @root does not exist
  3. Old root moved to @root.rollback.<N> for recovery

Both CLASSIC and TRANSACTIONAL ambits are supported.

Safety properties

  • Nested subvolume paths (e.g. subvol=root/@root) are rejected with a clear error before any disk operations. SDir operations require single-component names and would otherwise abort.
  • Old root is never deleted. After the atomic swap, the old root is renamed to @root.rollback.<N>. If that name already exists (e.g. rolling back to the same snapshot twice), a subvolume-ID-based fallback name @root.rollback.svid.<id> is used — guaranteed unique across the btrfs filesystem.
  • Stale .incoming from a prior interrupted rollback is renamed (not deleted) before starting. If a previous rollback completed the swap but failed to rename .incoming to .rollback.<N>, that subvolume is the old running root, still mounted by the kernel via subvolume ID. Deleting it risks corruption during the unmount sequence. It is renamed to @root.rollback.svid.<id> instead.
  • rename_exchange failure deletes only the newly-created .incoming copy (safe — the swap never occurred) and throws a clear error.

Testing

  • Unit tests for mount-option detection (testsuite/rollback-method.cc)
  • Unit tests for SDir::rename_exchange (testsuite/rename-exchange.cc)
  • Integration tests on real btrfs (testsuite-real/rollback-subvol-rename.cc): happy path + .rollback.N collision scenario with subvolid fallback

Documentation

  • snapper-configs(5) — new ROLLBACK_METHOD entry
  • snapper(8) — rollback section updated
  • doc/rollback.txt — design rationale and method descriptions

Fixes #722, #365, #159. Related: #1011.


Note: This patch was developed with the assistance of Claude (Anthropic AI). The design, testing, and review decisions were made by the author.

Max Schwaab added 8 commits May 14, 2026 21:46
Adds RollbackMethod enum (SET_DEFAULT, SUBVOL_RENAME) and two detection
functions to AppUtil:

- detect_rollback_method_from_options(): pure function over already-parsed
  mount options, testable without /proc/mounts access
- detect_rollback_method(): reads /proc/mounts via getMtabData() and
  delegates to the above

Returns SUBVOL_RENAME when a "subvol=<name>" mount option is present and
the name is not "/", SET_DEFAULT otherwise. This distinguishes systems
using named subvolumes (subvol=@root) from those relying on the btrfs
default subvolume id.

Also adds KEY_ROLLBACK_METHOD config key and ROLLBACK_METHOD="auto" to
the default config template, and a Boost unit test covering all detection
cases including the subvolid= vs subvol= prefix disambiguation.

config.h is included in AppUtil.cc as it now contains the first
conditional compilation block in that translation unit.
Reads ROLLBACK_METHOD from the snapper config and resolves the effective
rollback method before the ambit switch:

- "auto": detect from /proc/mounts via detect_rollback_method()
- "set-default": force the existing btrfs default subvolume ioctl
- "subvol-rename": use named subvolume swap; errors if root is not
  mounted with a named subvolume
- unknown value: error and exit

Adds get_subvol_name() as a thin wrapper around detect_rollback_method()
for the explicit "subvol-rename" path, making intent clear without
discarding the returned RollbackMethod.

Both CLASSIC and TRANSACTIONAL ambit paths switch on the resolved method.
The subvol-rename case exits with "not yet implemented" until the rename
logic is added in the next commit.
Adds Btrfs::rollbackSubvolRename() which performs rollback on systems
using named btrfs subvolumes (subvol=@root in fstab) by atomically
swapping subvolumes rather than changing the btrfs default subvolume id.

The operation:
  1. Mounts the btrfs top-level (subvolid=5) via TmpMount
  2. Cleans up any stale <subvol>.incoming from a previous failed attempt
  3. Creates a rw snapshot of the rollback target as <subvol>.incoming
  4. Atomically swaps <subvol> and <subvol>.incoming via renameat2(RENAME_EXCHANGE)
  5. Renames old root to <subvol>.rollback.<num> for preservation (non-fatal if fails)
  6. Fires set_default_snapshot pre/post plugin hooks as setDefault() does

Adds SDir::rename_exchange() wrapping renameat2(RENAME_EXCHANGE), with
syscall(SYS_renameat2) fallback for older glibc. Btrfs supports
RENAME_EXCHANGE since Linux 3.17. configure.ac detects renameat2 via
AC_CHECK_FUNCS.

Both CLASSIC and TRANSACTIONAL ambit paths in cmd-rollback.cc now
dispatch to rollbackSubvolRename() when rollback_method is SUBVOL_RENAME.

Tests:
- testsuite/rename-exchange.cc: Boost unit test for SDir::rename_exchange
  covering successful exchange and the ENOENT case
- testsuite-real/rollback-subvol-rename.cc: integration test exercising
  the full subvolume swap sequence on a real btrfs filesystem
Add ROLLBACK_METHOD to snapper-configs(5) with descriptions of all three
values (auto, set-default, subvol-rename). Update the rollback section of
snapper(8) to mention auto-detection from /proc/mounts. Add doc/rollback.txt
explaining the design rationale, the two rollback methods, and why
subvol-rename is needed on systems using named subvolumes (subvol=@root).
SDir operations assert that names contain no '/'. A nested subvol= path
(e.g. root/@root) would hit that assert and abort with no useful message.
Add an explicit check at the top of rollbackSubvolRename that throws
IOErrorException with a clear message before any disk operations.
If a previous subvol-rename rollback completed the rename_exchange but
failed to move .incoming to .rollback.N, that .incoming subvolume is
still the old running root (mounted by subvolume ID). Deleting it risks
corruption during the unmount sequence on reboot (kernel < 5.x deferred
deletion is unreliable under this scenario).

Rename it to .rollback.<timestamp> instead, which is always safe and
preserves the subvolume for recovery.
… name

The previous timestamp-based suffix had a 1-second granularity collision
window. Use get_id() on the stale .incoming subvolume instead — btrfs
subvolume IDs are unique across the filesystem for the lifetime of the
subvolume, so the rescued name is guaranteed not to collide.

Also removes the <ctime> include that is no longer needed.
If the target .rollback.N name is already occupied (e.g. rolling back to
the same snapshot number twice), the rename fails with EEXIST. Instead of
leaving the old root stranded as .incoming, fall back to a subvolume-ID
based name (.rollback.svid.<id>) which is guaranteed unique across the
btrfs filesystem.

Extends the integration test with a second test case that verifies the
collision scenario and the fallback rename.
@mxsb
Copy link
Copy Markdown
Author

mxsb commented May 18, 2026

I see a check failed. I’m currently on vacation, but will look into it asap when I’m back home.

@aschnell
Copy link
Copy Markdown
Member

The failure in Leap is unrelated to the changes.

Comment thread snapper/AppUtil.h

#ifdef ENABLE_ROLLBACK

enum class RollbackMethod { SET_DEFAULT, SUBVOL_RENAME };
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the enum and the functions are only used in cmd-rollback.cc they should not be defined in the library.

case RollbackMethod::SUBVOL_RENAME:
{
const Btrfs* btrfs = dynamic_cast<const Btrfs*>(filesystem.get());
if (!btrfs)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already a check for btrfs above. So the variable btrfs could also be defined there.

Another possibility would be to add rollbackSubvolRename to Filesystem like it is done for setDefault. Esp. since renaming subvolume could maybe also be used to rollback using LVM (there the logical volumes would be renamed). Although currently there are no plans to implement this for LVM.

@aschnell
Copy link
Copy Markdown
Member

I installed Fedora 44 and the mount option in fstab is subvol=root (not subvol=@root). The rollback fails.
It also cannot detect the ambit. Do I need special settings during installation of Fedora?

@aschnell
Copy link
Copy Markdown
Member

Now I installed Debian 13.5 and get the same error. Although in /etc/fstab subvol=@rootfs is used in /proc/mounts it's subvol=/@rootfs. So the check again fails.

Comment thread snapper/FileUtils.cc
#ifdef HAVE_RENAMEAT2
return ::renameat2(dirfd, name1.c_str(), dirfd, name2.c_str(), RENAME_EXCHANGE);
#else
return syscall(SYS_renameat2, dirfd, name1.c_str(), dirfd, name2.c_str(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renameat2 was added in glibc in 2018 so this fallback looks unnecessary. Even without the workaround it builds on all archs currently tested (e.g. https://build.opensuse.org/project/show/home:aschnell:snapper).

@mxsb
Copy link
Copy Markdown
Author

mxsb commented May 26, 2026

Thanks for your comments and looking at the PR, I'm working on the feedback.

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.

GRUB still looks for grub.cfg on previous subvolume after snapper rollback

2 participants