onelf-rt 0.2.7

Runtime stub for onelf packed binaries
//! ELF interpreter detection and bundled interpreter invocation.
//!
//! When a packed binary's ELF interpreter (PT_INTERP) doesn't exist on the
//! host system (e.g. running a glibc binary on musl), the runtime can fall
//! back to a bundled interpreter from the package's lib directories.
//!
//! Two execution modes:
//! 1. userland-execve: Maps interpreter directly, bypasses kernel loader (preferred)
//! 2. Command-based: Invokes interpreter via --argv0 (fallback for non-ELF entrypoints)

use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::Command;

/// Read the PT_INTERP (ELF interpreter path) from a binary file.
/// Only reads the first 8KB — enough for ELF headers and the interp string.
pub fn read_elf_interp(path: &Path) -> Option<String> {
    let mut file = std::fs::File::open(path).ok()?;
    let mut buf = vec![0u8; 8192];
    let n = file.read(&mut buf).ok()?;
    buf.truncate(n);
    parse_elf_interp(&buf)
}

fn parse_elf_interp(data: &[u8]) -> Option<String> {
    if data.len() < 64 || data[0..4] != *b"\x7fELF" {
        return None;
    }

    let class = data[4];

    let (e_phoff, e_phentsize, e_phnum) = match class {
        2 => {
            let e_phoff = u64::from_le_bytes(data[32..40].try_into().ok()?) as usize;
            let e_phentsize = u16::from_le_bytes(data[54..56].try_into().ok()?) as usize;
            let e_phnum = u16::from_le_bytes(data[56..58].try_into().ok()?) as usize;
            (e_phoff, e_phentsize, e_phnum)
        }
        1 => {
            let e_phoff = u32::from_le_bytes(data[28..32].try_into().ok()?) as usize;
            let e_phentsize = u16::from_le_bytes(data[42..44].try_into().ok()?) as usize;
            let e_phnum = u16::from_le_bytes(data[44..46].try_into().ok()?) as usize;
            (e_phoff, e_phentsize, e_phnum)
        }
        _ => return None,
    };

    for i in 0..e_phnum {
        let off = e_phoff + i * e_phentsize;
        if off + e_phentsize > data.len() {
            break;
        }

        let p_type = u32::from_le_bytes(data[off..off + 4].try_into().ok()?);
        if p_type != 3 {
            continue;
        }

        let (p_offset, p_filesz) = match class {
            2 => {
                let p_offset =
                    u64::from_le_bytes(data[off + 8..off + 16].try_into().ok()?) as usize;
                let p_filesz =
                    u64::from_le_bytes(data[off + 32..off + 40].try_into().ok()?) as usize;
                (p_offset, p_filesz)
            }
            1 => {
                let p_offset = u32::from_le_bytes(data[off + 4..off + 8].try_into().ok()?) as usize;
                let p_filesz =
                    u32::from_le_bytes(data[off + 16..off + 20].try_into().ok()?) as usize;
                (p_offset, p_filesz)
            }
            _ => return None,
        };

        if p_offset + p_filesz > data.len() {
            return None;
        }

        let interp = &data[p_offset..p_offset + p_filesz];
        let interp = match interp.iter().position(|&b| b == 0) {
            Some(pos) => &interp[..pos],
            None => interp,
        };
        return std::str::from_utf8(interp).ok().map(String::from);
    }

    None
}

/// Check if we should use userland-execve for this target.
///
/// Returns the bundled interpreter path if:
/// - Target is a PIE ELF binary (ET_DYN)
/// - Bundled interpreter exists
/// - userland-execve is supported on this platform
///
/// Non-PIE ELFs (ET_EXEC) go through the command-based fallback instead
/// because userland-execve can't relocate them.
pub fn should_use_userland_exec(
    target: &Path,
    pkg_root: &Path,
    bundled_interp_rel: Option<&str>,
) -> Option<PathBuf> {
    if !crate::ulexec::is_supported() {
        return None;
    }

    let rel_path = bundled_interp_rel?;
    let interp = pkg_root.join(rel_path);

    if !interp.exists() {
        return None;
    }

    if read_elf_interp(target).is_none() {
        return None;
    }

    if !is_pie(target) {
        return None;
    }

    // Self-extracting binaries (e.g. pre-1.3.12 Bun) need /proc/self/exe
    // to resolve to the binary itself. userland-execve doesn't update
    // /proc/self/exe, so we route these through the kernel-exec path
    // (see build_exec_command's self-extract handling).
    if crate::selfextract::has_self_extract_trailer(target) {
        return None;
    }

    Some(interp)
}

/// Read the ELF e_type field and return true for ET_DYN (PIE / shared object).
fn is_pie(path: &Path) -> bool {
    use std::io::Read;
    let Ok(mut f) = std::fs::File::open(path) else {
        return false;
    };
    let mut buf = [0u8; 20];
    if f.read(&mut buf).unwrap_or(0) < 20 {
        return false;
    }
    if buf[0..4] != *b"\x7fELF" {
        return false;
    }
    // e_type is at offset 16 as u16 little-endian. ET_DYN = 3.
    let e_type = u16::from_le_bytes([buf[16], buf[17]]);
    e_type == 3
}

/// Execute an ELF binary using userland-execve with bundled interpreter.
///
/// `lib_path` is passed to the linker via `--library-path` instead of via
/// the inherited `LD_LIBRARY_PATH` env var, so bundled libs aren't visible
/// to child processes the app spawns.
///
/// This function never returns on success.
pub fn exec_userland(
    target: &Path,
    interpreter: &Path,
    lib_path: &str,
    argv0: &str,
    args: &[String],
) -> ! {
    crate::ulexec::exec_with_interp(target, interpreter, lib_path, argv0, args)
}

/// Search for the interpreter in the package's lib directories.
///
/// Match by the PT_INTERP's own basename (e.g. `ld-linux-x86-64.so.2`),
/// not by whatever the host's symlink points at. On NixOS the host's
/// `/lib64/ld-linux-x86-64.so.2` is a symlink to a `stub-ld-*` store
/// path, and resolving the symlink would make us look for a file named
/// `stub-ld-...` in the bundle, which of course doesn't exist; we'd
/// then fall back to the kernel-loaded stub and fail.
fn find_bundled_interp(interp: &str, pkg_root: &Path, lib_dirs: &[&str]) -> Option<PathBuf> {
    let interp_name = Path::new(interp).file_name()?.to_os_string();

    for dir in lib_dirs {
        let candidate = pkg_root.join(dir).join(&interp_name);
        if candidate.exists() {
            return Some(candidate);
        }
    }

    let candidate = pkg_root.join(&interp_name);
    if candidate.exists() {
        return Some(candidate);
    }

    None
}

/// Build a `Command` for executing the target binary.
///
/// With the AT_EXECFN bootstrap, bundled ELFs resolve their own
/// interpreter relative to the binary's location. No CWD control needed.
///
/// Fallback: if PT_INTERP is absolute and unpatched (packed without
/// bundling), invoke the bundled loader explicitly with `--argv0`.
pub fn build_exec_command(
    target: &Path,
    pkg_root: &Path,
    lib_dirs: &[&str],
    lib_path: &str,
    private_ns: bool,
    argv0: &str,
    args: &[String],
) -> Command {
    use std::os::unix::process::CommandExt;

    if let Some(interp) = read_elf_interp(target) {
        let interp_path = Path::new(&interp);
        if let Some(bundled) = find_bundled_interp(&interp, pkg_root, lib_dirs) {
            // Self-extracting binaries (pre-1.3.12 Bun, etc.) read
            // /proc/self/exe to find their embedded payload. The
            // explicit linker invocation below sets /proc/self/exe to
            // the linker, which breaks payload detection. We need a
            // direct kernel-exec of the binary so /proc/self/exe
            // resolves to it.
            //
            // To make the kernel resolve PT_INTERP (typically
            // /lib64/ld-linux-x86-64.so.2) to our bundled linker:
            //   - In a private mount namespace (FUSE/tmpfs): bind-mount
            //     the bundled linker over PT_INTERP. Invisible outside.
            //   - Otherwise (cache mode): create a /tmp symlink and
            //     in-place patch PT_INTERP to point at it.
            if crate::selfextract::has_self_extract_trailer(target) {
                let prepped = if private_ns {
                    crate::selfextract::bind_mount_interp(target, &bundled).map(|_| ())
                } else {
                    crate::selfextract::symlink_interp(target, &bundled).map(|_| ())
                };
                match prepped {
                    Ok(()) => {
                        let mut cmd = Command::new(target);
                        cmd.arg0(argv0).args(args);
                        if !lib_path.is_empty() {
                            cmd.env("LD_LIBRARY_PATH", lib_path);
                        }
                        return cmd;
                    }
                    Err(e) => {
                        eprintln!(
                            "onelf-rt: warning: self-extract prep failed for {}: {e}; \
                             falling back to explicit linker invocation",
                            target.display()
                        );
                    }
                }
            }

            let is_musl = interp_path
                .file_name()
                .and_then(|n| n.to_str())
                .is_some_and(|n| n.starts_with("ld-musl-"));

            let mut cmd = Command::new(&bundled);
            if !is_musl {
                cmd.arg("--inhibit-cache");
            }
            if !lib_path.is_empty() {
                cmd.arg("--library-path").arg(lib_path);
            }
            cmd.arg("--argv0").arg(argv0).arg(target).args(args);
            return cmd;
        }
        if !interp_path.exists() {
            eprintln!(
                "onelf-rt: warning: ELF interpreter '{}' not found on this system \
                 and no bundled equivalent in the AppDir",
                interp
            );
        }
    }

    let mut cmd = Command::new(target);
    cmd.arg0(argv0).args(args);
    cmd
}

/// Parse the bundled interpreter relative path from `.onelf/interp` metadata.
pub fn parse_bundled_interp_rel(interp_data: &[u8]) -> Option<&str> {
    std::str::from_utf8(interp_data).ok()?.lines().next()
}