btrfs tools: do not compare against the booted profile on another filesystem - #144
Draft
Yury-MonZon wants to merge 8 commits into
Draft
btrfs tools: do not compare against the booted profile on another filesystem#144Yury-MonZon wants to merge 8 commits into
Yury-MonZon wants to merge 8 commits into
Conversation
This was referenced Jul 29, 2026
Yury-MonZon
force-pushed
the
btrfs-tools-device-scope
branch
3 times, most recently
from
July 31, 2026 12:59
ef3faee to
ee6b8f7
Compare
…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
force-pushed
the
btrfs-tools-device-scope
branch
from
July 31, 2026 15:15
ee6b8f7 to
72565b4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Six commits, one concern: under
-d/--devicethe tools operate on a filesystem we did not bootfrom, 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 -drefused 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.shdefaults to the absolute/boot/loader/entries, righton a booted system and wrong under
-d. In recovery/bootis an empty directory on the RAMrootfs, 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_nnreads the profile list, which lives inside a profile ratherthan in the running system's
/etc. Recovery has none, so every name resolved to an empty band andthe token silently lost its sort prefix:
flipperos-Desktopwhere the build wrote900-flipperos-Desktop, which also reorders the menu, since a leading letter sorts above a digit.The sort key.
os_sort_keyread the running/etc/os-release, so an entry written fromrecovery described the recovery system. It now reads the target's, which reproduces what the build
writes for every entry.
The filesystem UUID.
create-profilerewrites a profile's fstab, andflipper-blsrewrites thecmdline's
root=UUID, so a root built elsewhere still boots here. Both readcurrent_fsuuid(),which answers for the booted root, so both silently did nothing under
-d. Landing the 775 build'spacks 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_optsread$CONF_ROOT/cmdlinefrom the running root, absent inrecovery, so
BASE_OPTScame out empty: noroot=, noaudit=0, no console policy. It now prefersthe target's own copy, with
KERNEL_INSTALL_CONF_ROOTstill 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
_stockarriving over ssh (the form this tool's own examples show) was filed as anordinary restore point, where
migrate-profilenever looks for an ancestor. Buffering the head ofstdin makes the name readable and the buffer is replayed ahead of the rest. Relocating afterwards is
not possible: a received subvolume is read-only,
mvrefuses, and clearingroto force it dropsreceived_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 preserveparent_uuidwith a control showing a plain copy re-parents instead, and the full send-out, receive-back, promote
cycle.