syd 3.53.0

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

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

use nix::{
    errno::Errno,
    sys::signal::{kill, Signal},
    unistd::Pid,
};

use crate::{
    compat::{readlinkat, WaitStatus},
    confine::is_coredump,
    error,
    fd::PROC_FILE,
    info,
    path::XPathBuf,
    ptrace::{ptrace_cont, ptrace_getevent},
    sandbox::{Sandbox, SandboxGuard},
    workers::WorkerCache,
};

#[expect(clippy::cognitive_complexity)]
pub(crate) fn sysevent_exit(
    pid: Pid,
    cpid: Pid,
    wait_all: bool,
    cache: &Arc<WorkerCache>,
    sandbox: &Arc<RwLock<Sandbox>>,
) -> Option<u8> {
    #[cfg(feature = "kcov")]
    {
        crate::kcov::abi::kcov_attach(pid);
        crate::kcov::abi::kcov_set_syscall(libc::SYS_exit);
        let _ = crate::kcov::abi::kcov_enter_for(pid);
        crate::kcov_edge!();
    }

    // ptrace(2) stop before return from exit(2).
    // This stop is per-thread.
    #[expect(clippy::cast_possible_truncation)]
    let status = match ptrace_getevent(pid) {
        Ok(status) => WaitStatus::from_raw(pid, status as i32),
        Err(Errno::ESRCH) => return None,
        Err(errno) => {
            error!("ctx": "exit", "op": "ptrace_getevent",
                "msg": format!("error reading exit status with ptrace: {errno}"),
                "err": errno as i32, "pid": pid.as_raw(),
                "tip": "check with SYD_LOG=debug and/or submit a bug report");
            let _ = kill(pid, Some(Signal::SIGKILL));
            return None;
        }
    };

    // Track eldest child exit.
    let mut xcode = None;
    let is_child = pid == cpid;

    match status {
        WaitStatus::Exited(_, exit_code) => {
            cache.del_tgid(pid);

            if is_child {
                xcode = Some(exit_code.try_into().unwrap_or(127));
            }
        }
        WaitStatus::Signaled(_, signal, _) => {
            cache.del_tgid(pid);

            if is_child {
                xcode = Some(128_i32.saturating_add(signal).try_into().unwrap_or(128));
            }
        }
        _ => {}
    }

    // Apply SegvGuard.
    let has_segvguard = if is_child && !wait_all {
        false
    } else {
        let my_sandbox = SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));
        !my_sandbox.get_segvguard_expiry().is_zero()
    };

    // Setting expiry timeout to 0 disables SegvGuard.
    if has_segvguard {
        // 1. Check if process produced a core dump.
        // 2. Check if process received a signal with default action Core.
        let sig = match status {
            WaitStatus::Signaled(_, sig, true) => Some(sig),
            WaitStatus::Signaled(_, sig, _) if is_coredump(sig) => Some(sig),
            _ => None, // Process did not produce a core dump, move on.
        };

        // Record the crash as necessary.
        if let Some(sig) = sig {
            // Child received a signal that produces a coredump and
            // SegvGuard is enabled. Add the exec path to the segvguard
            // expiry map.
            let path = match XPathBuf::from_exe(pid).and_then(|exe| readlinkat(PROC_FILE(), &exe)) {
                Ok(path) => path,
                Err(_) => return xcode,
            };

            // Upgrade the sandbox lock to writable.
            let mut my_sandbox =
                SandboxGuard::Write(sandbox.write().unwrap_or_else(|err| err.into_inner()));

            // Record the crashing program.
            let (was_suspended, is_suspended, num_crashes) = my_sandbox.add_segvguard_crash(&path);

            drop(my_sandbox); // release the write-lock.

            // Convert sig to Signal for pretty printing.
            // Note, `Signal` does not support realtime signals,
            // therefore we log the original raw signal number
            // as well.
            let signal = Signal::try_from(sig).unwrap_or(Signal::SIGKILL);
            let crashes = if num_crashes > 1 { "crashes" } else { "crash" };
            if is_suspended {
                error!("ctx": "segvguard",
                    "msg": format!("suspending after {signal} due to {num_crashes} {crashes}"),
                    "tip": "increase `segvguard/maxcrashes'",
                    "pid": pid.as_raw(), "path": path, "sig": sig);
            } else {
                info!("ctx": "segvguard",
                    "msg": format!("{num_crashes} {crashes} recorded after {signal}{}",
                        if was_suspended { " (suspended)" } else { "" }),
                    "pid": pid.as_raw(), "path": path, "sig": sig);
            }
        }
    }

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

    // Continue the process so it exits cleanly.
    let _ = ptrace_cont(pid, None);

    // Return exit code of eldest child if available.
    xcode
}