syd 3.52.0

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

use std::sync::{Arc, RwLock};

use libc::pid_t;
use nix::{
    sys::signal::{kill, killpg, Signal},
    unistd::{getpgid, getpgrp, Pid},
};

use crate::{
    error,
    proc::{proc_task_limit, proc_task_nr_syd, proc_task_nr_sys},
    ptrace::{ptrace_cont, ptrace_getevent},
    sandbox::{Action, Capability, Sandbox, SandboxGuard},
    warn,
};

#[expect(clippy::cognitive_complexity)]
pub(crate) fn sysevent_fork(pid: Pid, sandbox: &Arc<RwLock<Sandbox>>) {
    let mut _child_tid: Option<Pid> = None;

    #[cfg(feature = "kcov")]
    {
        // KCOV: Inherit KCOV mapping from parent to child.
        #[expect(clippy::cast_possible_truncation)]
        if let Ok(child_tid) = ptrace_getevent(pid).map(|p| Pid::from_raw(p as libc::pid_t)) {
            crate::kcov::inherit_kcov_tid(pid, child_tid);
            crate::kcov::abi::kcov_attach(child_tid);
            crate::kcov::abi::kcov_set_syscall(libc::SYS_clone);
            let _ = crate::kcov::abi::kcov_enter_for(child_tid);
            crate::kcov_edge!();

            _child_tid = Some(child_tid);
        }
    }

    // Read-lock the sandbox.
    let sandbox = SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));

    // Check for PID sandboxing.
    //
    // sandbox/pid may be used dynamically after startup.
    if !sandbox.enabled(Capability::CAP_PID) {
        // PID sandboxing disabled,
        // nothing else to do.
        let _ = ptrace_cont(pid, None);
        return;
    }

    let pid_max = if sandbox.pid_max > 0 {
        sandbox.pid_max
    } else {
        // pid/max:0 disables PID sandboxing.
        // pid/max may be used dynamically after startup.
        let _ = ptrace_cont(pid, None);
        return;
    };
    let pid_act = sandbox.default_action(Capability::CAP_PID);
    drop(sandbox); // release the read lock.

    // Check for PID limit.
    let errno = match proc_task_limit(pid, pid_max) {
        Ok(false) => {
            // Limit not exceeded, continue process.
            let _ = ptrace_cont(pid, None);
            return;
        }
        Ok(true) => None,                 // Limit exceeded.
        Err(errno) => Some(errno as i32), // Error during limit check.
    };

    // Report error as necessary.
    let pgid = getpgid(Some(pid)).map(|p| p.as_raw()).unwrap_or(0);
    let syd_pgid = getpgrp().as_raw();
    let kill_gid = pgid != 0 && pgid != syd_pgid;
    if pid_act != Action::Filter {
        let cnt_sys = proc_task_nr_sys().unwrap_or(0);
        let cnt_syd = proc_task_nr_syd().unwrap_or(0);
        let syd_pid = Pid::this().as_raw();
        #[expect(clippy::cast_possible_truncation)]
        let cpid = ptrace_getevent(pid)
            .map(|p| Pid::from_raw(p as pid_t))
            .unwrap_or(pid);
        match pid_act {
            // Allow|Deny|Filter|Panic cannot happen.
            action if action.is_signaling() => {
                // is_signaling() ensures signal() returns Some.
                #[expect(clippy::disallowed_methods)]
                let kill_sig = action.signal().unwrap();
                let kill_it = if kill_gid {
                    format!("kill process group {pgid} with {kill_sig}")
                } else {
                    format!("kill process {pid} with {kill_sig}")
                };
                error!("ctx": "limit_pid",
                    "msg": format!("process limit {pid_max} reached, {kill_it}"),
                    "err": errno.unwrap_or(0), "tip": "increase `pid/max'",
                    "pid_max": pid_max, "sig": kill_sig as libc::c_int,
                    "sys_tasks": cnt_sys,  "syd_tasks": cnt_syd,
                    "pid": cpid.as_raw(), "ppid": pid.as_raw(), "pgid": pgid,
                    "syd_pid": syd_pid, "syd_pgid": syd_pgid);
            }
            Action::Warn => {
                warn!("ctx": "pid_limit",
                    "msg": format!("process limit {pid_max} reached with pid {pid}"),
                    "err": errno.unwrap_or(0), "tip": "increase `pid/max'",
                    "sys_tasks": cnt_sys,  "syd_tasks": cnt_syd,
                    "pid": cpid.as_raw(), "ppid": pid.as_raw(), "pgid": pgid,
                    "syd_pid": syd_pid, "syd_pgid": syd_pgid);
            }
            Action::Exit => {
                let act = pid_act.to_string().to_ascii_lowercase();
                error!("ctx": "limit_pid",
                    "msg": format!("process limit {pid_max} reached with pid {cpid}, {act}ing!"),
                    "err": errno.unwrap_or(0), "tip": "increase `pid/max'",
                    "sys_tasks": cnt_sys,  "syd_tasks": cnt_syd,
                    "pid": cpid.as_raw(), "ppid": pid.as_raw(), "pgid": pgid,
                    "syd_pid": syd_pid, "syd_pgid": syd_pgid);
            }
            _ => unreachable!(),
        };
    }

    #[cfg(feature = "kcov")]
    {
        if let Some(child_tid) = _child_tid {
            crate::kcov_edge!();
            let _ = crate::kcov::abi::kcov_exit_for(child_tid);
        }
    }

    let kill_sig = match pid_act {
        // Allow|Deny|Panic cannot happen.
        action if action.is_signaling() => action.signal(),
        Action::Filter => Some(Signal::SIGKILL),
        Action::Warn => None,
        Action::Exit => std::process::exit(errno.unwrap_or(libc::EACCES)),
        _ => unreachable!(),
    };

    // Send signal to the process group, unless process shares their
    // process group with the current process.
    if let Some(kill_sig) = kill_sig {
        if kill_gid {
            let _ = killpg(Pid::from_raw(pgid), Some(kill_sig));
        } else {
            let _ = kill(pid, Some(kill_sig));
        }
    } else {
        let _ = ptrace_cont(pid, None);
    }
}