99 lines
3.1 KiB
Rust
99 lines
3.1 KiB
Rust
|
|
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<Self> {
|
||
|
|
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}"))
|
||
|
|
}
|