use crate::operation::{MetadataParams, Operation, XattrNamespace};
use crate::state::path::StatResult;
use crate::state::{Probe, SystemState};
use super::caps::{has_cap, CAP_CHOWN, CAP_FOWNER, CAP_FSETID, CAP_SYS_ADMIN};
use super::LayerResult;
#[must_use]
pub fn check_metadata(state: &SystemState, params: &MetadataParams) -> LayerResult {
match state.operation {
Operation::Chmod => check_chmod(state, params),
Operation::ChownUid => check_chown_uid(state),
Operation::ChownGid => check_chown_gid(state, params),
Operation::SetXattr { namespace } => check_setxattr(state, namespace),
Operation::Read
| Operation::Write
| Operation::Execute
| Operation::Delete
| Operation::Create
| Operation::Stat => LayerResult::Pass {
detail: "not a metadata operation".into(),
warnings: vec![],
},
}
}
fn target_stat(state: &SystemState) -> Result<&StatResult, LayerResult> {
match state.walk.last().map(|c| &c.stat) {
Some(Probe::Known(s)) => Ok(s),
Some(Probe::Inaccessible) => Err(LayerResult::Degraded {
reason: "target stat inaccessible".into(),
}),
_ => Err(LayerResult::Degraded {
reason: "target stat unknown".into(),
}),
}
}
fn effective_caps(state: &SystemState) -> u64 {
match state.subject.capabilities {
Probe::Known(bitmask) => bitmask,
Probe::Unknown | Probe::Inaccessible => 0,
}
}
fn check_chmod(state: &SystemState, _params: &MetadataParams) -> LayerResult {
let stat = match target_stat(state) {
Ok(s) => s,
Err(d) => return d,
};
let caps = effective_caps(state);
if stat.uid == state.subject.uid {
return LayerResult::Pass {
detail: "subject is file owner".into(),
warnings: vec![],
};
}
if has_cap(caps, CAP_FOWNER) {
return LayerResult::Pass {
detail: "CAP_FOWNER bypasses ownership check".into(),
warnings: fowner_setgid_warning(state, stat, caps),
};
}
LayerResult::Fail {
detail: format!(
"subject (uid={}) is not the file owner (uid={}) and lacks CAP_FOWNER",
state.subject.uid, stat.uid
),
component_index: None,
}
}
fn fowner_setgid_warning(state: &SystemState, stat: &StatResult, caps: u64) -> Vec<String> {
if has_cap(caps, CAP_FSETID) || state.subject.in_group(stat.gid) {
return vec![];
}
vec![
"CAP_FOWNER bypasses ownership check; kernel will strip setgid bit \
if caller lacks CAP_FSETID and is not in file's group"
.into(),
]
}
fn check_chown_uid(state: &SystemState) -> LayerResult {
let caps = effective_caps(state);
if has_cap(caps, CAP_CHOWN) {
return LayerResult::Pass {
detail: "CAP_CHOWN permits UID change".into(),
warnings: vec![],
};
}
LayerResult::Fail {
detail: "changing file UID requires CAP_CHOWN".into(),
component_index: None,
}
}
fn check_chown_gid(state: &SystemState, params: &MetadataParams) -> LayerResult {
let Some(new_gid) = params.new_gid else {
return LayerResult::Degraded {
reason: "new_gid required for ChownGid check — pass --new-gid".into(),
};
};
let caps = effective_caps(state);
if has_cap(caps, CAP_CHOWN) {
return LayerResult::Pass {
detail: "CAP_CHOWN permits GID change".into(),
warnings: vec![],
};
}
let stat = match target_stat(state) {
Ok(s) => s,
Err(d) => return d,
};
if stat.uid != state.subject.uid {
return LayerResult::Fail {
detail: format!(
"subject (uid={}) is not the file owner (uid={}) and lacks CAP_CHOWN",
state.subject.uid, stat.uid
),
component_index: None,
};
}
if state.subject.in_group(new_gid) {
return LayerResult::Pass {
detail: format!("subject is file owner and member of target group (gid={new_gid})"),
warnings: vec![],
};
}
LayerResult::Fail {
detail: format!(
"subject is file owner but is not a member of group (gid={new_gid}) and lacks CAP_CHOWN"
),
component_index: None,
}
}
fn check_setxattr(state: &SystemState, namespace: XattrNamespace) -> LayerResult {
match namespace {
XattrNamespace::User | XattrNamespace::SystemPosixAcl => check_owner_or_fowner(state),
XattrNamespace::Trusted | XattrNamespace::Security => {
check_cap_required(state, CAP_SYS_ADMIN, "CAP_SYS_ADMIN")
}
}
}
fn check_owner_or_fowner(state: &SystemState) -> LayerResult {
let stat = match target_stat(state) {
Ok(s) => s,
Err(d) => return d,
};
let caps = effective_caps(state);
if stat.uid == state.subject.uid || has_cap(caps, CAP_FOWNER) {
LayerResult::Pass {
detail: "subject is file owner or holds CAP_FOWNER".into(),
warnings: vec![],
}
} else {
LayerResult::Fail {
detail: format!(
"subject (uid={}) is not the file owner (uid={}) and lacks CAP_FOWNER",
state.subject.uid, stat.uid
),
component_index: None,
}
}
}
fn check_cap_required(state: &SystemState, cap: u64, cap_name: &str) -> LayerResult {
let caps = effective_caps(state);
if has_cap(caps, cap) {
LayerResult::Pass {
detail: format!("{cap_name} permits xattr write"),
warnings: vec![],
}
} else {
LayerResult::Fail {
detail: format!("xattr namespace requires {cap_name}"),
component_index: None,
}
}
}
#[cfg(test)]
#[path = "metadata_tests.rs"]
mod tests;