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 ;
@@ -1207,11 +1208,28 @@ pub(crate) fn setup_composefs_uki_boot(
12071208/// `<tmpdir>/tmp` to provide a writable scratch area for tools invoked with
12081209/// `--root`.
12091210///
1210- /// Drop order matters: the ESP and tmpfs guards are declared before `composefs`
1211- /// so they are unmounted (and flushed) before the composefs root is detached.
1211+ /// Drop order matters: the tmpfs guard is declared before `composefs` so it
1212+ /// is unmounted (and flushed) before the composefs root is detached.
12121213pub ( crate ) struct MountedImageRoot {
1213- // Unmounted before `composefs` on drop; ESP before tmp (inner before outer).
1214- _esp : bootc_mount:: tempmount:: MountGuard ,
1214+ // The ESP's device path. The ESP is intentionally *not* mounted here for
1215+ // our whole lifetime; it's mounted on demand via `with_esp()` instead.
1216+ // That's because `install_via_bootupd` runs bootupd's install tooling
1217+ // inside a `ChrootCmd`, which unshares the mount namespace and
1218+ // recursively self-binds the chroot directory (our `root_path()`) onto
1219+ // itself. If the ESP were already mounted at `<root_path>/boot` when
1220+ // that happens, the self-bind would duplicate it into an
1221+ // orphaned/shadowed mountinfo entry that's still visible to naive
1222+ // `findmnt`-based scans (like bootupd's), causing it to pick the wrong
1223+ // filesystem UUID.
1224+ esp_device : Utf8PathBuf ,
1225+ // Tracks whether the ESP is currently mounted, i.e. whether we're
1226+ // inside a call to `with_esp()`. Guards `open_esp_dir()` against
1227+ // returning the wrong thing: without this, calling it while the ESP
1228+ // isn't mounted would silently open the empty `esp_subdir` directory
1229+ // that already exists directly in the composefs image, rather than
1230+ // failing.
1231+ esp_mounted : Cell < bool > ,
1232+ // Unmounted before `composefs` on drop.
12151233 _tmp : bootc_mount:: tempmount:: MountGuard ,
12161234 composefs : TempMount ,
12171235 pub ( crate ) esp_subdir : & ' static str ,
@@ -1247,10 +1265,6 @@ impl MountedImageRoot {
12471265 // unconditionally mount the ESP at /boot for now.
12481266 let esp_subdir = "boot" ;
12491267
1250- let esp_path = composefs. dir . path ( ) . join ( esp_subdir) ;
1251- let esp =
1252- mount_esp_at ( & esp_part. path ( ) , esp_path) . context ( "Mounting ESP into composefs root" ) ?;
1253-
12541268 // Mount a tmpfs over /tmp so that tools invoked with --root have a
12551269 // writable scratch area without touching the read-only EROFS root.
12561270 let tmp_path = composefs. dir . path ( ) . join ( "tmp" ) ;
@@ -1264,7 +1278,8 @@ impl MountedImageRoot {
12641278 . context ( "Mounting tmpfs into composefs root" ) ?;
12651279
12661280 Ok ( Self {
1267- _esp : esp,
1281+ esp_device : esp_part. path ( ) . into ( ) ,
1282+ esp_mounted : Cell :: new ( false ) ,
12681283 _tmp : tmp,
12691284 composefs,
12701285 esp_subdir,
@@ -1282,12 +1297,47 @@ impl MountedImageRoot {
12821297 }
12831298
12841299 /// Open the mounted ESP as a capability-safe directory.
1300+ ///
1301+ /// Fails unless called from within a call to [`Self::with_esp`]: the ESP
1302+ /// is only actually mounted for the duration of that call, and
1303+ /// `esp_subdir` otherwise refers to an ordinary (empty) directory that
1304+ /// already exists directly in the composefs image.
12851305 pub ( crate ) fn open_esp_dir ( & self ) -> Result < Dir > {
1306+ if !self . esp_mounted . get ( ) {
1307+ bail ! ( "BUG: attempted to open the ESP directory while it is not mounted" ) ;
1308+ }
12861309 self . composefs
12871310 . fd
12881311 . open_dir ( self . esp_subdir )
12891312 . with_context ( || format ! ( "Opening ESP at /{}" , self . esp_subdir) )
12901313 }
1314+
1315+ /// Mount the ESP at `<tmpdir>/<esp_subdir>` for the duration of `f`, then
1316+ /// unmount it again. Nothing is mounted there outside of calls to this
1317+ /// method, so callers that need to invoke bootloader-install tooling
1318+ /// that itself creates a private mount namespace (e.g. via `ChrootCmd`)
1319+ /// can safely do so without risking this mount being shadowed/duplicated
1320+ /// by that tooling's own mount-namespace setup.
1321+ pub ( crate ) fn with_esp < T > ( & self , f : impl FnOnce ( & Dir ) -> Result < T > ) -> Result < T > {
1322+ let esp_path = self . root_path ( ) . join ( self . esp_subdir ) ;
1323+ let _guard = mount_esp_at ( self . esp_device . as_str ( ) , esp_path)
1324+ . context ( "Mounting ESP into composefs root" ) ?;
1325+
1326+ self . esp_mounted . set ( true ) ;
1327+ // Reset `esp_mounted` when we return, including via `?` or panic,
1328+ // so a later `with_esp()` call (or a stray `open_esp_dir()` call
1329+ // after this one returns) doesn't see a stale "mounted" state.
1330+ struct ResetOnDrop < ' a > ( & ' a Cell < bool > ) ;
1331+ impl Drop for ResetOnDrop < ' _ > {
1332+ fn drop ( & mut self ) {
1333+ self . 0 . set ( false ) ;
1334+ }
1335+ }
1336+ let _reset = ResetOnDrop ( & self . esp_mounted ) ;
1337+
1338+ let dir = self . open_esp_dir ( ) ?;
1339+ f ( & dir)
1340+ }
12911341}
12921342
12931343pub struct SecurebootKeys {
@@ -1392,56 +1442,75 @@ pub(crate) async fn setup_composefs_boot(
13921442 postfetch. detected_bootloader,
13931443 Bootloader :: Grub | Bootloader :: GrubCC
13941444 ) {
1445+ let chroot_target = Utf8Path :: from_path ( mounted_root. root_path ( ) )
1446+ . ok_or_else ( || anyhow ! ( "composefs tmpdir path is not valid UTF-8" ) ) ?;
1447+ // Like the ostree backend, bind the physical root's real /boot (an
1448+ // ordinary ext4/xfs/... directory, not yet populated with kernels at
1449+ // this point) into the chroot. This gives bootupd both a correct
1450+ // filesystem to inspect for `--write-uuid` (rather than the ESP,
1451+ // which is otherwise mounted at the composefs root's own /boot) and
1452+ // an empty `boot/efi` directory for its EFI component to discover
1453+ // and mount the real ESP into, exactly as it would on ostree.
1454+ let bind_boot_path = root_setup. physical_root_path . join ( "boot" ) ;
13951455 crate :: bootloader:: install_via_bootupd (
13961456 & root_setup. device_info ,
13971457 & root_setup. physical_root_path ,
13981458 & state. config_opts ,
1399- None ,
1459+ Some ( chroot_target) ,
1460+ Some ( bind_boot_path. as_path ( ) ) ,
14001461 ) ?;
14011462
14021463 // FIXME: Remove this hack once we have support in bootupd
14031464 if matches ! ( postfetch. detected_bootloader, Bootloader :: GrubCC ) {
1465+ // bootupctl wrote this under the physical root's real /boot (via
1466+ // the bind mount above), not under the composefs root.
14041467 root_setup
14051468 . physical_root
1406- . remove_dir_all ( "boot/grub2" )
1469+ . remove_all_optional ( "boot/grub2" )
14071470 . context ( "removing grub2" ) ?;
14081471
1409- let ( os_id, ..) = parse_os_release ( mounted_root. dir ( ) ) ?
1410- . ok_or_else ( || anyhow:: anyhow!( "Failed to parse os-release" ) ) ?;
1411-
1412- let dir = format ! ( "EFI/{os_id}" ) ;
1413-
1414- // Files are in EFI/<os-name>/
1415- let efis_dir = mounted_root
1416- . open_esp_dir ( )
1417- . context ( "opening esp" ) ?
1418- . open_dir ( & dir)
1419- . with_context ( || format ! ( "Opening {dir}" ) ) ?;
1420-
1421- efis_dir
1422- . remove_file_optional ( "bootuuid.cfg" )
1423- . context ( "Removing bootuuid.cfg" ) ?;
1424- efis_dir
1425- . remove_file_optional ( "grub.cfg" )
1426- . context ( "Removing grub.cfg" ) ?;
1427-
1428- let final_name = match std:: env:: consts:: ARCH {
1429- "x86_64" => "grubx64.efi" ,
1430- "aarch64" => "grubaa64-cc.efi" ,
1431- arch => anyhow:: bail!( "GrubCC not supported for: {arch}" ) ,
1432- } ;
1472+ // install_via_bootupd above has already returned, so it's safe
1473+ // to mount the ESP here for the duration of this cleanup.
1474+ mounted_root. with_esp ( |esp_dir| {
1475+ let ( os_id, ..) = parse_os_release ( mounted_root. dir ( ) ) ?
1476+ . ok_or_else ( || anyhow:: anyhow!( "Failed to parse os-release" ) ) ?;
1477+
1478+ let dir = format ! ( "EFI/{os_id}" ) ;
1479+
1480+ // Files are in EFI/<os-name>/
1481+ let efis_dir = esp_dir
1482+ . open_dir ( & dir)
1483+ . with_context ( || format ! ( "Opening {dir}" ) ) ?;
1484+
1485+ efis_dir
1486+ . remove_file_optional ( "bootuuid.cfg" )
1487+ . context ( "Removing bootuuid.cfg" ) ?;
1488+ efis_dir
1489+ . remove_file_optional ( "grub.cfg" )
1490+ . context ( "Removing grub.cfg" ) ?;
1491+
1492+ let final_name = match std:: env:: consts:: ARCH {
1493+ "x86_64" => "grubx64.efi" ,
1494+ "aarch64" => "grubaa64-cc.efi" ,
1495+ arch => anyhow:: bail!( "GrubCC not supported for: {arch}" ) ,
1496+ } ;
1497+
1498+ mounted_root
1499+ . dir ( )
1500+ . copy ( "usr/lib/grub-cc/grub-cc.efi" , & efis_dir, final_name)
1501+ . context ( "Copying grub-cc binary" ) ?;
14331502
1434- mounted_root
1435- . dir ( )
1436- . copy ( "usr/lib/grub-cc/grub-cc.efi" , & efis_dir, final_name)
1437- . context ( "Copying grub-cc binary" ) ?;
1503+ Ok ( ( ) )
1504+ } ) ?;
14381505 }
14391506 } else {
1440- crate :: bootloader:: install_systemd_boot (
1441- & mounted_root,
1442- & state. config_opts ,
1443- get_secureboot_keys ( mounted_root. dir ( ) , BOOTC_AUTOENROLL_PATH ) ?,
1444- ) ?;
1507+ mounted_root. with_esp ( |_esp_dir| {
1508+ crate :: bootloader:: install_systemd_boot (
1509+ & mounted_root,
1510+ & state. config_opts ,
1511+ get_secureboot_keys ( mounted_root. dir ( ) , BOOTC_AUTOENROLL_PATH ) ?,
1512+ )
1513+ } ) ?;
14451514 }
14461515
14471516 let Some ( entry) = entries. iter ( ) . next ( ) else {
0 commit comments