supermachine 0.3.4

Run any OCI/Docker image as a hardware-isolated microVM on macOS HVF (Linux KVM and Windows WHP in progress). Single library API, zero flags for the common case, sub-100 ms cold-restore from snapshot.
//! First-run codesign autopilot.
//!
//! macOS HVF requires the `com.apple.security.hypervisor`
//! entitlement on whatever process calls `hv_vm_create`. In our
//! architecture that's the `supermachine-worker` binary, not the
//! `supermachine` CLI itself.
//!
//! `cargo install supermachine` builds an unsigned worker on the
//! user's machine. To make `cargo install … && supermachine run X`
//! Just Work with no manual setup, the CLI signs the worker with
//! the bundled entitlements plist on its first invocation —
//! transparently, in ~30–50 ms — and writes a sentinel so
//! subsequent invocations skip the signing in ~1 ms.
//!
//! The same path covers the dev-tree case where `cargo build`
//! strips the entitlement on every rebuild: the next CLI launch
//! re-signs automatically.

use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::OnceLock;

/// Sign `worker_path` with the bundled HVF entitlement (ad-hoc),
/// idempotent. Caches the result both in-process (one signing
/// attempt per CLI invocation) and on disk (one across CLI
/// invocations until the worker binary changes).
///
/// Returns `Ok(())` on success or no-op skip; `Err` on codesign
/// failure with a message suitable for the user. Callers should
/// not panic on `Err` — we'd rather let `hv_vm_create` surface
/// its own error than block on a codesign issue.
pub fn ensure_worker_signed(worker_path: &Path) -> Result<(), String> {
    static IN_PROCESS_DONE: OnceLock<()> = OnceLock::new();
    if IN_PROCESS_DONE.get().is_some() {
        return Ok(());
    }

    // The sentinel records the (size, mtime, path) the worker had
    // **after the most recent successful sign**. We compare today's
    // stat against that and skip re-signing if it matches. Critical
    // detail: codesign rewrites the binary, which bumps mtime on
    // every successful sign — so the cached sentinel must record
    // the *post-sign* mtime, not the pre-sign one. Otherwise every
    // future call sees current_mtime ≠ sentinel_mtime, re-signs,
    // bumps mtime again, and the bake-key (which includes the
    // worker's mtime) never matches a previously baked snapshot —
    // every fresh process rebakes from scratch.
    let stat_marker = |path: &Path| -> Option<String> {
        let meta = std::fs::metadata(path).ok()?;
        let mtime = meta
            .modified()
            .ok()?
            .duration_since(std::time::UNIX_EPOCH)
            .ok()?
            .as_secs();
        Some(format!(
            "size={}\nmtime={}\npath={}\n",
            meta.len(),
            mtime,
            path.display()
        ))
    };

    let current_marker = stat_marker(worker_path).ok_or_else(|| {
        format!("stat {}: file disappeared", worker_path.display())
    })?;

    if let Some(sentinel) = sentinel_path() {
        if let Ok(existing) = std::fs::read_to_string(&sentinel) {
            if existing == current_marker {
                IN_PROCESS_DONE.set(()).ok();
                return Ok(());
            }
        }
    }

    // Drop the entitlements plist (compile-time include_str) to a
    // temp file; codesign needs it on disk. Unique-per-pid so two
    // concurrent CLI invocations don't race on the same temp path.
    let plist = std::env::temp_dir().join(format!(
        "supermachine-entitlements-{}.plist",
        std::process::id()
    ));
    std::fs::write(&plist, crate::assets::ENTITLEMENTS_PLIST)
        .map_err(|e| format!("write entitlements plist: {e}"))?;

    let status = Command::new("codesign")
        .args(["-s", "-", "--entitlements"])
        .arg(&plist)
        .arg("--force")
        .arg(worker_path)
        .stdout(Stdio::null())
        .stderr(Stdio::piped())
        .status();
    let _ = std::fs::remove_file(&plist);

    match status {
        Ok(s) if s.success() => {
            // Re-stat AFTER signing so the sentinel records the
            // post-sign mtime — that's what future stat() calls
            // (and the bake-key calculation) will see.
            if let (Some(sentinel), Some(post_marker)) =
                (sentinel_path(), stat_marker(worker_path))
            {
                if let Some(parent) = sentinel.parent() {
                    let _ = std::fs::create_dir_all(parent);
                }
                let _ = std::fs::write(&sentinel, post_marker);
            }
            IN_PROCESS_DONE.set(()).ok();
            Ok(())
        }
        Ok(s) => Err(format!(
            "codesign exited with {:?} for {}",
            s.code(),
            worker_path.display()
        )),
        Err(e) => Err(format!(
            "failed to spawn codesign for {}: {e}\n\
             (codesign ships with macOS by default; if missing, \
             reinstall Xcode Command Line Tools)",
            worker_path.display()
        )),
    }
}

/// Locate `supermachine-worker`. The single source of truth —
/// the bake pipeline delegates here so the two paths can't drift
/// apart again. Resolution order, returning the first hit:
///
///   1. `$SUPERMACHINE_WORKER_BIN` (explicit override; must
///      exist as a file or it's ignored — protects against stale
///      env vars silently shadowing a working install).
///   2. Sibling of the currently running binary
///      (`cargo install supermachine` layout, where every
///      supermachine-* binary lands in the same directory).
///   3. Sibling of the *canonicalized* running binary
///      (handles `~/.local/bin/supermachine` → dev-tree symlinks).
///   4. `$CARGO_HOME/bin/supermachine-worker` (or
///      `~/.cargo/bin/supermachine-worker`) — the canonical
///      `cargo install` location, picked up even when an
///      embedder's *own* binary is `current_exe`.
///   5. Walk `$PATH` for `supermachine-worker` — anything on
///      PATH counts (release tarball, package manager install,
///      symlink farm, …).
///   6. Ancestor walk for `target/release/supermachine-worker`
///      (cargo dev-tree fallback when running tests/examples).
pub fn locate_worker_bin() -> Option<PathBuf> {
    if let Some(p) = std::env::var_os("SUPERMACHINE_WORKER_BIN") {
        let p = PathBuf::from(p);
        if p.is_file() {
            return Some(p);
        }
    }
    if let Ok(exe) = std::env::current_exe() {
        if let Some(p) = sibling_worker(&exe) {
            return Some(p);
        }
        if let Ok(canonical) = std::fs::canonicalize(&exe) {
            if canonical != exe {
                if let Some(p) = sibling_worker(&canonical) {
                    return Some(p);
                }
            }
        }
    }
    if let Some(p) = cargo_bin_worker() {
        return Some(p);
    }
    if let Some(p) = path_walk_worker() {
        return Some(p);
    }
    if let Ok(exe) = std::env::current_exe() {
        for ancestor in exe.ancestors() {
            let p = ancestor.join("target/release/supermachine-worker");
            if p.is_file() {
                return Some(p);
            }
        }
        if let Ok(canonical) = std::fs::canonicalize(&exe) {
            for ancestor in canonical.ancestors() {
                let p = ancestor.join("target/release/supermachine-worker");
                if p.is_file() {
                    return Some(p);
                }
            }
        }
    }
    None
}

fn sibling_worker(exe: &Path) -> Option<PathBuf> {
    let dir = exe.parent()?;
    let p = dir.join("supermachine-worker");
    if p.is_file() {
        Some(p)
    } else {
        None
    }
}

fn cargo_bin_worker() -> Option<PathBuf> {
    let bin_dir = if let Some(cargo) = std::env::var_os("CARGO_HOME") {
        PathBuf::from(cargo).join("bin")
    } else if let Some(home) = std::env::var_os("HOME") {
        PathBuf::from(home).join(".cargo").join("bin")
    } else {
        return None;
    };
    let p = bin_dir.join("supermachine-worker");
    if p.is_file() {
        Some(p)
    } else {
        None
    }
}

fn path_walk_worker() -> Option<PathBuf> {
    let path = std::env::var_os("PATH")?;
    for dir in std::env::split_paths(&path) {
        let p = dir.join("supermachine-worker");
        if p.is_file() {
            return Some(p);
        }
    }
    None
}

/// Check whether the *currently running* binary has the
/// `com.apple.security.hypervisor` entitlement. Returns `Ok(())`
/// if it does, or a [`String`] describing the situation for the
/// caller to surface as an error if it doesn't.
///
/// Used by [`crate::Vm::start`] to fail fast with a clear message
/// instead of letting `hv_vm_create` return the cryptic
/// `Hv(-85377017)` (HV_DENIED) when the embedder forgot to sign
/// their binary. `Image::acquire` doesn't need this check — the
/// auto-signed worker subprocess handles HVF for those callers.
///
/// We cache the result in-process: the entitlement on a running
/// binary doesn't change underneath us.
pub fn check_self_has_hvf_entitlement() -> Result<(), String> {
    static CACHED: OnceLock<Result<(), String>> = OnceLock::new();
    CACHED
        .get_or_init(check_self_has_hvf_entitlement_uncached)
        .clone()
}

fn check_self_has_hvf_entitlement_uncached() -> Result<(), String> {
    let exe = std::env::current_exe().map_err(|e| {
        format!(
            "could not resolve current_exe to check HVF entitlement: {e} \
             (your binary may need to be codesigned with \
             `cargo supermachine build`)"
        )
    })?;
    let output = Command::new("codesign")
        .args(["--display", "--entitlements", "-", "--xml"])
        .arg(&exe)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output();
    let output = match output {
        Ok(o) => o,
        Err(_) => return Ok(()),  // codesign missing — best-effort skip
    };
    if !output.status.success() {
        // Not signed at all, or signature is invalid.
        return Err(missing_entitlement_message(&exe));
    }
    let stdout = String::from_utf8_lossy(&output.stdout);
    if stdout.contains("com.apple.security.hypervisor") {
        Ok(())
    } else {
        Err(missing_entitlement_message(&exe))
    }
}

fn missing_entitlement_message(exe: &Path) -> String {
    format!(
        "this binary lacks the `com.apple.security.hypervisor` entitlement, \
         so `Vm::start` cannot call `hv_vm_create` (it would return HV_DENIED).\n\
         \n\
         Two ways to fix:\n\
         \n\
           (a) Use `Image::acquire` / `Image::acquire_with` instead of \
               `Vm::start`. The library spawns a pre-signed \
               `supermachine-worker` subprocess that handles HVF on your \
               behalf, so your own binary never calls into HVF and doesn't \
               need codesigning. This is the recommended path for embedders.\n\
           (b) Build your binary with the bundled cargo plugin:\n\
                   cargo supermachine build --release\n\
               which wraps `cargo build` and codesigns the output with the \
               HVF entitlement. Use this if you specifically want the \
               in-process VM thread (`Vm::start`).\n\
         \n\
         Path: {}",
        exe.display()
    )
}

/// `$XDG_DATA_HOME/supermachine/v{VERSION}/.worker-signed` (or the
/// `$HOME/.local/share/...` fallback). Versioned so a supermachine
/// upgrade doesn't reuse the prior version's sentinel.
fn sentinel_path() -> Option<PathBuf> {
    let base = if let Some(d) = std::env::var_os("XDG_DATA_HOME") {
        PathBuf::from(d)
    } else if let Some(h) = std::env::var_os("HOME") {
        PathBuf::from(h).join(".local/share")
    } else {
        return None;
    };
    Some(
        base.join("supermachine")
            .join(format!("v{}", env!("CARGO_PKG_VERSION")))
            .join(".worker-signed"),
    )
}