From fd49360efa960f8142f99d0cf24658a996b8af05 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 28 Jun 2024 13:45:38 -0400 Subject: [PATCH] Add cap-std, use in grubconfigs Starting on https://github.com/coreos/bootupd/issues/449 `openat` is pretty dead upstream, and especially going forward as we start to do more nontrivial file things, it's really useful to have cap-std's additional verification that we aren't accidentally escaping the root. Signed-off-by: Colin Walters --- Cargo.lock | 115 ++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/grubconfigs.rs | 55 +++++++++++++++------- src/util.rs | 12 +++++ 4 files changed, 165 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7af52d84..3b36d1f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -74,6 +80,7 @@ version = "0.2.19" dependencies = [ "anyhow", "bincode", + "cap-std-ext", "chrono", "clap", "env_logger", @@ -101,6 +108,58 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "cap-primitives" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00172660727e2d7f808e7cc2bfffd093fdb3ea2ff2ef819289418a3c3ffab5ac" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix", + "windows-sys 0.52.0", + "winx", +] + +[[package]] +name = "cap-std" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd9187bb3f7478a4c135ea10473a41a5f029d2ac800c1adf64f35ec7d4c8603" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix", +] + +[[package]] +name = "cap-std-ext" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1285af420ee8cbf7ec30c02c08f9f25125fc0f7efd6d34eacad5b18e62d4e58" +dependencies = [ + "cap-primitives", + "cap-tempfile", + "rustix", +] + +[[package]] +name = "cap-tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccdf3787d405176792605744d5fd59dcc66703f743c0c628516d7a5518b1e4de" +dependencies = [ + "cap-std", + "rand", + "rustix", + "uuid", +] + [[package]] name = "cc" version = "1.0.83" @@ -259,6 +318,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "fs-set-times" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "033b337d725b97690d86893f9de22b67b80dcc4e9ad815f348254c38119db8fb" +dependencies = [ + "io-lifetimes", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "fs2" version = "0.4.3" @@ -362,6 +432,28 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "io-extras" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9f046b9af244f13b3bd939f55d16830ac3a201e8a9ba9661bfcb03e2be72b9b" +dependencies = [ + "io-lifetimes", + "windows-sys 0.52.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "is-terminal" version = "0.4.9" @@ -430,6 +522,12 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "memchr" version = "2.6.4" @@ -699,14 +797,16 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.4.1", "errno", + "itoa", "libc", "linux-raw-sys", + "once_cell", "windows-sys 0.52.0", ] @@ -866,6 +966,7 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ + "getrandom", "serde", ] @@ -1128,3 +1229,13 @@ name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winx" +version = "0.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9643b83820c0cd246ecabe5fa454dd04ba4fa67996369466d0747472d337346" +dependencies = [ + "bitflags 2.4.1", + "windows-sys 0.52.0", +] diff --git a/Cargo.toml b/Cargo.toml index 4f1d4c09..7431e239 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0" bincode = "1.3.2" +cap-std-ext = "4.0.0" chrono = { version = "0.4.38", features = ["serde"] } clap = { version = "3.2", default-features = false, features = ["cargo", "derive", "std", "suggestions"] } env_logger = "0.10" diff --git a/src/grubconfigs.rs b/src/grubconfigs.rs index 09aeebfb..f063a113 100644 --- a/src/grubconfigs.rs +++ b/src/grubconfigs.rs @@ -2,8 +2,13 @@ use std::fmt::Write; use std::path::{Path, PathBuf}; use anyhow::{anyhow, Context, Result}; +use cap_std::fs::{Dir, DirBuilder, DirBuilderExt, MetadataExt}; +use cap_std_ext::cap_std; +use cap_std_ext::cap_std::fs::{Permissions, PermissionsExt}; +use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; -use openat_ext::OpenatDirExt; + +use crate::util; /// The subdirectory of /boot we use const GRUB2DIR: &str = "grub2"; @@ -17,27 +22,34 @@ pub(crate) fn install( installed_efi_vendor: Option<&str>, write_uuid: bool, ) -> Result<()> { - let bootdir = &target_root.sub_dir("boot").context("Opening /boot")?; + let target_root = &util::reopen_dir(target_root)?; + 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.dir_metadata()?.dev(); + let boot_dev = bootdir.dir_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.try_exists(GRUB2DIR)? { + let mut db = DirBuilder::new(); + db.mode(0o700); + bootdir.create_dir_with(GRUB2DIR, &db)?; } let mut config = std::fs::read_to_string(Path::new(CONFIGDIR).join("grub-static-pre.cfg"))?; - let dropindir = openat::Dir::open(&Path::new(CONFIGDIR).join(DROPINDIR))?; + let dropindir = Dir::open_ambient_dir( + &Path::new(CONFIGDIR).join(DROPINDIR), + cap_std::ambient_authority(), + )?; // 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())); + // cc https://github.com/rust-lang/rust/issues/85573#issuecomment-2195271304 + entries.sort_by(|a, b| a.file_name().cmp(&b.file_name())); for ent in entries { let name = ent.file_name(); let name = name @@ -49,7 +61,7 @@ pub(crate) fn install( } writeln!(config, "source $prefix/{name}")?; dropindir - .copy_file_at(name, bootdir, format!("{GRUB2DIR}/{name}")) + .copy(name, bootdir, format!("{GRUB2DIR}/{name}")) .with_context(|| format!("Copying {name}"))?; println!("Installed {name}"); } @@ -59,21 +71,27 @@ pub(crate) fn install( config.push_str(post.as_str()); } + let rperms = Permissions::from_mode(0o644); bootdir - .write_file_contents(format!("{GRUB2DIR}/grub.cfg"), 0o644, config.as_bytes()) + .atomic_write_with_perms( + format!("{GRUB2DIR}/grub.cfg"), + config.as_bytes(), + rperms.clone(), + ) .context("Copying grub-static.cfg")?; println!("Installed: grub.cfg"); let uuid_path = if write_uuid { let target_fs = if boot_is_mount { bootdir } else { target_root }; - let bootfs_meta = crate::filesystem::inspect_filesystem(target_fs, ".")?; + let target_fs_dir = &util::reopen_legacy_dir(target_fs)?; + let bootfs_meta = crate::filesystem::inspect_filesystem(target_fs_dir, ".")?; let bootfs_uuid = bootfs_meta .uuid .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 = format!("{GRUB2DIR}/bootuuid.cfg"); bootdir - .write_file_contents(&uuid_path, 0o644, grub2_uuid_contents) + .atomic_write_with_perms(&uuid_path, grub2_uuid_contents, rperms) .context("Writing bootuuid.cfg")?; Some(uuid_path) } else { @@ -85,11 +103,15 @@ pub(crate) fn install( 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 { efidir - .copy_file(&Path::new(CONFIGDIR).join("grub-static-efi.cfg"), target) + .copy( + &Path::new(CONFIGDIR).join("grub-static-efi.cfg"), + &efidir, + target, + ) .context("Copying static EFI")?; println!("Installed: {target:?}"); if let Some(uuid_path) = uuid_path { @@ -97,7 +119,7 @@ pub(crate) fn install( let filename = Path::new(&uuid_path).file_name().unwrap(); let target = &vendor.join(filename); bootdir - .copy_file_at(uuid_path, &efidir, target) + .copy(uuid_path, &efidir, target) .context("Writing bootuuid.cfg to efi dir")?; } } @@ -109,6 +131,7 @@ pub(crate) fn install( #[cfg(test)] mod tests { use super::*; + use openat_ext::OpenatDirExt; #[test] #[ignore] diff --git a/src/util.rs b/src/util.rs index c88d173e..3882b81b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,8 +1,10 @@ use std::collections::HashSet; +use std::os::fd::{AsRawFd, BorrowedFd}; use std::path::Path; use std::process::Command; use anyhow::{bail, Context, Result}; +use cap_std_ext::cap_std::fs::Dir; use openat_ext::OpenatDirExt; pub(crate) trait CommandRunExt { @@ -99,3 +101,13 @@ pub(crate) fn cmd_output(cmd: &mut Command) -> Result { String::from_utf8(result.stdout) .with_context(|| format!("decoding as UTF-8 output of `{:#?}`", cmd)) } + +// Re-open an [`openat::Dir`] via the cap-std version. +pub(crate) fn reopen_dir(d: &openat::Dir) -> Result { + Dir::reopen_dir(&unsafe { BorrowedFd::borrow_raw(d.as_raw_fd()) }).map_err(Into::into) +} + +// Re-open an [`cap_std::fs::Dir`] as a legacy openat::Dir. +pub(crate) fn reopen_legacy_dir(d: &Dir) -> Result { + openat::Dir::open(format!("/proc/self/fd/{}", d.as_raw_fd())).map_err(Into::into) +}