//
// Syd: rock-solid application kernel
// src/workers/out.rs: `syd_out' timeouter thread
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    env,
    os::fd::{FromRawFd, OwnedFd, RawFd},
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc,
    },
    thread,
};

use dur::Duration;
use libseccomp::{scmp_cmp, ScmpAction, ScmpFilterContext, ScmpSyscall};
use nix::{
    errno::Errno,
    sched::{unshare, CloneFlags},
    unistd::{write, Gid, Uid},
};

use crate::{
    alert,
    config::*,
    confine::{confine_scmp_madvise, confine_scmp_write, scmp_add_setid_rules, ExportMode},
    err::{err2no, scmp2no, SydJoinHandle, SydResult},
    error,
    fd::closeexcept,
    info,
    landlock::Errata,
    landlock_policy::LandlockPolicy,
    retry::retry_on_eintr,
    sandbox::Flags,
};

#[derive(Clone)]
pub(crate) struct Timeouter {
    tmout: Duration,
    flags: Flags,

    transit_uids: Vec<(Uid, Uid)>,
    transit_gids: Vec<(Gid, Gid)>,

    should_exit: Arc<AtomicBool>,
}

impl Timeouter {
    pub(crate) fn new(
        tmout: Duration,
        flags: Flags,
        transit_uids: &[(Uid, Uid)],
        transit_gids: &[(Gid, Gid)],
        should_exit: Arc<AtomicBool>,
    ) -> Self {
        Self {
            tmout,
            flags,
            should_exit,
            transit_uids: transit_uids.to_vec(),
            transit_gids: transit_gids.to_vec(),
        }
    }

    #[expect(clippy::cognitive_complexity)]
    pub(crate) fn try_spawn(self, notif_pipe: (RawFd, RawFd)) -> Result<SydJoinHandle<()>, Errno> {
        thread::Builder::new()
            .name("syd_out".to_string())
            .stack_size(OUT_STACK_SIZE)
            .spawn(move || {
                // SAFETY: We use exit_group(2) here to bail,
                // because this unsharing is a critical safety feature.
                if let Err(errno) = unshare(CloneFlags::CLONE_FS | CloneFlags::CLONE_FILES) {
                    alert!("ctx": "boot", "op": "unshare_timeout_thread",
                        "msg": format!("failed to unshare(CLONE_FS|CLONE_FILES): {errno}"),
                        "err": errno as i32);
                    std::process::exit(101);
                }

                // SAFETY: notif_pipe points to valid FDs.
                let (pipe_rd, pipe_wr) = unsafe {
                    (
                        OwnedFd::from_raw_fd(notif_pipe.0),
                        OwnedFd::from_raw_fd(notif_pipe.1),
                    )
                };
                drop(pipe_rd);
                let buf = [42u8; 1];
                #[expect(clippy::disallowed_methods)]
                match retry_on_eintr(|| write(&pipe_wr, &buf)).unwrap() {
                    0 => return Err(Errno::EIO.into()), // Syd died before reading.
                    1 => {}
                    n => unreachable!("BUG: invalid pipe write of size {n}!"),
                }

                // Close the notification pipe.
                drop(pipe_wr);

                // SAFETY: The Timeout thread needs to inherit the following FDs:
                // 1. Log FD.
                #[expect(clippy::cast_sign_loss)]
                let set = &[crate::log::LOG_FD.load(Ordering::Relaxed) as libc::c_uint];
                if let Err(errno) = closeexcept(set) {
                    alert!("ctx": "boot", "op": "close_range_timeout_thread",
                        "msg": format!("failed to close range: {errno}"),
                        "err": errno as i32);
                    std::process::exit(101);
                }

                // Honour dry-run when exporting.
                let dry_run =
                    env::var_os(ENV_SKIP_SCMP).is_some() || ExportMode::from_env().is_some();

                // Confine `syd_out' thread.
                if !dry_run {
                    // SAFETY: We use exit_group(2) here to bail,
                    // because this confinement is a critical safety feature.
                    let ctx = match Self::prepare_confine(
                        self.flags,
                        &self.transit_uids,
                        &self.transit_gids,
                        false,
                    ) {
                        Ok(ctx) => ctx,
                        Err(error) => {
                            let errno = error.errno().unwrap_or(Errno::ENOSYS);
                            alert!("ctx": "boot", "op": "confine_out_thread",
                                "msg": format!("failed to confine: {error}"),
                                "err": errno as i32);
                            std::process::exit(101);
                        }
                    };

                    // Load seccomp(2) BPF into the kernel.
                    // SAFETY: We use exit_group(2) here to bail,
                    // because this confinement is a critical safety feature.
                    if let Err(error) = ctx.load() {
                        let errno = scmp2no(&error).unwrap_or(Errno::ENOSYS);
                        alert!("ctx": "boot", "op": "confine_out_thread",
                            "msg": format!("failed to confine: {error}"),
                            "err": errno as i32);
                        std::process::exit(101);
                    }

                    let safe_setid = self
                        .flags
                        .intersects(Flags::FL_ALLOW_SAFE_SETUID | Flags::FL_ALLOW_SAFE_SETGID);
                    info!("ctx": "confine", "op": "confine_out_thread",
                        "msg": format!("timeout thread confined with{} SROP mitigation",
                            if safe_setid { "out" } else { "" }));
                } else {
                    error!("ctx": "confine", "op": "confine_out_thread",
                        "msg": "timeout thread is running unconfined in debug mode");
                }

                // Enter main loop.
                self.main()
            })
            .map_err(|err| err2no(&err))
    }

    fn main(self) -> SydResult<()> {
        // Sleep for sandbox timeout.
        std::thread::sleep(self.tmout.into());

        // Check if it's the time to exit.
        if self.should_exit.load(Ordering::Relaxed) {
            return Ok(());
        }

        let tmout = self.tmout.to_string();
        alert!("ctx": "out", "op": "timeout_exceeded",
            "msg": format!("sandbox timeout `{tmout}' exceeded"),
            "dur": self.tmout.as_secs());
        std::process::exit(124);
    }

    /// Confine Timeouter thread.
    #[expect(clippy::cognitive_complexity)]
    pub(crate) fn prepare_confine(
        flags: Flags,
        transit_uids: &[(Uid, Uid)],
        transit_gids: &[(Gid, Gid)],
        dry_run: bool,
    ) -> SydResult<ScmpFilterContext> {
        if !dry_run {
            // SAFETY: Set up a Landlock sandbox to disallow all access.
            let abi = crate::landlock::ABI::new_current();
            let errata = crate::landlock::Errata::query();
            let policy = LandlockPolicy {
                scoped_abs: true,
                scoped_sig: errata.contains(Errata::SCOPED_SIGNAL_SAME_TGID),
                ..Default::default()
            };
            let _ = policy.restrict_self(abi);
        }

        // Create seccomp filter with default action.
        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(flags.allow_unsafe_exec_speculative())?;

        // DO NOT synchronize filter to all threads.
        // Other threads will self-confine.
        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 rest of open and stat family 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", "openat", "stat", "lstat", "statx", "newfstatat"] {
            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_out_syscall",
                        "msg": format!("invalid or unsupported syscall {sysname}"));
                }
            }
        }

        // 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_out_syscall",
                        "msg": format!("invalid or unsupported syscall {sysname}"));
                    continue;
                }
            };

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

        // Prevent executable memory.
        const PROT_EXEC: u64 = libc::PROT_EXEC as u64;
        for sysname in ["mmap", "mmap2", "mprotect"] {
            match ScmpSyscall::from_name(sysname) {
                Ok(syscall) => {
                    ctx.add_rule_conditional(
                        ScmpAction::Allow,
                        syscall,
                        &[scmp_cmp!($arg2 & PROT_EXEC == 0)],
                    )?;
                }
                Err(_) => {
                    info!("ctx": "confine", "op": "allow_out_syscall",
                        "msg": format!("invalid or unsupported syscall {sysname}"));
                }
            }
        }

        // Allow writes to the log-fd.
        // No proc_pid_mem(5) access required here.
        confine_scmp_write(&mut ctx, None, false)?;

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

        // Allow safe system calls.
        for sysname in OUT_SYSCALLS.iter().chain(VDSO_SYSCALLS) {
            match ScmpSyscall::from_name(sysname) {
                Ok(syscall) => {
                    ctx.add_rule(ScmpAction::Allow, syscall)?;
                }
                Err(_) => {
                    info!("ctx": "confine", "op": "allow_out_syscall",
                        "msg": format!("invalid or unsupported syscall {sysname}"));
                }
            }
        }

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

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

        // Allow UID/GID changing system calls as necessary.
        let safe_setuid = flags.allow_safe_setuid();
        let safe_setgid = flags.allow_safe_setgid();
        if safe_setuid || safe_setgid {
            scmp_add_setid_rules(
                "out",
                &mut ctx,
                safe_setuid,
                safe_setgid,
                transit_uids,
                transit_gids,
            )?;
        }

        Ok(ctx)
    }
}
