syd 3.57.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/caps/util.rs: Utilities for Linux capabilities(7)
//
// Copyright (c) 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

//! Utilities for Linux capabilities(7)

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

use memchr::arch::all::{is_equal, is_prefix};
use nix::{
    errno::Errno,
    sys::stat::SFlag,
    unistd::{Gid, Uid},
};

use crate::{
    caps::{self, CapSet, Capabilities},
    compat::{fstatx, getgroups, STATX_GID, STATX_UID},
};

/// Returns true if accessing named xattr(7) is allowed for given file.
pub fn capable_xattr<Fd: AsFd>(fd: Fd, name: &CStr, write: bool) -> Result<(), Errno> {
    let name = name.to_bytes();

    if write && is_prefix(name, b"system.posix_acl_") {
        capable_or_inode_owner(fd)
    } else if is_prefix(name, b"trusted.") && !capable(Capabilities::CAP_SYS_ADMIN) {
        Err(if write { Errno::EPERM } else { Errno::ENODATA })
    } else if write && is_equal(name, b"security.capability") {
        if capable(Capabilities::CAP_SETFCAP) {
            Ok(())
        } else {
            Err(Errno::EPERM)
        }
    } else if write && is_prefix(name, b"security.") && !capable(Capabilities::CAP_MAC_ADMIN) {
        Err(Errno::EPERM)
    } else {
        Ok(())
    }
}

/// Returns true if creating the given file type is allowed.
pub fn capable_mknod(ftyp: SFlag, dev: libc::dev_t) -> Result<(), Errno> {
    match ftyp {
        SFlag::S_IFCHR if dev == 0 => {} // whiteout
        SFlag::S_IFBLK | SFlag::S_IFCHR if capable(Capabilities::CAP_MKNOD) => {}
        SFlag::S_IFBLK | SFlag::S_IFCHR => return Err(Errno::EPERM),
        _ => {}
    }

    Ok(())
}

/// Returns true if given UID/GID change is allowed on target file.
pub fn capable_uidgid<Fd: AsFd>(
    fd: Fd,
    owner: Option<Uid>,
    group: Option<Gid>,
) -> Result<(), Errno> {
    if owner.is_none() && group.is_none() {
        return Ok(());
    }
    if capable(Capabilities::CAP_CHOWN) {
        return Ok(());
    }

    let mut flags = STATX_UID;
    if group.is_some() {
        flags |= STATX_GID;
    }
    let stx = fstatx(fd, flags)?;

    let file_uid = Uid::from_raw(stx.stx_uid);
    let is_owner = file_uid == Uid::effective();

    if let Some(owner) = owner {
        if !is_owner || owner != file_uid {
            return Err(Errno::EPERM);
        }
    }

    if let Some(group) = group {
        let file_gid = Gid::from_raw(stx.stx_gid);

        if !is_owner || (!has_gid(group)? && group != file_gid) {
            return Err(Errno::EPERM);
        }
    }

    Ok(())
}

/// Returns true if caller owns inode or holds CAP_FOWNER.
pub fn capable_or_inode_owner<Fd: AsFd>(fd: Fd) -> Result<(), Errno> {
    let uid = fstatx(fd, STATX_UID)
        .map(|stx| stx.stx_uid)
        .map(Uid::from_raw)?;
    if uid == Uid::effective() {
        return Ok(());
    }

    if capable(Capabilities::CAP_FOWNER) {
        return Ok(());
    }

    Err(Errno::EPERM)
}

/// Returns true if caller is in inode's group or holds CAP_FSETID.
pub fn capable_or_inode_group<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
    let gid = fstatx(fd, STATX_GID)
        .map(|stx| stx.stx_gid)
        .map(Gid::from_raw)?;

    if has_gid(gid)? {
        Ok(true)
    } else {
        Ok(capable(Capabilities::CAP_FSETID))
    }
}

/// Returns true if current thread's effective set holds all capabilities in `caps`.
///
/// Reads effective set and returns `false` on error.
#[must_use]
pub fn capable(caps: Capabilities) -> bool {
    caps::read(None, CapSet::Effective)
        .map(|caps_eff| caps_eff.contains(caps))
        .unwrap_or(false)
}

/// Returns true if current thread's effective set holds any capabilities in `caps`.
///
/// Reads effective set and returns `false` on error.
#[must_use]
pub fn capable_any(caps: Capabilities) -> bool {
    caps::read(None, CapSet::Effective)
        .map(|caps_eff| caps_eff.intersects(caps))
        .unwrap_or(false)
}

fn has_gid(gid: Gid) -> Result<bool, Errno> {
    if gid == Gid::effective() {
        Ok(true)
    } else {
        Ok(getgroups()?.contains(&gid))
    }
}