Skip to content

btrfs tools: do not compare against the booted profile on another filesystem - #144

Draft
Yury-MonZon wants to merge 8 commits into
btrfs-tools-cleanup-on-exitfrom
btrfs-tools-device-scope
Draft

btrfs tools: do not compare against the booted profile on another filesystem#144
Yury-MonZon wants to merge 8 commits into
btrfs-tools-cleanup-on-exitfrom
btrfs-tools-device-scope

Conversation

@Yury-MonZon

@Yury-MonZon Yury-MonZon commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Six commits, one concern: under -d/--device the tools operate on a filesystem we did not boot
from, and in six different places they consulted the running root for a fact that belongs to the
target. A recovery boot is the only environment where those two diverge enough to notice, which is
why none of this surfaced earlier.

Subvolume ids. Five tools compared ids, or names, with the booted profile's. The two
filesystems hand out 256, 257 and so on, so a collision is a matter of time. Demonstrated by
creating profiles on a loop filesystem until one landed on the booted profile's id 265, at which
point delete-profile -d refused to delete it, reporting it as the profile you are booted into.
top_is_booted_fs() now answers the question once, by comparing filesystem UUIDs.

The entries directory. flipper-bls.sh defaults to the absolute /boot/loader/entries, right
on a booted system and wrong under -d. In recovery /boot is an empty directory on the RAM
rootfs, so a profile created there got its entry written to RAM and was left unbootable after the
next reboot, while a profile deleted there kept its real entry on disk. The three tools that mount
the top level now state where the entries live; the lib default is untouched for the
kernel-install hooks.

The menu band. profile_base_nn reads the profile list, which lives inside a profile rather
than in the running system's /etc. Recovery has none, so every name resolved to an empty band and
the token silently lost its sort prefix: flipperos-Desktop where the build wrote
900-flipperos-Desktop, which also reorders the menu, since a leading letter sorts above a digit.

The sort key. os_sort_key read the running /etc/os-release, so an entry written from
recovery described the recovery system. It now reads the target's, which reproduces what the build
writes for every entry.

The filesystem UUID. create-profile rewrites a profile's fstab, and flipper-bls rewrites the
cmdline's root=UUID, so a root built elsewhere still boots here. Both read current_fsuuid(),
which answers for the booted root, so both silently did nothing under -d. Landing the 775 build's
packs on a 777 filesystem from recovery showed the cost: the restored profile kept the build
machine's UUID in all five fstab entries, and its entry had no root= at all.

The base cmdline. compute_base_opts read $CONF_ROOT/cmdline from the running root, absent in
recovery, so BASE_OPTS came out empty: no root=, no audit=0, no console policy. It now prefers
the target's own copy, with KERNEL_INSTALL_CONF_ROOT still winning for the hooks.

Plus one usability fix in the same area: a piped golden base is now routed like a file. The
destination was chosen by peeking the subvolume name out of the stream, which only worked for a
file, so a _stock arriving over ssh (the form this tool's own examples show) was filed as an
ordinary restore point, where migrate-profile never looks for an ancestor. Buffering the head of
stdin makes the name readable and the buffer is replayed ahead of the rest. Relocating afterwards is
not possible: a received subvolume is read-only, mv refuses, and clearing ro to force it drops
received_uuid, which is what btrfs matches a later incremental's parent on.

Testing

All on hardware, in a recovery boot, since that is the only place these paths differ: 97 checks
across four suites, plus a dedicated run that lands the real 775 build's packs on a 777 filesystem
and checks the restored profile end to end. After the fixes that run reports the stream routed to
@stock-snapshots, the incremental following its parent, the fstab rewritten across every entry,
and an entry reading root=UUID=<this filesystem> audit=0 console=tty1 ... rootflags=subvol=@Desktop-775.

Also covered here for the first time: create-profile -m/--move, proven to preserve parent_uuid
with a control showing a plain copy re-parents instead, and the full send-out, receive-back, promote
cycle.

…esystem

Under -d/--device the tools operate on a filesystem we did not boot from, but five of them
still compared subvolume ids (or names) with the booted profile's. Those id spaces are
unrelated: both filesystems hand out 256, 257, and so on, so a collision is a matter of time
rather than bad luck. Verified on the device by creating profiles on a loop filesystem until
one landed on the booted profile's id 265, at which point delete-profile -d refused to delete
it, reporting it as the profile you are booted into.

top_is_booted_fs() answers the question once, by comparing the target's filesystem UUID with
the booted root's, and list-profiles, btrfs-show-space, delete-profile, rename-profile and
migrate-profile now gate on it. list-profiles also says which filesystem it is listing instead
of printing a booted profile that belongs to a different one.
flipper-bls.sh defaults its entries directory to the absolute /boot/loader/entries, which is
right on a booted system, where /boot is the shared subvolume, and wrong under -d, where we are
holding a different filesystem open at $TOP.

In a recovery boot it is wrong in both directions. Creating a profile wrote its entry into an
empty /boot on the RAM rootfs, so the entry vanished with the next reboot and the profile was
left unbootable; deleting a profile scanned that same RAM directory and left the real entry on
the eMMC behind. Both reproduced on the device: a profile created from recovery got no entry on
the target filesystem, and a deleted one kept its stale entry.

create-profile, delete-profile and rename-profile already mount the top level, so they now say
where the entries live rather than letting the lib guess. The lib default is untouched, since the
kernel-install hooks run on the booted root with /boot mounted and want exactly that.
Two more inputs were read from whatever root happened to be running rather than from the root
being written, which only diverges under -d.

profile_base_nn reads the profile list, which lives inside a profile rather than in the /etc of
the running system. A recovery boot has no list at all, so every name resolved to an empty band
and the entry silently lost its sort prefix: an entry-token of flipperos-Desktop where the build
had written 900-flipperos-Desktop. Since a name starting with a letter sorts above one starting
with a digit, that also moves the entry in the menu. target_profiles() prefers the target's own
list and falls back to ours.

os_sort_key read the running /etc/os-release, so an entry written from recovery described the
recovery system (sort-key flipperos-recovery) instead of the image being booted. It now reads the
target's copy, which reproduces exactly what the build writes for every entry (sort-key debian,
since the image sets ID=debian and no IMAGE_ID).

Verified in a recovery boot: a profile created there now lands its entry on the eMMC with the
correct band (601-flipperos-BlsProbe) and the same sort key the build uses, rename moves it, and
delete removes it, with the RAM /boot untouched throughout.
… root

create-profile rewrites a profile fstab so a root built elsewhere still mounts /boot, /home and
/var/* here, and flipper-bls rewrites the shipped cmdline root=UUID for the same reason. Both read
current_fsuuid(), which answers for the BOOTED root: a different filesystem under -d, and nothing
at all in a recovery boot, where both rewrites then silently did nothing.

Landing the 775 build packs on a 777 filesystem from recovery showed the cost. The restored profile
kept e1d4c0fe, the build machine filesystem, in all five of its fstab entries, and its boot entry
carried no root= at all, so it could not have booted.

top_fsuuid() answers for the filesystem mount_top opened and falls back to the booted root for
callers that run before it. create-profile uses it for the fstab, and the three tools that mount the
top level pass it to flipper-bls as TARGET_FSUUID for the cmdline rewrite.
compute_base_opts took root=UUID and the policy flags from $CONF_ROOT/cmdline, which defaults to the
running root /etc/kernel. Under -d that is the wrong image, and in a recovery boot the file does not
exist at all, so BASE_OPTS came out empty and the entry was written with no root=, no audit=0 and no
console policy.

It now prefers the target own /etc/kernel/cmdline, while kernel-install still wins when it sets
KERNEL_INSTALL_CONF_ROOT. Verified from recovery: an entry for a profile restored from another build
now reads root=UUID=<this filesystem> audit=0 console=tty1 ... rootflags=subvol=@Desktop-775.
A _stock belongs in @stock-snapshots, which is where migrate-profile looks for a merge ancestor. The
destination was chosen by peeking the subvolume name out of the stream, which only worked for a
file: a stream on stdin cannot be peeked, so a golden base arriving over ssh, the form this tool own
examples show, was filed as an ordinary restore point and the ancestor lookup would never find it.

Buffering the first few MB of stdin makes the name readable, after which the buffer is replayed
ahead of the rest. Relocating afterwards is not an option, which the device demonstrated when I
tried it first: a received subvolume is read-only so mv refuses, and clearing ro to force it drops
received_uuid, which is what btrfs matches the parent of a later incremental on.
<@DesT> defaults to 'current', the profile we booted from, and the only gate was that / is
btrfs. Under -d the target is a different filesystem where that profile does not exist: the
name was taken anyway, and whatever happened to be called that on the target was replaced and
its contents moved aside, without the user naming it.

Recovery testing could not see this, since there / is not btrfs and the existing gate caught it.
Verified on the device against a sandbox: before, the target's @desktop was replaced and an aside
created; after, it refuses with the same wording as the no-booted-profile case and the target is
untouched. 'current' still resolves normally without -d.
…ream

Same inheritance problem as the send side, and this is where it actually bit: the 4 MB peek blocks
until the peer sends or closes, and a killed receive left that head holding the lock with no owner,
alongside an orphaned flock, for as long as the machine stayed up.

The peek and the streaming pipeline now run with 9>&-. Measured by killing the lock holder during a
real 3.6 GB receive: before, three heirs kept the lock and a writer would block; after, the lock is
free immediately.
@Yury-MonZon
Yury-MonZon force-pushed the btrfs-tools-device-scope branch from ee6b8f7 to 72565b4 Compare July 31, 2026 15:15
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