//
// Syd: rock-solid application kernel
// src/kernel/ptrace/mod.rs: ptrace(2) hooks
//
// Copyright (c) 2023, 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

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

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

use crate::{
    config::{
        PTRACE_DATA_CHDIR, PTRACE_DATA_EXECVE, PTRACE_DATA_EXECVEAT, PTRACE_DATA_FCHDIR,
        PTRACE_DATA_MMAP, PTRACE_DATA_MMAP2, PTRACE_DATA_RT_SIGRETURN, PTRACE_DATA_SIGRETURN,
    },
    confine::{scmp_arch, SydArch},
    error,
    kernel::ptrace::{
        chdir::{sysenter_chdir, sysenter_fchdir, sysexit_chdir},
        exec::sysenter_exec,
        mmap::{sysenter_mmap, sysexit_mmap},
    },
    proc::{proc_maps, proc_status},
    ptrace::{ptrace_set_return, ptrace_skip_syscall, ptrace_syscall_info},
    req::RemoteProcess,
    sandbox::{Action, Capability, Sandbox, SandboxGuard},
    workers::WorkerCache,
};

// ptrace chdir handlers
pub(crate) mod chdir;

// ptrace exec handlers
pub(crate) mod exec;

// ptrace mmap handlers
pub(crate) mod mmap;

// ptrace event handlers
pub(crate) mod event;

#[expect(clippy::cognitive_complexity)]
pub(crate) fn handle_ptrace_sysenter(
    pid: Pid,
    info: ptrace_syscall_info,
    cache: &Arc<WorkerCache>,
    sandbox: &Arc<RwLock<Sandbox>>,
) -> Result<(), Errno> {
    #[expect(clippy::disallowed_methods)]
    let arch: SydArch = scmp_arch(info.arch).unwrap().into();

    #[expect(clippy::disallowed_methods)]
    let info_scmp = info.seccomp().unwrap();

    #[expect(clippy::cast_possible_truncation)]
    let scmp_trace_data = info_scmp.ret_data as u16;

    match scmp_trace_data {
        PTRACE_DATA_CHDIR | PTRACE_DATA_FCHDIR => {
            // Acquire a read lock to the sandbox.
            let my_sandbox =
                SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));

            if !my_sandbox.enabled(Capability::CAP_CHDIR) {
                // SAFETY: Chdir sandboxing is not enabled,
                // continue the system call without any
                // checking.
                return Err(Errno::ECANCELED);
            }

            let result = if scmp_trace_data == PTRACE_DATA_CHDIR {
                sysenter_chdir(pid, &my_sandbox, arch.into(), info_scmp)
            } else {
                sysenter_fchdir(pid, &my_sandbox, arch.into(), info_scmp)
            };

            drop(my_sandbox); // release the read lock.

            if let Err(errno) = result {
                // Set system call to -1 to skip the system call.
                // Write error value into the return register.
                return if let Err(errno) = ptrace_skip_syscall(pid, info.arch, Some(errno)) {
                    // SAFETY: Failed to set return value, terminate the process.
                    if errno != Errno::ESRCH {
                        let _ = kill(pid, Some(Signal::SIGKILL));
                    }
                    Err(Errno::ESRCH)
                } else if cfg!(any(
                    target_arch = "mips",
                    target_arch = "mips32r6",
                    target_arch = "mips64",
                    target_arch = "mips64r6",
                    target_arch = "s390x"
                )) {
                    // Skip to syscall-stop to write return value.
                    cache.add_error(pid, Some(errno));
                    Ok(())
                } else {
                    // Continue process.
                    Err(Errno::ECANCELED)
                };
            }

            // Record the chdir result.
            cache.add_chdir(pid);

            // Stop at syscall exit.
            Ok(())
        }
        PTRACE_DATA_MMAP | PTRACE_DATA_MMAP2 => {
            // Acquire a read lock to the sandbox.
            let my_sandbox =
                SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));

            let res = sysenter_mmap(pid, &my_sandbox, info);
            let exe = my_sandbox.enabled(Capability::CAP_EXEC);

            drop(my_sandbox); // release the read lock.

            match (res, exe) {
                (Ok(()), false) => {
                    // Exec sandboxing disabled, continue process.
                    Err(Errno::ECANCELED)
                }
                (Ok(()), true) => {
                    // Record mmap(2) pid for syscall-exit exec sandbox check.
                    cache.add_mmap(pid);
                    Ok(()) // Stop at syscall-exit.
                }
                (Err(errno), _) => {
                    // Record errno(3) to restore at syscall-exit.
                    cache.add_error(pid, Some(errno));
                    Ok(()) // Stop at syscall-exit.
                }
            }
        }
        PTRACE_DATA_EXECVE | PTRACE_DATA_EXECVEAT => {
            // Acquire a read lock to the sandbox.
            let my_sandbox =
                SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));

            // Call the system call handler, and record the result.
            let result = sysenter_exec(pid, &my_sandbox, info);

            drop(my_sandbox); // release the read lock.

            let (file, exe) = match result {
                Ok((file, exe)) => (file, exe),
                Err(errno) => {
                    // AT_EXECVE_CHECK success is indicated by ECANCELED.
                    // See sysenter_exec.
                    let errno = if errno == Errno::ECANCELED {
                        None
                    } else {
                        Some(errno)
                    };
                    // Set system call to -1 to skip the system call.
                    // Write error value into the return register.
                    return if let Err(errno) = ptrace_skip_syscall(pid, info.arch, errno) {
                        // SAFETY: Failed to set return value, terminate the process.
                        if errno != Errno::ESRCH {
                            error!("ctx": "skip_syscall",
                                "msg": format!("skip exec syscall error: {errno}"),
                                "err": errno as i32,
                                "tip": "check with SYD_LOG=debug and/or submit a bug report");
                            let _ = kill(pid, Some(Signal::SIGKILL));
                        }
                        Err(Errno::ESRCH)
                    } else if cfg!(any(
                        target_arch = "mips",
                        target_arch = "mips32r6",
                        target_arch = "mips64",
                        target_arch = "mips64r6",
                        target_arch = "s390x"
                    )) {
                        // Skip to syscall-stop to write return value.
                        cache.add_error(pid, errno);
                        Ok(())
                    } else {
                        // Continue process.
                        Err(Errno::ECANCELED)
                    };
                }
            };

            // Record the exec result.
            //
            // SAFETY: Terminate the process on errors.
            cache.add_exec(pid, exe, file);

            // Continue process, it will stop at EVENT_EXEC.
            Err(Errno::ECANCELED)
        }
        PTRACE_DATA_SIGRETURN | PTRACE_DATA_RT_SIGRETURN => {
            // Entry to sigreturn(2) or rt_sigreturn(2).
            //
            // SAFETY: Signal handlers are per-process not per-thread!
            let status = match proc_status(pid) {
                Ok(status) => status,
                Err(_) => {
                    // SAFETY: Failed to get TGID,
                    // terminate the process.
                    let _ = kill(pid, Some(Signal::SIGKILL));
                    return Err(Errno::ESRCH);
                }
            };

            // SAFETY: Check for signal counts for SROP mitigation.
            let tgid = status.pid;
            if cache.dec_sig_handle(tgid) {
                // Signal return has a corresponding signal.
                // All good, continue process normally.
                return Err(Errno::ECANCELED);
            }

            // !!! SIGRETURN W/O SIGNAL AKA SROP !!!
            //
            // Check sandbox verbosity.
            // Verbose logging is intended for malware analysis.
            let log_scmp = {
                SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner())).log_scmp()
            };

            // Read memory maps for logging.
            let memmap = if log_scmp { proc_maps(pid).ok() } else { None };

            // Read memory pointed by IP and SP.
            let ip = info.instruction_pointer;
            let sp = (info.stack_pointer & !0xF).saturating_sub(16);
            let ip_mem = if log_scmp { Some([0u8; 64]) } else { None };
            let sp_mem = if log_scmp { Some([0u8; 64]) } else { None };
            let process = RemoteProcess::new(pid);

            #[expect(clippy::disallowed_methods)]
            let arch: SydArch = scmp_arch(info.arch).unwrap().into();
            let is_realtime = scmp_trace_data == PTRACE_DATA_RT_SIGRETURN;

            if let Some(mut ip_mem) = ip_mem {
                // SAFETY: This is a ptrace hook, the PID cannot be validated.
                let _ = unsafe { process.read_mem(arch.into(), &mut ip_mem, ip, 64) };
            }
            if let Some(mut sp_mem) = sp_mem {
                // SAFETY: ditto.
                let _ = unsafe { process.read_mem(arch.into(), &mut sp_mem, sp, 64) };
            }

            // Terminate the process.
            let _ = kill(pid, Some(Signal::SIGKILL));

            // Log and return ESRCH.
            #[expect(clippy::disallowed_methods)]
            if !log_scmp {
                error!("ctx": "sigreturn", "op": "check_SROP",
                    "msg": "Artificial sigreturn(2) detected: assume SROP!",
                    "act": Action::Kill,
                    "pid": process.pid.as_raw(),
                    "sys": if is_realtime { "rt_sigreturn" } else { "sigreturn" },
                    "arch": arch,
                    "tgid": tgid.as_raw(),
                    "tip": "configure `trace/allow_unsafe_sigreturn:1'");
            } else {
                error!("ctx": "sigreturn", "op": "check_SROP",
                    "msg": "Artificial sigreturn(2) detected: assume SROP!",
                    "act": Action::Kill,
                    "pid": process.pid.as_raw(),
                    "sys": if is_realtime { "rt_sigreturn" } else { "sigreturn" },
                    "args": info_scmp.args,
                    "arch": arch,
                    "tgid": tgid.as_raw(),
                    "sig_caught": status.sig_caught,
                    "sig_blocked": status.sig_blocked,
                    "sig_ignored": status.sig_ignored,
                    "sig_pending_thread": status.sig_pending_thread,
                    "sig_pending_process": status.sig_pending_process,
                    "ip": ip,
                    "sp": sp,
                    "ip_mem": HEXLOWER.encode(ip_mem.as_ref().unwrap()),
                    "sp_mem": HEXLOWER.encode(sp_mem.as_ref().unwrap()),
                    "memmap": memmap,
                    "tip": "configure `trace/allow_unsafe_sigreturn:1'");
            }

            // Process is dead, Jim.
            Err(Errno::ESRCH)
        }

        data => unreachable!("BUG: invalid syscall data {data}!"),
    }
}

pub(crate) fn handle_ptrace_sysexit(
    pid: Pid,
    info: ptrace_syscall_info,
    cache: &Arc<WorkerCache>,
    sandbox: &Arc<RwLock<Sandbox>>,
) -> Result<(), Errno> {
    // Get and remove the syscall entry from the cache,
    // and call the respective syscall handler.
    if cache.get_chdir(pid) {
        let sandbox = SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));
        sysexit_chdir(pid, info, &sandbox)
    } else if cache.get_mmap(pid) {
        let sandbox = SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));
        sysexit_mmap(pid, info, &sandbox)
    } else if let Some((pid, errno)) = cache.get_error(pid) {
        // Architectures like mips, s390x where return value has to be written twice.
        // errno is None for success.
        ptrace_set_return(pid, info.arch, errno)
    } else {
        unreachable!("BUG: Invalid syscall exit stop: {info:?}");
    }
}
