From a610b290049d33132edbb1b5bdc2120f401c3dc5 Mon Sep 17 00:00:00 2001 From: Alexsander Falcucci Date: Tue, 9 Jun 2026 10:00:20 +0200 Subject: [PATCH] firma-run: add macOS VZ guest launch contract cherry-picking the VZ guest work from #141 the split is useful to isolated the guest work development to match Apples Virtualization.framework boundaries https://developer.apple.com/documentation/virtualization here we specifically use FIRMA_RUN_VZ_GUEST to select the macOS vz mode. before launch, the backend validates the configured runner and guest artifacts, rejects missing or relative paths, checks that the runner is executable on Unix and only then emits a macos_vz_guest proof. the contract carries the execution envelope the runner must enforce: - sandbox id - runtime dir - runner path - guest image paths - command - args - cwd - environment - mounts - identity mode - seccomp artifact path - proxy URL - DNS stub address - attribution headers - required invariants: - sidecar-only egress - confined DNS - fail-closed startup/runtime - direct-bypass resistance - stdio/signal/exit preservation **since there is still no Apple Virtualization.framework runner here**, it servers us as a bedside contract for the future runner. routing also learns that macOS structural modes may need a host DNS refusal stub even when they are not using the Linux namespace path. the sandbox-exec uses that stub on loopback. the VZ guest contract exposes the same endpoint so the future runner can make guest DNS deterministic instead of letting the agent fall back to ambient resolution. *Note that the ESF remains out of this change.* Tested with: cargo test -p firma-run macos_vz --- crates/firma-run/src/backend/macos_vz.rs | 475 +++++++++++++++++- crates/firma-run/src/backend/mod.rs | 3 + crates/firma-run/src/routing.rs | 109 ++-- crates/firma-run/src/runtime.rs | 4 +- docs-site/public/llms.txt | 4 +- .../concepts/macos-structural-strategy.md | 12 +- .../src/content/docs/concepts/sandbox.md | 15 +- .../src/content/docs/guides/firma-run.md | 12 +- .../e2e/macos-structural-assertions.md | 14 +- 9 files changed, 556 insertions(+), 92 deletions(-) diff --git a/crates/firma-run/src/backend/macos_vz.rs b/crates/firma-run/src/backend/macos_vz.rs index 805e5e00..9a910690 100644 --- a/crates/firma-run/src/backend/macos_vz.rs +++ b/crates/firma-run/src/backend/macos_vz.rs @@ -1,18 +1,29 @@ +use std::collections::BTreeMap; use std::fmt::Write as _; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; use std::process::{Child, Command}; use crate::backend::{ BackendKind, EnforcementProof, LaunchSpec, NetworkConfinement, PrepareRequest, SandboxBackend, SandboxHandle, }; -use crate::config::{MountSpec, NetworkPolicy}; +use crate::config::MountSpec; +use crate::config::NetworkPolicy; use crate::error::RunError; +const VZ_GUEST_MODE_ENV: &str = "FIRMA_RUN_VZ_GUEST"; const VZ_STRUCTURAL_NETWORK_ENV: &str = "FIRMA_RUN_VZ_STRUCTURAL_NETWORK"; +const VZ_GUEST_RUNNER_ENV: &str = "FIRMA_RUN_VZ_GUEST_RUNNER"; +const VZ_GUEST_KERNEL_ENV: &str = "FIRMA_RUN_VZ_GUEST_KERNEL"; +const VZ_GUEST_INITRD_ENV: &str = "FIRMA_RUN_VZ_GUEST_INITRD"; +const VZ_GUEST_ROOTFS_ENV: &str = "FIRMA_RUN_VZ_GUEST_ROOTFS"; +const VZ_GUEST_LAUNCH_CONTRACT_VERSION: u32 = 1; /// macOS runtime backend. /// -/// Operates in one of two modes: +/// Operates in one of three modes: /// /// - **Compatibility mode** (default): `sandbox-exec` + `HTTP_PROXY` injection. /// Proxy-only; requires `--allow-non-structural`. Equivalent to the current @@ -25,6 +36,13 @@ const VZ_STRUCTURAL_NETWORK_ENV: &str = "FIRMA_RUN_VZ_STRUCTURAL_NETWORK"; /// caveat until the guest-backed path can narrow the boundary further. This mode /// reports `structural=true` with `network_confinement=macos_sandbox_network_deny`. /// This is an intermediate structural step before the guest-backed path. +/// +/// - **VZ guest structural mode** (`FIRMA_RUN_VZ_GUEST=1`): launch a configured +/// host runner with an explicit JSON contract. The runner owns the +/// Virtualization.framework lifecycle and must boot a guest whose only +/// usable egress path is the sidecar bridge provided in the contract. +/// This mode reports `structural=true` with +/// `network_confinement=macos_vz_guest`. #[derive(Debug, Default)] pub struct VzBackend; @@ -37,11 +55,16 @@ impl VzBackend { /// Returns the active structural mode for the VZ backend. fn vz_structural_mode() -> VzStructuralMode { - vz_structural_mode_from_flag(crate::config::env_truthy(VZ_STRUCTURAL_NETWORK_ENV)) + vz_structural_mode_from_flags( + crate::config::env_truthy(VZ_GUEST_MODE_ENV), + crate::config::env_truthy(VZ_STRUCTURAL_NETWORK_ENV), + ) } -fn vz_structural_mode_from_flag(sandbox_exec_network: bool) -> VzStructuralMode { - if sandbox_exec_network { +fn vz_structural_mode_from_flags(vz_guest: bool, sandbox_exec_network: bool) -> VzStructuralMode { + if vz_guest { + VzStructuralMode::VzGuest + } else if sandbox_exec_network { VzStructuralMode::SandboxExecNetworkDeny } else { VzStructuralMode::Compatibility @@ -57,6 +80,10 @@ pub enum VzStructuralMode { /// The wrapped process may only reach loopback addresses (proxy bridge /// and DNS stub). All external outbound connections are denied. SandboxExecNetworkDeny, + /// Apple Virtualization.framework guest with isolated virtio networking. + /// The external runner owns the platform framework calls; this backend + /// validates inputs, emits the launch contract, and supervises the runner. + VzGuest, } impl SandboxBackend for VzBackend { @@ -72,17 +99,34 @@ impl SandboxBackend for VzBackend { }); } - if !command_available("sandbox-exec") { - return Err(RunError::Backend { - backend: BackendKind::Vz.to_string(), - reason: "sandbox-exec is not installed or not executable".to_string(), - }); - } - if vz_structural_mode() == VzStructuralMode::SandboxExecNetworkDeny { - tracing::info!( - mode = "sandbox_exec_network_deny", - "macOS VZ structural preflight: sandbox-exec with deny network-outbound selected" - ); + let mode = vz_structural_mode(); + match mode { + VzStructuralMode::Compatibility | VzStructuralMode::SandboxExecNetworkDeny => { + if !command_available("sandbox-exec") { + return Err(RunError::Backend { + backend: BackendKind::Vz.to_string(), + reason: "sandbox-exec is not installed or not executable".to_string(), + }); + } + + if mode == VzStructuralMode::SandboxExecNetworkDeny { + tracing::info!( + mode = "sandbox_exec_network_deny", + "macOS VZ structural preflight: sandbox-exec with deny network-outbound selected" + ); + } + } + VzStructuralMode::VzGuest => { + let inputs = VzGuestLaunchInputs::from_env()?; + tracing::info!( + mode = "vz_guest", + runner = %inputs.runner.display(), + kernel = %inputs.kernel.display(), + initrd = %inputs.initrd.display(), + rootfs = %inputs.rootfs.display(), + "macOS VZ structural preflight: guest runner and images validated" + ); + } } let runtime_dir = std::env::temp_dir() @@ -134,6 +178,17 @@ impl SandboxBackend for VzBackend { .to_string(), network_confinement: NetworkConfinement::MacosSandboxNetworkDeny, }), + VzStructuralMode::VzGuest => Ok(EnforcementProof { + backend: BackendKind::Vz, + structural: true, + fail_closed: policy.fail_closed, + detail: + "macOS Virtualization.framework guest mode selected; configured runner must \ + boot the guest with bridge-only egress, deterministic DNS, and \ + fail-closed sidecar reachability checks" + .to_string(), + network_confinement: NetworkConfinement::MacosVzGuest, + }), VzStructuralMode::Compatibility => Ok(EnforcementProof { backend: BackendKind::Vz, structural: false, @@ -159,7 +214,7 @@ impl SandboxBackend for VzBackend { Ok(()) } - fn start_agent(&self, _handle: &SandboxHandle, launch: &LaunchSpec) -> Result { + fn start_agent(&self, handle: &SandboxHandle, launch: &LaunchSpec) -> Result { if !cfg!(target_os = "macos") { return Err(RunError::UnsupportedBackend { backend: BackendKind::Vz.to_string(), @@ -168,6 +223,10 @@ impl SandboxBackend for VzBackend { } let mode = vz_structural_mode(); + if mode == VzStructuralMode::VzGuest { + return start_vz_guest_runner(handle, launch); + } + let mut command = Command::new("sandbox-exec"); let profile = build_sandbox_profile(launch, mode); @@ -217,6 +276,286 @@ impl SandboxBackend for VzBackend { } } +#[derive(Debug, Clone)] +struct VzGuestLaunchInputs { + runner: PathBuf, + kernel: PathBuf, + initrd: PathBuf, + rootfs: PathBuf, +} + +impl VzGuestLaunchInputs { + fn from_env() -> Result { + let runner = validate_required_file_env(VZ_GUEST_RUNNER_ENV)?; + #[cfg(unix)] + ensure_executable_file(VZ_GUEST_RUNNER_ENV, &runner)?; + #[cfg(not(unix))] + ensure_executable_file(VZ_GUEST_RUNNER_ENV, &runner); + Ok(Self { + runner, + kernel: validate_required_file_env(VZ_GUEST_KERNEL_ENV)?, + initrd: validate_required_file_env(VZ_GUEST_INITRD_ENV)?, + rootfs: validate_required_file_env(VZ_GUEST_ROOTFS_ENV)?, + }) + } +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "snake_case")] +struct VzGuestLaunchContract { + version: u32, + sandbox_id: String, + runtime_dir: PathBuf, + runner: VzGuestRunnerContract, + guest: VzGuestImageContract, + command: VzGuestCommandContract, + mounts: Vec, + network: VzGuestNetworkContract, + invariants: Vec, +} + +impl VzGuestLaunchContract { + fn from_launch( + handle: &SandboxHandle, + launch: &LaunchSpec, + inputs: VzGuestLaunchInputs, + ) -> Result { + Ok(Self { + version: VZ_GUEST_LAUNCH_CONTRACT_VERSION, + sandbox_id: handle.identity.sandbox_id.to_string(), + runtime_dir: handle.runtime_dir.clone(), + runner: VzGuestRunnerContract { + path: inputs.runner, + }, + guest: VzGuestImageContract { + kernel: inputs.kernel, + initrd: inputs.initrd, + rootfs: inputs.rootfs, + }, + command: VzGuestCommandContract { + executable: launch.executable.clone(), + args: launch.args.clone(), + cwd: launch.cwd.clone(), + env: launch.env.clone(), + identity_mode: launch.identity_mode, + seccomp_filter_path: launch.seccomp_filter_path.clone(), + }, + mounts: handle.mounts.clone(), + network: VzGuestNetworkContract::from_launch_env( + &launch.env, + handle.identity.full_attribution_headers(), + )?, + invariants: VzGuestInvariantContract::required_set(handle.network_policy.fail_closed), + }) + } +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "snake_case")] +struct VzGuestRunnerContract { + path: PathBuf, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "snake_case")] +struct VzGuestImageContract { + kernel: PathBuf, + initrd: PathBuf, + rootfs: PathBuf, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "snake_case")] +struct VzGuestCommandContract { + executable: String, + args: Vec, + cwd: PathBuf, + env: BTreeMap, + identity_mode: crate::config::SandboxIdentityMode, + seccomp_filter_path: Option, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "snake_case")] +struct VzGuestNetworkContract { + proxy_url: String, + dns_stub_addr: String, + attribution_headers: BTreeMap, +} + +impl VzGuestNetworkContract { + fn from_launch_env( + env: &BTreeMap, + attribution_headers: BTreeMap, + ) -> Result { + Ok(Self { + proxy_url: required_launch_env(env, "HTTP_PROXY")?.to_string(), + dns_stub_addr: required_launch_env(env, "FIRMA_DNS_STUB_ADDR")?.to_string(), + attribution_headers, + }) + } +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "snake_case")] +struct VzGuestInvariantContract { + name: VzGuestInvariantName, + mode: VzGuestInvariantMode, +} + +impl VzGuestInvariantContract { + fn required_set(fail_closed_startup: bool) -> Vec { + vec![ + Self::required(VzGuestInvariantName::SidecarOnlyEgress), + Self::required(VzGuestInvariantName::DnsConfined), + Self { + name: VzGuestInvariantName::FailClosedStartup, + mode: if fail_closed_startup { + VzGuestInvariantMode::Required + } else { + VzGuestInvariantMode::DisabledByPolicy + }, + }, + Self::required(VzGuestInvariantName::FailClosedRuntime), + Self::required(VzGuestInvariantName::DirectBypassResistant), + Self::required(VzGuestInvariantName::PreserveStdioSignalsExit), + ] + } + + fn required(name: VzGuestInvariantName) -> Self { + Self { + name, + mode: VzGuestInvariantMode::Required, + } + } +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "snake_case")] +enum VzGuestInvariantName { + SidecarOnlyEgress, + DnsConfined, + FailClosedStartup, + FailClosedRuntime, + DirectBypassResistant, + PreserveStdioSignalsExit, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "snake_case")] +enum VzGuestInvariantMode { + Required, + DisabledByPolicy, +} + +fn start_vz_guest_runner(handle: &SandboxHandle, launch: &LaunchSpec) -> Result { + let inputs = VzGuestLaunchInputs::from_env()?; + let runner = inputs.runner.clone(); + let contract = VzGuestLaunchContract::from_launch(handle, launch, inputs)?; + let contract_path = handle.runtime_dir.join("vz-guest-launch.json"); + let json = serde_json::to_vec_pretty(&contract).map_err(|error| { + RunError::Internal(format!( + "failed to serialize macOS VZ guest launch contract: {error}" + )) + })?; + + std::fs::write(&contract_path, json).map_err(|error| RunError::Backend { + backend: BackendKind::Vz.to_string(), + reason: format!( + "failed to write VZ guest launch contract {}: {error}", + contract_path.display() + ), + })?; + + tracing::info!( + mode = "vz_guest", + runner = %runner.display(), + contract = %contract_path.display(), + "macOS VZ: launching guest runner" + ); + + Command::new(&runner) + .arg("--launch-contract") + .arg(&contract_path) + .spawn() + .map_err(|error| { + RunError::Spawn(format!( + "failed to spawn macOS VZ guest runner {}: {error}", + runner.display() + )) + }) +} + +fn required_launch_env<'a>( + env: &'a BTreeMap, + key: &str, +) -> Result<&'a str, RunError> { + env.get(key) + .map(String::as_str) + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| RunError::Backend { + backend: BackendKind::Vz.to_string(), + reason: format!( + "VZ guest launch requires {key} from network runtime; sidecar bridge was not prepared" + ), + }) +} + +fn validate_required_file_env(name: &str) -> Result { + let path = read_required_path_env(name)?; + if !path.exists() { + return Err(RunError::Backend { + backend: BackendKind::Vz.to_string(), + reason: format!("{name} does not exist: {}", path.display()), + }); + } + if !path.is_file() { + return Err(RunError::Backend { + backend: BackendKind::Vz.to_string(), + reason: format!("{name} must point to a file: {}", path.display()), + }); + } + + Ok(path) +} + +fn read_required_path_env(name: &str) -> Result { + let value = std::env::var(name).map_err(|_| RunError::Backend { + backend: BackendKind::Vz.to_string(), + reason: format!("VZ guest mode requires {name}"), + })?; + let path = PathBuf::from(value); + if !path.is_absolute() { + return Err(RunError::Backend { + backend: BackendKind::Vz.to_string(), + reason: format!("{name} must be an absolute path: {}", path.display()), + }); + } + + Ok(path) +} + +#[cfg(unix)] +fn ensure_executable_file(name: &str, path: &Path) -> Result<(), RunError> { + let metadata = path.metadata().map_err(|error| RunError::Backend { + backend: BackendKind::Vz.to_string(), + reason: format!("failed to inspect {name} {}: {error}", path.display()), + })?; + if metadata.permissions().mode() & 0o111 == 0 { + return Err(RunError::Backend { + backend: BackendKind::Vz.to_string(), + reason: format!("{name} must be executable: {}", path.display()), + }); + } + + Ok(()) +} + +#[cfg(not(unix))] +fn ensure_executable_file(name: &str, path: &Path) { + let _ = (name, path); +} + /// Build the `sandbox-exec` SBPL profile for the given mode. /// /// In `SandboxExecNetworkDeny` mode the profile adds `deny network-outbound` @@ -301,10 +640,14 @@ mod tests { use std::collections::BTreeMap; use std::path::PathBuf; - use crate::backend::{BackendKind, LaunchSpec}; - use crate::config::SandboxIdentityMode; + use crate::backend::{BackendKind, LaunchSpec, SandboxHandle}; + use crate::config::{NetworkPolicy, SandboxIdentityMode}; + use crate::identity::RunIdentity; - use super::{VzStructuralMode, build_sandbox_profile, vz_structural_mode_from_flag}; + use super::{ + VzGuestLaunchContract, VzGuestLaunchInputs, VzStructuralMode, build_sandbox_profile, + vz_structural_mode_from_flags, + }; fn test_launch(profile_name: &str) -> LaunchSpec { let mut env = BTreeMap::new(); @@ -344,13 +687,21 @@ mod tests { // ── structural mode ─────────────────────────────────────────────────────── #[test] - fn structural_mode_follows_sandbox_exec_flag() { + fn vz_guest_mode_takes_precedence_over_sandbox_exec_flag() { + assert_eq!( + vz_structural_mode_from_flags(true, false), + VzStructuralMode::VzGuest + ); assert_eq!( - vz_structural_mode_from_flag(true), + vz_structural_mode_from_flags(true, true), + VzStructuralMode::VzGuest + ); + assert_eq!( + vz_structural_mode_from_flags(false, true), VzStructuralMode::SandboxExecNetworkDeny ); assert_eq!( - vz_structural_mode_from_flag(false), + vz_structural_mode_from_flags(false, false), VzStructuralMode::Compatibility ); } @@ -415,6 +766,82 @@ mod tests { ); } + #[test] + fn vz_guest_contract_carries_command_mounts_network_and_invariants() { + let identity = RunIdentity::new("claude-code"); + let handle = SandboxHandle { + backend: BackendKind::Vz, + runtime_dir: PathBuf::from("/tmp/firma-test-vz-guest"), + identity: identity.clone(), + mounts: vec![crate::config::MountSpec { + source: PathBuf::from("/Users/tester/project"), + target: PathBuf::from("/workspace"), + read_only: false, + }], + network_policy: NetworkPolicy { + enforce_network_namespace: false, + fail_closed: true, + }, + }; + let mut launch = test_launch("claude-code"); + launch.env.insert( + "HTTP_PROXY".to_string(), + "http://127.0.0.1:18080".to_string(), + ); + launch.env.insert( + "FIRMA_DNS_STUB_ADDR".to_string(), + "127.0.0.1:5353".to_string(), + ); + launch.args = vec!["--print".to_string()]; + + let contract = VzGuestLaunchContract::from_launch( + &handle, + &launch, + VzGuestLaunchInputs { + runner: PathBuf::from("/Applications/Firma/vz-runner"), + kernel: PathBuf::from("/var/lib/firma/vz/vmlinuz"), + initrd: PathBuf::from("/var/lib/firma/vz/initrd.img"), + rootfs: PathBuf::from("/var/lib/firma/vz/rootfs.img"), + }, + ) + .expect("guest contract should build from prepared launch"); + + let json = serde_json::to_value(&contract).expect("serialize contract"); + assert_eq!(json["version"], 1); + assert_eq!(json["sandbox_id"], identity.sandbox_id.to_string()); + assert_eq!(json["command"]["executable"], "/usr/bin/true"); + assert_eq!(json["command"]["args"][0], "--print"); + assert_eq!(json["mounts"][0]["target"], "/workspace"); + assert_eq!(json["network"]["proxy_url"], "http://127.0.0.1:18080"); + assert_eq!(json["network"]["dns_stub_addr"], "127.0.0.1:5353"); + assert_eq!( + json["network"]["attribution_headers"]["x-firma-profile"], + "claude-code" + ); + let invariants = json["invariants"].as_array().expect("invariants array"); + assert!( + invariants + .iter() + .any(|invariant| invariant["name"] == "sidecar_only_egress" + && invariant["mode"] == "required"), + "sidecar-only egress invariant must be required: {invariants:?}" + ); + assert!( + invariants + .iter() + .any(|invariant| invariant["name"] == "dns_confined" + && invariant["mode"] == "required"), + "DNS confinement invariant must be required: {invariants:?}" + ); + assert!( + invariants + .iter() + .any(|invariant| invariant["name"] == "direct_bypass_resistant" + && invariant["mode"] == "required"), + "direct-bypass invariant must be required: {invariants:?}" + ); + } + // ── EnforcementProof ───────────────────────────────────────────────────── #[test] @@ -460,6 +887,8 @@ mod tests { let json = serde_json::to_string(&NetworkConfinement::LinuxNetworkNamespace).expect("serialize"); assert_eq!(json, r#""linux_network_namespace""#); + let json = serde_json::to_string(&NetworkConfinement::MacosVzGuest).expect("serialize"); + assert_eq!(json, r#""macos_vz_guest""#); let json = serde_json::to_string(&NetworkConfinement::ProxyOnly).expect("serialize"); assert_eq!(json, r#""proxy_only""#); } diff --git a/crates/firma-run/src/backend/mod.rs b/crates/firma-run/src/backend/mod.rs index 71da9d4f..f09addb4 100644 --- a/crates/firma-run/src/backend/mod.rs +++ b/crates/firma-run/src/backend/mod.rs @@ -28,6 +28,9 @@ pub enum NetworkConfinement { /// macOS `TrustedBSD` MAC sandbox with `deny network-outbound` policy. /// Provides kernel-enforced loopback-only egress without a VM guest. MacosSandboxNetworkDeny, + /// Apple Virtualization.framework guest with isolated virtio networking. + /// Implemented as a runner launch contract owned by the macOS VZ backend. + MacosVzGuest, /// KVM micro-VM (Firecracker). Planned as enterprise additive path. KvmMicroVm, /// Proxy-only compatibility mode: enforcement depends on `HTTP_PROXY` diff --git a/crates/firma-run/src/routing.rs b/crates/firma-run/src/routing.rs index ef87a92e..cfe03e6d 100644 --- a/crates/firma-run/src/routing.rs +++ b/crates/firma-run/src/routing.rs @@ -171,9 +171,10 @@ pub fn prepare_network_runtime( #[cfg(unix)] let host_bridge = setup_host_bridge(&effective_endpoint, identity, &mut env_overrides)?; - // macOS sandbox-exec structural mode without a Linux network namespace - // also needs a host-side DNS refusal stub. sandbox-exec reaches it on - // loopback. + // macOS structural modes without a Linux network namespace also need a + // host-side DNS refusal stub. sandbox-exec reaches it on loopback; the + // VZ guest runner receives it through the launch contract and must wire + // guest DNS to this endpoint. #[cfg(unix)] let host_dns_stub = maybe_start_host_dns_stub(handle, proof, &mut env_overrides)?; @@ -294,10 +295,12 @@ fn setup_host_bridge( Ok(bridge) } -/// Start a host-side DNS refusal stub for macOS sandbox-exec structural mode. +/// Start a host-side DNS refusal stub when the active confinement mechanism is +/// one of the macOS structural paths. /// /// On the macOS sandbox-exec structural path the wrapped process can only reach -/// loopback. The stub refuses all DNS queries so the agent cannot resolve +/// loopback. On the VZ guest path the runner must expose this endpoint as the +/// guest resolver. The stub refuses all DNS queries so the agent cannot resolve /// external hostnames directly; it must use the proxy bridge, which the Sidecar /// controls. This function is a no-op for all other network confinement modes. #[cfg(unix)] @@ -309,7 +312,10 @@ fn maybe_start_host_dns_stub( if handle.backend != BackendKind::Vz { return Ok(None); } - if proof.network_confinement != NetworkConfinement::MacosSandboxNetworkDeny { + if !matches!( + proof.network_confinement, + NetworkConfinement::MacosSandboxNetworkDeny | NetworkConfinement::MacosVzGuest + ) { return Ok(None); } let stub = crate::dns_stub::HostDnsStubHandle::start()?; @@ -1072,11 +1078,11 @@ mod non_structural_env_tests { drop(runtime); } - /// When the proof carries the macOS sandbox-exec structural mechanism, a host-side DNS + /// When the proof carries a macOS structural mechanism, a host-side DNS /// refusal stub must be started and its address exposed as /// `FIRMA_DNS_STUB_ADDR`. #[test] - fn macos_sandbox_exec_structural_path_starts_dns_stub_and_exposes_env_var() { + fn macos_structural_paths_start_dns_stub_and_expose_env_var() { let fake_sidecar = TcpListener::bind("127.0.0.1:0").expect("bind"); let sidecar_addr = fake_sidecar.local_addr().expect("local_addr"); @@ -1096,48 +1102,55 @@ mod non_structural_env_tests { startup_timeout: Duration::from_secs(1), ..AutostartFlags::default() }; - let authority = ResolvedAuthority { - url: "https://authority.test".to_string(), - ca_cert_path: None, - pub_key_path: None, - supervisor: None, - }; - let structural_proof = crate::backend::EnforcementProof { - backend: BackendKind::Vz, - structural: true, - fail_closed: true, - detail: "test macos sandbox network deny".to_string(), - network_confinement: crate::backend::NetworkConfinement::MacosSandboxNetworkDeny, - }; - let runtime = prepare_network_runtime( - &handle, - &structural_proof, - &SidecarEndpoint::Tcp { addr: sidecar_addr }, - &identity, - &flags, - authority, - ) - .expect("prepare_network_runtime must succeed for macOS structural proof"); - let overrides = runtime.env_overrides(); - assert!( - overrides.contains_key("FIRMA_DNS_STUB_ADDR"), - "FIRMA_DNS_STUB_ADDR must be set for macOS sandbox-exec structural mode; \ - got keys: {:?}", - overrides.keys().collect::>() - ); + for mechanism in [ + crate::backend::NetworkConfinement::MacosSandboxNetworkDeny, + crate::backend::NetworkConfinement::MacosVzGuest, + ] { + let authority = ResolvedAuthority { + url: "https://authority.test".to_string(), + ca_cert_path: None, + pub_key_path: None, + supervisor: None, + }; + let structural_proof = crate::backend::EnforcementProof { + backend: BackendKind::Vz, + structural: true, + fail_closed: true, + detail: format!("test {mechanism:?}"), + network_confinement: mechanism.clone(), + }; + + let runtime = prepare_network_runtime( + &handle, + &structural_proof, + &SidecarEndpoint::Tcp { addr: sidecar_addr }, + &identity, + &flags, + authority, + ) + .expect("prepare_network_runtime must succeed for macOS structural proof"); + + let overrides = runtime.env_overrides(); + assert!( + overrides.contains_key("FIRMA_DNS_STUB_ADDR"), + "FIRMA_DNS_STUB_ADDR must be set when {mechanism:?} is active; \ + got keys: {:?}", + overrides.keys().collect::>() + ); - let stub_addr_str = overrides.get("FIRMA_DNS_STUB_ADDR").expect("present"); - let stub_addr: SocketAddr = stub_addr_str - .parse() - .expect("FIRMA_DNS_STUB_ADDR must be a valid SocketAddr"); - assert!( - stub_addr.ip().is_loopback(), - "DNS stub must be on loopback: {stub_addr}" - ); - assert_ne!(stub_addr.port(), 0, "DNS stub must have a real port"); + let stub_addr_str = overrides.get("FIRMA_DNS_STUB_ADDR").expect("present"); + let stub_addr: SocketAddr = stub_addr_str + .parse() + .expect("FIRMA_DNS_STUB_ADDR must be a valid SocketAddr"); + assert!( + stub_addr.ip().is_loopback(), + "DNS stub must be on loopback: {stub_addr}" + ); + assert_ne!(stub_addr.port(), 0, "DNS stub must have a real port"); - drop(runtime); + drop(runtime); + } } #[test] @@ -1172,7 +1185,7 @@ mod non_structural_env_tests { structural: true, fail_closed: true, detail: "miswired macOS-looking proof on non-vz backend".to_string(), - network_confinement: crate::backend::NetworkConfinement::MacosSandboxNetworkDeny, + network_confinement: crate::backend::NetworkConfinement::MacosVzGuest, }; let runtime = prepare_network_runtime( diff --git a/crates/firma-run/src/runtime.rs b/crates/firma-run/src/runtime.rs index c280b2ef..98ed016b 100644 --- a/crates/firma-run/src/runtime.rs +++ b/crates/firma-run/src/runtime.rs @@ -1190,7 +1190,9 @@ mod tests { let result = backend.enforce_network(&handle, &handle.network_policy); if cfg!(target_os = "macos") { let proof = result.unwrap(); - if crate::config::env_truthy("FIRMA_RUN_VZ_STRUCTURAL_NETWORK") { + if crate::config::env_truthy("FIRMA_RUN_VZ_GUEST") + || crate::config::env_truthy("FIRMA_RUN_VZ_STRUCTURAL_NETWORK") + { assert!( proof.structural, "vz backend must report structural=true when macOS structural mode is enabled" diff --git a/docs-site/public/llms.txt b/docs-site/public/llms.txt index ab2f0b70..d582700e 100644 --- a/docs-site/public/llms.txt +++ b/docs-site/public/llms.txt @@ -13,8 +13,8 @@ OpenFirma docs highlights for LLM-based retrieval: - Linux `bwrap` provides the current default structural confinement path. `firecracker` is planned. Default `vz` and `wsl2` are proxy-based compatibility paths that can be bypassed by non-cooperative clients. - macOS strategy: prioritize VZ guest-based structural parity first; treat Endpoint Security Framework (ESF) as selected host hardening/audit, not as standalone sidecar-only egress or DNS confinement. - Experimental macOS structural mode: set FIRMA_RUN_VZ_STRUCTURAL_NETWORK=1 on macOS vz backend for sandbox-exec network-deny mode; wrapped process may only reach loopback, so other host loopback services remain a caveat; network_confinement=macos_sandbox_network_deny; experimental until macOS E2E assertions pass on hardware. -- VZ guest-backed macOS structural parity is planned as a follow-up. The current branch does not expose an executable VZ guest launch contract path. -- EnforcementProof.network_confinement distinguishes linux_network_namespace (bwrap), macos_sandbox_network_deny (sandbox-exec structural), kvm_micro_vm (planned), proxy_only (current vz/wsl2 defaults). +- Experimental macOS VZ guest mode: set FIRMA_RUN_VZ_GUEST=1 plus FIRMA_RUN_VZ_GUEST_RUNNER, FIRMA_RUN_VZ_GUEST_KERNEL, FIRMA_RUN_VZ_GUEST_INITRD, and FIRMA_RUN_VZ_GUEST_ROOTFS absolute paths. firma run validates artifact paths and runner executability, emits macos_vz_guest proof metadata, writes vz-guest-launch.json, and spawns the runner with --launch-contract. The operator-provided runner must implement the Apple Virtualization.framework lifecycle and in-guest bridge-only egress. +- EnforcementProof.network_confinement distinguishes linux_network_namespace (bwrap), macos_sandbox_network_deny (sandbox-exec structural), macos_vz_guest (VZ guest runner contract), kvm_micro_vm (planned), proxy_only (current vz/wsl2 defaults). - Capability token non-exposure applies when capabilities are pre-seeded into the Sidecar; current `firma run --capability-file` exports capability material into the wrapped process environment for compatibility. - Runtime logs for non-structural backends use "backend compatibility proof" with `mode=proxy_only enforced=false` instead of "backend network enforcement proof". - `firma config` reads existing target `firma.toml` values as defaults; CLI flags override only supplied fields. diff --git a/docs-site/src/content/docs/concepts/macos-structural-strategy.md b/docs-site/src/content/docs/concepts/macos-structural-strategy.md index ab026aa7..012bae2e 100644 --- a/docs-site/src/content/docs/concepts/macos-structural-strategy.md +++ b/docs-site/src/content/docs/concepts/macos-structural-strategy.md @@ -25,13 +25,13 @@ This keeps OpenFirma's claim language simple: | ESF-native strategy | Secondary host hardening and audit path, not a standalone parity model. | | Current default claim | Proxy-only compatibility mode unless an experimental structural mode is explicitly enabled. | | Claim graduation bar | Hardware E2E evidence for sidecar-only egress, DNS confinement, fail-closed startup/runtime, direct-bypass resistance, and interactive CLI/TUI usability. | -| Implementation boundary | `firma run` owns proof metadata, preflight, routing artifacts, and process supervision. The planned VZ runner will own Virtualization.framework lifecycle and in-guest enforcement. | +| Implementation boundary | `firma run` owns proof metadata, preflight, routing artifacts, launch contracts, and process supervision. The VZ runner owns Virtualization.framework lifecycle and in-guest enforcement. | ## Baseline Reality Check Current macOS default behavior is compatibility mode. The backend is named `vz`, but the default implementation launches a host process through `sandbox-exec`, injects proxy environment variables, and starts a host-side proxy bridge. It reports `structural=false` and requires explicit non-structural opt-in before launch. That provides useful ergonomics, attribution, and some filesystem masking for supported profiles, but it does not remove the agent's ability to open direct sockets, use direct DNS, or spawn a child with a clean environment. -One experimental structural path exists behind an explicit environment gate. `FIRMA_RUN_VZ_STRUCTURAL_NETWORK=1` selects the intermediate sandbox-exec network-deny mode. The stronger VZ guest-backed path remains the primary parity direction, but its launch contract and runner integration are split into follow-up implementation work. +Two experimental structural paths now exist behind explicit environment gates. `FIRMA_RUN_VZ_STRUCTURAL_NETWORK=1` selects the intermediate sandbox-exec network-deny mode. `FIRMA_RUN_VZ_GUEST=1` selects the stronger VZ guest runner contract path, which validates configured runner and guest image artifact paths and writes a deterministic launch contract. The contract path does not by itself ship Apple Virtualization.framework lifecycle code; the configured runner must own guest boot, virtio networking, guest DNS, stdio/signal/exit plumbing, and route proof inside the VM. There is also no direct seccomp-BPF-to-ESF translation. Linux seccomp filters operate at syscall decision points; ESF exposes a different event model oriented around endpoint security events. ESF has authorization and notification events for areas such as process execution, file access, and some IPC, and it requires the Endpoint Security entitlement. It should be evaluated as its own macOS-native control plane, not as a syscall parity layer. @@ -145,9 +145,9 @@ Recommendation from scoring: pursue Option A as the primary path, keep ESF as an | ---- | ------ | ----- | | Proof metadata | Implemented | `NetworkConfinement` added to `EnforcementProof`; all backends updated. | | Intermediate macOS structural mode | Experimental | `sandbox-exec` network-deny mode via `FIRMA_RUN_VZ_STRUCTURAL_NETWORK=1`. It blocks external IP egress but still allows host loopback. | -| VZ guest runner contract | Planned | Follow-up work will add fail-closed artifact validation, `macos_vz_guest` proof metadata, a versioned launch contract, and runner supervision. | -| Host DNS refusal stub | Implemented | `HostDnsStubHandle` wired into macOS sandbox-exec structural routing. | -| Unit coverage | Implemented | Focused tests cover proof types, sandbox profile generation, DNS stub behavior, and routing setup. | +| VZ guest runner contract | Experimental | `FIRMA_RUN_VZ_GUEST=1` validates runner/kernel/initrd/rootfs paths, checks runner executability, emits `macos_vz_guest` proof metadata, writes `vz-guest-launch.json`, and spawns the configured runner. | +| Host DNS refusal stub | Implemented | `HostDnsStubHandle` wired into macOS structural routing paths and exposed to the VZ guest contract. | +| Unit coverage | Implemented | Focused tests cover proof types, sandbox profile generation, guest contract generation, DNS stub behavior, and routing setup. | | macOS E2E schema | Written | `examples/firma-run/e2e/macos-structural-assertions.md` defines the hardware validation suite. | | VZ guest runner implementation | Planned | Signed Apple Virtualization.framework runner, guest image lifecycle, and in-guest route proof remain the strategic parity work. | | ESF hardening | Planned | Separate enterprise hardening and audit path. | @@ -214,7 +214,7 @@ Recommendation from scoring: pursue Option A as the primary path, keep ESF as an | -------------- | --------- | ------ | ----- | | Structural proof metadata | Baseline proof | Done | `NetworkConfinement` on `EnforcementProof`; preflight logging. | | Intermediate macOS network-deny mode | Intermediate structural mode | Done | `sandbox-exec` network-deny structural mode + DNS stub + routing wiring. | -| VZ guest runner contract | VZ contract | Planned | Fail-closed artifact validation, `macos_vz_guest` proof, launch-contract JSON, and runner spawn path. | +| VZ guest runner contract | VZ contract | Done | Fail-closed artifact validation, `macos_vz_guest` proof, launch-contract JSON, runner spawn path. | | VZ runner implementation | VZ runner | Planned | Apple Virtualization.framework guest lifecycle with stdio, TTY, signals, terminal resize, and exit code preservation. | | Guest image and route proof | VZ runner | Planned | Guest image lifecycle, bridge-only route setup, DNS stub wiring, and machine-readable proof fields. | | macOS structural E2E suite | Evidence | Planned | Hardware tests for cooperative HTTP, policy deny, direct TCP/UDP, direct DNS, proxy-env-unset children, sidecar startup failure, and mid-session sidecar loss. | diff --git a/docs-site/src/content/docs/concepts/sandbox.md b/docs-site/src/content/docs/concepts/sandbox.md index 79c68c77..91d72547 100644 --- a/docs-site/src/content/docs/concepts/sandbox.md +++ b/docs-site/src/content/docs/concepts/sandbox.md @@ -3,7 +3,7 @@ title: The sandbox boundary description: How firma run contains agent egress, what structural vs proxy-only enforcement means, and what each backend protects against. --- -`firma run` wraps an agent in a sandbox, routes its outbound traffic through the Sidecar, and — on structural backends — *removes* the agent's ability to bypass that route. On default macOS and WSL2 paths, enforcement is proxy-based: traffic is mediated only for cooperative HTTP clients that honor `HTTP_PROXY`. macOS also has an experimental sandbox-exec structural path behind an explicit environment gate. This distinction is the most important thing to understand before relying on `firma run` for security enforcement. +`firma run` wraps an agent in a sandbox, routes its outbound traffic through the Sidecar, and — on structural backends — *removes* the agent's ability to bypass that route. On default macOS and WSL2 paths, enforcement is proxy-based: traffic is mediated only for cooperative HTTP clients that honor `HTTP_PROXY`. macOS also has experimental structural paths behind explicit environment gates. This distinction is the most important thing to understand before relying on `firma run` for security enforcement. ## The problem with proxy env vars alone @@ -20,7 +20,7 @@ For a cooperative agent on a developer laptop, none of this matters. For a less- `firma run` has two materially different enforcement modes: -**Structural confinement** (Linux `bwrap`; experimental macOS sandbox-exec mode): The sandbox removes the agent's ability to bypass the proxy at the OS level. In the Linux path, the agent runs in a network namespace where the only reachable destination is the proxy bridge — raw sockets, DNS lookups, and child processes all dead-end inside the sandbox. The macOS sandbox-exec experiment blocks external IP egress but remains loopback-scoped. No extra cooperation from the agent is required once the structural boundary is active. +**Structural confinement** (Linux `bwrap`; experimental macOS structural modes): The sandbox removes the agent's ability to bypass the proxy at the OS level. In the Linux path, the agent runs in a network namespace where the only reachable destination is the proxy bridge — raw sockets, DNS lookups, and child processes all dead-end inside the sandbox. The macOS sandbox-exec experiment blocks external IP egress but remains loopback-scoped. The macOS VZ guest path emits a strict launch contract for an operator-provided Virtualization.framework runner, which must boot the guest with bridge-only egress. No extra cooperation from the agent is required once the structural boundary is active. **Proxy-only / compatibility mode** (macOS `vz`, Windows/WSL2 `wsl2`): The agent runs in the host environment with `HTTP_PROXY` and related environment variables injected. Outbound mediation depends on the agent (or its HTTP library) respecting those variables. Raw sockets, proxy-env-unset children, and non-HTTP protocols can bypass the Sidecar. @@ -45,13 +45,13 @@ Without the opt-in, `firma run` prints a typed error explaining that the selecte ### Structural backends -1. **Network boundary.** On Linux `bwrap`, the agent runs in a network namespace where the only reachable network destination is a host-side process listening on `127.0.0.1:18080` (the proxy bridge). On experimental macOS network-deny mode, the agent is restricted to loopback and external IP egress is blocked. -2. **Proxy bridge.** A small helper inside the sandbox listens on `127.0.0.1:18080` and forwards bytes over a Unix socket to the host's Sidecar (typically at `$XDG_RUNTIME_DIR/firma/sidecar.sock`). On Linux `bwrap`, the agent's traffic has nowhere else to go. On experimental macOS network-deny mode, external IP egress is blocked but other loopback services remain a caveat. -3. **DNS stub.** A stub resolver answers DNS queries deterministically. On Linux it runs inside the sandbox path. On macOS sandbox-exec structural mode, `firma run` starts a host-side refusal stub reachable on loopback. Random outbound DNS must not be a successful bypass. +1. **Network boundary.** On Linux `bwrap`, the agent runs in a network namespace where the only reachable network destination is a host-side process listening on `127.0.0.1:18080` (the proxy bridge). On experimental macOS network-deny mode, the agent is restricted to loopback and external IP egress is blocked. On macOS VZ guest mode, `firma run` writes a launch contract that requires the configured guest runner to expose only the sidecar bridge and DNS stub to the guest. +2. **Proxy bridge.** A small helper inside the sandbox listens on `127.0.0.1:18080` and forwards bytes over a Unix socket to the host's Sidecar (typically at `$XDG_RUNTIME_DIR/firma/sidecar.sock`). On Linux `bwrap`, the agent's traffic has nowhere else to go. On experimental macOS network-deny mode, external IP egress is blocked but other loopback services remain a caveat. On macOS VZ guest mode, the bridge URL is part of the runner contract. +3. **DNS stub.** A stub resolver answers DNS queries deterministically. On Linux it runs inside the sandbox path. On macOS structural paths, `firma run` starts a host-side refusal stub; sandbox-exec reaches it on loopback, and the VZ guest runner must wire guest DNS to it. Random outbound DNS must not be a successful bypass. 4. **`HTTP_PROXY` injection.** For agents that *do* respect proxy env vars, `firma run` sets `HTTP_PROXY=http://127.0.0.1:18080` so they don't need any code change. 5. **Identity remap.** The agent runs under a sandbox user (configurable via `--identity-mode`), so it can't read host secrets via filesystem. -The result is that an agent running under Linux structural `firma run` can attempt to bypass the proxy in any way it likes — open raw sockets, set its own DNS, fork a child process — and every one of those attempts dead-ends inside the sandbox. Experimental macOS network-deny mode provides a narrower claim: external IP egress is blocked, while host loopback remains a known limit. The stronger macOS VZ guest-backed path remains the planned parity direction. +The result is that an agent running under Linux structural `firma run` can attempt to bypass the proxy in any way it likes — open raw sockets, set its own DNS, fork a child process — and every one of those attempts dead-ends inside the sandbox. Experimental macOS network-deny mode provides a narrower claim: external IP egress is blocked, while host loopback remains a known limit. Experimental macOS VZ guest mode has the stronger structural target, but depends on a runner and guest image bundle and still needs hardware E2E evidence before it becomes the default claim. ### Proxy-only backends (vz, wsl2) @@ -70,7 +70,7 @@ Network sandboxing primitives differ across OSes, so `firma run` selects a backe | ------------- | -------- | -------------------------------------------- | -------------- | | `bwrap` | Linux | Unprivileged user namespaces (bubblewrap) | Structural | | `firecracker` | Linux | KVM micro-VM | Planned | -| `vz` | macOS | Host proxy bridge + HTTP proxy injection (`HTTP_PROXY`); optional sandbox-exec network-deny | Proxy-only (default); experimental structural mode via `FIRMA_RUN_VZ_STRUCTURAL_NETWORK=1` | +| `vz` | macOS | Host proxy bridge + HTTP proxy injection (`HTTP_PROXY`); optional sandbox-exec network-deny or VZ guest runner contract | Proxy-only (default); experimental structural modes via `FIRMA_RUN_VZ_STRUCTURAL_NETWORK=1` or `FIRMA_RUN_VZ_GUEST=1` | | `wsl2` | Windows | HTTP proxy injection (`HTTP_PROXY`) | Proxy-only | You can override the platform default with `--backend`: @@ -93,6 +93,7 @@ The *strength* of the enforcement boundary differs by platform. This is the most | Linux (native) | `firecracker` | KVM micro-VM network isolation | Planned | N/A | N/A | | macOS `vz` (default) | `vz` | Host proxy bridge + HTTP proxy injection | No | Yes, if agent ignores `HTTP_PROXY` | Yes | | macOS `vz` (experimental) | `vz` | `sandbox-exec` with `deny network-outbound`; host bridge + DNS stub on loopback | Yes (experimental) | No for external IP egress; loopback-all scope is residual caveat | No; requires experimental env opt-in | +| macOS `vz` guest (experimental) | `vz` | Virtualization.framework runner contract; guest image must enforce bridge-only egress and DNS stub | Yes (experimental) | Target: no; requires runner/guest proof and hardware E2E evidence | No; requires guest env opt-in and artifacts | | Windows / WSL2 | `wsl2` | HTTP proxy injection (`HTTP_PROXY`) | No | Yes, if agent ignores `HTTP_PROXY` | Yes | **Structural** means the sandbox removes the agent's ability to bypass the proxy at the OS level — no extra cooperation from the agent is required. The experimental macOS mode is narrower than Linux because it is loopback-scoped rather than bridge-port-scoped. **Proxy-only** means enforcement depends on the agent (or its HTTP library) respecting `HTTP_PROXY`. On proxy-only backends, `firma run` fails closed unless you pass `--allow-non-structural` to acknowledge this limitation. diff --git a/docs-site/src/content/docs/guides/firma-run.md b/docs-site/src/content/docs/guides/firma-run.md index c23e5e21..8be5f414 100644 --- a/docs-site/src/content/docs/guides/firma-run.md +++ b/docs-site/src/content/docs/guides/firma-run.md @@ -50,7 +50,7 @@ For the conceptual background, read [The sandbox boundary](../../concepts/sandbo | Platform | Default backend | Notes | | -------- | --------------- | ----- | | Linux | `bwrap` | Structural mode; requires unprivileged user namespaces + AppArmor allowance for bwrap. | -| macOS | `vz` | Default compatibility mode: host process, `sandbox-exec`, proxy bridge, explicit `--allow-non-structural` opt-in. Experimental sandbox-exec network-deny mode is available behind an environment gate. | +| macOS | `vz` | Default compatibility mode: host process, `sandbox-exec`, proxy bridge, explicit `--allow-non-structural` opt-in. Experimental structural modes are available via sandbox-exec network-deny or the VZ guest runner contract. | | Windows | `wsl2` | Current compatibility mode; explicit `--allow-non-structural` opt-in. | Verify the platform default works on your host. On Linux, the bwrap backend @@ -79,9 +79,17 @@ macOS structural modes are explicit opt-ins: ```bash # Intermediate: block external IP egress with sandbox-exec, loopback remains reachable. FIRMA_RUN_VZ_STRUCTURAL_NETWORK=1 firma run --profile generic -- curl https://example.com + +# Stronger target: launch through an operator-provided Virtualization.framework runner. +FIRMA_RUN_VZ_GUEST=1 \ +FIRMA_RUN_VZ_GUEST_RUNNER=/Applications/Firma/vz-runner \ +FIRMA_RUN_VZ_GUEST_KERNEL=/var/lib/firma/vz/vmlinuz \ +FIRMA_RUN_VZ_GUEST_INITRD=/var/lib/firma/vz/initrd.img \ +FIRMA_RUN_VZ_GUEST_ROOTFS=/var/lib/firma/vz/rootfs.img \ +firma run --profile generic -- curl https://example.com ``` -The stronger VZ guest-backed path is the planned parity direction, but the executable launch contract and runner integration are intentionally split into a follow-up change. +Guest mode validates those artifact paths, writes `vz-guest-launch.json` under the run directory, and spawns the runner with `--launch-contract`. The runner is responsible for the Apple Virtualization.framework lifecycle and for enforcing the contract inside the guest. ## Step 2: Scaffold a config directory with `firma config` diff --git a/examples/firma-run/e2e/macos-structural-assertions.md b/examples/firma-run/e2e/macos-structural-assertions.md index fd2850d7..74f633f7 100644 --- a/examples/firma-run/e2e/macos-structural-assertions.md +++ b/examples/firma-run/e2e/macos-structural-assertions.md @@ -13,12 +13,20 @@ acceptance cases. Each assertion maps to one required runtime invariant. # Intermediate sandbox-exec structural mode: FIRMA_RUN_VZ_STRUCTURAL_NETWORK=1 firma run --profile generic ... + +# VZ guest structural mode: +FIRMA_RUN_VZ_GUEST=1 +FIRMA_RUN_VZ_GUEST_RUNNER=/Applications/Firma/vz-runner +FIRMA_RUN_VZ_GUEST_KERNEL=/var/lib/firma/vz/vmlinuz +FIRMA_RUN_VZ_GUEST_INITRD=/var/lib/firma/vz/initrd.img +FIRMA_RUN_VZ_GUEST_ROOTFS=/var/lib/firma/vz/rootfs.img +firma run --profile generic ... ``` macOS 12+ (Monterey). Apple Silicon or Intel with sandbox-exec available for -the intermediate mode. VZ guest mode is a follow-up path that will require an -operator-provided runner and guest image bundle. Production deployment should -sign and package the runner for macOS. +the intermediate mode. VZ guest mode additionally requires an operator-provided +runner and guest image bundle that implements the launch contract. Production +deployment should sign and package the runner for macOS. ---