whyno-cli 0.2.0

Linux permission debugger
//! VFS capability management for whyno binary.
//!
//! Installs, removes, checks `CAP_DAC_READ_SEARCH` via raw
//! `setxattr()`/`getxattr()`/`removexattr()` syscalls.
//! No `libcap` or `setcap` dependency.
//!
//! Uses VFS cap v2 format (20 bytes, little-endian), kernels >= 2.6.25.

#[path = "caps_xattr.rs"]
mod caps_xattr;

use std::path::PathBuf;

use crate::error::WhynoError;

/// VFS cap v2 xattr: `CAP_DAC_READ_SEARCH` in permitted+effective.
///
/// Layout (20 bytes, little-endian):
/// - `magic_etc` = `0x0200_0002` (revision 2 | effective flag)
/// - `data[0].permitted` = `0x0000_0004` (bit 2 = `CAP_DAC_READ_SEARCH`)
/// - `data[0].inheritable` = `0x0000_0000`
/// - `data[1].permitted` = `0x0000_0000`
/// - `data[1].inheritable` = `0x0000_0000`
pub const VFS_CAP_DATA: [u8; 20] = [
    0x02, 0x00, 0x00, 0x02, // magic_etc
    0x04, 0x00, 0x00, 0x00, // data[0].permitted
    0x00, 0x00, 0x00, 0x00, // data[0].inheritable
    0x00, 0x00, 0x00, 0x00, // data[1].permitted
    0x00, 0x00, 0x00, 0x00, // data[1].inheritable
];

/// Xattr name for security capabilities.
pub const XATTR_NAME: &[u8] = b"security.capability\0";

/// VFS cap v2 data size in bytes.
pub const VFS_CAP_SIZE: usize = 20;

/// Installs `CAP_DAC_READ_SEARCH` on whyno binary.
///
/// Locates binary via `/proc/self/exe`, verifies xattr support,
/// Writes cap xattr, reads back to verify.
/// Fails if binary not found, fs lacks xattr support, or not root.
pub fn caps_install() -> Result<(), WhynoError> {
    let exe = locate_self()?;
    caps_xattr::check_xattr_support(&exe)?;
    caps_xattr::write_cap_xattr(&exe)?;
    verify_cap_xattr(&exe)?;
    println!("Installed CAP_DAC_READ_SEARCH on {}", exe.display());
    Ok(())
}

/// Removes capability xattr from whyno binary.
pub fn caps_uninstall() -> Result<(), WhynoError> {
    let exe = locate_self()?;
    caps_xattr::remove_cap_xattr(&exe)?;
    println!("Removed capabilities from {}", exe.display());
    Ok(())
}

/// Checks whether whyno binary has expected capability xattr.
pub fn caps_check() -> Result<(), WhynoError> {
    let exe = locate_self()?;
    match caps_xattr::read_cap_xattr(&exe) {
        Ok(data) if data == VFS_CAP_DATA => {
            println!("CAP_DAC_READ_SEARCH is installed on {}", exe.display());
        }
        Ok(data) => {
            println!(
                "Capability xattr found but has unexpected content ({} bytes)",
                data.len()
            );
        }
        Err(msg) => {
            println!("No capabilities installed: {msg}");
        }
    }
    Ok(())
}

/// Locates whyno binary via `/proc/self/exe`.
fn locate_self() -> Result<PathBuf, WhynoError> {
    std::env::current_exe()
        .map_err(|e| WhynoError::Caps(format!("cannot locate whyno binary: {e}")))
}

/// Reads back cap xattr and verifies against expected bytes.
fn verify_cap_xattr(path: &std::path::Path) -> Result<(), WhynoError> {
    let data = caps_xattr::read_cap_xattr(path)
        .map_err(|e| WhynoError::Caps(format!("verification read-back failed: {e}")))?;
    if data != VFS_CAP_DATA {
        return Err(WhynoError::Caps(
            "verification failed: xattr content does not match expected bytes".to_string(),
        ));
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn vfs_cap_data_is_20_bytes() {
        assert_eq!(VFS_CAP_DATA.len(), 20);
    }

    #[test]
    fn vfs_cap_magic_etc_is_correct() {
        let magic = u32::from_le_bytes([
            VFS_CAP_DATA[0],
            VFS_CAP_DATA[1],
            VFS_CAP_DATA[2],
            VFS_CAP_DATA[3],
        ]);
        assert_eq!(magic, 0x0200_0002);
    }

    #[test]
    fn vfs_cap_permitted_has_dac_read_search() {
        let permitted = u32::from_le_bytes([
            VFS_CAP_DATA[4],
            VFS_CAP_DATA[5],
            VFS_CAP_DATA[6],
            VFS_CAP_DATA[7],
        ]);
        // CAP_DAC_READ_SEARCH = bit 2 = 0x04
        assert_eq!(permitted, 0x0000_0004);
    }

    #[test]
    fn vfs_cap_inheritable_is_zero() {
        for byte in &VFS_CAP_DATA[8..20] {
            assert_eq!(*byte, 0);
        }
    }

    /// read_cap_xattr on a file without security.capability xattr returns an error.
    ///
    /// This exercises the read path without requiring CAP_SETFCAP.
    #[test]
    fn read_cap_xattr_no_caps_returns_error() {
        let dir = tempfile::TempDir::new().unwrap();
        let path = dir.path().join("nocaps.bin");
        std::fs::write(&path, b"binary").unwrap();

        let result = caps_xattr::read_cap_xattr(&path);
        // on a fresh file without security.capability xattr, getxattr returns ENODATA
        // which the implementation maps to an error string
        assert!(result.is_err(), "expected error for file with no caps xattr");
    }

    /// check_xattr_support on a temp file exercises the xattr existence probe.
    ///
    /// Returns Ok if the filesystem supports xattrs (ENODATA is acceptable),
    /// Or Err(WhynoError::Caps) if xattrs are not supported (ENOTSUP).
    #[test]
    fn check_xattr_support_on_temp_file_does_not_panic() {
        let dir = tempfile::TempDir::new().unwrap();
        let path = dir.path().join("test.bin");
        std::fs::write(&path, b"data").unwrap();

        // result can be Ok (ENODATA is acceptable) or Err (ENOTSUP on some fs)
        // but must not panic
        let _result = caps_xattr::check_xattr_support(&path);
    }

    /// Verifies that reading the cap xattr from a path that does not exist returns an error.
    #[test]
    fn read_cap_xattr_nonexistent_path_returns_error() {
        let dir = tempfile::TempDir::new().unwrap();
        let path = dir.path().join("ghost.bin");
        // don't create the file

        let result = caps_xattr::read_cap_xattr(&path);
        assert!(result.is_err(), "expected error for nonexistent path");
    }

    /// Verifies that the xattr support probe on a valid path does not panic.
    #[test]
    fn path_to_cstring_nul_byte_returns_error() {
        // path_to_cstring is private; test via check_xattr_support which calls it
        // NUL bytes can't be passed via PathBuf on Linux; test the public surface
        // by checking the function handles real paths correctly
        let path = std::path::Path::new("/tmp");
        let _result = caps_xattr::check_xattr_support(path);
        // no panic = success
    }
}