#![cfg(unix)]
use super::*;
#[cfg(unix)]
pub async fn run_daemon<D: DaemonDispatch>(dispatcher: D) -> anyhow::Result<()> {
let boot_guard = Some(acquire_daemon_boot_guard()?);
run_daemon_with_boot_guard_inner(
dispatcher,
boot_guard,
false,
DaemonOptions::default(),
|_| DaemonStartupReport::default(),
)
.await
}
#[cfg(all(unix, any(test, feature = "fault-injection")))]
#[doc(hidden)]
pub async fn run_daemon_in_process_test<D: DaemonDispatch>(dispatcher: D) -> anyhow::Result<()> {
let boot_guard = Some(acquire_daemon_boot_guard()?);
run_daemon_with_boot_guard_inner(
dispatcher,
boot_guard,
true,
DaemonOptions::default(),
|_| DaemonStartupReport::default(),
)
.await
}
#[cfg(unix)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum RendezvousPathRole<'a> {
Socket,
PidFile,
Store(&'a std::path::Path),
}
#[cfg(unix)]
impl RendezvousPathRole<'_> {
pub(super) fn refusal(
self,
rendezvous: std::fmt::Arguments<'_>,
store: std::fmt::Arguments<'_>,
) -> anyhow::Error {
match self {
Self::Store(db_path) => anyhow::anyhow!(
"refusing to open events database {}: {store}. Check the offending directory's \
ownership and permissions (remove group/other write access), or move the store \
to a location trusted end to end. Only the first offending component is \
reported; further untrusted ancestors or components may remain.",
db_path.display()
),
Self::Socket | Self::PidFile => anyhow::anyhow!("{rendezvous}"),
}
}
pub(super) fn env_name(self) -> &'static str {
match self {
Self::Socket => SOCKET_PATH_ENV,
Self::PidFile => PID_PATH_ENV,
Self::Store(_) => "events database",
}
}
pub(super) fn directory_name(self) -> &'static str {
match self {
Self::Socket => "socket directory",
Self::PidFile => "PID-file directory",
Self::Store(_) => "store directory",
}
}
pub(super) fn path_name(self) -> &'static str {
match self {
Self::Socket => "socket path",
Self::PidFile => "PID-file path",
Self::Store(_) => "store path",
}
}
pub(super) fn path_component_name(self) -> &'static str {
match self {
Self::Socket => "socket-path",
Self::PidFile => "PID-file-path",
Self::Store(_) => "store-path",
}
}
}
#[cfg(unix)]
pub(crate) fn ensure_socket_dir_is_trusted(parent: &std::path::Path) -> anyhow::Result<()> {
let daemon_euid = unsafe { libc::geteuid() } as u32;
ensure_rendezvous_dir_is_trusted(parent, RendezvousPathRole::Socket, daemon_euid, true)
}
#[cfg(unix)]
pub(crate) fn ensure_store_dir_is_trusted(
parent: &std::path::Path,
db_path: &std::path::Path,
) -> anyhow::Result<()> {
let daemon_euid = unsafe { libc::geteuid() } as u32;
ensure_rendezvous_dir_is_trusted(
parent,
RendezvousPathRole::Store(db_path),
daemon_euid,
true,
)
}
#[cfg(unix)]
pub fn ensure_pid_file_dir_is_trusted(pid_file: &std::path::Path) -> anyhow::Result<()> {
let parent = pid_file
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| std::path::Path::new("."));
let daemon_euid = unsafe { libc::geteuid() } as u32;
ensure_rendezvous_dir_is_trusted(parent, RendezvousPathRole::PidFile, daemon_euid, false)
}
#[cfg(unix)]
pub(super) fn ensure_rendezvous_dir_is_trusted(
parent: &std::path::Path,
role: RendezvousPathRole<'_>,
daemon_euid: u32,
repair_owned_default: bool,
) -> anyhow::Result<()> {
let env_name = role.env_name();
let directory_name = role.directory_name();
if repair_owned_default && parent == khive_dir() {
std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700)).map_err(|e| {
role.refusal(
format_args!(
"refusing to start: cannot chmod 0700 {}: {e}. The khive directory must be \
owner-only as the {directory_name} for {env_name}; it is part of the \
same-uid guarantee this daemon enforces.",
parent.display()
),
format_args!(
"cannot chmod 0700 store directory {}: {e}",
parent.display()
),
)
})?;
return ensure_rendezvous_path_is_swap_resistant(parent, daemon_euid, role);
}
let meta = std::fs::metadata(parent).map_err(|e| {
role.refusal(
format_args!(
"refusing to start: cannot stat {directory_name} {} for {env_name}: {e}. \
It gates rendezvous-path safety, and unreadable metadata is not a passing state.",
parent.display()
),
format_args!(
"cannot stat store directory {}: {e}; unreadable metadata is not a passing state",
parent.display()
),
)
})?;
use std::os::unix::fs::MetadataExt;
let owner = meta.uid();
if owner != daemon_euid && owner != 0 {
return Err(role.refusal(
format_args!(
"refusing to start: {directory_name} {} for {env_name} is owned by uid {owner}, \
not this daemon's uid ({daemon_euid}) or root. A directory owner can replace \
the rendezvous path regardless of mode bits. Point {env_name} at a directory \
you own, or unset it for the default.",
parent.display()
),
format_args!(
"store directory {} is owned by uid {owner}, not this process's uid \
({daemon_euid}) or root; its owner can replace the database path",
parent.display()
),
));
}
let mode = meta.permissions().mode();
if mode & 0o022 != 0 {
return Err(role.refusal(
format_args!(
"refusing to start: {directory_name} {} for {env_name} is mode {:04o} — writable \
by group or other, so another local user could replace the rendezvous path. \
Use a directory only you can write, or unset {env_name} for the default. \
This daemon is not changing the permissions of a directory it does not own.",
parent.display(),
mode & 0o7777
),
format_args!(
"store directory {} is mode {:04o}, writable by group or other, so another local \
user could replace the database path",
parent.display(),
mode & 0o7777
),
));
}
ensure_rendezvous_path_is_swap_resistant(parent, daemon_euid, role)
}
#[cfg(all(unix, test))]
pub(super) fn ensure_socket_path_is_swap_resistant(
parent: &std::path::Path,
daemon_euid: u32,
) -> anyhow::Result<()> {
ensure_rendezvous_path_is_swap_resistant(parent, daemon_euid, RendezvousPathRole::Socket)
}
#[cfg(unix)]
pub(super) fn ensure_rendezvous_path_is_swap_resistant(
parent: &std::path::Path,
daemon_euid: u32,
role: RendezvousPathRole<'_>,
) -> anyhow::Result<()> {
use std::os::unix::fs::MetadataExt;
let env_name = role.env_name();
let directory_name = role.directory_name();
let path_name = role.path_name();
let component_name = role.path_component_name();
let absolute = if parent.is_absolute() {
parent.to_path_buf()
} else {
std::env::current_dir()
.map_err(|e| {
role.refusal(
format_args!(
"refusing to start: cannot resolve the working directory to absolutize \
{directory_name} {} for {env_name}: {e}.",
parent.display()
),
format_args!(
"cannot resolve the working directory for store directory {}: {e}",
parent.display()
),
)
})?
.join(parent)
};
fn push_components(stack: &mut Vec<std::ffi::OsString>, path: &std::path::Path) {
let components: Vec<_> = path
.components()
.map(|c| c.as_os_str().to_os_string())
.collect();
stack.extend(components.into_iter().rev());
}
let mut stack: Vec<std::ffi::OsString> = Vec::new();
push_components(&mut stack, &absolute);
let mut resolved = std::path::PathBuf::new();
let mut symlinks_followed = 0u32;
while let Some(component) = stack.pop() {
if component == "/" {
resolved = std::path::PathBuf::from("/");
continue;
}
if component == "." {
continue;
}
if component == ".." {
resolved.pop();
continue;
}
let candidate = resolved.join(&component);
let meta = std::fs::symlink_metadata(&candidate).map_err(|e| {
role.refusal(
format_args!(
"refusing to start: cannot stat {component_name} component {} for {env_name}: \
{e}. An unreadable component is not a passing one.",
candidate.display()
),
format_args!(
"cannot stat store-path component {}: {e}; unreadable metadata is not a \
passing state",
candidate.display()
),
)
})?;
let owner = meta.uid();
if meta.file_type().is_symlink() {
symlinks_followed += 1;
if symlinks_followed > 40 {
return Err(role.refusal(
format_args!(
"refusing to start: {path_name} for {env_name} resolves through more than \
40 symlinks at {} — treating this as a loop.",
candidate.display()
),
format_args!(
"store path resolves through more than 40 symlinks at {}; treating this as \
a loop",
candidate.display()
),
));
}
if owner != daemon_euid && owner != 0 {
return Err(role.refusal(
format_args!(
"refusing to start: {component_name} symlink component {} for {env_name} \
is owned by uid {owner}, not this daemon's uid ({daemon_euid}) or root — \
its owner could retarget it after this check and re-root the {path_name}. \
Point {env_name} somewhere trusted end to end, or unset it for the default.",
candidate.display()
),
format_args!(
"store-path symlink component {} is owned by uid {owner}, not this \
process's uid ({daemon_euid}) or root; its owner could retarget the \
database path",
candidate.display()
),
));
}
let target = std::fs::read_link(&candidate).map_err(|e| {
role.refusal(
format_args!(
"refusing to start: cannot read {component_name} symlink component {} \
for {env_name}: {e}.",
candidate.display()
),
format_args!(
"cannot read store-path symlink component {}: {e}",
candidate.display()
),
)
})?;
push_components(&mut stack, &target);
continue;
}
if meta.is_dir() {
let mode = meta.permissions().mode();
let sticky = mode & 0o1000 != 0;
if owner != daemon_euid && owner != 0 {
return Err(role.refusal(
format_args!(
"refusing to start: {component_name} ancestor {} for {env_name} is owned by \
uid {owner}, not this daemon's uid ({daemon_euid}) or root — its owner \
could rename the next path component and re-root the {path_name}. Point \
{env_name} somewhere trusted end to end, or unset it for the default.",
candidate.display()
),
format_args!(
"store-path ancestor {} is owned by uid {owner}, not this process's uid \
({daemon_euid}) or root; its owner could rename the next path component",
candidate.display()
),
));
}
if mode & 0o022 != 0 && !sticky {
return Err(role.refusal(
format_args!(
"refusing to start: {component_name} ancestor {} for {env_name} is mode \
{:04o} — writable by group or other without the sticky bit, so another \
local user could rename the next path component and re-root the {path_name}. \
Point {env_name} somewhere trusted end to end, or unset it for the default.",
candidate.display(),
mode & 0o7777
),
format_args!(
"store-path ancestor {} is mode {:04o}, writable by group or other without \
the sticky bit, so another local user could rename the next path \
component",
candidate.display(), mode & 0o7777
),
));
}
resolved = candidate;
continue;
}
return Err(role.refusal(
format_args!(
"refusing to start: {component_name} component {} for {env_name} is neither a \
directory nor a symlink — the {path_name} cannot traverse it.",
candidate.display()
),
format_args!(
"store-path component {} is neither a directory nor a symlink; the database path \
cannot traverse it",
candidate.display()
),
));
}
Ok(())
}