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")]); + } +}