/
githubmirror
/
rpm-ostree
Обзор
Документация
Войти
/
githubmirror
/
rpm-ostree
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
rust/src/composepost.rs
1 588 строк
57 KB
Joseph Marrero Corchado
composepost: Add subs_dist equivalency for /var/lib/selinux
20 июл 2026, 22:59
20 июл 2026, 22:59
ccb5892
Код
Авторство
О чём код?
//! Logic for post-processing a filesystem tree, server-side. //! //! This code runs server side to "postprocess" a filesystem tree (usually //! containing mostly RPMs) in order to prepare it as an OSTree commit. // SPDX-License-Identifier: Apache-2.0 OR MIT use crate::bwrap; use crate::bwrap::Bubblewrap; use crate::capstdext::dirbuilder_from_mode; use crate::cmdutils::CommandRunExt; use crate::cxxrsutil::*; use crate::ffi::BubblewrapMutability; use crate::ffiutil::ffi_dirfd; use crate::normalization; use crate::treefile::{OptUsrLocal, Treefile}; use anyhow::{anyhow, bail, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; use cap_std::fs::Dir; use cap_std::fs_utf8::Dir as Utf8Dir; use cap_std::io_lifetimes::AsFilelike; use cap_std_ext::cap_std; use cap_std_ext::cap_std::fs::{DirBuilderExt, MetadataExt, Permissions, PermissionsExt}; use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; use gio::prelude::*; use ostree_ext::{gio, glib, ostree}; use rayon::prelude::*; use std::borrow::Cow; use std::fmt::Write as FmtWrite; use std::io::{BufRead, BufReader, Seek, Write}; use std::os::unix::io::AsRawFd; use std::os::unix::prelude::IntoRawFd; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::process::{Command, Stdio}; /// Directories that are moved out and symlinked from their `/var/lib/<entry>` /// location to `/usr/lib/<entry>`. pub(crate) static COMPAT_VARLIB_SYMLINKS: &[&str] = &["alternatives", "vagrant"]; const DEFAULT_DIRMODE: u32 = 0o755; /// Symlinks to ensure home directories persist by default. const OSTREE_HOME_SYMLINKS: &[(&str, &str)] = &[("var/roothome", "root"), ("var/home", "home")]; /* See rpmostree-core.h */ const RPMOSTREE_BASE_RPMDB: &str = "usr/lib/sysimage/rpm-ostree-base-db"; pub(crate) const RPMOSTREE_RPMDB_LOCATION: &str = "usr/share/rpm"; pub(crate) const RPMOSTREE_SYSIMAGE_RPMDB: &str = "usr/lib/sysimage/rpm"; pub(crate) const TRADITIONAL_RPMDB_LOCATION: &str = "var/lib/rpm"; const SD_LOCAL_FS_TARGET_REQUIRES: &str = "usr/lib/systemd/system/local-fs.target.requires"; #[context("Moving {}", name)] fn dir_move_if_exists(src: &cap_std::fs::Dir, dest: &cap_std::fs::Dir, name: &str) -> Result<()> { if src.symlink_metadata(name).is_ok() { src.rename(name, dest, name)?; } Ok(()) } /// Initialize an ostree-oriented root filesystem. /// /// Now unfortunately today, we're not generating toplevel filesystem entries /// because the `filesystem` package does it from Lua code, which we don't run. /// (See rpmostree-core.cxx) #[context("Initializing rootfs (base)")] fn compose_init_rootfs_base(rootfs_dfd: &cap_std::fs::Dir, tmp_is_dir: bool) -> Result<()> { const TOPLEVEL_DIRS: &[&str] = &["dev", "proc", "run", "sys", "var", "sysroot"]; let default_dirbuilder = &dirbuilder_from_mode(DEFAULT_DIRMODE); let default_dirmode = cap_std::fs::Permissions::from_mode(DEFAULT_DIRMODE); rootfs_dfd .set_permissions(".", default_dirmode) .context("Setting rootdir permissions")?; TOPLEVEL_DIRS.par_iter().try_for_each(|&d| { rootfs_dfd .ensure_dir_with(d, default_dirbuilder) .with_context(|| format!("Creating {d}")) .map(|_: bool| ()) })?; if tmp_is_dir { let tmp_mode = 0o1777; rootfs_dfd .ensure_dir_with("tmp", &dirbuilder_from_mode(tmp_mode)) .context("tmp")?; rootfs_dfd .set_permissions("tmp", cap_std::fs::Permissions::from_mode(tmp_mode)) .context("Setting permissions for tmp")?; } else { rootfs_dfd.symlink("sysroot/tmp", "tmp")?; } OSTREE_HOME_SYMLINKS .par_iter() .try_for_each(|&(dest, src)| { rootfs_dfd .symlink(dest, src) .with_context(|| format!("Creating {src}")) })?; rootfs_dfd .symlink("sysroot/ostree", "ostree") .context("Symlinking ostree -> sysroot/ostree")?; Ok(()) } /// Initialize a root filesystem set up for use with ostree's `root.transient` mode. #[context("Initializing rootfs (base)")] fn compose_init_rootfs_transient(rootfs_dfd: &cap_std::fs::Dir) -> Result<()> { // Enforce tmp-is-dir in this, because there's really no reason not to. compose_init_rootfs_base(rootfs_dfd, true)?; // Again we need to make these directories here because we don't run // the `filesystem` package's lua script. const EXTRA_TOPLEVEL_DIRS: &[&str] = &["opt", "media", "mnt", "usr/local"]; let mut db = dirbuilder_from_mode(DEFAULT_DIRMODE); db.recursive(true); EXTRA_TOPLEVEL_DIRS.par_iter().try_for_each(|&d| { // We need to handle the case where these may have been created as a symlink // by tmpfiles.d snippets for example. if let Some(meta) = rootfs_dfd.symlink_metadata_optional(d)? { if !meta.is_dir() { rootfs_dfd.remove_file(d)?; } } rootfs_dfd .ensure_dir_with(d, &db) .with_context(|| format!("Creating {d}")) .map(|_: bool| ()) })?; Ok(()) } /// Initialize an ostree-oriented root filesystem. /// /// This is hardcoded; in the future we may make more things configurable, /// but the goal is for all state to be in `/etc` and `/var`. #[context("Initializing rootfs")] fn compose_init_rootfs_strict( rootfs_dfd: &cap_std::fs::Dir, tmp_is_dir: bool, opt_usrlocal: OptUsrLocal, ) -> Result<()> { println!("Initializing rootfs"); compose_init_rootfs_base(rootfs_dfd, tmp_is_dir)?; let opt_symlink = match opt_usrlocal { OptUsrLocal::Var => Some("var/opt"), OptUsrLocal::Root => { rootfs_dfd.create_dir_all("opt")?; None } OptUsrLocal::StateOverlay => Some("usr/lib/opt"), }; // This is used in the case where we don't have a transient rootfs; redirect // these toplevel directories underneath /var. let ostree_strict_mode_symlinks = [ ("var/srv", "srv"), ("var/mnt", "mnt"), ("run/media", "media"), ]; ostree_strict_mode_symlinks .into_iter() .chain(opt_symlink.map(|link| (link, "opt"))) .try_for_each(|(dest, src)| { rootfs_dfd .symlink(dest, src) .with_context(|| format!("Creating {src}")) })?; Ok(()) } /// Prepare rootfs for commit. /// /// In the default mode, we initialize a basic root filesystem in @target_root_dfd, then walk over the /// root filesystem in @src_rootfs_fd and take the basic content (/usr, /boot, /var) /// and cherry pick only specific bits of the rest of the toplevel like compatibility /// symlinks (e.g. /lib64 -> /usr/lib64) if they exist. /// /// However, if the rootfs is setup as transient, then we just copy everything. #[context("Preparing rootfs for commit")] pub fn compose_prepare_rootfs( src_rootfs_dfd: i32, target_rootfs_dfd: i32, treefile: &mut Treefile, ) -> CxxResult<()> { let src_rootfs_dfd = unsafe { &ffi_dirfd(src_rootfs_dfd)? }; let target_rootfs_dfd = unsafe { &ffi_dirfd(target_rootfs_dfd)? }; let tmp_is_dir = treefile.parsed.base.tmp_is_dir.unwrap_or_default(); if crate::ostree_prepareroot::transient_root_enabled(src_rootfs_dfd)? { println!("Target has transient root enabled"); // While sadly tmp-is-dir: false by default, we want to encourage // people to switch, so just error out if they're somehow configured // things for the newer transient root model but forgot to set `tmp-is-dir`. if !tmp_is_dir { return Err("Transient root conflicts with tmp-is-dir: false" .to_string() .into()); } // We grab all the content from the source root by default on general principle, // but note this won't be very much right now because // we're not executing the `filesystem` package's lua script. for entry in src_rootfs_dfd.entries()? { let entry = entry?; let name = entry.file_name(); src_rootfs_dfd .rename(&name, target_rootfs_dfd, &name) .with_context(|| format!("Moving {name:?}"))?; } compose_init_rootfs_transient(target_rootfs_dfd)?; return Ok(()); } compose_init_rootfs_strict( target_rootfs_dfd, tmp_is_dir, treefile.parsed.base.opt_usrlocal.unwrap_or_default(), )?; println!("Moving /usr to target"); src_rootfs_dfd.rename("usr", target_rootfs_dfd, "usr")?; /* The kernel may be in the source rootfs /boot; to handle that, we always * rename the source /boot to the target, and will handle everything after * that in the target root. */ dir_move_if_exists(src_rootfs_dfd, target_rootfs_dfd, "boot")?; /* And grab /var - we'll convert to tmpfiles.d later */ dir_move_if_exists(src_rootfs_dfd, target_rootfs_dfd, "var")?; const TOPLEVEL_LINKS: &[&str] = &["lib", "lib64", "lib32", "bin", "sbin"]; println!("Copying toplevel compat symlinks"); TOPLEVEL_LINKS .par_iter() .try_for_each(|&l| dir_move_if_exists(src_rootfs_dfd, target_rootfs_dfd, l))?; Ok(()) } // rpm-ostree uses /home → /var/home by default as generated by our // rootfs; we don't expect people to change this. Let's be nice // and also fixup the $HOME entries generated by `useradd` so // that `~` shows up as expected in shells, etc. // // https://github.com/coreos/fedora-coreos-config/pull/18 // https://pagure.io/workstation-ostree-config/pull-request/121 // https://discussion.fedoraproject.org/t/adapting-user-home-in-etc-passwd/487/6 // https://github.com/justjanne/powerline-go/issues/94 #[context("Postprocessing useradd")] fn postprocess_useradd(rootfs_dfd: &cap_std::fs::Dir) -> Result<()> { let path = Utf8Path::new("usr/etc/default/useradd"); let perms = cap_std::fs::Permissions::from_mode(0o644); if let Some(f) = rootfs_dfd.open_optional(path).context("opening")? { rootfs_dfd .atomic_replace_with(path, |bufw| -> Result<_> { bufw.get_mut().as_file_mut().set_permissions(perms)?; let f = BufReader::new(&f); for line in f.lines() { let line = line?; if !line.starts_with("HOME=") { bufw.write_all(line.as_bytes())?; } else { bufw.write_all(b"HOME=/var/home")?; } bufw.write_all(b"\n")?; } Ok(()) }) .with_context(|| format!("Replacing {}", path))?; } Ok(()) } fn is_overlay_whiteout(meta: &cap_std::fs::Metadata) -> bool { (meta.mode() & libc::S_IFMT) == libc::S_IFCHR && meta.rdev() == 0 } /// Automatically "quote" embeded overlayfs whiteouts as regular files, and /// if configured error out on devices or ignore them. /// For more on overlayfs, see https://github.com/ostreedev/ostree/pull/2722/commits/0085494e350c72599fc5c0e00422885d80b3c660 #[context("Postprocessing devices")] fn postprocess_devices(root: &Dir, treefile: &Treefile) -> Result<()> { const OSTREE_WHITEOUT_PREFIX: &str = ".ostree-wh."; let mut n_overlay = 0u64; let mut n_devices = 0u64; fn recurse( root: &Dir, path: &Utf8Path, ignore_devices: bool, n_overlay: &mut u64, n_devices: &mut u64, ) -> Result<()> { for entry in root.read_dir(path)? { let entry = entry?; let meta = entry.metadata()?; let name = PathBuf::from(entry.file_name()); let name: Utf8PathBuf = name.try_into()?; if meta.is_dir() { recurse(root, &path.join(name), ignore_devices, n_overlay, n_devices)?; continue; } let is_device = matches!(meta.mode() & libc::S_IFMT, libc::S_IFCHR | libc::S_IFBLK); if !is_device { continue; } let srcpath = path.join(&name); if is_overlay_whiteout(&meta) { let targetname = format!("{OSTREE_WHITEOUT_PREFIX}{name}"); let destpath = path.join(&targetname); root.remove_file(srcpath)?; root.atomic_write_with_perms(destpath, "", meta.permissions())?; *n_overlay += 1; continue; } if ignore_devices { root.remove_file(srcpath)?; *n_devices += 1; } else { anyhow::bail!("Unsupported device file: {srcpath}") } } Ok(()) } recurse( root, ".".into(), treefile.get_ignore_devices(), &mut n_overlay, &mut n_devices, )?; if n_overlay > 0 { println!("Processed {n_overlay} embedded whiteouts"); } else { println!("No embedded whiteouts found"); } if n_devices > 0 { println!("Ignored {n_devices} device files"); } else { println!("No device files found"); } Ok(()) } /// Write an RPM macro file to ensure the rpmdb path is set on the client side. #[context("Writing _dbpath RPM macro")] fn compose_postprocess_rpm_macro(rootfs_dfd: &Dir) -> Result<()> { static RPM_MACROS_DIR: &str = "usr/lib/rpm/macros.d"; static MACRO_FILENAME: &str = "macros.rpm-ostree"; let mut db = cap_std::fs::DirBuilder::new(); db.recursive(true); db.mode(0o755); rootfs_dfd.create_dir_with(RPM_MACROS_DIR, &db)?; let rpm_macros_dfd = rootfs_dfd.open_dir(RPM_MACROS_DIR)?; let perms = cap_std::fs::Permissions::from_mode(0o644); rpm_macros_dfd.atomic_replace_with(MACRO_FILENAME, |w| -> Result<()> { w.get_mut().as_file_mut().set_permissions(perms)?; w.write_all(b"%_dbpath /")?; w.write_all(RPMOSTREE_RPMDB_LOCATION.as_bytes())?; w.write_all(b"\n")?; Ok(()) })?; Ok(()) } // This function does two things: (1) make sure there is a /home --> /var/home substitution rule, // and (2) make sure there *isn't* a /var/home -> /home substition rule. The latter check won't // technically be needed once downstreams have: // https://src.fedoraproject.org/rpms/selinux-policy/pull-request/14 #[context("Postprocessing subs_dist")] fn postprocess_subs_dist(rootfs_dfd: &Dir) -> Result<()> { let sepolicy = ostree::SePolicy::new_at(rootfs_dfd.as_raw_fd(), gio::Cancellable::NONE)?; if let Some(name) = sepolicy.name() { let path = format!("usr/etc/selinux/{name}/contexts/files/file_contexts.subs_dist"); if let Some(f) = rootfs_dfd.open_optional(Path::new(&path))? { let perms = cap_std::fs::Permissions::from_mode(0o644); rootfs_dfd.atomic_replace_with(path, |w| -> Result<()> { w.get_mut().as_file_mut().set_permissions(perms)?; let f = BufReader::new(&f); let mut has_usr_etc = false; let mut etc_aliases = Vec::new(); for line in f.lines() { let line = line?; if line.starts_with("/var/home ") { writeln!(w, "# https://github.com/projectatomic/rpm-ostree/pull/1754")?; write!(w, "# ")?; } if line.starts_with("/usr/etc ") { has_usr_etc = true } if line.starts_with("/etc/") { etc_aliases.push(line.clone()) } writeln!(w, "{}", line)?; } writeln!(w, "# https://github.com/projectatomic/rpm-ostree/pull/1754")?; writeln!(w, "/home /var/home")?; if !has_usr_etc { writeln!(w, "# https://github.com/coreos/rpm-ostree/pull/4640")?; writeln!(w, "/usr/etc /etc")?; } if etc_aliases.len() > 0 { writeln!(w, "# https://github.com/coreos/rpm-ostree/pull/5485")?; for line in etc_aliases { writeln!(w, "/usr{}", line)?; } } writeln!(w, "# https://github.com/coreos/rpm-ostree/pull/1795")?; writeln!(w, "/usr/lib/opt /opt")?; // The SELinux policy store is moved from /var/lib/selinux to // /etc/selinux (see postprocess_selinux_policy_store_location), // and a compatibility symlink is created via tmpfiles.d. Add an // equivalency rule so that label lookups through the old path // resolve correctly. // https://github.com/coreos/rpm-ostree/issues/5616 writeln!(w, "# https://github.com/coreos/rpm-ostree/issues/5616")?; writeln!(w, "/var/lib/selinux /etc/selinux")?; Ok(()) })?; } } Ok(()) } #[context("Cleaning up rpmdb leftovers")] fn postprocess_cleanup_rpmdb_impl(rootfs_dfd: &Dir) -> Result<()> { let d = if let Some(d) = rootfs_dfd.open_dir_optional(RPMOSTREE_RPMDB_LOCATION)? { Utf8Dir::from_cap_std(d) } else { return Ok(()); }; for ent in d.entries()? { let ent = ent?; let name = ent.file_name()?; let name = name.as_str(); if matches!(name, ".dbenv.lock" | ".rpm.lock") || name.starts_with("__db.") { d.remove_file(name)?; } } Ok(()) } pub(crate) fn postprocess_cleanup_rpmdb(rootfs_dfd: i32) -> CxxResult<()> { postprocess_cleanup_rpmdb_impl(unsafe { &ffi_dirfd(rootfs_dfd)? }).map_err(Into::into) } /// Final processing steps. /// /// This function is called from rpmostree_postprocess_final(); think of /// it as the bits of that function that we've chosen to implement in Rust. /// It takes care of all things that are really required to use rpm-ostree /// on the target host. pub fn compose_postprocess_final_pre(rootfs_dfd: i32, treefile: &Treefile) -> CxxResult<()> { let rootfs_dfd = unsafe { &crate::ffiutil::ffi_dirfd(rootfs_dfd)? }; // These tasks can safely run in parallel, so just for fun we do so via rayon. let tasks = [postprocess_useradd, postprocess_subs_dist]; tasks.par_iter().try_for_each(|f| f(rootfs_dfd))?; // This task recursively traverses the filesystem and hence should be serial. postprocess_devices(rootfs_dfd, treefile)?; Ok(()) } #[context("Handling treefile 'units'")] fn compose_postprocess_units(rootfs_dfd: &Dir, treefile: &mut Treefile) -> Result<()> { let mut db = cap_std::fs::DirBuilder::new(); db.recursive(true); db.mode(0o755); let units = if let Some(u) = treefile.parsed.base.units.as_ref() { u } else { return Ok(()); }; let multiuser_wants = Path::new("usr/etc/systemd/system/multi-user.target.wants"); // Sanity check if !rootfs_dfd.try_exists("usr/etc")? { return Err(anyhow!("Missing usr/etc in rootfs")); } rootfs_dfd.ensure_dir_with(multiuser_wants, &db)?; for unit in units { let dest = multiuser_wants.join(unit); if rootfs_dfd .symlink_metadata_optional(&dest) .with_context(|| format!("Querying {unit}"))? .is_some() { continue; } println!("Adding {} to multi-user.target.wants", unit); let target = format!("/usr/lib/systemd/system/{unit}"); cap_primitives::fs::symlink_contents(target, &rootfs_dfd.as_filelike_view(), dest) .with_context(|| format!("Linking {unit}"))?; } Ok(()) } #[context("Handling treefile 'default-target'")] fn compose_postprocess_default_target(rootfs: &Dir, target: &str) -> Result<()> { /* This used to be in /etc, but doing it in /usr makes more sense, as it's * part of the OS defaults. This was changed in particular to work with * ConditionFirstBoot= which runs `systemctl preset-all`: * https://github.com/projectatomic/rpm-ostree/pull/1425 */ let default_target_path = "usr/lib/systemd/system/default.target"; rootfs.remove_file_optional(default_target_path)?; rootfs.symlink(target, default_target_path)?; Ok(()) } pub(crate) enum PostprocessBwrap { None, Wrap { unified_core: bool }, } /// The treefile format has two kinds of postprocessing scripts; /// there's a single `postprocess-script` as well as inline (anonymous) /// scripts. This function executes both kinds in bwrap containers. pub(crate) fn compose_postprocess_scripts( rootfs_dfd: &Dir, treefile: &mut Treefile, bwrap: PostprocessBwrap, ) -> Result<()> { // Execute the anonymous (inline) scripts. for (i, script) in treefile .parsed .base .postprocess .iter() .flatten() .enumerate() { let binpath = format!("/usr/bin/rpmostree-postprocess-inline-{}", i); let target_binpath = &binpath[1..]; rootfs_dfd.atomic_write_with_perms( target_binpath, script, Permissions::from_mode(0o755), )?; println!("\n>>>> Executing `postprocess` inline script '{i}' <<<<\n"); let child_argv = vec![binpath.to_string()]; if let PostprocessBwrap::Wrap { unified_core } = bwrap { let _ = bwrap::bubblewrap_run_sync( rootfs_dfd.as_raw_fd(), &child_argv, false, BubblewrapMutability::for_unified_core(unified_core), ) .context("Executing inline postprocessing script")?; } else { let st = Command::new(&binpath).status()?; if !st.success() { anyhow::bail!("Executing inline postprocessing script: {st:?}"); } } rootfs_dfd.remove_file(target_binpath)?; } println!("Completed running `postprocess` inline scripts"); // And the single postprocess script. if let Some(postprocess_script) = treefile.get_postprocess_script() { let binpath = "/usr/bin/rpmostree-treefile-postprocess-script"; let target_binpath = &binpath[1..]; postprocess_script.seek(std::io::SeekFrom::Start(0))?; let mut reader = std::io::BufReader::new(postprocess_script); rootfs_dfd.atomic_replace_with(target_binpath, |w| { std::io::copy(&mut reader, w)?; w.get_mut() .as_file_mut() .set_permissions(Permissions::from_mode(0o755))?; Ok::<_, anyhow::Error>(()) })?; println!("Executing postprocessing script"); let child_argv = &vec![binpath.to_string()]; if let PostprocessBwrap::Wrap { unified_core } = bwrap { let _ = crate::bwrap::bubblewrap_run_sync( rootfs_dfd.as_raw_fd(), child_argv, false, BubblewrapMutability::for_unified_core(unified_core), ) .context("Executing postprocessing script")?; } else { let st = Command::new(&binpath).status()?; if !st.success() { anyhow::bail!("Executing postprocessing script: {st:?}"); } } rootfs_dfd.remove_file(target_binpath)?; println!("Finished postprocessing script"); } Ok(()) } /// Logic for handling treefile `remove-files`. #[context("Handling `remove-files`")] pub fn compose_postprocess_remove_files( rootfs_dfd: &Dir, treefile: &mut Treefile, ) -> CxxResult<()> { for name in treefile.parsed.base.remove_files.iter().flatten() { let p = Path::new(name); if p.is_absolute() { return Err(anyhow!("Invalid absolute path: {}", name).into()); } if name.contains("..") { return Err(anyhow!("Invalid '..' in path: {}", name).into()); } println!("Deleting: {}", name); rootfs_dfd.remove_all_optional(name)?; } Ok(()) } fn compose_postprocess_add_files(rootfs_dfd: &Dir, treefile: &mut Treefile) -> Result<()> { let mut db = cap_std::fs::DirBuilder::new(); db.recursive(true); db.mode(0o755); // Make a deep copy here because get_add_file_fd() also wants an &mut // reference. let add_files: Vec<_> = treefile .parsed .base .add_files .iter() .flatten() .cloned() .collect(); for (src, dest) in add_files { let reldest = dest.trim_start_matches('/'); if reldest.is_empty() { return Err(anyhow!("Invalid add-files destination: {}", dest)); } let dest = if reldest.starts_with("etc/") { Cow::Owned(format!("usr/{}", reldest)) } else { Cow::Borrowed(reldest) }; println!("Adding file {}", dest); let dest = Path::new(&*dest); if let Some(parent) = dest.parent() { rootfs_dfd.ensure_dir_with(parent, &db)?; } let fd = treefile.get_add_file(&src); fd.seek(std::io::SeekFrom::Start(0))?; let mut reader = std::io::BufReader::new(fd); let perms = reader.get_mut().metadata()?.permissions(); rootfs_dfd.atomic_replace_with(dest, |w| { std::io::copy(&mut reader, w)?; w.get_mut() .as_file_mut() .set_permissions(cap_std::fs::Permissions::from_std(perms))?; Ok::<_, anyhow::Error>(()) })?; } Ok(()) } #[context("Symlinking {}", TRADITIONAL_RPMDB_LOCATION)] fn compose_postprocess_rpmdb(rootfs_dfd: &Dir) -> Result<()> { /* This works around a potential issue with libsolv if we go down the * rpmostree_get_pkglist_for_root() path. Though rpm has been using the * /usr/share/rpm location (since the RpmOstreeContext set the _dbpath macro), * the /var/lib/rpm directory will still exist, but be empty. libsolv gets * confused because it sees the /var/lib/rpm dir and doesn't even try the * /usr/share/rpm location, and eventually dies when it tries to load the * data. XXX: should probably send a patch upstream to libsolv. * * So we set the symlink now. This is also what we do on boot anyway for * compatibility reasons using tmpfiles. * */ rootfs_dfd.remove_all_optional(TRADITIONAL_RPMDB_LOCATION)?; rootfs_dfd.symlink( format!("../../{}", RPMOSTREE_RPMDB_LOCATION), TRADITIONAL_RPMDB_LOCATION, )?; // And write a symlink from the standard /usr/lib/sysimage/rpm // to our /usr/share/rpm - eventually we will invert this. rootfs_dfd.remove_all_optional(RPMOSTREE_SYSIMAGE_RPMDB)?; rootfs_dfd.symlink("../../share/rpm", RPMOSTREE_SYSIMAGE_RPMDB)?; Ok(()) } /// Enables ostree-state-overlay@.service for /usr/lib/opt and /usr/local. These /// symlinks are also used later in the compose process (and client-side composes) /// as a way to check that state overlays are turned on. fn compose_postprocess_state_overlays(rootfs_dfd: &Dir) -> Result<()> { let mut db = cap_std::fs::DirBuilder::new(); db.recursive(true); db.mode(0o755); let localfs_requires = Path::new(SD_LOCAL_FS_TARGET_REQUIRES); rootfs_dfd.ensure_dir_with(localfs_requires, &db)?; const UNITS: &[&str] = &[ "ostree-state-overlay@usr-lib-opt.service", "ostree-state-overlay@usr-local.service", ]; UNITS.par_iter().try_for_each(|&unit| { let target = Path::new("..").join(unit); let linkpath = localfs_requires.join(unit); rootfs_dfd .symlink(target, linkpath) .with_context(|| format!("Enabling {unit}")) })?; Ok(()) } /// Rust portion of rpmostree_treefile_postprocessing() pub fn compose_postprocess( rootfs_dfd: i32, treefile: &mut Treefile, next_version: &str, unified_core: bool, ) -> CxxResult<()> { let rootfs = unsafe { &crate::ffiutil::ffi_dirfd(rootfs_dfd)? }; // One of several dances we do around this that really needs to be completely // reworked. if rootfs.try_exists("etc")? { rootfs.rename("etc", rootfs, "usr/etc")?; } compose_postprocess_rpmdb(rootfs)?; compose_postprocess_rpm_macro(rootfs)?; compose_postprocess_units(rootfs, treefile)?; if let Some(t) = treefile.parsed.base.default_target.as_deref() { compose_postprocess_default_target(rootfs, t)?; } if matches!( treefile.parsed.base.opt_usrlocal, Some(OptUsrLocal::StateOverlay) ) { compose_postprocess_state_overlays(rootfs)?; } treefile.write_compose_json(rootfs)?; let etc_guard = crate::core::prepare_tempetc_guard(rootfs_dfd.as_raw_fd())?; // These ones depend on the /etc path compose_postprocess_mutate_os_release(rootfs, treefile, next_version)?; compose_postprocess_remove_files(rootfs, treefile)?; compose_postprocess_add_files(rootfs, treefile)?; etc_guard.undo()?; compose_postprocess_scripts(rootfs, treefile, PostprocessBwrap::Wrap { unified_core })?; Ok(()) } /// Implementation of the treefile `mutate-os-release` field. #[context("Updating os-release with commit version")] fn compose_postprocess_mutate_os_release( rootfs: &Dir, treefile: &mut Treefile, next_version: &str, ) -> Result<()> { let base_version = if let Some(base_version) = treefile.parsed.base.mutate_os_release.as_deref() { base_version } else { return Ok(()); }; if next_version.is_empty() { println!("Ignoring mutate-os-release: no commit version specified."); return Ok(()); } // find the real path to os-release using bwrap; this is an overkill but safer way // of resolving a symlink relative to a rootfs (see discussions in // https://github.com/projectatomic/rpm-ostree/pull/410/) let mut bwrap = crate::bwrap::Bubblewrap::new_with_mutability( rootfs, crate::ffi::BubblewrapMutability::Immutable, )?; bwrap.append_child_argv(["realpath", "/etc/os-release"]); let cancellable = &gio::Cancellable::new(); let cancellable = Some(cancellable); let path = bwrap.run_captured(cancellable)?; let path = std::str::from_utf8(&path) .context("Parsing realpath")? .trim_start_matches('/') .trim_end(); let path = if path.is_empty() { // fallback on just overwriting etc/os-release "etc/os-release" } else { path }; println!("Updating {}", path); let contents = rootfs .read_to_string(path) .with_context(|| format!("Reading {path}"))?; let new_contents = mutate_os_release_contents(&contents, base_version, next_version); rootfs .atomic_write(path, new_contents.as_bytes()) .with_context(|| format!("Writing {path}"))?; Ok(()) } /// Given the contents of a /usr/lib/os-release file, /// update the `VERSION` and `PRETTY_NAME` fields. fn mutate_os_release_contents(contents: &str, base_version: &str, next_version: &str) -> String { let mut buf = String::new(); for line in contents.lines() { if line.is_empty() { continue; } let prefixes = &["VERSION=", "PRETTY_NAME="]; if let Some((prefix, rest)) = strip_any_prefix(line, prefixes) { buf.push_str(prefix); let replaced = rest.replace(base_version, next_version); buf.push_str(&replaced); } else { buf.push_str(line); } buf.push('\n'); } let quoted_version = glib::shell_quote(next_version); let quoted_version = quoted_version.to_str().unwrap(); // Unwrap safety: write! to a String can't fail writeln!(buf, "OSTREE_VERSION={}", quoted_version).unwrap(); buf } /// Given a string and a set of possible prefixes, return the split /// prefix and remaining string, or `None` if no matches. fn strip_any_prefix<'a, 'b>(s: &'a str, prefixes: &[&'b str]) -> Option<(&'b str, &'a str)> { prefixes .iter() .find_map(|&p| s.strip_prefix(p).map(|r| (p, r))) } /// Inject `altfiles` after `files` for `passwd:` and `group:` entries. fn add_altfiles(buf: &str) -> Result<String> { let mut r = String::with_capacity(buf.len()); for line in buf.lines() { let parts = if let Some(p) = strip_any_prefix(line, &["passwd:", "group:"]) { p } else { r.push_str(line); r.push('\n'); continue; }; let (prefix, rest) = parts; r.push_str(prefix); let mut inserted = false; for elt in rest.split_whitespace() { // Already have altfiles? We're done if elt == "altfiles" { return Ok(buf.to_string()); } // We prefer `files altfiles` if !inserted && elt == "files" { r.push_str(" files altfiles"); inserted = true; } else { r.push(' '); r.push_str(elt); } } if !inserted { r.push_str(" altfiles"); } r.push('\n'); } Ok(r) } /// Add `altfiles` entries to `nsswitch.conf`. /// /// rpm-ostree currently depends on `altfiles` #[context("Adding altfiles to /etc/nsswitch.conf")] pub fn composepost_nsswitch_altfiles(rootfs_dfd: i32) -> CxxResult<()> { let rootfs_dfd = unsafe { &crate::ffiutil::ffi_dirfd(rootfs_dfd)? }; let path = "usr/etc/nsswitch.conf"; if let Some(meta) = rootfs_dfd.symlink_metadata_optional(path)? { // If it's a symlink, then something else e.g. authselect must own it. if meta.is_symlink() { return Ok(()); } let nsswitch = rootfs_dfd.read_to_string(path)?; let nsswitch = add_altfiles(&nsswitch)?; rootfs_dfd.atomic_write(path, nsswitch.as_bytes())?; } Ok(()) } /// Go over `/var` in the rootfs and convert them to tmpfiles.d entries. Only directories and /// symlinks are handled. rpm-ostree itself creates some symlinks for various reasons. /// /// In the non-unified core path, conversion is necessary to ensure that (1) any subdirs/symlinks /// from the RPM itself and (2) any subdirs/symlinks from scriptlets will be created on first boot. /// In the unified core path, (1) is handled by the importer, and (2) is blocked by bwrap, so it's /// really just for rpm-ostree-created bits itself. /// /// In theory, once we drop non-unified core support, we should be able to drop this and make those /// few rpm-ostree compat symlinks just directly write tmpfiles.d dropins. pub fn convert_var_to_tmpfiles_d( rootfs_dfd: i32, cancellable: &crate::FFIGCancellable, ) -> CxxResult<()> { let rootfs = unsafe { crate::ffiutil::ffi_dirfd(rootfs_dfd)? }; let cancellable = &cancellable.glib_reborrow(); // TODO(lucab): unify this logic with the one in rpmostree-importer.cxx. crate::var_to_tmpfiles(&rootfs, Some(cancellable))?; Ok(()) } fn state_overlay_enabled(rootfs_dfd: &cap_std::fs::Dir, state_overlay: &str) -> Result<bool> { let linkname = format!("{SD_LOCAL_FS_TARGET_REQUIRES}/ostree-state-overlay@{state_overlay}.service"); match rootfs_dfd.symlink_metadata_optional(&linkname)? { Some(meta) if meta.is_symlink() => Ok(true), Some(_) => Err(anyhow!("{linkname} is not a symlink")), None => Ok(false), } } /// Walk over the root filesystem and perform some core conversions /// from RPM conventions to OSTree conventions. /// /// For example: /// - Symlink /usr/local -> /var/usrlocal /// - If present, symlink /var/lib/alternatives -> /usr/lib/alternatives /// - If present, symlink /var/lib/vagrant -> /usr/lib/vagrant #[context("Preparing symlinks in rootfs")] pub fn rootfs_prepare_links( rootfs_dfd: i32, treefile: &Treefile, skip_usrlocal: bool, ) -> CxxResult<()> { let rootfs = unsafe { &crate::ffiutil::ffi_dirfd(rootfs_dfd)? }; let mut db = dirbuilder_from_mode(0o755); db.recursive(true); if !skip_usrlocal { let usrlocal_root = matches!(treefile.parsed.base.opt_usrlocal, Some(OptUsrLocal::Root)); if usrlocal_root || state_overlay_enabled(rootfs, "usr-local")? { // because of the filesystem lua issue (see // compose_init_rootfs_base()) we need to create this manually rootfs.ensure_dir_with("usr/local", &db)?; } else if !crate::ostree_prepareroot::transient_root_enabled(rootfs)? { // Unconditionally drop /usr/local and replace it with a symlink. rootfs .remove_all_optional("usr/local") .context("Removing /usr/local")?; ensure_symlink(rootfs, "../var/usrlocal", "usr/local") .context("Creating /usr/local symlink")?; } } // Move existing content to /usr/lib, then put a symlink in its // place under /var/lib. rootfs .ensure_dir_with("usr/lib", &db) .context("Creating /usr/lib")?; for entry in COMPAT_VARLIB_SYMLINKS { let varlib_path = format!("var/lib/{}", entry); let is_var_dir = rootfs .symlink_metadata_optional(&varlib_path)? .map(|m| m.is_dir()) .unwrap_or(false); if !is_var_dir { continue; } let usrlib_path = format!("usr/lib/{}", entry); rootfs .remove_all_optional(&usrlib_path) .with_context(|| format!("Removing /{}", &usrlib_path))?; rootfs .rename(&varlib_path, rootfs, &usrlib_path) .with_context(|| format!("Moving /{} to /{}", &varlib_path, &usrlib_path))?; let target = format!("../../{}", &usrlib_path); ensure_symlink(rootfs, &target, &varlib_path) .with_context(|| format!("Creating /{} symlink", &varlib_path))?; } Ok(()) } /// Create a symlink at `linkpath` if it does not exist, pointing to `target`. /// /// This is idempotent and does not alter any content already existing at `linkpath`. /// It returns `true` if the symlink has been created, `false` otherwise. #[context("Symlinking '/{}' to empty directory '/{}'", linkpath, target)] fn ensure_symlink(rootfs: &Dir, target: &str, linkpath: &str) -> Result<bool> { let mut db = dirbuilder_from_mode(0o755); db.recursive(true); if let Some(meta) = rootfs.symlink_metadata_optional(linkpath)? { if meta.is_symlink() { // We assume linkpath already points to the correct target, // thus this short-circuits in an idempotent way. return Ok(false); } else if meta.is_dir() { rootfs.remove_dir(linkpath)?; } else { bail!("Content already exists at link path"); } } else { // For maximum compatibility, create parent directories too. This // is necessary when we're doing layering on top of a base commit, // and the /var will be empty. We should probably consider running // systemd-tmpfiles to setup the temporary /var. if let Some(parent) = Path::new(linkpath).parent() { rootfs.ensure_dir_with(parent, &db)?; } } rootfs.symlink(target, linkpath)?; Ok(true) } pub fn workaround_selinux_cross_labeling( rootfs_dfd: i32, cancellable: Pin<&mut crate::FFIGCancellable>, ) -> CxxResult<()> { let rootfs = unsafe { &crate::ffiutil::ffi_dirfd(rootfs_dfd)? }; let cancellable = &cancellable.gobj_wrap(); tweak_selinux_timestamps(rootfs, Some(cancellable))?; Ok(()) } /// Tweak timestamps on SELinux policy (workaround cross-host leak). /// /// SELinux uses PCRE pre-compiled regexps for binary caches, which can /// fail if the version of PCRE on the host differs from the version /// which generated the cache (in the target root). /// /// Note also this function is probably already broken in Fedora /// 23+ from https://bugzilla.redhat.com/show_bug.cgi?id=1265406 #[context("Tweaking SELinux timestamps")] fn tweak_selinux_timestamps(rootfs: &Dir, cancellable: Option<&gio::Cancellable>) -> Result<()> { // Handle the policy being in both /usr/etc and /etc since // this function can be called at different points. let policy_path = if rootfs.try_exists("usr/etc")? { "usr/etc/selinux" } else { "etc/selinux" }; if rootfs.try_exists(policy_path)? { let mut prefix = policy_path.to_string(); workaround_selinux_cross_labeling_recurse(rootfs, &mut prefix, &cancellable) .with_context(|| format!("Analyzing /{} content", prefix))?; } Ok(()) } /// Recursively explore target directory and tweak SELinux policy timestamps. /// /// `prefix` is updated at each recursive step, so that in case of errors it can be /// used to pinpoint the faulty path. fn workaround_selinux_cross_labeling_recurse( rootfs: &Dir, prefix: &mut String, cancellable: &Option<&gio::Cancellable>, ) -> Result<()> { let current_prefix = prefix.clone(); for subpath in rootfs.read_dir(¤t_prefix)? { if let Some(c) = cancellable { c.set_error_if_cancelled()?; } let subpath = subpath?; let full_path = { let fname = subpath.file_name(); let path_name = fname .to_str() .ok_or_else(|| anyhow!("invalid non-UTF-8 path: {:?}", fname))?; format!("{}/{}", ¤t_prefix, &path_name) }; if subpath.file_type()?.is_dir() { // New subdirectory discovered, recurse into it. *prefix = full_path.clone(); workaround_selinux_cross_labeling_recurse(rootfs, prefix, cancellable)?; } else if let Some(nonbin_name) = full_path.strip_suffix(".bin") { rootfs .update_timestamps(nonbin_name) .with_context(|| format!("Updating timestamps of /{}", nonbin_name))?; } } Ok(()) } /// This is the nearly the last code executed before we run `ostree commit`. pub fn compose_postprocess_final(rootfs_dfd: i32, treefile: &Treefile) -> CxxResult<()> { let rootfs = unsafe { &crate::ffiutil::ffi_dirfd(rootfs_dfd)? }; hardlink_rpmdb_base_location(rootfs, None)?; treefile.exec_finalize_d(rootfs)?; Ok(()) } /// Recurse into this directory and return the total size of all regular files. #[context("Computing directory size")] pub fn directory_size(dfd: i32, cancellable: &crate::FFIGCancellable) -> CxxResult<u64> { let cancellable = cancellable.glib_reborrow(); let dfd = unsafe { &crate::ffiutil::ffi_dirfd(dfd)? }; fn directory_size_recurse(d: &Dir, cancellable: &gio::Cancellable) -> Result<u64> { let mut r = 0; for ent in d.entries()? { cancellable.set_error_if_cancelled()?; let ent = ent?; let meta = ent .metadata() .with_context(|| format!("Failed to access {:?}", ent.file_name()))?; if meta.is_dir() { let child = d.open_dir(ent.file_name())?; r += directory_size_recurse(&child, cancellable)?; } else if meta.is_file() { r += meta.size(); } } Ok(r) } Ok(directory_size_recurse(dfd, &cancellable)?) } #[context("Hardlinking rpmdb to base location")] fn hardlink_rpmdb_base_location( rootfs: &Dir, cancellable: Option<&gio::Cancellable>, ) -> Result<bool> { if !rootfs.try_exists(RPMOSTREE_RPMDB_LOCATION)? { return Ok(false); } // Hardlink our own `/usr/lib/sysimage/rpm-ostree-base-db/` hierarchy // to the well-known `/usr/share/rpm/`. let mut db = dirbuilder_from_mode(0o755); db.recursive(true); rootfs.ensure_dir_with(RPMOSTREE_BASE_RPMDB, &db)?; let perms = Permissions::from_mode(0o755); rootfs.set_permissions(RPMOSTREE_BASE_RPMDB, perms)?; hardlink_hierarchy( rootfs, RPMOSTREE_RPMDB_LOCATION, RPMOSTREE_BASE_RPMDB, cancellable, )?; Ok(true) } #[context("Rewriting rpmdb for target native format")] fn rewrite_rpmdb_for_target_inner(rootfs_dfd: &Dir, normalize: bool) -> Result<()> { let tempetc = crate::core::prepare_tempetc_guard(rootfs_dfd.as_raw_fd())?; let mut dbfd = cap_std_ext::cap_tempfile::TempFile::new_anonymous(rootfs_dfd)?; let dbpath_arg = format!("--dbpath=/proc/self/cwd/{}", RPMOSTREE_RPMDB_LOCATION); // Fork rpmdb from the *host* rootfs to read the rpmdb back into memory std::process::Command::new("rpmdb") .args([dbpath_arg.as_str(), "--exportdb"]) .current_dir(format!("/proc/self/fd/{}", rootfs_dfd.as_raw_fd())) .stdout(Stdio::from(dbfd.try_clone()?)) .run()?; // Clear out the db on disk rootfs_dfd.remove_all_optional(RPMOSTREE_RPMDB_LOCATION)?; let db = dirbuilder_from_mode(0o755); rootfs_dfd.create_dir_with(RPMOSTREE_RPMDB_LOCATION, &db)?; // Only one owner now dbfd.seek(std::io::SeekFrom::Start(0))?; // In the interests of build stability, rewrite the INSTALLTIME and INSTALLTID tags // to be deterministic and dervied from `SOURCE_DATE_EPOCH` if requested. if normalize { normalization::rewrite_rpmdb_timestamps(&mut dbfd)?; } // Fork the target rpmdb to write the content from memory to disk let mut bwrap = Bubblewrap::new_with_mutability(rootfs_dfd, BubblewrapMutability::RoFiles)?; bwrap.append_child_argv(["rpmdb", dbpath_arg.as_str(), "--importdb"]); bwrap.take_stdin_fd(dbfd.into_raw_fd()); let cancellable = gio::Cancellable::new(); bwrap .run(cancellable.reborrow_cxx()) .context("Failed to run rpmdb --importdb")?; // Sometimes we can end up with build-to-build variance in the underlying rpmdb // files. Attempt to sort that out, if requested. if normalize { normalization::normalize_rpmdb(rootfs_dfd, RPMOSTREE_RPMDB_LOCATION)?; } tempetc.undo()?; Ok(()) } pub(crate) fn rewrite_rpmdb_for_target(rootfs_dfd: i32, normalize: bool) -> CxxResult<()> { Ok(rewrite_rpmdb_for_target_inner( unsafe { &crate::ffiutil::ffi_dirfd(rootfs_dfd)? }, normalize, )?) } /// Recursively hard-link `source` hierarchy to `target` directory. /// /// Both directories must exist beforehand. #[context("Hardlinking /{} to /{}", source, target)] fn hardlink_hierarchy( rootfs: &Dir, source: &str, target: &str, cancellable: Option<&gio::Cancellable>, ) -> Result<()> { let mut prefix = "".to_string(); hardlink_recurse(rootfs, source, target, &mut prefix, &cancellable) .with_context(|| format!("Analyzing /{}/{} content", source, prefix))?; Ok(()) } /// Recursively hard-link `source_prefix` to `dest_prefix.` /// /// `relative_path` is updated at each recursive step, so that in case of errors /// it can be used to pinpoint the faulty path. fn hardlink_recurse( rootfs: &Dir, source_prefix: &str, dest_prefix: &str, relative_path: &mut String, cancellable: &Option<&gio::Cancellable>, ) -> Result<()> { let current_dir = relative_path.clone(); let current_source_dir = format!("{}/{}", source_prefix, relative_path); for subpath in rootfs.read_dir(¤t_source_dir)? { if let Some(c) = cancellable { c.set_error_if_cancelled()?; } let subpath = subpath?; let full_path = { let fname = subpath.file_name(); let path_name = fname .to_str() .ok_or_else(|| anyhow!("invalid non-UTF-8 path: {:?}", fname))?; if !current_dir.is_empty() { format!("{}/{}", current_dir, path_name) } else { path_name.to_string() } }; let source_path = format!("{}/{}", source_prefix, full_path); let dest_path = format!("{}/{}", dest_prefix, full_path); if subpath.file_type()?.is_dir() { // New subdirectory discovered, create it at the target. let perms = rootfs.metadata(&source_path)?.mode() & !libc::S_IFMT; let db = dirbuilder_from_mode(perms); rootfs.ensure_dir_with(&dest_path, &db)?; let perms = Permissions::from_mode(perms); rootfs.set_permissions(&dest_path, perms)?; // Recurse into the subdirectory. *relative_path = full_path.clone(); hardlink_recurse( rootfs, source_prefix, dest_prefix, relative_path, cancellable, )?; } else { rustix::fs::linkat( rootfs, source_path, rootfs, dest_path, rustix::fs::AtFlags::empty(), )?; } } Ok(()) } #[cfg(test)] mod tests { use super::*; use cap_std::fs::{Dir, DirBuilder}; use cap_std_ext::{cap_std, cap_tempfile}; #[test] fn stripany() { let s = "foo: bar"; assert!(strip_any_prefix(s, &[]).is_none()); assert_eq!( strip_any_prefix(s, &["baz:", "foo:", "bar:"]).unwrap(), ("foo:", " bar") ); } #[test] fn altfiles_replaced() { let orig = r##"# blah blah nss stuff # more blah blah # passwd: db files # shadow: db files # shadow: db files passwd: sss files systemd shadow: files group: sss files systemd hosts: files resolve [!UNAVAIL=return] myhostname dns automount: files sss "##; let expected = r##"# blah blah nss stuff # more blah blah # passwd: db files # shadow: db files # shadow: db files passwd: sss files altfiles systemd shadow: files group: sss files altfiles systemd hosts: files resolve [!UNAVAIL=return] myhostname dns automount: files sss "##; let replaced = add_altfiles(orig).unwrap(); assert_eq!(replaced.as_str(), expected); let replaced2 = add_altfiles(replaced.as_str()).unwrap(); assert_eq!(replaced2.as_str(), expected); } #[test] fn test_mutate_os_release() { let orig = r##"NAME=Fedora VERSION="33 (Container Image)" ID=fedora VERSION_ID=33 VERSION_CODENAME="" PRETTY_NAME="Fedora 33 (Container Image)" CPE_NAME="cpe:/o:fedoraproject:fedora:33" "##; let expected = r##"NAME=Fedora VERSION="33.4 (Container Image)" ID=fedora VERSION_ID=33 VERSION_CODENAME="" PRETTY_NAME="Fedora 33.4 (Container Image)" CPE_NAME="cpe:/o:fedoraproject:fedora:33" OSTREE_VERSION='33.4' "##; let replaced = mutate_os_release_contents(orig, "33", "33.4"); assert_eq!(replaced.as_str(), expected); } fn verify_base(rootfs: &Dir) -> Result<()> { // Not exhaustive, just a sanity check for d in ["proc", "sys"] { assert!(rootfs.symlink_metadata(d)?.is_dir()); } let homelink = rootfs.read_link("home")?; assert_eq!(homelink.to_str().unwrap(), "var/home"); Ok(()) } #[test] fn test_init_rootfs_strict() -> Result<()> { // Test the legacy tmp_is_dir path { let rootfs = cap_tempfile::tempdir(cap_tempfile::ambient_authority())?; compose_init_rootfs_base(&rootfs, false)?; let target = rootfs.read_link("tmp").unwrap(); assert_eq!(target, Path::new("sysroot/tmp")); verify_base(&rootfs)?; } // Default expected strict mode let rootfs = cap_tempfile::tempdir(cap_tempfile::ambient_authority())?; compose_init_rootfs_base(&rootfs, true)?; let tmpdir_meta = rootfs.metadata("tmp").unwrap(); assert!(tmpdir_meta.is_dir()); assert_eq!(tmpdir_meta.permissions().mode() & 0o7777, 0o1777); verify_base(&rootfs)?; Ok(()) } #[test] fn test_init_rootfs_transient() -> Result<()> { let rootfs = cap_tempfile::tempdir(cap_tempfile::ambient_authority())?; compose_init_rootfs_transient(&rootfs)?; let tmpdir_meta = rootfs.metadata("tmp").unwrap(); assert!(tmpdir_meta.is_dir()); assert_eq!(tmpdir_meta.permissions().mode() & 0o7777, 0o1777); verify_base(&rootfs)?; for d in ["opt", "usr/local"] { assert!( rootfs .symlink_metadata(d) .with_context(|| format!("Verifying {d} is dir"))? .is_dir(), "Verifying {d} is dir" ); } Ok(()) } #[test] fn test_overlay() -> Result<()> { // We don't actually test creating whiteout devices here as that // may not work. let td = cap_tempfile::tempdir(cap_std::ambient_authority())?; let tf = crate::treefile::tests::new_test_tf_basic("")?; // Verify no-op case postprocess_devices(&td, &tf).unwrap(); td.create("foo")?; td.symlink("foo", "bar")?; postprocess_devices(&td, &tf).unwrap(); assert!(td.try_exists("foo")?); assert!(td.try_exists("bar")?); Ok(()) } #[test] fn test_prepare_symlinks() -> Result<()> { let tempdir = tempfile::tempdir()?; let tempdir: &Utf8Path = tempdir.path().try_into().unwrap(); let tf = crate::treefile::tests::new_test_treefile(tempdir, "", None)?; let rootfs = Dir::open_ambient_dir(tempdir, cap_std::ambient_authority())?; let mut db = DirBuilder::new(); db.recursive(true); db.mode(0o755); rootfs.ensure_dir_with("usr/local", &db).unwrap(); rootfs.ensure_dir_with("var/lib/alternatives", &db).unwrap(); rootfs.ensure_dir_with("var/lib/vagrant", &db).unwrap(); rootfs_prepare_links(rootfs.as_raw_fd(), &tf, false).unwrap(); { let usr_dir = rootfs.open_dir("usr").unwrap(); let local_target = usr_dir.read_link("local").unwrap(); assert_eq!(local_target.to_str(), Some("../var/usrlocal")); } { let varlib_dir = rootfs.open_dir("var/lib").unwrap(); let varcases = &[ ("alternatives", "../../usr/lib/alternatives"), ("vagrant", "../../usr/lib/vagrant"), ]; for (linkpath, content) in varcases { let target = varlib_dir.read_link(*linkpath); assert!(target.is_ok(), "/var/lib/{}", linkpath); assert_eq!(target.unwrap().to_str(), Some(*content)); } } Ok(()) } #[test] fn test_tweak_selinux_timestamps() { static PREFIX: &str = "usr/etc/selinux/targeted/contexts/files"; let rootfs = &cap_tempfile::tempdir(cap_std::ambient_authority()).unwrap(); let mut db = dirbuilder_from_mode(0o755); db.recursive(true); rootfs .ensure_dir_with("usr/etc/selinux/targeted/contexts/files", &db) .unwrap(); let files = &["file_contexts", "file_contexts.homedirs"]; let perms = cap_std::fs::Permissions::from_mode(0o755); let mut metas = vec![]; for fname in files { let binpath = format!("{PREFIX}/{fname}.bin"); rootfs .atomic_write_with_perms(&binpath, "", perms.clone()) .unwrap(); let fpath = format!("{PREFIX}/{fname}"); rootfs .atomic_write_with_perms(&fpath, "", perms.clone()) .unwrap(); let before_meta = rootfs.symlink_metadata(fpath).unwrap(); metas.push(before_meta); } // File timestamps may not increment faster than e.g. 10ms. Really // what we should be doing is using inode versions. std::thread::sleep(std::time::Duration::from_millis(100)); tweak_selinux_timestamps(&rootfs, gio::Cancellable::NONE).unwrap(); for (fname, before_meta) in files.iter().zip(metas.iter()) { let fpath = format!("{}/{}", PREFIX, fname); let after = rootfs.symlink_metadata(&fpath).unwrap(); if before_meta.mtime() == after.mtime() { assert_ne!(before_meta.mtime_nsec(), after.mtime_nsec()); } } } #[test] fn test_hardlink_rpmdb_base_location() { let rootfs = &cap_tempfile::tempdir(cap_std::ambient_authority()).unwrap(); { let done = hardlink_rpmdb_base_location(&rootfs, gio::Cancellable::NONE).unwrap(); assert_eq!(done, false); } let dirs = &[RPMOSTREE_RPMDB_LOCATION, "usr/share/rpm/foo/bar"]; let mut db = dirbuilder_from_mode(0o755); db.recursive(true); for entry in dirs { rootfs.ensure_dir_with(*entry, &db).unwrap(); } let files = &[ "usr/share/rpm/rpmdb.sqlite", "usr/share/rpm/foo/bar/placeholder", ]; for entry in files { rootfs.create(*entry).unwrap(); } let done = hardlink_rpmdb_base_location(&rootfs, gio::Cancellable::NONE).unwrap(); assert_eq!(done, true); assert_eq!(rootfs.try_exists(RPMOSTREE_BASE_RPMDB).unwrap(), true); let placeholder = rootfs .metadata(format!("{}/foo/bar/placeholder", RPMOSTREE_BASE_RPMDB)) .unwrap(); assert_eq!(placeholder.is_file(), true); let rpmdb = rootfs .metadata(format!("{}/rpmdb.sqlite", RPMOSTREE_BASE_RPMDB)) .unwrap(); assert_eq!(rpmdb.is_file(), true); } #[test] fn test_compose_postprocess_rpm_macro() { static MACRO_PATH: &str = "usr/lib/rpm/macros.d/macros.rpm-ostree"; let expected_content = format!("%_dbpath /{}\n", RPMOSTREE_RPMDB_LOCATION); let rootfs = cap_tempfile::tempdir(cap_std::ambient_authority()).unwrap(); { compose_postprocess_rpm_macro(&rootfs).unwrap(); assert_eq!(rootfs.exists(MACRO_PATH), true); let macrofile = rootfs.metadata(MACRO_PATH).unwrap(); assert_eq!(macrofile.is_file(), true); assert_eq!(macrofile.permissions().mode() & 0o777, 0o644); let content = rootfs.read_to_string(MACRO_PATH).unwrap(); assert_eq!(content, expected_content); } { // Re-run, check basic idempotency. compose_postprocess_rpm_macro(&rootfs).unwrap(); let content = rootfs.read_to_string(MACRO_PATH).unwrap(); assert_eq!(content, expected_content); } } }