syd 3.54.1

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/kernel/ptrace/event/fork.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 nix::{
    sys::signal::{killpg, Signal},
    unistd::{getpgid, getpgrp, Pid},
};

use crate::{
    confine::SydNotifResp,
    cookie::safe_kill,
    error,
    proc::{proc_task_limit, proc_task_nr_syd, proc_task_nr_sys},
    sandbox::{Action, Capability, Sandbox, SandboxGuard},
    warn,
};

#[expect(clippy::cognitive_complexity)]
pub(crate) fn sysevent_fork(
    pid: Pid,
    cpid: Pid,
    sandbox: &Arc<RwLock<Sandbox>>,
) -> Option<SydNotifResp> {
    #[cfg(feature = "kcov")]
    {
        // KCOV: Inherit KCOV mapping from parent to child.
        crate::kcov::inherit_kcov_tid(pid, cpid);
        crate::kcov::abi::kcov_attach(cpid);
        crate::kcov::abi::kcov_set_syscall(libc::SYS_clone);
        let _ = crate::kcov::abi::kcov_enter_for(cpid);
        crate::kcov_edge!();
    }

    // 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.
        return Some(SydNotifResp::Cont { pid, signal: None });
    }

    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.
        return Some(SydNotifResp::Cont { pid, signal: None });
    };
    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.
            return Some(SydNotifResp::Cont { pid, signal: None });
        }
        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();
        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")]
    {
        crate::kcov_edge!();
        let _ = crate::kcov::abi::kcov_exit_for(cpid);
    }

    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 _ = safe_kill(pid, kill_sig as libc::c_int);
        }
        None
    } else {
        Some(SydNotifResp::Cont { pid, signal: None })
    }
}