syd 3.58.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/utils/syd-run.rs: Run a program inside a container (requires Linux-5.8 or newer).
//
// Copyright (c) 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    os::unix::ffi::OsStrExt,
    process::{Command, ExitCode},
};

use nix::{
    errno::Errno,
    libc::pid_t,
    sched::{setns, CloneFlags},
    unistd::Pid,
};
use syd::{
    confine::{run_cmd, syd_shell, CLONE_NEWTIME},
    eprintfln,
    err::{err2exit, SydResult},
    fd::pidfd_open,
    path::XPath,
    printfln,
    proc::util::proc_namespaces,
};

// 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
    //
    // Empty clone flags is the default and imply all except pid and time.
    let mut opt_cfl = CloneFlags::empty();
    let mut opt_pid = None;
    let mut opt_cmd = vec![];
    let mut opt_log = false;

    let mut parser = lexopt::Parser::from_env();
    while let Some(arg) = parser.next()? {
        match arg {
            Short('h') => {
                help()?;
                return Ok(ExitCode::SUCCESS);
            }
            Short('a') => opt_cfl = CloneFlags::empty(),
            Short('c') => opt_cfl |= CloneFlags::CLONE_NEWCGROUP,
            Short('i') => opt_cfl |= CloneFlags::CLONE_NEWIPC,
            Short('m') => opt_cfl |= CloneFlags::CLONE_NEWNS,
            Short('n') => opt_cfl |= CloneFlags::CLONE_NEWNET,
            Short('p') => opt_cfl |= CloneFlags::CLONE_NEWPID,
            Short('t') => opt_cfl |= CLONE_NEWTIME,
            Short('u') => opt_cfl |= CloneFlags::CLONE_NEWUTS,
            Short('U') => opt_cfl |= CloneFlags::CLONE_NEWUSER,
            Short('v') => opt_log = true,
            Value(pid) => {
                opt_pid = Some(pid);
                opt_cmd.extend(parser.raw_args()?);
            }
            _ => return Err(arg.unexpected().into()),
        }
    }

    let pid = if let Some(pid) = opt_pid {
        let pid = pid.parse::<pid_t>()?;
        if pid <= 0 {
            return Err(Errno::EINVAL.into());
        }
        pid
    } else {
        help()?;
        return Ok(ExitCode::FAILURE);
    };

    let namespaces = if opt_cfl.is_empty() {
        match nsget(pid, opt_log) {
            Ok(namespaces) => namespaces,
            Err(errno) => {
                eprintfln!("syd-run: nsget: {errno}!")?;
                return Ok(ExitCode::FAILURE);
            }
        }
    } else {
        opt_cfl
    };

    if !namespaces.is_empty() {
        if let Err(errno) = nsenter(pid, namespaces) {
            eprintfln!("syd-run: nsenter: {errno}!")?;
            return Ok(ExitCode::FAILURE);
        }
    }

    // Command is Syd shell by default.
    let argv = if opt_cmd.is_empty() {
        syd_shell()
    } else {
        opt_cmd
    };

    let prog = argv.first().ok_or(Errno::EINVAL)?;

    if opt_log {
        eprintfln!("syd-run: exec command `{}'...", XPath::new(prog))?;
    }

    // Execute command.
    Ok(err2exit(run_cmd(
        Command::new(prog).args(argv.get(1..).unwrap_or(&[])),
    )))
}

fn help() -> Result<(), Errno> {
    printfln!("Usage: syd-run [-hvacimnptuU] pid [<program> [<argument>...]]")?;
    printfln!("Run a program inside a container (requires Linux-5.8 or newer).")?;
    Ok(())
}

fn nsenter(pid: pid_t, namespaces: CloneFlags) -> Result<(), Errno> {
    setns(pidfd_open(Pid::from_raw(pid), 0)?, namespaces)
}

fn nsget(pid: pid_t, log: bool) -> SydResult<CloneFlags> {
    let current_pid = Pid::this();
    let current_namespaces = proc_namespaces(current_pid)?;

    let target_pid = Pid::from_raw(pid);
    let target_namespaces = proc_namespaces(target_pid)?.0;

    let mut flags = CloneFlags::empty();

    for (name, target_ns) in target_namespaces {
        if let Some(current_ns) = current_namespaces.0.get(&name) {
            if target_ns.identifier != current_ns.identifier {
                let name = name.as_bytes();
                flags |= match name {
                    b"cgroup" => CloneFlags::CLONE_NEWCGROUP,
                    b"ipc" => CloneFlags::CLONE_NEWIPC,
                    b"mnt" => CloneFlags::CLONE_NEWNS,
                    b"net" => CloneFlags::CLONE_NEWNET,
                    b"user" => CloneFlags::CLONE_NEWUSER,
                    b"uts" => CloneFlags::CLONE_NEWUTS,
                    // Entering pid or time is privileged, so we only enter
                    // them in case user explicitly specified them.
                    b"pid" | b"pid_for_children" => continue, // CloneFlags::CLONE_NEWPID,
                    b"time" | b"time_for_children" => continue, // CLONE_NEWTIME,
                    _ => {
                        if log {
                            eprintfln!(
                                "syd-run: skip unsupported {} namespace switch from id:{} to id:{}!",
                                XPath::from_bytes(name),
                                current_ns.identifier,
                                target_ns.identifier
                            )?;
                        }
                        continue;
                    }
                };
                if log {
                    eprintfln!(
                        "syd-run: switch {} namespace from id:{} to id:{}...",
                        XPath::from_bytes(name),
                        current_ns.identifier,
                        target_ns.identifier
                    )?;
                }
            }
        }
    }

    Ok(flags)
}