use std::path::{Path, PathBuf};
#[derive(Debug)]
pub(crate) struct DataRootAncestorError {
component: PathBuf,
reason: String,
}
impl DataRootAncestorError {
pub(super) fn new(component: PathBuf, reason: impl Into<String>) -> Self {
Self {
component,
reason: reason.into(),
}
}
pub(crate) fn into_parts(self) -> (PathBuf, String) {
(self.component, self.reason)
}
}
impl std::fmt::Display for DataRootAncestorError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"ancestor `{}` is not owner-controlled: {}",
self.component.display(),
self.reason
)
}
}
impl std::error::Error for DataRootAncestorError {}
pub(crate) fn validate_ambient_backend_ancestors(
data_root: &Path,
) -> Result<(), DataRootAncestorError> {
use std::os::unix::fs::MetadataExt as _;
if !data_root.is_absolute() {
return Err(DataRootAncestorError::new(
data_root.to_path_buf(),
"descriptor-resolved backend path is not absolute",
));
}
let effective_uid = rustix::process::geteuid().as_raw();
let mut component_path = PathBuf::new();
for component in data_root.components() {
component_path.push(component.as_os_str());
let metadata = std::fs::symlink_metadata(&component_path).map_err(|error| {
DataRootAncestorError::new(
component_path.clone(),
format!("could not inspect ownership and mode: {error}"),
)
})?;
if !metadata.is_dir() || metadata.file_type().is_symlink() {
return Err(DataRootAncestorError::new(
component_path,
"component is not a real directory",
));
}
let mode = metadata.mode() & 0o7777;
if mode & 0o022 != 0 {
return Err(DataRootAncestorError::new(
component_path,
format!(
"mode {mode:04o} grants group/world write access (sticky bit is not accepted)"
),
));
}
let owner_uid = metadata.uid();
if owner_uid != effective_uid && owner_uid != 0 {
return Err(DataRootAncestorError::new(
component_path,
format!("owner uid {owner_uid} is neither server euid {effective_uid} nor root"),
));
}
#[cfg(target_os = "macos")]
super::darwin_acl::validate_extended_acl(&component_path, effective_uid)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_world_writable_ancestor_is_still_refused() -> Result<(), Box<dyn std::error::Error>> {
let sandbox = crate::test_support::private_tempdir()?;
let shared = sandbox.path().join("shared");
std::fs::create_dir(&shared)?;
std::fs::set_permissions(
&shared,
std::os::unix::fs::PermissionsExt::from_mode(0o1777),
)?;
let data_root = shared.join("aion-data");
std::fs::create_dir(&data_root)?;
std::fs::set_permissions(
&data_root,
std::os::unix::fs::PermissionsExt::from_mode(0o700),
)?;
let data_root = std::fs::canonicalize(&data_root)?;
let shared = std::fs::canonicalize(&shared)?;
let error = validate_ambient_backend_ancestors(&data_root)
.err()
.ok_or("a world-writable ancestor was accepted")?;
let (component, reason) = error.into_parts();
assert_eq!(component, shared);
assert!(reason.contains("1777"), "reason did not state the mode");
assert!(reason.contains("group/world write"));
Ok(())
}
#[cfg(target_os = "macos")]
#[test]
fn the_shared_system_temp_directory_is_still_refused() -> Result<(), Box<dyn std::error::Error>>
{
let error = validate_ambient_backend_ancestors(Path::new("/private/tmp/aion-data"))
.err()
.ok_or("a data root under /private/tmp was accepted")?;
let (component, reason) = error.into_parts();
assert_eq!(component, Path::new("/private/tmp"));
assert!(reason.contains("group/world write"));
Ok(())
}
#[test]
fn a_relative_backend_path_is_refused() -> Result<(), Box<dyn std::error::Error>> {
let error = validate_ambient_backend_ancestors(Path::new("aion-data"))
.err()
.ok_or("a relative backend path was accepted")?;
let (_, reason) = error.into_parts();
assert!(reason.contains("not absolute"));
Ok(())
}
}