use std::path::Path;
use super::landlock::Rules;
use super::patterns::is_below;
use crate::context::path_string;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Grant {
pub path: String,
pub shown: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Exposure {
Inside(String),
Holds(String),
}
pub fn within(path: &str, dir: &str, case_insensitive: bool) -> bool {
let fold = |p: &str| {
let p = trimmed(p);
if case_insensitive {
p.to_lowercase()
} else {
p.to_owned()
}
};
let (path, dir) = (fold(path), fold(dir));
path == dir || is_below(&path, &dir)
}
fn trimmed(path: &str) -> &str {
let trimmed = path.trim_end_matches('/');
if trimmed.is_empty() && path.starts_with('/') {
"/"
} else {
trimmed
}
}
fn depth(path: &str) -> usize {
path.split('/').filter(|c| !c.is_empty()).count()
}
pub fn exposing(state: &str, grants: &[Grant], case_insensitive: bool) -> Option<Exposure> {
let inside_state = grants.iter().find(|g| {
g.shown
&& within(&g.path, state, case_insensitive)
&& !within(state, &g.path, case_insensitive)
});
if let Some(grant) = inside_state {
return Some(Exposure::Holds(grant.path.clone()));
}
let holder = grants
.iter()
.filter(|g| within(state, &g.path, case_insensitive))
.max_by_key(|g| (depth(&g.path), !g.shown))?;
holder.shown.then(|| Exposure::Inside(holder.path.clone()))
}
pub fn grants(rules: &Rules, empty: Option<&str>) -> Vec<Grant> {
rules
.read
.iter()
.chain(&rules.write)
.map(|granted| {
let path = resolved(Path::new(granted));
let shown = empty != Some(path.as_str());
Grant { path, shown }
})
.collect()
}
pub fn resolved(path: &Path) -> String {
for ancestor in path.ancestors() {
if let Ok(real) = std::fs::canonicalize(ancestor) {
let rest = path.strip_prefix(ancestor).unwrap_or(Path::new(""));
let real = if rest.as_os_str().is_empty() {
real
} else {
real.join(rest)
};
return path_string(&real);
}
}
path_string(path)
}
pub fn find(state: &Path, rules: &Rules, empty: Option<&str>) -> Option<(String, Exposure)> {
let state = resolved(state);
let ci = crate::context::CASE_INSENSITIVE_PATHS;
let exposure = exposing(&state, &grants(rules, empty), ci)?;
Some((state, exposure))
}
pub fn refusal(
state: &Path,
rules: &Rules,
tmpdir: Option<&str>,
isolated: bool,
) -> Option<String> {
let (state_dir, exposure) = find(state, rules, tmpdir.filter(|_| isolated))?;
let isolate_hides = !isolated && find(state, rules, tmpdir).is_none();
let fix = if isolate_hides {
", or use `openmoat run --isolate`, which hides it under an empty temp directory"
} else {
""
};
let cause = "Landlock cannot keep the audit log and the policy lock from commands inside a \
granted directory";
Some(match exposure {
Exposure::Inside(root) => format!(
"OpenMoat's state directory {state_dir} is inside {root}, which the sandbox grants; \
{cause}, so `openmoat run` refuses to start. Move MOAT_HOME out of {root}{fix}"
),
Exposure::Holds(path) => format!(
"{path}, which the sandbox grants, is inside OpenMoat's state directory {state_dir}; \
{cause}, so `openmoat run` refuses to start. Grant a path outside it, or move MOAT_HOME \
elsewhere"
),
})
}
#[cfg(all(test, unix))]
mod tests;