use crate::state::acl::PosixAcl;
use crate::state::path::{PathComponent, StatResult};
use crate::state::subject::ResolvedSubject;
use crate::state::{Probe, SystemState};
use super::LayerResult;
#[must_use]
pub fn check_traversal(state: &SystemState) -> LayerResult {
let ancestors = ancestor_count(&state.walk);
if ancestors == 0 {
return LayerResult::Pass {
detail: "no ancestors to check (single component walk)".into(),
warnings: vec![],
};
}
for idx in 0..ancestors {
let result = check_ancestor(&state.walk[idx], idx, &state.subject);
if !result.is_pass() {
return result;
}
}
LayerResult::Pass {
detail: format!("all {ancestors} ancestors allow traverse"),
warnings: vec![],
}
}
fn ancestor_count(walk: &[PathComponent]) -> usize {
walk.len().saturating_sub(1)
}
fn check_ancestor(comp: &PathComponent, idx: usize, subject: &ResolvedSubject) -> LayerResult {
let stat = match &comp.stat {
Probe::Known(s) => s,
Probe::Inaccessible => return degraded_for(comp, "stat inaccessible"),
Probe::Unknown => return degraded_for(comp, "stat unknown"),
};
if subject.uid == 0 {
return LayerResult::Pass {
detail: format!("{}: root bypasses traverse check", comp.path.display()),
warnings: vec![],
};
}
if has_extended_acl(&comp.acl) {
return check_acl_execute(comp, stat, subject, idx);
}
check_mode_execute(comp, stat, subject, idx)
}
fn check_mode_execute(
comp: &PathComponent,
stat: &StatResult,
subject: &ResolvedSubject,
idx: usize,
) -> LayerResult {
let has_x = if stat.uid == subject.uid {
(stat.mode >> 6) & 1 != 0
} else if subject.in_group(stat.gid) {
(stat.mode >> 3) & 1 != 0
} else {
stat.mode & 1 != 0
};
if has_x {
LayerResult::Pass {
detail: format!("{}: execute permitted", comp.path.display()),
warnings: vec![],
}
} else {
let class = dac_class_name(stat, subject);
LayerResult::Fail {
detail: format!("{}: {class} has no execute permission", comp.path.display()),
component_index: Some(idx),
}
}
}
fn check_acl_execute(
comp: &PathComponent,
stat: &StatResult,
subject: &ResolvedSubject,
idx: usize,
) -> LayerResult {
let Probe::Known(acl) = &comp.acl else {
return degraded_for(comp, "ACL inaccessible for traversal");
};
let has_x = acl_grants_execute(acl, stat, subject);
if has_x {
LayerResult::Pass {
detail: format!("{}: ACL grants execute", comp.path.display()),
warnings: vec![],
}
} else {
LayerResult::Fail {
detail: format!("{}: ACL denies execute permission", comp.path.display()),
component_index: Some(idx),
}
}
}
pub(crate) fn acl_grants_execute(
acl: &PosixAcl,
stat: &StatResult,
subject: &ResolvedSubject,
) -> bool {
if stat.uid == subject.uid {
return acl.user_obj().is_some_and(|e| e.perms.execute);
}
if let Some(entry) = acl.named_user(subject.uid) {
return acl.effective_perms(entry).execute;
}
let group_exec = best_group_execute(acl, stat, subject);
if let Some(has_x) = group_exec {
return has_x;
}
acl.other().is_some_and(|e| e.perms.execute)
}
fn best_group_execute(
acl: &PosixAcl,
stat: &StatResult,
subject: &ResolvedSubject,
) -> Option<bool> {
let mut matched = false;
let mut best = false;
if subject.in_group(stat.gid) {
if let Some(entry) = acl.group_obj() {
let eff = acl.effective_perms(entry);
best = best || eff.execute;
matched = true;
}
}
for entry in acl.named_groups() {
if let Some(gid) = entry.qualifier {
if subject.in_group(gid) {
let eff = acl.effective_perms(entry);
best = best || eff.execute;
matched = true;
}
}
}
matched.then_some(best)
}
fn has_extended_acl(acl: &Probe<PosixAcl>) -> bool {
matches!(acl, Probe::Known(a) if a.has_extended_entries())
}
fn dac_class_name(stat: &StatResult, subject: &ResolvedSubject) -> &'static str {
if stat.uid == subject.uid {
"owner"
} else if subject.in_group(stat.gid) {
"group"
} else {
"other"
}
}
fn degraded_for(comp: &PathComponent, reason: &str) -> LayerResult {
LayerResult::Degraded {
reason: format!("{}: {reason}", comp.path.display()),
}
}
#[cfg(test)]
#[path = "traversal_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "traversal_edge_tests.rs"]
mod edge_tests;