syd 3.52.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/xattr.rs: Extended attribute utilities
//
// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    ffi::CStr,
    os::fd::{AsFd, AsRawFd},
};

use libc::{c_int, c_void, size_t};
use memchr::{arch::all::is_prefix, memchr};
use nix::{errno::Errno, NixPath};

/// Get an extended attribute value.
pub fn fgetxattr<Fd: AsFd, P: ?Sized + NixPath>(
    fd: Fd,
    name: &P,
    value: Option<&mut [u8]>,
) -> Result<usize, Errno> {
    let (value, len) = match value {
        Some(v) => (v.as_mut_ptr() as *mut c_void, v.len() as size_t),
        None => (std::ptr::null_mut(), 0),
    };

    // SAFETY: nix lacks a wrapper for fgetxattr.
    let res = name.with_nix_path(|name_ptr| unsafe {
        libc::fgetxattr(fd.as_fd().as_raw_fd(), name_ptr.as_ptr(), value, len)
    })?;

    #[expect(clippy::cast_sign_loss)]
    Errno::result(res).map(|res| res as usize)
}

/// Set an extended attribute value.
pub fn fsetxattr<Fd: AsFd, P: ?Sized + NixPath>(
    fd: Fd,
    name: &P,
    value: &[u8],
    flags: i32,
) -> Result<(), Errno> {
    // SAFETY: nix lacks a wrapper for fsetxattr.
    let res = name.with_nix_path(|name_ptr| unsafe {
        libc::fsetxattr(
            fd.as_fd().as_raw_fd(),
            name_ptr.as_ptr(),
            value.as_ptr() as *const c_void,
            value.len() as size_t,
            flags as c_int,
        )
    })?;

    Errno::result(res).map(drop)
}

/// Remove an extended attribute value.
pub fn fremovexattr<Fd: AsFd, P: ?Sized + NixPath>(fd: Fd, name: &P) -> Result<(), Errno> {
    // SAFETY: nix lacks a wrapper for fremovexattr.
    let res = name.with_nix_path(|name_ptr| unsafe {
        libc::fremovexattr(fd.as_fd().as_raw_fd(), name_ptr.as_ptr())
    })?;

    Errno::result(res).map(drop)
}

// List of restricted extended attribute prefixes.
const XATTR_SEC: &[&[u8]] = &[b"security.", b"system.", b"trusted."];

/// Deny access to the extended attribute prefixes security.* and trusted.*
pub fn denyxattr(name: &CStr) -> Result<(), Errno> {
    let name = name.to_bytes();

    for prefix in XATTR_SEC {
        if is_prefix(name, prefix) {
            return Err(Errno::EPERM);
        }
    }

    Ok(())
}

/// Filters out extended attribute prefixes `security.*` and `trusted.*`
pub fn filterxattr(buf: &[u8], n: usize) -> Result<Vec<u8>, Errno> {
    let mut soff = 0;
    let mut fbuf = Vec::new();
    while soff < n {
        let end = if let Some(end) = memchr(0, &buf[soff..]) {
            end
        } else {
            break;
        };

        // Add +1 to include the NUL byte.
        let eoff = soff
            .checked_add(end)
            .ok_or(Errno::EOVERFLOW)?
            .checked_add(1)
            .ok_or(Errno::EOVERFLOW)?;
        let name = &buf[soff..eoff];

        // SAFETY: memchr check above guarantees:
        // 1. The slice is nul-terminated.
        // 2. The slice has no interior nul bytes.
        let cstr = unsafe { CStr::from_bytes_with_nul_unchecked(name) };
        let cstr = cstr.to_bytes();

        let mut filter = false;
        for prefix in XATTR_SEC {
            if is_prefix(cstr, prefix) {
                filter = true;
                break;
            }
        }

        if !filter {
            fbuf.try_reserve(name.len()).or(Err(Errno::ENOMEM))?;
            fbuf.extend_from_slice(name);
        }

        soff = eoff;
    }

    Ok(fbuf)
}

#[cfg(test)]
mod tests {
    use std::{ffi::CStr, os::fd::AsFd};

    use tempfile::NamedTempFile;

    use super::*;

    #[test]
    fn test_denyxattr_1() {
        let name = CStr::from_bytes_with_nul(b"user.test\0").unwrap();
        assert!(denyxattr(name).is_ok());
    }

    #[test]
    fn test_denyxattr_2() {
        let name = CStr::from_bytes_with_nul(b"system.posix_acl_access\0").unwrap();
        assert_eq!(denyxattr(name), Err(Errno::EPERM));
    }

    #[test]
    fn test_denyxattr_3() {
        let name = CStr::from_bytes_with_nul(b"security.selinux\0").unwrap();
        assert_eq!(denyxattr(name), Err(Errno::EPERM));
    }

    #[test]
    fn test_denyxattr_4() {
        let name = CStr::from_bytes_with_nul(b"trusted.overlay\0").unwrap();
        assert_eq!(denyxattr(name), Err(Errno::EPERM));
    }

    #[test]
    fn test_denyxattr_5() {
        let name = CStr::from_bytes_with_nul(b"securitynodot\0").unwrap();
        assert!(denyxattr(name).is_ok());
    }

    #[test]
    fn test_denyxattr_6() {
        let name = CStr::from_bytes_with_nul(b"security.\0").unwrap();
        assert_eq!(denyxattr(name), Err(Errno::EPERM));
    }

    #[test]
    fn test_denyxattr_7() {
        let name = CStr::from_bytes_with_nul(b"trusted.\0").unwrap();
        assert_eq!(denyxattr(name), Err(Errno::EPERM));
    }

    #[test]
    fn test_filterxattr_1() {
        let result = filterxattr(&[], 0).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_filterxattr_2() {
        let buf = b"user.test\0user.foo\0";
        let result = filterxattr(buf, buf.len()).unwrap();
        assert_eq!(result, buf);
    }

    #[test]
    fn test_filterxattr_3() {
        let buf = b"security.selinux\0user.test\0";
        let result = filterxattr(buf, buf.len()).unwrap();
        assert_eq!(result, b"user.test\0");
    }

    #[test]
    fn test_filterxattr_4() {
        let buf = b"trusted.overlay\0user.test\0";
        let result = filterxattr(buf, buf.len()).unwrap();
        assert_eq!(result, b"user.test\0");
    }

    #[test]
    fn test_filterxattr_5() {
        let buf = b"security.selinux\0trusted.overlay\0user.test\0";
        let result = filterxattr(buf, buf.len()).unwrap();
        assert_eq!(result, b"user.test\0");
    }

    #[test]
    fn test_filterxattr_6() {
        let buf = b"security.selinux\0trusted.overlay\0";
        let result = filterxattr(buf, buf.len()).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_filterxattr_7() {
        let buf = b"system.posix_acl\0security.ima\0";
        let result = filterxattr(buf, buf.len()).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_filterxattr_8() {
        let buf = b"user.a\0security.b\0user.c\0";

        let result = filterxattr(buf, 7).unwrap();
        assert_eq!(result, b"user.a\0");
    }

    #[test]
    fn test_filterxattr_9() {
        let buf = b"user.test";
        let result = filterxattr(buf, buf.len()).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_fgetxattr_1() {
        let tmp = NamedTempFile::new().unwrap();
        let fd = tmp.as_file().as_fd();

        let mut buf = [0u8; 256];
        let result = fgetxattr(fd, c"user.test", Some(&mut buf));
        assert!(result.is_err());
    }

    #[test]
    fn test_fsetxattr_1() {
        let tmp = NamedTempFile::new().unwrap();
        let fd = tmp.as_file().as_fd();

        let value = b"hello";
        let set_result = fsetxattr(fd, c"user.test", value, 0);
        if set_result.is_err() {
            return;
        }

        let mut buf = [0u8; 256];
        let len = fgetxattr(fd, c"user.test", Some(&mut buf)).unwrap();
        assert_eq!(&buf[..len], value);
    }

    #[test]
    fn test_fremovexattr_1() {
        let tmp = NamedTempFile::new().unwrap();
        let fd = tmp.as_file().as_fd();

        let value = b"hello";
        if fsetxattr(fd, c"user.test", value, 0).is_err() {
            return;
        }

        fremovexattr(fd, c"user.test").unwrap();

        let mut buf = [0u8; 256];
        assert!(fgetxattr(fd, c"user.test", Some(&mut buf)).is_err());
    }

    #[test]
    fn test_fgetxattr_2() {
        let tmp = NamedTempFile::new().unwrap();
        let fd = tmp.as_file().as_fd();

        let value = b"test_value";
        if fsetxattr(fd, c"user.size_test", value, 0).is_err() {
            return;
        }

        let size = fgetxattr(fd, c"user.size_test", None::<&mut [u8]>).unwrap();
        assert_eq!(size, value.len());
    }
}