From 024643ef4830c529ef8b541f0f09891586b52a5b Mon Sep 17 00:00:00 2001 From: Shubham Samrat Date: Sat, 24 Oct 2026 21:41:29 +0900 Subject: [PATCH] podstorage: Make the storage bind mount namespace private bind_storage_roots() unshares a mount namespace in the podman child and bind mounts the storage root onto /run/bootc/storage, but never changes the propagation of the new namespace. `install --source-imgref` skips our usual unshare, so when run directly on a systemd host, where / and /run are shared, the child's mounts are peers of the host's and the bind propagates back. It stays mounted on the host's /run/bootc/storage (and /target/run/bootc/storage) after bootc exits. If the storage directory is later removed, e.g. `rm -rf /ostree` before retrying a failed install, the leaked mount's root is deleted and every following bind onto /run/bootc/storage fails with ENOENT until reboot: error: Installing to filesystem: Creating imgstorage: Initializing images: No such file or directory (os error 2) Mark the new namespace recursively private right after the unshare, as ChrootCmd already does, so the bind stays inside the child. Add a booted test that installs with / made shared inside run_install's MountFlags=slave namespace and checks that /run/bootc/storage is not left mounted. Closes: https://github.com/bootc-dev/bootc/issues/2455 Assisted-by: AI Signed-off-by: Shubham Samrat --- crates/lib/src/podstorage.rs | 11 +++++ tmt/plans/integration.fmf | 8 ++++ .../booted/test-install-shared-propagation.nu | 41 +++++++++++++++++++ tmt/tests/tests.fmf | 5 +++ 4 files changed, 65 insertions(+) create mode 100644 tmt/tests/booted/test-install-shared-propagation.nu diff --git a/crates/lib/src/podstorage.rs b/crates/lib/src/podstorage.rs index fa940a4cf5..d55aa2d9b2 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 805a7c125a..9246137535 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 0000000000..36d9d5dfe0 --- /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 a344255455..1147151541 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