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

use bitflags::bitflags;
use libseccomp::ScmpNotifResp;
use nix::{
    errno::Errno,
    unistd::{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 {
    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 request.fail_syscall(Errno::ESRCH);
    }

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

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

    // rt_{tg,}sigqueueinfo return ESRCH/EINVAL on negative TGID.
    if syscall.is_queue() && !(1..=PID_MAX_LIMIT).contains(&pid) {
        return request.fail_syscall(if syscall.is_group() {
            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 request.fail_syscall(Errno::EINVAL);
    }

    // tgkill(2) returns EINVAL on negative TID.
    if syscall.is_group() && !(1..=PID_MAX_LIMIT).contains(&tid) {
        return request.fail_syscall(Errno::EINVAL);
    }

    // No need to check for group signals directed at the current
    // process group here because Syd does not share the process group
    // with the sandbox process as of 3.48.0.
    //
    // 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 request.fail_syscall(Errno::ESRCH);
    }

    // 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();

    // SAFETY: Note, we deny with EPERM, rather than returning success
    // because we have a kernel-level bpf filter that _allows_ the
    // respective signaler system call _only when_ the signal is 0.
    // Therefore we're not leaking any further information by returning
    // EPERM here.
    if !syscall.is_thread() && syd == pid_abs {
        return request.fail_syscall(Errno::EPERM);
    }

    if syscall.is_thread() && syd == pid {
        return request.fail_syscall(Errno::EPERM);
    }

    if syscall.is_thread_group() && syd == tid {
        return request.fail_syscall(Errno::EPERM);
    }

    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 request.fail_syscall(Errno::EPERM);
    }

    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 request.fail_syscall(Errno::EPERM);
    }

    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 request.fail_syscall(Errno::EPERM);
    }

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

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