syd 3.57.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/kernel/ptrace/exec.rs: ptrace exec handlers
//
// Copyright (c) 2023, 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{io::Seek, os::fd::AsRawFd, sync::Arc};

use nix::{
    errno::Errno,
    fcntl::{AtFlags, OFlag},
    unistd::Pid,
};

use crate::{
    compat::ResolveFlag,
    confine::{scmp_arch, SydArch, SydSys},
    elf::{ElfFileType, ElfType, ExecutableFile, LinkingType},
    err::err2no,
    error,
    fd::{check_executable, AT_EXECVE_CHECK, PROC_FILE},
    kernel::{ptrace::enforce_action, sandbox_path, to_atflags},
    lookup::{safe_open_msym, CanonicalPath, FileType, FsFlags, MaybeFd},
    path::XPathBuf,
    ptrace::ptrace_syscall_info,
    req::{RemoteProcess, SysArg, SysFlags},
    sandbox::{Action, Capability, IntegrityError, SandboxGuard},
    warn,
    workers::WorkerCache,
    xfmt,
};

// Note, sysenter_exec is a ptrace(2) hook, not a seccomp hook!
// The seccomp hooks are only used with trace/allow_unsafe_ptrace:1.
#[expect(clippy::cognitive_complexity)]
pub(crate) fn sysenter_exec(
    pid: Pid,
    info: ptrace_syscall_info,
    cache: &Arc<WorkerCache>,
    sandbox: &SandboxGuard,
) -> Result<(), Errno> {
    let data = if let Some(data) = info.seccomp() {
        data
    } else {
        unreachable!("BUG: Invalid system call information returned by kernel!");
    };

    #[expect(clippy::cast_possible_truncation)]
    let (syscall, arg) = match SydSys::try_from(data.ret_data as u16) {
        Ok(SydSys::SysExecve) => (
            SydSys::SysExecve,
            SysArg {
                path: Some(0),
                fsflags: FsFlags::MUST_PATH,
                ..Default::default()
            },
        ),
        Ok(SydSys::SysExecveat) => {
            // Reject undefined/invalid flags.
            let flags = to_atflags(
                data.args[4],
                AtFlags::AT_SYMLINK_NOFOLLOW | AtFlags::AT_EMPTY_PATH | AT_EXECVE_CHECK,
            )?;

            let mut fsflags = FsFlags::MUST_PATH;
            if flags.contains(AtFlags::AT_SYMLINK_NOFOLLOW) {
                fsflags.insert(FsFlags::NO_FOLLOW_LAST);
            }

            let empty_path = flags.contains(AtFlags::AT_EMPTY_PATH);
            (
                SydSys::SysExecveat,
                SysArg {
                    dirfd: Some(0),
                    path: Some(1),
                    flags: if empty_path {
                        SysFlags::EMPTY_PATH | SysFlags::PASS_DELETE
                    } else {
                        SysFlags::empty()
                    },
                    fsflags,
                },
            )
        }
        _ => unreachable!("BUG: invalid syscall data {}!", data.ret_data as u16),
    };

    // Read remote path.
    let process = RemoteProcess::new(pid);

    #[expect(clippy::disallowed_methods)]
    let arch: SydArch = scmp_arch(info.arch).unwrap().into();

    // This is a ptrace(2) hook, the PID cannot be validated.
    let (path, _, empty_path) = process.read_path(sandbox, arch.into(), data.args, arg, None)?;

    // Check for chroot.
    if !empty_path && sandbox.is_chroot() {
        return Err(Errno::ENOENT);
    }

    // Call sandbox access checker if Exec sandboxing is on.
    let caps = sandbox.getcaps(Capability::CAP_EXEC | Capability::CAP_TPE);
    if caps.contains(Capability::CAP_EXEC) {
        sandbox_path(
            None,
            sandbox,
            pid,
            path.tgid,
            path.abs(),
            Capability::CAP_EXEC,
            syscall.name(),
        )?;
    }

    if !arg.fsflags.follow_last()
        && path
            .typ
            .as_ref()
            .map(|typ| typ.is_symlink() || typ.is_magic_link())
            .unwrap_or(false)
    {
        // AT_SYMLINK_NOFOLLOW: If the file identified by dirfd and a
        // non-NULL pathname is a symbolic link, then the call fails
        // with the error ELOOP.
        return Err(Errno::ELOOP);
    }

    // Return EACCES without any more processing if the file is not a
    // regular file or a memory fd. Mfd check depends on
    // trace/allow_unsafe_memfd option.
    //
    // Linux rejects attempts to execute directories with EACCES.
    match path.typ.as_ref() {
        Some(FileType::Reg) => {}
        Some(FileType::Mfd) if sandbox.options.allow_unsafe_memfd() => {}
        _ => return Err(Errno::EACCES),
    };

    // Reject without any more processing if file isn't executable.
    check_executable(path.dir())?;

    // Check SegvGuard.
    if let Some(action) = cache.check_segvguard(
        path.abs(),
        sandbox.segvguard_act(),
        sandbox.get_segvguard_expiry(),
    ) {
        if action != Action::Filter {
            let (_, bin) = path.abs().split();
            error!("ctx": "exec", "op": "segvguard",
                "msg": xfmt!("max crashes {} exceeded, execution of `{bin}' denied",
                    sandbox.segvguard_maxcrashes),
                "tip": "increase `segvguard/maxcrashes'",
                "pid": pid.as_raw(), "path": path.abs());
        }
        enforce_action(action, Errno::EACCES, pid)?;
    }

    // Trusted Path Execution.
    if caps.contains(Capability::CAP_TPE) {
        let (action, msg) = sandbox.check_tpe(path.dir(), path.abs())?;
        if !matches!(action, Action::Allow | Action::Filter) {
            // TODO: Fix proc_mmap to work in ptrace hooks.
            let msg = msg.as_deref().unwrap_or("?");
            error!("ctx": "exec", "op": "check_tpe",
                "msg": xfmt!("exec from untrusted path blocked: {msg}"),
                "pid": pid.as_raw(), "path": path.abs(),
                "sys": syscall, "arch": info.arch, "args": data.args,
                "tip": "move the binary to a safe location or use `sandbox/tpe:off'");
        }
        enforce_action(action, Errno::EACCES, pid)?;
    }

    // Verify executable content as necessary.
    if sandbox.options.deny_exec_script()
        || sandbox.options.deny_exec_elf32()
        || sandbox.options.deny_exec_elf_dynamic()
        || sandbox.options.deny_exec_elf_static()
        || !sandbox.options.allow_unsafe_exec_ldso()
        || !sandbox.options.allow_unsafe_exec_nopie()
        || !sandbox.options.allow_unsafe_exec_stack()
        || sandbox.enabled(Capability::CAP_FORCE)
    {
        verify_exec(sandbox, path, pid)?;
    }

    Ok(())
}

#[expect(clippy::cognitive_complexity)]
fn verify_exec(sandbox: &SandboxGuard, mut path: CanonicalPath, pid: Pid) -> Result<(), Errno> {
    // 1. Reopen file as read-only.
    // 2. Use O_NOCTTY to avoid acquiring controlling terminal.
    let mut fd = match path.take_dir() {
        MaybeFd::Owned(fd) => {
            let pfd = XPathBuf::from_self_fd(fd.as_raw_fd())?;
            let pfl = OFlag::O_RDONLY | OFlag::O_NOCTTY;
            safe_open_msym(PROC_FILE(), &pfd, pfl, ResolveFlag::empty())?
        }
        _ => return Err(Errno::ENOEXEC),
    };

    // Parse ELF as necessary for restrictions.
    let deny_script = sandbox.options.deny_exec_script();
    let restrict_32 = sandbox.options.deny_exec_elf32();
    let restrict_dyn = sandbox.options.deny_exec_elf_dynamic();
    let restrict_sta = sandbox.options.deny_exec_elf_static();
    let restrict_ldd = !sandbox.options.allow_unsafe_exec_ldso();
    let restrict_pie = !sandbox.options.allow_unsafe_exec_nopie();
    let restrict_xs = !sandbox.options.allow_unsafe_exec_stack();

    let check_linking = restrict_ldd || restrict_dyn || restrict_sta || restrict_pie || restrict_xs;

    // Parse ELF.
    let exe = ExecutableFile::parse(&mut fd, check_linking)?;

    let is_script = exe == ExecutableFile::Script;
    if is_script && deny_script {
        warn!("ctx": "exec", "op": "deny_exec_script", "pid": pid.as_raw(),
            "path": path.abs(), "exe": xfmt!("{exe}"),
            "msg": "script execution denied",
            "tip": "configure `trace/deny_exec_script:0'");
        return Err(Errno::EACCES);
    }

    if !is_script
        && restrict_ldd
        && !matches!(
            exe,
            ExecutableFile::Elf {
                file_type: ElfFileType::Executable,
                ..
            }
        )
    {
        warn!("ctx": "exec", "op": "check_elf", "pid": pid.as_raw(),
            "path": path.abs(), "exe": xfmt!("{exe}"),
            "msg": "ld.so(8) exec-indirection prevented",
            "tip": "fix your program or configure `trace/allow_unsafe_exec_ldso:1'");
        return Err(Errno::EACCES);
    }

    if !is_script && restrict_pie && matches!(exe, ExecutableFile::Elf { pie: false, .. }) {
        warn!("ctx": "exec", "op": "check_elf", "pid": pid.as_raw(),
            "path": path.abs(), "exe": xfmt!("{exe}"),
            "msg": "ELF is not Position Independent Executable (PIE)",
            "tip": "fix your program or configure `trace/allow_unsafe_exec_nopie:1'");
        return Err(Errno::EACCES);
    }

    if !is_script && restrict_xs && matches!(exe, ExecutableFile::Elf { xs: true, .. }) {
        error!("ctx": "exec", "op": "check_elf", "pid": pid.as_raw(),
            "path": path.abs(), "exe": xfmt!("{exe}"),
            "msg": "ELF has executable stack",
            "tip": "fix your program or configure `trace/allow_unsafe_exec_stack:1'");
        return Err(Errno::EACCES);
    }

    if !is_script
        && restrict_32
        && matches!(
            exe,
            ExecutableFile::Elf {
                elf_type: ElfType::Elf32,
                ..
            }
        )
    {
        error!("ctx": "exec", "op": "check_elf", "pid": pid.as_raw(),
            "path": path.abs(), "exe": xfmt!("{exe}"),
            "msg": "32-bit ELF execution prevented",
            "tip": "configure `trace/deny_exec_elf32:0'");
        return Err(Errno::EACCES);
    }

    if !is_script
        && restrict_dyn
        && matches!(
            exe,
            ExecutableFile::Elf {
                linking_type: Some(LinkingType::Dynamic),
                ..
            }
        )
    {
        warn!("ctx": "exec", "op": "check_elf", "pid": pid.as_raw(),
            "path": path.abs(), "exe": xfmt!("{exe}"),
            "msg": "dynamic linked ELF execution prevented",
            "tip": "configure `trace/deny_exec_elf_dynamic:0'");
        return Err(Errno::EACCES);
    }

    if !is_script
        && restrict_sta
        && matches!(
            exe,
            ExecutableFile::Elf {
                linking_type: Some(LinkingType::Static),
                ..
            }
        )
    {
        warn!("ctx": "exec", "op": "check_elf", "pid": pid.as_raw(),
            "path": path, "exe": xfmt!("{exe}"),
            "msg": "static linked ELF execution prevented",
            "tip": "configure `trace/deny_exec_elf_static:0'");
        return Err(Errno::EACCES);
    }

    // Check for Force sandboxing.
    if sandbox.enabled(Capability::CAP_FORCE) {
        // Reset the file offset and calculate checksum.
        fd.rewind().map_err(|err| err2no(&err))?;

        match sandbox.check_force2(fd, path.abs()) {
            Ok(action) => {
                match action {
                    Action::Allow | Action::Filter => {}
                    Action::Exit => {
                        error!("ctx": "exec", "op": "verify_elf", "act": action,
                            "pid": pid.as_raw(), "path": path.abs(),
                            "tip": xfmt!("configure `force+{path}:<algorithm>:<checksum>'"));
                    }
                    // Warn|Abort|Deny|Panic|Stop|Kill
                    _ => {
                        warn!("ctx": "exec", "op": "verify_elf", "act": action,
                            "pid": pid.as_raw(), "path": path.abs(),
                            "tip": xfmt!("configure `force+{path}:<algorithm>:<checksum>'"));
                    }
                }
                enforce_action(action, Errno::EACCES, pid)?;
            }
            Err(IntegrityError::Sys(errno)) => {
                error!("ctx": "exec", "op": "verify_elf", "act": Action::Deny,
                    "pid": pid.as_raw(), "path": path.abs(),
                    "msg": xfmt!("system error during ELF checksum calculation: {errno}"),
                    "tip": xfmt!("configure `force+{path}:<algorithm>:<checksum>'"));
                return Err(Errno::EACCES);
            }
            Err(IntegrityError::Hash {
                action,
                expected,
                found,
            }) => {
                if !matches!(action, Action::Allow | Action::Filter) {
                    warn!("ctx": "exec", "op": "verify_elf", "act": action,
                        "pid": pid.as_raw(), "path": path.abs(),
                        "msg": xfmt!("ELF checksum mismatch: {found} is not {expected}"),
                        "tip": xfmt!("configure `force+{path}:<algorithm>:<checksum>'"));
                }
                enforce_action(action, Errno::EACCES, pid)?;
            }
        }
    }

    Ok(())
}