diff --git a/Cargo.lock b/Cargo.lock index 8434a04c..245a5971 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,12 +88,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.11.0" @@ -178,6 +172,7 @@ dependencies = [ "bootc-internal-blockdev", "bootc-internal-utils", "camino", + "cap-std", "cap-std-ext", "chrono", "clap", @@ -189,8 +184,6 @@ dependencies = [ "libc", "libsystemd", "log", - "openat", - "openat-ext", "openssl", "os-release", "regex", @@ -784,7 +777,7 @@ dependencies = [ "hmac", "libc", "log", - "nix 0.29.0", + "nix", "nom 8.0.0", "once_cell", "serde", @@ -836,15 +829,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -871,30 +855,17 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "nix" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" -dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if", - "libc", - "memoffset 0.6.5", -] - [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.11.0", + "bitflags", "cfg-if", "cfg_aliases", "libc", - "memoffset 0.9.1", + "memoffset", ] [[package]] @@ -1021,34 +992,13 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "openat" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95aa7c05907b3ebde2610d602f4ddd992145cc6a84493647c30396f30ba83abe" -dependencies = [ - "libc", -] - -[[package]] -name = "openat-ext" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cf3e4baa7f516441f58373f58aaf6e91a5dfa2e2b50e68a0d313b082014c61d" -dependencies = [ - "libc", - "nix 0.23.2", - "openat", - "rand 0.8.5", -] - [[package]] name = "openssl" version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.11.0", + "bitflags", "cfg-if", "foreign-types", "libc", @@ -1264,7 +1214,7 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "422ffe5b31ea4c7c76a361dc29b986e11fb6987313453088e1fd95f34ecc1ba6" dependencies = [ - "bitflags 2.11.0", + "bitflags", "cpio", "digest", "enum-display-derive", @@ -1288,7 +1238,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -1763,7 +1713,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags", "hashbrown 0.15.5", "indexmap", "semver", @@ -2027,7 +1977,7 @@ version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" dependencies = [ - "bitflags 2.11.0", + "bitflags", "windows-sys 0.59.0", ] @@ -2089,7 +2039,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags", "indexmap", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index f7c9fae1..490a7952 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,8 +35,7 @@ hex = "0.4.3" libc = "^0.2" libsystemd = ">= 0.3, < 0.8" log = "^0.4" -openat = "0.1.20" -openat-ext = ">= 0.2.2, < 0.3.0" +cap-std = "4.0.2" openssl = "^0.10" os-release = "0.1.0" regex = "1.12.3" diff --git a/src/backend/statefile.rs b/src/backend/statefile.rs index 83ffd6b2..4f7bef26 100644 --- a/src/backend/statefile.rs +++ b/src/backend/statefile.rs @@ -3,9 +3,11 @@ use crate::model::SavedState; use crate::util::SignalTerminationGuard; use anyhow::{bail, Context, Result}; +use cap_std::ambient_authority; +use cap_std::fs::{Dir, Permissions, PermissionsExt}; +use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; use fs2::FileExt; -use openat_ext::OpenatDirExt; use std::fs::File; use std::io::prelude::*; use std::path::Path; @@ -23,9 +25,18 @@ impl SavedState { /// While ordinarily the daemon runs as a systemd unit (which implicitly /// ensures a single instance) this is a double check against other /// execution paths. - pub(crate) fn acquire_write_lock(sysroot: openat::Dir) -> Result { - let lockfile = sysroot.write_file(Self::WRITE_LOCK_PATH, 0o644)?; - lockfile.lock_exclusive()?; + pub(crate) fn acquire_write_lock(sysroot: Dir) -> Result { + sysroot + .atomic_write_with_perms(Self::WRITE_LOCK_PATH, "", Permissions::from_mode(0o644)) + .context("Creating lock file")?; + + let lockfile = sysroot + .open(Self::WRITE_LOCK_PATH) + .context("Opening lock file")? + .into_std(); + + lockfile.lock_exclusive().context("Acquiring lock")?; + let guard = StateLockGuard { sysroot, termguard: Some(SignalTerminationGuard::new()?), @@ -36,7 +47,7 @@ impl SavedState { /// Use this for cases when the target root isn't booted, which is /// offline installs. - pub(crate) fn unlocked(sysroot: openat::Dir) -> Result { + pub(crate) fn unlocked(sysroot: Dir) -> Result { Ok(StateLockGuard { sysroot, termguard: None, @@ -48,11 +59,11 @@ impl SavedState { #[context("Loading saved state")] pub(crate) fn load_from_disk(root_path: impl AsRef) -> Result> { let root_path = root_path.as_ref(); - let sysroot = openat::Dir::open(root_path) + let sysroot = Dir::open_ambient_dir(root_path, ambient_authority()) .with_context(|| format!("opening sysroot '{}'", root_path.display()))?; let statefile_path = Path::new(Self::STATEFILE_DIR).join(Self::STATEFILE_NAME); - let saved_state = if let Some(statusf) = sysroot.open_file_optional(&statefile_path)? { + let saved_state = if let Some(statusf) = sysroot.open_optional(&statefile_path)? { let mut bufr = std::io::BufReader::new(statusf); let mut s = String::new(); bufr.read_to_string(&mut s)?; @@ -92,7 +103,7 @@ impl SavedState { /// Write-lock guard for statefile, protecting against concurrent state updates. #[derive(Debug)] pub(crate) struct StateLockGuard { - pub(crate) sysroot: openat::Dir, + pub(crate) sysroot: Dir, #[allow(dead_code)] termguard: Option, #[allow(dead_code)] @@ -102,11 +113,16 @@ pub(crate) struct StateLockGuard { impl StateLockGuard { /// Atomically replace the on-disk state with a new version. pub(crate) fn update_state(&mut self, state: &SavedState) -> Result<()> { - let subdir = self.sysroot.sub_dir(SavedState::STATEFILE_DIR)?; - subdir.write_file_with_sync(SavedState::STATEFILE_NAME, 0o644, |w| -> Result<()> { - serde_json::to_writer(w, state)?; - Ok(()) - })?; + let subdir = self.sysroot.open_dir(SavedState::STATEFILE_DIR)?; + + subdir + .atomic_write_with_perms( + SavedState::STATEFILE_NAME, + serde_json::to_vec(state).context("Serializing state")?, + Permissions::from_mode(0o644), + ) + .context("Writing state file")?; + Ok(()) } } diff --git a/src/bios.rs b/src/bios.rs index ce7fc540..1ee0b51a 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -1,6 +1,8 @@ use anyhow::{bail, Context, Result}; use camino::Utf8PathBuf; -use openat_ext::OpenatDirExt; +use cap_std::ambient_authority; +use cap_std::fs::Dir; +use cap_std_ext::dirext::CapStdExtDirExt; use std::io::prelude::*; use std::path::Path; use std::process::Command; @@ -109,7 +111,7 @@ impl Component for Bios { ) -> Result { let device = device.ok_or_else(|| anyhow::anyhow!("BIOS component requires a target device"))?; - let src_dir = openat::Dir::open(src_root) + let src_dir = Dir::open_ambient_dir(src_root, ambient_authority()) .with_context(|| format!("opening source directory {src_root}"))?; let Some(meta) = get_component_update(&src_dir, self)? else { anyhow::bail!("No update metadata for component {} found", self.name()); @@ -151,11 +153,11 @@ impl Component for Bios { // - Backup "/boot/loader/grub.cfg" to "/boot/grub2/grub.cfg.bak" // - Remove symlink "/boot/grub2/grub.cfg" // - Replace "/boot/grub2/grub.cfg" symlink with new static "grub.cfg" - fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &openat::Dir) -> Result<()> { + fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &Dir) -> Result<()> { let grub = "boot/grub2"; // sysroot_path is /, destdir is Dir of / let grub_config_path = Utf8PathBuf::from(sysroot_path).join(grub); - let grub_config_dir = destdir.sub_dir(grub).context("Opening boot/grub2")?; + let grub_config_dir = destdir.open_dir(grub).context("Opening boot/grub2")?; let grub_config = grub_config_path.join(grubconfigs::GRUBCONFIG); @@ -201,7 +203,7 @@ impl Component for Bios { } // Synchronize the filesystem containing /boot/grub2 to disk. - fsfreeze_thaw_cycle(grub_config_dir.open_file(".")?)?; + fsfreeze_thaw_cycle(grub_config_dir.reopen_as_ownedfd()?)?; Ok(()) } @@ -243,13 +245,13 @@ impl Component for Bios { })) } - fn query_update(&self, sysroot: &openat::Dir) -> Result> { + fn query_update(&self, sysroot: &Dir) -> Result> { get_component_update(sysroot, self) } - fn query_requires_update(&self, sysroot: &openat::Dir) -> Result<()> { + fn query_requires_update(&self, sysroot: &Dir) -> Result<()> { // Failed as expected if booted with BIOS and no update metadata - if !sysroot.exists("sys/firmware/efi")? { + if !sysroot.exists("sys/firmware/efi") { anyhow::bail!("Failed to find BIOS update metadata"); } Ok(()) diff --git a/src/bootupd.rs b/src/bootupd.rs index 99558be9..16a49138 100644 --- a/src/bootupd.rs +++ b/src/bootupd.rs @@ -21,18 +21,19 @@ use crate::model::{ComponentStatus, ComponentUpdatable, ContentMetadata, SavedSt use crate::{ostreeutil, util}; use anyhow::{anyhow, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; +use cap_std_ext::dirext::CapStdExtDirExt; use clap::crate_version; use fn_error_context::context; -use libc::mode_t; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::collections::BTreeMap; use std::fs::{self, File}; -use std::io::{BufRead, BufReader, BufWriter, Write}; +use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; use bootc_internal_blockdev::Device; -use cap_std::fs::Dir; +use cap_std::ambient_authority; +use cap_std::fs::{Dir, Permissions, PermissionsExt}; use cap_std_ext::cap_std; pub(crate) enum ConfigMode { @@ -60,7 +61,8 @@ pub(crate) fn install( target_components: Option<&[String]>, auto_components: bool, ) -> Result<()> { - let source_root_dir = openat::Dir::open(source_root).context("Opening source root")?; + let source_root_dir = + Dir::open_ambient_dir(source_root, ambient_authority()).context("Opening source root")?; SavedState::ensure_not_present(dest_root) .context("failed to install, invalid re-install attempted")?; @@ -175,7 +177,7 @@ pub(crate) fn install( } } } - let sysroot = &openat::Dir::open(dest_root)?; + let sysroot = &Dir::open_ambient_dir(dest_root, ambient_authority())?; #[cfg(any( target_arch = "x86_64", @@ -339,7 +341,7 @@ pub(crate) fn update(name: &str, rootcxt: &RootContext) -> Result Result { pub(crate) fn status() -> Result { let mut ret: Status = Default::default(); let mut known_components = get_components(); - let sysroot = openat::Dir::open("/")?; + let sysroot = Dir::open_ambient_dir("/", ambient_authority())?; let state = SavedState::load_from_disk("/")?; if let Some(state) = state { for (name, ic) in state.installed.iter() { @@ -592,7 +594,7 @@ pub(crate) fn print_status(status: &Status) -> Result<()> { } pub struct RootContext { - pub sysroot: openat::Dir, + pub sysroot: Dir, pub path: Utf8PathBuf, /// The block device backing the root filesystem. @@ -603,7 +605,7 @@ pub struct RootContext { } impl RootContext { - fn new(sysroot: openat::Dir, path: &str, device: Device) -> Self { + fn new(sysroot: Dir, path: &str, device: Device) -> Self { Self { sysroot, path: Utf8Path::new(path).into(), @@ -615,7 +617,7 @@ impl RootContext { /// Initialize parent devices to prepare the update fn prep_before_update() -> Result { let path = "/"; - let sysroot = openat::Dir::open(path).context("Opening root dir")?; + let sysroot = Dir::open_ambient_dir(path, ambient_authority()).context("Opening root dir")?; let device = list_dev_current_root()?; Ok(RootContext::new(sysroot, path, device)) } @@ -741,7 +743,8 @@ pub(crate) fn client_run_migrate_static_grub_config() -> Result<()> { ensure_writable_boot()?; let grub_config_dir = PathBuf::from("/boot/grub2"); - let dirfd = openat::Dir::open(&grub_config_dir).context("Opening /boot/grub2")?; + let dirfd = Dir::open_ambient_dir(&grub_config_dir, ambient_authority()) + .context("Opening /boot/grub2")?; // We mark the bootloader as BLS capable to disable the ostree-grub2 logic. // We can do that as we know that we are run after the bootloader has been @@ -789,10 +792,10 @@ pub(crate) fn client_run_migrate_static_grub_config() -> Result<()> { // Atomically replace the symlink dirfd - .local_rename(stripped_config, "grub.cfg") + .rename(stripped_config, &dirfd, "grub.cfg") .context("Failed to replace symlink with current GRUB config")?; - fsfreeze_thaw_cycle(dirfd.open_file(".")?)?; + fsfreeze_thaw_cycle(dirfd.reopen_as_ownedfd()?)?; println!("GRUB config symlink successfully replaced with the current config"); } @@ -809,15 +812,10 @@ pub(crate) fn client_run_migrate_static_grub_config() -> Result<()> { /// `### BEGIN /etc/grub.d/15_ostree ###` and `### END /etc/grub.d/15_ostree ###`. fn strip_grub_config_file( current_config_content: impl BufRead, - dirfd: &openat::Dir, + dirfd: &Dir, stripped_config_name: &str, ) -> Result<()> { - // mode = -rw------- (600) - let mut writer = BufWriter::new( - dirfd - .write_file(stripped_config_name, 0o600 as mode_t) - .context("Failed to open temporary GRUB config")?, - ); + let mut writer = vec![]; let mut skip = false; for line in current_config_content.lines() { @@ -841,11 +839,10 @@ fn strip_grub_config_file( .context("Failed to write stripped GRUB config")?; } - writer - .into_inner() - .context("Failed to flush stripped GRUB config")? - .sync_data() - .context("Failed to sync stripped GRUB config")?; + // mode = -rw------- (600) + dirfd + .atomic_write_with_perms(stripped_config_name, writer, Permissions::from_mode(0o600)) + .with_context(|| format!("Writing to {stripped_config_name}"))?; Ok(()) } @@ -867,8 +864,8 @@ mod tests { fn test_strip_grub_config_file() -> Result<()> { let root: &tempfile::TempDir = &tempfile::tempdir()?; let root_path = root.path(); - let rootd = openat::Dir::open(root_path)?; - let stripped_config = root_path.join("stripped"); + let rootd = Dir::open_ambient_dir(root_path, ambient_authority())?; + let stripped_config = "stripped"; let content = r" ### BEGIN /etc/grub.d/10_linux ### @@ -897,9 +894,9 @@ initrdefi /ostree/rhcos-bf3b382/initramfs.img strip_grub_config_file( BufReader::new(std::io::Cursor::new(content)), &rootd, - stripped_config.to_str().unwrap(), + stripped_config, )?; - let stripped_content = fs::read_to_string(stripped_config)?; + let stripped_content = rootd.read_to_string(stripped_config)?; let expected = r" ### BEGIN /etc/grub.d/10_linux ### diff --git a/src/cli/bootupd.rs b/src/cli/bootupd.rs index 10e0f256..b560910f 100644 --- a/src/cli/bootupd.rs +++ b/src/cli/bootupd.rs @@ -1,6 +1,7 @@ use crate::bootupd::{self, ConfigMode}; use anyhow::{Context, Result}; use camino::Utf8Path; +use cap_std::ambient_authority; use cap_std::fs::Dir; use cap_std_ext::cap_std; use clap::Parser; @@ -123,7 +124,7 @@ impl DCommand { // Resolve devices: either discover backing devices from a filesystem path, // or convert explicitly specified device paths to Device objects. let devices = if let Some(ref fs_path) = opts.filesystem { - let dir = Dir::open_ambient_dir(fs_path, cap_std::ambient_authority()) + let dir = Dir::open_ambient_dir(fs_path, ambient_authority()) .with_context(|| format!("Opening filesystem path {fs_path}"))?; let device = bootc_internal_blockdev::list_dev_by_dir(&dir)?; device.find_all_roots()? diff --git a/src/component.rs b/src/component.rs index 6e148b03..403ffeec 100644 --- a/src/component.rs +++ b/src/component.rs @@ -5,8 +5,10 @@ */ use anyhow::{Context, Result}; +use cap_std::fs::{Dir, PermissionsExt}; +use cap_std::{ambient_authority, fs::Permissions}; +use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; -use openat_ext::OpenatDirExt; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; @@ -38,7 +40,7 @@ pub(crate) trait Component { fn query_adopt(&self, devices: &Option>) -> Result>; // Backup the current grub config, and install static grub config from tree - fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &openat::Dir) -> Result<()>; + fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &Dir) -> Result<()>; /// Given an adoptable system and an update, perform the update. fn adopt_update( @@ -71,12 +73,12 @@ pub(crate) trait Component { fn generate_update_metadata(&self, sysroot: &str) -> Result>; /// Used on the client to query for an update cached in the current booted OS. - fn query_update(&self, sysroot: &openat::Dir) -> Result>; + fn query_update(&self, sysroot: &Dir) -> Result>; /// This is called in the update code if query_update() returned no metadata. /// It should return an error if the current booted system should expect some /// metadata for this component. - fn query_requires_update(&self, sysroot: &openat::Dir) -> Result<()>; + fn query_requires_update(&self, sysroot: &Dir) -> Result<()>; /// Used on the client to run an update. fn run_update( @@ -144,24 +146,28 @@ pub(crate) fn write_update_metadata( component: &dyn Component, meta: &ContentMetadata, ) -> Result<()> { - let sysroot = openat::Dir::open(sysroot)?; - let dir = sysroot.sub_dir(BOOTUPD_UPDATES_DIR)?; + let sysroot = Dir::open_ambient_dir(sysroot, ambient_authority())?; + let dir = sysroot.open_dir(BOOTUPD_UPDATES_DIR)?; let name = component_update_data_name(component); - dir.write_file_with(name, 0o644, |w| -> Result<_> { - Ok(serde_json::to_writer(w, &meta)?) - })?; + + dir.atomic_write_with_perms( + name, + serde_json::to_vec(&meta).context("Serializing metadata")?, + Permissions::from_mode(0o644), + )?; + Ok(()) } /// Given a component, return metadata on the available update (if any) #[context("Loading update for component {}", component.name())] pub(crate) fn get_component_update( - sysroot: &openat::Dir, + sysroot: &Dir, component: &dyn Component, ) -> Result> { let name = component_update_data_name(component); let path = Path::new(BOOTUPD_UPDATES_DIR).join(name); - if let Some(f) = sysroot.open_file_optional(&path)? { + if let Some(f) = sysroot.open_optional(&path)? { let mut f = std::io::BufReader::new(f); let u = serde_json::from_reader(&mut f) .with_context(|| format!("failed to parse {:?}", &path))?; @@ -207,6 +213,8 @@ pub(crate) fn query_adopt_state() -> Result> { #[cfg(test)] mod tests { + use cap_std::fs::{DirBuilder, DirBuilderExt, Permissions, PermissionsExt}; + use super::*; #[test] @@ -214,23 +222,28 @@ mod tests { let td = tempfile::tempdir()?; let tdp = td.path(); let tdupdates = "usr/lib/bootupd/updates/EFI"; - let tdir = openat::Dir::open(tdp)?; + let tdir = Dir::open_ambient_dir(tdp, ambient_authority())?; - tdir.ensure_dir_all(tdupdates, 0o755)?; - let efi = tdir.sub_dir(tdupdates)?; - efi.create_dir("BOOT", 0o755)?; - efi.create_dir("fedora", 0o755)?; - efi.create_dir("centos", 0o755)?; + let mut dir_builder = DirBuilder::new(); + dir_builder.mode(0o755); + dir_builder.recursive(true); - efi.write_file_contents( + tdir.create_dir_with(tdupdates, &dir_builder)?; + let efi = tdir.open_dir(tdupdates)?; + efi.create_dir_with("BOOT", &dir_builder)?; + efi.create_dir_with("fedora", &dir_builder)?; + efi.create_dir_with("centos", &dir_builder)?; + + efi.atomic_write_with_perms( format!("fedora/{}", crate::efi::SHIM), - 0o644, "shim data".as_bytes(), + Permissions::from_mode(0o644), )?; - efi.write_file_contents( + + efi.atomic_write_with_perms( format!("centos/{}", crate::efi::SHIM), - 0o644, "shim data".as_bytes(), + Permissions::from_mode(0o644), )?; let all_components = crate::bootupd::get_components(); @@ -242,16 +255,17 @@ mod tests { if component.name() == "EFI" { let x = component.get_efi_vendor(tdp); assert_eq!(x.is_err(), true); - efi.remove_all("centos")?; + efi.remove_dir_all("centos")?; assert_eq!(component.get_efi_vendor(tdp)?, Some("fedora".to_string())); { let td_vendor = "usr/lib/efi/shim/15.8-3/EFI/centos"; - tdir.ensure_dir_all(td_vendor, 0o755)?; - let shim_dir = tdir.sub_dir(td_vendor)?; - shim_dir.write_file_contents( + tdir.create_dir_with(td_vendor, &dir_builder)?; + let shim_dir = tdir.open_dir(td_vendor)?; + + shim_dir.atomic_write_with_perms( crate::efi::SHIM, - 0o644, "shim data".as_bytes(), + Permissions::from_mode(0o644), )?; // usr/lib/efi wins and get 'centos' @@ -268,8 +282,8 @@ mod tests { component.get_efi_vendor(&td_efi)?, Some("fedora".to_string()) ); - tdir.remove_all("usr/lib/efi")?; - tdir.remove_all(tdupdates)?; + tdir.remove_dir_all("usr/lib/efi")?; + tdir.remove_dir_all(tdupdates)?; let err = component.get_efi_vendor(&td_usr).unwrap_err(); assert_eq!(err.to_string(), "Failed to find valid target path"); } diff --git a/src/efi.rs b/src/efi.rs index 37b4cdb4..cb5c1b0c 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -12,11 +12,11 @@ use std::process::Command; use anyhow::{bail, Context, Result}; use bootc_internal_utils::CommandRunExt; use camino::{Utf8Path, Utf8PathBuf}; +use cap_std::ambient_authority; use cap_std::fs::Dir; use cap_std_ext::cap_std; use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; -use openat_ext::OpenatDirExt; use os_release::OsRelease; use rustix::{fd::AsFd, fd::BorrowedFd, fs::StatVfsMountFlags}; use walkdir::WalkDir; @@ -190,12 +190,7 @@ impl Efi { } #[context("Updating EFI firmware variables")] - fn update_firmware( - &self, - device: &Device, - espdir: &openat::Dir, - vendordir: &str, - ) -> Result<()> { + fn update_firmware(&self, device: &Device, espdir: &Dir, vendordir: &str) -> Result<()> { if !is_efi_booted()? { log::debug!("Not booted via EFI, skipping firmware update"); return Ok(()); @@ -216,7 +211,7 @@ impl Efi { // Check shim exists and return earlier if not let shim_path = format!("EFI/{vendordir}/{SHIM}"); - if !espdir.exists(&shim_path)? { + if !espdir.exists(&shim_path) { anyhow::bail!("Failed to find {shim_path}"); } let loader = format!("\\EFI\\{vendordir}\\{SHIM}"); @@ -336,28 +331,32 @@ impl Component for Efi { // Backup "/boot/efi/EFI/{vendor}/grub.cfg" to "/boot/efi/EFI/{vendor}/grub.cfg.bak" // Replace "/boot/efi/EFI/{vendor}/grub.cfg" with new static "grub.cfg" - fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &openat::Dir) -> Result<()> { - let sysroot = - openat::Dir::open(sysroot_path).with_context(|| format!("Opening {sysroot_path}"))?; + fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &Dir) -> Result<()> { + let sysroot = Dir::open_ambient_dir(sysroot_path, ambient_authority()) + .with_context(|| format!("Opening {sysroot_path}"))?; let Some(vendor) = self.get_efi_vendor(&Path::new(sysroot_path))? else { anyhow::bail!("Failed to find efi vendor"); }; // destdir is /boot/efi/EFI let efidir = destdir - .sub_dir(&vendor) + .open_dir(&vendor) .with_context(|| format!("Opening EFI/{}", vendor))?; - if !efidir.exists(grubconfigs::GRUBCONFIG_BACKUP)? { + if !efidir.exists(grubconfigs::GRUBCONFIG_BACKUP) { println!("Creating a backup of the current GRUB config on EFI"); efidir - .copy_file(grubconfigs::GRUBCONFIG, grubconfigs::GRUBCONFIG_BACKUP) + .copy( + grubconfigs::GRUBCONFIG, + &efidir, + grubconfigs::GRUBCONFIG_BACKUP, + ) .context("Failed to backup GRUB config")?; } grubconfigs::install(&sysroot, None, Some(&vendor), true)?; // Synchronize the filesystem containing /boot/efi/EFI/{vendor} to disk. - fsfreeze_thaw_cycle(efidir.open_file(".")?)?; + fsfreeze_thaw_cycle(efidir.reopen_as_ownedfd()?)?; Ok(()) } @@ -385,7 +384,7 @@ impl Component for Efi { }; let updated = rootcxt .sysroot - .sub_dir(&updated_path) + .open_dir(&updated_path) .with_context(|| format!("opening update dir {}", updated_path.display()))?; let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?; @@ -394,7 +393,8 @@ impl Component for Efi { let destpath = &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(&esp.path()))?; - let efidir = openat::Dir::open(&destpath.join("EFI")).context("opening EFI dir")?; + let efidir = Dir::open_ambient_dir(&destpath.join("EFI"), ambient_authority()) + .context("opening EFI dir")?; validate_esp_fstype(&efidir)?; // For adoption, we should only touch files that we know about. @@ -417,7 +417,7 @@ impl Component for Efi { } // Do the sync before unmount - fsfreeze_thaw_cycle(efidir.open_file(".")?)?; + fsfreeze_thaw_cycle(efidir.reopen_as_ownedfd()?)?; drop(efidir); self.unmount().context("unmount after adopt")?; } @@ -435,7 +435,7 @@ impl Component for Efi { device: Option<&Device>, update_firmware: bool, ) -> Result { - let src_dir = openat::Dir::open(src_root) + let src_dir = Dir::open_ambient_dir(src_root, ambient_authority()) .with_context(|| format!("opening source directory {src_root}"))?; let Some(meta) = get_component_update(&src_dir, self)? else { anyhow::bail!("No update metadata for component {} found", self.name()); @@ -462,7 +462,7 @@ impl Component for Efi { anyhow::bail!("No device specified and no mounted ESP found"); }; - let destd = &openat::Dir::open(&destpath) + let destd = &Dir::open_ambient_dir(&destpath, ambient_authority()) .with_context(|| format!("opening dest dir {}", destpath.display()))?; validate_esp_fstype(destd)?; @@ -494,7 +494,7 @@ impl Component for Efi { }; // Get filetree from efi path - let ft = crate::filetree::FileTree::new_from_dir(&src_dir.sub_dir(efi_path)?)?; + let ft = crate::filetree::FileTree::new_from_dir(&src_dir.open_dir(efi_path)?)?; if update_firmware { if let Some(dev) = device { if let Some(vendordir) = @@ -534,7 +534,7 @@ impl Component for Efi { let updated = rootcxt .sysroot - .sub_dir(&updated_path) + .open_dir(&updated_path) .with_context(|| format!("opening update dir {}", updated_path.display()))?; let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?; let diff = currentf.diff(&updatef)?; @@ -546,14 +546,15 @@ impl Component for Efi { for esp in esp_devices { let destpath = &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(&esp.path()))?; - let destdir = openat::Dir::open(&destpath.join("EFI")).context("opening EFI dir")?; + let destdir = Dir::open_ambient_dir(&destpath.join("EFI"), ambient_authority()) + .context("opening EFI dir")?; validate_esp_fstype(&destdir)?; log::trace!("applying diff: {}", &diff); filetree::apply_diff(&updated, &destdir, &diff, None) .context("applying filesystem changes")?; // Do the sync before unmount - fsfreeze_thaw_cycle(destdir.open_file(".")?)?; + fsfreeze_thaw_cycle(destdir.reopen_as_ownedfd()?)?; drop(destdir); self.unmount().context("unmount after update")?; } @@ -612,11 +613,11 @@ impl Component for Efi { Ok(Some(metadata)) } - fn query_update(&self, sysroot: &openat::Dir) -> Result> { + fn query_update(&self, sysroot: &Dir) -> Result> { get_component_update(sysroot, self) } - fn query_requires_update(&self, _sysroot: &openat::Dir) -> Result<()> { + fn query_requires_update(&self, _sysroot: &Dir) -> Result<()> { // Failed as expected if booted with EFI and no update metadata if is_efi_booted()? { anyhow::bail!("Failed to find EFI update metadata"); @@ -639,7 +640,7 @@ impl Component for Efi { for esp in esp_devices.iter() { let destpath = &self.ensure_mounted_esp(Path::new("/"), Path::new(&esp.path()))?; - let efidir = openat::Dir::open(&destpath.join("EFI")) + let efidir = Dir::open_ambient_dir(&destpath.join("EFI"), ambient_authority()) .with_context(|| format!("opening EFI dir {}", destpath.display()))?; let diff = currentf.relative_diff_to(&efidir)?; @@ -696,7 +697,7 @@ impl Drop for Efi { } } -fn validate_esp_fstype(dir: &openat::Dir) -> Result<()> { +fn validate_esp_fstype(dir: &Dir) -> Result<()> { let dir = unsafe { BorrowedFd::borrow_raw(dir.as_raw_fd()) }; let stat = rustix::fs::fstatfs(&dir)?; if stat.f_type != libc::MSDOS_SUPER_MAGIC { @@ -904,7 +905,7 @@ fn transfer_ostree_boot_to_bootupd_updates( .args([&efisrc, &dest_efidir]) .run_capture_stderr()?; - let efidir = openat::Dir::open(dest_efidir.as_std_path()) + let efidir = Dir::open_ambient_dir(dest_efidir.as_std_path(), ambient_authority()) .with_context(|| format!("Opening {}", dest_efidir))?; let files = crate::util::filenames(&efidir)?.into_iter().map(|mut f| { diff --git a/src/filesystem.rs b/src/filesystem.rs index 07a96b8f..84238c8f 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -4,6 +4,7 @@ use std::process::Command; use anyhow::Result; use bootc_internal_utils::CommandRunExt; +use cap_std::fs::Dir; use fn_error_context::context; use rustix::fd::BorrowedFd; use serde::Deserialize; @@ -24,7 +25,7 @@ pub(crate) struct Findmnt { } #[context("Inspecting filesystem {path:?}")] -pub(crate) fn inspect_filesystem(root: &openat::Dir, path: &str) -> Result { +pub(crate) fn inspect_filesystem(root: &Dir, path: &str) -> Result { let rootfd = unsafe { BorrowedFd::borrow_raw(root.as_raw_fd()) }; // SAFETY: This is unsafe just for the pre_exec, when we port to cap-std we can use cap-std-ext let o: Findmnt = unsafe { diff --git a/src/filetree.rs b/src/filetree.rs index a9eee816..31d0916d 100644 --- a/src/filetree.rs +++ b/src/filetree.rs @@ -27,7 +27,8 @@ use camino::{Utf8Path, Utf8PathBuf}; target_arch = "aarch64", target_arch = "riscv64" ))] -use openat_ext::OpenatDirExt; +use cap_std::fs::Dir; +use cap_std_ext::dirext::CapStdExtDirExt; #[cfg(any( target_arch = "x86_64", target_arch = "aarch64", @@ -130,11 +131,11 @@ impl FileMetadata { target_arch = "aarch64", target_arch = "riscv64" ))] - pub(crate) fn new_from_path( - dir: &openat::Dir, + pub(crate) fn new_from_path>( + dir: &Dir, name: P, ) -> Result { - let mut r = dir.open_file(name)?; + let mut r = dir.open(name)?; let meta = r.metadata()?; let mut hasher = Hasher::new(MessageDigest::sha512()).expect("openssl sha512 hasher creation failed"); @@ -155,36 +156,30 @@ impl FileTree { target_arch = "aarch64", target_arch = "riscv64" ))] - fn unsorted_from_dir(dir: &openat::Dir) -> Result> { + fn unsorted_from_dir(dir: &Dir) -> Result> { let mut ret = HashMap::new(); - for entry in dir.list_dir(".")? { + for entry in dir.entries_utf8()? { let entry = entry?; - let Some(name) = entry.file_name().to_str() else { - bail!("Invalid UTF-8 filename: {:?}", entry.file_name()) - }; + let name = entry.file_name().context("Getting file name")?; if name.starts_with(TMP_PREFIX) { bail!("File {} contains our temporary prefix!", name); } - match dir.get_file_type(&entry)? { - openat::SimpleType::File => { - let meta = FileMetadata::new_from_path(dir, name)?; - let _ = ret.insert(name.to_string(), meta); - } - openat::SimpleType::Dir => { - let child = dir.sub_dir(name)?; - for (mut k, v) in FileTree::unsorted_from_dir(&child)?.drain() { - k.reserve(name.len() + 1); - k.insert(0, '/'); - k.insert_str(0, name); - let _ = ret.insert(k, v); - } - } - openat::SimpleType::Symlink => { - bail!("Unsupported symbolic link {:?}", entry.file_name()) - } - openat::SimpleType::Other => { - bail!("Unsupported non-file/directory {:?}", entry.file_name()) + let file_type = entry.file_type()?; + if file_type.is_file() { + let meta = FileMetadata::new_from_path(dir, &name)?; + let _ = ret.insert(name.to_string(), meta); + } else if file_type.is_dir() { + let child = dir.open_dir(&name)?; + for (mut k, v) in FileTree::unsorted_from_dir(&child)?.drain() { + k.reserve(name.len() + 1); + k.insert(0, '/'); + k.insert_str(0, &name); + let _ = ret.insert(k, v); } + } else if file_type.is_symlink() { + bail!("Unsupported symbolic link {:?}", entry.file_name()) + } else { + bail!("Unsupported non-file/directory {:?}", entry.file_name()) } } Ok(ret) @@ -196,7 +191,7 @@ impl FileTree { target_arch = "aarch64", target_arch = "riscv64" ))] - pub(crate) fn new_from_dir(dir: &openat::Dir) -> Result { + pub(crate) fn new_from_dir(dir: &Dir) -> Result { let mut children = BTreeMap::new(); for (k, mut v) in Self::unsorted_from_dir(dir)?.drain() { let k_path = get_dest_efi_path(Utf8Path::new(&k)).to_string(); @@ -275,26 +270,24 @@ impl FileTree { target_arch = "aarch64", target_arch = "riscv64" ))] - pub(crate) fn relative_diff_to(&self, dir: &openat::Dir) -> Result { + pub(crate) fn relative_diff_to(&self, dir: &Dir) -> Result { let mut removals = HashSet::new(); let mut changes = HashSet::new(); for (path, info) in self.children.iter() { assert!(!path.starts_with('/')); - if let Some(meta) = dir.metadata_optional(path)? { - match meta.simple_type() { - openat::SimpleType::File => { - let target_info = FileMetadata::new_from_path(dir, path)?; - if info != &target_info { - // Save the source path for changes - changes.insert(info.source.as_ref().unwrap_or(path).clone()); - } - } - _ => { - // If a file became a directory + if let Ok(meta) = dir.metadata(path) { + let file_type = meta.file_type(); + if file_type.is_file() { + let target_info = FileMetadata::new_from_path(dir, path)?; + if info != &target_info { + // Save the source path for changes changes.insert(info.source.as_ref().unwrap_or(path).clone()); } + } else { + // If a file became a directory + changes.insert(info.source.as_ref().unwrap_or(path).clone()); } } else { removals.insert(path.clone()); @@ -314,30 +307,24 @@ impl FileTree { target_arch = "aarch64", target_arch = "riscv64" ))] -fn cleanup_tmp(dir: &openat::Dir) -> Result<()> { - for entry in dir.list_dir(".")? { +fn cleanup_tmp(dir: &Dir) -> Result<()> { + for entry in dir.entries_utf8()? { let entry = entry?; - let Some(name) = entry.file_name().to_str() else { - // Skip invalid UTF-8 for now, we will barf on it later though. - continue; - }; + let name = entry.file_name().context("Getting file name")?; - match dir.get_file_type(&entry)? { - openat::SimpleType::Dir => { - if name.starts_with(TMP_PREFIX) { - dir.remove_all(name)?; - continue; - } else { - let child = dir.sub_dir(name)?; - cleanup_tmp(&child)?; - } + let file_type = entry.file_type()?; + if file_type.is_dir() { + if name.starts_with(TMP_PREFIX) { + dir.remove_dir_all(name)?; + continue; + } else { + let child = dir.open_dir(name)?; + cleanup_tmp(&child)?; } - openat::SimpleType::File => { - if name.starts_with(TMP_PREFIX) { - dir.remove_file(name)?; - } + } else if file_type.is_file() { + if name.starts_with(TMP_PREFIX) { + dir.remove_file(name)?; } - _ => {} } } Ok(()) @@ -360,7 +347,7 @@ pub(crate) struct ApplyUpdateOptions { target_arch = "aarch64", target_arch = "riscv64" ))] -pub(crate) fn copy_dir(root: &openat::Dir, src: &str, dst: &str) -> Result<()> { +pub(crate) fn copy_dir(root: &Dir, src: &str, dst: &str) -> Result<()> { copy_dir_with_args(root, src, dst, ["-a"]) } @@ -370,12 +357,7 @@ pub(crate) fn copy_dir(root: &openat::Dir, src: &str, dst: &str) -> Result<()> { target_arch = "aarch64", target_arch = "riscv64" ))] -pub(crate) fn copy_dir_with_args( - root: &openat::Dir, - src: &str, - dst: &str, - args: I, -) -> Result<()> +pub(crate) fn copy_dir_with_args(root: &Dir, src: &str, dst: &str, args: I) -> Result<()> where I: IntoIterator, S: AsRef, @@ -436,8 +418,8 @@ fn get_dest_efi_path(path: &Utf8Path) -> Utf8PathBuf { target_arch = "riscv64" ))] pub(crate) fn apply_diff( - srcdir: &openat::Dir, - destdir: &openat::Dir, + srcdir: &Dir, + destdir: &Dir, diff: &FileTreeDiff, opts: Option<&ApplyUpdateOptions>, ) -> Result<()> { @@ -458,7 +440,7 @@ pub(crate) fn apply_diff( path_tmp = Utf8Path::new(&first_dir_tmp).join(path.strip_prefix(&first_dir)?); // copy to temp dir and remember // skip copying if dir not existed in dest - if !destdir.exists(&first_dir_tmp)? && destdir.exists(first_dir.as_std_path())? { + if !destdir.exists(&first_dir_tmp) && destdir.exists(first_dir.as_std_path()) { copy_dir(destdir, first_dir.as_str(), &first_dir_tmp).with_context(|| { format!("copy {first_dir} to {first_dir_tmp} before removing {pathstr}") })?; @@ -479,7 +461,7 @@ pub(crate) fn apply_diff( let (first_dir, first_dir_tmp) = get_first_dir(&path)?; let mut path_tmp = Utf8PathBuf::from(&first_dir_tmp); if first_dir != path { - if !destdir.exists(&first_dir_tmp)? && destdir.exists(first_dir.as_std_path())? { + if !destdir.exists(&first_dir_tmp) && destdir.exists(first_dir.as_std_path()) { // copy to temp dir if not exists copy_dir(destdir, first_dir.as_str(), &first_dir_tmp).with_context(|| { format!("copy {first_dir} to {first_dir_tmp} before updating {pathstr}") @@ -488,7 +470,11 @@ pub(crate) fn apply_diff( path_tmp = path_tmp.join(path.strip_prefix(&first_dir)?); // ensure new additions dir exists if let Some(parent) = path_tmp.parent() { - destdir.ensure_dir_all(parent.as_std_path(), DEFAULT_FILE_MODE)?; + use cap_std::fs::{DirBuilder, DirBuilderExt}; + + let mut dir_opts = DirBuilder::new(); + dir_opts.recursive(true).mode(DEFAULT_FILE_MODE); + destdir.create_dir_with(parent.as_std_path(), &dir_opts)?; } // remove changed file before copying destdir @@ -497,7 +483,7 @@ pub(crate) fn apply_diff( } updates.insert(first_dir, first_dir_tmp); srcdir - .copy_file_at(src_path.as_std_path(), destdir, path_tmp.as_std_path()) + .copy(src_path.as_std_path(), destdir, path_tmp.as_std_path()) .with_context(|| format!("copying {:?} to {:?}", src_path, path_tmp))?; } @@ -505,43 +491,47 @@ pub(crate) fn apply_diff( for (dst, tmp) in updates.iter() { let dst = dst.as_std_path(); log::trace!("doing local exchange for {} and {:?}", tmp, dst); - if destdir.exists(dst)? { - destdir - .local_exchange(tmp, dst) + if destdir.exists(dst) { + use rustix::fs::{renameat_with, RenameFlags}; + + renameat_with(&destdir, tmp, &destdir, dst, RenameFlags::EXCHANGE) .with_context(|| format!("exchange for {} and {:?}", tmp, dst))?; } else { destdir - .local_rename(tmp, dst) + .rename(tmp, destdir, dst) .with_context(|| format!("rename for {} and {:?}", tmp, dst))?; } crate::try_fail_point!("update::exchange"); } // Ensure all of the updates & changes are written persistently to disk if !opts.skip_sync { - destdir.syncfs()?; + rustix::fs::syncfs(destdir.reopen_as_ownedfd()?)?; } // finally remove the temp dir for (_, tmp) in updates.iter() { log::trace!("cleanup: {}", tmp); - destdir.remove_all(tmp).context("clean up temp")?; + // [`remove_all`] from openat is essentially [`remove_all_optional`] + destdir.remove_all_optional(tmp).context("clean up temp")?; } // A second full filesystem sync to narrow any races rather than // waiting for writeback to kick in. if !opts.skip_sync { - fsfreeze_thaw_cycle(destdir.open_file(".")?)?; + fsfreeze_thaw_cycle(destdir.reopen_as_ownedfd()?)?; } Ok(()) } #[cfg(test)] mod tests { + use cap_std::ambient_authority; + use cap_std::fs::{DirBuilder, DirBuilderExt, Permissions, PermissionsExt}; + use super::*; use std::fs; - use std::io::Write; use std::path::Path; - fn run_diff(a: &openat::Dir, b: &openat::Dir) -> Result { + fn run_diff(a: &Dir, b: &Dir) -> Result { let ta = FileTree::new_from_dir(a)?; let tb = FileTree::new_from_dir(b)?; let diff = ta.diff(&tb)?; @@ -564,9 +554,9 @@ mod tests { if !r.success() { bail!("failed to cp"); }; - let c = openat::Dir::open(&c)?; - let da = openat::Dir::open(a)?; - let db = openat::Dir::open(b)?; + let c = Dir::open_ambient_dir(&c, ambient_authority())?; + let da = Dir::open_ambient_dir(a, ambient_authority())?; + let db = Dir::open_ambient_dir(b, ambient_authority())?; let ta = FileTree::new_from_dir(&da)?; let tb = FileTree::new_from_dir(&db)?; let diff = ta.diff(&tb)?; @@ -584,8 +574,8 @@ mod tests { assert_eq!(n, diff.removals.len()); } for f in diff.removals.iter() { - assert!(c.exists(f)?); - assert!(da.exists(f)?); + assert!(c.exists(f)); + assert!(da.exists(f)); } } else { assert_eq!(newdiff.count(), 0); @@ -613,16 +603,21 @@ mod tests { let pb = p.join("b"); std::fs::create_dir(&pa)?; std::fs::create_dir(&pb)?; - let a = openat::Dir::open(&pa)?; - let b = openat::Dir::open(&pb)?; + let a = Dir::open_ambient_dir(&pa, ambient_authority())?; + let b = Dir::open_ambient_dir(&pb, ambient_authority())?; let diff = run_diff(&a, &b)?; assert_eq!(diff.count(), 0); - a.create_dir("foo", 0o755)?; + let mut dir_builder = DirBuilder::new(); + dir_builder.recursive(true).mode(0o755); + a.create_dir_with("foo", &dir_builder)?; let diff = run_diff(&a, &b)?; assert_eq!(diff.count(), 0); { - let mut bar = a.write_file("foo/bar", 0o644)?; - bar.write_all("foobarcontents".as_bytes())?; + a.atomic_write_with_perms( + "foo/bar", + "foobarcontents".as_bytes(), + Permissions::from_mode(0o644), + )?; } let diff = run_diff(&a, &b)?; assert_eq!(diff.count(), 1); @@ -638,17 +633,23 @@ mod tests { let rdiff = ta.relative_diff_to(&b)?; assert_eq!(rdiff.removals.len(), cdiff.removals.len()); - b.create_dir("foo", 0o755)?; + b.create_dir_with("foo", &dir_builder)?; { - let mut bar = b.write_file("foo/bar", 0o644)?; - bar.write_all("foobarcontents".as_bytes())?; + b.atomic_write_with_perms( + "foo/bar", + "foobarcontents".as_bytes(), + Permissions::from_mode(0o644), + )?; } let diff = run_diff(&a, &b)?; assert_eq!(diff.count(), 0); test_apply(&pa, &pb).context("testing apply 2")?; { - let mut bar2 = b.write_file("foo/bar", 0o644)?; - bar2.write_all("foobarcontents2".as_bytes())?; + b.atomic_write_with_perms( + "foo/bar", + "foobarcontents2".as_bytes(), + Permissions::from_mode(0o644), + )?; } let diff = run_diff(&a, &b)?; assert_eq!(diff.count(), 1); @@ -682,8 +683,8 @@ mod tests { fs::write(b.join(&newsubp).join("newgrub.x64"), "newgrub data")?; fs::remove_file(b.join(relp).join("shim.x64"))?; { - let a = openat::Dir::open(&a)?; - let b = openat::Dir::open(&b)?; + let a = Dir::open_ambient_dir(&a, ambient_authority())?; + let b = Dir::open_ambient_dir(&b, ambient_authority())?; let ta = FileTree::new_from_dir(&a)?; let tb = FileTree::new_from_dir(&b)?; let diff = ta.diff(&tb)?; @@ -713,8 +714,8 @@ mod tests { fs::write(c.join(foo).join("grub.x64"), "grub data 3")?; // addition: "bar/2.0/EFI/new/newfile" fs::write(c.join(bar).join("newfile"), "filedata")?; - let a = openat::Dir::open(&a.join("EFI"))?; - let c = openat::Dir::open(&c)?; + let a = Dir::open_ambient_dir(&a.join("EFI"), ambient_authority())?; + let c = Dir::open_ambient_dir(&c, ambient_authority())?; let ta = FileTree::new_from_dir(&a)?; let tc = FileTree::new_from_dir(&c)?; let diff = ta.diff(&tc)?; @@ -774,26 +775,37 @@ mod tests { let pb = p.join(".btmp.b/b"); std::fs::create_dir_all(&pa)?; std::fs::create_dir_all(&pb)?; - let dp = openat::Dir::open(p)?; + let dp = Dir::open_ambient_dir(p, ambient_authority())?; { - let mut buf = dp.write_file("a/foo", 0o644)?; - buf.write_all("foocontents".as_bytes())?; - let mut buf = dp.write_file("a/.btmp.foo", 0o644)?; - buf.write_all("foocontents".as_bytes())?; - let mut buf = dp.write_file(".btmp.b/foo", 0o644)?; - buf.write_all("foocontents".as_bytes())?; + dp.atomic_write_with_perms( + "a/foo", + "foocontents".as_bytes(), + Permissions::from_mode(0o644), + )?; + + dp.atomic_write_with_perms( + "a/.btmp.foo", + "foocontents".as_bytes(), + Permissions::from_mode(0o644), + )?; + + dp.atomic_write_with_perms( + ".btmp.b/foo", + "foocontents".as_bytes(), + Permissions::from_mode(0o644), + )?; } - assert!(dp.exists("a/.btmp.a")?); - assert!(dp.exists("a/foo")?); - assert!(dp.exists("a/.btmp.foo")?); - assert!(dp.exists("a/.btmp.a")?); - assert!(dp.exists(".btmp.b/b")?); - assert!(dp.exists(".btmp.b/foo")?); + assert!(dp.exists("a/.btmp.a")); + assert!(dp.exists("a/foo")); + assert!(dp.exists("a/.btmp.foo")); + assert!(dp.exists("a/.btmp.a")); + assert!(dp.exists(".btmp.b/b")); + assert!(dp.exists(".btmp.b/foo")); cleanup_tmp(&dp)?; - assert!(!dp.exists("a/.btmp.a")?); - assert!(dp.exists("a/foo")?); - assert!(!dp.exists("a/.btmp.foo")?); - assert!(!dp.exists(".btmp.b")?); + assert!(!dp.exists("a/.btmp.a")); + assert!(dp.exists("a/foo")); + assert!(!dp.exists("a/.btmp.foo")); + assert!(!dp.exists(".btmp.b")); Ok(()) } // Waiting on https://github.com/rust-lang/rust/pull/125692 @@ -806,28 +818,47 @@ mod tests { let pb = p.join("b"); std::fs::create_dir(&pa)?; std::fs::create_dir(&pb)?; - let a = openat::Dir::open(&pa)?; - let b = openat::Dir::open(&pb)?; - a.create_dir("foo", 0o755)?; - a.create_dir("bar", 0o755)?; + let a = Dir::open_ambient_dir(&pa, ambient_authority())?; + let b = Dir::open_ambient_dir(&pb, ambient_authority())?; + + let mut dir_builder = DirBuilder::new(); + dir_builder.recursive(true).mode(0o755); + + a.create_dir_with("foo", &dir_builder)?; + a.create_dir_with("bar", &dir_builder)?; + let foo = Path::new("foo/bar"); let bar = Path::new("bar/foo"); let testfile = "testfile"; { - let mut buf = a.write_file(foo, 0o644)?; - buf.write_all("foocontents".as_bytes())?; - let mut buf = a.write_file(bar, 0o644)?; - buf.write_all("barcontents".as_bytes())?; - let mut buf = a.write_file(testfile, 0o644)?; - buf.write_all("testfilecontents".as_bytes())?; + a.atomic_write_with_perms( + foo, + "foocontents".as_bytes(), + Permissions::from_mode(0o644), + )?; + + a.atomic_write_with_perms( + bar, + "barcontents".as_bytes(), + Permissions::from_mode(0o644), + )?; + + a.atomic_write_with_perms( + testfile, + "testfilecontents".as_bytes(), + Permissions::from_mode(0o644), + )?; } let diff = run_diff(&a, &b)?; assert_eq!(diff.count(), 3); - b.create_dir("foo", 0o755)?; + b.create_dir_with("foo", &dir_builder)?; { - let mut buf = b.write_file(foo, 0o644)?; - buf.write_all("foocontents".as_bytes())?; + b.atomic_write_with_perms( + foo, + "foocontents".as_bytes(), + Permissions::from_mode(0o644), + )?; } let b_btime_foo = fs::metadata(pb.join(foo))?.created()?; @@ -869,7 +900,7 @@ mod tests { assert_eq!(diff.count(), 1); assert_eq!(diff.removals.len(), 1); apply_diff(&a, &b, &diff, None).context("test removed files with relative_diff")?; - assert_eq!(b.exists(testfile)?, false); + assert_eq!(b.exists(testfile), false); // creation time is not changed for unchanged file let b_btime_foo_new = fs::metadata(pb.join(foo))?.created()?; assert_eq!(b_btime_foo_new, b_btime_foo); @@ -879,8 +910,8 @@ mod tests { let diff = run_diff(&b, &a)?; assert_eq!(diff.count(), 2); apply_diff(&a, &b, &diff, None).context("test removed files")?; - assert_eq!(b.exists(testfile)?, true); - assert_eq!(b.exists(bar)?, false); + assert_eq!(b.exists(testfile), true); + assert_eq!(b.exists(bar), false); let diff = run_diff(&b, &a)?; assert_eq!(diff.count(), 0); // creation time is not changed for unchanged file diff --git a/src/freezethaw.rs b/src/freezethaw.rs index 7904a419..251fe49a 100644 --- a/src/freezethaw.rs +++ b/src/freezethaw.rs @@ -33,6 +33,7 @@ fn ioctl_fithaw(fd: Fd) -> io::Result<()> { /// /// This function always call syncfs() first, then calls /// `ioctl(FIFREEZE)` and `ioctl(FITHAW)`, ignoring `EOPNOTSUPP` and `EPERM` +#[fn_error_context::context("fsfreeze_thaw_cycle")] pub(crate) fn fsfreeze_thaw_cycle(fd: Fd) -> anyhow::Result<()> { rustix::fs::syncfs(&fd)?; diff --git a/src/grubconfigs.rs b/src/grubconfigs.rs index 65196c3d..476f934c 100644 --- a/src/grubconfigs.rs +++ b/src/grubconfigs.rs @@ -1,13 +1,14 @@ use std::fmt::Write; use std::io::Read; -use std::os::unix::fs::PermissionsExt; use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use anyhow::{anyhow, Context, Result}; use bootc_internal_utils::CommandRunExt; +use cap_std::ambient_authority; +use cap_std::fs::{Dir, DirBuilder, DirBuilderExt, MetadataExt, Permissions, PermissionsExt}; +use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; -use openat_ext::OpenatDirExt; use crate::freezethaw::fsfreeze_thaw_cycle; @@ -29,46 +30,48 @@ pub(crate) const GRUBCONFIG_FILE_MODE: u32 = 0o600; /// Install the static GRUB config files. #[context("Installing static GRUB configs")] pub(crate) fn install( - target_root: &openat::Dir, - src_root: Option<&openat::Dir>, + target_root: &Dir, + src_root: Option<&Dir>, installed_efi_vendor: Option<&str>, write_uuid: bool, ) -> Result<()> { - let bootdir = &target_root.sub_dir("boot").context("Opening /boot")?; + let bootdir = &target_root.open_dir("boot").context("Opening /boot")?; let boot_is_mount = { - let root_dev = target_root.self_metadata()?.stat().st_dev; - let boot_dev = bootdir.self_metadata()?.stat().st_dev; + let root_dev = target_root.metadata(".")?.dev(); + let boot_dev = bootdir.metadata(".")?.dev(); log::debug!("root_dev={root_dev} boot_dev={boot_dev}"); root_dev != boot_dev }; - if !bootdir.exists(GRUB2DIR)? { - bootdir.create_dir(GRUB2DIR, 0o700)?; + if !bootdir.exists(GRUB2DIR) { + let mut dir_options = DirBuilder::new(); + dir_options.recursive(true).mode(0o700); + bootdir.create_dir_with(GRUB2DIR, &dir_options)?; } let configdir = if let Some(src_root) = src_root { // strip the leading `/` to make the path relative to the given // source root - src_root.sub_dir(Path::new(CONFIGDIR).strip_prefix("/")?)? + src_root.open_dir(Path::new(CONFIGDIR).strip_prefix("/")?)? } else { - openat::Dir::open(Path::new(CONFIGDIR))? + Dir::open_ambient_dir(Path::new(CONFIGDIR), ambient_authority())? }; let mut config = String::from("# Generated by bootupd / do not edit\n\n"); let mut pre = String::new(); configdir - .open_file("grub-static-pre.cfg")? + .open("grub-static-pre.cfg")? .read_to_string(&mut pre)?; config.push_str(pre.as_str()); - let dropindir = configdir.sub_dir(Path::new(DROPINDIR))?; + let dropindir = configdir.open_dir(Path::new(DROPINDIR))?; // Sort the files for reproducibility let mut entries = dropindir - .list_dir(".")? + .entries()? .map(|e| e.map_err(anyhow::Error::msg)) .collect::>>()?; - entries.sort_by(|a, b| a.file_name().cmp(b.file_name())); + entries.sort_by_key(|a| a.file_name()); for ent in entries { let name = ent.file_name(); let name = name @@ -80,16 +83,20 @@ pub(crate) fn install( } writeln!(config, "\n### BEGIN {name} ###")?; let mut dropin = String::new(); - dropindir.open_file(name)?.read_to_string(&mut dropin)?; + dropindir.open(name)?.read_to_string(&mut dropin)?; config.push_str(dropin.as_str()); writeln!(config, "### END {name} ###")?; println!("Added {name}"); } - let grub2dir = bootdir.sub_dir(GRUB2DIR)?; + let grub2dir = bootdir.open_dir(GRUB2DIR)?; grub2dir - .write_file_contents("grub.cfg", GRUBCONFIG_FILE_MODE, config.as_bytes()) + .atomic_write_with_perms( + "grub.cfg", + config.as_bytes(), + Permissions::from_mode(GRUBCONFIG_FILE_MODE), + ) .context("Copying grub-static.cfg")?; println!("Installed: grub.cfg"); @@ -104,37 +111,43 @@ pub(crate) fn install( .ok_or_else(|| anyhow::anyhow!("Failed to find UUID for boot"))?; let grub2_uuid_contents = format!("set BOOT_UUID=\"{bootfs_uuid}\"\n"); let uuid_path = "bootuuid.cfg"; + grub2dir - .write_file_contents(uuid_path, GRUBCONFIG_FILE_MODE, grub2_uuid_contents) + .atomic_write_with_perms( + uuid_path, + grub2_uuid_contents, + Permissions::from_mode(GRUBCONFIG_FILE_MODE), + ) .context("Writing bootuuid.cfg")?; println!("Installed: bootuuid.cfg"); + Some(uuid_path) } else { None }; - fsfreeze_thaw_cycle(grub2dir.open_file(".")?)?; + fsfreeze_thaw_cycle(grub2dir.reopen_as_ownedfd()?)?; if let Some(vendordir) = installed_efi_vendor { log::debug!("vendordir={:?}", &vendordir); let vendor = PathBuf::from(vendordir); let target = &vendor.join("grub.cfg"); let dest_efidir = target_root - .sub_dir_optional("boot/efi/EFI") + .open_dir_optional("boot/efi/EFI") .context("Opening /boot/efi/EFI")?; if let Some(efidir) = dest_efidir { configdir - .copy_file_at("grub-static-efi.cfg", &efidir, target) + .copy("grub-static-efi.cfg", &efidir, target) .context("Copying static EFI")?; println!("Installed: {target:?}"); if let Some(uuid_path) = uuid_path { let target = &vendor.join(uuid_path); grub2dir - .copy_file_at(uuid_path, &efidir, target) + .copy(uuid_path, &efidir, target) .context("Writing bootuuid.cfg to efi dir")?; println!("Installed: {target:?}"); } - fsfreeze_thaw_cycle(efidir.open_file(".")?)?; + fsfreeze_thaw_cycle(efidir.reopen_as_ownedfd()?)?; } else { println!("Could not find /boot/efi/EFI when installing {target:?}"); } @@ -144,8 +157,8 @@ pub(crate) fn install( } #[context("Create file boot/grub2/grubenv")] -fn write_grubenv(grubdir: &openat::Dir) -> Result<()> { - if grubdir.exists(GRUBENV)? { +fn write_grubenv(grubdir: &Dir) -> Result<()> { + if grubdir.exists(GRUBENV) { return Ok(()); } let editenv = Path::new("/usr/bin/grub2-editenv"); @@ -160,7 +173,7 @@ fn write_grubenv(grubdir: &openat::Dir) -> Result<()> { } #[context("Ensure file permissions are 0600")] -fn ensure_file_permissions(target_dir: &openat::Dir, target_file: &str) -> Result<()> { +fn ensure_file_permissions(target_dir: &Dir, target_file: &str) -> Result<()> { let metadata = match target_dir.metadata(target_file) { Ok(metadata) => metadata, Err(e) if e.kind() == std::io::ErrorKind::NotFound => { @@ -173,14 +186,14 @@ fn ensure_file_permissions(target_dir: &openat::Dir, target_file: &str) -> Resul let mode = metadata.permissions().mode() & 0o777; if mode != GRUBCONFIG_FILE_MODE { target_dir - .set_mode(target_file, GRUBCONFIG_FILE_MODE) + .set_permissions(target_file, Permissions::from_mode(GRUBCONFIG_FILE_MODE)) .with_context(|| format!("Setting {} permissions to 0600", target_file))?; } Ok(()) } #[context("Ensure grub files permissions are 0600")] -pub(crate) fn ensure_grub_permissions(grub_dir: &openat::Dir) -> Result<()> { +pub(crate) fn ensure_grub_permissions(grub_dir: &Dir) -> Result<()> { for file_name in GRUB_FILES.iter() { ensure_file_permissions(&grub_dir, file_name) .with_context(|| format!("Failed to ensure permissions for {}", file_name))?; @@ -199,14 +212,14 @@ mod tests { env_logger::init(); let td = tempfile::tempdir()?; let tdp = td.path(); - let td = openat::Dir::open(tdp)?; + let td = Dir::open_ambient_dir(tdp, ambient_authority())?; std::fs::create_dir_all(tdp.join("boot/grub2"))?; std::fs::create_dir_all(tdp.join("boot/efi/EFI/BOOT"))?; std::fs::create_dir_all(tdp.join("boot/efi/EFI/fedora"))?; install(&td, None, Some("fedora"), false).unwrap(); - assert!(td.exists("boot/grub2/grub.cfg")?); - assert!(td.exists("boot/efi/EFI/fedora/grub.cfg")?); + assert!(td.exists("boot/grub2/grub.cfg")); + assert!(td.exists("boot/efi/EFI/fedora/grub.cfg")); Ok(()) } #[test] @@ -220,10 +233,10 @@ mod tests { let tdp = td.path(); let grub = tdp.join("boot/grub2"); std::fs::create_dir_all(&grub)?; - let td = openat::Dir::open(&grub)?; + let td = Dir::open_ambient_dir(&grub, ambient_authority())?; write_grubenv(&td)?; - assert!(td.exists("grubenv")?); + assert!(td.exists("grubenv")); ensure_file_permissions(&td, "grubenv")?; // Verify permissions are now 0600 { @@ -239,12 +252,20 @@ mod tests { let tdp = td.path(); let grub = tdp.join("boot/grub2"); std::fs::create_dir_all(&grub)?; - let grub_dir = openat::Dir::open(&grub)?; + let grub_dir = Dir::open_ambient_dir(&grub, ambient_authority())?; // Create grubenv, grub.cfg with permissions (0o644), no bootuuid.cfg { - grub_dir.write_file_contents("grubenv", 0o644, "grubenv content".as_bytes())?; - grub_dir.write_file_contents("grub.cfg", 0o644, "grub.cfg content".as_bytes())?; + grub_dir.atomic_write_with_perms( + "grubenv", + "grubenv content".as_bytes(), + Permissions::from_mode(0o644), + )?; + grub_dir.atomic_write_with_perms( + "grub.cfg", + "grub.cfg content".as_bytes(), + Permissions::from_mode(0o644), + )?; // bootuuid.cfg doesn't exist assert!(grub_dir.metadata("bootuuid.cfg").is_err()); diff --git a/src/util.rs b/src/util.rs index e2d4e237..733bf80c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -3,9 +3,9 @@ use std::path::Path; use std::process::Command; use anyhow::{bail, Context, Result}; +use cap_std::fs::Dir; use chrono::{DateTime, Utc}; use fn_error_context::context; -use openat_ext::OpenatDirExt; /// Parse an environment variable as UTF-8 #[allow(dead_code)] @@ -22,32 +22,28 @@ pub(crate) fn getenv_utf8(n: &str) -> Result> { } #[allow(dead_code)] -pub(crate) fn filenames(dir: &openat::Dir) -> Result> { +pub(crate) fn filenames(dir: &Dir) -> Result> { let mut ret = HashSet::new(); - for entry in dir.list_dir(".")? { + for entry in dir.entries_utf8()? { let entry = entry?; - let Some(name) = entry.file_name().to_str() else { - bail!("Invalid UTF-8 filename: {:?}", entry.file_name()) + let Ok(name) = entry.file_name() else { + bail!("Invalid filename: {:?}", entry.file_name()) }; - match dir.get_file_type(&entry)? { - openat::SimpleType::File => { - ret.insert(format!("/{name}")); - } - openat::SimpleType::Dir => { - let child = dir.sub_dir(name)?; - for mut k in filenames(&child)?.drain() { - k.reserve(name.len() + 1); - k.insert_str(0, name); - k.insert(0, '/'); - ret.insert(k); - } - } - openat::SimpleType::Symlink => { - bail!("Unsupported symbolic link {:?}", entry.file_name()) - } - openat::SimpleType::Other => { - bail!("Unsupported non-file/directory {:?}", entry.file_name()) + let file_type = entry.file_type()?; + if file_type.is_file() { + ret.insert(format!("/{name}")); + } else if file_type.is_dir() { + let child = dir.open_dir(&name)?; + for mut k in filenames(&child)?.drain() { + k.reserve(name.len() + 1); + k.insert_str(0, &name); + k.insert(0, '/'); + ret.insert(k); } + } else if file_type.is_symlink() { + bail!("Unsupported symbolic link {:?}", entry.file_name()) + } else { + bail!("Unsupported non-file/directory {:?}", entry.file_name()) } } Ok(ret)