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

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

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

use crate::{
    compat::ResolveFlag,
    config::{PAGE_SIZE, PROC_FILE, PTRACE_DATA_MMAP},
    elf::ExecutableFile,
    error,
    fd::{fd_status_flags, pidfd_getfd, pidfd_open, PIDFD_THREAD},
    kernel::sandbox_path,
    lookup::{safe_open_msym, CanonicalPath},
    path::XPathBuf,
    proc::{proc_executables, proc_mem, proc_statm},
    ptrace::{ptrace_get_error, ptrace_syscall_info},
    sandbox::{Action, Capability, IntegrityError, SandboxGuard},
    warn,
};

const PROT_EXEC: u64 = libc::PROT_EXEC as u64;
const MAP_ANONYMOUS: u64 = libc::MAP_ANONYMOUS as u64;
const MAP_SHARED: u64 = libc::MAP_SHARED as u64;

// Note, sysenter_mmap 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_mmap(
    pid: Pid,
    sandbox: &SandboxGuard,
    info: ptrace_syscall_info,
) -> Result<bool, 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 scmp_trace_data = data.ret_data as u16;
    let size = data.args[1];
    let name = if scmp_trace_data == PTRACE_DATA_MMAP {
        "mmap"
    } else {
        "mmap2"
    };

    let caps = sandbox.getcaps(Capability::CAP_MMAP);
    let exec = caps.contains(Capability::CAP_EXEC);
    let force = caps.contains(Capability::CAP_FORCE);
    let tpe = caps.contains(Capability::CAP_TPE);
    let mem = caps.contains(Capability::CAP_MEM);
    let mem_max = sandbox.mem_max;
    let mem_vm_max = sandbox.mem_vm_max;
    let mem_act = sandbox.default_action(Capability::CAP_MEM);
    let restrict_exec_memory = !sandbox.flags.allow_unsafe_exec_memory();
    let restrict_exec_stack = !sandbox.flags.allow_unsafe_exec_stack();
    let restrict_append_only = sandbox.has_append() || sandbox.enabled(Capability::CAP_CRYPT);

    if !exec
        && !force
        && !tpe
        && !restrict_exec_memory
        && !restrict_exec_stack
        && !restrict_append_only
        && (!mem || (mem_max == 0 && mem_vm_max == 0))
    {
        // Continue system call.
        return Ok(false);
    }

    let check_exec = (exec || force || tpe || restrict_exec_memory || restrict_exec_stack)
        && data.args[2] & PROT_EXEC != 0
        && data.args[3] & MAP_ANONYMOUS == 0;
    let check_append_only = restrict_append_only && data.args[3] & MAP_SHARED != 0;

    // Get the file descriptor before access check.
    let fd = if check_exec || check_append_only {
        #[expect(clippy::cast_possible_truncation)]
        let remote_fd = data.args[4] as RawFd;
        if remote_fd < 0 {
            return Err(Errno::EBADF);
        }

        let pid_fd = pidfd_open(pid, PIDFD_THREAD)?;
        match pidfd_getfd(pid_fd, remote_fd) {
            Ok(fd) => Some(fd),
            Err(_) => return Err(Errno::EBADF),
        }
    } else {
        None
    };

    #[expect(clippy::disallowed_methods)]
    let oflags = if check_append_only || (check_exec && restrict_exec_memory) {
        fd_status_flags(fd.as_ref().unwrap()).ok()
    } else {
        None
    };

    if check_append_only {
        // Prevent shared mappings on writable append-only fds.
        let deny = oflags
            .map(|fl| {
                fl.contains(OFlag::O_APPEND)
                    && (fl.contains(OFlag::O_RDWR) || fl.contains(OFlag::O_WRONLY))
            })
            .unwrap_or(true);

        if deny {
            return Err(Errno::EACCES);
        }
    }

    if check_exec {
        // Step 1: Check if file is open for write,
        // but set as PROT_READ|PROT_EXEC which breaks W^X!
        // We do not need to check for PROT_WRITE here as
        // this is already enforced at kernel-level when
        // trace/allow_unsafe_exec_memory:1 is not set at startup.
        if restrict_exec_memory {
            let deny = oflags
                .map(|fl| fl.contains(OFlag::O_RDWR) || fl.contains(OFlag::O_WRONLY))
                .unwrap_or(true);

            if deny {
                return Err(Errno::EACCES);
            }
        }

        #[expect(clippy::disallowed_methods)]
        let mut path = CanonicalPath::new_fd(fd.unwrap().into(), pid)?;

        // Step 2: Check for Exec sandboxing.
        if exec {
            sandbox_path(
                None,
                sandbox,
                pid,
                path.abs(),
                Capability::CAP_EXEC,
                false,
                name,
            )?;
        }

        // Step 3: Check for TPE sandboxing.
        if 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) {
                let msg = msg.as_deref().unwrap_or("?");
                error!("ctx": "trusted_path_execution",
                    "msg": format!("library load from untrusted path blocked: {msg}"),
                    "sys": &name, "path": &path,
                    "pid": pid.as_raw(),
                    "tip": "move the library 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);
                }
            }
        }

        if force || restrict_exec_stack {
            // The following checks require the contents of the file.
            // SAFETY:
            // 1. Reopen the file via `/proc/thread-self/fd` to avoid sharing the file offset.
            // 2. `path` is a remote-fd transfer which asserts `path.dir` is Some.
            #[expect(clippy::disallowed_methods)]
            let fd = path.dir.take().unwrap();

            let mut file = match XPathBuf::from_self_fd(fd.as_raw_fd())
                .and_then(|pfd| {
                    safe_open_msym(PROC_FILE(), &pfd, OFlag::O_RDONLY, ResolveFlag::empty())
                })
                .map(File::from)
            {
                Ok(file) => file,
                Err(_) => {
                    return Err(Errno::EBADF);
                }
            };

            if restrict_exec_stack {
                // Step 4: Check for non-executable stack.
                // An execstack library that is dlopened into an executable
                // that is otherwise mapped no-execstack can change the
                // stack permissions to executable! This has been
                // (ab)used in at least one CVE:
                // https://www.qualys.com/2023/07/19/cve-2023-38408/rce-openssh-forwarded-ssh-agent.txt
                let exe = ExecutableFile::parse(&mut file, true).or(Err(Errno::EACCES))?;
                if matches!(exe, ExecutableFile::Elf { xs: true, .. }) {
                    if !sandbox.filter_path(Capability::CAP_EXEC, path.abs()) {
                        error!("ctx": "check_lib",
                            "msg": "library load with executable stack blocked",
                            "sys": &name, "path": path.abs(),
                            "tip": "configure `trace/allow_unsafe_exec_stack:1'",
                            "lib": format!("{exe}"),
                            "pid": pid.as_raw());
                    }
                    return Err(Errno::EACCES);
                }
            }

            if force {
                // Step 5: Check for Force sandboxing.
                if restrict_exec_stack && file.rewind().is_err() {
                    return Err(Errno::EBADF);
                }
                let result = sandbox.check_force2(path.abs(), &mut file);

                let deny = match result {
                    Ok(action) => {
                        if !matches!(action, Action::Allow | Action::Filter) {
                            warn!("ctx": "verify_lib", "act": action,
                                "sys": &name, "path": path.abs(),
                                "tip": format!("configure `force+{}:<checksum>'", path.abs()),
                                "pid": pid.as_raw());
                        }
                        match action {
                            Action::Allow | Action::Warn => false,
                            Action::Panic | Action::Deny | Action::Filter => true,
                            //Do NOT panic the main thread!
                            //Action::Panic => panic!(),
                            Action::Exit => std::process::exit(libc::EACCES),
                            Action::Stop => {
                                let _ = kill(pid, Some(Signal::SIGSTOP));
                                true
                            }
                            Action::Abort => {
                                let _ = kill(pid, Some(Signal::SIGABRT));
                                true
                            }
                            Action::Kill => {
                                let _ = kill(pid, Some(Signal::SIGKILL));
                                true
                            }
                        }
                    }
                    Err(IntegrityError::Sys(errno)) => {
                        error!("ctx": "verify_lib",
                            "msg": format!("system error during library checksum calculation: {errno}"),
                            "sys": &name, "path": path.abs(),
                            "tip": format!("configure `force+{}:<checksum>'", path.abs()),
                            "pid": pid.as_raw());
                        true
                    }
                    Err(IntegrityError::Hash {
                        action,
                        expected,
                        found,
                    }) => {
                        if action != Action::Filter {
                            error!("ctx": "verify_lib", "act": action,
                                "msg": format!("library checksum mismatch: {found} is not {expected}"),
                                "sys": &name, "path": path.abs(),
                                "tip": format!("configure `force+{}:<checksum>'", path.abs()),
                                "pid": pid.as_raw());
                        }
                        match action {
                            // Allow cannot happen.
                            Action::Allow => unreachable!(),
                            Action::Warn => false,
                            Action::Panic | Action::Deny | Action::Filter => true,
                            //Do NOT panic the main thread!
                            //Action::Panic => panic!(),
                            Action::Exit => std::process::exit(libc::EACCES),
                            Action::Stop => {
                                let _ = kill(pid, Some(Signal::SIGSTOP));
                                true
                            }
                            Action::Abort => {
                                let _ = kill(pid, Some(Signal::SIGABRT));
                                true
                            }
                            Action::Kill => {
                                let _ = kill(pid, Some(Signal::SIGKILL));
                                true
                            }
                        }
                    }
                };

                if deny {
                    return Err(Errno::EACCES);
                }
            }
        }
    }

    if !mem || (mem_max == 0 && mem_vm_max == 0) {
        // (a) Exec and Memory sandboxing are both disabled.
        // (b) Exec granted access, Memory sandboxing is disabled.
        // Stop at syscall exit if check_exec is true, otherwise continue.
        return Ok(check_exec);
    }

    // Check VmSize
    if mem_vm_max > 0 {
        let mem_vm_cur = match proc_statm(pid) {
            Ok(statm) => statm.size.saturating_mul(*PAGE_SIZE),
            Err(errno) => return Err(errno),
        };
        if mem_vm_cur.saturating_add(size) >= mem_vm_max {
            if mem_act != Action::Filter {
                warn!("ctx": "access", "cap": Capability::CAP_MEM, "act": mem_act,
                    "sys": &name, "mem_vm_max": mem_vm_max, "mem_vm_cur": mem_vm_cur,
                    "mem_size": size, "tip": "increase `mem/vm_max'",
                    "pid": pid.as_raw());
            }
            match mem_act {
                // Allow cannot happen.
                Action::Allow => unreachable!(),
                Action::Warn => {}
                Action::Panic | Action::Deny | Action::Filter => return Err(Errno::ENOMEM),
                //Do NOT panic the main thread!
                //Action::Panic => panic!(),
                Action::Exit => std::process::exit(libc::ENOMEM),
                Action::Stop => {
                    let _ = kill(pid, Some(Signal::SIGSTOP));
                    return Err(Errno::ENOMEM);
                }
                Action::Abort => {
                    let _ = kill(pid, Some(Signal::SIGABRT));
                    return Err(Errno::ENOMEM);
                }
                Action::Kill => {
                    let _ = kill(pid, Some(Signal::SIGKILL));
                    return Err(Errno::ENOMEM);
                }
            }
        }
    }

    // Check PSS
    if mem_max > 0 {
        let mem_cur = proc_mem(pid)?;
        if mem_cur.saturating_add(size) >= mem_max {
            if mem_act != Action::Filter {
                warn!("ctx": "access", "cap": Capability::CAP_MEM, "act": mem_act,
                    "sys": &name, "mem_max": mem_max, "mem_cur": mem_cur,
                    "mem_size": size, "tip": "increase `mem/max'",
                    "pid": pid.as_raw());
            }
            return match mem_act {
                // Allow cannot happen.
                Action::Allow => unreachable!(),
                // Stop at syscall exit if check_exec, otherwise continue.
                Action::Warn => Ok(check_exec),
                Action::Panic | Action::Deny | Action::Filter => Err(Errno::ENOMEM),
                //Do NOT panic the main thread!
                //Action::Panic => panic!(),
                Action::Exit => std::process::exit(libc::ENOMEM),
                Action::Stop => {
                    let _ = kill(pid, Some(Signal::SIGSTOP));
                    return Err(Errno::ENOMEM);
                }
                Action::Abort => {
                    let _ = kill(pid, Some(Signal::SIGABRT));
                    return Err(Errno::ENOMEM);
                }
                Action::Kill => {
                    let _ = kill(pid, Some(Signal::SIGKILL));
                    return Err(Errno::ENOMEM);
                }
            };
        }
    }

    // Stop at syscall exit if check_exec is true, otherwise continue.
    Ok(check_exec)
}

#[expect(clippy::cognitive_complexity)]
pub(crate) fn sysexit_mmap(
    pid: Pid,
    info: ptrace_syscall_info,
    sandbox: &SandboxGuard,
) -> Result<(), Errno> {
    // Check for successful mmap exit.
    match ptrace_get_error(pid, info.arch) {
        Ok(None) => {
            // Successful mmap call, validate proc_pid_maps(5).
        }
        Ok(Some(_)) => {
            // Unsuccessful mmap call, continue process.
            return Ok(());
        }
        Err(Errno::ESRCH) => return Err(Errno::ESRCH),
        Err(errno) => {
            // SAFETY: Failed to get return value, terminate the process.
            error!("ctx": "mmap", "op": "read_return",
                "msg": format!("failed to read mmap return: {errno}"),
                "err": errno as i32, "pid": pid.as_raw(),
                "tip": "check with SYD_LOG=debug and/or submit a bug report");
            let _ = kill(pid, Some(Signal::SIGKILL));
            return Err(Errno::ESRCH);
        }
    };

    // SAFETY: Validate executables in proc_pid_maps(5) against TOCTOU.
    let bins = match proc_executables(pid) {
        Ok(bins) => bins,
        Err(errno) => {
            // SAFETY: Failed to read executables,
            // assume TOCTTOU: terminate the process.
            error!("ctx": "mmap", "op": "read_proc_maps",
                "msg": format!("failed to read proc maps: {errno}"),
                "err": errno as i32, "pid": pid.as_raw(),
                "tip": "check with SYD_LOG=debug and/or submit a bug report");
            let _ = kill(pid, Some(Signal::SIGKILL));
            return Err(Errno::ESRCH);
        }
    };

    for exec in bins {
        let path = &exec.path;
        let (action, _) = sandbox.check_path(Capability::CAP_EXEC, path);
        if action.is_allowing() {
            continue;
        }

        // SAFETY: Denied executable appeared in proc_pid_maps(5).
        // successful TOCTTOU attempt: terminate the process.
        error!("ctx": "mmap", "op": "map_mismatch",
            "msg": format!("map mismatch detected for executable `{path}': assume TOCTTOU!"),
            "pid": pid.as_raw(), "path": &path,
            "inode": exec.inode,
            "dev_major": exec.dev_major,
            "dev_minor": exec.dev_minor);
        let _ = kill(pid, Some(Signal::SIGKILL));
        return Err(Errno::ESRCH);
    }

    // Continue process.
    Ok(())
}
