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

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

use libseccomp::ScmpNotifResp;
use nix::{errno::Errno, fcntl::OFlag};
use serde::{Serialize, Serializer};

use crate::{
    compat::ResolveFlag,
    config::{PAGE_SIZE, PROC_FILE},
    elf::ExecutableFile,
    error,
    fd::fd_status_flags,
    kernel::sandbox_path,
    lookup::{safe_open_msym, CanonicalPath},
    path::XPathBuf,
    proc::{proc_mem, proc_stat, proc_statm},
    req::UNotifyEventRequest,
    sandbox::{Action, Capability, IntegrityError},
    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;

// `MemSyscall` represents possible memory family system calls.
//
// This list of memory family system calls are: brk(2), mmap(2),
// mmap2(2), and mremap(2).
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MemSyscall {
    Brk,
    Mmap,
    Mmap2,
    Mremap,
}

impl MemSyscall {
    const fn is_mmap(self) -> bool {
        matches!(self, Self::Mmap | Self::Mmap2)
    }

    const fn caps(self) -> Capability {
        match self {
            Self::Brk | Self::Mremap => Capability::CAP_MEM,
            Self::Mmap | Self::Mmap2 => Capability::CAP_MMAP,
        }
    }
}

impl fmt::Display for MemSyscall {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let name = match self {
            Self::Brk => "brk",
            Self::Mmap => "mmap",
            Self::Mmap2 => "mmap2",
            Self::Mremap => "mremap",
        };
        f.write_str(name)
    }
}

impl Serialize for MemSyscall {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

pub(crate) fn sys_brk(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    let size = match proc_stat(req.pid()) {
        Ok(stat) => req.data.args[0].saturating_sub(stat.startbrk),
        Err(errno) => return request.fail_syscall(errno),
    };
    if size == 0 {
        // SAFETY: System call wants to shrink memory.
        // No pointer dereference in size check.
        return unsafe { request.continue_syscall() };
    }
    syscall_mem_handler(request, MemSyscall::Brk, size)
}

pub(crate) fn sys_mmap(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_mem_handler(request, MemSyscall::Mmap, req.data.args[1])
}

pub(crate) fn sys_mmap2(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_mem_handler(request, MemSyscall::Mmap2, req.data.args[1])
}

pub(crate) fn sys_mremap(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    let old_size = req.data.args[1];
    let new_size = req.data.args[2];
    let size = new_size.saturating_sub(old_size);
    if size == 0 {
        // SAFETY: System call wants to shrink memory.
        // No pointer dereference in size check.
        return unsafe { request.continue_syscall() };
    }
    syscall_mem_handler(request, MemSyscall::Mremap, size)
}

#[expect(clippy::cognitive_complexity)]
fn syscall_mem_handler(
    request: UNotifyEventRequest,
    syscall: MemSyscall,
    size: u64,
) -> ScmpNotifResp {
    let req = request.scmpreq;
    let caps = syscall.caps();

    // Get mem & vm max.
    let sandbox = request.get_sandbox();
    let log_scmp = sandbox.log_scmp();
    let caps = sandbox.getcaps(caps);
    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))
    {
        // SAFETY: No pointer dereference in security check.
        // This is safe to continue.
        return unsafe { request.continue_syscall() };
    }

    let name = syscall.to_string();

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

    // Get the file descriptor before access check.
    let fd = if check_exec || check_append_only {
        let remote_fd = match RawFd::try_from(req.data.args[4]) {
            Ok(fd) if fd < 0 => return request.fail_syscall(Errno::EBADF),
            Ok(fd) => fd,
            Err(_) => return request.fail_syscall(Errno::EBADF),
        };

        match request.get_fd(remote_fd) {
            Ok(fd) => Some(fd),
            Err(_) => return request.fail_syscall(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 request.fail_syscall(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 request.fail_syscall(Errno::EACCES);
            }
        }

        #[expect(clippy::disallowed_methods)]
        let mut path = match CanonicalPath::new_fd(fd.unwrap().into(), req.pid()) {
            Ok(path) => path,
            Err(errno) => return request.fail_syscall(errno),
        };

        // Step 2: Check for Exec sandboxing.
        if exec {
            if let Err(errno) = sandbox_path(
                Some(&request),
                &sandbox,
                request.scmpreq.pid(), // Unused when request.is_some()
                path.abs(),
                Capability::CAP_EXEC,
                false,
                &name,
            ) {
                return request.fail_syscall(errno);
            }
        }

        // 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("?");
                if log_scmp {
                    error!("ctx": "trusted_path_execution",
                        "msg": format!("library load from untrusted path blocked: {msg}"),
                        "sys": &name, "path": &path,
                        "req": &request,
                        "tip": "move the library to a safe location or use `sandbox/tpe:off'");
                } else {
                    error!("ctx": "trusted_path_execution",
                        "msg": format!("library load from untrusted path blocked: {msg}"),
                        "sys": &name, "path": &path,
                        "pid": request.scmpreq.pid,
                        "tip": "move the library to a safe location or use `sandbox/tpe:off'");
                }
            }
            match action {
                Action::Allow | Action::Warn => {}
                Action::Deny | Action::Filter => return request.fail_syscall(Errno::EACCES),
                Action::Panic => panic!(),
                Action::Exit => std::process::exit(libc::EACCES),
                action => {
                    // Stop|Kill
                    let _ = request.kill(action);
                    return request.fail_syscall(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 request.fail_syscall(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 result = (|file: &mut File| -> Result<(), Errno> {
                    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()) {
                            if log_scmp {
                                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}"),
                                    "req": &request);
                            } else {
                                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": request.scmpreq.pid);
                            }
                        }
                        Err(Errno::EACCES)
                    } else {
                        Ok(())
                    }
                })(&mut file);

                if let Err(errno) = result {
                    return request.fail_syscall(errno);
                }
            }

            if force {
                // Step 5: Check for Force sandboxing.
                if restrict_exec_stack && file.rewind().is_err() {
                    drop(sandbox); // release the read-lock.
                    return request.fail_syscall(Errno::EBADF);
                }
                let result = sandbox.check_force2(path.abs(), &mut file);

                let deny = match result {
                    Ok(action) => {
                        if !matches!(action, Action::Allow | Action::Filter) {
                            if log_scmp {
                                warn!("ctx": "verify_lib", "act": action,
                                    "sys": &name, "path": path.abs(),
                                    "tip": format!("configure `force+{}:<checksum>'", path.abs()),
                                    "sys": &name, "req": &request);
                            } else {
                                warn!("ctx": "verify_lib", "act": action,
                                    "sys": &name, "path": path.abs(),
                                    "tip": format!("configure `force+{}:<checksum>'", path.abs()),
                                    "pid": request.scmpreq.pid);
                            }
                        }
                        match action {
                            Action::Allow | Action::Warn => false,
                            Action::Deny | Action::Filter => true,
                            Action::Panic => panic!(),
                            Action::Exit => std::process::exit(libc::EACCES),
                            _ => {
                                // Stop|Kill
                                let _ = request.kill(action);
                                true
                            }
                        }
                    }
                    Err(IntegrityError::Sys(errno)) => {
                        if log_scmp {
                            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()),
                                "req": &request);
                        } else {
                            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": request.scmpreq.pid);
                        }
                        true
                    }
                    Err(IntegrityError::Hash {
                        action,
                        expected,
                        found,
                    }) => {
                        if action != Action::Filter {
                            if log_scmp {
                                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()),
                                    "req": &request);
                            } else {
                                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": request.scmpreq.pid);
                            }
                        }
                        match action {
                            // Allow cannot happen.
                            Action::Warn => false,
                            Action::Deny | Action::Filter => true,
                            Action::Panic => panic!(),
                            Action::Exit => std::process::exit(libc::EACCES),
                            _ => {
                                // Stop|Kill
                                let _ = request.kill(action);
                                true
                            }
                        }
                    }
                };

                if deny {
                    return request.fail_syscall(Errno::EACCES);
                }
            }
        }
    }
    drop(sandbox); // release the read-lock.

    if !mem || (mem_max == 0 && mem_vm_max == 0) {
        // SAFETY:
        // (a) Exec and Memory sandboxing are both disabled.
        // (b) Exec granted access, Memory sandboxing is disabled.
        // The first candidate is safe as sandboxing is disabled,
        // however (b) suffers from VFS TOCTOU as the fd can change
        // after the access check. This is why by default we hook
        // into mmap{,2} with ptrace(2) and guard it with the
        // TOCTOU-mitigator. mmap{,2} only ends up here with
        // trace/allow_unsafe_ptrace:1.
        return unsafe { request.continue_syscall() };
    }

    // Check VmSize
    if mem_vm_max > 0 {
        let mem_vm_cur = match proc_statm(req.pid()) {
            Ok(statm) => statm.size.saturating_mul(*PAGE_SIZE),
            Err(errno) => return request.fail_syscall(errno),
        };
        if mem_vm_cur.saturating_add(size) >= mem_vm_max {
            if mem_act != Action::Filter {
                if log_scmp {
                    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'",
                        "req": &request);
                } else {
                    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": request.scmpreq.pid);
                }
            }
            match mem_act {
                // Allow cannot happen.
                Action::Warn => {}
                Action::Deny | Action::Filter => return request.fail_syscall(Errno::ENOMEM),
                Action::Panic => panic!(),
                Action::Exit => std::process::exit(libc::ENOMEM),
                _ => {
                    // Stop|Kill
                    let _ = request.kill(mem_act);
                    return request.fail_syscall(Errno::ENOMEM);
                }
            }
        }
    }

    // Check PSS
    if mem_max > 0 {
        let mem_cur = match proc_mem(req.pid()) {
            Ok(mem_cur) => mem_cur,
            Err(errno) => return request.fail_syscall(errno),
        };
        if mem_cur.saturating_add(size) >= mem_max {
            if mem_act != Action::Filter {
                if log_scmp {
                    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'",
                        "req": &request);
                } else {
                    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": request.scmpreq.pid);
                }
            }
            return match mem_act {
                // Allow cannot happen.
                Action::Warn => {
                    // SAFETY: No pointer dereference in security check.
                    unsafe { request.continue_syscall() }
                }
                Action::Deny | Action::Filter => request.fail_syscall(Errno::ENOMEM),
                Action::Panic => panic!(),
                Action::Exit => std::process::exit(libc::ENOMEM),
                _ => {
                    // Stop|Kill
                    let _ = request.kill(mem_act);
                    request.fail_syscall(Errno::ENOMEM)
                }
            };
        }
    }

    // SAFETY: No pointer dereference in security check.
    unsafe { request.continue_syscall() }
}
