use crate::operation::Operation;
use crate::state::path::StatResult;
use crate::state::subject::ResolvedSubject;
use crate::state::{Probe, SystemState};
use super::LayerResult;
#[must_use]
pub fn check_dac(state: &SystemState) -> LayerResult {
if state.operation.is_metadata() {
return LayerResult::Pass {
detail: "metadata ops bypass DAC mode-bit checks".into(),
warnings: vec![],
};
}
if state.operation == Operation::Stat {
return LayerResult::Pass {
detail: "stat requires only traversal, no DAC check".into(),
warnings: vec![],
};
}
if state.operation.checks_parent() {
return check_parent_dac(state);
}
check_target_dac(state)
}
fn check_target_dac(state: &SystemState) -> LayerResult {
let Some(idx) = state.operation.target_component(state.walk.len()) else {
return degraded("path walk too short to determine target");
};
let comp = &state.walk[idx];
let stat = match &comp.stat {
Probe::Known(s) => s,
Probe::Unknown => return degraded("target stat unknown"),
Probe::Inaccessible => return degraded("target stat inaccessible"),
};
let needed = required_bit(state.operation);
let has_perm = check_bit(stat, &state.subject, needed);
format_result(stat, &state.subject, has_perm, needed)
}
fn check_parent_dac(state: &SystemState) -> LayerResult {
let Some(parent_idx) = state.operation.target_component(state.walk.len()) else {
return degraded("path walk too short to determine parent");
};
let parent = &state.walk[parent_idx];
let stat = match &parent.stat {
Probe::Known(s) => s,
Probe::Unknown => return degraded("parent stat unknown"),
Probe::Inaccessible => return degraded("parent stat inaccessible"),
};
let has_wx = check_bit(stat, &state.subject, 'w') && check_bit(stat, &state.subject, 'x');
if !has_wx {
return format_parent_fail(stat, &state.subject, state.operation);
}
if state.operation == Operation::Delete {
return check_sticky_bit(state, stat, parent_idx);
}
LayerResult::Pass {
detail: format!("parent has w+x for {}", class_name(stat, &state.subject)),
warnings: vec![],
}
}
fn check_sticky_bit(
state: &SystemState,
parent_stat: &StatResult,
_parent_idx: usize,
) -> LayerResult {
if parent_stat.mode & 0o1000 == 0 {
return LayerResult::Pass {
detail: format!(
"parent has w+x for {}, no sticky bit",
class_name(parent_stat, &state.subject)
),
warnings: vec![],
};
}
let target_idx = state.walk.len() - 1;
let target = &state.walk[target_idx];
let Probe::Known(target_stat) = &target.stat else {
return degraded("target stat unavailable for sticky bit check");
};
let is_target_owner = target_stat.uid == state.subject.uid;
let is_parent_owner = parent_stat.uid == state.subject.uid;
if is_target_owner || is_parent_owner {
LayerResult::Pass {
detail: "sticky bit set, but subject owns target or parent".into(),
warnings: vec![],
}
} else {
LayerResult::Fail {
detail: format!(
"sticky bit set on parent, subject (uid={}) owns neither target (uid={}) nor parent (uid={})",
state.subject.uid, target_stat.uid, parent_stat.uid
),
component_index: Some(target_idx),
}
}
}
fn required_bit(op: Operation) -> char {
match op {
Operation::Read | Operation::Stat => 'r',
Operation::Write | Operation::Delete | Operation::Create => 'w',
Operation::Execute => 'x',
Operation::Chmod
| Operation::ChownUid
| Operation::ChownGid
| Operation::SetXattr { .. } => {
unreachable!("is_metadata() guard must precede required_bit()")
}
}
}
fn check_bit(stat: &StatResult, subject: &ResolvedSubject, bit: char) -> bool {
let shift = match bit {
'r' => 2,
'w' => 1,
'x' => 0,
other => unreachable!("check_bit called with invalid permission char: {other:?}"),
};
if stat.uid == subject.uid {
(stat.mode >> (6 + shift)) & 1 != 0
} else if subject.in_group(stat.gid) {
(stat.mode >> (3 + shift)) & 1 != 0
} else {
(stat.mode >> shift) & 1 != 0
}
}
fn format_result(
stat: &StatResult,
subject: &ResolvedSubject,
has_perm: bool,
bit: char,
) -> LayerResult {
let class = class_name(stat, subject);
let mode_str = format!("{:04o}", stat.mode & 0o7777);
if has_perm {
LayerResult::Pass {
detail: format!("mode {mode_str}: {class} has {bit} permission"),
warnings: vec![],
}
} else {
LayerResult::Fail {
detail: format!("mode {mode_str}: {class} has no {bit} permission"),
component_index: None,
}
}
}
fn format_parent_fail(stat: &StatResult, subject: &ResolvedSubject, op: Operation) -> LayerResult {
let class = class_name(stat, subject);
let mode_str = format!("{:04o}", stat.mode & 0o7777);
LayerResult::Fail {
detail: format!("mode {mode_str}: {class} lacks w+x on parent for {op:?}"),
component_index: None,
}
}
fn 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(reason: &str) -> LayerResult {
LayerResult::Degraded {
reason: reason.to_owned(),
}
}
#[cfg(test)]
#[path = "dac_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "dac_caps_tests.rs"]
mod caps_tests;