syd 3.58.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/utils/syd-sec.rs: Print secure bits or run command with secure bits set
//
// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    io::{stdout, Write},
    process::{Command, ExitCode},
};

use nix::errno::Errno;
use serde_json::json;
use syd::{
    caps::securebits::{get_securebits, set_securebits, SecureBits},
    compat::{get_no_new_privs, set_no_new_privs},
    confine::run_cmd,
    eprintfln,
    err::err2exit,
    printfln,
};

// Set global allocator to GrapheneOS allocator.
#[cfg(all(
    not(target_os = "android"),
    not(target_arch = "loongarch64"),
    not(target_arch = "riscv64"),
    target_page_size_4k,
    target_pointer_width = "64"
))]
#[global_allocator]
static GLOBAL: hardened_malloc::HardenedMalloc = hardened_malloc::HardenedMalloc;

syd::main! {
    use lexopt::prelude::*;

    syd::set_sigpipe_dfl()?;

    // Parse CLI options.
    //
    // Note, option parsing is POSIXly correct:
    // POSIX recommends that no more options are parsed after the first
    // positional argument. The other arguments are then all treated as
    // positional arguments.
    // See: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02
    let mut opt_nnp = false;
    let mut opt_sec = SecureBits::empty();
    let mut opt_cmd = None;
    let mut opt_arg = Vec::new();

    let mut parser = lexopt::Parser::from_env();
    while let Some(arg) = parser.next()? {
        match arg {
            Short('h') => {
                help()?;
                return Ok(ExitCode::SUCCESS);
            }
            Short('p' | 'P') => opt_nnp = true,
            Short('r') => opt_sec.insert(SecureBits::SECBIT_NOROOT),
            Short('R') => opt_sec.insert(SecureBits::SECBIT_NOROOT_LOCKED),
            Short('s') => opt_sec.insert(SecureBits::SECBIT_NO_SETUID_FIXUP),
            Short('S') => opt_sec.insert(SecureBits::SECBIT_NO_SETUID_FIXUP_LOCKED),
            Short('k') => opt_sec.insert(SecureBits::SECBIT_KEEP_CAPS),
            Short('K') => opt_sec.insert(SecureBits::SECBIT_KEEP_CAPS_LOCKED),
            Short('a') => opt_sec.insert(SecureBits::SECBIT_NO_CAP_AMBIENT_RAISE),
            Short('A') => opt_sec.insert(SecureBits::SECBIT_NO_CAP_AMBIENT_RAISE_LOCKED),
            Short('x') => opt_sec.insert(SecureBits::SECBIT_EXEC_RESTRICT_FILE),
            Short('X') => opt_sec.insert(SecureBits::SECBIT_EXEC_RESTRICT_FILE_LOCKED),
            Short('i') => opt_sec.insert(SecureBits::SECBIT_EXEC_DENY_INTERACTIVE),
            Short('I') => opt_sec.insert(SecureBits::SECBIT_EXEC_DENY_INTERACTIVE_LOCKED),
            Value(prog) => {
                opt_cmd = Some(prog);
                opt_arg.extend(parser.raw_args()?);
            }
            _ => return Err(arg.unexpected().into()),
        }
    }

    let cmd = if let Some(cmd) = opt_cmd {
        // Run a command with secure bits set.
        if !opt_nnp && opt_sec.is_empty() {
            eprintfln!("syd-sec: No secure bits specified for command!")?;
            return Err(Errno::EINVAL.into());
        }
        cmd
    } else if !opt_nnp && opt_sec.is_empty() {
        // Print information on process secure bits.
        let nnp = get_no_new_privs()?;
        let sec = get_securebits()?;

        #[expect(clippy::disallowed_methods, clippy::disallowed_types)]
        let data = json!({
            "nnp": nnp,
            "sec": sec,
        });

        #[expect(clippy::disallowed_methods)]
        let mut data = serde_json::to_string(&data).expect("JSON");
        data.push('\n');
        stdout().write_all(data.as_bytes())?;

        return Ok(ExitCode::SUCCESS);
    } else {
        // Test given secure bits against process secure bits.
        if opt_nnp && !get_no_new_privs()? {
            return Ok(ExitCode::FAILURE);
        }
        if !opt_sec.is_empty() && !get_securebits()?.contains(opt_sec) {
            return Ok(ExitCode::FAILURE);
        }

        return Ok(ExitCode::SUCCESS);
    };

    // Set given secure bits.
    if opt_nnp {
        set_no_new_privs()?;
    }
    if !opt_sec.is_empty() {
        opt_sec.insert(get_securebits()?);
        set_securebits(opt_sec)?;
    }

    // Execute command.
    Ok(err2exit(run_cmd(Command::new(cmd).args(opt_arg))))
}

fn help() -> Result<(), Errno> {
    printfln!("Usage: syd-sec [-ahikprsxAIKPRSX] {{command [args...]}}")?;
    printfln!("Print secure bits or run command with secure bits set.")?;
    printfln!("Given no arguments, print information on process secure bits in compact JSON.")?;
    printfln!("Given command with arguments, set given secure bits and execute the command.")?;
    printfln!("Given no commands and some arguments, test given secure bits and exit with success if all are set.")?;
    printfln!("Use -p, -P to set/test no_new_privs attribute")?;
    printfln!("Use -r, -R to set/test bit SECBIT_NOROOT")?;
    printfln!("Use -s, -S to set/test bit SECBIT_NO_SETUID_FIXUP")?;
    printfln!("Use -k, -K to set/test bit SECBIT_KEEP_CAPS")?;
    printfln!("Use -a, -A to set/test bit SECBIT_NO_CAP_AMBIENT_RAISE")?;
    printfln!("Use -x, -X to set/test bit SECBIT_EXEC_RESTRICT_FILE")?;
    printfln!("Use -i, -I to set/test bit SECBIT_DENY_INTERACTIVE")?;
    printfln!("Capital letter options set/test locked version of the respective secure bit.")?;
    Ok(())
}