use std::fs::File; use std::io; use std::path::{Path, PathBuf}; use rustix::fs::{FileType, Mode, OFlags, fstat}; use rustix::mount::{ MountFlags, MoveMountFlags, OpenTreeFlags, UnmountFlags, mount_remount, move_mount, open_tree, unmount, }; use crate::backing::BackingRoot; #[derive(Clone, Debug)] pub struct GitSentryConfiguration { pub mounted_workspace: PathBuf, pub host_git_common_directory: PathBuf, pub transient_hooks: PathBuf, pub transient_config: PathBuf, } pub struct GitSentry { config: InstalledMount, hooks: InstalledMount, } struct InstalledMount { mounted_target: File, cleanup_target: File, cleanup_label: PathBuf, } impl InstalledMount { fn unmount(self) -> io::Result<()> { descriptor_move(&self.mounted_target, &self.cleanup_target)?; unmount(self.cleanup_label, UnmountFlags::DETACH).map_err(Into::into) } } impl GitSentry { /// # Errors /// Refuses linked or missing sources and targets and incomplete protective mounts. pub fn install(configuration: &GitSentryConfiguration) -> io::Result { let workspace = BackingRoot::open(&configuration.mounted_workspace)?; let common = BackingRoot::open(&configuration.host_git_common_directory)?; let transient_hooks = BackingRoot::open(&configuration.transient_hooks)?; let transient_config = File::options() .read(true) .write(true) .open(&configuration.transient_config)?; Self::install_from_roots( &workspace, &common, &transient_hooks, configuration.transient_hooks.clone(), transient_config, configuration.transient_config.clone(), ) } /// # Errors /// Refuses aliased, linked, or incomplete authorities and incomplete protective mounts. pub fn install_from_authorities( mounted_workspace: File, host_git_common_directory: File, transient_hooks: File, transient_hooks_label: PathBuf, transient_config: File, transient_config_label: PathBuf, ) -> io::Result { let workspace = BackingRoot::from_file(mounted_workspace)?; let common = BackingRoot::from_file(host_git_common_directory)?; let transient_hooks = BackingRoot::from_file(transient_hooks)?; Self::install_from_roots( &workspace, &common, &transient_hooks, transient_hooks_label, transient_config, transient_config_label, ) } fn install_from_roots( _workspace: &BackingRoot, common: &BackingRoot, transient_hooks: &BackingRoot, transient_hooks_label: PathBuf, transient_config: File, transient_config_label: PathBuf, ) -> io::Result { require_empty_regular_file(&transient_config) .map_err(|error| sentry_error("validating transient config", &error))?; let config_source = common .open_entry(Path::new("config"), OFlags::RDONLY, Mode::empty()) .map_err(|error| sentry_error("opening host config", &error))?; require_regular_file(&config_source) .map_err(|error| sentry_error("validating host config", &error))?; let staged_config = File::from( descriptor_bind(&config_source, &transient_config) .map_err(|error| sentry_error("staging config mount", &error))?, ); if let Err(error) = descriptor_remount_read_only(&transient_config_label) .map_err(|error| sentry_error("making config mount read-only", &error)) { return Err(cleanup_staged(error, &transient_config_label)); } let config_target = match common.open_entry(Path::new("config"), OFlags::RDONLY, Mode::empty()) { Ok(config_target) => config_target, Err(error) => { return Err(cleanup_staged( sentry_error("opening config mount target", &error), &transient_config_label, )); } }; if let Err(error) = descriptor_move(&staged_config, &config_target) .map_err(|error| sentry_error("installing config mount", &error)) { return Err(cleanup_staged(error, &transient_config_label)); } let config = InstalledMount { mounted_target: staged_config, cleanup_target: transient_config, cleanup_label: transient_config_label, }; let hook_source = match transient_hooks.duplicate() { Ok(hook_source) => hook_source, Err(error) => { return Err(cleanup_installed( sentry_error("retaining transient hooks", &error), config, )); } }; let hook_target = match common.open_entry( Path::new("hooks"), OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), ) { Ok(hook_target) => hook_target, Err(error) => { return Err(cleanup_installed( sentry_error("opening hooks mount target", &error), config, )); } }; let mounted_hooks = match descriptor_bind(&hook_source, &hook_target) .map_err(|error| sentry_error("installing hooks mount", &error)) { Ok(mounted_hooks) => File::from(mounted_hooks), Err(error) => { return Err(cleanup_installed(error, config)); } }; Ok(Self { config, hooks: InstalledMount { mounted_target: mounted_hooks, cleanup_target: hook_source, cleanup_label: transient_hooks_label, }, }) } /// # Errors /// Returns an error unless both exact protective mountpoints are detached. pub fn unmount(self) -> io::Result<()> { let config = self.config.unmount(); let hooks = self.hooks.unmount(); match (config, hooks) { (Ok(()), Ok(())) => Ok(()), (config, hooks) => Err(io::Error::other(format!( "config={config:?}, hooks={hooks:?}" ))), } } } fn sentry_error(action: &str, error: &io::Error) -> io::Error { io::Error::new(error.kind(), format!("{action}: {error}")) } fn cleanup_staged(error: io::Error, label: &Path) -> io::Error { combine_cleanup( error, unmount(label, UnmountFlags::DETACH).map_err(Into::into), ) } fn cleanup_installed(error: io::Error, mount: InstalledMount) -> io::Error { combine_cleanup(error, mount.unmount()) } fn combine_cleanup(error: io::Error, cleanup: io::Result<()>) -> io::Error { match cleanup { Ok(()) => error, Err(cleanup) => io::Error::new(error.kind(), format!("{error}; cleanup failed: {cleanup}")), } } fn descriptor_bind(source: &File, target: &File) -> io::Result { let mount = open_tree( source, "", OpenTreeFlags::OPEN_TREE_CLONE | OpenTreeFlags::OPEN_TREE_CLOEXEC | OpenTreeFlags::AT_EMPTY_PATH, )?; move_mount( &mount, "", target, "", MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH | MoveMountFlags::MOVE_MOUNT_T_EMPTY_PATH, ) .map_err(io::Error::from)?; Ok(mount) } fn descriptor_move(source: &File, target: &File) -> io::Result<()> { move_mount( source, "", target, "", MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH | MoveMountFlags::MOVE_MOUNT_T_EMPTY_PATH, ) .map_err(Into::into) } fn descriptor_remount_read_only(target: &Path) -> io::Result<()> { mount_remount( target, MountFlags::BIND | MountFlags::RDONLY | MountFlags::NOSUID | MountFlags::NODEV | MountFlags::NOEXEC, "", ) .map_err(Into::into) } fn require_empty_regular_file(file: &File) -> io::Result<()> { let metadata = fstat(file)?; if !FileType::from_raw_mode(metadata.st_mode).is_file() { return Err(io::Error::other( "transient Git config authority is not a regular file", )); } if metadata.st_size != 0 { return Err(io::Error::other( "transient Git config authority is not an empty regular file", )); } Ok(()) } fn require_regular_file(file: &File) -> io::Result<()> { if FileType::from_raw_mode(fstat(file)?.st_mode).is_file() { Ok(()) } else { Err(io::Error::other("host Git config is not a regular file")) } }