whyno-core 0.5.0

Permission check pipeline, fix engine, and state types
Documentation
//! Fix application semantics for cascade simulation.
//!
//! Mutates cloned [`SystemState`] to reflect a [`Fix`]'s effect.
//! Used by cascade simulator to determine if later fixes become
//! redundant after applying earlier ones.

use crate::checks::caps::{CAP_CHOWN, CAP_FOWNER, CAP_FSETID, CAP_SYS_ADMIN};
use crate::state::acl::{AclEntry, AclPerms, AclTag, PosixAcl};
use crate::state::path::PathComponent;
use crate::state::Probe;
use crate::state::SystemState;

use super::super::{Fix, FixAction};

pub fn apply_fix(fix: &Fix, state: &mut SystemState) {
    match &fix.action {
        FixAction::Chmod { path, mode_change } => apply_chmod(state, path, mode_change),
        FixAction::SetAcl { path, entry } => apply_setacl(state, path, entry),
        FixAction::Chattr { path, flags } => apply_chattr(state, path, flags),
        FixAction::Remount {
            mountpoint,
            options,
        } => apply_remount(state, mountpoint, options),
        FixAction::Chown { path, owner, group } => apply_chown(state, path, *owner, *group),
        FixAction::GrantCap { capability, .. } => apply_grant_cap(state, capability),
    }
}

fn apply_chmod(state: &mut SystemState, path: &std::path::Path, mode_change: &str) {
    let Some(comp) = find_component_mut(state, path) else {
        return;
    };
    let Probe::Known(ref mut stat) = comp.stat else {
        return;
    };
    let Some((class, bits)) = parse_mode_change(mode_change) else {
        return;
    };
    apply_mode_bits(&mut stat.mode, class, bits);
}

fn apply_setacl(state: &mut SystemState, path: &std::path::Path, entry_str: &str) {
    let Some(comp) = find_component_mut(state, path) else {
        return;
    };
    let Some(acl_entry) = parse_acl_entry(entry_str) else {
        return;
    };
    match &mut comp.acl {
        Probe::Known(acl) => merge_acl_entry(acl, acl_entry),
        Probe::Unknown | Probe::Inaccessible => {
            comp.acl = Probe::Known(PosixAcl(vec![acl_entry]));
        }
    }
}

fn apply_chattr(state: &mut SystemState, path: &std::path::Path, flags: &str) {
    let Some(comp) = find_component_mut(state, path) else {
        return;
    };
    let Probe::Known(ref mut fs_flags) = comp.flags else {
        return;
    };
    match flags {
        "-i" => fs_flags.immutable = false,
        "-a" => fs_flags.append_only = false,
        _ => {}
    }
}

fn apply_remount(state: &mut SystemState, mountpoint: &std::path::Path, options: &str) {
    let entry = state
        .mounts
        .0
        .iter_mut()
        .find(|e| e.mountpoint == mountpoint);
    let Some(entry) = entry else { return };
    match options {
        "rw" => entry.options.read_only = false,
        "exec" => entry.options.noexec = false,
        _ => {}
    }
}

// parses capability name to bitmask bit; unknown strings silently ignored (no wrong grants)
fn apply_grant_cap(state: &mut SystemState, capability: &str) {
    let bit: u64 = match capability {
        "cap_fowner" => CAP_FOWNER,
        "cap_chown" => CAP_CHOWN,
        "cap_sys_admin" => CAP_SYS_ADMIN,
        "cap_fsetid" => CAP_FSETID,
        _ => return,
    };
    state.subject.capabilities = match state.subject.capabilities {
        Probe::Known(existing) => Probe::Known(existing | bit),
        Probe::Unknown | Probe::Inaccessible => Probe::Known(bit),
    };
}

/// Applies chown to target path component, mirrors `ATTR_KILL_SUID`/`ATTR_KILL_SGID`.
///
/// Clears `S_ISUID` when owner changes. Clears `S_ISGID` on group change
/// only when `S_IXGRP` is set, matching kernel `notify_change()` semantics.
fn apply_chown(
    state: &mut SystemState,
    path: &std::path::Path,
    owner: Option<u32>,
    group: Option<u32>,
) {
    let Some(comp) = find_component_mut(state, path) else {
        return;
    };
    let Probe::Known(ref mut stat) = comp.stat else {
        return;
    };
    if let Some(uid) = owner {
        stat.uid = uid;
        stat.mode &= !0o4000; // ATTR_KILL_SUID: clear S_ISUID
    }
    if let Some(gid) = group {
        stat.gid = gid;
        if stat.mode & 0o010 != 0 {
            stat.mode &= !0o2000; // ATTR_KILL_SGID: clear S_ISGID only if S_IXGRP set
        }
    }
}

fn find_component_mut<'a>(
    state: &'a mut SystemState,
    path: &std::path::Path,
) -> Option<&'a mut PathComponent> {
    state.walk.iter_mut().find(|c| c.path == path)
}

/// Parses mode change string like "o+x" into (class, permission bits).
fn parse_mode_change(s: &str) -> Option<(char, &str)> {
    let mut chars = s.chars();
    let class = chars.next()?;
    let plus = chars.next()?;
    if plus != '+' {
        return None;
    }
    let rest = &s[2..];
    if rest.is_empty() {
        return None;
    }
    Some((class, rest))
}

fn apply_mode_bits(mode: &mut u32, class: char, bits: &str) {
    let shift = match class {
        'u' => 6,
        'g' => 3,
        'o' => 0,
        _ => return,
    };
    for ch in bits.chars() {
        let bit = match ch {
            'r' => 4,
            'w' => 2,
            'x' => 1,
            _ => continue,
        };
        *mode |= bit << shift;
    }
}

/// Parses ACL entry string like "u:33:r" or "u:33:--x".
fn parse_acl_entry(s: &str) -> Option<AclEntry> {
    let parts: Vec<&str> = s.splitn(3, ':').collect();
    if parts.len() != 3 {
        return None;
    }
    let tag = match parts[0] {
        "u" => AclTag::User,
        "g" => AclTag::Group,
        _ => return None,
    };
    let qualifier = parts[1].parse::<u32>().ok();
    let perms = parse_acl_perms(parts[2]);
    Some(AclEntry {
        tag,
        qualifier,
        perms,
    })
}

fn parse_acl_perms(s: &str) -> AclPerms {
    AclPerms {
        read: s.contains('r'),
        write: s.contains('w'),
        execute: s.contains('x'),
    }
}

/// Merges ACL entry into existing ACL, replacing if same tag+qualifier.
fn merge_acl_entry(acl: &mut PosixAcl, new_entry: AclEntry) {
    let existing = acl
        .0
        .iter_mut()
        .find(|e| e.tag == new_entry.tag && e.qualifier == new_entry.qualifier);
    if let Some(entry) = existing {
        entry.perms = new_entry.perms;
    } else {
        acl.0.push(new_entry);
    }
}