use std::fs::File; use std::io; use std::os::fd::{AsRawFd, RawFd}; use std::path::{Path, PathBuf}; use rustix::fs::{Mode, OFlags}; use rustix::mount::{MountFlags, UnmountFlags, mount_bind, mount_remount, 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 struct GitSentry { config_target: File, hook_target: File, } 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 = BackingRoot::open(&configuration.transient_hooks)?; let hook_source = transient.duplicate()?; let hook_target = common.open_entry( Path::new("hooks"), OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), )?; descriptor_bind(&hook_source, &hook_target)?; let hook_target = common.open_entry( Path::new("hooks"), OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), )?; let config_source = common.open_entry(Path::new("config"), OFlags::RDONLY, Mode::empty())?; let config_target = common.open_entry(Path::new("config"), OFlags::RDONLY, Mode::empty())?; descriptor_bind(&config_source, &config_target)?; let config_target = common.open_entry(Path::new("config"), OFlags::RDONLY, Mode::empty())?; if let Err(error) = descriptor_remount_read_only(&config_target) { let _ = descriptor_unmount(&hook_target); return Err(error); } Ok(Self { config_target, hook_target, }) } /// # Errors /// Returns an error unless both exact protective mountpoints are detached. pub fn unmount(self) -> io::Result<()> { let config = descriptor_unmount(&self.config_target); let hooks = descriptor_unmount(&self.hook_target); config.and(hooks) } } fn descriptor_bind(source: &File, target: &File) -> io::Result<()> { Ok(mount_bind( proc_fd(source.as_raw_fd()), proc_fd(target.as_raw_fd()), )?) } fn descriptor_remount_read_only(target: &File) -> io::Result<()> { let result = mount_remount( proc_fd(target.as_raw_fd()), MountFlags::BIND | MountFlags::RDONLY | MountFlags::NOSUID | MountFlags::NODEV | MountFlags::NOEXEC, "", ); if result.is_err() { let _ = unmount(proc_fd(target.as_raw_fd()), UnmountFlags::DETACH); } Ok(result?) } fn descriptor_unmount(target: &File) -> io::Result<()> { Ok(unmount(proc_fd(target.as_raw_fd()), UnmountFlags::DETACH)?) } fn proc_fd(fd: RawFd) -> PathBuf { PathBuf::from(format!("/proc/self/fd/{fd}")) }