From d34b57b287b6b8a8e15ed78e3e835e990921722a Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Tue, 22 Sep 2026 19:00:25 +0100 Subject: [PATCH] install: Read --root-ssh-authorized-keys before changing mounts prepare_install() mounts a tmpfs over /tmp and mirrors the host's /var/tmp before reading the --root-ssh-authorized-keys file, so a file bind mounted into the install container under /tmp was hidden and the install failed with "No such file or directory" even though the file was visible in the container. Read the file before touching mounts. Since we may re-exec afterwards (unshare, SELinux install_t) and run prepare_install() again with the mounts in place, carry the content to the child via the environment using a small bootc_utils::reexec helper shared by both re-exec sites. Mount the key file under /tmp in the integration test to cover this. Also make `just package` pull the base image with retries; the CI package job failed on a transient quay.io EOF during `podman build`. Generated-by: AI Signed-off-by: Eric Curtin --- Justfile | 2 + crates/lib/src/install.rs | 33 ++++++++++++---- crates/lib/src/lsm.rs | 3 +- crates/tests-integration/src/install.rs | 3 +- crates/utils/src/reexec.rs | 50 ++++++++++++++++++++++++- 5 files changed, 78 insertions(+), 13 deletions(-) diff --git a/Justfile b/Justfile index 86f14dd278..5ac42343c5 100644 --- a/Justfile +++ b/Justfile @@ -437,6 +437,8 @@ package: if [[ -z "{{no_auto_local_deps}}" ]]; then local_deps_args=$(cargo xtask local-rust-deps) fi + # Pull the base image up front with more retries than `podman build` defaults to + podman pull -q --retry 5 --retry-delay 5s {{base}} podman build {{base_buildargs}} --build-arg=SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} --build-arg=pkgversion=${VERSION} -t localhost/bootc-pkg --target=build $local_deps_args . mkdir -p "${packages}" rm -vf "${packages}"/*.rpm diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index dfa32ad929..0bf678d210 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -1569,6 +1569,9 @@ async fn verify_target_fetch( Ok(()) } +/// Carries the content of `--root-ssh-authorized-keys` across re-execs; see `prepare_install`. +const ROOT_SSH_AUTHORIZED_KEYS_ENV: &str = "_BOOTC_ROOT_SSH_AUTHORIZED_KEYS"; + /// Preparation for an install; validates and prepares some (thereafter immutable) global state. async fn prepare_install( mut config_opts: InstallConfigOpts, @@ -1695,6 +1698,28 @@ async fn prepare_install( anyhow::bail!("Bootloader set to none is not supported with the composefs backend"); } + // Read the file eagerly so we error out early, and before the mount changes + // below hide a file bind mounted under e.g. /tmp. We may re-exec further down + // and run this again with those mounts in place, so carry the content across + // via the environment. + let root_ssh_authorized_keys = config_opts + .root_ssh_authorized_keys + .as_ref() + .map(|p| -> Result { + use std::env::VarError; + match std::env::var(ROOT_SSH_AUTHORIZED_KEYS_ENV) { + // Set by our parent; further re-execs inherit our environment + Ok(v) => Ok(v), + Err(VarError::NotPresent) => { + let v = std::fs::read_to_string(p).with_context(|| format!("Reading {p}"))?; + bootc_utils::reexec::set_reexec_env(ROOT_SSH_AUTHORIZED_KEYS_ENV, &v); + Ok(v) + } + Err(e) => Err(e).with_context(|| format!("Parsing {ROOT_SSH_AUTHORIZED_KEYS_ENV}")), + } + }) + .transpose()?; + // We need to access devices that are set up by the host udev bootc_mount::ensure_mirrored_host_mount("/dev")?; // We need to read our own container image (and any logically bound images) @@ -1807,14 +1832,6 @@ async fn prepare_install( r }; - // Eagerly read the file now to ensure we error out early if e.g. it doesn't exist, - // instead of much later after we're 80% of the way through an install. - let root_ssh_authorized_keys = config_opts - .root_ssh_authorized_keys - .as_ref() - .map(|p| std::fs::read_to_string(p).with_context(|| format!("Reading {p}"))) - .transpose()?; - // Create our global (read-only) state which gets wrapped in an Arc // so we can pass it to worker threads too. Right now this just // combines our command line options along with some bind mounts from the host. diff --git a/crates/lib/src/lsm.rs b/crates/lib/src/lsm.rs index 38458f2ad0..4a7ebc2209 100644 --- a/crates/lib/src/lsm.rs +++ b/crates/lib/src/lsm.rs @@ -131,8 +131,7 @@ pub(crate) fn selinux_ensure_install() -> Result { let mut cmd = Command::new(&tmpf); cmd.env(guardenv, tmpf); cmd.env(bootc_utils::reexec::ORIG, srcpath); - cmd.args(std::env::args_os().skip(1)); - cmd.arg0(bootc_utils::NAME); + bootc_utils::reexec::prepare_reexec(&mut cmd); cmd.log_debug(); Err(anyhow::Error::msg(cmd.exec()).context("execve")) } diff --git a/crates/tests-integration/src/install.rs b/crates/tests-integration/src/install.rs index 21963ed25b..93cc746648 100644 --- a/crates/tests-integration/src/install.rs +++ b/crates/tests-integration/src/install.rs @@ -102,7 +102,8 @@ pub(crate) fn run_alongside(image: &str, mut testargs: libtest_mimic::Arguments) let tmp_keys = tmpd.path().join("test_authorized_keys"); let tmp_keys = tmp_keys.to_str().unwrap(); std::fs::write(&tmp_keys, b"ssh-ed25519 ABC0123 testcase@example.com")?; - cmd!(sh, "sudo {BASE_ARGS...} {target_args...} -v {tmp_keys}:/test_authorized_keys {image} bootc install to-filesystem --acknowledge-destructive --karg=foo=bar --replace=alongside --root-ssh-authorized-keys=/test_authorized_keys /target").run()?; + // Mount under /tmp, which the install later covers with a tmpfs + cmd!(sh, "sudo {BASE_ARGS...} {target_args...} -v {tmp_keys}:/tmp/test_authorized_keys {image} bootc install to-filesystem --acknowledge-destructive --karg=foo=bar --replace=alongside --root-ssh-authorized-keys=/tmp/test_authorized_keys /target").run()?; // Also test install finalize here cmd!( diff --git a/crates/utils/src/reexec.rs b/crates/utils/src/reexec.rs index 7dd6e5941e..4fbb7d10ef 100644 --- a/crates/utils/src/reexec.rs +++ b/crates/utils/src/reexec.rs @@ -1,9 +1,36 @@ +use std::ffi::OsString; use std::os::unix::process::CommandExt; use std::path::PathBuf; use std::process::Command; +use std::sync::Mutex; use anyhow::Result; +/// Environment variables to set on re-executions of ourself; see [`set_reexec_env`]. +static REEXEC_ENV: Mutex> = Mutex::new(Vec::new()); + +/// Record an environment variable to set on any subsequent re-execution of ourself. +/// +/// This carries state computed before a re-exec (e.g. the content of a file that is +/// no longer visible after we change mounts) into the new process without mutating +/// our own environment, which is not thread safe. +pub fn set_reexec_env(k: impl Into, v: impl Into) { + let mut env = REEXEC_ENV.lock().unwrap(); + let k = k.into(); + env.retain(|(existing, _)| *existing != k); + env.push((k, v.into())); +} + +/// Set up `cmd` to re-execute ourself: pass along our arguments, `argv[0]` +/// and the environment recorded via [`set_reexec_env`]. +pub fn prepare_reexec(cmd: &mut Command) { + for (k, v) in REEXEC_ENV.lock().unwrap().iter() { + cmd.env(k, v); + } + cmd.args(std::env::args_os().skip(1)); + cmd.arg0(crate::NAME); +} + /// Environment variable holding a reference to our original binary pub const ORIG: &str = "_BOOTC_ORIG_EXE"; @@ -35,8 +62,27 @@ pub fn reexec_with_guardenv(k: &str, prefix_args: &[&str]) -> Result<()> { Command::new(self_exe) }; cmd.env(k, "1"); - cmd.args(std::env::args_os().skip(1)); - cmd.arg0(crate::NAME); + prepare_reexec(&mut cmd); tracing::debug!("Re-executing current process for {k}"); Err(cmd.exec().into()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_reexec_env() { + set_reexec_env("_BOOTC_TEST_A", "1"); + set_reexec_env("_BOOTC_TEST_A", "2"); + set_reexec_env("_BOOTC_TEST_B", "3"); + let mut cmd = Command::new("true"); + prepare_reexec(&mut cmd); + let env: Vec<_> = cmd + .get_envs() + .filter_map(|(k, v)| Some((k.to_str()?, v?.to_str()?))) + .filter(|(k, _)| k.starts_with("_BOOTC_TEST_")) + .collect(); + assert_eq!(env, [("_BOOTC_TEST_A", "2"), ("_BOOTC_TEST_B", "3")]); + } +}