Skip to main content

running_process_platform_internal/platform/
ape.rs

1//! Actually Portable Executable (APE / cosmocc) launch support.
2//!
3//! An APE image is simultaneously a Windows PE, a Bourne-shell script, and
4//! (through its embedded loader) an ELF/Mach-O program. Windows runs it
5//! natively. Unix kernels do not: `execve` returns `ENOEXEC` unless the host
6//! registered an APE `binfmt_misc` handler, and `posix_spawn` -- what Rust's
7//! `Command` uses -- never retries through `/bin/sh` the way an interactive
8//! shell does. NixOS ships no APE handler, so a bare spawn fails with
9//! "Exec format error".
10//!
11//! An APE image is launched through a loader instead: `<loader> <image>
12//! <args...>`. Both loader kinds accept that argv shape:
13//!
14//! * the Cosmopolitan `ape` loader, which maps the image directly; or
15//! * a POSIX `sh`, which runs the image's shell prologue; the prologue
16//!   extracts its embedded loader to `$TMPDIR/.ape-*` and `exec`s it.
17//!
18//! On Linux this crate first provides the loader itself: every cosmocc image
19//! embeds a gzip'd static ELF loader per CPU, located by `dd skip=N count=M`
20//! lines in its prologue. The one for the host CPU is inflated, validated,
21//! and installed content-addressed into an owner-only, exec-capable cache
22//! directory, falling back to a sealed `memfd` when no such directory
23//! exists. That path needs no `sh`, coreutils, `gzip`, `PATH`, or writable
24//! `$TMPDIR` in the child environment.
25//!
26//! Loader precedence ([`plan_launch`]): an explicit loader
27//! ([`LOADER_ENV`] or [`ApeOptions::loader`]) -> the loader embedded in the
28//! image (Linux) -> `ape` on `PATH` -> well-known `ape` install locations ->
29//! `/bin/sh` -> `sh` on `PATH`.
30//!
31//! Two ways in:
32//!
33//! * **Before a spawn**, wherever the child's program and environment are
34//!   known: [`SpawnSpec`](crate::SpawnSpec) plans every launch, and
35//!   [`command`] / [`tokio_command`] build a command that already runs
36//!   through the planned loader; [`plan_launch`] exposes the plan to callers
37//!   that build their own. Planning first matters: once a `pre_exec` hook
38//!   routes std through `execvp`, glibc hands a refused image to `/bin/sh`
39//!   silently, and the prologue then needs `PATH`, `dd` and `gzip`.
40//! * **After a refusal**, for a command the caller built: it keeps every
41//!   caller setting and is retried once through `execvp`, whose POSIX
42//!   `ENOEXEC` rule runs the image's prologue ([`spawn_std`]).
43//!
44//! Writing a loader and executing it races with other threads' forks: a
45//! child forked while the writable descriptor is open holds it until its own
46//! `exec`, and executing the loader fails with `ETXTBSY` meanwhile. The
47//! process-wide [`fork_guard`] / [`exclusive_fork_guard`] pair (Go's
48//! `syscall.ForkLock`) closes that window for every spawn that goes through
49//! this crate, and [`retry_while_busy`] covers spawns that do not.
50
51use std::collections::HashMap;
52use std::ffi::{OsStr, OsString};
53use std::fs::File;
54use std::io::{self, Read};
55use std::path::{Path, PathBuf};
56use std::sync::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
57use std::time::SystemTime;
58
59pub use crate::{
60    ape_is_exec_format_error as is_exec_format_error,
61    APE_EXECVP_SHELL_FALLBACK as EXECVP_SHELL_FALLBACK, APE_LOADER_HOST as LOADER_HOST,
62    APE_NEEDS_LOADER as NEEDS_LOADER, APE_SHELL as SHELL, APE_SYSTEM_LOADERS as SYSTEM_LOADERS,
63};
64
65/// Environment variable naming an explicit APE loader (an `ape` binary or a
66/// POSIX shell). Read from the child environment first, then this process's.
67pub const LOADER_ENV: &str = "RUNNING_PROCESS_APE_LOADER";
68
69/// Environment variable naming the preferred directory for loaders extracted
70/// from APE images. Tried before the host defaults; like them it is used only
71/// when owned by the current user, not group/world-writable, and on an
72/// exec-capable mount.
73pub const CACHE_DIR_ENV: &str = "RUNNING_PROCESS_APE_CACHE_DIR";
74
75/// Leading bytes of every APE image: `MZqFpD='` (the standard header, also a
76/// valid DOS/PE `MZ` stub), `jartsr='` (non-Windows), `APEDBG='` (debug).
77pub const MAGICS: [&[u8]; 3] = [b"MZqFpD='", b"jartsr='", b"APEDBG='"];
78
79/// Bytes of an image scanned for the shell prologue. Real prologues end
80/// within the first ~8 KiB; the cap bounds work on hostile input.
81#[cfg(feature = "ape-loader")]
82const PROLOGUE_SCAN_BYTES: u64 = 64 * 1024;
83
84/// Upper bound on a compressed or inflated embedded loader (real ones are
85/// ~4-5 KiB compressed, ~10 KiB inflated).
86#[cfg(feature = "ape-loader")]
87const MAX_LOADER_BYTES: u64 = 4 * 1024 * 1024;
88
89/// Whether `header` begins with an APE magic.
90pub fn is_ape_header(header: &[u8]) -> bool {
91    MAGICS.iter().any(|magic| header.starts_with(magic))
92}
93
94/// Whether the file at `path` is an APE image. Unreadable files are not.
95pub fn is_ape_file(path: &Path) -> bool {
96    let mut header = [0u8; 8];
97    File::open(path)
98        .and_then(|mut file| file.read_exact(&mut header))
99        .is_ok_and(|()| is_ape_header(&header))
100}
101
102// ---------------------------------------------------------------------------
103// Fork lock
104// ---------------------------------------------------------------------------
105
106/// Process-wide fork lock, as Go's `syscall.ForkLock`.
107///
108/// Spawns hold it shared across fork->exec (`spawn` returns only after the
109/// child has exec'd); writers of to-be-executed files hold it exclusively
110/// while their descriptor is open, so no child can inherit it.
111static FORK_LOCK: RwLock<()> = RwLock::new(());
112
113/// Hold across a spawn so no executable is being written meanwhile.
114///
115/// Never hold it while planning a launch: planning may take the exclusive
116/// guard to install a loader.
117pub fn fork_guard() -> RwLockReadGuard<'static, ()> {
118    FORK_LOCK.read().unwrap_or_else(|error| error.into_inner())
119}
120
121/// Hold while a file that will be executed is open for writing.
122pub fn exclusive_fork_guard() -> RwLockWriteGuard<'static, ()> {
123    FORK_LOCK.write().unwrap_or_else(|error| error.into_inner())
124}
125
126/// Run `spawn` again while the host reports the program busy (`ETXTBSY`).
127///
128/// For spawns outside this crate's fork lock: a file another thread has just
129/// written can still be open for writing in a child forked before the write
130/// finished, until that child execs. A short backoff closes the window.
131pub fn retry_while_busy<T>(mut spawn: impl FnMut() -> io::Result<T>) -> io::Result<T> {
132    for delay_ms in [1, 4, 16, 64, 256] {
133        match spawn() {
134            Err(error) if error.kind() == io::ErrorKind::ExecutableFileBusy => {
135                std::thread::sleep(std::time::Duration::from_millis(delay_ms));
136            }
137            result => return result,
138        }
139    }
140    spawn()
141}
142
143// ---------------------------------------------------------------------------
144// Planning
145// ---------------------------------------------------------------------------
146
147/// What a launch plan consults, as the child will see it.
148#[derive(Clone, Debug, Default, Eq, PartialEq)]
149pub struct ApeOptions {
150    /// The child's `PATH`: locates a bare program name, `ape` and `sh`.
151    pub path: Option<OsString>,
152    /// An explicit loader, path or bare name; wins over every other choice.
153    pub loader: Option<OsString>,
154    /// Directories to install an extracted loader in, in order. The first
155    /// usable one wins; with none usable a sealed `memfd` is used (Linux).
156    pub cache_dirs: Vec<PathBuf>,
157}
158
159impl ApeOptions {
160    /// The values a child inherits from this process: `PATH`,
161    /// [`LOADER_ENV`], then [`CACHE_DIR_ENV`] ahead of the host default
162    /// cache directories.
163    pub fn inherited() -> Self {
164        Self::with_overrides(false, [])
165    }
166
167    /// Apply a command's environment edits over the inherited values; `None`
168    /// removes a variable and `clear` starts from an empty environment, as
169    /// `env_clear` does. The host default cache directories always follow,
170    /// since they do not depend on the child's environment.
171    pub fn with_overrides<'a>(
172        clear: bool,
173        overrides: impl IntoIterator<Item = (&'a OsStr, Option<&'a OsStr>)>,
174    ) -> Self {
175        let inherited = |name: &str| {
176            if clear {
177                return None;
178            }
179            match name {
180                "PATH" => crate::env_vars::PATH.os(),
181                LOADER_ENV => crate::env_vars::APE_LOADER.os(),
182                _ => crate::env_vars::APE_CACHE_DIR.os(),
183            }
184        };
185        let mut path = inherited("PATH");
186        let mut loader = inherited(LOADER_ENV);
187        let mut cache_dir = inherited(CACHE_DIR_ENV);
188        for (key, value) in overrides {
189            let slot = match key.to_str() {
190                Some("PATH") => &mut path,
191                Some(LOADER_ENV) => &mut loader,
192                Some(CACHE_DIR_ENV) => &mut cache_dir,
193                _ => continue,
194            };
195            *slot = value.map(OsStr::to_os_string);
196        }
197        let set = |value: Option<OsString>| value.filter(|value| !value.is_empty());
198        let mut cache_dirs: Vec<PathBuf> = set(cache_dir).map(PathBuf::from).into_iter().collect();
199        cache_dirs.extend(crate::ape_default_loader_dirs());
200        Self {
201            path: set(path),
202            loader: set(loader),
203            cache_dirs,
204        }
205    }
206}
207
208/// Which loader a planned launch runs the image through.
209#[derive(Clone, Copy, Debug, Eq, PartialEq)]
210pub enum LoaderKind {
211    /// The loader named by [`LOADER_ENV`] or [`ApeOptions::loader`].
212    Explicit,
213    /// The loader embedded in the image, extracted by this crate.
214    Embedded,
215    /// An `ape` loader installed on the host.
216    System,
217    /// A POSIX shell, which runs the image's own prologue.
218    Shell,
219}
220
221/// How to run one APE image: `loader image args...`.
222#[derive(Clone, Debug, Eq, PartialEq)]
223pub struct ApeLaunch {
224    pub kind: LoaderKind,
225    /// Program to execute in place of the image.
226    pub loader: PathBuf,
227    /// Absolute path of the image, passed as the loader's first argument.
228    pub image: PathBuf,
229    /// A private directory holding this loader as `ape`. Spawners put it
230    /// first on the child's `PATH` ([`Self::child_path`]) so APE programs the
231    /// child spawns in turn (gcc -> `cc1`) find a loader through their
232    /// prologue's `type ape`; without it those nested spawns need `sh`,
233    /// coreutils and a writable temporary directory.
234    pub ape_path_dir: Option<PathBuf>,
235}
236
237impl ApeLaunch {
238    /// The child's `PATH` with [`Self::ape_path_dir`] first, given the `PATH`
239    /// it would otherwise get. `None` when there is nothing to add.
240    pub fn child_path(&self, inherited: Option<&OsStr>) -> Option<OsString> {
241        let dir = self.ape_path_dir.as_ref()?;
242        let rest = inherited.filter(|path| !path.is_empty());
243        std::env::join_paths(
244            std::iter::once(dir.clone()).chain(rest.into_iter().flat_map(std::env::split_paths)),
245        )
246        .ok()
247    }
248
249    /// Loader arguments: the image, then the caller's arguments unchanged.
250    pub fn args<I, S>(&self, args: I) -> Vec<OsString>
251    where
252        I: IntoIterator<Item = S>,
253        S: AsRef<OsStr>,
254    {
255        std::iter::once(self.image.clone().into_os_string())
256            .chain(args.into_iter().map(|arg| arg.as_ref().to_os_string()))
257            .collect()
258    }
259}
260
261/// Plan how to run `program` if it resolves to an APE image.
262///
263/// `current_dir` is the child's working directory, used to resolve a
264/// relative program path. `None` means `program` is not an APE image, the
265/// host runs APE images natively, or no loader is available; in each case
266/// the program should be spawned as it is.
267pub fn plan_launch(
268    program: &OsStr,
269    current_dir: Option<&Path>,
270    options: &ApeOptions,
271) -> Option<ApeLaunch> {
272    if !NEEDS_LOADER {
273        return None;
274    }
275    let image = resolve_program(program, current_dir, options.path.as_deref())?;
276    if !is_ape_file(&image) {
277        return None;
278    }
279    let launch = |kind, loader: PathBuf| ApeLaunch {
280        kind,
281        ape_path_dir: ape_path_dir(&loader, &options.cache_dirs),
282        loader,
283        image: image.clone(),
284    };
285    if let Some(explicit) = options.loader.as_deref() {
286        return resolve_explicit_loader(explicit, options.path.as_deref())
287            .map(|loader| launch(LoaderKind::Explicit, loader));
288    }
289    if let Some(loader) = embedded_loader(&image, &options.cache_dirs) {
290        return Some(launch(LoaderKind::Embedded, loader));
291    }
292    if let Some(loader) = system_loader(options.path.as_deref()) {
293        return Some(launch(LoaderKind::System, loader));
294    }
295    shell(options.path.as_deref()).map(|loader| launch(LoaderKind::Shell, loader))
296}
297
298/// Resolve `program` to the file the child's `execvp` would execute: a path
299/// with a separator is taken as-is (relative to `current_dir` when given), a
300/// bare name is searched on `path`. The result is absolute.
301pub fn resolve_program(
302    program: &OsStr,
303    current_dir: Option<&Path>,
304    path: Option<&OsStr>,
305) -> Option<PathBuf> {
306    let program_path = Path::new(program);
307    if program_path.components().count() > 1 || program_path.is_absolute() {
308        let resolved = match current_dir {
309            Some(dir) if program_path.is_relative() => dir.join(program_path),
310            _ => program_path.to_path_buf(),
311        };
312        return std::path::absolute(resolved).ok();
313    }
314    find_executable_on_path(program, path).and_then(|found| std::path::absolute(found).ok())
315}
316
317fn resolve_explicit_loader(explicit: &OsStr, path: Option<&OsStr>) -> Option<PathBuf> {
318    let explicit_path = Path::new(explicit);
319    if explicit_path.components().count() > 1 || explicit_path.is_absolute() {
320        return Some(explicit_path.to_path_buf());
321    }
322    find_executable_on_path(explicit, path)
323}
324
325/// An installed `ape`: the child's `PATH` first, then the host's
326/// conventional install locations.
327fn system_loader(path: Option<&OsStr>) -> Option<PathBuf> {
328    find_executable_on_path(OsStr::new("ape"), path)
329        .or_else(|| first_executable(SYSTEM_LOADERS.iter().map(PathBuf::from)))
330}
331
332fn shell(path: Option<&OsStr>) -> Option<PathBuf> {
333    first_executable([PathBuf::from(SHELL)])
334        .or_else(|| find_executable_on_path(OsStr::new("sh"), path))
335}
336
337fn find_executable_on_path(name: &OsStr, path: Option<&OsStr>) -> Option<PathBuf> {
338    let dirs = std::env::split_paths(path?).filter(|dir| !dir.as_os_str().is_empty());
339    first_executable(dirs.map(|dir| dir.join(name)))
340}
341
342fn first_executable(candidates: impl IntoIterator<Item = PathBuf>) -> Option<PathBuf> {
343    candidates.into_iter().find(|candidate| {
344        std::fs::metadata(candidate)
345            .is_ok_and(|meta| meta.is_file() && crate::ape_is_executable(&meta))
346    })
347}
348
349// ---------------------------------------------------------------------------
350// Embedded loader
351// ---------------------------------------------------------------------------
352
353/// Which loader embedded in an APE image a host can run.
354#[derive(Clone, Copy, Debug, Eq, PartialEq)]
355pub enum LoaderHost {
356    /// No embedded loader is used (Windows runs APE natively).
357    None,
358    /// The static ELF loader from the prologue's Linux branch.
359    Linux,
360    /// The x86_64 Mach-O loader, or the Apple Silicon loader compiled from
361    /// the image's `ape-m1.c` with the host `cc`.
362    Macos,
363}
364
365/// Byte range `(skip, count)` within an image.
366pub type Range = (u64, u64);
367
368/// Where one image's prologue keeps its loaders.
369///
370/// cosmocc emits the same line shapes for every image; only the numbers
371/// change. Recognized lines, keyed by the enclosing `if [ "$m" = <cpu> ]`:
372///
373/// * `dd if="$o" skip=N count=M ... | gzip -dc >"$t.$$"` -- a gzip'd loader.
374///   When the next line patches it (`dd if="$t.$$" of="$t.$$" skip=5 count=8
375///   bs=64`) it is the macOS x86_64 loader (the ELF blob turned Mach-O),
376///   otherwise the Linux loader for the CPU.
377/// * `dd if="$o" skip=N count=M ... | gzip -dc >"$t.c.$$"` -- the gzip'd C
378///   source of the Apple Silicon loader (`ape-m1.c`), compiled with `cc`.
379#[derive(Clone, Debug, Default, Eq, PartialEq)]
380pub struct Prologue {
381    pub linux_loader_x86_64: Option<Range>,
382    pub linux_loader_aarch64: Option<Range>,
383    pub macos_loader_x86_64: Option<Range>,
384    pub macos_loader_source_aarch64: Option<Range>,
385}
386
387impl Prologue {
388    /// Parse the prologue text at the start of an APE image.
389    pub fn parse(prologue: &[u8]) -> Self {
390        let text = String::from_utf8_lossy(prologue);
391        let mut parsed = Self::default();
392        let mut cpu = None;
393        let mut lines = text.lines().map(str::trim).peekable();
394        while let Some(line) = lines.next() {
395            if line.contains("\"$m\" = x86_64") {
396                cpu = Some("x86_64");
397            } else if line.contains("\"$m\" = aarch64") {
398                cpu = Some("aarch64");
399            }
400            let Some(cpu) = cpu else { continue };
401            if !line.starts_with("dd if=\"$o\" ") {
402                continue;
403            }
404            if line.contains("| gzip -dc >\"$t.c.$$\"") {
405                if cpu == "aarch64" {
406                    parsed.macos_loader_source_aarch64 = range(line);
407                }
408            } else if line.contains("| gzip -dc >\"$t.$$\"") {
409                let patched = lines
410                    .peek()
411                    .is_some_and(|next| next.starts_with("dd if=\"$t.$$\" of=\"$t.$$\""));
412                match (cpu, patched) {
413                    ("x86_64", true) => parsed.macos_loader_x86_64 = range(line),
414                    ("x86_64", false) => parsed.linux_loader_x86_64 = range(line),
415                    ("aarch64", false) => parsed.linux_loader_aarch64 = range(line),
416                    _ => {}
417                }
418            }
419        }
420        parsed
421    }
422
423    /// The Linux loader for `machine` (`uname -m` spelling).
424    pub fn linux_loader(&self, machine: &str) -> Option<Range> {
425        match machine {
426            "x86_64" => self.linux_loader_x86_64,
427            "aarch64" => self.linux_loader_aarch64,
428            _ => None,
429        }
430    }
431}
432
433fn range(line: &str) -> Option<Range> {
434    let field = |key: &str| {
435        line.split_whitespace()
436            .find_map(|token| token.strip_prefix(key))
437            .and_then(|value| value.parse::<u64>().ok())
438    };
439    Some((field("skip=")?, field("count=")?))
440}
441
442/// `(skip, count)` of the gzip'd Linux loader for `machine`.
443pub fn loader_blob_range(prologue: &[u8], machine: &str) -> Option<Range> {
444    Prologue::parse(prologue).linux_loader(machine)
445}
446
447/// Identity of an image (and where its loader may live) for the memos: a
448/// rebuilt image at the same path changes length or mtime and is
449/// re-extracted.
450type ImageKey = (PathBuf, u64, Option<SystemTime>, Vec<PathBuf>);
451
452struct Memo(Mutex<Option<HashMap<ImageKey, PathBuf>>>);
453
454impl Memo {
455    const fn new() -> Self {
456        Self(Mutex::new(None))
457    }
458
459    fn get(&self, key: &ImageKey) -> Option<PathBuf> {
460        let guard = self.0.lock().unwrap_or_else(|error| error.into_inner());
461        let hit = guard.as_ref()?.get(key)?.clone();
462        // A cache cleaner may have removed it since; re-materialize then.
463        hit.exists().then_some(hit)
464    }
465
466    fn put(&self, key: ImageKey, path: PathBuf) {
467        let mut guard = self.0.lock().unwrap_or_else(|error| error.into_inner());
468        guard.get_or_insert_with(HashMap::new).insert(key, path);
469    }
470}
471
472static LOADER_MEMO: Memo = Memo::new();
473
474fn image_key(image: &Path, cache_dirs: &[PathBuf]) -> Option<ImageKey> {
475    let meta = std::fs::metadata(image).ok()?;
476    Some((
477        image.to_path_buf(),
478        meta.len(),
479        meta.modified().ok(),
480        cache_dirs.to_vec(),
481    ))
482}
483
484/// The host-runnable loader embedded in `image`, installed into the first
485/// usable `cache_dirs` entry (or, on Linux, a sealed memfd). `None` when the
486/// host runs APE natively, the image carries no valid loader for this host,
487/// or nothing could be materialized.
488pub fn embedded_loader(image: &Path, cache_dirs: &[PathBuf]) -> Option<PathBuf> {
489    if LOADER_HOST == LoaderHost::None {
490        return None;
491    }
492    let key = image_key(image, cache_dirs)?;
493    if let Some(hit) = LOADER_MEMO.get(&key) {
494        return Some(hit);
495    }
496    let loader = install_embedded_loader(image, cache_dirs)?;
497    LOADER_MEMO.put(key, loader.clone());
498    Some(loader)
499}
500
501#[cfg(feature = "ape-loader")]
502fn install_embedded_loader(image: &Path, cache_dirs: &[PathBuf]) -> Option<PathBuf> {
503    let machine = std::env::consts::ARCH;
504    match (LOADER_HOST, machine) {
505        (LoaderHost::Linux, _) => {
506            let bytes = extract_loader(image, machine)?;
507            let name = format!("ape-loader-linux-{machine}-{}", short_hash(&bytes));
508            materialize(&bytes, &name, cache_dirs)
509        }
510        (LoaderHost::Macos, "x86_64") => {
511            let bytes = extract_macos_x86_64_loader(image)?;
512            let name = format!("ape-loader-macos-{machine}-{}", short_hash(&bytes));
513            materialize(&bytes, &name, cache_dirs)
514        }
515        (LoaderHost::Macos, "aarch64") => compile_macos_aarch64_loader(image, cache_dirs),
516        _ => None,
517    }
518}
519
520#[cfg(not(feature = "ape-loader"))]
521fn install_embedded_loader(_image: &Path, _cache_dirs: &[PathBuf]) -> Option<PathBuf> {
522    None
523}
524
525/// Install `bytes` as an executable named `name` in the first usable cache
526/// directory, else as a host anonymous executable (a Linux memfd).
527#[cfg(feature = "ape-loader")]
528fn materialize(bytes: &[u8], name: &str, dirs: &[PathBuf]) -> Option<PathBuf> {
529    install(dirs, None, name, bytes).or_else(|| crate::ape_anonymous_executable(bytes, name))
530}
531
532/// Content address for an installed file. Collisions are harmless: an
533/// installed file is reused only when its bytes match exactly.
534fn short_hash(bytes: &[u8]) -> String {
535    let hash = bytes.iter().fold(0xcbf2_9ce4_8422_2325_u64, |hash, &byte| {
536        (hash ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3)
537    });
538    format!("{hash:016x}")
539}
540
541/// Install `bytes` as executable `<dir>/<subdir>/<name>` in the first usable
542/// candidate `dir`, reusing an identical existing file. `None` when no
543/// candidate is usable.
544pub fn install(
545    dirs: &[PathBuf],
546    subdir: Option<&str>,
547    name: &str,
548    bytes: &[u8],
549) -> Option<PathBuf> {
550    dirs.iter().find_map(|dir| match subdir {
551        // The parent must pass the same checks before anything nests in it.
552        Some(sub) => crate::ape_private_exec_dir(dir)
553            .then(|| install_in(&dir.join(sub), bytes, name))
554            .flatten(),
555        None => install_in(dir, bytes, name),
556    })
557}
558
559/// Install `bytes` as executable `<dir>/<name>` when `dir` is private to this
560/// user and exec-capable. Installs are atomic (staging file + `rename`), a
561/// tampered, truncated or non-executable copy is replaced, and the staging
562/// file is written under the exclusive fork guard.
563pub fn install_in(dir: &Path, bytes: &[u8], name: &str) -> Option<PathBuf> {
564    use std::io::Write;
565    use std::sync::atomic::{AtomicU64, Ordering};
566
567    if !crate::ape_private_exec_dir(dir) {
568        return None;
569    }
570    let target = dir.join(name);
571    if is_installed(&target, bytes) {
572        return Some(target);
573    }
574    static SEQ: AtomicU64 = AtomicU64::new(0);
575    let staging = dir.join(format!(
576        ".{name}.{}.{}",
577        std::process::id(),
578        SEQ.fetch_add(1, Ordering::Relaxed)
579    ));
580    // No child may inherit the writable descriptor, or executing the file
581    // would hit ETXTBSY until that child execs.
582    let written = {
583        let _fork = exclusive_fork_guard();
584        std::fs::OpenOptions::new()
585            .write(true)
586            .create_new(true)
587            .open(&staging)
588            .and_then(|mut file| {
589                file.write_all(bytes)?;
590                file.sync_all()
591            })
592    }
593    .and_then(|()| crate::ape_mark_executable(&staging))
594    // rename(2) is atomic: concurrent installers race benignly to identical
595    // content, and readers never see a partial file.
596    .and_then(|()| std::fs::rename(&staging, &target));
597    if written.is_err() {
598        let _ = std::fs::remove_file(&staging);
599        return None;
600    }
601    is_installed(&target, bytes).then_some(target)
602}
603
604/// A regular (not symlinked), executable file whose content is exactly
605/// `bytes`.
606fn is_installed(path: &Path, bytes: &[u8]) -> bool {
607    std::fs::symlink_metadata(path).is_ok_and(|meta| {
608        meta.file_type().is_file()
609            && crate::ape_is_executable(&meta)
610            && meta.len() == bytes.len() as u64
611    }) && std::fs::read(path).is_ok_and(|content| content == bytes)
612}
613
614/// A private directory containing `loader` under the name `ape`, for
615/// [`ApeLaunch::ape_path_dir`]: with it first on the child's `PATH`, APE
616/// programs the child spawns in turn (gcc -> `cc1`) find a loader through
617/// their prologue's `type ape`. A shell is never exposed as `ape`, and a
618/// memfd loader cannot serve a grandchild, so neither yields a directory.
619fn ape_path_dir(loader: &Path, cache_dirs: &[PathBuf]) -> Option<PathBuf> {
620    if loader.file_name().is_some_and(|name| name == "sh") || loader.starts_with("/proc/self") {
621        return None;
622    }
623    if loader.file_name().is_some_and(|name| name == "ape") {
624        return loader.parent().map(Path::to_path_buf);
625    }
626    let bytes = std::fs::read(loader).ok()?;
627    if bytes.len() as u64 > MAX_PATH_LOADER_BYTES {
628        return None;
629    }
630    let sub = format!("bin-{}", short_hash(&bytes));
631    install(cache_dirs, Some(&sub), "ape", &bytes)?
632        .parent()
633        .map(Path::to_path_buf)
634}
635
636/// Largest loader copied into an `ape` `PATH` directory.
637const MAX_PATH_LOADER_BYTES: u64 = 4 * 1024 * 1024;
638
639/// An opened image with its parsed prologue.
640#[cfg(feature = "ape-loader")]
641struct Image {
642    file: File,
643    len: u64,
644    prologue: Prologue,
645}
646
647#[cfg(feature = "ape-loader")]
648impl Image {
649    fn open(image: &Path) -> Option<Self> {
650        let mut file = File::open(image).ok()?;
651        let len = file.metadata().ok()?.len();
652        let mut head = Vec::new();
653        (&mut file)
654            .take(PROLOGUE_SCAN_BYTES)
655            .read_to_end(&mut head)
656            .ok()?;
657        if !is_ape_header(&head) {
658            return None;
659        }
660        Some(Self {
661            file,
662            len,
663            prologue: Prologue::parse(&head),
664        })
665    }
666
667    /// Read `range`, bounded by the file length and [`MAX_LOADER_BYTES`].
668    fn read(&mut self, (skip, count): Range) -> Option<Vec<u8>> {
669        use std::io::{Seek, SeekFrom};
670        let end = skip.checked_add(count)?;
671        if count == 0 || count > MAX_LOADER_BYTES || end > self.len {
672            return None;
673        }
674        self.file.seek(SeekFrom::Start(skip)).ok()?;
675        let mut out = Vec::with_capacity(count as usize);
676        (&mut self.file).take(count).read_to_end(&mut out).ok()?;
677        (out.len() as u64 == count).then_some(out)
678    }
679
680    fn inflate(&mut self, range: Range) -> Option<Vec<u8>> {
681        gunzip(&self.read(range)?, MAX_LOADER_BYTES as usize).ok()
682    }
683}
684
685/// ELF `e_machine` of the static loader cosmocc embeds for `machine`
686/// (`uname -m` spelling).
687#[cfg(feature = "ape-loader")]
688fn elf_machine(machine: &str) -> Option<u16> {
689    match machine {
690        "x86_64" => Some(62),   // EM_X86_64
691        "aarch64" => Some(183), // EM_AARCH64
692        _ => None,
693    }
694}
695
696/// Inflate and validate the Linux ELF loader `image` embeds for `machine`
697/// (`"x86_64"` or `"aarch64"`).
698#[cfg(feature = "ape-loader")]
699pub fn extract_loader(image: &Path, machine: &str) -> Option<Vec<u8>> {
700    let elf_machine = elf_machine(machine)?;
701    let mut image = Image::open(image)?;
702    let range = image.prologue.linux_loader(machine)?;
703    let elf = image.inflate(range)?;
704    is_static_elf_for(&elf, elf_machine).then_some(elf)
705}
706
707/// The macOS x86_64 loader: the same gzip'd blob as Linux, with the Mach-O
708/// header it carries at offset 320 (8 x 64 bytes) moved to offset 0 -- what
709/// the prologue's `dd if="$t.$$" of="$t.$$" skip=5 count=8 bs=64` does.
710#[cfg(feature = "ape-loader")]
711pub fn extract_macos_x86_64_loader(image: &Path) -> Option<Vec<u8>> {
712    let mut image = Image::open(image)?;
713    let range = image.prologue.macos_loader_x86_64?;
714    let mut bin = image.inflate(range)?;
715    if bin.len() < 320 + 512 {
716        return None;
717    }
718    bin.copy_within(320..320 + 512, 0);
719    is_macho_x86_64(&bin).then_some(bin)
720}
721
722/// The Apple Silicon loader source (`ape-m1.c`) embedded in `image`.
723#[cfg(feature = "ape-loader")]
724pub fn extract_macos_aarch64_loader_source(image: &Path) -> Option<Vec<u8>> {
725    let mut image = Image::open(image)?;
726    let range = image.prologue.macos_loader_source_aarch64?;
727    let source = image.inflate(range)?;
728    (std::str::from_utf8(&source).is_ok() && source.windows(4).any(|window| window == b"main"))
729        .then_some(source)
730}
731
732/// Compile the image's `ape-m1.c` with the host C compiler (Xcode Command
733/// Line Tools) into a content-addressed cache entry, as the prologue would,
734/// without depending on `sh`, `dd`, `gzip` or `$TMPDIR`.
735#[cfg(feature = "ape-loader")]
736fn compile_macos_aarch64_loader(image: &Path, dirs: &[PathBuf]) -> Option<PathBuf> {
737    let source = extract_macos_aarch64_loader_source(image)?;
738    let name = format!("ape-loader-macos-aarch64-{}", short_hash(&source));
739    let dir = dirs.iter().find(|dir| crate::ape_private_exec_dir(dir))?;
740    let target = dir.join(&name);
741    if first_executable([target.clone()]).is_some() {
742        return Some(target);
743    }
744    let tag = format!(
745        "{}.{}",
746        std::process::id(),
747        short_hash(target.as_os_str().as_encoded_bytes())
748    );
749    let source_path = dir.join(format!(".{name}.{tag}.c"));
750    let out_path = dir.join(format!(".{name}.{tag}"));
751    std::fs::write(&source_path, &source).ok()?;
752    let cc =
753        first_executable([PathBuf::from("/usr/bin/cc")]).unwrap_or_else(|| PathBuf::from("cc"));
754    let mut command = std::process::Command::new(&cc);
755    command
756        .args(["-w", "-O", "-o"])
757        .arg(&out_path)
758        .arg(&source_path)
759        .stdin(std::process::Stdio::null());
760    let compiled = retry_while_busy(|| {
761        let _fork = fork_guard();
762        command.output()
763    });
764    let _ = std::fs::remove_file(&source_path);
765    let built = compiled.is_ok_and(|output| output.status.success())
766        && std::fs::rename(&out_path, &target).is_ok();
767    if !built {
768        let _ = std::fs::remove_file(&out_path);
769        return None;
770    }
771    Some(target)
772}
773
774/// A 64-bit little-endian ELF executable for `machine`.
775#[cfg(feature = "ape-loader")]
776fn is_static_elf_for(elf: &[u8], machine: u16) -> bool {
777    elf.len() >= 64
778        && elf.len() as u64 <= MAX_LOADER_BYTES
779        && elf.starts_with(b"\x7fELF")
780        && elf[4] == 2 // ELFCLASS64
781        && elf[5] == 1 // ELFDATA2LSB
782        // ET_EXEC (x86_64 loader) or ET_DYN (position-independent aarch64 loader)
783        && matches!(u16::from_le_bytes([elf[16], elf[17]]), 2 | 3)
784        && u16::from_le_bytes([elf[18], elf[19]]) == machine
785}
786
787/// A 64-bit Mach-O for x86_64 (`MH_MAGIC_64`, `CPU_TYPE_X86_64`).
788#[cfg(feature = "ape-loader")]
789fn is_macho_x86_64(bin: &[u8]) -> bool {
790    bin.len() >= 32 && bin.starts_with(&[0xcf, 0xfa, 0xed, 0xfe]) && bin[4..8] == [7, 0, 0, 1]
791}
792
793/// Decode one gzip member (RFC 1952), refusing output over `limit` bytes.
794#[cfg(feature = "ape-loader")]
795pub fn gunzip(member: &[u8], limit: usize) -> io::Result<Vec<u8>> {
796    const FHCRC: u8 = 0x02;
797    const FEXTRA: u8 = 0x04;
798    const FNAME: u8 = 0x08;
799    const FCOMMENT: u8 = 0x10;
800
801    if member.len() < 18 || member[..3] != [0x1f, 0x8b, 8] {
802        return Err(invalid("not a deflate gzip member"));
803    }
804    let flags = member[3];
805    let mut at = 10;
806    if flags & FEXTRA != 0 {
807        let extra = member
808            .get(at..at + 2)
809            .ok_or_else(|| invalid("truncated gzip"))?;
810        at += 2 + usize::from(u16::from_le_bytes([extra[0], extra[1]]));
811    }
812    for flag in [FNAME, FCOMMENT] {
813        if flags & flag != 0 {
814            let terminator = member
815                .get(at..)
816                .and_then(|rest| rest.iter().position(|&byte| byte == 0))
817                .ok_or_else(|| invalid("truncated gzip"))?;
818            at += terminator + 1;
819        }
820    }
821    if flags & FHCRC != 0 {
822        at += 2;
823    }
824    let trailer = member.len() - 8;
825    let deflate = member
826        .get(at..trailer)
827        .ok_or_else(|| invalid("truncated gzip"))?;
828    let output = miniz_oxide::inflate::decompress_to_vec_with_limit(deflate, limit)
829        .map_err(|error| invalid(&format!("corrupt gzip member: {error}")))?;
830    let size = u32::from_le_bytes(member[trailer + 4..].try_into().expect("four bytes"));
831    if output.len() as u32 != size {
832        return Err(invalid("gzip member size does not match its trailer"));
833    }
834    Ok(output)
835}
836
837#[cfg(feature = "ape-loader")]
838fn invalid(message: &str) -> io::Error {
839    io::Error::new(io::ErrorKind::InvalidData, message.to_owned())
840}
841
842// ---------------------------------------------------------------------------
843// Launching
844// ---------------------------------------------------------------------------
845
846/// A [`std::process::Command`] for `program` that runs an APE image through
847/// its planned loader, with this process's environment as the child's.
848///
849/// Use in place of `Command::new` for an external tool. A relative `program`
850/// path is resolved against this process's working directory; pass an
851/// absolute path when the child's `current_dir` differs.
852pub fn command(program: impl AsRef<OsStr>) -> std::process::Command {
853    let program = program.as_ref();
854    let options = ApeOptions::inherited();
855    match plan_launch(program, None, &options) {
856        Some(launch) => {
857            let mut command = std::process::Command::new(&launch.loader);
858            command.arg(&launch.image);
859            if let Some(path) = launch.child_path(options.path.as_deref()) {
860                command.env("PATH", path);
861            }
862            command
863        }
864        None => std::process::Command::new(program),
865    }
866}
867
868/// Tokio counterpart of [`command`].
869#[cfg(feature = "async-process")]
870pub fn tokio_command(program: impl AsRef<OsStr>) -> tokio::process::Command {
871    let program = program.as_ref();
872    let options = ApeOptions::inherited();
873    match plan_launch(program, None, &options) {
874        Some(launch) => {
875            let mut command = tokio::process::Command::new(&launch.loader);
876            command.arg(&launch.image);
877            if let Some(path) = launch.child_path(options.path.as_deref()) {
878                command.env("PATH", path);
879            }
880            command
881        }
882        None => tokio::process::Command::new(program),
883    }
884}
885
886/// Prepare a caller-built command to be spawned again as an APE image.
887///
888/// Returns `true` when `error` is the kernel refusing an APE image and the
889/// command now routes through `execvp`, whose `ENOEXEC` rule runs the
890/// image's prologue with the host shell; spawn it once more. Every setting
891/// the caller made is kept. For the direct-loader route, build the command
892/// with [`command`] instead.
893pub fn prepare_std_retry(command: &mut std::process::Command, error: &io::Error) -> bool {
894    if !retryable(command, error) {
895        return false;
896    }
897    crate::ape_route_through_execvp(command);
898    true
899}
900
901/// [`prepare_std_retry`] for a Tokio command.
902#[cfg(feature = "async-process")]
903pub fn prepare_tokio_retry(command: &mut tokio::process::Command, error: &io::Error) -> bool {
904    if !retryable(command.as_std(), error) {
905        return false;
906    }
907    crate::ape_route_tokio_through_execvp(command);
908    true
909}
910
911fn retryable(command: &std::process::Command, error: &io::Error) -> bool {
912    if !EXECVP_SHELL_FALLBACK || !is_exec_format_error(error) {
913        return false;
914    }
915    let options = ApeOptions::with_overrides(false, command.get_envs());
916    resolve_program(
917        command.get_program(),
918        command.get_current_dir(),
919        options.path.as_deref(),
920    )
921    .is_some_and(|image| is_ape_file(&image))
922}
923
924/// Spawn a caller-built command under the fork lock, retrying while the
925/// program is transiently busy (`ETXTBSY`) and once if the host refused it as
926/// an APE image.
927pub fn spawn_std<T>(
928    command: &mut std::process::Command,
929    mut spawn: impl FnMut(&mut std::process::Command) -> io::Result<T>,
930) -> io::Result<T> {
931    let first = retry_while_busy(|| {
932        let _fork = fork_guard();
933        spawn(command)
934    });
935    match first {
936        Err(error) if prepare_std_retry(command, &error) => {
937            let _fork = fork_guard();
938            spawn(command)
939        }
940        result => result,
941    }
942}
943
944/// Spawn a Tokio command under the fork lock, retrying once if the host
945/// refused it as an APE image.
946#[cfg(feature = "async-process")]
947pub fn spawn_tokio<T>(
948    command: &mut tokio::process::Command,
949    mut spawn: impl FnMut(&mut tokio::process::Command) -> io::Result<T>,
950) -> io::Result<T> {
951    let first = retry_while_busy(|| {
952        let _fork = fork_guard();
953        spawn(command)
954    });
955    match first {
956        Err(error) if prepare_tokio_retry(command, &error) => {
957            let _fork = fork_guard();
958            spawn(command)
959        }
960        result => result,
961    }
962}
963
964#[cfg(test)]
965#[path = "ape_tests.rs"]
966mod tests;