From d8322174756a260fe13402affd2d47da2715ae1b Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Wed, 19 Jul 2023 17:34:48 -0400 Subject: [PATCH] repair: New functionality to detect (future: fix) inodes Initial code to detect the situation resulting from https://github.com/ostreedev/ostree/pull/2874/commits/de6fddc6adee09a93901243dc7074090828a1912 --- lib/src/cli.rs | 21 ++++++ lib/src/container/store.rs | 2 +- lib/src/lib.rs | 1 + lib/src/repair.rs | 138 +++++++++++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 lib/src/repair.rs diff --git a/lib/src/cli.rs b/lib/src/cli.rs index 62a56cad..a26657db 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -75,6 +75,19 @@ pub(crate) enum TarOpts { Export(ExportOpts), } +/// Check for consistenty of deployments and container image merge commits +/// and attempt auto-repair. Not yet officially stable API. +#[derive(Debug, Parser)] +pub(crate) struct ProvisionalRepairOpts { + /// Path to the system root + #[clap(long)] + sysroot: Utf8PathBuf, + + /// Do not perform any mutation + #[clap(long)] + dry_run: bool, +} + /// Options for container import/export. #[derive(Debug, Subcommand)] pub(crate) enum ContainerOpts { @@ -410,6 +423,8 @@ pub(crate) enum Opt { #[clap(hide(true))] #[cfg(feature = "docgen")] Man(ManOpts), + #[clap(hide = true)] + ProvisionalRepair(ProvisionalRepairOpts), } #[allow(clippy::from_over_into)] @@ -978,5 +993,11 @@ where Opt::InternalOnlyForTesting(ref opts) => testing(opts).await, #[cfg(feature = "docgen")] Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory), + Opt::ProvisionalRepair(ref opts) => { + let sysroot = &ostree::Sysroot::new(Some(&gio::File::for_path(&opts.sysroot))); + sysroot.load(gio::Cancellable::NONE)?; + let sysroot = &SysrootLock::new_from_sysroot(sysroot).await?; + crate::repair::repair(&sysroot, opts.dry_run) + } } } diff --git a/lib/src/container/store.rs b/lib/src/container/store.rs index 3b72c29a..e57d2881 100644 --- a/lib/src/container/store.rs +++ b/lib/src/container/store.rs @@ -37,7 +37,7 @@ const IMAGE_PREFIX: &str = "ostree/container/image"; pub const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage"; /// The key injected into the merge commit for the manifest digest. -const META_MANIFEST_DIGEST: &str = "ostree.manifest-digest"; +pub(crate) const META_MANIFEST_DIGEST: &str = "ostree.manifest-digest"; /// The key injected into the merge commit with the manifest serialized as JSON. const META_MANIFEST: &str = "ostree.manifest"; /// The key injected into the merge commit with the image configuration serialized as JSON. diff --git a/lib/src/lib.rs b/lib/src/lib.rs index c9a424b3..0338bf5d 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -39,6 +39,7 @@ pub mod ima; pub mod keyfileext; pub(crate) mod logging; pub mod refescape; +pub(crate) mod repair; pub mod sysroot; pub mod tar; pub mod tokio_util; diff --git a/lib/src/repair.rs b/lib/src/repair.rs new file mode 100644 index 00000000..e3741460 --- /dev/null +++ b/lib/src/repair.rs @@ -0,0 +1,138 @@ +//! System repair functionality + +use std::collections::{BTreeMap, BTreeSet}; + +use anyhow::{anyhow, Context, Result}; +use cap_std::fs::Dir; +use cap_tempfile::cap_std; +use fn_error_context::context; +use ostree::{gio, glib}; +use std::os::unix::fs::MetadataExt; + +use crate::sysroot::SysrootLock; + +// Find the inode numbers for objects +fn gather_inodes( + prefix: &str, + dir: &Dir, + little_inodes: &mut BTreeMap, + big_inodes: &mut BTreeMap, +) -> Result<()> { + for child in dir.entries()? { + let child = child?; + let metadata = child.metadata()?; + if !(metadata.is_file() || metadata.is_symlink()) { + continue; + } + let name = child.file_name(); + let name = name + .to_str() + .ok_or_else(|| anyhow::anyhow!("Invalid {name:?}"))?; + let object_rest = name + .split_once('.') + .ok_or_else(|| anyhow!("Invalid object {name}"))? + .0; + let checksum = format!("{prefix}{object_rest}"); + let inode = metadata.ino(); + if let Some(little) = u32::try_from(inode).ok() { + little_inodes.insert(little, checksum); + } else { + big_inodes.insert(inode, checksum); + } + } + Ok(()) +} + +#[context("Analyzing commit for derivation")] +fn commit_is_derived(commit: &glib::Variant) -> Result { + let commit_meta = &glib::VariantDict::new(Some(&commit.child_value(0))); + if commit_meta + .lookup::(crate::container::store::META_MANIFEST_DIGEST)? + .is_some() + { + return Ok(true); + } + if commit_meta + .lookup::("rpmostree.clientlayer")? + .is_some() + { + return Ok(true); + } + Ok(false) +} + +#[context("Repairing inodes")] +pub(crate) fn repair(sysroot: &SysrootLock, dry_run: bool) -> Result<()> { + let repo = &sysroot.repo(); + let repo_dir = repo.dfd_as_dir()?; + let objects = repo_dir.open_dir("objects")?; + + println!("Attempting analysis of ostree state for files that may be incorrectly linked"); + println!("For more information, see https://github.com/ostreedev/ostree/pull/2874/commits/de6fddc6adee09a93901243dc7074090828a1912"); + println!(); + println!("Gathering inodes..."); + let mut little_inodes = BTreeMap::new(); + let mut big_inodes = BTreeMap::new(); + + for child in objects.entries()? { + let child = child?; + if !child.file_type()?.is_dir() { + continue; + } + let name = child.file_name(); + if name.len() != 2 { + continue; + } + let name = name + .to_str() + .ok_or_else(|| anyhow::anyhow!("Invalid {name:?}"))?; + let objdir = child.open_dir()?; + gather_inodes(name, &objdir, &mut little_inodes, &mut big_inodes) + .with_context(|| format!("Processing {name:?}"))?; + } + + let mut colliding_inodes = BTreeMap::new(); + for (big_inum, big_inum_checksum) in big_inodes { + let truncated = big_inum as u32; + if let Some(small_inum_object) = little_inodes.get(&truncated) { + println!( + r#"collision: + inode (>32 bit): {big_inum} + object: {big_inum_checksum} + inode (truncated): {truncated} + object: {small_inum_object} +"# + ); + colliding_inodes.insert(big_inum, big_inum_checksum); + } + } + + if !colliding_inodes.is_empty() { + let l = colliding_inodes.len(); + eprintln!("warning: Found {l} potentially colliding objects"); + } else { + println!("No colliding objects found."); + return Ok(()); + } + + println!("Analyzing deployments..."); + + let mut potentially_corrupted_commits = BTreeSet::new(); + for (_refname, digest) in repo.list_refs(None, gio::Cancellable::NONE)? { + let commit = repo.load_commit(&digest)?.0; + if commit_is_derived(&commit)? { + eprintln!("Found potentially corrupted derived commit: {commit}"); + potentially_corrupted_commits.insert(digest); + } + } + + if potentially_corrupted_commits.is_empty() { + println!("No derived commits found."); + } + + if !dry_run { + anyhow::bail!("Repair mode not implemented yet"); + } + + Ok(()) +}