whyno-core 0.5.0

Permission check pipeline, fix engine, and state types
Documentation
//! Filesystem inode flags check layer.
//!
//! Checks `FS_IMMUTABLE_FL` and `FS_APPEND_FL` on target component.
//! These flags block operations regardless of DAC permissions or
//! capabilities -- even root cannot write to an immutable file
//! without first clearing the flag via `chattr -i`.

use crate::operation::Operation;
use crate::state::fsflags::FsFlags;
use crate::state::{Probe, SystemState};

use super::LayerResult;

/// Checks whether filesystem inode flags block the requested operation.
///
/// Evaluates `immutable` and `append_only` on target path component.
/// Read, stat, and execute are never blocked by inode flags.
#[must_use]
pub fn check_fs_flags(state: &SystemState) -> LayerResult {
    let target_idx = match resolve_target(state) {
        Ok(idx) => idx,
        Err(result) => return result,
    };

    let component = &state.walk[target_idx];
    match &component.flags {
        Probe::Unknown | Probe::Inaccessible => LayerResult::Degraded {
            reason: format!(
                "could not read filesystem flags for {}",
                component.path.display()
            ),
        },
        Probe::Known(flags) => check_operation_against_flags(state.operation, *flags, target_idx),
    }
}

fn resolve_target(state: &SystemState) -> Result<usize, LayerResult> {
    state
        .operation
        .target_component(state.walk.len())
        .ok_or_else(|| LayerResult::Degraded {
            reason: "path walk too short to determine target component".into(),
        })
}

fn check_operation_against_flags(
    op: Operation,
    flags: FsFlags,
    component_idx: usize,
) -> LayerResult {
    match op {
        Operation::Read | Operation::Stat | Operation::Execute => LayerResult::Pass {
            detail: "filesystem flags do not restrict this operation".into(),
            warnings: vec![],
        },
        Operation::Write
        | Operation::Chmod
        | Operation::ChownUid
        | Operation::ChownGid
        | Operation::SetXattr { .. } => check_write_flags(flags, component_idx),
        Operation::Delete | Operation::Create => {
            check_delete_create_flags(op, flags, component_idx)
        }
    }
}

fn check_write_flags(flags: FsFlags, component_idx: usize) -> LayerResult {
    if flags.immutable {
        return LayerResult::Fail {
            detail: "file has immutable flag (chattr +i)".into(),
            component_index: Some(component_idx),
        };
    }
    if flags.append_only {
        return LayerResult::Fail {
            detail: "file has append-only flag (chattr +a)".into(),
            component_index: Some(component_idx),
        };
    }
    LayerResult::Pass {
        detail: "no blocking filesystem flags set".into(),
        warnings: vec![],
    }
}

/// Checks delete/create flag restrictions.
///
/// Immutable blocks both delete and create. Append-only on parent
/// directory blocks delete (kernel `may_delete()` checks `IS_APPEND(dir)`)
/// but allows creation -- append-only directories permit adding new entries.
fn check_delete_create_flags(op: Operation, flags: FsFlags, component_idx: usize) -> LayerResult {
    if flags.immutable {
        return LayerResult::Fail {
            detail: "parent directory has immutable flag (chattr +i), \
                     cannot delete or create files in it"
                .into(),
            component_index: Some(component_idx),
        };
    }
    if op == Operation::Delete && flags.append_only {
        return LayerResult::Fail {
            detail: "parent directory has append-only flag (chattr +a), \
                     cannot delete files from it"
                .into(),
            component_index: Some(component_idx),
        };
    }
    LayerResult::Pass {
        detail: "no blocking filesystem flags set".into(),
        warnings: vec![],
    }
}

#[cfg(test)]
#[path = "fsflags_tests.rs"]
mod tests;