use std::path::Path;
use whyno_core::checks::CheckReport;
use whyno_core::operation::Operation;
use whyno_core::state::SystemState;
const MIN_KERNEL_MAJOR: u32 = 5;
const MIN_KERNEL_MINOR: u32 = 8;
pub fn maybe_cross_check(state: &SystemState, report: &CheckReport) {
if !subject_is_self(state) {
return;
}
if !kernel_supports_faccessat2() {
return;
}
let Some(path) = target_path(state) else {
return;
};
run_cross_check(path, state.operation, report);
}
fn subject_is_self(state: &SystemState) -> bool {
state.subject.uid == nix::unistd::geteuid().as_raw()
}
fn kernel_supports_faccessat2() -> bool {
let Ok(uname) = nix::sys::utsname::uname() else {
return false;
};
let release = uname.release().to_string_lossy();
parse_kernel_version(&release).is_some_and(|(major, minor)| meets_minimum(major, minor))
}
fn parse_kernel_version(release: &str) -> Option<(u32, u32)> {
let mut parts = release.split(|c: char| !c.is_ascii_digit());
let major = parts.next()?.parse::<u32>().ok()?;
let minor = parts.next()?.parse::<u32>().ok()?;
Some((major, minor))
}
fn meets_minimum(major: u32, minor: u32) -> bool {
(major, minor) >= (MIN_KERNEL_MAJOR, MIN_KERNEL_MINOR)
}
fn target_path(state: &SystemState) -> Option<&Path> {
state.walk.last().map(|c| c.path.as_path())
}
fn operation_to_access_flags(op: Operation) -> nix::unistd::AccessFlags {
match op {
Operation::Read => nix::unistd::AccessFlags::R_OK,
Operation::Write => nix::unistd::AccessFlags::W_OK,
Operation::Execute => nix::unistd::AccessFlags::X_OK,
Operation::Delete | Operation::Create => {
nix::unistd::AccessFlags::W_OK | nix::unistd::AccessFlags::X_OK
}
_ => nix::unistd::AccessFlags::F_OK,
}
}
fn run_cross_check(path: &Path, operation: Operation, report: &CheckReport) {
let check_path = if operation.checks_parent() {
path.parent().unwrap_or(path)
} else {
path
};
let flags = operation_to_access_flags(operation);
let kernel_ok = faccessat_eaccess(check_path, flags);
let whyno_ok = report.is_allowed();
if kernel_ok != whyno_ok {
eprintln!(
"whyno self-test: mismatch on {} {:?} -- \
whyno says {}, kernel says {}",
check_path.display(),
operation,
verdict(whyno_ok),
verdict(kernel_ok),
);
}
}
fn faccessat_eaccess(path: &Path, flags: nix::unistd::AccessFlags) -> bool {
nix::unistd::faccessat(None, path, flags, nix::fcntl::AtFlags::AT_EACCESS).is_ok()
}
fn verdict(allowed: bool) -> &'static str {
if allowed {
"allowed"
} else {
"denied"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_kernel_version_standard() {
assert_eq!(parse_kernel_version("5.8.0"), Some((5, 8)));
}
#[test]
fn parse_kernel_version_with_suffix() {
assert_eq!(parse_kernel_version("6.8.12-17-pve"), Some((6, 8)));
}
#[test]
fn parse_kernel_version_old() {
assert_eq!(parse_kernel_version("4.19.0-20-amd64"), Some((4, 19)));
}
#[test]
fn meets_minimum_exact() {
assert!(meets_minimum(5, 8));
}
#[test]
fn meets_minimum_higher_major() {
assert!(meets_minimum(6, 0));
}
#[test]
fn meets_minimum_lower() {
assert!(!meets_minimum(5, 7));
assert!(!meets_minimum(4, 19));
}
#[test]
fn operation_to_flags_read() {
let f = operation_to_access_flags(Operation::Read);
assert!(f.contains(nix::unistd::AccessFlags::R_OK));
}
#[test]
fn operation_to_flags_execute() {
let f = operation_to_access_flags(Operation::Execute);
assert!(f.contains(nix::unistd::AccessFlags::X_OK));
}
#[test]
fn operation_to_flags_delete() {
let f = operation_to_access_flags(Operation::Delete);
assert!(f.contains(nix::unistd::AccessFlags::W_OK));
assert!(f.contains(nix::unistd::AccessFlags::X_OK));
}
#[test]
fn operation_to_flags_write() {
let f = operation_to_access_flags(Operation::Write);
assert!(f.contains(nix::unistd::AccessFlags::W_OK));
}
#[test]
fn operation_to_flags_create() {
let f = operation_to_access_flags(Operation::Create);
assert!(f.contains(nix::unistd::AccessFlags::W_OK));
assert!(f.contains(nix::unistd::AccessFlags::X_OK));
}
#[test]
fn operation_to_flags_stat() {
let f = operation_to_access_flags(Operation::Stat);
assert!(f.contains(nix::unistd::AccessFlags::F_OK));
}
#[test]
fn meets_minimum_minor_boundary() {
assert!(meets_minimum(5, 9));
assert!(!meets_minimum(5, 7));
}
#[test]
fn meets_minimum_major_6_any_minor() {
assert!(meets_minimum(6, 0));
assert!(meets_minimum(6, 8));
}
#[test]
fn meets_minimum_major_4_fails() {
assert!(!meets_minimum(4, 99));
}
#[test]
fn parse_kernel_version_two_parts_only() {
assert_eq!(parse_kernel_version("5.8"), Some((5, 8)));
}
#[test]
fn parse_kernel_version_empty_string() {
assert_eq!(parse_kernel_version(""), None);
}
#[test]
fn parse_kernel_version_non_numeric() {
assert_eq!(parse_kernel_version("not.a.version"), None);
}
#[test]
fn verdict_allowed_returns_allowed() {
assert_eq!(verdict(true), "allowed");
}
#[test]
fn verdict_denied_returns_denied() {
assert_eq!(verdict(false), "denied");
}
#[test]
fn target_path_returns_last_component() {
use whyno_core::operation::Operation;
use whyno_core::test_helpers::StateBuilder;
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.mount("/", "ext4", "rw")
.build();
let path = target_path(&state).unwrap();
assert_eq!(path.to_str().unwrap(), "/file.txt");
}
#[test]
fn subject_is_self_matching_uid() {
use whyno_core::operation::Operation;
use whyno_core::test_helpers::StateBuilder;
let euid = nix::unistd::geteuid().as_raw();
let state = StateBuilder::new()
.subject(euid, euid, vec![])
.operation(Operation::Read)
.component_file("/file.txt", euid, euid, 0o644)
.build();
assert!(subject_is_self(&state));
}
#[test]
fn subject_is_self_different_uid() {
use whyno_core::operation::Operation;
use whyno_core::test_helpers::StateBuilder;
let euid = nix::unistd::geteuid().as_raw();
let other_uid = if euid == 0 { 1000 } else { 0 };
let state = StateBuilder::new()
.subject(other_uid, other_uid, vec![])
.operation(Operation::Read)
.component_file("/file.txt", other_uid, other_uid, 0o644)
.build();
assert!(!subject_is_self(&state));
}
#[test]
fn kernel_supports_faccessat2_returns_bool() {
let _result = kernel_supports_faccessat2();
}
#[test]
fn faccessat_eaccess_existing_file_returns_true() {
let result =
faccessat_eaccess(std::path::Path::new("/tmp"), nix::unistd::AccessFlags::F_OK);
assert!(result);
}
#[test]
fn faccessat_eaccess_nonexistent_path_returns_false() {
let result = faccessat_eaccess(
std::path::Path::new("/definitely_does_not_exist_whyno_test"),
nix::unistd::AccessFlags::F_OK,
);
assert!(!result);
}
#[test]
fn run_cross_check_on_real_path_does_not_panic() {
use whyno_core::checks::run_checks;
use whyno_core::operation::{MetadataParams, Operation};
use whyno_core::test_helpers::StateBuilder;
let euid = nix::unistd::geteuid().as_raw();
let state = StateBuilder::new()
.subject(euid, euid, vec![])
.operation(Operation::Read)
.component("/", 0, 0, 0o755)
.component_file("/tmp", 0, 0, 0o1777)
.mount("/", "ext4", "rw")
.build();
let report = run_checks(&state, &MetadataParams::default());
run_cross_check(std::path::Path::new("/tmp"), Operation::Read, &report);
}
#[test]
fn maybe_cross_check_with_self_subject_does_not_panic() {
use whyno_core::checks::run_checks;
use whyno_core::operation::{MetadataParams, Operation};
use whyno_core::test_helpers::StateBuilder;
let euid = nix::unistd::geteuid().as_raw();
let effective_gid = nix::unistd::getegid().as_raw();
let state = StateBuilder::new()
.subject(euid, effective_gid, vec![])
.operation(Operation::Read)
.component("/", 0, 0, 0o755)
.component_file("/tmp", 0, 0, 0o1777)
.mount("/", "ext4", "rw")
.build();
let report = run_checks(&state, &MetadataParams::default());
maybe_cross_check(&state, &report);
}
}