From 999007f6f805ee1bdd5c5287f120dda32269dd86 Mon Sep 17 00:00:00 2001 From: James Forcier Date: Sun, 14 Dec 2025 09:30:30 -0500 Subject: [PATCH 1/2] lib: Pass absolute authfile path when pulling LBIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ostree-ext explicitly handles authfile paths as relative; this works fine for most callers of get_global_authfile, as they only read the returned open file descriptor, and ignore the path. However, pulling logically bound images requires passing the actual authfile path to Podman, so we must resolve the absolute path in this case - otherwise, we see errors like the following: ``` [root@fedora ~]# bootc upgrade layers already present: 69; layers needed: 1 (242.2 MB) Fetched layers: 230.95 MiB in 3 seconds (90.88 MiB/s) Deploying: done (3 seconds) Fetching bound image: quay.io/prometheus/node-exporter:v1.10.2: done (0 seconds) error: Upgrading: Staging: Pulling bound images: Pulling bound images: Failed to pull image: Subprocess failed: ExitStatus(unix_wait_status(32000)) Error: credential file is not accessible: faccessat etc/ostree/auth.json: no such file or directory ``` Since cap_std::fs::Dir intentionally does not expose its filesystem path, we must resort to reconstructing it from a file descriptor. We could do this by inspectingthe file descriptor for `sysroot` and combining that with the relative path returned by get_global_authfile, but since get_global_authfile returns the descriptor of the actual authfile, we can simply read that directly. Signed-off-by: James Forcier --- crates/lib/src/podstorage.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/lib/src/podstorage.rs b/crates/lib/src/podstorage.rs index eaff56572..65081ed6d 100644 --- a/crates/lib/src/podstorage.rs +++ b/crates/lib/src/podstorage.rs @@ -24,7 +24,7 @@ use cap_std_ext::cmdext::CapStdExtCommandExt; use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; use ostree_ext::ostree::{self}; -use std::os::fd::OwnedFd; +use std::os::fd::{AsRawFd, OwnedFd}; use tokio::process::Command as AsyncCommand; // Pass only 100 args at a time just to avoid potentially overflowing argument @@ -353,10 +353,15 @@ impl CStorage { cmd.stdin(Stdio::null()); cmd.stdout(Stdio::null()); cmd.args(["pull", image]); - let authfile = ostree_ext::globals::get_global_authfile(&self.sysroot)? - .map(|(authfile, _fd)| authfile); - if let Some(authfile) = authfile { - cmd.args(["--authfile", authfile.as_str()]); + let authfile_fd = + ostree_ext::globals::get_global_authfile(&self.sysroot)?.map(|(_authfile, fd)| fd); + if let Some(fd) = authfile_fd { + let authfile_path = std::fs::read_link(format!("/proc/self/fd/{}", fd.as_raw_fd())) + .map_err(Into::into) + .and_then(|p| { + Utf8PathBuf::from_path_buf(p).map_err(|_| anyhow::anyhow!("Invalid UTF-8")) + })?; + cmd.args(["--authfile", authfile_path.as_str()]); } tracing::debug!("Pulling image: {image}"); let mut cmd = AsyncCommand::from(cmd); From 37382b6559a6be692cf94a8f55f0f2bb522f7671 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Tue, 16 Dec 2025 09:29:03 -0500 Subject: [PATCH 2/2] podstorage: Improve authfile handling more We should match exactly the logic we use with containers-image-proxy. - If bootc doesn't have auth setup, then we need to not let podman fall back to the defaults - Always pass a copy of the auth in a tempfile so we aren't reliant on absolute paths as we're continually trying to reduce our usage of those. Signed-off-by: Colin Walters --- crates/lib/src/podstorage.rs | 49 +++++++++++++------ .../010-test-bootc-container-store.nu | 8 +++ .../booted/test-logically-bound-switch.nu | 5 ++ 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/crates/lib/src/podstorage.rs b/crates/lib/src/podstorage.rs index 65081ed6d..4b9d398cb 100644 --- a/crates/lib/src/podstorage.rs +++ b/crates/lib/src/podstorage.rs @@ -9,7 +9,7 @@ //! At the current time, this is only used for Logically Bound Images. use std::collections::HashSet; -use std::io::Seek; +use std::io::{Seek, Write}; use std::os::unix::process::CommandExt; use std::process::{Command, Stdio}; use std::sync::Arc; @@ -17,14 +17,14 @@ use std::sync::Arc; use anyhow::{Context, Result}; use bootc_utils::{AsyncCommandRunExt, CommandRunExt, ExitStatusExt}; use camino::{Utf8Path, Utf8PathBuf}; -use cap_std_ext::cap_std; use cap_std_ext::cap_std::fs::Dir; use cap_std_ext::cap_tempfile::TempDir; use cap_std_ext::cmdext::CapStdExtCommandExt; use cap_std_ext::dirext::CapStdExtDirExt; +use cap_std_ext::{cap_std, cap_tempfile}; use fn_error_context::context; use ostree_ext::ostree::{self}; -use std::os::fd::{AsRawFd, OwnedFd}; +use std::os::fd::{AsFd, AsRawFd, OwnedFd}; use tokio::process::Command as AsyncCommand; // Pass only 100 args at a time just to avoid potentially overflowing argument @@ -119,11 +119,38 @@ fn bind_storage_roots(cmd: &mut Command, storage_root: &Dir, run_root: &Dir) -> Ok(()) } -fn new_podman_cmd_in(storage_root: &Dir, run_root: &Dir) -> Result { +// Initialize a `podman` subprocess with: +// - storage overridden to point to to storage_root +// - Authentication (auth.json) using the bootc/ostree owned auth +fn new_podman_cmd_in(sysroot: &Dir, storage_root: &Dir, run_root: &Dir) -> Result { let mut cmd = Command::new("podman"); bind_storage_roots(&mut cmd, storage_root, run_root)?; let run_root = format!("/proc/self/fd/{STORAGE_RUN_FD}"); cmd.args(["--root", STORAGE_ALIAS_DIR, "--runroot", run_root.as_str()]); + + let tmpd = &cap_std::fs::Dir::open_ambient_dir("/tmp", cap_std::ambient_authority())?; + let mut tempfile = cap_tempfile::TempFile::new_anonymous(tmpd).map(std::io::BufWriter::new)?; + + // Keep this in sync with https://github.com/bootc-dev/containers-image-proxy-rs/blob/b5e0861ad5065f47eaf9cda0d48da3529cc1bc43/src/imageproxy.rs#L310 + // We always override the auth to match the bootc setup. + let authfile_fd = ostree_ext::globals::get_global_authfile(sysroot)?.map(|v| v.1); + if let Some(mut fd) = authfile_fd { + std::io::copy(&mut fd, &mut tempfile)?; + } else { + // Note that if there's no bootc-owned auth, then we force an empty authfile to ensure + // that podman doesn't fall back to searching the user-owned paths. + tempfile.write_all(b"{}")?; + } + + let tempfile = tempfile + .into_inner() + .map_err(|e| e.into_error())? + .into_std(); + let fd: Arc = std::sync::Arc::new(tempfile.into()); + let target_fd = fd.as_fd().as_raw_fd(); + cmd.take_fd_n(fd, target_fd); + cmd.env("REGISTRY_AUTH_FILE", format!("/proc/self/fd/{target_fd}")); + Ok(cmd) } @@ -167,7 +194,7 @@ impl CStorage { /// Create a `podman image` Command instance prepared to operate on our alternative /// root. pub(crate) fn new_image_cmd(&self) -> Result { - let mut r = new_podman_cmd_in(&self.storage_root, &self.run)?; + let mut r = new_podman_cmd_in(&self.sysroot, &self.storage_root, &self.run)?; // We want to limit things to only manipulating images by default. r.arg("image"); Ok(r) @@ -233,7 +260,7 @@ impl CStorage { // There's no explicit API to initialize a containers-storage: // root, simply passing a path will attempt to auto-create it. // We run "podman images" in the new root. - new_podman_cmd_in(&storage_root, &run)? + new_podman_cmd_in(&sysroot, &storage_root, &run)? .stdout(Stdio::null()) .arg("images") .run_capture_stderr() @@ -353,16 +380,6 @@ impl CStorage { cmd.stdin(Stdio::null()); cmd.stdout(Stdio::null()); cmd.args(["pull", image]); - let authfile_fd = - ostree_ext::globals::get_global_authfile(&self.sysroot)?.map(|(_authfile, fd)| fd); - if let Some(fd) = authfile_fd { - let authfile_path = std::fs::read_link(format!("/proc/self/fd/{}", fd.as_raw_fd())) - .map_err(Into::into) - .and_then(|p| { - Utf8PathBuf::from_path_buf(p).map_err(|_| anyhow::anyhow!("Invalid UTF-8")) - })?; - cmd.args(["--authfile", authfile_path.as_str()]); - } tracing::debug!("Pulling image: {image}"); let mut cmd = AsyncCommand::from(cmd); cmd.run().await.context("Failed to pull image")?; diff --git a/tmt/tests/booted/readonly/010-test-bootc-container-store.nu b/tmt/tests/booted/readonly/010-test-bootc-container-store.nu index a7ac5b6c0..ef23a039d 100644 --- a/tmt/tests/booted/readonly/010-test-bootc-container-store.nu +++ b/tmt/tests/booted/readonly/010-test-bootc-container-store.nu @@ -15,6 +15,14 @@ if $is_composefs { # And verify this works bootc image cmd list -q o>/dev/null + + bootc image cmd pull busybox + podman --storage-opt=additionalimagestore=/usr/lib/bootc/storage image exists busybox + + 'corrupted JSON!@#%!@#' | save -f /run/ostree/auth.json + let e = bootc image cmd pull busybox | complete | get exit_code + assert not equal $e 0 + rm -v /run/ostree/auth.json } tap ok diff --git a/tmt/tests/booted/test-logically-bound-switch.nu b/tmt/tests/booted/test-logically-bound-switch.nu index 7e579ed69..fbe69a9da 100644 --- a/tmt/tests/booted/test-logically-bound-switch.nu +++ b/tmt/tests/booted/test-logically-bound-switch.nu @@ -21,6 +21,11 @@ bootc status let st = bootc status --json | from json let booted = $st.status.booted.image +# The tests here aren't fetching from a registry which requires auth by default, +# but we can replicate the failure in https://github.com/bootc-dev/bootc/pull/1852 +# by just injecting any auth file. +echo '{}' | save -f /run/ostree/auth.json + def initial_setup [] { bootc image copy-to-storage podman images