//
// Syd: rock-solid application kernel
// src/hash.rs: Utilities for caching
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

// SAFETY: This module has been liberated from unsafe code!
#![forbid(unsafe_code)]

use std::{
    fs::File,
    os::fd::OwnedFd,
    sync::{Arc, Condvar, Mutex, RwLock},
};

use ahash::{HashMapExt, HashSetExt};
use libseccomp::ScmpSyscall;
use nix::{errno::Errno, sys::socket::UnixAddr, unistd::Pid};
use serde::{ser::SerializeMap, Serializer};

use crate::{
    confine::{ScmpNotifReq, SydArch},
    elf::ExecutableFile,
    hash::{SydHashMap, SydHashSet},
    sigset::SydSigSet,
};

/// Metadata on a blocking syscall invocation
#[derive(Debug)]
pub(crate) struct SysInterrupt {
    /// Syd handler thread ID
    pub(crate) handler: Pid,
    /// System call request
    pub(crate) request: ScmpNotifReq,
    /// proc_pid_status(5) file handle
    pub(crate) status: Option<OwnedFd>,
    /// Used by syd_emu to signal syd_int to delete the entry and close the file.
    /// This is because the status file descriptor is not valid in syd_emu's fs space.
    pub(crate) delete: bool,
    /// Used by syd_mon to signal syd_int to signal stuck emulators manually,
    /// when not enough resources are available to spawn new emulator threads.
    /// This is because the status file descriptor is not valid in syd_mon's fs space.
    pub(crate) signal: bool,
    /// True if `SA_RESTART` is ignored
    /// (e.g. due to a socket timeout).
    pub(crate) ignore_restart: bool,
}

/// Map of metadata on blocking syscall invocations.
pub(crate) type BlockVec = Vec<SysInterrupt>;

/// Map of restarting signals by TGID.
pub(crate) type RestartMap = SydHashMap<Pid, SydSigSet>;

/// This is the data type used to handle syscall interrupts.
#[derive(Debug)]
pub(crate) struct SysInterruptMap {
    /// Map of blocking syscalls by request id.
    pub(crate) sys_block: Arc<(Mutex<BlockVec>, Condvar)>,
    /// Map of restarting signals by TGID.
    /// Used for SA_RESTART tracking.
    pub(crate) sig_restart: Arc<Mutex<RestartMap>>,
}

/// Represents an exec(3) check result
#[derive(Debug)]
pub(crate) struct ExecResult {
    pub(crate) exe: ExecutableFile,
    pub(crate) file: File,
}

/// Syscall-agnostic error map.
pub(crate) type ErrorMap = SydHashMap<Pid, Option<Errno>>;

/// chdir(2) result set.
pub(crate) type ChdirSet = SydHashSet<Pid>;

/// exec(3) result map.
pub(crate) type ExecvMap = SydHashMap<Pid, ExecResult>;

/// mmap(2) pid set.
pub(crate) type MmapSet = SydHashSet<Pid>;

// [inode,(pid,path)] map of unix binds.
// Path is only used for UNIX domain sockets.
//
// SAFETY:
// 1. /proc/net/unix only gives inode information,
//    and does not include information on device id
//    or mount id so unfortunately we cannot check
//    for that here.
// 2. Pid is used for SO_PEERCRED getsockopt(2).
#[derive(Copy, Clone)]
pub(crate) struct UnixVal {
    pub(crate) pid: Pid,
    pub(crate) addr: Option<UnixAddr>,
    pub(crate) peer: Option<UnixAddr>,
}
pub(crate) type UnixMap = Arc<RwLock<SydHashMap<u64, UnixVal>>>;

// [tid, tgid] map for ptrace(PTRACE_TRACEME) calling tids.
// This is used to prevent ptrace(2) detection efficiently.
pub(crate) type PtraceMap = Arc<RwLock<SydHashMap<Pid, Pid>>>;

/// Results map for ptrace(2) hooks chdir, execve, sigaction and sigreturn.
#[derive(Debug)]
pub(crate) struct SysResultMap {
    /// syscall-agnostic error map
    pub(crate) trace_error: Arc<Mutex<ErrorMap>>,
    /// chdir(2) result map
    pub(crate) trace_chdir: Arc<Mutex<ChdirSet>>,
    /// exec(3) result map
    pub(crate) trace_execv: Arc<Mutex<ExecvMap>>,
    /// mmap(2) pid set.
    pub(crate) trace_mmap: Arc<Mutex<MmapSet>>,
}

/// Map of TGIDs that have received count signals for handled signals.
pub(crate) type SighandleMap = SydHashMap<Pid, u64>;

/// Signal map, used by signal counting for SROP mitigation:
/// If a TGID is not in sig_handle_map at the entry of sigreturn(2),
/// we terminate the process because the sigreturn(2) is artificial.
#[derive(Debug)]
pub(crate) struct SignalMap {
    /// Set of TGIDs that have received count signals for handled signals.
    pub(crate) sig_handle: Arc<Mutex<SighandleMap>>,
}

impl SysInterrupt {
    pub(crate) fn new(
        request: ScmpNotifReq,
        handler: Pid,
        ignore_restart: bool,
    ) -> Result<Self, Errno> {
        Ok(Self {
            handler,
            request,
            ignore_restart,
            status: None,
            delete: false,
            signal: false,
        })
    }

    // Marks the interrupt for deletion as needed.
    //
    // Returns true if drop should be handled by syd_int.
    pub(crate) fn delete(&mut self) -> bool {
        // interrupt.status is Some if syd_int thread
        // has already opened proc_pid_status(5), in
        // which case we let it close the file because
        // the file descriptor is not valid in syd_emu's
        // FS space.
        if self.status.is_some() {
            self.delete = true;
            true // syd_int drops interrupt.
        } else {
            false // syd_emu drops interrupt.
        }
    }
}

impl serde::Serialize for SysInterrupt {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut map = serializer.serialize_map(Some(3))?;

        let data = &self.request.data;
        let syscall = ScmpSyscall::get_name_by_arch(data.syscall, data.arch)
            .unwrap_or_else(|_| format!("{}", i32::from(data.syscall)));
        let _ = map.serialize_entry("pid", &self.request.pid);
        let _ = map.serialize_entry("sys", &syscall);
        let _ = map.serialize_entry("arch", &SydArch::from(data.arch));
        let _ = map.serialize_entry("args", &data.args);
        let _ = map.serialize_entry("handler", &self.handler.as_raw());
        let _ = map.serialize_entry("ignore_restart", &self.ignore_restart);

        map.end()
    }
}

/// Create a new UnixMap.
pub(crate) fn unix_map_new() -> UnixMap {
    Arc::new(RwLock::new(SydHashMap::default()))
}

/// Create a new PtraceMap.
pub(crate) fn ptrace_map_new() -> PtraceMap {
    Arc::new(RwLock::new(SydHashMap::default()))
}

/// Create a new SysInterruptMap.
pub(crate) fn sys_interrupt_map_new() -> SysInterruptMap {
    SysInterruptMap {
        sys_block: Arc::new((Mutex::new(BlockVec::new()), Condvar::new())),
        sig_restart: Arc::new(Mutex::new(RestartMap::new())),
    }
}

/// Create a new SysResultMap.
pub(crate) fn sys_result_map_new() -> SysResultMap {
    SysResultMap {
        trace_error: Arc::new(Mutex::new(ErrorMap::new())),
        trace_chdir: Arc::new(Mutex::new(ChdirSet::new())),
        trace_execv: Arc::new(Mutex::new(ExecvMap::new())),
        trace_mmap: Arc::new(Mutex::new(MmapSet::new())),
    }
}

/// Create a new SignalMap.
pub(crate) fn signal_map_new() -> SignalMap {
    SignalMap {
        sig_handle: Arc::new(Mutex::new(SighandleMap::new())),
    }
}
