/
githubmirror
/
rpm-ostree
Обзор
Документация
Войти
/
githubmirror
/
rpm-ostree
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
rust/src/tmpfiles.rs
577 строк
21 KB
Colin Walters
tmpfiles: Don't call umask() in tests
24 янв 2025, 17:31
24 янв 2025, 17:31
9df317a
Код
Авторство
О чём код?
/* * Copyright (C) 2023 Red Hat, Inc. * * SPDX-License-Identifier: Apache-2.0 OR MIT */ use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write; use std::io::{BufRead, BufReader, Write as StdWrite}; use std::path::{Path, PathBuf}; use anyhow::bail; use anyhow::{Context, Result}; use camino::Utf8Path; use camino::Utf8PathBuf; use cap_std::fs::{Dir, Permissions, PermissionsExt}; use cap_std::fs::{DirBuilderExt, MetadataExt}; use cap_std::io_lifetimes::AsFilelike; use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; use ostree_ext::gio; use ostree_ext::gio::{FileInfo, FileType}; use ostree_ext::prelude::CancellableExtManual; use crate::cxxrsutil::*; use crate::ffiutil; const TMPFILESD: &str = "usr/lib/tmpfiles.d"; /// A "staging" tmpfiles.d location generated when importing packages /// in rpmostree-importer.cxx. const RPMOSTREE_TMPFILESD: &str = "usr/lib/rpm-ostree/tmpfiles.d"; /// The final merged tmpfiles.d entry we generate from the per-package /// "staged" versions stored above. const AUTOVAR_PATH: &str = "rpm-ostree-autovar.conf"; /// Canonicalize and escape a path value for tmpfiles.d /// At the current time the only canonicalization we do is remap /var/run -> /run. fn canonicalize_escape_path(abs_path: Cow<str>) -> Cow<str> { let mut tweaked_path = abs_path; // systemd-tmpfiles complains loudly about writing to /var/run; // ideally, all of the packages get fixed for this but...eh. if tweaked_path.starts_with("/var/run/") { let trimmed = tweaked_path.trim_start_matches("/var"); tweaked_path = Cow::Owned(trimmed.to_string()); } // Handle file paths with spaces and other chars; // see https://github.com/coreos/rpm-ostree/issues/2029 */ crate::utils::shellsafe_quote(tweaked_path) } /// Translate a filepath entry to an equivalent tmpfiles.d line. #[context("Translating {}", abs_path)] pub(crate) fn translate_to_tmpfiles_d( abs_path: &str, file_info: &FileInfo, username: &str, groupname: &str, ) -> Result<String> { let mut bufwr = String::new(); let path_type = file_info.file_type(); let filetype_char = match path_type { FileType::Directory => 'd', FileType::Regular => 'f', FileType::SymbolicLink => 'L', x => bail!("path '{}' has invalid type: {:?}", abs_path, x), }; let fixed_path = canonicalize_escape_path(Cow::Borrowed(abs_path)); write!(&mut bufwr, "{} {}", filetype_char, fixed_path)?; if path_type == FileType::SymbolicLink { let link_target = file_info .symlink_target() .ok_or_else(|| anyhow::anyhow!("missing symlink target"))?; let link_target = link_target .to_str() .ok_or_else(|| anyhow::anyhow!("Invalid non-UTF8 symlink: {link_target:?}"))?; write!(&mut bufwr, " - - - - {}", link_target)?; } else { let mode = file_info.attribute_uint32("unix::mode") & !libc::S_IFMT; write!(&mut bufwr, " {:04o} {} {} - -", mode, username, groupname)?; }; Ok(bufwr) } pub fn tmpfiles_translate( abs_path: &str, file_info: &crate::FFIGFileInfo, username: &str, groupname: &str, ) -> CxxResult<String> { let entry = translate_to_tmpfiles_d(abs_path, &file_info.glib_reborrow(), username, groupname)?; Ok(entry) } #[context("Converting /var to tmpfiles.d")] pub(crate) fn var_to_tmpfiles(rootfs: &Dir, cancellable: Option<&gio::Cancellable>) -> Result<()> { /* List of files that are known to possibly exist, but in practice * things work fine if we simply ignore them. Don't add something * to this list unless you've verified it's handled correctly at * runtime. (And really both in CentOS and Fedora) */ static KNOWN_STATE_FILES: &[&str] = &[ // https://bugzilla.redhat.com/show_bug.cgi?id=789407 "var/lib/systemd/random-seed", "var/lib/systemd/catalog/database", "var/lib/plymouth/boot-duration", // These two are part of systemd's var.tmp "var/log/wtmp", "var/log/btmp", ]; let pwdb = crate::PasswdDB::populate_new(rootfs)?; // We never want to traverse into /run when making tmpfiles since it's a tmpfs // Note that in a Fedora root, /var/run is a symlink, though on el7, it can be a dir. // See: https://github.com/projectatomic/rpm-ostree/pull/831 if rootfs.try_exists("var/run")? { rootfs .remove_dir_all("var/run") .context("Failed to remove /var/run")?; } // Here, delete some files ahead of time to avoid emitting warnings // for things that are known to be harmless. for path in KNOWN_STATE_FILES { rootfs .remove_file_optional(*path) .with_context(|| format!("unlinkat({})", path))?; } // Convert /var wholesale to tmpfiles.d. Note that with unified core, this // code should no longer be necessary as we convert packages on import. // Make output file world-readable, no reason why not to // https://bugzilla.redhat.com/show_bug.cgi?id=1631794 let mut db = cap_std::fs::DirBuilder::new(); db.recursive(true); db.mode(0o755); rootfs.create_dir_with("usr/lib/tmpfiles.d", &db)?; let mode = Permissions::from_mode(0o644); rootfs.atomic_replace_with( "usr/lib/tmpfiles.d/rpm-ostree-1-autovar.conf", |bufwr| -> Result<()> { bufwr.get_mut().as_file_mut().set_permissions(mode)?; let mut prefix = Utf8PathBuf::from("var"); let mut entries = BTreeSet::new(); convert_path_to_tmpfiles_d_recurse( &mut entries, &pwdb, rootfs, &mut prefix, &cancellable, ) .with_context(|| format!("Processing var content /{}", prefix))?; for line in entries { bufwr.write_all(line.as_bytes())?; writeln!(bufwr)?; } Ok(()) }, )?; Ok(()) } /// Recursively explore target directory and translate content to tmpfiles.d entries. See /// `convert_var_to_tmpfiles_d` for more background. /// /// This proceeds depth-first and progressively deletes translated subpaths as it goes. /// `prefix` is updated at each recursive step, so that in case of errors it can be /// used to pinpoint the faulty path. #[allow(clippy::nonminimal_bool)] fn convert_path_to_tmpfiles_d_recurse( out_entries: &mut BTreeSet<String>, pwdb: &crate::PasswdDB, rootfs: &Dir, prefix: &mut Utf8PathBuf, cancellable: &Option<&gio::Cancellable>, ) -> Result<()> { let current_prefix = prefix.clone(); for subpath in rootfs.read_dir(¤t_prefix).context("Reading dir")? { if let Some(c) = cancellable { c.set_error_if_cancelled()?; } let subpath = subpath?; let meta = subpath.metadata()?; let fname: Utf8PathBuf = PathBuf::from(subpath.file_name()).try_into()?; let full_path = Utf8Path::new(¤t_prefix).join(&fname); // Workaround for nfs-utils in RHEL7: // https://bugzilla.redhat.com/show_bug.cgi?id=1427537 let retain_entry = meta.is_file() && full_path.starts_with("var/lib/nfs"); if !retain_entry && !(meta.is_dir() || meta.is_symlink()) { rootfs .remove_file_optional(&full_path) .with_context(|| format!("Removing {:?}", &full_path))?; println!("Ignoring non-directory/non-symlink '{:?}'", &full_path); continue; } // Translate this file entry. let entry = { let mode = meta.mode() & !libc::S_IFMT; let file_info = gio::FileInfo::new(); file_info.set_attribute_uint32("unix::mode", mode); if meta.is_dir() { file_info.set_file_type(FileType::Directory); } else if meta.is_symlink() { file_info.set_file_type(FileType::SymbolicLink); let link_target = cap_primitives::fs::read_link_contents( &rootfs.as_filelike_view(), full_path.as_std_path(), ) .context("Reading symlink")?; let link_target = link_target.to_str().ok_or_else(|| { anyhow::anyhow!("non UTF-8 symlink target '{:?}'", link_target) })?; file_info.set_symlink_target(link_target); } else if meta.is_file() { file_info.set_file_type(FileType::Regular); } else { unreachable!("invalid path type: {:?}", full_path); } let abs_path = Utf8Path::new("/").join(&full_path); let username = pwdb.lookup_user(meta.uid())?; let groupname = pwdb.lookup_group(meta.gid())?; translate_to_tmpfiles_d(abs_path.as_str(), &file_info, &username, &groupname)? }; out_entries.insert(entry); if meta.is_dir() { // New subdirectory discovered, recurse into it. *prefix = full_path.clone(); convert_path_to_tmpfiles_d_recurse(out_entries, pwdb, rootfs, prefix, cancellable)?; rootfs.remove_dir_all(&full_path)?; } else { rootfs.remove_file(&full_path)?; } } Ok(()) } #[context("Deduplicate tmpfiles entries")] pub fn deduplicate_tmpfiles_entries(tmprootfs_dfd: i32) -> CxxResult<()> { let tmprootfs_dfd = unsafe { ffiutil::ffi_dirfd(tmprootfs_dfd)? }; // scan all rpm-ostree auto generated entries and save let tmpfiles_dir = tmprootfs_dfd .open_dir_optional(RPMOSTREE_TMPFILESD) .context(RPMOSTREE_TMPFILESD)?; let mut rpmostree_tmpfiles_entries = if let Some(tmpfiles_dir) = tmpfiles_dir { read_tmpfiles(&tmpfiles_dir)? } else { Default::default() }; // remove autovar.conf first, then scan all system entries and save let tmpfiles_dir = if let Some(d) = tmprootfs_dfd .open_dir_optional(TMPFILESD) .context(TMPFILESD)? { d } else { if !rpmostree_tmpfiles_entries.is_empty() { return Err( format!("No {TMPFILESD} directory found, but have tmpfiles to process").into(), ); } // Nothing to do here return Ok(()); }; if tmpfiles_dir.try_exists(AUTOVAR_PATH)? { tmpfiles_dir.remove_file(AUTOVAR_PATH)?; } let system_tmpfiles_entries = read_tmpfiles(&tmpfiles_dir)?; // remove duplicated entries in auto-generated tmpfiles.d, // which are already in system tmpfiles for (key, _) in system_tmpfiles_entries { rpmostree_tmpfiles_entries.retain(|k, _value| k != &key); } { // save the noduplicated entries let mut entries = String::from("# This file was generated by rpm-ostree.\n"); for (_key, value) in rpmostree_tmpfiles_entries { writeln!(entries, "{value}").unwrap(); } let perms = Permissions::from_mode(0o644); tmpfiles_dir.atomic_write_with_perms(AUTOVAR_PATH, entries.as_bytes(), perms)?; } Ok(()) } /// Read all tmpfiles.d entries in the target directory, and return a mapping /// from (file path) => (single tmpfiles.d entry line) #[context("Read systemd tmpfiles.d")] fn read_tmpfiles(tmpfiles_dir: &Dir) -> Result<BTreeMap<String, String>> { let mut result = BTreeMap::new(); for entry in tmpfiles_dir.entries()? { let entry = entry?; let name = entry.file_name(); let Some(extension) = Path::new(&name).extension() else { continue; }; if extension != "conf" { continue; } let r = BufReader::new(entry.open()?); for line in r.lines() { let line = line?; if line.is_empty() || line.starts_with("#") { continue; } let path = tmpfiles_entry_get_path(&line)?; result.insert(path.to_owned(), line); } } Ok(result) } #[context("Scan tmpfiles entries and get path")] fn tmpfiles_entry_get_path(line: &str) -> Result<&str> { line.split_whitespace() .nth(1) .ok_or_else(|| anyhow::anyhow!("Malformed tmpfiles.d entry ({line})")) } #[cfg(test)] mod tests { use cap_std::fs::DirBuilder; use cap_std_ext::cap_tempfile; use rustix::fd::AsRawFd; use super::*; #[test] fn test_tmpfiles_entry_get_path() { let cases = [ ("z /dev/kvm 0666 - kvm -", "/dev/kvm"), ("d /run/lock/lvm 0700 root root -", "/run/lock/lvm"), ("a+ /var/lib/tpm2-tss/system/keystore - - - - default:group:tss:rwx", "/var/lib/tpm2-tss/system/keystore"), ]; for (input, expected) in cases { let path = tmpfiles_entry_get_path(input).unwrap(); assert_eq!(path, expected, "Input: {input}"); } } fn newroot() -> Result<cap_std_ext::cap_tempfile::TempDir> { let root = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?; root.create_dir_all(RPMOSTREE_TMPFILESD)?; root.create_dir_all(TMPFILESD)?; Ok(root) } #[test] fn test_deduplicate_noop() -> Result<()> { let root = &newroot()?; deduplicate_tmpfiles_entries(root.as_raw_fd())?; Ok(()) } // The first and 3rd are duplicates const PKG_FILESYSTEM_CONTENTS: &str = indoc::indoc! { r#" d /var/cache 0755 root root - - d /var/lib/games 0755 root root - - d /var/tmp 1777 root root - - d /var/spool/mail 0775 root mail - - "# }; const VAR_CONF: &str = indoc::indoc! { r#" d /var/cache 0755 - - - "# }; const TMP_CONF: &str = indoc::indoc! { r#" q /var/tmp 1777 root root 30d "# }; #[test] fn test_deduplicate() -> Result<()> { let root = &newroot()?; let rpmostree_tmpfiles_dir = root.open_dir(RPMOSTREE_TMPFILESD)?; let tmpfiles_dir = root.open_dir(TMPFILESD)?; rpmostree_tmpfiles_dir .atomic_write(format!("pkg-filesystem.conf"), PKG_FILESYSTEM_CONTENTS)?; tmpfiles_dir.atomic_write("var.conf", VAR_CONF)?; tmpfiles_dir.atomic_write("tmp.conf", TMP_CONF)?; assert!(!tmpfiles_dir.try_exists(AUTOVAR_PATH)?); deduplicate_tmpfiles_entries(root.as_raw_fd())?; let contents = tmpfiles_dir.read_to_string(AUTOVAR_PATH).unwrap(); assert!(contents.contains("# This file was generated by rpm-ostree.")); let entries = contents .lines() .filter(|l| !(l.is_empty() || l.starts_with('#'))) .collect::<Vec<_>>(); assert_eq!(entries.len(), 2); assert_eq!(entries[0], "d /var/lib/games 0755 root root - -"); assert_eq!(entries[1], "d /var/spool/mail 0775 root mail - -"); Ok(()) } #[test] /// Verify that we no-op if the directories don't exist. fn test_deduplicate_emptydir() -> Result<()> { let root = &cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?; deduplicate_tmpfiles_entries(root.as_raw_fd())?; Ok(()) } #[test] fn test_tmpfiles_d_translation() { use rustix::process::{getegid, geteuid}; // Create an empty file with the given mode fn touch(d: &Dir, p: impl AsRef<Utf8Path>, mode: u32) -> Result<()> { d.atomic_replace_with(p.as_ref(), |w| { w.get_mut() .as_file_mut() .set_permissions(Permissions::from_mode(mode)) .map_err(Into::into) }) } let mut db = DirBuilder::new(); db.recursive(true); db.mode(0o755); // Prepare a minimal rootfs as playground. let rootfs = cap_tempfile::tempdir(cap_std::ambient_authority()).unwrap(); let uid = geteuid().as_raw(); let gid = getegid().as_raw(); let uid_str = format!("{uid}"); let gid_str = format!("{gid}"); let mut expected_disk_size = 30u64; { for dirpath in &["usr/lib", "usr/etc", "var"] { rootfs.ensure_dir_with(*dirpath, &db).unwrap(); } for filepath in &["usr/lib/passwd", "usr/lib/group"] { touch(&rootfs, *filepath, 0o755).unwrap() } rootfs .atomic_write( "usr/etc/passwd", format!("test-user:x:{uid_str}:{gid_str}:::",), ) .unwrap(); expected_disk_size += uid_str.len() as u64; expected_disk_size += gid_str.len() as u64; rootfs .atomic_write("usr/etc/group", format!("test-group:x:{gid_str}:")) .unwrap(); expected_disk_size += gid_str.len() as u64; } // Add test content. rootfs.ensure_dir_with("var/lib/systemd", &db).unwrap(); touch(&rootfs, "var/lib/systemd/random-seed", 0o770).unwrap(); rootfs.ensure_dir_with("var/lib/nfs", &db).unwrap(); touch(&rootfs, "var/lib/nfs/etab", 0o770).unwrap(); let global_rwx = Permissions::from_mode(0o777); rootfs.ensure_dir_with("var/lib/test/nested", &db).unwrap(); rootfs .set_permissions("var/lib/test", global_rwx.clone()) .unwrap(); rootfs .set_permissions("var/lib/test/nested", global_rwx) .unwrap(); touch(&rootfs, "var/lib/test/nested/file", 0o770).unwrap(); rootfs .symlink("../", "var/lib/test/nested/symlink") .unwrap(); cap_primitives::fs::symlink_contents( "/var/lib/foo", &rootfs.as_filelike_view(), "var/lib/test/absolute-symlink", ) .unwrap(); // Also make this a sanity test for our directory size API let cancellable = gio::Cancellable::new(); assert_eq!( crate::directory_size(rootfs.as_raw_fd(), cancellable.reborrow_cxx()).unwrap(), expected_disk_size ); crate::var_to_tmpfiles(&rootfs, gio::Cancellable::NONE).unwrap(); let autovar_path = "usr/lib/tmpfiles.d/rpm-ostree-1-autovar.conf"; assert!(!rootfs.try_exists("var/lib").unwrap()); assert!(rootfs.try_exists(autovar_path).unwrap()); let entries: Vec<String> = rootfs .read_to_string(autovar_path) .unwrap() .lines() .map(|s| s.to_owned()) .collect(); let expected = &[ "L /var/lib/test/absolute-symlink - - - - /var/lib/foo", "L /var/lib/test/nested/symlink - - - - ../", "d /var/lib 0755 test-user test-group - -", "d /var/lib/nfs 0755 test-user test-group - -", "d /var/lib/systemd 0755 test-user test-group - -", "d /var/lib/test 0777 test-user test-group - -", "d /var/lib/test/nested 0777 test-user test-group - -", "f /var/lib/nfs/etab 0770 test-user test-group - -", ]; similar_asserts::assert_eq!(entries, expected); } #[test] fn test_canonicalize_escape_path() { let intact_cases = vec!["/", "/var", "/var/foo", "/run/foo"]; for entry in intact_cases { let output = canonicalize_escape_path(Cow::Borrowed(entry)); similar_asserts::assert_eq!(output, entry); } let quoting_cases = maplit::btreemap! { "/var/foo bar" => r#"'/var/foo bar'"#, "/var/run/" => "/run/", "/var/run/foo bar" => r#"'/run/foo bar'"#, }; for (input, expected) in quoting_cases { let output = canonicalize_escape_path(Cow::Borrowed(input)); similar_asserts::assert_eq!(output, expected); } } #[test] fn test_translate_to_tmpfiles_d() { let path = r#"/var/foo bar"#; let username = "testuser"; let groupname = "testgroup"; { // Directory let file_info = FileInfo::new(); file_info.set_file_type(FileType::Directory); file_info.set_attribute_uint32("unix::mode", 0o721); let out = translate_to_tmpfiles_d(path, &file_info, username, groupname).unwrap(); let expected = r#"d '/var/foo bar' 0721 testuser testgroup - -"#; similar_asserts::assert_eq!(out, expected); } { // Symlink let file_info = FileInfo::new(); file_info.set_file_type(FileType::SymbolicLink); file_info.set_symlink_target("/mytarget"); let out = translate_to_tmpfiles_d(path, &file_info, username, groupname).unwrap(); let expected = r#"L '/var/foo bar' - - - - /mytarget"#; similar_asserts::assert_eq!(out, expected); } { // File let file_info = FileInfo::new(); file_info.set_file_type(FileType::Regular); file_info.set_attribute_uint32("unix::mode", 0o123); let out = translate_to_tmpfiles_d(path, &file_info, username, groupname).unwrap(); let expected = r#"f '/var/foo bar' 0123 testuser testgroup - -"#; similar_asserts::assert_eq!(out, expected); } { // Other unsupported let file_info = FileInfo::new(); file_info.set_file_type(FileType::Unknown); translate_to_tmpfiles_d(path, &file_info, username, groupname).unwrap_err(); } } }