supermachine 0.3.3

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(());
    }

    let meta = std::fs::metadata(worker_path)
        .map_err(|e| format!("stat {}: {e}", worker_path.display()))?;
    let worker_size = meta.len();
    // mtime gives us a cheap "did the binary change?" check.
    // Combined with the path string, this catches both "rebuilt
    // since last sign" and "different binary at a new path."
    let worker_mtime = meta
        .modified()
        .ok()
        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
        .map(|d| d.as_secs())
        .unwrap_or(0);

    let sentinel_marker = format!(
        "size={}\nmtime={}\npath={}\n",
        worker_size,
        worker_mtime,
        worker_path.display()
    );

    if let Some(sentinel) = sentinel_path() {
        if let Ok(existing) = std::fs::read_to_string(&sentinel) {
            if existing == sentinel_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() => {
            // Update sentinel — best-effort, swallow IO errors.
            if let Some(sentinel) = sentinel_path() {
                if let Some(parent) = sentinel.parent() {
                    let _ = std::fs::create_dir_all(parent);
                }
                let _ = std::fs::write(&sentinel, sentinel_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`. Resolution order:
///   1. `$SUPERMACHINE_WORKER_BIN` (explicit override).
///   2. Sibling of the currently running binary
///      (`cargo install supermachine` layout).
///   3. Sibling of the *canonicalized* running binary — handles
///      the case where `~/.cargo/bin/supermachine` is a symlink
///      into a dev tree whose `target/release/` has the worker.
///   4. Ancestor walk for `target/release/supermachine-worker`
///      (cargo dev-tree fallback).
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);
        }
    }
    let exe = std::env::current_exe().ok()?;
    if let Some(dir) = exe.parent() {
        let sibling = dir.join("supermachine-worker");
        if sibling.is_file() {
            return Some(sibling);
        }
    }
    if let Ok(canonical) = std::fs::canonicalize(&exe) {
        if canonical != exe {
            if let Some(dir) = canonical.parent() {
                let sibling = dir.join("supermachine-worker");
                if sibling.is_file() {
                    return Some(sibling);
                }
            }
            for ancestor in canonical.ancestors() {
                let p = ancestor.join("target/release/supermachine-worker");
                if p.is_file() {
                    return Some(p);
                }
            }
        }
    }
    for ancestor in exe.ancestors() {
        let p = ancestor.join("target/release/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"),
    )
}