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

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

use nix::{
    sys::{ptrace, signal::Signal},
    unistd::Pid,
};

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

#[expect(clippy::cognitive_complexity)]
pub(crate) fn sysevent_exit(pid: Pid, cache: &Arc<WorkerCache>, sandbox: &Arc<RwLock<Sandbox>>) {
    // We stopped before return from exit(2).
    // Apply SegvGuard.
    let mut my_sandbox = SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));
    let has_segvguard = !my_sandbox.get_segvguard_expiry().is_zero();
    drop(my_sandbox);

    // Setting expiry timeout to 0 disables SegvGuard.
    if has_segvguard {
        // Step 1:
        // (a) Check if process produced a core dump.
        // (b) Check if process received a signal with default action Core.
        let sig = match ptrace::getevent(pid) {
            Ok(status) => {
                #[expect(clippy::cast_possible_truncation)]
                match WaitStatus::from_raw(pid, status as i32) {
                    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.
                }
            }
            Err(_) => None, // Process dead? move on.
        };

        // Step 2: 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,
            };

            // Upgrade the sandbox lock to writable.
            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);
            }
        }
    }

    // Step 3: Remove PID from cache.
    cache.del_pid(pid);

    // Step 4: Continue the process so it exits cleanly.
    let _ = ptrace::cont(pid, None);
}
