2026-07-16 13:03:04 +09:00
|
|
|
use std::fs::File;
|
|
|
|
|
use std::io;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
2026-07-18 02:08:14 +09:00
|
|
|
use rustix::fs::{FileType, Mode, OFlags, fstat};
|
|
|
|
|
use rustix::mount::{
|
|
|
|
|
MountFlags, MoveMountFlags, OpenTreeFlags, UnmountFlags, mount_remount, move_mount, open_tree,
|
|
|
|
|
unmount,
|
|
|
|
|
};
|
2026-07-16 13:03:04 +09:00
|
|
|
|
|
|
|
|
use crate::backing::BackingRoot;
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct GitSentryConfiguration {
|
|
|
|
|
pub mounted_workspace: PathBuf,
|
|
|
|
|
pub host_git_common_directory: PathBuf,
|
|
|
|
|
pub transient_hooks: PathBuf,
|
2026-07-18 02:08:14 +09:00
|
|
|
pub transient_config: PathBuf,
|
2026-07-16 13:03:04 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct GitSentry {
|
2026-07-18 02:08:14 +09:00
|
|
|
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)
|
|
|
|
|
}
|
2026-07-16 13:03:04 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl GitSentry {
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses linked or missing sources and targets and incomplete protective mounts.
|
|
|
|
|
pub fn install(configuration: &GitSentryConfiguration) -> io::Result<Self> {
|
2026-07-18 02:08:14 +09:00
|
|
|
let workspace = BackingRoot::open(&configuration.mounted_workspace)?;
|
2026-07-16 13:03:04 +09:00
|
|
|
let common = BackingRoot::open(&configuration.host_git_common_directory)?;
|
2026-07-18 02:08:14 +09:00
|
|
|
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(),
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-07-16 13:03:04 +09:00
|
|
|
|
2026-07-18 02:08:14 +09:00
|
|
|
/// # 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<Self> {
|
|
|
|
|
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,
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-07-16 13:03:04 +09:00
|
|
|
|
2026-07-18 02:08:14 +09:00
|
|
|
fn install_from_roots(
|
|
|
|
|
_workspace: &BackingRoot,
|
|
|
|
|
common: &BackingRoot,
|
|
|
|
|
transient_hooks: &BackingRoot,
|
|
|
|
|
transient_hooks_label: PathBuf,
|
|
|
|
|
transient_config: File,
|
|
|
|
|
transient_config_label: PathBuf,
|
|
|
|
|
) -> io::Result<Self> {
|
|
|
|
|
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));
|
2026-07-16 13:03:04 +09:00
|
|
|
}
|
2026-07-18 02:08:14 +09:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-07-16 13:03:04 +09:00
|
|
|
Ok(Self {
|
2026-07-18 02:08:14 +09:00
|
|
|
config,
|
|
|
|
|
hooks: InstalledMount {
|
|
|
|
|
mounted_target: mounted_hooks,
|
|
|
|
|
cleanup_target: hook_source,
|
|
|
|
|
cleanup_label: transient_hooks_label,
|
|
|
|
|
},
|
2026-07-16 13:03:04 +09:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error unless both exact protective mountpoints are detached.
|
|
|
|
|
pub fn unmount(self) -> io::Result<()> {
|
2026-07-18 02:08:14 +09:00
|
|
|
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:?}"
|
|
|
|
|
))),
|
|
|
|
|
}
|
2026-07-16 13:03:04 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 02:08:14 +09:00
|
|
|
fn sentry_error(action: &str, error: &io::Error) -> io::Error {
|
|
|
|
|
io::Error::new(error.kind(), format!("{action}: {error}"))
|
2026-07-16 13:03:04 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-18 02:08:14 +09:00
|
|
|
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<std::os::fd::OwnedFd> {
|
|
|
|
|
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,
|
2026-07-16 13:03:04 +09:00
|
|
|
MountFlags::BIND
|
|
|
|
|
| MountFlags::RDONLY
|
|
|
|
|
| MountFlags::NOSUID
|
|
|
|
|
| MountFlags::NODEV
|
|
|
|
|
| MountFlags::NOEXEC,
|
|
|
|
|
"",
|
2026-07-18 02:08:14 +09:00
|
|
|
)
|
|
|
|
|
.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",
|
|
|
|
|
));
|
2026-07-16 13:03:04 +09:00
|
|
|
}
|
2026-07-18 02:08:14 +09:00
|
|
|
if metadata.st_size != 0 {
|
|
|
|
|
return Err(io::Error::other(
|
|
|
|
|
"transient Git config authority is not an empty regular file",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
2026-07-16 13:03:04 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-18 02:08:14 +09:00
|
|
|
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"))
|
|
|
|
|
}
|
2026-07-16 13:03:04 +09:00
|
|
|
}
|