Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 25 additions & 8 deletions crates/lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<String> {
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)
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions crates/lib/src/lsm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,7 @@ pub(crate) fn selinux_ensure_install() -> Result<bool> {
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"))
}
Expand Down
3 changes: 2 additions & 1 deletion crates/tests-integration/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
50 changes: 48 additions & 2 deletions crates/utils/src/reexec.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<(OsString, OsString)>> = Mutex::new(Vec::new());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only ever call this from one thread so I'm sure if the mutex is required. Also, not a fan of this being a global var. We should only ever re-exec when we are installing, so putting this in prepare_install would make sense and passing in the vector of env vars to reexec_with_guardenv as an extra_args param?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a mutex or equiv around any static, no problem with that from my PoV.

Also, not a fan of this being a global var.

Yes, but doing it differently would require threading this state from the install code into the lsm code...doable but ugly in a different way.

BTW I would generalize this slightly and e.g.:

  • Define a struct we can serialize to JSON of stuff we need to save between re-exec
  • In the install path, gather that state before we re-exec
  • Serialize it to a memfd
  • Set an env var _BOOTC_INSTALL_REEXEC_STATE
  • Deserialize it early in the install path

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept the global per Colin (threading it from install into lsm was the alternative), but folded the env application together with the argv/argv0 setup into a single prepare_reexec() used by both re-exec sites, so the duplication there is gone too, plus a unit test.

On the memfd/JSON generalization: set_reexec_env is already generic over key/value, and the only payload today is a small text file, so I left the env approach for now. Happy to move to a memfd-backed struct if we grow more state to carry across re-exec (or if key files near the 128KiB per-env-string limit turn out to be a real concern).


/// 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<OsString>, v: impl Into<OsString>) {
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";

Expand Down Expand Up @@ -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")]);
}
}
Loading