a3s-box-runtime 3.2.3

MicroVM runtime engine — VM lifecycle, OCI images, attestation, networking
Documentation
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};

use a3s_box_core::rootfs_metadata::RUNTIME_ENV_PATH;
use a3s_box_core::secret::SECRET_ENVIRONMENT_MANIFEST;
use a3s_box_core::{ExecutionBackend, ExecutionIsolation, IsolationClass};
use a3s_runtime::contract::RuntimeObservation;

use super::fixture::{BoxRuntimeConformanceFixture, SECRET_ENV_VALUE, SECRET_FILE_VALUE};
use super::{require, Result};

pub(super) fn verify_provider_least_privilege(
    fixture: &BoxRuntimeConformanceFixture,
    record: &crate::BoxRecord,
    observation: &RuntimeObservation,
) -> Result<()> {
    let execution_isolation = fixture.driver.execution_isolation();
    let metadata = record
        .managed_execution
        .as_ref()
        .ok_or_else(|| super::protocol("security record lost managed metadata"))?;
    let config = &metadata.request.config;
    require(
        record.isolation == execution_isolation && config.isolation == execution_isolation,
        "security execution changed its requested Box isolation",
    )?;
    let expected_plan = a3s_box_core::resolve_execution(config)
        .map_err(|error| super::external("resolve persisted security execution plan", error))?;
    require(
        metadata.plan == expected_plan,
        "security execution persisted a plan that drifted from its creation intent",
    )?;
    let exact_boundary = match execution_isolation {
        ExecutionIsolation::Sandbox => {
            metadata.plan.backend == ExecutionBackend::A3sOci
                && metadata.plan.isolation_class == IsolationClass::SharedKernel
                && !metadata.plan.required_controls.is_empty()
        }
        ExecutionIsolation::Microvm => {
            metadata.plan.backend == ExecutionBackend::Krun
                && metadata.plan.isolation_class == IsolationClass::HardwareVm
                && metadata.plan.required_controls.is_empty()
        }
    };
    require(
        metadata.plan.requested_isolation == execution_isolation && exact_boundary,
        "security execution did not retain its exact isolation backend and boundary",
    )?;
    require(
        !config.privileged
            && config.cap_add.is_empty()
            && exactly_one(&config.cap_drop, "ALL")
            && exactly_one(&config.security_opt, "no-new-privileges")
            && !record.privileged
            && record.cap_add.is_empty()
            && exactly_one(&record.cap_drop, "ALL")
            && exactly_one(&record.security_opt, "no-new-privileges"),
        "security execution did not persist the exact least-privilege policy",
    )?;

    let provider_build = observation
        .provider_build
        .as_deref()
        .ok_or_else(|| super::protocol("security observation omitted provider build identity"))?;
    match execution_isolation {
        ExecutionIsolation::Sandbox => {
            require(
                provider_build.starts_with("a3s-box/")
                    && provider_build.contains(" isolation/sandbox a3s-oci/sha256:")
                    && provider_build.contains(" agent/sha256:"),
                format!("Sandbox provider build identity is incomplete: {provider_build:?}"),
            )?;
            verify_sandbox_least_privilege(record)
        }
        ExecutionIsolation::Microvm => {
            let hypervisor = provider_build
                .split_once(" isolation/microvm hypervisor/")
                .filter(|(prefix, backend)| {
                    prefix.starts_with("a3s-box/")
                        && !backend.is_empty()
                        && !backend.chars().any(char::is_whitespace)
                });
            require(
                hypervisor.is_some(),
                format!("MicroVM provider build identity is incomplete: {provider_build:?}"),
            )?;
            verify_microvm_process_identity(fixture, record)
        }
    }
}

pub(super) fn verify_secret_persistence(
    fixture: &BoxRuntimeConformanceFixture,
    record: &crate::BoxRecord,
    creation_intent: &[u8],
    boxes: &[u8],
) -> Result<()> {
    verify_no_plaintext("managed creation intent", creation_intent)?;
    verify_no_plaintext("boxes.json", boxes)?;

    match fixture.driver.execution_isolation() {
        ExecutionIsolation::Sandbox => verify_sandbox_secret_persistence(record),
        ExecutionIsolation::Microvm => verify_microvm_secret_persistence(record),
    }
}

fn exactly_one(values: &[String], expected: &str) -> bool {
    matches!(values, [value] if value == expected)
}

fn verify_sandbox_least_privilege(record: &crate::BoxRecord) -> Result<()> {
    const BOOTSTRAP_CAPABILITIES: [&str; 11] = [
        "CAP_CHOWN",
        "CAP_DAC_OVERRIDE",
        "CAP_FOWNER",
        "CAP_FSETID",
        "CAP_KILL",
        "CAP_NET_ADMIN",
        "CAP_NET_BIND_SERVICE",
        "CAP_SETGID",
        "CAP_SETPCAP",
        "CAP_SETUID",
        "CAP_SYS_CHROOT",
    ];
    const BOOTSTRAP_CAPABILITY_MASK: u64 = 0x415fb;

    let path = record.box_dir.join("sandbox/bundle/config.json");
    let config: serde_json::Value = serde_json::from_slice(
        &std::fs::read(&path)
            .map_err(|error| super::external("read Sandbox OCI configuration", error))?,
    )
    .map_err(|error| super::external("decode Sandbox OCI configuration", error))?;
    require(
        config
            .pointer("/process/noNewPrivileges")
            .and_then(serde_json::Value::as_bool)
            == Some(true),
        "Sandbox OCI process did not enable no-new-privileges",
    )?;
    let expected = BOOTSTRAP_CAPABILITIES.into_iter().collect::<BTreeSet<_>>();
    for set in ["bounding", "effective", "permitted"] {
        let actual = config
            .pointer(&format!("/process/capabilities/{set}"))
            .and_then(serde_json::Value::as_array)
            .map(|values| {
                values
                    .iter()
                    .filter_map(serde_json::Value::as_str)
                    .collect::<BTreeSet<_>>()
            });
        require(
            actual.as_ref() == Some(&expected),
            format!("Sandbox OCI bootstrap capability set {set} changed: actual={actual:?}"),
        )?;
    }
    for set in ["inheritable", "ambient"] {
        require(
            config
                .pointer(&format!("/process/capabilities/{set}"))
                .and_then(serde_json::Value::as_array)
                .is_some_and(Vec::is_empty),
            format!("Sandbox OCI capability set {set} is not empty"),
        )?;
    }
    require(
        config.pointer("/linux/seccomp/defaultAction").is_some(),
        "Sandbox OCI configuration omitted seccomp",
    )?;
    let namespaces = config
        .pointer("/linux/namespaces")
        .and_then(serde_json::Value::as_array)
        .ok_or_else(|| super::protocol("Sandbox OCI namespaces are missing"))?;
    for required in ["user", "mount", "pid", "ipc", "uts", "network", "cgroup"] {
        require(
            namespaces.iter().any(|namespace| {
                namespace.get("type").and_then(serde_json::Value::as_str) == Some(required)
            }),
            format!("Sandbox OCI configuration omitted the {required} namespace"),
        )?;
    }
    let mappings = config
        .pointer("/linux/uidMappings")
        .and_then(serde_json::Value::as_array)
        .ok_or_else(|| super::protocol("Sandbox OCI UID mappings are missing"))?;
    require(
        mappings.iter().any(|mapping| {
            mapping
                .get("containerID")
                .and_then(serde_json::Value::as_u64)
                == Some(0)
                && mapping.get("hostID").and_then(serde_json::Value::as_u64) != Some(0)
        }),
        "Sandbox container root maps to host root",
    )?;
    require(
        config
            .pointer("/linux/cgroupsPath")
            .and_then(serde_json::Value::as_str)
            == Some(format!("a3s-box/{}", record.id).as_str()),
        "Sandbox OCI cgroup path is not execution-scoped",
    )?;

    let pid = record
        .pid
        .ok_or_else(|| super::protocol("running Sandbox record has no init PID"))?;
    let status = std::fs::read_to_string(Path::new("/proc").join(pid.to_string()).join("status"))
        .map_err(|error| super::external("read Sandbox init process status", error))?;
    require(
        status.lines().any(|line| line == "NoNewPrivs:\t1"),
        "Sandbox init process does not have no_new_privs",
    )?;
    let effective = status
        .lines()
        .find_map(|line| line.strip_prefix("CapEff:\t"))
        .ok_or_else(|| super::protocol("Sandbox init process has no CapEff evidence"))?;
    let effective = u64::from_str_radix(effective, 16)
        .map_err(|error| super::external("decode Sandbox init CapEff", error))?;
    require(
        effective & !BOOTSTRAP_CAPABILITY_MASK == 0,
        format!("Sandbox init process escaped its bootstrap capability set: {effective:#x}"),
    )?;
    let host_uid = status
        .lines()
        .find_map(|line| line.strip_prefix("Uid:\t"))
        .and_then(|line| line.split_whitespace().next())
        .and_then(|value| value.parse::<u32>().ok())
        .ok_or_else(|| super::protocol("Sandbox init process has no host UID evidence"))?;
    require(host_uid != 0, "Sandbox init process runs as host root")
}

fn verify_microvm_process_identity(
    fixture: &BoxRuntimeConformanceFixture,
    record: &crate::BoxRecord,
) -> Result<()> {
    let pid = record
        .pid
        .ok_or_else(|| super::protocol("running MicroVM record has no shim PID"))?;
    let start_time = record
        .pid_start_time
        .ok_or_else(|| super::protocol("running MicroVM record has no shim start identity"))?;
    require(
        crate::process::is_process_alive_with_identity(pid, Some(start_time)),
        format!("MicroVM shim identity {pid}/{start_time} is not alive"),
    )?;
    verify_microvm_shim_binary(fixture, pid)
}

#[cfg(target_os = "linux")]
fn verify_microvm_shim_binary(fixture: &BoxRuntimeConformanceFixture, pid: u32) -> Result<()> {
    let expected = fixture
        .home_dir
        .join("bin/a3s-box-shim")
        .canonicalize()
        .map_err(|error| super::external("canonicalize packaged MicroVM shim", error))?;
    let actual = std::fs::read_link(Path::new("/proc").join(pid.to_string()).join("exe"))
        .map_err(|error| super::external("read MicroVM shim executable identity", error))?
        .canonicalize()
        .map_err(|error| super::external("canonicalize live MicroVM shim", error))?;
    require(
        actual == expected,
        format!(
            "MicroVM process did not launch the packaged shim: actual={} expected={}",
            actual.display(),
            expected.display()
        ),
    )
}

#[cfg(not(target_os = "linux"))]
fn verify_microvm_shim_binary(_fixture: &BoxRuntimeConformanceFixture, _pid: u32) -> Result<()> {
    Ok(())
}

fn verify_sandbox_secret_persistence(record: &crate::BoxRecord) -> Result<()> {
    let bundle_directory = record.box_dir.join("sandbox/bundle");
    let oci = std::fs::read(bundle_directory.join("config.json"))
        .map_err(|error| super::external("read Secret Sandbox OCI configuration", error))?;
    let oci_spec: serde_json::Value = serde_json::from_slice(&oci)
        .map_err(|error| super::external("decode Secret Sandbox OCI configuration", error))?;
    let rootfs_path = oci_spec
        .pointer("/root/path")
        .and_then(serde_json::Value::as_str)
        .ok_or_else(|| super::protocol("Secret Sandbox OCI configuration omitted root.path"))?;
    let rootfs_path = if Path::new(rootfs_path).is_absolute() {
        PathBuf::from(rootfs_path)
    } else {
        bundle_directory.join(rootfs_path)
    };
    let staged_environment = read_regular_file(
        &rootfs_path.join(RUNTIME_ENV_PATH.trim_start_matches('/')),
        "Secret Sandbox staged environment",
    )?;
    verify_no_plaintext("Sandbox OCI configuration", &oci)?;
    verify_no_plaintext("Sandbox staged environment", &staged_environment)?;
    require(
        String::from_utf8_lossy(&oci).contains(&format!("BOX_EXEC_ENV_FILE={RUNTIME_ENV_PATH}")),
        "Sandbox OCI configuration omitted the protected environment staging pointer",
    )?;
    verify_environment_manifest("Sandbox", &staged_environment)
}

fn verify_microvm_secret_persistence(record: &crate::BoxRecord) -> Result<()> {
    let resolved_image = read_regular_file(
        &record.box_dir.join(crate::RESOLVED_IMAGE_CONFIG_FILE),
        "Secret MicroVM resolved image configuration",
    )?;
    let staged_environment = read_microvm_staged_environment(record)?;
    verify_no_plaintext("MicroVM resolved image configuration", &resolved_image)?;
    verify_no_plaintext("MicroVM staged environment", &staged_environment)?;
    verify_environment_manifest("MicroVM", &staged_environment)
}

fn read_microvm_staged_environment(record: &crate::BoxRecord) -> Result<Vec<u8>> {
    let relative = RUNTIME_ENV_PATH.trim_start_matches('/');
    let candidates = [
        record.box_dir.join("merged").join(relative),
        record.box_dir.join("rootfs/.a3s-rootfs").join(relative),
        record.box_dir.join("rootfs").join(relative),
        record.box_dir.join("upper").join(relative),
    ];
    for path in &candidates {
        match std::fs::symlink_metadata(path) {
            Ok(_) => return read_regular_file(path, "Secret MicroVM staged environment"),
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
            Err(error) => {
                return Err(super::external(
                    "inspect Secret MicroVM staged environment",
                    error,
                ))
            }
        }
    }
    Err(super::protocol(format!(
        "Secret MicroVM staged environment is missing below {}",
        record.box_dir.display()
    )))
}

fn read_regular_file(path: &Path, label: &str) -> Result<Vec<u8>> {
    let metadata = std::fs::symlink_metadata(path)
        .map_err(|error| super::external(&format!("inspect {label}"), error))?;
    require(
        metadata.file_type().is_file() && !metadata.file_type().is_symlink(),
        format!("{label} is not a regular file: {}", path.display()),
    )?;
    std::fs::read(path).map_err(|error| super::external(&format!("read {label}"), error))
}

fn verify_no_plaintext(label: &str, bytes: &[u8]) -> Result<()> {
    let value = String::from_utf8_lossy(bytes);
    require(
        !value.contains(SECRET_ENV_VALUE) && !value.contains(SECRET_FILE_VALUE),
        format!("{label} persisted Secret plaintext"),
    )
}

fn verify_environment_manifest(provider: &str, staged_environment: &[u8]) -> Result<()> {
    require(
        String::from_utf8_lossy(staged_environment)
            .lines()
            .any(|line| line.starts_with(&format!("{SECRET_ENVIRONMENT_MANIFEST}="))),
        format!(
            "{provider} staged environment omitted the non-secret environment binding manifest"
        ),
    )
}