@@ -1196,11 +1196,21 @@ pub(crate) fn setup_composefs_uki_boot(
11961196/// `<tmpdir>/tmp` to provide a writable scratch area for tools invoked with
11971197/// `--root`.
11981198///
1199- /// Drop order matters: the ESP and tmpfs guards are declared before `composefs`
1200- /// so they are unmounted (and flushed) before the composefs root is detached.
1199+ /// Drop order matters: the tmpfs guard is declared before `composefs` so it
1200+ /// is unmounted (and flushed) before the composefs root is detached.
12011201pub ( crate ) struct MountedImageRoot {
1202- // Unmounted before `composefs` on drop; ESP before tmp (inner before outer).
1203- _esp : bootc_mount:: tempmount:: MountGuard ,
1202+ // The ESP's device path. The ESP is intentionally *not* mounted here for
1203+ // our whole lifetime; it's mounted on demand via `with_esp()` instead.
1204+ // That's because `install_via_bootupd` runs bootupd's install tooling
1205+ // inside a `ChrootCmd`, which unshares the mount namespace and
1206+ // recursively self-binds the chroot directory (our `root_path()`) onto
1207+ // itself. If the ESP were already mounted at `<root_path>/boot` when
1208+ // that happens, the self-bind would duplicate it into an
1209+ // orphaned/shadowed mountinfo entry that's still visible to naive
1210+ // `findmnt`-based scans (like bootupd's), causing it to pick the wrong
1211+ // filesystem UUID. See the commit introducing this comment for details.
1212+ esp_device : Utf8PathBuf ,
1213+ // Unmounted before `composefs` on drop.
12041214 _tmp : bootc_mount:: tempmount:: MountGuard ,
12051215 composefs : TempMount ,
12061216 pub ( crate ) esp_subdir : & ' static str ,
@@ -1236,10 +1246,6 @@ impl MountedImageRoot {
12361246 // unconditionally mount the ESP at /boot for now.
12371247 let esp_subdir = "boot" ;
12381248
1239- let esp_path = composefs. dir . path ( ) . join ( esp_subdir) ;
1240- let esp =
1241- mount_esp_at ( & esp_part. path ( ) , esp_path) . context ( "Mounting ESP into composefs root" ) ?;
1242-
12431249 // Mount a tmpfs over /tmp so that tools invoked with --root have a
12441250 // writable scratch area without touching the read-only EROFS root.
12451251 let tmp_path = composefs. dir . path ( ) . join ( "tmp" ) ;
@@ -1253,7 +1259,7 @@ impl MountedImageRoot {
12531259 . context ( "Mounting tmpfs into composefs root" ) ?;
12541260
12551261 Ok ( Self {
1256- _esp : esp ,
1262+ esp_device : esp_part . path ( ) . into ( ) ,
12571263 _tmp : tmp,
12581264 composefs,
12591265 esp_subdir,
@@ -1271,12 +1277,29 @@ impl MountedImageRoot {
12711277 }
12721278
12731279 /// Open the mounted ESP as a capability-safe directory.
1280+ ///
1281+ /// This only succeeds while the ESP is actually mounted, i.e. from
1282+ /// within (or after) a call to [`Self::with_esp`].
12741283 pub ( crate ) fn open_esp_dir ( & self ) -> Result < Dir > {
12751284 self . composefs
12761285 . fd
12771286 . open_dir ( self . esp_subdir )
12781287 . with_context ( || format ! ( "Opening ESP at /{}" , self . esp_subdir) )
12791288 }
1289+
1290+ /// Mount the ESP at `<tmpdir>/<esp_subdir>` for the duration of `f`, then
1291+ /// unmount it again. Nothing is mounted there outside of calls to this
1292+ /// method, so callers that need to invoke bootloader-install tooling
1293+ /// that itself creates a private mount namespace (e.g. via `ChrootCmd`)
1294+ /// can safely do so without risking this mount being shadowed/duplicated
1295+ /// by that tooling's own mount-namespace setup.
1296+ pub ( crate ) fn with_esp < T > ( & self , f : impl FnOnce ( & Dir ) -> Result < T > ) -> Result < T > {
1297+ let esp_path = self . root_path ( ) . join ( self . esp_subdir ) ;
1298+ let _guard = mount_esp_at ( self . esp_device . as_str ( ) , esp_path)
1299+ . context ( "Mounting ESP into composefs root" ) ?;
1300+ let dir = self . open_esp_dir ( ) ?;
1301+ f ( & dir)
1302+ }
12801303}
12811304
12821305pub struct SecurebootKeys {
@@ -1381,56 +1404,75 @@ pub(crate) async fn setup_composefs_boot(
13811404 postfetch. detected_bootloader,
13821405 Bootloader :: Grub | Bootloader :: GrubCC
13831406 ) {
1407+ let chroot_target = Utf8Path :: from_path ( mounted_root. root_path ( ) )
1408+ . ok_or_else ( || anyhow ! ( "composefs tmpdir path is not valid UTF-8" ) ) ?;
1409+ // Like the ostree backend, bind the physical root's real /boot (an
1410+ // ordinary ext4/xfs/... directory, not yet populated with kernels at
1411+ // this point) into the chroot. This gives bootupd both a correct
1412+ // filesystem to inspect for `--write-uuid` (rather than the ESP,
1413+ // which is otherwise mounted at the composefs root's own /boot) and
1414+ // an empty `boot/efi` directory for its EFI component to discover
1415+ // and mount the real ESP into, exactly as it would on ostree.
1416+ let bind_boot_path = root_setup. physical_root_path . join ( "boot" ) ;
13841417 crate :: bootloader:: install_via_bootupd (
13851418 & root_setup. device_info ,
13861419 & root_setup. physical_root_path ,
13871420 & state. config_opts ,
1388- None ,
1421+ Some ( chroot_target) ,
1422+ Some ( bind_boot_path. as_path ( ) ) ,
13891423 ) ?;
13901424
13911425 // FIXME: Remove this hack once we have support in bootupd
13921426 if matches ! ( postfetch. detected_bootloader, Bootloader :: GrubCC ) {
1427+ // bootupctl wrote this under the physical root's real /boot (via
1428+ // the bind mount above), not under the composefs root.
13931429 root_setup
13941430 . physical_root
1395- . remove_dir_all ( "boot/grub2" )
1431+ . remove_all_optional ( "boot/grub2" )
13961432 . context ( "removing grub2" ) ?;
13971433
1398- let ( os_id, ..) = parse_os_release ( mounted_root. dir ( ) ) ?
1399- . ok_or_else ( || anyhow:: anyhow!( "Failed to parse os-release" ) ) ?;
1400-
1401- let dir = format ! ( "EFI/{os_id}" ) ;
1402-
1403- // Files are in EFI/<os-name>/
1404- let efis_dir = mounted_root
1405- . open_esp_dir ( )
1406- . context ( "opening esp" ) ?
1407- . open_dir ( & dir)
1408- . with_context ( || format ! ( "Opening {dir}" ) ) ?;
1409-
1410- efis_dir
1411- . remove_file_optional ( "bootuuid.cfg" )
1412- . context ( "Removing bootuuid.cfg" ) ?;
1413- efis_dir
1414- . remove_file_optional ( "grub.cfg" )
1415- . context ( "Removing grub.cfg" ) ?;
1416-
1417- let final_name = match std:: env:: consts:: ARCH {
1418- "x86_64" => "grubx64.efi" ,
1419- "aarch64" => "grubaa64-cc.efi" ,
1420- arch => anyhow:: bail!( "GrubCC not supported for: {arch}" ) ,
1421- } ;
1434+ // install_via_bootupd above has already returned, so it's safe
1435+ // to mount the ESP here for the duration of this cleanup.
1436+ mounted_root. with_esp ( |esp_dir| {
1437+ let ( os_id, ..) = parse_os_release ( mounted_root. dir ( ) ) ?
1438+ . ok_or_else ( || anyhow:: anyhow!( "Failed to parse os-release" ) ) ?;
1439+
1440+ let dir = format ! ( "EFI/{os_id}" ) ;
1441+
1442+ // Files are in EFI/<os-name>/
1443+ let efis_dir = esp_dir
1444+ . open_dir ( & dir)
1445+ . with_context ( || format ! ( "Opening {dir}" ) ) ?;
1446+
1447+ efis_dir
1448+ . remove_file_optional ( "bootuuid.cfg" )
1449+ . context ( "Removing bootuuid.cfg" ) ?;
1450+ efis_dir
1451+ . remove_file_optional ( "grub.cfg" )
1452+ . context ( "Removing grub.cfg" ) ?;
1453+
1454+ let final_name = match std:: env:: consts:: ARCH {
1455+ "x86_64" => "grubx64.efi" ,
1456+ "aarch64" => "grubaa64-cc.efi" ,
1457+ arch => anyhow:: bail!( "GrubCC not supported for: {arch}" ) ,
1458+ } ;
1459+
1460+ mounted_root
1461+ . dir ( )
1462+ . copy ( "usr/lib/grub-cc/grub-cc.efi" , & efis_dir, final_name)
1463+ . context ( "Copying grub-cc binary" ) ?;
14221464
1423- mounted_root
1424- . dir ( )
1425- . copy ( "usr/lib/grub-cc/grub-cc.efi" , & efis_dir, final_name)
1426- . context ( "Copying grub-cc binary" ) ?;
1465+ Ok ( ( ) )
1466+ } ) ?;
14271467 }
14281468 } else {
1429- crate :: bootloader:: install_systemd_boot (
1430- & mounted_root,
1431- & state. config_opts ,
1432- get_secureboot_keys ( mounted_root. dir ( ) , BOOTC_AUTOENROLL_PATH ) ?,
1433- ) ?;
1469+ mounted_root. with_esp ( |_esp_dir| {
1470+ crate :: bootloader:: install_systemd_boot (
1471+ & mounted_root,
1472+ & state. config_opts ,
1473+ get_secureboot_keys ( mounted_root. dir ( ) , BOOTC_AUTOENROLL_PATH ) ?,
1474+ )
1475+ } ) ?;
14341476 }
14351477
14361478 let Some ( entry) = entries. iter ( ) . next ( ) else {
0 commit comments