//
// Syd: rock-solid application kernel
// src/workers/gdb.rs: `syd_main' ptrace(2) thread
//
// Copyright (c) 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
// Based in part upon rusty_pool which is:
//     Copyright (c) Robin Friedli <robinfriedli@icloud.com>
//     SPDX-License-Identifier: Apache-2.0
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    os::fd::RawFd,
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc, RwLock,
    },
};

use libseccomp::{scmp_cmp, ScmpAction, ScmpFilterContext, ScmpSyscall};
use nix::{
    errno::Errno,
    sys::{
        ptrace,
        wait::{Id, WaitPidFlag},
    },
    unistd::{Gid, Pid, Uid},
};

use crate::{
    compat::{waitid, WaitStatus},
    config::*,
    confine::{confine_scmp_madvise, confine_scmp_write, extend_ioctl, scmp_add_setid_rules},
    cookie::{CookieIdx, SYSCOOKIE_POOL},
    err::SydResult,
    fd::AT_EXECVE_CHECK,
    info,
    kernel::ptrace::event::{
        exec::sysevent_exec, exit::sysevent_exit, fork::sysevent_fork, scmp::sysevent_scmp,
        sig::sysevent_sig, sysx::sysevent_sysx,
    },
    path::dotdot_with_nul,
    proc::PROCMAP_QUERY,
    sandbox::{Flags, Sandbox},
    workers::WorkerCache,
};

#[derive(Clone)]
pub(crate) struct Tracer {
    cache: Arc<WorkerCache>,
    sandbox: Arc<RwLock<Sandbox>>,
    should_exit: Arc<AtomicBool>,
}

impl Tracer {
    pub(crate) fn new(
        cache: Arc<WorkerCache>,
        sandbox: Arc<RwLock<Sandbox>>,
        should_exit: Arc<AtomicBool>,
    ) -> Self {
        Self {
            cache,
            sandbox,
            should_exit,
        }
    }

    /// Run the ptrace(2) loop. This is the main entry point.
    pub(crate) fn run(self, child: Pid, wait_all: bool) -> SydResult<u8> {
        // Wait in a loop and push WaitStatus into queue.
        let mut xcode = 127;
        loop {
            match waitid(Id::All, WaitPidFlag::WEXITED | WaitPidFlag::__WNOTHREAD) {
                Ok(WaitStatus::Exited(pid, exit_code)) => {
                    self.cache.del_pid(pid);
                    if pid == child {
                        xcode = exit_code;
                        if !wait_all {
                            break;
                        }
                    }
                }
                Ok(WaitStatus::Signaled(pid, signal, _core)) => {
                    self.cache.del_pid(pid);
                    if pid == child {
                        xcode = 128_i32.saturating_add(signal);
                        if !wait_all {
                            break;
                        }
                    }
                }
                Ok(status) => self.handle(status),
                Err(Errno::EINTR | Errno::EAGAIN) => {}
                Err(Errno::ECHILD) => break,
                Err(errno) => return Err(errno.into()),
            };

            if self.should_exit.load(Ordering::Relaxed) {
                // Time to exit.
                break;
            }
        }

        // Set should_exit to true and notify the syd_aes thread.
        self.should_exit.store(true, Ordering::Relaxed);
        if let Some(ref crypt_map) = self.cache.crypt_map {
            let (aes_map, cvar) = &**crypt_map;
            let _aes_map = aes_map.lock().unwrap_or_else(|e| e.into_inner());
            cvar.notify_one();
        } // Lock is released here.

        Ok(u8::try_from(xcode).unwrap_or(127))
    }

    fn handle(&self, status: WaitStatus) {
        match status {
            // WaitStatus::Exited and WaitStatus::Signaled
            // are handled by caller, therefore they never
            // reach here. We panic if they do.
            WaitStatus::PtraceEvent(
                pid,
                libc::SIGSTOP | libc::SIGTSTP | libc::SIGTTIN | libc::SIGTTOU,
                libc::PTRACE_EVENT_STOP,
            ) => {
                // SAFETY: nix does not have a wrapper for PTRACE_LISTEN,s
                // so we fallback to libc here.
                let _ = unsafe { libc::ptrace(crate::compat::PTRACE_LISTEN, pid.as_raw(), 0, 0) };
            }
            WaitStatus::PtraceEvent(
                pid,
                _, // Can this ever be !SIGTRAP?
                libc::PTRACE_EVENT_STOP,
            ) => {
                // ptrace-stop, do not forward the signal.
                let _ = ptrace::cont(pid, None);
            }
            WaitStatus::PtraceEvent(pid, sig, 0) => {
                sysevent_sig(pid, sig, &self.cache);
            }
            WaitStatus::PtraceEvent(pid, libc::SIGTRAP, libc::PTRACE_EVENT_SECCOMP) => {
                sysevent_scmp(pid, &self.cache, &self.sandbox);
            }
            WaitStatus::PtraceSyscall(pid) => {
                sysevent_sysx(pid, &self.cache, &self.sandbox);
            }
            WaitStatus::PtraceEvent(
                pid,
                libc::SIGTRAP,
                libc::PTRACE_EVENT_CLONE | libc::PTRACE_EVENT_FORK | libc::PTRACE_EVENT_VFORK,
            ) => {
                sysevent_fork(pid, &self.sandbox);
            }
            WaitStatus::PtraceEvent(pid, libc::SIGTRAP, libc::PTRACE_EVENT_EXEC) => {
                sysevent_exec(pid, &self.cache, &self.sandbox);
            }
            WaitStatus::PtraceEvent(pid, libc::SIGTRAP, libc::PTRACE_EVENT_EXIT) => {
                sysevent_exit(pid, &self.cache, &self.sandbox);
            }
            status => panic!("Unhandled wait event: {status:?}"),
        }
    }

    /// Prepare to confine the Tracer threads.
    #[expect(clippy::cognitive_complexity)]
    pub(crate) fn prepare_confine(
        flags: Flags,
        transit_uids: &[(Uid, Uid)],
        transit_gids: &[(Gid, Gid)],
    ) -> SydResult<ScmpFilterContext> {
        let ssb = flags.allow_unsafe_exec_speculative();
        let restrict_sys = !flags.allow_unsafe_nocookie();
        let safe_setuid = flags.allow_safe_setuid();
        let safe_setgid = flags.allow_safe_setgid();
        let safe_setid = safe_setuid || safe_setgid;

        let mut ctx = ScmpFilterContext::new(ScmpAction::KillProcess)?;

        // Enforce the NO_NEW_PRIVS functionality before
        // loading the seccomp filter into the kernel.
        ctx.set_ctl_nnp(true)?;

        // Disable Speculative Store Bypass mitigations
        // with trace/allow_unsafe_exec_speculative:1
        ctx.set_ctl_ssb(ssb)?;

        // DO NOT synchronize filter to all threads.
        // Thread pool confines itself as necessary.
        ctx.set_ctl_tsync(false)?;

        // We kill for bad system call and bad arch.
        ctx.set_act_badarch(ScmpAction::KillProcess)?;

        // Use a binary tree sorted by syscall number if possible.
        let _ = ctx.set_ctl_optimize(2);

        // SAFETY: Do NOT add supported architectures to the filter.
        // This ensures Syd can never run a non-native system call,
        // which we do not need at all.
        // seccomp_add_architectures(&mut ctx)?;

        // Deny open and {l,}stat with ENOSYS rather than KillProcess.
        // We need this because std::thread::spawn has unwanted
        // side-effects such as opening /sys/devices/system/cpu/online
        // on some architectures.
        //
        // Note, we avoid this when profiling is enabled,
        // as gperf requires it to write profiling data.
        for sysname in ["open", "stat", "lstat"] {
            match ScmpSyscall::from_name(sysname) {
                Ok(syscall) => {
                    let action = if !cfg!(feature = "prof") {
                        ScmpAction::Errno(Errno::ENOSYS as i32)
                    } else {
                        ScmpAction::Allow
                    };
                    ctx.add_rule(action, syscall)?;
                }
                Err(_) => {
                    info!("ctx": "confine", "op": "allow_gdb_syscall",
                        "msg": format!("invalid or unsupported syscall {sysname}"));
                }
            }
        }

        // Allow writes to the log-fd and proc_pid_mem(5) as necessary.
        confine_scmp_write(&mut ctx, None, true)?;

        // Allow safe madvise(2) advice.
        confine_scmp_madvise(&mut ctx)?;

        // Allow safe fcntl(2) utility calls.
        for sysname in ["fcntl", "fcntl64"] {
            let syscall = match ScmpSyscall::from_name(sysname) {
                Ok(syscall) => syscall,
                Err(_) => {
                    info!("ctx": "confine", "op": "allow_gdb_syscall",
                        "msg": format!("invalid or unsupported syscall {sysname}"));
                    continue;
                }
            };

            for op in MAIN_FCNTL_OPS {
                ctx.add_rule_conditional(ScmpAction::Allow, syscall, &[scmp_cmp!($arg1 == *op)])?;
            }
        }

        // Allow safe prctl(2) operations.
        let sysname = "prctl";
        if let Ok(syscall) = ScmpSyscall::from_name(sysname) {
            for (_, op) in MAIN_PRCTL_OPS {
                ctx.add_rule_conditional(ScmpAction::Allow, syscall, &[scmp_cmp!($arg0 == *op)])?;
            }
        } else {
            info!("ctx": "confine", "op": "allow_gdb_syscall",
                "msg": format!("invalid or unsupported syscall {sysname}"));
        }

        // Deny installing new signal handlers for {rt_,}sigaction(2).
        for sysname in ["sigaction", "rt_sigaction"] {
            let syscall = match ScmpSyscall::from_name(sysname) {
                Ok(syscall) => syscall,
                Err(_) => {
                    info!("ctx": "confine", "op": "allow_gdb_syscall",
                        "msg": format!("invalid or unsupported syscall {sysname}"));
                    continue;
                }
            };

            // Installs a signal handler if first argument is non-NULL.
            // We deny this case, but allow returning the current handler.
            ctx.add_rule_conditional(ScmpAction::Allow, syscall, &[scmp_cmp!($arg1 == 0)])?;
        }

        // Allow ioctl(2) request PROCMAP_QUERY to lookup proc_pid_maps(5) efficiently.
        // This request is new in Linux-6.11.
        if *HAVE_PROCMAP_QUERY {
            let sysname = "ioctl";
            #[expect(clippy::unnecessary_cast)]
            let ioctl_request = PROCMAP_QUERY as u64;
            #[expect(clippy::useless_conversion)]
            match ScmpSyscall::from_name(sysname) {
                Ok(syscall) => {
                    let mut rules = Vec::with_capacity(if restrict_sys { 4 } else { 1 });
                    if restrict_sys {
                        rules.extend(&[
                            scmp_cmp!($arg3 == SYSCOOKIE_POOL.get(CookieIdx::ProcmapQueryArg3).into()),
                            scmp_cmp!($arg4 == SYSCOOKIE_POOL.get(CookieIdx::ProcmapQueryArg4).into()),
                            scmp_cmp!($arg5 == SYSCOOKIE_POOL.get(CookieIdx::ProcmapQueryArg5).into()),
                        ]);
                    }

                    rules.push(scmp_cmp!($arg1 == ioctl_request));
                    ctx.add_rule_conditional(ScmpAction::Allow, syscall, &rules)?;

                    if let Some(ioctl_request) = extend_ioctl(ioctl_request) {
                        rules.pop();
                        rules.push(scmp_cmp!($arg1 == ioctl_request));
                        ctx.add_rule_conditional(ScmpAction::Allow, syscall, &rules)?;
                    }
                }
                Err(_) => {
                    info!("ctx": "confine", "op": "allow_gdb_syscall",
                        "msg": format!("invalid or unsupported syscall {sysname}"));
                }
            }
        }

        // Allow safe system calls.
        //
        // Note, `PROF_SYSCALLS` is empty in case `prof` feature is disabled.
        for sysname in MAIN_SYSCALLS
            .iter()
            .chain(PROF_SYSCALLS)
            .chain(VDSO_SYSCALLS)
        {
            if let Ok(syscall) = ScmpSyscall::from_name(sysname) {
                ctx.add_rule(ScmpAction::Allow, syscall)?;
            } else {
                info!("ctx": "confine", "op": "allow_gdb_syscall",
                    "msg": format!("invalid or unsupported syscall {sysname}"));
            }
        }

        let sysname = "openat2";
        if restrict_sys {
            // openat2(2) may be used only with syscall argument cookies.
            // We also prevent AT_FDCWD usage as fd argument.
            #[expect(clippy::useless_conversion)]
            match ScmpSyscall::from_name(sysname) {
                Ok(syscall) => {
                    ctx.add_rule_conditional(
                        ScmpAction::Allow,
                        syscall,
                        &[
                            scmp_cmp!($arg0 <= RawFd::MAX as u64),
                            scmp_cmp!($arg4 == SYSCOOKIE_POOL.get(CookieIdx::Openat2Arg4).into()),
                            scmp_cmp!($arg5 == SYSCOOKIE_POOL.get(CookieIdx::Openat2Arg5).into()),
                        ],
                    )?;
                }
                Err(_) => {
                    info!("ctx": "confine", "op": "allow_gdb_syscall",
                        "msg": format!("invalid or unsupported syscall {sysname}"));
                }
            }
        } else {
            // trace_allow_unsafe_nocookie:1
            match ScmpSyscall::from_name(sysname) {
                Ok(syscall) => {
                    ctx.add_rule(ScmpAction::Allow, syscall)?;
                }
                Err(_) => {
                    info!("ctx": "confine", "op": "allow_gdb_syscall",
                        "msg": format!("invalid or unsupported syscall {sysname}"));
                }
            }
        }

        // openat(2) may be used to open the parent directory only by getdir_long()
        // The rest of the attempts are denied with ENOSYS for compat.
        //
        // Note we allow openat when profiling is enabled,
        // as gperf requires it to write profiling data.
        let sysname = "openat";
        match ScmpSyscall::from_name(sysname) {
            Ok(syscall) => {
                if !cfg!(feature = "prof") {
                    let dotdot = dotdot_with_nul();
                    let oflags = (libc::O_RDONLY
                        | libc::O_CLOEXEC
                        | libc::O_DIRECTORY
                        | libc::O_LARGEFILE
                        | libc::O_NOCTTY
                        | libc::O_NOFOLLOW) as u64;
                    ctx.add_rule_conditional(
                        ScmpAction::Allow,
                        syscall,
                        &[
                            scmp_cmp!($arg0 <= RawFd::MAX as u64),
                            scmp_cmp!($arg1 == dotdot),
                            scmp_cmp!($arg2 & oflags == oflags),
                        ],
                    )?;
                    ctx.add_rule_conditional(
                        ScmpAction::Errno(Errno::ENOSYS as i32),
                        syscall,
                        &[scmp_cmp!($arg0 > RawFd::MAX as u64)],
                    )?;
                    ctx.add_rule_conditional(
                        ScmpAction::Errno(Errno::ENOSYS as i32),
                        syscall,
                        &[scmp_cmp!($arg1 != dotdot)],
                    )?;
                } else {
                    // Profiling is enabled, allow openat(2).
                    ctx.add_rule(ScmpAction::Allow, syscall)?;
                }
            }
            Err(_) => {
                info!("ctx": "confine", "op": "allow_gdb_syscall",
                    "msg": format!("invalid or unsupported syscall {sysname}"));
            }
        }

        // Allow futex system calls.
        for sysname in FUTEX_SYSCALLS {
            if let Ok(syscall) = ScmpSyscall::from_name(sysname) {
                ctx.add_rule(ScmpAction::Allow, syscall)?;
            } else {
                info!("ctx": "confine", "op": "allow_gdb_syscall",
                    "msg": format!("invalid or unsupported syscall {sysname}"));
            }
        }

        // Allow getid system calls.
        for sysname in GET_ID_SYSCALLS {
            if let Ok(syscall) = ScmpSyscall::from_name(sysname) {
                ctx.add_rule(ScmpAction::Allow, syscall)?;
            } else {
                info!("ctx": "confine", "op": "allow_gdb_syscall",
                    "msg": format!("invalid or unsupported syscall {sysname}"));
            }
        }

        // Allow execveat(2) with AT_EXECVE_CHECK for Linux>=6.14.
        if *HAVE_AT_EXECVE_CHECK {
            let sysname = "execveat";
            #[expect(clippy::cast_sign_loss)]
            let atcheck = AT_EXECVE_CHECK.bits() as u64;
            match ScmpSyscall::from_name(sysname) {
                Ok(syscall) => {
                    ctx.add_rule_conditional(
                        ScmpAction::Allow,
                        syscall,
                        &[scmp_cmp!($arg4 & atcheck == atcheck)],
                    )?;
                }
                Err(_) => {
                    info!("ctx": "confine", "op": "allow_syscall",
                        "msg": format!("invalid or unsupported syscall {sysname}"));
                }
            }
        }

        // Allow UID/GID changing system calls as necessary.
        if safe_setid {
            scmp_add_setid_rules(
                "main",
                &mut ctx,
                safe_setuid,
                safe_setgid,
                transit_uids,
                transit_gids,
            )?;
        }

        Ok(ctx)
    }
}
