syd 3.52.0

rock-solid application kernel
Documentation
//! Manipulate securebits flags
//!
//! This module exposes methods to get and set per-thread securebits
//! flags, which can be used to disable special handling of capabilities
//! for UID 0 (root).

use std::fmt;

use bitflags::bitflags;
use nix::errno::Errno;
use serde::{ser::SerializeSeq, Serialize, Serializer};

use crate::caps::{errors::CapsError, nr};

/// Return whether the current thread's "keep capabilities" flag is set.
pub fn has_keepcaps() -> Result<bool, CapsError> {
    let ret = Errno::result(unsafe { nix::libc::prctl(nr::PR_GET_KEEPCAPS, 0, 0, 0) })
        .map_err(CapsError)?;

    match ret {
        0 => Ok(false),
        _ => Ok(true),
    }
}

/// Set the value of the current thread's "keep capabilities" flag.
pub fn set_keepcaps(keep_caps: bool) -> Result<(), CapsError> {
    let flag = if keep_caps { 1 } else { 0 };

    Errno::result(unsafe { nix::libc::prctl(nr::PR_SET_KEEPCAPS, flag, 0, 0) })
        .map(drop)
        .map_err(CapsError)
}

bitflags! {
    /// Process "securebits" mask controlling capability semantics (per-thread).
    ///
    /// Mirrors `<linux/securebits.h>`; see also `capabilities(7)` and `prctl(2)`.
    /// Locks make their corresponding base bits immutable until exec/privilege reset.
    #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
    pub struct SecureBits: u32 {
        /// Ignore special-casing of UID 0 for capability gain/loss.
        /// (Disables the legacy "root is magical" behavior.)
        ///
        /// Setting this bit requires CAP_SETPCAP capability.
        const SECBIT_NOROOT = 1 << 0;
        /// Lock `NOROOT`.
        const SECBIT_NOROOT_LOCKED = 1 << 1;

        /// Don't auto-add/drop caps on set*uid transitions to/from UID 0.
        ///
        /// Setting this bit requires CAP_SETPCAP capability.
        const SECBIT_NO_SETUID_FIXUP = 1 << 2;
        /// Lock `NO_SETUID_FIXUP`.
        const SECBIT_NO_SETUID_FIXUP_LOCKED = 1 << 3;

        /// Keep capabilities across `setuid(2)` transitions.
        /// (Note: **cleared by `execve(2)`** even if locked-kernel behavior.)
        ///
        /// Setting this bit requires CAP_SETPCAP capability.
        const SECBIT_KEEP_CAPS = 1 << 4;
        /// Lock `KEEP_CAPS`.
        const SECBIT_KEEP_CAPS_LOCKED = 1 << 5;

        /// Disallow `PR_CAP_AMBIENT_RAISE` operations (ambient caps).
        ///
        /// Setting this bit requires CAP_SETPCAP capability.
        const SECBIT_NO_CAP_AMBIENT_RAISE = 1 << 6;
        /// Lock `NO_CAP_AMBIENT_RAISE`.
        const SECBIT_NO_CAP_AMBIENT_RAISE_LOCKED = 1 << 7;

        /// Require `execveat(2)` with `AT_EXECVE_CHECK` to succeed before
        /// interpreting/executing a file (for interpreters/dynlinkers).
        ///
        /// Unprivileged-settable; inherited across exec/fork.
        const SECBIT_EXEC_RESTRICT_FILE = 1 << 8;
        /// Lock `EXEC_RESTRICT_FILE`.
        const SECBIT_EXEC_RESTRICT_FILE_LOCKED = 1 << 9;

        /// Deny interactive user commands (REPL / snippet). Allow only if content
        /// comes via FD and `AT_EXECVE_CHECK` succeeds.
        ///
        /// Unprivileged-settable; inherited across exec/fork.
        const SECBIT_EXEC_DENY_INTERACTIVE = 1 << 10;
        /// Lock `EXEC_DENY_INTERACTIVE`.
        const SECBIT_EXEC_DENY_INTERACTIVE_LOCKED = 1 << 11;

        /// Convenience: all base bits (no locks).
        const SECBIT_ALL_BASE =
            Self::SECBIT_NOROOT.bits() |
            Self::SECBIT_NO_SETUID_FIXUP.bits() |
            Self::SECBIT_KEEP_CAPS.bits() |
            Self::SECBIT_NO_CAP_AMBIENT_RAISE.bits() |
            Self::SECBIT_EXEC_RESTRICT_FILE.bits() |
            Self::SECBIT_EXEC_DENY_INTERACTIVE.bits();

        /// Convenience: all lock bits.
        const SECBIT_ALL_LOCK =
            Self::SECBIT_NOROOT_LOCKED.bits() |
            Self::SECBIT_NO_SETUID_FIXUP_LOCKED.bits() |
            Self::SECBIT_KEEP_CAPS_LOCKED.bits() |
            Self::SECBIT_NO_CAP_AMBIENT_RAISE_LOCKED.bits() |
            Self::SECBIT_EXEC_RESTRICT_FILE_LOCKED.bits() |
            Self::SECBIT_EXEC_DENY_INTERACTIVE_LOCKED.bits();

        /// Convenience: all privileged bits.
        const SECBIT_ALL_BASE_PRIV =
            Self::SECBIT_NOROOT.bits() |
            Self::SECBIT_NO_SETUID_FIXUP.bits() |
            Self::SECBIT_KEEP_CAPS.bits() |
            Self::SECBIT_NO_CAP_AMBIENT_RAISE.bits();

        /// Convenience: all privileged lock bits.
        const SECBIT_ALL_LOCK_PRIV =
            Self::SECBIT_NOROOT_LOCKED.bits() |
            Self::SECBIT_NO_SETUID_FIXUP_LOCKED.bits() |
            Self::SECBIT_KEEP_CAPS_LOCKED.bits() |
            Self::SECBIT_NO_CAP_AMBIENT_RAISE_LOCKED.bits();

        /// Convenience: all unprivileged bits.
        const SECBIT_ALL_BASE_UNPRIV =
            Self::SECBIT_EXEC_RESTRICT_FILE.bits() |
            Self::SECBIT_EXEC_DENY_INTERACTIVE.bits();

        /// Convenience: all unprivileged lock bits.
        const SECBIT_ALL_LOCK_UNPRIV =
            Self::SECBIT_EXEC_RESTRICT_FILE_LOCKED.bits() |
            Self::SECBIT_EXEC_DENY_INTERACTIVE_LOCKED.bits();
    }
}

impl fmt::Display for SecureBits {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut securebits: Vec<&str> = vec![];

        // NOROOT
        if self.contains(Self::SECBIT_NOROOT_LOCKED) {
            securebits.push("secure-no-root-locked");
        } else if self.contains(Self::SECBIT_NOROOT) {
            securebits.push("secure-no-root");
        }

        // NO_SETUID_FIXUP
        if self.contains(Self::SECBIT_NO_SETUID_FIXUP_LOCKED) {
            securebits.push("secure-no-setuid-fixup-locked");
        } else if self.contains(Self::SECBIT_NO_SETUID_FIXUP) {
            securebits.push("secure-no-setuid-fixup");
        }

        // KEEP_CAPS
        if self.contains(Self::SECBIT_KEEP_CAPS_LOCKED) {
            securebits.push("secure-keep-caps-locked");
        } else if self.contains(Self::SECBIT_KEEP_CAPS) {
            securebits.push("secure-keep-caps");
        }

        // NO_CAP_AMBIENT_RAISE
        if self.contains(Self::SECBIT_NO_CAP_AMBIENT_RAISE_LOCKED) {
            securebits.push("secure-no-ambient-raise-locked");
        } else if self.contains(Self::SECBIT_NO_CAP_AMBIENT_RAISE) {
            securebits.push("secure-no-ambient-raise");
        }

        // EXEC_RESTRICT_FILE
        if self.contains(Self::SECBIT_EXEC_RESTRICT_FILE_LOCKED) {
            securebits.push("secure-exec-restrict-file-locked");
        } else if self.contains(Self::SECBIT_EXEC_RESTRICT_FILE) {
            securebits.push("secure-exec-restrict-file");
        }

        // SECBIT_EXEC_DENY_INTERACTIVE
        if self.contains(Self::SECBIT_EXEC_DENY_INTERACTIVE_LOCKED) {
            securebits.push("secure-exec-deny-interactive-locked");
        } else if self.contains(Self::SECBIT_EXEC_DENY_INTERACTIVE) {
            securebits.push("secure-exec-deny-interactive");
        }

        write!(f, "{}", securebits.join(","))
    }
}

impl Serialize for SecureBits {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut seq = serializer.serialize_seq(Some(self.iter().count()))?;

        for bit in self.iter() {
            seq.serialize_element(&bit.to_string())?;
        }

        seq.end()
    }
}

/// Get the current thread's securebits mask.
///
/// Returns the raw mask as `SecureBits` (unknown bits retained).
pub fn get_securebits() -> Result<SecureBits, CapsError> {
    // SAFETY: `PR_GET_SECUREBITS` reads a per-thread mask;
    // remaining args are unused zeros per prctl(2) contract.
    #[expect(clippy::cast_sign_loss)]
    Errno::result(unsafe { libc::prctl(libc::PR_GET_SECUREBITS, 0, 0, 0, 0) })
        .map(|r| r as u32)
        .map(SecureBits::from_bits_retain)
        .map_err(CapsError)
}

/// Set the current thread's securebits mask **exactly** to `bits`.
pub fn set_securebits(bits: SecureBits) -> Result<(), CapsError> {
    // SAFETY: `PR_SET_SECUREBITS` sets a per-thread mask;
    // `bits` is a valid `SecureBits` value, remaining args unused.
    Errno::result(unsafe { libc::prctl(libc::PR_SET_SECUREBITS, bits.bits(), 0, 0, 0) })
        .map(drop)
        .map_err(CapsError)
}

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

    #[test]
    fn test_securebits_1() {
        let s = SecureBits::empty().to_string();
        assert_eq!(s, "");
    }

    #[test]
    fn test_securebits_2() {
        let s = SecureBits::SECBIT_NOROOT.to_string();
        assert_eq!(s, "secure-no-root");
    }

    #[test]
    fn test_securebits_3() {
        let s = SecureBits::SECBIT_NOROOT_LOCKED.to_string();
        assert_eq!(s, "secure-no-root-locked");
    }

    #[test]
    fn test_securebits_4() {
        let s = SecureBits::SECBIT_KEEP_CAPS.to_string();
        assert_eq!(s, "secure-keep-caps");
    }

    #[test]
    fn test_securebits_5() {
        let s = SecureBits::SECBIT_KEEP_CAPS_LOCKED.to_string();
        assert_eq!(s, "secure-keep-caps-locked");
    }

    #[test]
    fn test_securebits_6() {
        let s = SecureBits::SECBIT_NO_SETUID_FIXUP.to_string();
        assert_eq!(s, "secure-no-setuid-fixup");
    }

    #[test]
    fn test_securebits_7() {
        let s = SecureBits::SECBIT_NO_CAP_AMBIENT_RAISE.to_string();
        assert_eq!(s, "secure-no-ambient-raise");
    }

    #[test]
    fn test_securebits_8() {
        let s = SecureBits::SECBIT_EXEC_RESTRICT_FILE.to_string();
        assert_eq!(s, "secure-exec-restrict-file");
    }

    #[test]
    fn test_securebits_9() {
        let s = SecureBits::SECBIT_EXEC_DENY_INTERACTIVE.to_string();
        assert_eq!(s, "secure-exec-deny-interactive");
    }

    #[test]
    fn test_securebits_10() {
        let bits = SecureBits::SECBIT_NOROOT | SecureBits::SECBIT_KEEP_CAPS;
        let s = bits.to_string();
        assert!(s.contains("secure-no-root"));
        assert!(s.contains("secure-keep-caps"));
        assert!(s.contains(','));
    }

    #[test]
    fn test_securebits_11() {
        assert!(SecureBits::SECBIT_ALL_BASE.contains(SecureBits::SECBIT_NOROOT));
    }

    #[test]
    fn test_securebits_12() {
        assert!(SecureBits::SECBIT_ALL_LOCK.contains(SecureBits::SECBIT_NOROOT_LOCKED));
    }

    #[test]
    fn test_securebits_13() {
        assert_eq!(SecureBits::SECBIT_NOROOT.bits(), 1 << 0);
        assert_eq!(SecureBits::SECBIT_NOROOT_LOCKED.bits(), 1 << 1);
        assert_eq!(SecureBits::SECBIT_KEEP_CAPS.bits(), 1 << 4);
    }
}