syd 3.55.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::Signal, unistd::Pid};

use crate::{
    compat::WaitStatus,
    confine::is_coredump,
    error, info,
    lookup::{FileMapEntry, SysInfo},
    magic::ProcMagic,
    proc::proc_tgid,
    ptrace::ptrace_cont,
    sandbox::Sandbox,
    workers::WorkerCache,
    xfmt,
};

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

    // Determine whether SegvGuard must record this exit.
    let sig = if has_segvguard && !(is_child && !wait_all) {
        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.
        }
    } else {
        None
    };

    // Domain moves for transit happen on group-exit.
    let move_on_exit = has_exit_moves && proc_tgid(pid) == Ok(pid);

    // Resolve proc_pid_exe(5) of exiting process.
    // TODO: Handle long paths.
    let exe_path = if sig.is_some() || move_on_exit {
        FileMapEntry::from_magic_link(
            ProcMagic::Exe { pid },
            false,
            &SysInfo {
                request: None,
                sandbox: None,
            },
        )
        .and_then(|(entry, _)| entry.target.unwrap_or(Err(Errno::ENOENT)))
    } else {
        Err(Errno::ENOSYS)
    };

    // Apply SegvGuard.
    if let Some(sig) = sig {
        let path = match exe_path.as_ref() {
            Ok(path) => path,
            Err(Errno::ESRCH) => return,
            Err(_) => {
                let _ = ptrace_cont(pid, None);
                return;
            }
        };

        // 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(_) => {
                let _ = ptrace_cont(pid, None);
                return;
            }
        };

        // 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": xfmt!("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": xfmt!("{num_crashes} {crashes} recorded after {signal}{}",
                    if was_suspended { " (suspended)" } else { "" }),
                "pid": pid.as_raw(), "path": path, "sig": sig);
        }
    }

    // Move domain on exit as necessary.
    if move_on_exit {
        match exe_path.as_ref() {
            Ok(path) => {
                sandbox
                    .read()
                    .unwrap_or_else(|err| err.into_inner())
                    .move_on_exit(path);
            }
            Err(Errno::ESRCH) => return,
            _ => {} // Fall through to ptrace_cont
        }
    }

    #[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);
}