//
// Syd: rock-solid application kernel
// src/kernel/signal.rs: Signal syscall handlers
//
// Copyright (c) 2023, 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use bitflags::bitflags;
use libseccomp::ScmpNotifResp;
use nix::{
    errno::Errno,
    unistd::{getpgid, getpgrp, Pid},
};

use crate::req::UNotifyEventRequest;

bitflags! {
    // Bitflags describing signal system calls
    #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
    struct SignalSyscall: u8 {
        // True if signal is group directed.
        const SIGSYS_GROUP = 1 << 0;
        // True if signal is thread directed.
        const SIGSYS_THREAD = 1 << 1;
        // True if signal is queued.
        const SIGSYS_QUEUE = 1 << 2;
    }
}

impl SignalSyscall {
    fn is_group(self) -> bool {
        self.contains(Self::SIGSYS_GROUP)
    }

    fn is_thread(self) -> bool {
        self.contains(Self::SIGSYS_THREAD)
    }

    fn is_thread_group(self) -> bool {
        self.contains(Self::SIGSYS_GROUP | Self::SIGSYS_THREAD)
    }

    fn is_queue(self) -> bool {
        self.contains(Self::SIGSYS_QUEUE)
    }
}

pub(crate) fn sys_kill(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_signal_handler(request, SignalSyscall::empty())
}

pub(crate) fn sys_tgkill(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_signal_handler(
        request,
        SignalSyscall::SIGSYS_GROUP | SignalSyscall::SIGSYS_THREAD,
    )
}

pub(crate) fn sys_tkill(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_signal_handler(request, SignalSyscall::SIGSYS_THREAD)
}

pub(crate) fn sys_rt_sigqueueinfo(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_signal_handler(request, SignalSyscall::SIGSYS_QUEUE)
}

pub(crate) fn sys_rt_tgsigqueueinfo(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_signal_handler(
        request,
        SignalSyscall::SIGSYS_QUEUE | SignalSyscall::SIGSYS_GROUP | SignalSyscall::SIGSYS_THREAD,
    )
}

pub(crate) fn sys_pidfd_open(request: UNotifyEventRequest) -> ScmpNotifResp {
    // TODO: Recognize PIDFD_THREAD flag.
    syscall_signal_handler(request, SignalSyscall::SIGSYS_THREAD)
}

// Maximum PID (wrap-around limit) supported by the kernel.
//
// On 32-bit platforms this is fixed at 32768.
// On 64-bit platforms it can go up to 2^22 (approximately 4 million).
//
// Note, Syd may be built as 32-bit on a 64-bit platform,
// therefore we always use the 64-bit limit.
const PID_MAX_LIMIT: libc::pid_t = 1 << 22;

/// Handles syscalls related to signal handling, protecting the syd
/// process and their threads from signals.
///
/// # Parameters
///
/// - `request`: User notification request from seccomp.
/// - `thread`: true if the system call is directed to a thread rather
///   than a process.
/// - `group`: true if the system call has both progress group id and
///   process id (tgkill), false otherwise.
/// - `queue`: true if the system call is sigqueueinfo(2) or rt_sigqueueinfo.
///
/// - `ScmpNotifResp`: Response indicating the result of the syscall handling.
fn syscall_signal_handler(request: UNotifyEventRequest, syscall: SignalSyscall) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;

        // Validate pid/tid.
        #[expect(clippy::cast_possible_truncation)]
        let pid = req.data.args[0] as libc::pid_t;
        #[expect(clippy::cast_possible_truncation)]
        let tid = req.data.args[1] as libc::pid_t;

        // See:
        // https://github.com/torvalds/linux/blob/f66bc387efbee59978e076ce9bf123ac353b389c/kernel/signal.c#L1579-L1581
        // wrt. i32::MIN check.
        if pid == i32::MIN {
            return Err(Errno::ESRCH);
        }

        if syscall.is_group() && tid == i32::MIN {
            return Err(Errno::ESRCH);
        }

        // kill(2) allows negative PIDs.
        if syscall.is_empty() && !(-PID_MAX_LIMIT..=PID_MAX_LIMIT).contains(&pid) {
            return Err(Errno::ESRCH);
        }

        // rt_{tg,}sigqueueinfo return ESRCH/EINVAL on negative TGID.
        if syscall.is_queue() && !(1..=PID_MAX_LIMIT).contains(&pid) {
            return Err(if syscall.is_group() {
                if pid > 0 && tid > 0 {
                    Errno::ESRCH
                } else {
                    Errno::EINVAL
                }
            } else {
                Errno::ESRCH
            });
        }

        // tkill(2), tgkill(2) and pidfd_open(2) return EINVAL on negative TGID.
        if syscall.is_thread() && !(1..=PID_MAX_LIMIT).contains(&pid) {
            return Err(if pid > 0 && tid > 0 {
                Errno::ESRCH
            } else {
                Errno::EINVAL
            });
        }

        // tgkill(2) returns EINVAL on negative TID.
        if syscall.is_group() && !(1..=PID_MAX_LIMIT).contains(&tid) {
            return Err(if pid > 0 && tid > 0 {
                Errno::ESRCH
            } else {
                Errno::EINVAL
            });
        }

        // Guard syd tasks.
        //
        // pid <=0 only for kill here.
        if pid == -1 {
            // SAFETY: We do not allow mass signaling with -1.
            // ESRCH errno(3) is consistent with landlock(7) scoped signals.
            return Err(Errno::ESRCH);
        } else if pid == 0 {
            // SAFETY: This is a version of killpg():
            // We must prevent this signal if Syd is in the same process group
            // as the sandbox process, otherwise continue is safe.
            return match getpgid(Some(req.pid())) {
                Ok(pgrp) if pgrp == getpgrp() => Err(Errno::EPERM),
                Err(_) => Err(Errno::ESRCH),
                _ => {
                    // SAFETY: This is safe because we haven't dereferenced
                    // any pointers during access check.
                    Ok(unsafe { request.continue_syscall() })
                }
            };
        }

        // kill and sigqueue support negative PIDs.
        let pid_abs = if syscall.is_thread() { pid } else { pid.abs() };

        // Check for Syd tasks.
        let syd = Pid::this().as_raw();

        // ESRCH errno(3) is consistent with landlock(7) scoped signals.
        if !syscall.is_thread() && syd == pid_abs {
            return Err(Errno::ESRCH);
        }

        if syscall.is_thread() && syd == pid {
            return Err(Errno::ESRCH);
        }

        if syscall.is_thread_group() && syd == tid {
            return Err(Errno::ESRCH);
        }

        if !syscall.is_thread()
            // SAFETY: Check for Syd threads with the abstract PID.
            && Errno::result(unsafe { libc::syscall(libc::SYS_tgkill, syd, pid_abs, 0) }).is_ok()
        {
            return Err(Errno::ESRCH);
        }

        if syscall.is_thread()
            // SAFETY: Check for Syd threads with the PID.
            && Errno::result(unsafe { libc::syscall(libc::SYS_tgkill, syd, pid, 0) }).is_ok()
        {
            return Err(Errno::ESRCH);
        }

        if syscall.is_thread_group()
            && pid != tid
            && Errno::result(
                // SAFETY: Check for Syd threads with the TID.
                unsafe { libc::syscall(libc::SYS_tgkill, syd, tid, 0) },
            )
            .is_ok()
        {
            return Err(Errno::ESRCH);
        }

        // Check signals directed to Syd's process group.
        let syd_pgid = getpgrp().as_raw();
        if !syscall.is_thread() && syd_pgid == pid_abs {
            return Err(Errno::ESRCH);
        }
        if syscall.is_thread() && syd_pgid == pid {
            return Err(Errno::ESRCH);
        }
        if syscall.is_thread_group() && syd_pgid == tid {
            return Err(Errno::ESRCH);
        }

        // SAFETY: This is safe because we haven't dereferenced
        // any pointers during access check.
        Ok(unsafe { request.continue_syscall() })
    })
}
