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

use std::{fs::File, io::Seek, os::fd::AsRawFd};

use data_encoding::HEXLOWER;
use nix::{
    errno::Errno,
    fcntl::{AtFlags, OFlag},
    sys::signal::{kill, Signal},
    unistd::Pid,
    NixPath,
};

use crate::{
    compat::ResolveFlag,
    config::{PROC_FILE, PTRACE_DATA_EXECVE, PTRACE_DATA_EXECVEAT},
    confine::{is_valid_ptr, scmp_arch, SydArch},
    debug,
    elf::{ElfError, ElfFileType, ElfType, ExecutableFile, LinkingType},
    err::err2no,
    error,
    fd::{is_executable, AT_EXECVE_CHECK},
    kernel::{sandbox_path, to_atflags},
    log_enabled,
    lookup::{safe_open_msym, FileType, FsFlags, MaybeFd},
    path::XPathBuf,
    proc::proc_get_vma,
    ptrace::ptrace_syscall_info,
    req::{RemoteProcess, SysArg, SysFlags},
    sandbox::{Action, Capability, IntegrityError, SandboxGuard},
    syslog::LogLevel,
    warn,
};

// Note, sysenter_exec is a ptrace(2) hook, not a seccomp hook!
// The seccomp hooks are only used with trace/allow_unsafe_ptrace:1.
#[expect(clippy::cognitive_complexity)]
pub(crate) fn sysenter_exec(
    pid: Pid,
    sandbox: &SandboxGuard,
    info: ptrace_syscall_info,
) -> Result<(File, ExecutableFile), Errno> {
    let data = if let Some(data) = info.seccomp() {
        data
    } else {
        unreachable!("BUG: Invalid system call information returned by kernel!");
    };

    #[expect(clippy::cast_possible_truncation)]
    let (syscall_name, arg, chk) = match data.ret_data as u16 {
        PTRACE_DATA_EXECVE => (
            "execve",
            SysArg {
                path: Some(0),
                fsflags: FsFlags::MUST_PATH,
                ..Default::default()
            },
            false,
        ),
        PTRACE_DATA_EXECVEAT => {
            // SAFETY: Reject undefined/invalid flags.
            let flags = to_atflags(
                data.args[4],
                AtFlags::AT_SYMLINK_NOFOLLOW | AtFlags::AT_EMPTY_PATH | AT_EXECVE_CHECK,
            )?;

            let mut fsflags = FsFlags::MUST_PATH;
            if flags.contains(AtFlags::AT_SYMLINK_NOFOLLOW) {
                fsflags.insert(FsFlags::NO_FOLLOW_LAST);
            }

            let empty_path = flags.contains(AtFlags::AT_EMPTY_PATH);
            (
                "execveat",
                SysArg {
                    dirfd: Some(0),
                    path: Some(1),
                    flags: if empty_path {
                        SysFlags::EMPTY_PATH
                    } else {
                        SysFlags::empty()
                    },
                    fsflags,
                    ..Default::default()
                },
                flags.contains(AT_EXECVE_CHECK),
            )
        }
        data => unreachable!("BUG: invalid syscall data {data}!"),
    };

    // Read remote path.
    let process = RemoteProcess::new(pid);

    #[expect(clippy::disallowed_methods)]
    let arch: SydArch = scmp_arch(info.arch).unwrap().into();

    // SAFETY: This is a ptrace hook, the PID cannot be validated.
    let (mut path, _, _, empty_path) =
        process.read_path(sandbox, arch.into(), data.args, arg, None)?;

    // Call sandbox access checker if Exec sandboxing is on.
    let caps = sandbox.getcaps(Capability::CAP_EXEC | Capability::CAP_TPE);
    if caps.contains(Capability::CAP_EXEC) {
        let hide = !(empty_path && arg.flags.contains(SysFlags::EMPTY_PATH))
            && sandbox.enabled(Capability::CAP_STAT);
        sandbox_path(
            None,
            sandbox,
            pid,
            path.abs(),
            Capability::CAP_EXEC,
            hide,
            syscall_name,
        )?;
    }

    if !arg.fsflags.follow_last()
        && path
            .typ
            .as_ref()
            .map(|typ| typ.is_symlink() || typ.is_magic_link())
            .unwrap_or(false)
    {
        // SAFETY: AT_SYMLINK_NOFOLLOW: If the file identified by dirfd
        // and a non-NULL pathname is a symbolic link, then the call
        // fails with the error ELOOP.
        return Err(Errno::ELOOP);
    }

    // SAFETY: Return EACCES without any more processing
    // if the file is not a regular file or a memory fd.
    // Mfd check depends on trace/allow_unsafe_memfd option.
    //
    // Note, attempting to execute directories on Linux
    // return EACCES, not EISDIR like the manual page
    // claims. GNU make has a test checking this errno.
    match path.typ.as_ref() {
        Some(FileType::Reg) => {}
        Some(FileType::Mfd) if sandbox.flags.allow_unsafe_memfd() => {}
        //Some(FileType::Dir) => return Err(Errno::EISDIR),
        _ => return Err(Errno::EACCES),
    };

    // SAFETY:
    // 1. Return EACCES without any more processing
    //    if the file is not executable.
    // 2. We set MUST_PATH in FsFlags, path.dir is always Some.
    // This uses AT_EXECVE_CHECK on Linux>=6.14.
    #[expect(clippy::disallowed_methods)]
    if !is_executable(path.dir.as_ref().unwrap()) {
        return Err(Errno::EACCES);
    }

    // Check SegvGuard.
    if let Some(action) = sandbox.check_segvguard(path.abs()) {
        if action != Action::Filter {
            let (_, bin) = path.abs().split();
            error!("ctx": "exec", "op": "segvguard",
                "msg": format!("max crashes {} exceeded, execution of `{bin}' denied",
                    sandbox.segvguard_maxcrashes),
                "tip": "increase `segvguard/maxcrashes'",
                "pid": pid.as_raw(), "path": path.abs());
        }

        match action {
            Action::Allow | Action::Warn => {}
            Action::Panic | Action::Deny | Action::Filter => return Err(Errno::EACCES),
            //Do NOT panic the main thread!
            //Action::Panic => panic!(),
            Action::Exit => std::process::exit(libc::EACCES),
            Action::Stop => {
                let _ = kill(process.pid, Some(Signal::SIGSTOP));
                return Err(Errno::EACCES);
            }
            Action::Abort => {
                let _ = kill(process.pid, Some(Signal::SIGABRT));
                return Err(Errno::EACCES);
            }
            Action::Kill => {
                let _ = kill(process.pid, Some(Signal::SIGKILL));
                return Err(Errno::EACCES);
            }
        }
    }

    // Trusted Path Execution.
    if caps.contains(Capability::CAP_TPE) {
        // MUST_PATH ensures path.dir is Some.
        #[expect(clippy::disallowed_methods)]
        let file = path.dir.as_ref().unwrap();
        let (action, msg) = sandbox.check_tpe(file, path.abs());
        if !matches!(action, Action::Allow | Action::Filter) {
            // TODO: Fix proc_mmap to work in ptrace hooks.
            let msg = msg.as_deref().unwrap_or("?");
            error!("ctx": "exec", "op": "trusted_path_execution",
                "msg": format!("exec from untrusted path blocked: {msg}"),
                "pid": pid.as_raw(), "path": path.abs(),
                "sys": syscall_name, "arch": info.arch, "args": data.args,
                "tip": "move the binary to a safe location or use `sandbox/tpe:off'");
        }
        match action {
            Action::Allow | Action::Warn => {}
            Action::Panic | Action::Deny | Action::Filter => return Err(Errno::EACCES),
            //Do NOT panic the main thread!
            //Action::Panic => panic!(),
            Action::Exit => std::process::exit(libc::EACCES),
            Action::Stop => {
                let _ = kill(pid, Some(Signal::SIGSTOP));
                return Err(Errno::EACCES);
            }
            Action::Abort => {
                let _ = kill(pid, Some(Signal::SIGABRT));
                return Err(Errno::EACCES);
            }
            Action::Kill => {
                let _ = kill(pid, Some(Signal::SIGKILL));
                return Err(Errno::EACCES);
            }
        }
    }

    // Open a new instance of the file for read.
    // We prefer this over WANT_READ to ensure
    // we do not share OFD with the sandbox process.
    assert!(
        path.base().is_empty(),
        "BUG: MUST_PATH returned a directory for exec, report a bug!"
    );
    let mut file = match path.dir.take() {
        Some(MaybeFd::Owned(fd)) => {
            let pfd = XPathBuf::from_self_fd(fd.as_raw_fd())?;
            safe_open_msym(PROC_FILE(), &pfd, OFlag::O_RDONLY, ResolveFlag::empty())
                .map(File::from)?
        }
        _ => return Err(Errno::ENOEXEC),
    };

    // Parse ELF as necessary for restrictions.
    let deny_script = sandbox.flags.deny_exec_script();
    let restrict_32 = sandbox.flags.deny_exec_elf32();
    let restrict_dyn = sandbox.flags.deny_exec_elf_dynamic();
    let restrict_sta = sandbox.flags.deny_exec_elf_static();
    let restrict_ldd = !sandbox.flags.allow_unsafe_exec_ldso();
    let restrict_pie = !sandbox.flags.allow_unsafe_exec_nopie();
    let restrict_xs = !sandbox.flags.allow_unsafe_exec_stack();

    let check_linking = restrict_ldd || restrict_dyn || restrict_sta || restrict_pie || restrict_xs;

    let exe = match ExecutableFile::parse(&file, check_linking) {
        Ok(exe) => exe,
        Err(ElfError::IoError(err)) => {
            let errno = err2no(&err);
            if !sandbox.filter_path(Capability::CAP_EXEC, path.abs()) {
                error!("ctx": "parse_elf",
                    "msg": format!("io error: {errno}"),
                    "pid": pid.as_raw(), "path": path.abs());
            }
            return Err(errno);
        }
        Err(ElfError::BadMagic) => {
            if !sandbox.filter_path(Capability::CAP_EXEC, path.abs()) {
                error!("ctx": "exec", "op": "parse_elf",
                    "msg": "invalid ELF file",
                    "pid": pid.as_raw(), "path": path.abs());
            }
            return Err(Errno::ENOEXEC);
        }
        Err(ElfError::Malformed) => {
            if !sandbox.filter_path(Capability::CAP_EXEC, path.abs()) {
                error!("ctx": "exec", "op": "parse_elf",
                    "msg": "malformed ELF file",
                    "pid": pid.as_raw(), "path": path.abs());
            }
            return Err(Errno::ENOEXEC);
        }
    };

    let is_script = exe == ExecutableFile::Script;
    if is_script && deny_script {
        if !sandbox.filter_path(Capability::CAP_EXEC, path.abs()) {
            error!("ctx": "exec", "op": "deny_exec_script",
                "msg": "script execution denied",
                "pid": pid.as_raw(), "path": path.abs(),
                "tip": "configure `trace/deny_exec_script:0'",
                "exe": format!("{exe}"));
        }
        return Err(Errno::EACCES);
    }

    if !is_script
        && restrict_ldd
        && !matches!(
            exe,
            ExecutableFile::Elf {
                file_type: ElfFileType::Executable,
                ..
            }
        )
    {
        if !sandbox.filter_path(Capability::CAP_EXEC, path.abs()) {
            error!("ctx": "exec", "op": "check_elf",
                "msg": "ld.so(8) exec-indirection prevented",
                "pid": pid.as_raw(), "path": path.abs(),
                "tip": "configure `trace/allow_unsafe_exec_ldso:1'",
                "exe": format!("{exe}"));
        }
        return Err(Errno::EACCES);
    }

    if !is_script && restrict_pie && matches!(exe, ExecutableFile::Elf { pie: false, .. }) {
        if !sandbox.filter_path(Capability::CAP_EXEC, path.abs()) {
            error!("ctx": "check_elf", "msg": "ELF is not Position Independent Executable (PIE)",
                "pid": pid.as_raw(), "path": path.abs(),
                "tip": "configure `trace/allow_unsafe_exec_nopie:1'",
                "exe": format!("{exe}"));
        }
        return Err(Errno::EACCES);
    }

    if !is_script && restrict_xs && matches!(exe, ExecutableFile::Elf { xs: true, .. }) {
        if !sandbox.filter_path(Capability::CAP_EXEC, path.abs()) {
            error!("ctx": "exec", "op": "check_elf",
                "msg": "ELF has executable stack",
                "pid": pid.as_raw(), "path": path.abs(),
                "tip": "configure `trace/allow_unsafe_exec_stack:1'",
                "exe": format!("{exe}"));
        }
        return Err(Errno::EACCES);
    }

    if !is_script
        && restrict_32
        && matches!(
            exe,
            ExecutableFile::Elf {
                elf_type: ElfType::Elf32,
                ..
            }
        )
    {
        if !sandbox.filter_path(Capability::CAP_EXEC, path.abs()) {
            error!("ctx": "exec", "op": "check_elf",
                "msg": "32-bit ELF execution prevented",
                "pid": pid.as_raw(), "path": path.abs(),
                "tip": "configure `trace/deny_exec_elf32:0'",
                "exe": format!("{exe}"));
        }
        return Err(Errno::EACCES);
    }

    if !is_script
        && restrict_dyn
        && matches!(
            exe,
            ExecutableFile::Elf {
                linking_type: Some(LinkingType::Dynamic),
                ..
            }
        )
    {
        if !sandbox.filter_path(Capability::CAP_EXEC, path.abs()) {
            error!("ctx": "exec", "op": "check_elf",
                "msg": "dynamic linked ELF execution prevented",
                "pid": pid.as_raw(), "path": path.abs(),
                "tip": "configure `trace/deny_exec_elf_dynamic:0'",
                "exe": format!("{exe}"));
        }
        return Err(Errno::EACCES);
    }

    if !is_script
        && restrict_sta
        && matches!(
            exe,
            ExecutableFile::Elf {
                linking_type: Some(LinkingType::Static),
                ..
            }
        )
    {
        if !sandbox.filter_path(Capability::CAP_EXEC, path.abs()) {
            error!("ctx": "exec", "op": "check_elf",
                "msg": "static linked ELF execution prevented",
                "pid": pid.as_raw(), "path": path,
                "tip": "configure `trace/deny_exec_elf_static:0'",
                "exe": format!("{exe}"));
        }
        return Err(Errno::EACCES);
    }

    // Check for Force sandboxing.
    if sandbox.enabled(Capability::CAP_FORCE) {
        // Reset the file offset and calculate checksum.
        file.rewind().or(Err(Errno::EACCES))?;

        match sandbox.check_force2(path.abs(), &mut file) {
            Ok(Action::Allow) => {}
            Ok(Action::Warn) => {
                warn!("ctx": "exec", "op": "verify_elf", "act": Action::Warn,
                    "pid": pid.as_raw(), "path": path.abs(),
                    "tip": format!("configure `force+{path}:<checksum>'"));
            }
            Ok(Action::Filter) => return Err(Errno::EACCES),
            Ok(act @ (Action::Deny | Action::Panic)) => {
                warn!("ctx": "exec", "op": "verify_elf", "act": act,
                    "pid": pid.as_raw(), "path": path.abs(),
                    "tip": format!("configure `force+{path}:<checksum>'"));
                return Err(Errno::EACCES);
            }
            //Do NOT panic the main thread!
            //Ok(Action::Panic) => panic!(),
            Ok(Action::Stop) => {
                warn!("ctx": "exec", "op": "verify_elf", "act": Action::Stop,
                    "pid": pid.as_raw(), "path": path.abs(),
                    "tip": format!("configure `force+{path}:<checksum>'"));
                let _ = kill(pid, Some(Signal::SIGSTOP));
                return Err(Errno::EACCES);
            }
            Ok(Action::Abort) => {
                warn!("ctx": "exec", "op": "verify_elf", "act": Action::Abort,
                    "pid": pid.as_raw(), "path": path.abs(),
                    "tip": format!("configure `force+{path}:<checksum>'"));
                let _ = kill(pid, Some(Signal::SIGABRT));
                return Err(Errno::EACCES);
            }
            Ok(Action::Kill) => {
                warn!("ctx": "exec", "op": "verify_elf", "act": Action::Kill,
                    "pid": pid.as_raw(), "path": path.abs(),
                    "tip": format!("configure `force+{path}:<checksum>'"));
                let _ = kill(pid, Some(Signal::SIGKILL));
                return Err(Errno::EACCES);
            }
            Ok(Action::Exit) => {
                error!("ctx": "exec", "op": "verify_elf", "act": Action::Exit,
                    "pid": pid.as_raw(), "path": path.abs(),
                    "tip": format!("configure `force+{path}:<checksum>'"));
                std::process::exit(libc::EACCES);
            }
            Err(IntegrityError::Sys(errno)) => {
                error!("ctx": "exec", "op": "verify_elf",
                    "msg": format!("system error during ELF checksum calculation: {errno}"),
                    "pid": pid.as_raw(), "path": path.abs(),
                    "tip": format!("configure `force+{path}:<checksum>'"));
                return Err(Errno::EACCES);
            }
            Err(IntegrityError::Hash {
                action,
                expected,
                found,
            }) => {
                if !matches!(action, Action::Allow | Action::Filter) {
                    error!("ctx": "exec", "op": "verify_elf", "act": action,
                        "msg": format!("ELF checksum mismatch: {found} is not {expected}"),
                        "pid": pid.as_raw(), "path": path.abs(),
                        "tip": format!("configure `force+{path}:<checksum>'"));
                }
                match action {
                    Action::Allow | Action::Warn => {}
                    Action::Panic | Action::Deny | Action::Filter => return Err(Errno::EACCES),
                    //Do NOT panic the main thread!
                    //Action::Panic => panic!(),
                    Action::Stop | Action::Abort | Action::Kill => {
                        let _ = kill(
                            pid,
                            Some(
                                Signal::try_from(
                                    action
                                        .signal()
                                        .map(|sig| sig as i32)
                                        .unwrap_or(libc::SIGKILL),
                                )
                                .unwrap_or(Signal::SIGKILL),
                            ),
                        );
                        return Err(Errno::EACCES);
                    }
                    Action::Exit => std::process::exit(libc::EACCES),
                };
            }
        }
    }

    if chk {
        // AT_EXECVE_CHECK, no need to execute the file.
        // Caller checks this errno value to before calling
        // ptrace_skip_syscall.
        return Err(Errno::ECANCELED);
    }

    if log_enabled!(LogLevel::Debug) {
        let exe = exe.to_string();
        let path = path.abs();
        let ip_vma = proc_get_vma(pid, info.instruction_pointer).ok();
        let sp_vma = proc_get_vma(pid, info.stack_pointer).ok();

        let ip = info.instruction_pointer;
        let sp = info.stack_pointer;

        let process = RemoteProcess::new(pid);
        let ip_mem = if is_valid_ptr(ip, arch.into()) {
            let mut ip_mem = [0u8; 64];
            // SAFETY: This is a ptrace hook, the PID cannot be validated.
            match unsafe { process.read_mem(arch.into(), &mut ip_mem, ip, 64) } {
                Ok(_) => Some(ip_mem),
                Err(Errno::EFAULT) => None,
                Err(Errno::ESRCH) => return Err(Errno::ESRCH),
                Err(_) => {
                    // SAFETY: Process is alive, but
                    // we cannot read memory: Terminate!
                    let _ = kill(pid, Some(Signal::SIGKILL));
                    return Err(Errno::ESRCH);
                }
            }
        } else {
            None
        };

        let sp_mem = if is_valid_ptr(sp, arch.into()) {
            let mut sp_mem = [0u8; 64];
            // SAFETY: This is a ptrace hook, the PID cannot be validated.
            match unsafe { process.read_mem(arch.into(), &mut sp_mem, sp, 64) } {
                Ok(_) => Some(sp_mem),
                Err(Errno::EFAULT) => None,
                Err(Errno::ESRCH) => return Err(Errno::ESRCH),
                Err(_) => {
                    // SAFETY: Process is alive, but
                    // we cannot read memory: Terminate!
                    let _ = kill(process.pid, Some(Signal::SIGKILL));
                    return Err(Errno::ESRCH);
                }
            }
        } else {
            None
        };
        let ip_mem = ip_mem.map(|ip_mem| HEXLOWER.encode(&ip_mem));
        let sp_mem = sp_mem.map(|sp_mem| HEXLOWER.encode(&sp_mem));

        debug!("ctx": "exec", "op": "verify_exec",
            "msg": format!("execution of `{path}' of type {exe} resumed"),
            "pid": pid.as_raw(), "path": &path, "exe": &exe, "args": data.args,
            "ip": info.instruction_pointer, "sp": info.stack_pointer,
            "ip_mem": ip_mem, "sp_mem": sp_mem,
            "ip_vma": ip_vma, "sp_vma": sp_vma);
    }

    Ok((file, exe))
}
