syd 3.54.1

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::{sys::signal::Signal, unistd::Pid};

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

#[expect(clippy::cognitive_complexity)]
pub(crate) fn sysevent_exit(
    pid: Pid,
    status: WaitStatus,
    wait_all: bool,
    cache: &Arc<WorkerCache>,
    sandbox: &Arc<RwLock<Sandbox>>,
) -> Option<SydNotifResp> {
    #[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.
    let my_sandbox = SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));
    let is_child = pid == my_sandbox.get_child_pid();
    let has_segvguard = !my_sandbox.get_segvguard_expiry().is_zero();
    let segvguard_expiry = my_sandbox.get_segvguard_expiry();
    let segvguard_suspension = my_sandbox.get_segvguard_suspension();
    let segvguard_maxcrashes = my_sandbox.get_segvguard_maxcrashes();
    drop(my_sandbox); // release read lock.

    // Apply SegvGuard.
    if has_segvguard && !(is_child && !wait_all) {
        // 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 exec path to segvguard expiry
            // map. TODO: Handle long paths for SegvGuard.
            let path = match XPathBuf::from_exe(pid).and_then(|exe| readlinkat(PROC_FILE(), &exe)) {
                Ok(path) => path,
                Err(_) => return Some(SydNotifResp::Cont { pid, signal: None }),
            };

            // Record crashing program.
            let (was_suspended, is_suspended, num_crashes) = match cache.add_segvguard_crash(
                &path,
                segvguard_expiry,
                segvguard_suspension,
                segvguard_maxcrashes,
            ) {
                Ok(result) => result,
                Err(_) => return Some(SydNotifResp::Cont { pid, signal: None }),
            };

            // Convert sig to Signal for pretty printing.
            // nix' 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.
    Some(SydNotifResp::Cont { pid, signal: None })
}