use std::path::{Path, PathBuf};
use murmur_artifact::{security_warning_link, W_SEC_028};
pub const MURMUR_HOME_DIR_MODE: u32 = 0o700;
pub const PRIVATE_FILE_MODE: u32 = 0o600;
const WIDE_DESCENDANT_CAP: usize = 20;
pub fn ensure_murmur_home() -> Result<PathBuf, String> {
use std::os::unix::fs::PermissionsExt;
let home = crate::state_store::murmur_home_dir()?;
let located = |reason: String| format!("{}: {reason}", home.display());
match std::fs::metadata(&home) {
Ok(metadata) if !metadata.is_dir() => return Err(located("not a directory".to_string())),
Ok(metadata) => {
let mode = metadata.permissions().mode() & 0o777;
let narrowed = mode & MURMUR_HOME_DIR_MODE;
if narrowed != mode {
std::fs::set_permissions(&home, std::fs::Permissions::from_mode(narrowed))
.map_err(|err| located(format!("failed to set mode {narrowed:04o}: {err}")))?;
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
hold_private_dir(&home).map_err(located)?;
}
Err(err) => return Err(located(err.to_string())),
}
Ok(home)
}
pub fn hold_private_dir(path: &Path) -> Result<(), String> {
crate::state_store::ensure_private_dir(path, MURMUR_HOME_DIR_MODE)
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum HomeUnusable {
#[error(
"HOME is not set, so there is no ~/.murmur to hold this session's runtime and records \
directories"
)]
NotSet,
#[error(
"HOME is '{0}', which is not an absolute path, so there is no ~/.murmur to hold this \
session's runtime and records directories"
)]
NotAbsolute(String),
#[error("~/.murmur ({}) cannot hold this session's runtime and records directories: {reason}", path.display())]
CannotHold { path: PathBuf, reason: String },
}
pub const HOME_UNUSABLE_HINT: &str = "set HOME to the absolute path of a directory you own, in \
which ~/.murmur exists or can be created as a 0700 directory";
pub fn usable_murmur_home() -> Result<PathBuf, HomeUnusable> {
let home = home_path()?;
let murmur_home = home.join(".murmur");
let cannot_hold = |reason: String| HomeUnusable::CannotHold {
path: murmur_home.clone(),
reason,
};
for sub in [
crate::runtime_dir::SESSIONS_DIR_NAME,
crate::records_dir::RECORDS_DIR_NAME,
] {
let path = murmur_home.join(sub);
if std::fs::symlink_metadata(&path).is_ok() {
writable_dir(&path)
.map_err(|reason| cannot_hold(format!("{}: {reason}", path.display())))?;
}
}
if std::fs::symlink_metadata(&murmur_home).is_ok() {
writable_dir(&murmur_home).map_err(cannot_hold)?;
} else {
writable_dir(&home)
.map_err(|reason| cannot_hold(format!("HOME {}: {reason}", home.display())))?;
}
Ok(murmur_home)
}
pub(crate) fn home_path() -> Result<PathBuf, HomeUnusable> {
let home = match std::env::var_os("HOME").filter(|home| !home.is_empty()) {
None => return Err(HomeUnusable::NotSet),
Some(home) => PathBuf::from(home),
};
if !home.is_absolute() {
return Err(HomeUnusable::NotAbsolute(home.display().to_string()));
}
Ok(home)
}
fn writable_dir(path: &Path) -> Result<(), String> {
match std::fs::metadata(path) {
Ok(metadata) if metadata.is_dir() => {}
Ok(_) => return Err("not a directory".to_string()),
Err(error) => return Err(error.to_string()),
}
rustix::fs::access(
path,
rustix::fs::Access::WRITE_OK | rustix::fs::Access::EXEC_OK,
)
.map_err(|errno| format!("cannot be written ({})", std::io::Error::from(errno)))
}
pub fn write_private_file(target: &Path, contents: &[u8]) -> Result<(), String> {
crate::retention::StagedRewrite::stage_with_mode(target, contents, Some(PRIVATE_FILE_MODE))?
.commit()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct FileStamp {
dev: u64,
ino: u64,
len: u64,
uid: u32,
pub(crate) mode: u32,
mtime: (i64, i64),
ctime: (i64, i64),
}
impl FileStamp {
pub(crate) fn of(metadata: &std::fs::Metadata) -> Self {
use std::os::unix::fs::MetadataExt;
Self {
dev: metadata.dev(),
ino: metadata.ino(),
len: metadata.len(),
uid: metadata.uid(),
mode: metadata.mode(),
mtime: (metadata.mtime(), metadata.mtime_nsec()),
ctime: (metadata.ctime(), metadata.ctime_nsec()),
}
}
}
impl std::fmt::Display for FileStamp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} {} {} {} {:o} {}.{:09} {}.{:09}",
self.dev,
self.ino,
self.len,
self.uid,
self.mode,
self.mtime.0,
self.mtime.1,
self.ctime.0,
self.ctime.1,
)
}
}
pub(crate) fn private_subdir(
home: &Path,
name: &str,
create: bool,
reachable: impl Fn(&Path) -> bool,
) -> Option<(PathBuf, bool)> {
if reachable(home) {
return None;
}
let dir = home.join(name);
let mut widened = false;
if create {
match std::fs::symlink_metadata(&dir) {
Ok(metadata) if metadata.is_dir() => widened = !held_private(&dir),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
_ => return None,
}
hold_private_dir(&dir).ok()?;
}
if !held_private(&dir) || reachable(&dir) {
return None;
}
if widened {
for (path, _) in direct_files(&dir) {
let _ = std::fs::remove_file(path);
}
}
Some((dir, widened))
}
pub(crate) fn reachable_from(dir: &Path, root: &Path) -> bool {
match (dir.canonicalize(), root.canonicalize()) {
(Ok(dir), Ok(root)) => dir.starts_with(root),
(Ok(_), Err(err)) => err.kind() != std::io::ErrorKind::NotFound,
(Err(_), _) => true,
}
}
pub(crate) fn held_private(dir: &Path) -> bool {
std::fs::symlink_metadata(dir).is_ok_and(|metadata| {
metadata.is_dir() && owned_within(&metadata, current_euid(), MURMUR_HOME_DIR_MODE)
})
}
pub(crate) fn owned_within(metadata: &std::fs::Metadata, euid: u32, allowed: u32) -> bool {
use std::os::unix::fs::MetadataExt;
metadata.uid() == euid && !is_wider_than(metadata.mode(), allowed)
}
#[allow(unsafe_code)]
pub(crate) fn current_euid() -> u32 {
unsafe { libc::geteuid() }
}
pub(crate) fn open_private(path: &Path) -> Option<(std::fs::File, std::fs::Metadata)> {
let (file, metadata) = crate::resource_plane::open_no_follow(path).ok()?;
(metadata.is_file() && owned_within(&metadata, current_euid(), PRIVATE_FILE_MODE))
.then_some((file, metadata))
}
pub(crate) fn direct_files(dir: &Path) -> impl Iterator<Item = (PathBuf, std::fs::Metadata)> {
std::fs::read_dir(dir)
.into_iter()
.flatten()
.filter_map(Result::ok)
.filter_map(|entry| {
let path = entry.path();
let metadata = std::fs::symlink_metadata(&path).ok()?;
(!metadata.is_dir()).then_some((path, metadata))
})
}
pub fn is_wider_than(mode: u32, allowed: u32) -> bool {
(mode & 0o777) & !allowed != 0
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HomeEntryKind {
Home,
Config,
DeployKeys,
DeployStaging,
Deployments,
Spend,
Conversations,
Running,
State,
Artifacts,
CompiledForms,
HostFacts,
BinaryCache,
Formations,
Sessions,
Records,
Programs,
Keys,
Unrecognised(String),
}
impl HomeEntryKind {
const KNOWN: [HomeEntryKind; 17] = [
Self::Config,
Self::DeployKeys,
Self::DeployStaging,
Self::Deployments,
Self::Spend,
Self::Conversations,
Self::Running,
Self::State,
Self::Artifacts,
Self::CompiledForms,
Self::HostFacts,
Self::BinaryCache,
Self::Formations,
Self::Sessions,
Self::Records,
Self::Programs,
Self::Keys,
];
pub fn file_name(&self) -> Option<&str> {
Some(match self {
Self::Home => return None,
Self::Config => "config.yaml",
Self::DeployKeys => "deploy_keys",
Self::DeployStaging => "deploy_staging",
Self::Deployments => "deployments.json",
Self::Spend => "spend",
Self::Conversations => "conversations",
Self::Running => "running",
Self::State => "state",
Self::Artifacts => "artifacts",
Self::CompiledForms => "compiled",
Self::HostFacts => "host_facts",
Self::BinaryCache => "bin",
Self::Formations => crate::formation_launch::FORMATIONS_DIR,
Self::Sessions => crate::runtime_dir::SESSIONS_DIR_NAME,
Self::Records => crate::records_dir::RECORDS_DIR_NAME,
Self::Programs => crate::pinned_programs::PROGRAMS_DIR,
Self::Keys => crate::record_seal::KEYS_DIR,
Self::Unrecognised(name) => name,
})
}
pub fn holds(&self) -> &'static str {
match self {
Self::Home => "the murmur home",
Self::Config => "provider credentials",
Self::DeployKeys => "SSH private keys",
Self::DeployStaging => "deployment staging copies",
Self::Deployments => "deployment records",
Self::Spend => "the machine spend ledger",
Self::Conversations => "conversation records",
Self::Running => "running-capsule records",
Self::State => "capsule state stores",
Self::Artifacts => "installed artifacts",
Self::CompiledForms => "compiled WASM cache",
Self::HostFacts => "facts about allowlisted host binaries",
Self::BinaryCache => "cached mur binaries",
Self::Formations => "formation members' working directories",
Self::Sessions => "running sessions' runtime directories",
Self::Records => "every session's trace, logs, eval scores and delegation outcome",
Self::Programs => "pinned program downloads",
Self::Keys => "the record-seal key",
Self::Unrecognised(_) => "something not recognised by this build",
}
}
pub fn kept(&self) -> Option<&'static str> {
match self {
Self::Formations => Some(
"each kept until its formation has ended and the entry member's trace.retain \
expires it",
),
Self::Sessions => Some(
"each removed when its session ends, and by the next launch when a session \
ended without removing its own",
),
Self::Records => Some(
"each kept after its session ends, until its capsule's trace.retain expires it",
),
_ => None,
}
}
pub fn safe_to_delete(&self) -> bool {
matches!(self, Self::CompiledForms | Self::HostFacts)
}
pub fn expected_private(&self) -> bool {
!matches!(
self,
Self::Artifacts | Self::BinaryCache | Self::Unrecognised(_)
)
}
pub fn allowed_file_mode(&self) -> u32 {
match self {
Self::Programs => crate::pinned_programs::EXECUTABLE_FILE_MODE,
Self::Keys => crate::record_seal::PUBLIC_KEY_MODE,
_ => PRIVATE_FILE_MODE,
}
}
pub fn private_depth(&self) -> Option<usize> {
match self {
Self::Formations => Some(2),
Self::Sessions => Some(2),
Self::Records => Some(1),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WideEntry {
pub path: PathBuf,
pub mode: u32,
pub allowed: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HomeEntryState {
Absent,
Present {
mode: u32,
wide: bool,
wide_descendants: Vec<WideEntry>,
wide_descendants_omitted: usize,
},
Unreadable {
error: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HomeEntryReport {
pub path: PathBuf,
pub kind: HomeEntryKind,
pub state: HomeEntryState,
}
impl HomeEntryReport {
pub fn allowed(&self) -> u32 {
allowed_for(&self.path)
}
}
pub fn audit_murmur_home(murmur_home: &Path) -> Vec<HomeEntryReport> {
let mut reports = vec![report_entry(murmur_home.to_path_buf(), HomeEntryKind::Home)];
for kind in HomeEntryKind::KNOWN {
let name = kind.file_name().expect("a known kind has a fixed name");
reports.push(report_entry(murmur_home.join(name), kind));
}
let mut others: Vec<String> = std::fs::read_dir(murmur_home)
.map(|entries| {
entries
.filter_map(Result::ok)
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.filter(|name| {
!HomeEntryKind::KNOWN
.iter()
.any(|kind| kind.file_name() == Some(name.as_str()))
})
.collect()
})
.unwrap_or_default();
others.sort();
for name in others {
reports.push(report_entry(
murmur_home.join(&name),
HomeEntryKind::Unrecognised(name),
));
}
reports
}
fn report_entry(path: PathBuf, kind: HomeEntryKind) -> HomeEntryReport {
use std::os::unix::fs::PermissionsExt;
let metadata = match std::fs::symlink_metadata(&path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return HomeEntryReport {
path,
kind,
state: HomeEntryState::Absent,
}
}
Err(err) => {
return HomeEntryReport {
path,
kind,
state: HomeEntryState::Unreadable {
error: err.to_string(),
},
}
}
};
let mode = metadata.permissions().mode() & 0o777;
let private = kind.expected_private();
let file_type = metadata.file_type();
let wide =
private && !file_type.is_symlink() && is_wider_than(mode, allowed_for_type(file_type));
let mut wide_descendants = Vec::new();
let mut omitted = 0;
if private && file_type.is_dir() && kind != HomeEntryKind::Home {
collect_wide_descendants(
&path,
kind.private_depth(),
kind.allowed_file_mode(),
&mut wide_descendants,
&mut omitted,
);
}
HomeEntryReport {
path,
kind,
state: HomeEntryState::Present {
mode,
wide,
wide_descendants,
wide_descendants_omitted: omitted,
},
}
}
fn collect_wide_descendants(
dir: &Path,
depth: Option<usize>,
file_allowed: u32,
found: &mut Vec<WideEntry>,
omitted: &mut usize,
) {
use std::os::unix::fs::PermissionsExt;
if depth == Some(0) {
return;
}
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
let mut paths: Vec<PathBuf> = entries.filter_map(Result::ok).map(|e| e.path()).collect();
paths.sort();
for path in paths {
let Ok(metadata) = std::fs::symlink_metadata(&path) else {
continue;
};
let file_type = metadata.file_type();
if file_type.is_symlink() || !(file_type.is_dir() || file_type.is_file()) {
continue;
}
let mode = metadata.permissions().mode() & 0o777;
let allowed = if file_type.is_dir() {
MURMUR_HOME_DIR_MODE
} else {
file_allowed
};
if is_wider_than(mode, allowed) {
if found.len() < WIDE_DESCENDANT_CAP {
found.push(WideEntry {
path: path.clone(),
mode,
allowed,
});
} else {
*omitted += 1;
}
}
if file_type.is_dir() {
collect_wide_descendants(
&path,
depth.map(|depth| depth - 1),
file_allowed,
found,
omitted,
);
}
}
}
fn allowed_for_type(file_type: std::fs::FileType) -> u32 {
if file_type.is_dir() {
MURMUR_HOME_DIR_MODE
} else {
PRIVATE_FILE_MODE
}
}
fn allowed_for(path: &Path) -> u32 {
match std::fs::symlink_metadata(path) {
Ok(metadata) => allowed_for_type(metadata.file_type()),
Err(_) => PRIVATE_FILE_MODE,
}
}
pub fn wide_entry_warning(path: &Path, holds: &str, mode: u32, allowed: u32) -> String {
let path = path.display();
format!(
"[capsule-runtime] warning[{W_SEC_028}]: {path} holds {holds} and is mode {:04o}, which \
other accounts on this host can read; run `chmod {:o} {path}` ({})",
mode & 0o777,
allowed,
security_warning_link(W_SEC_028)
)
}
pub fn warn_on_wide_credential_file(path: &Path, name: &str, mode: u32) {
if let Some(warning) = wide_credential_file_warning(path, name, mode) {
crate::runtime_err!("{warning}");
}
}
fn wide_credential_file_warning(path: &Path, name: &str, mode: u32) -> Option<String> {
if !is_wider_than(mode, PRIVATE_FILE_MODE) {
return None;
}
let path = path.display();
Some(format!(
"[capsule-runtime] warning[{W_SEC_028}]: the credential credentials.{name} is \
read from {path}, which is mode {:04o} and readable by other accounts on this host; run \
`chmod 600 {path}` ({})",
mode & 0o777,
security_warning_link(W_SEC_028)
))
}
#[cfg(test)]
pub(crate) fn run_with_home(test: &str, home: &Path) {
let exe = std::env::current_exe().expect("test binary path");
let output = std::process::Command::new(exe)
.args([
test,
"--exact",
"--ignored",
"--test-threads=1",
"--nocapture",
])
.env("HOME", home)
.env(INNER_HOME_ENV, "1")
.output()
.expect("re-exec the test binary with a scratch HOME");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"{test} failed\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}"
);
assert!(
stdout.contains("1 passed"),
"{test} was not selected\n--- stdout ---\n{stdout}"
);
}
#[cfg(test)]
pub(crate) const INNER_HOME_ENV: &str = "MURMUR_TEST_INNER_HOME";
#[cfg(test)]
pub(crate) fn in_scratch_home() -> bool {
std::env::var_os(INNER_HOME_ENV).is_some()
}
#[cfg(test)]
pub(crate) fn mode_of(path: &Path) -> u32 {
use std::os::unix::fs::PermissionsExt;
std::fs::symlink_metadata(path)
.unwrap_or_else(|err| panic!("{}: {err}", path.display()))
.permissions()
.mode()
& 0o777
}
#[cfg(test)]
pub(crate) fn wide_dir(path: &Path, mode: u32) {
use std::os::unix::fs::PermissionsExt;
std::fs::create_dir_all(path).unwrap();
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode)).unwrap();
}
#[cfg(test)]
pub(crate) fn wide_file(path: &Path, contents: &str, mode: u32) {
use std::os::unix::fs::PermissionsExt;
std::fs::write(path, contents).unwrap();
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode)).unwrap();
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
#[test]
fn murmur_home_is_wider_than_truth_table() {
for (mode, allowed, wide) in [
(0o600, 0o600, false),
(0o644, 0o600, true),
(0o640, 0o600, true),
(0o700, 0o700, false),
(0o711, 0o700, true),
(0o400, 0o600, false),
] {
assert_eq!(
is_wider_than(mode, allowed),
wide,
"{mode:04o} against {allowed:04o}"
);
}
}
#[test]
fn ensure_murmur_home_creates_and_tightens_the_home_directory() {
let wide = tempfile::tempdir().unwrap();
wide_dir(&wide.path().join(".murmur"), 0o755);
run_with_home(
"murmur_home::tests::inner_ensure_murmur_home_holds_the_home_at_0700",
wide.path(),
);
assert_eq!(mode_of(&wide.path().join(".murmur")), 0o700);
let missing = tempfile::tempdir().unwrap();
run_with_home(
"murmur_home::tests::inner_ensure_murmur_home_holds_the_home_at_0700",
missing.path(),
);
assert_eq!(mode_of(&missing.path().join(".murmur")), 0o700);
let closed = tempfile::tempdir().unwrap();
wide_dir(&closed.path().join(".murmur"), 0o555);
run_with_home(
"murmur_home::tests::inner_ensure_murmur_home_holds_the_home_at_0700",
closed.path(),
);
assert_eq!(
mode_of(&closed.path().join(".murmur")),
0o500,
"an owner-closed home loses group and other bits and is not reopened"
);
}
#[test]
#[ignore = "run by ensure_murmur_home_creates_and_tightens_the_home_directory"]
fn inner_ensure_murmur_home_holds_the_home_at_0700() {
if !in_scratch_home() {
return;
}
let home = ensure_murmur_home().unwrap();
assert!(home.ends_with(".murmur"));
assert_eq!(mode_of(&home) & 0o077, 0);
}
#[test]
fn murmur_home_write_private_file_replaces_a_wide_file_owner_only() {
use std::os::unix::fs::MetadataExt;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.yaml");
wide_file(&path, "old", 0o644);
let before = std::fs::metadata(&path).unwrap().ino();
write_private_file(&path, b"new").unwrap();
assert_eq!(std::fs::read_to_string(&path).unwrap(), "new");
assert_eq!(mode_of(&path), 0o600);
assert_ne!(std::fs::metadata(&path).unwrap().ino(), before);
}
fn run_without_home(test: &str) {
let exe = std::env::current_exe().expect("test binary path");
let output = std::process::Command::new(exe)
.args([
test,
"--exact",
"--ignored",
"--test-threads=1",
"--nocapture",
])
.env_remove("HOME")
.env(INNER_HOME_ENV, "1")
.output()
.expect("re-exec the test binary with no HOME");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success() && stdout.contains("1 passed"),
"{test} failed\n{stdout}\n{}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn an_unusable_home_names_home_and_its_cause() {
run_without_home("murmur_home::tests::an_unset_home_inner");
let relative = std::path::Path::new("relative-home");
run_with_home("murmur_home::tests::a_relative_home_inner", relative);
let closed = tempfile::tempdir().unwrap();
run_with_home("murmur_home::tests::a_closed_home_inner", closed.path());
}
#[test]
#[ignore = "run by an_unusable_home_names_home_and_its_cause"]
fn an_unset_home_inner() {
if !in_scratch_home() {
return;
}
let error = usable_murmur_home().unwrap_err();
assert_eq!(error, HomeUnusable::NotSet);
assert_eq!(
error.to_string(),
"HOME is not set, so there is no ~/.murmur to hold this session's runtime and \
records directories"
);
assert!(crate::runtime_dir::sessions_dir().is_err());
assert!(crate::records_dir::records_group(Path::new("/w")).is_err());
}
#[test]
#[ignore = "run by an_unusable_home_names_home_and_its_cause"]
fn a_relative_home_inner() {
if !in_scratch_home() {
return;
}
let error = usable_murmur_home().unwrap_err();
assert_eq!(
error.to_string(),
"HOME is 'relative-home', which is not an absolute path, so there is no ~/.murmur to \
hold this session's runtime and records directories"
);
assert!(!Path::new("relative-home").exists());
}
#[test]
#[ignore = "run by an_unusable_home_names_home_and_its_cause"]
fn a_closed_home_inner() {
if !in_scratch_home() {
return;
}
let home = PathBuf::from(std::env::var_os("HOME").unwrap());
let murmur = home.join(".murmur");
std::fs::create_dir(&murmur).unwrap();
std::fs::set_permissions(&murmur, std::fs::Permissions::from_mode(0o500)).unwrap();
if std::fs::write(murmur.join("probe"), b"x").is_err() {
let error = usable_murmur_home().unwrap_err();
let text = error.to_string();
assert!(
text.starts_with(&format!(
"~/.murmur ({}) cannot hold this session's runtime and records directories: ",
murmur.display()
)),
"{text}"
);
assert!(text.contains("cannot be written"), "{text}");
}
std::fs::set_permissions(&murmur, std::fs::Permissions::from_mode(0o700)).unwrap();
assert_eq!(usable_murmur_home().unwrap(), murmur);
}
#[test]
fn murmur_home_audit_reports_absent_entries_in_kind_order() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join(".murmur");
let reports = audit_murmur_home(&home);
assert_eq!(reports.len(), 18);
assert_eq!(reports[0].kind, HomeEntryKind::Home);
assert_eq!(reports[1].kind, HomeEntryKind::Config);
assert_eq!(reports[10].kind, HomeEntryKind::CompiledForms);
assert_eq!(reports[11].kind, HomeEntryKind::HostFacts);
assert_eq!(reports[11].path, home.join("host_facts"));
assert_eq!(reports[12].kind, HomeEntryKind::BinaryCache);
assert_eq!(reports[13].kind, HomeEntryKind::Formations);
assert_eq!(reports[14].kind, HomeEntryKind::Sessions);
assert_eq!(reports[15].kind, HomeEntryKind::Records);
assert_eq!(reports[16].kind, HomeEntryKind::Programs);
assert_eq!(reports[16].path, home.join("programs"));
assert_eq!(reports[17].kind, HomeEntryKind::Keys);
assert_eq!(reports[17].path, home.join("keys"));
assert!(reports
.iter()
.all(|report| report.state == HomeEntryState::Absent));
}
#[test]
fn murmur_home_programs_entry_names_the_store_and_allows_owner_executables() {
use std::os::unix::fs::PermissionsExt;
assert_eq!(HomeEntryKind::Programs.file_name(), Some("programs"));
assert_eq!(HomeEntryKind::Programs.holds(), "pinned program downloads");
assert_eq!(HomeEntryKind::Programs.kept(), None);
assert!(HomeEntryKind::Programs.expected_private());
assert!(!HomeEntryKind::Programs.safe_to_delete());
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join(".murmur");
let files = home.join("programs/abc/files");
std::fs::create_dir_all(&files).unwrap();
for path in [
home.clone(),
home.join("programs"),
home.join("programs/abc"),
files.clone(),
] {
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).unwrap();
}
for (name, mode) in [("prog", 0o700), ("data.txt", 0o600)] {
std::fs::write(files.join(name), "x").unwrap();
std::fs::set_permissions(files.join(name), std::fs::Permissions::from_mode(mode))
.unwrap();
}
std::os::unix::fs::symlink("../files/prog", home.join("programs/abc/prog-link")).unwrap();
let programs = |reports: Vec<HomeEntryReport>| {
reports
.into_iter()
.find(|report| report.kind == HomeEntryKind::Programs)
.unwrap()
.state
};
assert!(matches!(
programs(audit_murmur_home(&home)),
HomeEntryState::Present { wide: false, wide_descendants, .. } if wide_descendants.is_empty()
));
std::fs::set_permissions(
files.join("data.txt"),
std::fs::Permissions::from_mode(0o644),
)
.unwrap();
assert!(matches!(
programs(audit_murmur_home(&home)),
HomeEntryState::Present { wide_descendants, .. }
if wide_descendants.len() == 1 && wide_descendants[0].path == files.join("data.txt")
));
}
#[test]
fn murmur_home_kinds_say_how_formations_are_kept_and_only_the_caches_are_safe_to_delete() {
let every = HomeEntryKind::KNOWN
.into_iter()
.chain([HomeEntryKind::Home, HomeEntryKind::Unrecognised("x".into())]);
for kind in every {
assert_eq!(
kind.kept().is_some(),
matches!(
kind,
HomeEntryKind::Formations | HomeEntryKind::Sessions | HomeEntryKind::Records
),
"{kind:?}"
);
assert_eq!(
kind.safe_to_delete(),
matches!(
kind,
HomeEntryKind::CompiledForms | HomeEntryKind::HostFacts
),
"{kind:?}"
);
if kind.safe_to_delete() {
assert!(kind.expected_private(), "{kind:?}");
}
}
assert_eq!(
HomeEntryKind::HostFacts.holds(),
"facts about allowlisted host binaries"
);
assert_eq!(
HomeEntryKind::Formations.kept(),
Some(
"each kept until its formation has ended and the entry member's trace.retain \
expires it"
)
);
}
#[test]
fn owned_within_refuses_an_owner_other_than_the_expected_uid() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("form.cwasm");
std::fs::write(&path, b"form").unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
let metadata = std::fs::metadata(&path).unwrap();
assert!(owned_within(&metadata, current_euid(), 0o600));
assert!(!owned_within(
&metadata,
current_euid().wrapping_add(1),
0o600
));
}
#[test]
fn held_private_accepts_only_an_owner_only_directory_reached_without_a_symlink() {
let temp = tempfile::tempdir().unwrap();
let dir = temp.path().join("forms");
std::fs::create_dir(&dir).unwrap();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap();
assert!(held_private(&dir));
let link = temp.path().join("link");
std::os::unix::fs::symlink(&dir, &link).unwrap();
assert!(!held_private(&link));
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o750)).unwrap();
assert!(!held_private(&dir));
}
#[test]
fn a_directory_beneath_the_capsule_workdir_is_reachable_from_it() {
let temp = tempfile::tempdir().unwrap();
let workdir = temp.path().join("work");
let inside = workdir.join(".murmur");
let outside = temp.path().join("home");
std::fs::create_dir_all(&inside).unwrap();
std::fs::create_dir(&outside).unwrap();
assert!(reachable_from(&workdir, &workdir));
assert!(reachable_from(&inside, &workdir));
assert!(!reachable_from(&outside, &workdir));
assert!(!reachable_from(&outside, &temp.path().join("not-created")));
assert!(reachable_from(&temp.path().join("missing"), &workdir));
}
#[test]
fn murmur_home_audit_knows_formations_as_owner_only() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join(".murmur");
wide_dir(&home, 0o700);
wide_dir(&home.join("formations"), 0o700);
wide_dir(&home.join("formations/frm_x"), 0o700);
wide_dir(&home.join("formations/frm_x/worker"), 0o755);
wide_dir(&home.join("formations/frm_x/worker/.murmur/ses_1"), 0o775);
wide_file(
&home.join("formations/frm_x/worker/.murmur/ses_1/trace.jsonl"),
"{}\n",
0o664,
);
wide_file(&home.join("formations/frm_x/worker/task.md"), "t", 0o664);
let report = audit_murmur_home(&home)
.into_iter()
.find(|report| report.kind == HomeEntryKind::Formations)
.unwrap();
assert_eq!(report.path, home.join("formations"));
assert_eq!(
report.kind.holds(),
"formation members' working directories"
);
assert!(report.kind.expected_private());
assert!(!report.kind.safe_to_delete());
match report.state {
HomeEntryState::Present {
wide,
wide_descendants,
..
} => {
assert!(!wide);
assert_eq!(wide_descendants.len(), 1);
assert!(wide_descendants[0].path.ends_with("frm_x/worker"));
}
other => panic!("{other:?}"),
}
}
#[test]
fn murmur_home_audit_flags_wide_private_entries_and_never_non_private_ones() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join(".murmur");
wide_dir(&home, 0o755);
wide_file(&home.join("config.yaml"), "credentials: {}\n", 0o644);
wide_dir(&home.join("artifacts"), 0o777);
wide_file(&home.join("artifacts/loose"), "x", 0o666);
wide_dir(&home.join("spend"), 0o700);
wide_file(&home.join("spend/2026-09-13.jsonl"), "", 0o600);
wide_file(&home.join("nexus-config.json"), "{}", 0o644);
let reports = audit_murmur_home(&home);
let find = |kind: &HomeEntryKind| {
reports
.iter()
.find(|report| &report.kind == kind)
.unwrap()
.state
.clone()
};
let wide = |state: HomeEntryState| match state {
HomeEntryState::Present {
wide,
wide_descendants,
..
} => (wide, wide_descendants.len()),
other => panic!("{other:?}"),
};
assert_eq!(wide(find(&HomeEntryKind::Home)), (true, 0));
assert_eq!(wide(find(&HomeEntryKind::Config)), (true, 0));
assert_eq!(wide(find(&HomeEntryKind::Artifacts)), (false, 0));
assert_eq!(wide(find(&HomeEntryKind::Spend)), (false, 0));
let last = reports.last().unwrap();
assert_eq!(
last.kind,
HomeEntryKind::Unrecognised("nexus-config.json".to_string())
);
assert_eq!(wide(last.state.clone()), (false, 0));
assert_eq!(mode_of(&home), 0o755, "the audit changes nothing");
}
#[test]
fn murmur_home_audit_does_not_follow_symlinks() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join(".murmur");
wide_dir(&home.join("state"), 0o700);
let outside = dir.path().join("outside");
wide_dir(&outside, 0o777);
wide_file(&outside.join("secret"), "x", 0o666);
std::os::unix::fs::symlink(&outside, home.join("state/link")).unwrap();
let reports = audit_murmur_home(&home);
let state = &reports
.iter()
.find(|report| report.kind == HomeEntryKind::State)
.unwrap()
.state;
assert!(
matches!(state, HomeEntryState::Present { wide: false, wide_descendants, .. } if wide_descendants.is_empty()),
"{state:?}"
);
}
#[test]
fn murmur_home_audit_caps_wide_descendants_and_counts_the_rest() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join(".murmur");
let keys = home.join("deploy_keys");
wide_dir(&keys, 0o700);
for index in 0..25 {
wide_file(&keys.join(format!("key_{index:02}")), "x", 0o644);
}
let reports = audit_murmur_home(&home);
let state = &reports
.iter()
.find(|report| report.kind == HomeEntryKind::DeployKeys)
.unwrap()
.state;
match state {
HomeEntryState::Present {
wide,
wide_descendants,
wide_descendants_omitted,
..
} => {
assert!(!wide);
assert_eq!(wide_descendants.len(), WIDE_DESCENDANT_CAP);
assert_eq!(*wide_descendants_omitted, 5);
assert!(wide_descendants
.iter()
.all(|entry| entry.mode == 0o644 && entry.allowed == 0o600));
}
other => panic!("{other:?}"),
}
}
#[test]
fn murmur_home_wide_credential_file_warning_names_the_path_and_mode_only() {
let path = Path::new("/home/someone/.murmur/config.yaml");
assert_eq!(
wide_credential_file_warning(path, "PROVIDER_KEY", 0o600),
None
);
assert_eq!(
wide_credential_file_warning(path, "PROVIDER_KEY", 0o100600),
None
);
let warning = wide_credential_file_warning(path, "PROVIDER_KEY", 0o100644).unwrap();
assert!(warning.contains("warning[W-SEC-028]"), "{warning}");
assert!(warning.contains("credentials.PROVIDER_KEY"), "{warning}");
assert!(
warning.contains("/home/someone/.murmur/config.yaml"),
"{warning}"
);
assert!(warning.contains("mode 0644"), "{warning}");
assert!(
warning.contains("`chmod 600 /home/someone/.murmur/config.yaml`"),
"{warning}"
);
assert!(warning.ends_with("#w-sec-028)"), "{warning}");
}
#[test]
fn murmur_home_wide_entry_warning_prints_the_chmod_for_the_allowed_mode() {
let warning = wide_entry_warning(Path::new("/h/.murmur"), "the murmur home", 0o755, 0o700);
assert_eq!(
warning,
"[capsule-runtime] warning[W-SEC-028]: /h/.murmur holds the murmur home and is mode \
0755, which other accounts on this host can read; run `chmod 700 /h/.murmur` \
(https://docs.murmur.nexus/reference/diagnostics/#w-sec-028)"
);
}
}