//
// Syd: rock-solid application kernel
// src/utils/syd-elf.rs: Syd's ELF information utility
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{env, fs::File, os::fd::AsRawFd, process::ExitCode};

use libseccomp::{scmp_cmp, ScmpAction, ScmpFilterContext, ScmpSyscall};
use nix::{errno::Errno, sys::resource::Resource};
use syd::{
    config::{ENV_SKIP_SCMP, VDSO_SYSCALLS},
    confine::{confine_mdwe, confine_rlimit_zero, confine_scmp_madvise, confine_scmp_wx},
    elf::{ElfError, ElfType, ExecutableFile, LinkingType},
    err::SydResult,
    landlock_policy::LandlockPolicy,
    path::XPathBuf,
};

// Set global allocator to GrapheneOS allocator.
#[cfg(all(
    not(coverage),
    not(feature = "prof"),
    not(target_os = "android"),
    target_page_size_4k,
    target_pointer_width = "64"
))]
#[global_allocator]
static GLOBAL: hardened_malloc::HardenedMalloc = hardened_malloc::HardenedMalloc;

// Set global allocator to tcmalloc if profiling is enabled.
#[cfg(feature = "prof")]
#[global_allocator]
static GLOBAL: tcmalloc::TCMalloc = tcmalloc::TCMalloc;

syd::main! {
    use lexopt::prelude::*;

    syd::set_sigpipe_dfl()?;

    // Parse CLI options.
    let mut etyp = false;
    let mut is_32bit = false;
    let mut is_64bit = false;
    let mut is_dynamic = false;
    let mut is_static = false;
    let mut is_pie = false;
    let mut is_script = false;
    let mut is_xstack = false;
    let mut opt_path = None;

    let mut parser = lexopt::Parser::from_env();
    while let Some(arg) = parser.next()? {
        match arg {
            Short('h') => {
                help();
                return Ok(ExitCode::SUCCESS);
            }
            Short('3') => is_32bit = true,
            Short('6') => is_64bit = true,
            Short('d') => is_dynamic = true,
            Short('s') => is_static = true,
            Short('p') => is_pie = true,
            Short('x') => is_script = true,
            Short('X') => is_xstack = true,
            Short('t') => etyp = true,
            Value(path) => opt_path = Some(XPathBuf::from(path)),
            _ => return Err(arg.unexpected().into()),
        }
    }

    let flags = [
        is_32bit, is_64bit, is_dynamic, is_static, is_pie, etyp, is_script, is_xstack,
    ];
    let info = match flags.iter().filter(|&&flag| flag).count() {
        0 => true,
        1 => false,
        _ => {
            eprintln!("syd-elf: At most one of -3, -6, -d, -s, -p, -t, -x and -X must be given!");
            return Err(Errno::EINVAL.into());
        }
    };

    let path = if let Some(path) = opt_path {
        path
    } else {
        eprintln!("syd-elf: Expected exactly one path as argument!");
        return Err(Errno::EINVAL.into());
    };

    let check_linking = info || is_dynamic || is_static || is_pie || is_xstack;

    // Open file.
    #[expect(clippy::disallowed_methods)]
    let file = File::open(&path)?;

    // Confine unless SYD_SKIP_SCMP is set.
    if env::var_os(ENV_SKIP_SCMP).is_none() {
        confine(&file)?;
    }

    // Parse ELF.
    let exe = match ExecutableFile::parse(file, check_linking) {
        Ok(exe) => Some(exe),
        Err(ElfError::BadMagic) => None,
        Err(error) => return Err(error.into()),
    };

    // Report result.
    if is_script {
        return Ok(match exe {
            Some(ExecutableFile::Script) => ExitCode::SUCCESS,
            _ => ExitCode::FAILURE,
        });
    } else if is_32bit {
        return Ok(match exe {
            Some(ExecutableFile::Elf {
                elf_type: ElfType::Elf32,
                ..
            }) => ExitCode::SUCCESS,
            _ => ExitCode::FAILURE,
        });
    } else if is_64bit {
        return Ok(match exe {
            Some(ExecutableFile::Elf {
                elf_type: ElfType::Elf64,
                ..
            }) => ExitCode::SUCCESS,
            _ => ExitCode::FAILURE,
        });
    } else if is_dynamic {
        return Ok(match exe {
            Some(ExecutableFile::Elf {
                linking_type: Some(LinkingType::Dynamic),
                ..
            }) => ExitCode::SUCCESS,
            _ => ExitCode::FAILURE,
        });
    } else if is_static {
        return Ok(match exe {
            Some(ExecutableFile::Elf {
                linking_type: Some(LinkingType::Static),
                ..
            }) => ExitCode::SUCCESS,
            _ => ExitCode::FAILURE,
        });
    } else if is_pie {
        return Ok(match exe {
            Some(ExecutableFile::Elf { pie: true, .. }) => ExitCode::SUCCESS,
            _ => ExitCode::FAILURE,
        });
    } else if is_xstack {
        return Ok(match exe {
            Some(ExecutableFile::Elf { xs: true, .. }) => ExitCode::SUCCESS,
            _ => ExitCode::FAILURE,
        });
    } else if etyp {
        let name = match exe {
            Some(ExecutableFile::Elf { file_type, .. }) => file_type.to_string(),
            Some(ExecutableFile::Script) => "script".to_string(),
            None => "unknown".to_string(),
        };
        println!("{name}");
        return Ok(ExitCode::SUCCESS);
    } else if let Some(exe) = exe {
        // Print ELF information or SCRIPT.
        println!("{path}:{exe}");
    } else {
        println!("{path}:UNKNOWN");
    }

    Ok(ExitCode::SUCCESS)
}

fn help() {
    println!("Usage: syd-elf [-36dhpstxX] binary|script");
    println!("Given a binary, print file name and ELF information.");
    println!("Given a script, print file name and \"SCRIPT\".");
    println!("The information line is a list of fields delimited by colons.");
    println!("Given -3, exit with success if the given binary is 32-bit.");
    println!("Given -6, exit with success if the given binary is 64-bit.");
    println!("Given -d, exit with success if the given binary is dynamically linked.");
    println!("Given -s, exit with success if the given binary is statically linked.");
    println!("Given -p, exit with success if the given binary is PIE.");
    println!("Given -t, print the type of the file.");
    println!("Given -x, exit with success if the given executable is a script.");
    println!("Given -X, exit with success if the given binary has executable stack.");
}

fn confine<Fd: AsRawFd>(fd: &Fd) -> SydResult<()> {
    // Set nfiles, nprocs, and filesize rlimits to zero.
    // Ignore errors as setrlimit(2) may be denied.
    let _ = confine_rlimit_zero(&[
        Resource::RLIMIT_FSIZE,
        Resource::RLIMIT_NOFILE,
        Resource::RLIMIT_NPROC,
    ]);

    // Set up a Landlock sandbox to disallow all access.
    // Ignore errors as Landlock may not be supported.
    let abi = syd::landlock::ABI::new_current();
    let policy = LandlockPolicy {
        scoped_abs: true,
        scoped_sig: true,

        ..Default::default()
    };
    let _ = policy.restrict_self(abi);

    // Ensure W^X via MDWE (if available) and seccomp-bpf.
    //
    // Ignore errors as
    // 1. MDWE may be unsupported -> EINVAL.
    // 2. MDWE may already be applied -> EPERM.
    let _ = confine_mdwe(false);

    // W^X filter allows by default and kills offending memory access.
    //
    // Ignore errors which may mean at least one of:
    // a. CONFIG_SECCOMP_FILTER not enabled in kernel.
    // b. Syd is denying stacked seccomp cbpf filters.
    let _ = confine_scmp_wx();

    // Confine system calls to least privilege using seccomp-bpf.
    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)?;

    // Kill process for bad arch.
    ctx.set_act_badarch(ScmpAction::KillProcess)?;

    // We don't want ECANCELED, we want actual errnos.
    ctx.set_api_sysrawrc(true)?;

    // Use a binary tree sorted by syscall number, if possible.
    let _ = ctx.set_ctl_optimize(2);

    // Allow base set.
    const BASE_SET: &[&str] = &[
        "brk",
        "exit",
        "exit_group",
        //"madvise", advice are confined.
        "mmap",
        "mmap2",
        "mprotect",
        "mremap",
        "munmap",
        "rt_sigprocmask",
        "sigaltstack",
        "sigprocmask",
    ];
    for sysname in BASE_SET.iter().chain(VDSO_SYSCALLS) {
        ctx.add_rule(ScmpAction::Allow, ScmpSyscall::from_name(sysname)?)?;
    }

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

    // Allow read, seek, close of file.
    let fd = fd.as_raw_fd() as u64;
    for sysname in ["close", "read", "readv", "_llseek", "lseek"] {
        ctx.add_rule_conditional(
            ScmpAction::Allow,
            ScmpSyscall::from_name(sysname)?,
            &[scmp_cmp!($arg0 == fd)],
        )?;
    }

    // Allow {g,s}etting file descriptor flags.
    const F_GETFD: u64 = nix::libc::F_GETFD as u64;
    const F_SETFD: u64 = nix::libc::F_SETFD as u64;
    for sysname in ["fcntl", "fcntl64"] {
        if let Ok(syscall) = ScmpSyscall::from_name(sysname) {
            for op in [F_GETFD, F_SETFD] {
                ctx.add_rule_conditional(
                    ScmpAction::Allow,
                    syscall,
                    &[scmp_cmp!($arg0 == fd), scmp_cmp!($arg1 == op)],
                )?;
            }
        }
    }

    // Allow safe prctl(2) operations.
    let sysname = "prctl";
    if let Ok(syscall) = ScmpSyscall::from_name(sysname) {
        let op = libc::PR_SET_VMA as u64;
        ctx.add_rule_conditional(ScmpAction::Allow, syscall, &[scmp_cmp!($arg0 == op)])?;
    }

    // Allow writes to standard output and error.
    const FD_1: u64 = nix::libc::STDOUT_FILENO as u64;
    const FD_2: u64 = nix::libc::STDERR_FILENO as u64;
    if let Ok(syscall) = ScmpSyscall::from_name("write") {
        for fd in [FD_1, FD_2] {
            ctx.add_rule_conditional(ScmpAction::Allow, syscall, &[scmp_cmp!($arg0 == fd)])?;
        }
    }

    // All set, load the seccomp filter.
    //
    // SAFETY: Ignore EINVAL which means at least one of:
    // a. CONFIG_SECCOMP_FILTER not enabled in kernel.
    // b. Syd is denying stacked seccomp cbpf filters.
    if let Err(error) = ctx.load() {
        if error
            .sysrawrc()
            .map(|errno| errno.abs())
            .unwrap_or(libc::ECANCELED)
            != libc::EINVAL
        {
            return Err(error.into());
        }
    }

    Ok(())
}
