6161//! 1. **Primary**: New/upgraded deployment (default boot target)
6262//! 2. **Secondary**: Currently booted deployment (rollback option)
6363
64+ use std:: cell:: Cell ;
6465use std:: fs:: create_dir_all;
6566use std:: io:: { Read , Seek , SeekFrom , Write } ;
6667use std:: path:: Path ;
@@ -1201,11 +1202,28 @@ pub(crate) fn setup_composefs_uki_boot(
12011202/// `<tmpdir>/tmp` to provide a writable scratch area for tools invoked with
12021203/// `--root`.
12031204///
1204- /// Drop order matters: the ESP and tmpfs guards are declared before `composefs`
1205- /// so they are unmounted (and flushed) before the composefs root is detached.
1205+ /// Drop order matters: the tmpfs guard is declared before `composefs` so it
1206+ /// is unmounted (and flushed) before the composefs root is detached.
12061207pub ( crate ) struct MountedImageRoot {
1207- // Unmounted before `composefs` on drop; ESP before tmp (inner before outer).
1208- _esp : bootc_mount:: tempmount:: MountGuard ,
1208+ // The ESP's device path. The ESP is intentionally *not* mounted here for
1209+ // our whole lifetime; it's mounted on demand via `with_esp()` instead.
1210+ // That's because `install_via_bootupd` runs bootupd's install tooling
1211+ // inside a `ChrootCmd`, which unshares the mount namespace and
1212+ // recursively self-binds the chroot directory (our `root_path()`) onto
1213+ // itself. If the ESP were already mounted at `<root_path>/boot` when
1214+ // that happens, the self-bind would duplicate it into an
1215+ // orphaned/shadowed mountinfo entry that's still visible to naive
1216+ // `findmnt`-based scans (like bootupd's), causing it to pick the wrong
1217+ // filesystem UUID.
1218+ esp_device : Utf8PathBuf ,
1219+ // Tracks whether the ESP is currently mounted, i.e. whether we're
1220+ // inside a call to `with_esp()`. Guards `open_esp_dir()` against
1221+ // returning the wrong thing: without this, calling it while the ESP
1222+ // isn't mounted would silently open the empty `esp_subdir` directory
1223+ // that already exists directly in the composefs image, rather than
1224+ // failing.
1225+ esp_mounted : Cell < bool > ,
1226+ // Unmounted before `composefs` on drop.
12091227 _tmp : bootc_mount:: tempmount:: MountGuard ,
12101228 composefs : TempMount ,
12111229 pub ( crate ) esp_subdir : & ' static str ,
@@ -1241,10 +1259,6 @@ impl MountedImageRoot {
12411259 // unconditionally mount the ESP at /boot for now.
12421260 let esp_subdir = "boot" ;
12431261
1244- let esp_path = composefs. dir . path ( ) . join ( esp_subdir) ;
1245- let esp =
1246- mount_esp_at ( & esp_part. path ( ) , esp_path) . context ( "Mounting ESP into composefs root" ) ?;
1247-
12481262 // Mount a tmpfs over /tmp so that tools invoked with --root have a
12491263 // writable scratch area without touching the read-only EROFS root.
12501264 let tmp_path = composefs. dir . path ( ) . join ( "tmp" ) ;
@@ -1258,7 +1272,8 @@ impl MountedImageRoot {
12581272 . context ( "Mounting tmpfs into composefs root" ) ?;
12591273
12601274 Ok ( Self {
1261- _esp : esp,
1275+ esp_device : esp_part. path ( ) . into ( ) ,
1276+ esp_mounted : Cell :: new ( false ) ,
12621277 _tmp : tmp,
12631278 composefs,
12641279 esp_subdir,
@@ -1276,12 +1291,47 @@ impl MountedImageRoot {
12761291 }
12771292
12781293 /// Open the mounted ESP as a capability-safe directory.
1294+ ///
1295+ /// Fails unless called from within a call to [`Self::with_esp`]: the ESP
1296+ /// is only actually mounted for the duration of that call, and
1297+ /// `esp_subdir` otherwise refers to an ordinary (empty) directory that
1298+ /// already exists directly in the composefs image.
12791299 pub ( crate ) fn open_esp_dir ( & self ) -> Result < Dir > {
1300+ if !self . esp_mounted . get ( ) {
1301+ bail ! ( "BUG: attempted to open the ESP directory while it is not mounted" ) ;
1302+ }
12801303 self . composefs
12811304 . fd
12821305 . open_dir ( self . esp_subdir )
12831306 . with_context ( || format ! ( "Opening ESP at /{}" , self . esp_subdir) )
12841307 }
1308+
1309+ /// Mount the ESP at `<tmpdir>/<esp_subdir>` for the duration of `f`, then
1310+ /// unmount it again. Nothing is mounted there outside of calls to this
1311+ /// method, so callers that need to invoke bootloader-install tooling
1312+ /// that itself creates a private mount namespace (e.g. via `ChrootCmd`)
1313+ /// can safely do so without risking this mount being shadowed/duplicated
1314+ /// by that tooling's own mount-namespace setup.
1315+ pub ( crate ) fn with_esp < T > ( & self , f : impl FnOnce ( & Dir ) -> Result < T > ) -> Result < T > {
1316+ let esp_path = self . root_path ( ) . join ( self . esp_subdir ) ;
1317+ let _guard = mount_esp_at ( self . esp_device . as_str ( ) , esp_path)
1318+ . context ( "Mounting ESP into composefs root" ) ?;
1319+
1320+ self . esp_mounted . set ( true ) ;
1321+ // Reset `esp_mounted` when we return, including via `?` or panic,
1322+ // so a later `with_esp()` call (or a stray `open_esp_dir()` call
1323+ // after this one returns) doesn't see a stale "mounted" state.
1324+ struct ResetOnDrop < ' a > ( & ' a Cell < bool > ) ;
1325+ impl Drop for ResetOnDrop < ' _ > {
1326+ fn drop ( & mut self ) {
1327+ self . 0 . set ( false ) ;
1328+ }
1329+ }
1330+ let _reset = ResetOnDrop ( & self . esp_mounted ) ;
1331+
1332+ let dir = self . open_esp_dir ( ) ?;
1333+ f ( & dir)
1334+ }
12851335}
12861336
12871337pub struct SecurebootKeys {
@@ -1386,56 +1436,75 @@ pub(crate) async fn setup_composefs_boot(
13861436 postfetch. detected_bootloader,
13871437 Bootloader :: Grub | Bootloader :: GrubCC
13881438 ) {
1439+ let chroot_target = Utf8Path :: from_path ( mounted_root. root_path ( ) )
1440+ . ok_or_else ( || anyhow ! ( "composefs tmpdir path is not valid UTF-8" ) ) ?;
1441+ // Like the ostree backend, bind the physical root's real /boot (an
1442+ // ordinary ext4/xfs/... directory, not yet populated with kernels at
1443+ // this point) into the chroot. This gives bootupd both a correct
1444+ // filesystem to inspect for `--write-uuid` (rather than the ESP,
1445+ // which is otherwise mounted at the composefs root's own /boot) and
1446+ // an empty `boot/efi` directory for its EFI component to discover
1447+ // and mount the real ESP into, exactly as it would on ostree.
1448+ let bind_boot_path = root_setup. physical_root_path . join ( "boot" ) ;
13891449 crate :: bootloader:: install_via_bootupd (
13901450 & root_setup. device_info ,
13911451 & root_setup. physical_root_path ,
13921452 & state. config_opts ,
1393- None ,
1453+ Some ( chroot_target) ,
1454+ Some ( bind_boot_path. as_path ( ) ) ,
13941455 ) ?;
13951456
13961457 // FIXME: Remove this hack once we have support in bootupd
13971458 if matches ! ( postfetch. detected_bootloader, Bootloader :: GrubCC ) {
1459+ // bootupctl wrote this under the physical root's real /boot (via
1460+ // the bind mount above), not under the composefs root.
13981461 root_setup
13991462 . physical_root
1400- . remove_dir_all ( "boot/grub2" )
1463+ . remove_all_optional ( "boot/grub2" )
14011464 . context ( "removing grub2" ) ?;
14021465
1403- let ( os_id, ..) = parse_os_release ( mounted_root. dir ( ) ) ?
1404- . ok_or_else ( || anyhow:: anyhow!( "Failed to parse os-release" ) ) ?;
1405-
1406- let dir = format ! ( "EFI/{os_id}" ) ;
1407-
1408- // Files are in EFI/<os-name>/
1409- let efis_dir = mounted_root
1410- . open_esp_dir ( )
1411- . context ( "opening esp" ) ?
1412- . open_dir ( & dir)
1413- . with_context ( || format ! ( "Opening {dir}" ) ) ?;
1414-
1415- efis_dir
1416- . remove_file_optional ( "bootuuid.cfg" )
1417- . context ( "Removing bootuuid.cfg" ) ?;
1418- efis_dir
1419- . remove_file_optional ( "grub.cfg" )
1420- . context ( "Removing grub.cfg" ) ?;
1421-
1422- let final_name = match std:: env:: consts:: ARCH {
1423- "x86_64" => "grubx64.efi" ,
1424- "aarch64" => "grubaa64-cc.efi" ,
1425- arch => anyhow:: bail!( "GrubCC not supported for: {arch}" ) ,
1426- } ;
1466+ // install_via_bootupd above has already returned, so it's safe
1467+ // to mount the ESP here for the duration of this cleanup.
1468+ mounted_root. with_esp ( |esp_dir| {
1469+ let ( os_id, ..) = parse_os_release ( mounted_root. dir ( ) ) ?
1470+ . ok_or_else ( || anyhow:: anyhow!( "Failed to parse os-release" ) ) ?;
1471+
1472+ let dir = format ! ( "EFI/{os_id}" ) ;
1473+
1474+ // Files are in EFI/<os-name>/
1475+ let efis_dir = esp_dir
1476+ . open_dir ( & dir)
1477+ . with_context ( || format ! ( "Opening {dir}" ) ) ?;
1478+
1479+ efis_dir
1480+ . remove_file_optional ( "bootuuid.cfg" )
1481+ . context ( "Removing bootuuid.cfg" ) ?;
1482+ efis_dir
1483+ . remove_file_optional ( "grub.cfg" )
1484+ . context ( "Removing grub.cfg" ) ?;
1485+
1486+ let final_name = match std:: env:: consts:: ARCH {
1487+ "x86_64" => "grubx64.efi" ,
1488+ "aarch64" => "grubaa64-cc.efi" ,
1489+ arch => anyhow:: bail!( "GrubCC not supported for: {arch}" ) ,
1490+ } ;
1491+
1492+ mounted_root
1493+ . dir ( )
1494+ . copy ( "usr/lib/grub-cc/grub-cc.efi" , & efis_dir, final_name)
1495+ . context ( "Copying grub-cc binary" ) ?;
14271496
1428- mounted_root
1429- . dir ( )
1430- . copy ( "usr/lib/grub-cc/grub-cc.efi" , & efis_dir, final_name)
1431- . context ( "Copying grub-cc binary" ) ?;
1497+ Ok ( ( ) )
1498+ } ) ?;
14321499 }
14331500 } else {
1434- crate :: bootloader:: install_systemd_boot (
1435- & mounted_root,
1436- & state. config_opts ,
1437- get_secureboot_keys ( mounted_root. dir ( ) , BOOTC_AUTOENROLL_PATH ) ?,
1438- ) ?;
1501+ mounted_root. with_esp ( |_esp_dir| {
1502+ crate :: bootloader:: install_systemd_boot (
1503+ & mounted_root,
1504+ & state. config_opts ,
1505+ get_secureboot_keys ( mounted_root. dir ( ) , BOOTC_AUTOENROLL_PATH ) ?,
1506+ )
1507+ } ) ?;
14391508 }
14401509
14411510 let Some ( entry) = entries. iter ( ) . next ( ) else {
0 commit comments