diff --git a/crates/lib/src/podstorage.rs b/crates/lib/src/podstorage.rs index fa940a4cf..d55aa2d9b 100644 --- a/crates/lib/src/podstorage.rs +++ b/crates/lib/src/podstorage.rs @@ -126,6 +126,7 @@ pub(crate) fn bind_storage_roots( // But...how did we avoid that circular dependency by using the process cwd? // // I tried making the mounts recursively private, but that didn't help. + // (We do make them private below, but for a different reason.) let oldwd = rustix::fs::open( ".", OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::RDONLY, @@ -133,6 +134,16 @@ pub(crate) fn bind_storage_roots( )?; rustix::process::fchdir(&storage_root)?; rustix::thread::unshare_unsafe(rustix::thread::UnshareFlags::NEWNS)?; + // The new namespace inherits the caller's propagation. If that is shared + // (e.g. `install to-existing-root --source-imgref` run directly on a systemd + // host, which skips our usual unshare), the bind below would propagate back + // and stay mounted on the host's /run/bootc/storage after we exit. + // https://github.com/bootc-dev/bootc/issues/2455 + rustix::mount::mount_change( + c"/", + rustix::mount::MountPropagationFlags::PRIVATE + | rustix::mount::MountPropagationFlags::REC, + )?; rustix::mount::mount_bind(".", STORAGE_ALIAS_DIR)?; rustix::process::fchdir(&oldwd)?; Ok(()) diff --git a/tmt/plans/integration.fmf b/tmt/plans/integration.fmf index 805a7c125..924613753 100644 --- a/tmt/plans/integration.fmf +++ b/tmt/plans/integration.fmf @@ -392,4 +392,12 @@ execute: enabled: true extra-try_bind_storage: true extra-skip_if_ostree: true + +/plan-63-install-shared-propagation: + summary: Install from a shared mount namespace without leaking the storage bind mount + discover: + how: fmf + test: + - /tmt/tests/tests/test-63-install-shared-propagation + extra-fixme_skip_if_composefs: true # END GENERATED PLANS diff --git a/tmt/tests/booted/test-install-shared-propagation.nu b/tmt/tests/booted/test-install-shared-propagation.nu new file mode 100644 index 000000000..36d9d5dfe --- /dev/null +++ b/tmt/tests/booted/test-install-shared-propagation.nu @@ -0,0 +1,41 @@ +# number: 63 +# tmt: +# summary: Install from a shared mount namespace without leaking the storage bind mount +# duration: 30m +# extra: +# # bootloader=none is not supported for composefs (see test-install-no-boot-dir.nu) +# fixme_skip_if_composefs: true +# +# Regression test for https://github.com/bootc-dev/bootc/issues/2455 +# +# Installing with --source-imgref skips bootc's own mount namespace unshare, +# so on a host where / is shared (the systemd default) the bind mount made +# for podman onto /run/bootc/storage used to propagate back and stay mounted. +# run_install already uses MountFlags=slave, which hides that, so make / +# shared again inside that namespace before installing. + +use std assert +use tap.nu + +def main [] { + tap begin "install to-filesystem with shared mount propagation" + + bootc image copy-to-storage + let target_image = "containers-storage:localhost/bootc" + + mkdir /var/mnt + truncate -s 10G disk.img + mkfs.ext4 disk.img + mount -o loop disk.img /var/mnt + + setenforce 0 + + tap run_install $"mount --make-rshared / +bootc install to-filesystem --disable-selinux --bootloader=none --source-imgref ($target_image) /var/mnt +if findmnt /run/bootc/storage; then echo 'error: /run/bootc/storage mount leaked'; exit 1; fi" + + umount /var/mnt + rm -f disk.img + + tap ok +} diff --git a/tmt/tests/tests.fmf b/tmt/tests/tests.fmf index a34425545..114715154 100644 --- a/tmt/tests/tests.fmf +++ b/tmt/tests/tests.fmf @@ -251,3 +251,8 @@ check: - when: boot_type == aboot enabled: true test: python3 booted/test-aboot-update-rollback.py + +/test-63-install-shared-propagation: + summary: Install from a shared mount namespace without leaking the storage bind mount + duration: 30m + test: nu booted/test-install-shared-propagation.nu