envseal 0.3.4

Write-only secret vault with process-level access control — post-agent secret management
Documentation
//! macOS sandbox backend — `sandbox_init(3)` profile-based isolation.
//!
//! macOS exposes per-process sandboxing through the deprecated-but-still-shipped
//! `sandbox_init` C function in `libSystem`. Calling it from `pre_exec`
//! installs a profile on the child before `execve`, equivalent to Linux's
//! `unshare`.
//!
//! # Tier mapping
//!
//! - **None** — no-op.
//! - **Hardened** — a custom SBPL profile that denies network access,
//!   hides other processes' metadata (`process-info*`), blocks raw
//!   `IOKit` device access (so the child can't hook the HID stack to
//!   keylog the user), denies privileged Mach task-port lookups (so
//!   it can't `task_for_pid` into other processes), and denies write
//!   access to POSIX shared memory (a non-network exfil channel). The
//!   filesystem is kept open because Hardened is intended to allow
//!   typical `npm run` / `cargo build` / `wrangler deploy` workloads.
//! - **Lockdown** — `pure-computation` builtin profile. Child cannot
//!   reach the network *or* write to the filesystem outside ephemeral
//!   areas. The closest macOS analogue to Linux Lockdown's "private
//!   mount + denied network".
//!
//! The chosen profiles are macOS's predefined string-name profiles
//! (`kSBXProfilePureComputation`) for Lockdown, and a custom SBPL
//! profile for Hardened. `sandbox_init` historically prints a deprecation
//! diagnostic at link time but the function is still present and
//! exercised by Chrome / Firefox / Apple's own apps.

#![cfg(target_os = "macos")]

use std::os::raw::{c_char, c_int};

use super::SandboxTier;

extern "C" {
    /// `int sandbox_init(const char *profile, uint64_t flags, char **errorbuf);`
    fn sandbox_init(profile: *const c_char, flags: u64, errorbuf: *mut *mut c_char) -> c_int;
    /// `void sandbox_free_error(char *errorbuf);`
    fn sandbox_free_error(errorbuf: *mut c_char);
}

/// `SANDBOX_NAMED` — the `profile` argument is a predefined builtin name.
const SANDBOX_NAMED: u64 = 1;

/// `SANDBOX_RAW` — the `profile` argument is raw SBPL string.
const SANDBOX_RAW: u64 = 0;

/// macOS custom Hardened SBPL — defense-in-depth around the secret in
/// the child's environment.
///
/// - `(allow default)` — start from the unrestricted base; we only
///   *deny* specific operations rather than building an allow-list,
///   because Hardened needs to support arbitrary developer tooling
///   (npm, cargo, wrangler, …).
/// - `(deny network*)` — closes outbound TCP/UDP, the obvious
///   network-exfil path.
/// - `(deny process-info* (with no-log))` — hides other processes'
///   metadata so the child can't enumerate the system to find the
///   envseal supervisor / GUI helper. `no-log` keeps the macOS
///   `log stream` clean of denied-access spam from routine
///   `proc_listpids` calls inside libSystem.
/// - `(deny iokit-open)` — blocks raw `IOKit` device handles so the
///   child cannot, e.g., open `IOHIDSystem` to keylog the desktop
///   while the user types in another window.
/// - `(deny mach-priv-task-port)` — blocks `task_for_pid()` against
///   privileged tasks; without this a child running as the same uid
///   could attach the supervisor's address space.
/// - `(deny ipc-posix-shm-write)` — closes POSIX shared-memory writes
///   so a compromised child can't drop the secret into a `shm_open`
///   region that another local user maps to read it back.
const PROFILE_HARDENED: &[u8] = b"(version 1)
(allow default)
(deny network*)
(deny process-info* (with no-log))
(deny iokit-open)
(deny mach-priv-task-port)
(deny ipc-posix-shm-write)\0";

/// macOS predefined profile that denies network *and* file writes outside
/// ephemeral areas.
const PROFILE_PURE_COMPUTATION: &[u8] = b"pure-computation\0";

/// Apply sandbox isolation matching the requested tier.
///
/// Called between `fork()` and `exec()` via `Command::pre_exec`. Each tier
/// maps to a predefined macOS profile (see module docs).
///
/// # Errors
///
/// Returns the OS error if `sandbox_init` rejects the profile (in
/// practice, a `PermissionDenied` containing the message libSystem emits
/// into `errorbuf`).
///
/// # Safety
///
/// Must only be called from a `pre_exec` closure.
pub fn apply_sandbox(tier: SandboxTier) -> std::io::Result<()> {
    let (profile_bytes, flags) = match tier {
        SandboxTier::None => return Ok(()),
        SandboxTier::Hardened => (PROFILE_HARDENED, SANDBOX_RAW),
        SandboxTier::Lockdown => (PROFILE_PURE_COMPUTATION, SANDBOX_NAMED),
    };

    let mut errorbuf: *mut c_char = std::ptr::null_mut();
    // SAFETY: profile_bytes is a 0-terminated byte string; we pass a valid
    // pointer to errorbuf. sandbox_init does not retain the profile pointer.
    let rc = unsafe {
        sandbox_init(
            profile_bytes.as_ptr().cast::<c_char>(),
            flags,
            &mut errorbuf,
        )
    };

    if rc == 0 {
        // Even on success, errorbuf may be unused (NULL).
        if !errorbuf.is_null() {
            unsafe {
                sandbox_free_error(errorbuf);
            }
        }
        return Ok(());
    }

    // On failure libSystem allocates an error string. Copy it out and free.
    let detail = if errorbuf.is_null() {
        String::from("unknown sandbox_init error")
    } else {
        let cstr = unsafe { std::ffi::CStr::from_ptr(errorbuf) };
        let s = cstr.to_string_lossy().into_owned();
        unsafe {
            sandbox_free_error(errorbuf);
        }
        s
    };

    Err(std::io::Error::new(
        std::io::ErrorKind::PermissionDenied,
        format!(
            "sandbox_init({}) failed: {detail}",
            std::str::from_utf8(&profile_bytes[..profile_bytes.len() - 1]).unwrap_or("?")
        ),
    ))
}

/// Whether the macOS sandbox primitive is reachable on this build.
///
/// Always `true` on macOS — `sandbox_init` is in `libSystem`, present on
/// every shipping macOS. Used by [`super::OsCapabilities`] for diagnostic
/// reporting.
#[must_use]
pub fn available() -> bool {
    true
}