//
// 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::sync::{
    atomic::{AtomicBool, Ordering},
    Arc, RwLock,
};

use libseccomp::{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_execveat, confine_scmp_faccessat2, confine_scmp_fcntl, confine_scmp_ioctl_syd,
        confine_scmp_madvise, confine_scmp_open, confine_scmp_openat, confine_scmp_openat2,
        confine_scmp_prctl, confine_scmp_sigaction, confine_scmp_write, scmp_add_setid_rules,
    },
    err::SydResult,
    info,
    kernel::ptrace::event::{
        exec::sysevent_exec, exit::sysevent_exit, fork::sysevent_fork, scmp::sysevent_scmp,
        sig::sysevent_sig, sysx::sysevent_sysx,
    },
    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.
    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_cookie = !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.
        confine_scmp_open(&mut ctx)?;

        // openat(2) may be used to open the parent directory only by getdir_long().
        confine_scmp_openat(&mut ctx)?;

        // openat2(2) may be used only with syscall argument cookies.
        confine_scmp_openat2(&mut ctx, restrict_cookie)?;

        // 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.
        confine_scmp_fcntl(&mut ctx, MAIN_FCNTL_OPS)?;

        // Allow safe prctl(2) operations.
        confine_scmp_prctl(&mut ctx, MAIN_PRCTL_OPS)?;

        // Allow ioctl(2) request PROCMAP_QUERY to lookup proc_pid_maps(5) efficiently.
        // This request is new in Linux-6.11.
        confine_scmp_ioctl_syd(&mut ctx, restrict_cookie, None /*seccomp_fd*/)?;

        // Deny installing new signal handlers for {rt_,}sigaction(2).
        confine_scmp_sigaction(&mut ctx)?;

        // Allow safe system calls.
        //
        // Note, `PROF_SYSCALLS` is empty in case `prof` feature is disabled.
        for sysname in MAIN_SYSCALLS
            .iter()
            .chain(FUTEX_SYSCALLS)
            .chain(GETID_SYSCALLS)
            .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}"));
            }
        }

        // Allow execveat(2) with AT_EXECVE_CHECK for Linux>=6.14.
        confine_scmp_execveat(&mut ctx, restrict_cookie)?;

        // Allow faccessat2(2) system call.
        confine_scmp_faccessat2(&mut ctx, restrict_cookie)?;

        // 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)
    }
}
