diff --git a/lib/src/cli.rs b/lib/src/cli.rs index d1cf0d9ce..5596704e2 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -3,11 +3,12 @@ //! Command line tool to manage bootable ostree-based containers. use std::ffi::{CString, OsStr, OsString}; -use std::io::Seek; +use std::io::{BufRead, Seek}; use std::os::unix::process::CommandExt; use std::process::Command; +use std::sync::OnceLock; -use anyhow::{Context, Result}; +use anyhow::{ensure, Context, Result}; use camino::Utf8PathBuf; use cap_std_ext::cap_std; use cap_std_ext::cap_std::fs::Dir; @@ -20,6 +21,7 @@ use ostree_ext::container as ostree_container; use ostree_ext::container_utils::ostree_booted; use ostree_ext::keyfileext::KeyFileExt; use ostree_ext::ostree; +use regex::Regex; use schemars::schema_for; use serde::{Deserialize, Serialize}; @@ -576,15 +578,46 @@ pub(crate) async fn get_storage() -> Result { } #[context("Querying root privilege")] -pub(crate) fn require_root() -> Result<()> { - let uid = rustix::process::getuid(); - if !uid.is_root() { - anyhow::bail!("This command requires root privileges"); - } - if !rustix::thread::capability_is_in_bounding_set(rustix::thread::Capability::SystemAdmin)? { - anyhow::bail!("This command requires full root privileges (CAP_SYS_ADMIN)"); - } +pub(crate) fn require_root(is_container: bool) -> Result<()> { + ensure!( + rustix::process::getuid().is_root(), + match is_container { + true => + "The user inside the container from which you are running this command must be root", + false => "This command must be executed as the root user", + } + ); + + ensure!( + rustix::thread::capability_is_in_bounding_set(rustix::thread::Capability::SystemAdmin)?, + match is_container { + true => "The container must be executed with the podman --privileged flag", + false => "This command requires full root privileges (CAP_SYS_ADMIN)", + } + ); + tracing::trace!("Verified uid 0 with CAP_SYS_ADMIN"); + + Ok(()) +} + +#[context("Querying root podman")] +pub(crate) fn require_root_podman() -> Result<()> { + let proc_self = Dir::open_ambient_dir("/proc/self", cap_std::ambient_authority())?; + let uid_map = proc_self.open("uid_map")?; + let uid_map = std::io::BufReader::new(uid_map); + + let map_lines = uid_map.lines().collect::>>()?; + + // TODO: I'm unaware of more official channels to check for rootless podman, we should + // probably move to these if and when they exist + static REGEX: OnceLock = OnceLock::new(); + let root_pattern = REGEX.get_or_init(|| Regex::new(r#"^\s+0\s+0\s+\d+"#).expect("regex")); + ensure!( + map_lines.into_iter().any(|l| root_pattern.is_match(&l)), + "rootless podman unsupported, please run podman as root" + ); + Ok(()) } @@ -616,7 +649,7 @@ fn prepare_for_write() -> Result<()> { ostree_booted()?, "This command requires an ostree-booted host system" ); - crate::cli::require_root()?; + crate::cli::require_root(false)?; ensure_self_unshared_mount_namespace()?; if crate::lsm::selinux_enabled()? && !crate::lsm::selinux_ensure_install()? { tracing::warn!("Do not have install_t capabilities"); diff --git a/lib/src/install.rs b/lib/src/install.rs index 368794ef0..6754e314d 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -1003,7 +1003,7 @@ pub(crate) fn finalize_filesystem( /// A heuristic check that we were invoked with --pid=host fn require_host_pidns() -> Result<()> { if rustix::process::getpid().is_init() { - anyhow::bail!("This command must be run with --pid=host") + anyhow::bail!("This command must be run with the podman --pid=host flag") } tracing::trace!("OK: we're not pid 1"); Ok(()) @@ -1154,8 +1154,6 @@ async fn prepare_install( target_opts: InstallTargetOpts, ) -> Result> { tracing::trace!("Preparing install"); - // We need full root privileges, i.e. --privileged in podman - crate::cli::require_root()?; let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority()) .context("Opening /")?; @@ -1163,9 +1161,12 @@ async fn prepare_install( let external_source = source_opts.source_imgref.is_some(); let source = match source_opts.source_imgref { None => { - if !host_is_container { - anyhow::bail!("Either --source-imgref must be defined or this command must be executed inside a podman container.") - } + ensure!(host_is_container, "Either --source-imgref must be defined or this command must be executed inside a podman container."); + + crate::cli::require_root(true)?; + + crate::cli::require_root_podman()?; + require_host_pidns()?; // Out of conservatism we only verify the host userns path when we're expecting // to do a self-install (e.g. not bootc-image-builder or equivalent). @@ -1187,7 +1188,10 @@ async fn prepare_install( SourceInfo::from_container(&rootfs, &container_info)? } - Some(source) => SourceInfo::from_imageref(&source, &rootfs)?, + Some(source) => { + crate::cli::require_root(false)?; + SourceInfo::from_imageref(&source, &rootfs)? + } }; // Parse the target CLI image reference options and create the *target* image diff --git a/lib/src/install/completion.rs b/lib/src/install/completion.rs index 1cc33345f..e37d2865a 100644 --- a/lib/src/install/completion.rs +++ b/lib/src/install/completion.rs @@ -197,7 +197,7 @@ pub(crate) async fn run_from_anaconda(rootfs: &Dir) -> Result<()> { // unshare our mount namespace, so any *further* mounts aren't leaked. // Note that because this does a re-exec, anything *before* this point // should be idempotent. - crate::cli::require_root()?; + crate::cli::require_root(false)?; crate::cli::ensure_self_unshared_mount_namespace()?; if std::env::var_os(ANACONDA_ENV_HINT).is_none() { @@ -245,7 +245,7 @@ pub(crate) async fn run_from_anaconda(rootfs: &Dir) -> Result<()> { /// From ostree-rs-ext, run through the rest of bootc install functionality pub async fn run_from_ostree(rootfs: &Dir, sysroot: &Utf8Path, stateroot: &str) -> Result<()> { - crate::cli::require_root()?; + crate::cli::require_root(false)?; // Load sysroot from the provided path let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(sysroot))); sysroot.load(gio::Cancellable::NONE)?;