use std::ffi::OsStr;
use std::fs::{self, DirBuilder, Metadata};
use std::io;
use std::os::unix::fs::{DirBuilderExt, MetadataExt};
use std::path::{Path, PathBuf};
use crate::daemon::egress::EGRESS_DIR_NAME;
use crate::daemon::machines::MACHINE_DIR_NAME;
use crate::dirs::{self, PRIVATE_DIRECTORY_MODE};
use crate::{Error, Result};
const LAYOUT: [&str; 2] = [MACHINE_DIR_NAME, EGRESS_DIR_NAME];
const PERMISSION_BITS: u32 = 0o7777;
const WRITABLE_BY_OTHERS: u32 = 0o022;
const STICKY_BIT: u32 = 0o1000;
const ROOT_UID: u32 = 0;
pub(super) fn prepare_in(leaf: &Path, uid: u32) -> Result<PathBuf> {
let (parent, name) = split(leaf)?;
DirBuilder::new()
.recursive(true)
.mode(PRIVATE_DIRECTORY_MODE)
.create(parent)
.map_err(|source| Error::io(parent, source))?;
let leaf = trusted_parent(parent, uid)?.join(name);
dirs::create_private_directory(&leaf)?;
validate_private(&lstat(&leaf)?, &leaf, uid)?;
for directory in LAYOUT {
dirs::create_private_directory(&leaf.join(directory))?;
}
Ok(leaf)
}
pub(super) fn locate_in(leaf: &Path, uid: u32) -> Result<PathBuf> {
match fs::symlink_metadata(leaf) {
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(leaf.to_path_buf()),
Err(source) => return Err(Error::io(leaf, source)),
}
let (parent, name) = split(leaf)?;
let leaf = trusted_parent(parent, uid)?.join(name);
validate_private(&lstat(&leaf)?, &leaf, uid)?;
Ok(leaf)
}
fn split(leaf: &Path) -> Result<(&Path, &OsStr)> {
match (leaf.parent(), leaf.file_name()) {
(Some(parent), Some(name)) => Ok((parent, name)),
_ => Err(unsafe_directory(
leaf,
"the filesystem root is not a private directory",
)),
}
}
fn trusted_parent(parent: &Path, uid: u32) -> Result<PathBuf> {
let parent = fs::canonicalize(parent).map_err(|source| Error::io(parent, source))?;
for ancestor in parent.ancestors() {
validate_ancestor(&lstat(ancestor)?, ancestor, uid)?;
}
Ok(parent)
}
fn lstat(path: &Path) -> Result<Metadata> {
fs::symlink_metadata(path).map_err(|source| Error::io(path, source))
}
fn validate_ancestor(metadata: &Metadata, path: &Path, uid: u32) -> Result<()> {
if !has_trusted_owner(metadata, uid) {
return Err(unsafe_directory(
path,
"an ancestor must belong to root or the effective user",
));
}
if is_writable_by_others(metadata) && !is_root_owned_sticky(metadata) {
return Err(unsafe_directory(
path,
"an ancestor is writable by other users without root-owned sticky protection",
));
}
Ok(())
}
fn validate_private(metadata: &Metadata, path: &Path, uid: u32) -> Result<()> {
if !metadata.is_dir() {
return Err(unsafe_directory(
path,
"the path must be a real directory, not a symlink",
));
}
if metadata.uid() != uid {
return Err(unsafe_directory(
path,
"the directory must belong to the effective user",
));
}
if !has_private_mode(metadata) {
return Err(unsafe_directory(
path,
"the directory must have permissions 0700",
));
}
Ok(())
}
fn has_trusted_owner(metadata: &Metadata, uid: u32) -> bool {
metadata.uid() == ROOT_UID || metadata.uid() == uid
}
fn is_writable_by_others(metadata: &Metadata) -> bool {
metadata.mode() & WRITABLE_BY_OTHERS != 0
}
fn is_root_owned_sticky(metadata: &Metadata) -> bool {
metadata.uid() == ROOT_UID && metadata.mode() & STICKY_BIT != 0
}
fn has_private_mode(metadata: &Metadata) -> bool {
metadata.mode() & PERMISSION_BITS == PRIVATE_DIRECTORY_MODE
}
fn unsafe_directory(path: &Path, reason: &'static str) -> Error {
Error::UnsafeDirectory {
path: path.to_path_buf(),
reason,
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt, symlink};
use std::path::{Path, PathBuf};
use rustix::process::geteuid;
use super::{locate_in, prepare_in, validate_private};
use crate::{Error, Result};
fn prepare(leaf: &Path) -> Result<PathBuf> {
prepare_in(leaf, geteuid().as_raw())
}
fn locate(leaf: &Path) -> Result<PathBuf> {
locate_in(leaf, geteuid().as_raw())
}
fn prepare_at(leaf: &Path) -> PathBuf {
let directory = prepare(leaf).unwrap();
assert_eq!(directory, leaf.canonicalize().unwrap());
directory
}
fn assert_unsafe(leaf: &Path) {
assert!(matches!(prepare(leaf), Err(Error::UnsafeDirectory { .. })));
}
fn mode(path: &Path) -> u32 {
fs::symlink_metadata(path).unwrap().mode() & 0o7777
}
#[test]
fn creates_the_leaf_private_and_reuses_it_without_changing_contents() {
let parent = tempfile::tempdir().unwrap();
let leaf = parent.path().join("moonmchn");
let directory = prepare_at(&leaf);
assert_eq!(fs::metadata(&directory).unwrap().uid(), geteuid().as_raw());
assert_eq!(mode(&directory), 0o700);
fs::write(directory.join("keep"), "keep").unwrap();
assert_eq!(prepare_at(&leaf), directory);
assert_eq!(fs::read_to_string(directory.join("keep")).unwrap(), "keep");
}
#[test]
fn lays_out_the_daemon_directories() {
let root = tempfile::tempdir().unwrap();
let directory = prepare_at(&root.path().join("moonmchn"));
for name in ["machines", "egress"] {
assert_eq!(mode(&directory.join(name)), 0o700, "{name}");
}
let leaf = root.path().join("other");
fs::create_dir(&leaf).unwrap();
fs::set_permissions(&leaf, fs::Permissions::from_mode(0o700)).unwrap();
fs::create_dir(leaf.join("machines")).unwrap();
fs::set_permissions(leaf.join("machines"), fs::Permissions::from_mode(0o750)).unwrap();
prepare_at(&leaf);
assert_eq!(mode(&leaf.join("machines")), 0o750);
}
#[test]
fn locate_creates_nothing_for_a_missing_directory() {
let root = tempfile::tempdir().unwrap();
let leaf = root.path().join(".local/state/moonmchn");
assert_eq!(locate(&leaf).unwrap(), leaf);
assert!(!root.path().join(".local").exists());
}
#[test]
fn locate_checks_an_existing_directory() {
let root = tempfile::tempdir().unwrap();
let leaf = root.path().join("moonmchn");
fs::create_dir(&leaf).unwrap();
fs::set_permissions(&leaf, fs::Permissions::from_mode(0o750)).unwrap();
assert!(matches!(locate(&leaf), Err(Error::UnsafeDirectory { .. })));
assert_eq!(mode(&leaf), 0o750);
let prepared = prepare_at(&root.path().join("prepared"));
assert_eq!(locate(&root.path().join("prepared")).unwrap(), prepared);
}
#[test]
fn creates_missing_parents_private() {
let root = tempfile::tempdir().unwrap();
let leaf = root.path().join(".local/state/moonmchn");
let directory = prepare_at(&leaf);
assert_eq!(mode(&directory), 0o700);
for parent in [".local", ".local/state"] {
assert_eq!(mode(&root.path().join(parent)), 0o700, "{parent}");
}
}
#[test]
fn rejects_a_world_writable_ancestor_before_creating_the_leaf() {
let root = tempfile::tempdir().unwrap();
fs::set_permissions(root.path(), fs::Permissions::from_mode(0o777)).unwrap();
let leaf = root.path().join("moonmchn");
assert_unsafe(&leaf);
assert!(!leaf.exists());
}
#[test]
fn rejects_an_insecure_existing_leaf_without_repairing_it() {
let root = tempfile::tempdir().unwrap();
let leaf = root.path().join("moonmchn");
fs::create_dir(&leaf).unwrap();
fs::set_permissions(&leaf, fs::Permissions::from_mode(0o750)).unwrap();
assert_unsafe(&leaf);
assert_eq!(mode(&leaf), 0o750);
}
#[test]
fn rejects_a_symlinked_leaf_including_dangling_ones() {
for target_exists in [false, true] {
let root = tempfile::tempdir().unwrap();
let target = root.path().join("target");
if target_exists {
fs::create_dir(&target).unwrap();
fs::set_permissions(&target, fs::Permissions::from_mode(0o700)).unwrap();
}
let leaf = root.path().join("moonmchn");
symlink(&target, &leaf).unwrap();
for path in [leaf.clone(), leaf.join(""), leaf.join(".")] {
assert_unsafe(&path);
}
assert_eq!(fs::read_link(&leaf).unwrap(), target);
assert_eq!(target.exists(), target_exists);
}
}
#[test]
fn rejects_a_file_in_place_of_the_leaf() {
let root = tempfile::tempdir().unwrap();
let leaf = root.path().join("moonmchn");
fs::write(&leaf, "keep").unwrap();
assert_unsafe(&leaf);
assert_eq!(fs::read_to_string(&leaf).unwrap(), "keep");
}
#[test]
fn allows_symlinked_ancestors_and_returns_the_resolved_path() {
let root = tempfile::tempdir().unwrap();
let target = tempfile::tempdir().unwrap();
let link = root.path().join("ancestor");
symlink(target.path(), &link).unwrap();
let directory = prepare_at(&link.join("moonmchn"));
assert_eq!(
directory,
target.path().canonicalize().unwrap().join("moonmchn")
);
}
#[test]
fn rejects_the_filesystem_root() {
assert_unsafe(Path::new("/"));
}
#[test]
fn rejects_wrong_owner_without_needing_chown() {
let dir = tempfile::tempdir().unwrap();
let metadata = fs::metadata(dir.path()).unwrap();
let wrong_uid = geteuid().as_raw().wrapping_add(1);
assert!(matches!(
validate_private(&metadata, dir.path(), wrong_uid),
Err(Error::UnsafeDirectory { .. })
));
}
}