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

use std::{
    fs::File,
    io::{Seek, SeekFrom},
    os::fd::AsFd,
    sync::{Arc, RwLock},
};

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

use crate::{
    cache::ExecResult,
    compat::{fstatx, FileStatx, FsType, ResolveFlag, STATX_INO},
    debug,
    elf::{ElfError, ElfFileType, ElfType, ExecutableFile, LinkingType},
    err::err2no,
    error,
    fd::AT_BADFD,
    log_enabled,
    lookup::safe_open,
    proc::{proc_executables, proc_set_at_secure, SydExecMap},
    sandbox::{Action, Capability, IntegrityError, Sandbox, SandboxGuard},
    syslog::LogLevel,
    warn,
    workers::WorkerCache,
};

#[expect(clippy::cognitive_complexity)]
pub(crate) fn sysevent_exec(pid: Pid, cache: &Arc<WorkerCache>, sandbox: &Arc<RwLock<Sandbox>>) {
    // This is ptrace syscall exec stop.
    //
    // An important caveat is the TGID may have switched.

    // Retrieve the exec record from the cache.
    // Handles TGID switch as necessary.
    let rx = match exec_get_cache(pid, cache) {
        Some(rx) => rx,
        None => return,
    };

    // Read executable file information.
    let (exe_inode, exe_dev_major, exe_dev_minor) = match exec_get_stat(pid, &rx.file) {
        Some(stx) => (stx.stx_ino, stx.stx_dev_major, stx.stx_dev_minor),
        None => return,
    };
    let mut exe = rx.exe;

    // Read executable paths.
    // This includes the executable, and the loader if executable is dynamically linked.
    let bins = match exec_get_proc(pid) {
        Some(bins) => bins,
        None => return,
    };
    let path = &bins[0].path; // Path to the executable.
    let mut deny_action: Option<Action> = None;

    // Determine open flags.
    let flags = if exe == ExecutableFile::Script {
        // We will read from the file and parse ELF.
        OFlag::O_RDONLY | OFlag::O_NOFOLLOW | OFlag::O_NOCTTY
    } else {
        // ELF parsing was done at syscall entry, verify paths.
        OFlag::O_PATH | OFlag::O_NOFOLLOW
    };

    // Open paths and verify the open FDs match the device ID and inode information.
    // The FDs will be used for two things:
    // 1. Parsing ELF to determine bitness, PIE etc.
    // 2. Checksumming binary for Force sandboxing.
    let mut file = Some(rx.file);
    let mut files = Vec::with_capacity(2);
    for bin in &bins {
        let is_exe;
        #[expect(clippy::cast_sign_loss)]
        let result = if file.is_some() {
            is_exe = bin.inode == exe_inode
                && bin.dev_major as u32 == exe_dev_major
                && bin.dev_minor as u32 == exe_dev_minor;
            if is_exe {
                #[expect(clippy::disallowed_methods)]
                Ok(file.take().unwrap().into())
            } else {
                safe_open(AT_BADFD, &bin.path, flags, ResolveFlag::empty())
            }
        } else {
            is_exe = false;
            safe_open(AT_BADFD, &bin.path, flags, ResolveFlag::empty())
        };

        match result {
            Ok(fd) if is_exe => {
                // Executable file checked out!
                files.push(File::from(fd));
            }
            Ok(fd) => {
                // WORKAROUND: Check if the FS reports sane device ids.
                // Check the comment on has_broken_device_ids() function
                // for more information.
                // Assume true on errors for safety.
                let dev_check = match FsType::get(&fd).map(|fs_type| !fs_type.has_broken_devid()) {
                    Ok(dev_check) => dev_check,
                    Err(Errno::ENOSYS) => {
                        // Filesystem type does not support this call.
                        // Assume true for safety.
                        true
                    }
                    Err(errno) => {
                        error!("ctx": "exec", "op": "open_elf",
                            "msg": format!("statfs error: {errno}"),
                            "err": errno as i32,
                            "pid": pid.as_raw(), "path": path);
                        let _ = kill(pid, Some(Signal::SIGKILL));
                        return;
                    }
                };
                let statx = match fstatx(&fd, STATX_INO) {
                    Ok(stat) => stat,
                    Err(errno) => {
                        error!("ctx": "exec", "op": "open_elf",
                            "msg": format!("statx error: {errno}"),
                            "err": errno as i32,
                            "pid": pid.as_raw(), "path": path);
                        let _ = kill(pid, Some(Signal::SIGKILL));
                        return;
                    }
                };
                // SAFETY: Verify we opened the same file!
                #[expect(clippy::cast_sign_loss)]
                let dev_major = bin.dev_major as libc::c_uint;
                #[expect(clippy::cast_sign_loss)]
                let dev_minor = bin.dev_minor as libc::c_uint;
                if bin.inode != statx.stx_ino
                    || (dev_check
                        && (dev_major != statx.stx_dev_major || dev_minor != statx.stx_dev_minor))
                {
                    let error = format!(
                        "metadata mismatch: {}:{}={} is not {}:{}={}",
                        statx.stx_dev_major,
                        statx.stx_dev_minor,
                        statx.stx_ino,
                        dev_major,
                        dev_minor,
                        bin.inode
                    );
                    error!("ctx": "exec", "op": "open_elf",
                        "msg": error,
                        "pid": pid.as_raw(),"path": path);
                    let _ = kill(pid, Some(Signal::SIGKILL));
                    return;
                }
                files.push(File::from(fd));
            }
            Err(errno) => {
                error!("ctx": "exec", "op": "open_elf",
                    "msg": format!("open error: {errno}"),
                    "err": errno as i32,
                    "pid": pid.as_raw(), "path": path);
                let _ = kill(pid, Some(Signal::SIGKILL));
                return;
            }
        }
    }
    drop(file);

    // Parse ELF file to figure out type, if the original file we've checked was a script.
    let mut my_sandbox = SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));
    if exe == ExecutableFile::Script {
        // Check SegvGuard.
        if let Some(action) = my_sandbox.check_segvguard(path) {
            if action != Action::Filter {
                error!("ctx": "exec", "op": "segvguard",
                    "msg": format!("Max crashes {} exceeded, kill process {}",
                        my_sandbox.segvguard_maxcrashes,
                        pid.as_raw()),
                    "tip": "increase `segvguard/maxcrashes'",
                    "pid": pid.as_raw(), "path": path);
            }
            if action == Action::Exit {
                std::process::exit(libc::EACCES);
            } else if action.is_signaling() {
                deny_action = Some(action);
            } else if action.is_denying() {
                deny_action = Some(Action::Kill);
            }
        }

        // Check for Exec sandboxing.
        if deny_action.is_none() && my_sandbox.enabled(Capability::CAP_EXEC) {
            for bin in &bins {
                let path = &bin.path;
                let (mut action, filter) = my_sandbox.check_path(Capability::CAP_EXEC, path);
                if action == Action::Deny {
                    // ptrace-event-exec stop:
                    // promote deny action to kill.
                    action = Action::Kill;
                }
                if !filter {
                    warn!("ctx": "access", "cap": Capability::CAP_EXEC, "act": action,
                        "pid": pid.as_raw(), "sys": "exec", "path": path,
                        "tip": format!("configure `allow/exec+{path}'"));
                }
                match action {
                    Action::Allow | Action::Warn => {}
                    Action::Stop => {
                        deny_action = Some(Action::Stop);
                        break;
                    }
                    Action::Abort => {
                        deny_action = Some(Action::Abort);
                        break;
                    }
                    Action::Exit => std::process::exit(libc::EACCES),
                    _ => {
                        // Deny|Filter|Kill
                        deny_action = Some(Action::Kill);
                        break;
                    }
                }
            }
        }

        // Check for Trusted Path Execution (TPE).
        if deny_action.is_none() && my_sandbox.enabled(Capability::CAP_TPE) {
            for (idx, bin) in bins.iter().enumerate() {
                let file = &files[idx];
                let path = &bin.path;
                let (action, msg) = my_sandbox.check_tpe(file, path);
                if !matches!(action, Action::Allow | Action::Filter) {
                    let msg = msg.as_deref().unwrap_or("?");
                    error!("ctx": "exec", "op": "trusted_path_execution", "err": libc::EACCES,
                        "pid": pid.as_raw(), "sys": "exec", "path": path, "act": action,
                        "msg": format!("exec from untrusted path blocked: {msg}"),
                        "tip": "move the binary to a safe location or use `sandbox/tpe:off'");
                }
                match action {
                    Action::Allow | Action::Warn => {}
                    Action::Stop => deny_action = Some(Action::Stop),
                    Action::Abort => deny_action = Some(Action::Abort),
                    Action::Exit => std::process::exit(libc::EACCES),
                    _ => {
                        // Deny|Filter|Kill
                        deny_action = Some(Action::Kill);
                    }
                }
            }
        }

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

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

        // Drop sandbox lock before blocking operation.
        drop(my_sandbox);

        // Ensure the file offset is maintained,
        // as the file might be sharing the OFD
        // with the sandbox process.
        let mut file = &files[0];
        let offset = match file.stream_position().map_err(|err| err2no(&err)) {
            Ok(offset) => offset,
            Err(errno) => {
                // This should never happen in an ideal world,
                // let's handle it as gracefully as we can...
                error!("ctx": "exec", "op": "read_offset",
                    "msg": format!("failed to read exec file offset: {errno}"),
                    "err": errno as i32,
                    "tip": "check with SYD_LOG=debug and/or submit a bug report");
                let _ = kill(pid, Some(Signal::SIGKILL));
                return;
            }
        };

        let result = (|| -> Result<ExecutableFile, ElfError> {
            // Parse ELF and reset the file offset.
            if offset != 0 {
                file.rewind().map_err(ElfError::IoError)?;
            }
            let result = ExecutableFile::parse(file, check_linking);
            file.seek(SeekFrom::Start(offset))
                .map_err(ElfError::IoError)?;
            result
        })();

        // Re-acquire the read-lock.
        my_sandbox = SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));

        match result {
            // Update ELF information.
            Ok(exe_bin) => exe = exe_bin,
            Err(ElfError::IoError(err)) => {
                deny_action = Some(Action::Kill);
                if !my_sandbox.filter_path(Capability::CAP_EXEC, path) {
                    error!("ctx": "exec", "op": "parse_elf",
                        "msg": format!("io error: {}", err2no(&err)),
                        "err": err2no(&err) as i32,
                        "pid": pid.as_raw(), "path": path);
                }
            }
            Err(ElfError::BadMagic) => {
                deny_action = Some(Action::Kill);
                if !my_sandbox.filter_path(Capability::CAP_EXEC, path) {
                    error!("ctx": "exec", "op": "parse_elf",
                        "msg": format!("BUG: not an ELF"),
                        "pid": pid.as_raw(), "path": path);
                }
            }
            Err(ElfError::Malformed) => {
                deny_action = Some(Action::Kill);
                if !my_sandbox.filter_path(Capability::CAP_EXEC, path) {
                    error!("ctx": "exec", "op": "parse_elf",
                        "msg": format!("BUG: malformed ELF"),
                        "pid": pid.as_raw(), "path": path);
                }
            }
        };

        if restrict_ldd
            && !matches!(
                exe,
                ExecutableFile::Elf {
                    file_type: ElfFileType::Executable,
                    ..
                }
            )
        {
            deny_action = Some(Action::Kill);
            if !my_sandbox.filter_path(Capability::CAP_EXEC, path) {
                error!("ctx": "exec", "op": "check_elf",
                    "msg": "ld.so(8) exec-indirection prevented",
                    "pid": pid.as_raw(), "path": path,
                    "tip": "configure `trace/allow_unsafe_exec_ldso:1'",
                    "exe": format!("{exe}"));
            }
        }

        if deny_action.is_none()
            && restrict_pie
            && matches!(exe, ExecutableFile::Elf { pie: false, .. })
        {
            deny_action = Some(Action::Kill);
            if !my_sandbox.filter_path(Capability::CAP_EXEC, path) {
                error!("ctx": "exec", "op": "check_elf",
                    "msg": "ELF is not a Position Independent Executable (PIE)",
                    "pid": pid.as_raw(), "path": path,
                    "tip": "configure `trace/allow_unsafe_exec_nopie:1'",
                    "exe": format!("{exe}"));
            }
        }

        if deny_action.is_none()
            && restrict_xs
            && matches!(exe, ExecutableFile::Elf { xs: true, .. })
        {
            deny_action = Some(Action::Kill);
            if !my_sandbox.filter_path(Capability::CAP_EXEC, path) {
                error!("ctx": "exec", "op": "check_elf",
                    "msg": "ELF has Executable Stack (PT_GNU_STACK)",
                    "pid": pid.as_raw(), "path": path,
                    "tip": "configure `trace/allow_unsafe_exec_stack:1'",
                    "exe": format!("{exe}"));
            }
        }

        if deny_action.is_none()
            && restrict_32
            && matches!(
                exe,
                ExecutableFile::Elf {
                    elf_type: ElfType::Elf32,
                    ..
                }
            )
        {
            deny_action = Some(Action::Kill);
            if !my_sandbox.filter_path(Capability::CAP_EXEC, path) {
                error!("ctx": "exec", "op": "check_elf",
                    "msg": "32-bit execution prevented",
                    "pid": pid.as_raw(), "path": path,
                    "tip": "configure `trace/deny_exec_elf32:0'",
                    "exe": format!("{exe}"));
            }
        }

        if deny_action.is_none()
            && restrict_dyn
            && matches!(
                exe,
                ExecutableFile::Elf {
                    linking_type: Some(LinkingType::Dynamic),
                    ..
                }
            )
        {
            deny_action = Some(Action::Kill);
            if !my_sandbox.filter_path(Capability::CAP_EXEC, path) {
                error!("ctx": "exec", "op": "check_elf",
                    "msg": "dynamic-link execution prevented",
                    "pid": pid.as_raw(), "path": path,
                    "tip": "configure `trace/deny_exec_elf_dynamic:0'",
                    "exe": format!("{exe}"));
            }
        }

        if deny_action.is_none()
            && restrict_sta
            && matches!(
                exe,
                ExecutableFile::Elf {
                    linking_type: Some(LinkingType::Static),
                    ..
                }
            )
        {
            deny_action = Some(Action::Kill);
            if !my_sandbox.filter_path(Capability::CAP_EXEC, path) {
                error!("ctx": "exec", "op": "check_elf",
                    "msg": "static-link execution prevented",
                    "pid": pid.as_raw(), "path": path,
                    "tip": "configure `trace/deny_exec_elf_static:0'",
                    "exe": format!("{exe}"));
            }
        }

        // Check for Force sandboxing.
        if deny_action.is_none() && my_sandbox.enabled(Capability::CAP_FORCE) {
            for (idx, bin) in bins.iter().enumerate() {
                let file = &mut files[idx];
                let path = &bin.path;
                let result =
                    (|mut file: &mut File, idx, offset| -> Result<Action, IntegrityError> {
                        // Calculate checksum and reset file offset as necessary.
                        if idx == 0 {
                            if offset != 0 {
                                file.rewind().map_err(IntegrityError::from)?;
                            }
                            let result = my_sandbox.check_force2(path, &mut file);
                            file.seek(SeekFrom::Start(offset))
                                .map_err(IntegrityError::from)?;
                            result
                        } else {
                            my_sandbox.check_force2(path, &mut file)
                        }
                    })(file, idx, offset);
                match result {
                    Ok(Action::Allow) => {}
                    Ok(Action::Warn) => {
                        warn!("ctx": "exec", "op": "verify_elf", "act": Action::Warn,
                            "pid": pid.as_raw(), "path": path,
                            "tip": format!("configure `force+{path}:<checksum>'"));
                    }
                    Ok(Action::Stop) => {
                        deny_action = Some(Action::Stop);
                        warn!("ctx": "exec", "op": "verify_elf", "act": Action::Stop,
                            "pid": pid.as_raw(), "path": path,
                            "tip": format!("configure `force+{path}:<checksum>'"));
                    }
                    Ok(Action::Abort) => {
                        deny_action = Some(Action::Abort);
                        warn!("ctx": "exec", "op": "verify_elf", "act": Action::Abort,
                            "pid": pid.as_raw(), "path": path,
                            "tip": format!("configure `force+{path}:<checksum>'"));
                    }
                    Ok(Action::Exit) => {
                        error!("ctx": "exec", "op": "verify_elf", "act": Action::Exit,
                            "pid": pid.as_raw(), "path": path,
                            "tip": format!("configure `force+{path}:<checksum>'"));
                        std::process::exit(libc::EACCES);
                    }
                    Ok(mut action) => {
                        // Deny|Filter|Kill
                        deny_action = Some(Action::Kill);
                        if action == Action::Deny {
                            // ptrace-event-exec stop:
                            // promote deny action to kill.
                            action = Action::Kill;
                        }
                        if action != Action::Filter {
                            warn!("ctx": "exec", "op": "verify_elf", "act": action,
                                "pid": pid.as_raw(), "path": path,
                                "tip": format!("configure `force+{path}:<checksum>'"));
                        }
                    }
                    Err(IntegrityError::Sys(errno)) => {
                        deny_action = Some(Action::Kill);
                        error!("ctx": "exec", "op": "verify_elf",
                            "msg": format!("system error during ELF checksum calculation: {errno}"),
                            "err": errno as i32,
                            "pid": pid.as_raw(), "path": path,
                            "tip": format!("configure `force+{path}:<checksum>'"));
                    }
                    Err(IntegrityError::Hash {
                        mut action,
                        expected,
                        found,
                    }) => {
                        if action == Action::Deny {
                            // ptrace-event-exec stop:
                            // promote deny action to kill.
                            action = Action::Kill;
                        }
                        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,
                                "tip": format!("configure `force+{path}:<checksum>'"));
                        }
                        match action {
                            Action::Allow | Action::Warn => {}
                            Action::Stop => deny_action = Some(Action::Stop),
                            Action::Abort => deny_action = Some(Action::Abort),
                            Action::Exit => std::process::exit(libc::EACCES),
                            _ =>
                            /*Deny|Filter|Kill*/
                            {
                                deny_action = Some(Action::Kill)
                            }
                        };
                    }
                }
            }
        }
    }

    if deny_action.is_none() && !my_sandbox.flags.allow_unsafe_exec_libc() {
        let elf_type = match exe {
            ExecutableFile::Elf { elf_type, .. } => elf_type,
            _ => unreachable!(), // Script is not possible here.
        };

        // SAFETY:
        // 1. Sets AT_SECURE.
        // 2. Verifies AT_{E,}{U,G}ID matches Syd's own.
        // 3. Overwrite AT_SYSINFO{,_EHDR} if trace/deny_vdso:1.
        match proc_set_at_secure(pid, elf_type, my_sandbox.flags.deny_vdso()) {
            Ok(_) | Err(Errno::ESRCH) => {}
            Err(errno) => {
                deny_action = Some(Action::Kill);
                if !my_sandbox.filter_path(Capability::CAP_EXEC, path) {
                    error!("ctx": "exec", "op": "secure_exec",
                        "msg": format!("error setting AT_SECURE: {errno}"),
                        "err": errno as i32,
                        "tip": "configure `trace/allow_unsafe_exec_libc:1'",
                        "pid": pid.as_raw(), "path": path);
                }
            }
        }
    }

    // Release the read lock.
    drop(my_sandbox);

    if let Some(action) = deny_action {
        let _ = kill(
            pid,
            Some(
                Signal::try_from(
                    action
                        .signal()
                        .map(|sig| sig as i32)
                        .unwrap_or(libc::SIGKILL),
                )
                .unwrap_or(Signal::SIGKILL),
            ),
        );
    } else {
        if log_enabled!(LogLevel::Debug) {
            let exe = exe.to_string();
            debug!("ctx": "exec", "op": "verify_exec",
                "msg": format!("execution of `{path}' of type {exe} approved"),
                "pid": pid.as_raw(), "path": &path, "exe": &exe);
        }

        let _ = ptrace::cont(pid, None);
    }
}

fn exec_get_cache(pid: Pid, cache: &Arc<WorkerCache>) -> Option<ExecResult> {
    if let Some((_, result)) = cache.get_exec(pid) {
        // Quick path: pid is in execmap.
        return Some(result);
    }

    // Pid may have been switched to the thread group ID,
    // so we need to call getevent to get the actual thread ID.
    #[expect(clippy::cast_possible_truncation)]
    match ptrace::getevent(pid).map(|tid| Pid::from_raw(tid as i32)) {
        Ok(tid) if pid != tid => {
            if let Some((_, result)) = cache.get_exec(tid) {
                Some(result)
            } else {
                // SAFETY: Exec sandboxing is/was disabled.
                let _ = ptrace::cont(pid, None);
                None
            }
        }
        Ok(_) => {
            // SAFETY: Exec sandboxing is/was disabled.
            let _ = ptrace::cont(pid, None);
            None
        }
        Err(Errno::ESRCH) => None,
        Err(errno) => {
            error!("ctx": "exec", "op": "getevent",
                "msg": format!("failed to get ptrace event message: {errno}"),
                "err": errno as i32,
                "tip": "check with SYD_LOG=debug and/or submit a bug report");
            let _ = kill(pid, Some(Signal::SIGKILL));
            None
        }
    }
}

fn exec_get_stat<Fd: AsFd>(pid: Pid, fd: Fd) -> Option<FileStatx> {
    match fstatx(fd, STATX_INO) {
        Ok(stx) => Some(stx),
        Err(errno) => {
            // This should never happen in an ideal world,
            // let's handle it as gracefully as we can...
            error!("ctx": "exec", "op": "read_stat",
                "msg": format!("failed to read exec file stats: {errno}"),
                "err": errno as i32,
                "tip": "check with SYD_LOG=debug and/or submit a bug report");
            let _ = kill(pid, Some(Signal::SIGKILL));
            None
        }
    }
}

fn exec_get_proc(pid: Pid) -> Option<Vec<SydExecMap>> {
    match proc_executables(pid) {
        Ok(bins) => Some(bins),
        Err(errno) => {
            // This should never happen in an ideal world,
            // let's handle it as gracefully as we can...
            error!("ctx": "exec", "op": "read_maps",
                "msg": format!("failed to read /proc/{}/maps: {errno}", pid.as_raw()),
                "err": errno as i32,
                "tip": "check with SYD_LOG=debug and/or submit a bug report");
            let _ = kill(pid, Some(Signal::SIGKILL));
            None
        }
    }
}
