whyno-cli 0.5.0

Linux permission debugger
//! Raw xattr syscall wrappers for VFS capability management.

use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;

use crate::error::WhynoError;

use super::{VFS_CAP_DATA, VFS_CAP_SIZE, XATTR_NAME};

/// Verifies filesystem xattr support via zero-size `getxattr` probe.
pub fn check_xattr_support(path: &Path) -> Result<(), WhynoError> {
    let c_path = path_to_cstring(path)?;
    let name_ptr = XATTR_NAME.as_ptr().cast::<libc::c_char>();

    // SAFETY: c_path and name_ptr are valid NUL-terminated C strings.
    // Passing null buffer with size 0 queries for existence without reading.
    let ret = unsafe { libc::getxattr(c_path.as_ptr(), name_ptr, std::ptr::null_mut(), 0) };

    if ret < 0 {
        let errno = nix::errno::Errno::last();
        if errno == nix::errno::Errno::ENOTSUP {
            return Err(WhynoError::Caps(
                "filesystem does not support extended attributes".to_string(),
            ));
        }
        // ENODATA is fine -- xattr doesn't exist yet, but fs supports them.
    }
    Ok(())
}

pub fn write_cap_xattr(path: &Path) -> Result<(), WhynoError> {
    let c_path = path_to_cstring(path)?;
    let name_ptr = XATTR_NAME.as_ptr().cast::<libc::c_char>();

    // SAFETY: c_path and name_ptr are valid NUL-terminated C strings.
    // VFS_CAP_DATA is a valid 20-byte buffer with the correct format.
    let ret = unsafe {
        libc::setxattr(
            c_path.as_ptr(),
            name_ptr,
            VFS_CAP_DATA.as_ptr().cast::<libc::c_void>(),
            VFS_CAP_SIZE,
            0, // flags: 0 = create or replace
        )
    };

    if ret < 0 {
        let errno = nix::errno::Errno::last();
        return Err(WhynoError::Caps(format!("setxattr failed: {errno}")));
    }
    Ok(())
}

pub fn read_cap_xattr(path: &Path) -> Result<Vec<u8>, String> {
    let c_path = path_to_cstring(path).map_err(|e| e.to_string())?;
    let name_ptr = XATTR_NAME.as_ptr().cast::<libc::c_char>();
    let mut buf = [0u8; VFS_CAP_SIZE];

    // SAFETY: c_path and name_ptr are valid NUL-terminated C strings.
    // buf is a valid mutable buffer of VFS_CAP_SIZE bytes.
    let ret = unsafe {
        libc::getxattr(
            c_path.as_ptr(),
            name_ptr,
            buf.as_mut_ptr().cast::<libc::c_void>(),
            VFS_CAP_SIZE,
        )
    };

    if ret < 0 {
        let errno = nix::errno::Errno::last();
        return Err(format!("getxattr failed: {errno}"));
    }

    // SAFETY: ret >= 0 is guaranteed by the early return above.
    #[allow(clippy::cast_sign_loss)]
    let len = ret as usize;
    Ok(buf[..len].to_vec())
}

pub fn remove_cap_xattr(path: &Path) -> Result<(), WhynoError> {
    let c_path = path_to_cstring(path)?;
    let name_ptr = XATTR_NAME.as_ptr().cast::<libc::c_char>();

    // SAFETY: c_path and name_ptr are valid NUL-terminated C strings.
    let ret = unsafe { libc::removexattr(c_path.as_ptr(), name_ptr) };

    if ret < 0 {
        let errno = nix::errno::Errno::last();
        return Err(WhynoError::Caps(format!("removexattr failed: {errno}")));
    }
    Ok(())
}

fn path_to_cstring(path: &Path) -> Result<CString, WhynoError> {
    CString::new(path.as_os_str().as_bytes())
        .map_err(|_| WhynoError::Caps("path contains interior NUL byte".to_string()))
}