cellos-host-gvisor 0.5.0

gVisor runsc backend for CellOS — runs cells in user-space syscall-emulated sandboxes for environments without KVM.
Documentation

cellos-host-gvisor

The gVisor (runsc) [CellBackend] — an alternative to Firecracker for hosts where /dev/kvm is not available.

What it is

cellos-host-gvisor is the skeleton of an L2 host backend that isolates a cell using gVisor's user-mode kernel (runsc) instead of a KVM-backed microVM. It targets environments where Firecracker is not viable: GKE pods (KVM is gated behind a paid kvm feature flag), restricted CI runners (the public GitHub ubuntu-latest image does not expose /dev/kvm), and any host where booting nested virtualization is too costly. gVisor's ptrace / KVM / systrap switches intercept the workload's syscalls and form a defence-in-depth boundary that Linux namespaces alone do not provide. L2 in the layer model is "map an authority bundle to real isolation"; this backend takes the runsc-as-a-syscall-monitor path.

The crate ships in two pieces. Platform-independent: the OCI bundle generator [generate_bundle_config] (src/bundle.rs:103) — a pure function that translates an ExecutionCellDocument into the subset of the OCI runtime spec that runsc actually consumes (process.args, process.cwd, root.path, linux.namespaces). Linux-only: the [GVisorCellBackend] implementation of CellBackend (src/backend.rs:64) that shells out to the runsc CLI. On non-Linux hosts the backend module is cfg'd out and the crate compiles to just the bundle generator (src/lib.rs:36-49), so downstream workspace crates can keep using cellos_host_gvisor::* in Linux-gated blocks without breaking macOS or Windows dev builds.

What it deliberately does not do today (L2-06-5 skeleton scope, called out in the crate-level rustdoc at src/lib.rs:5 and the backend doc-comment at src/backend.rs:1):

  • It does not apply host-side nftables rules. gVisor manages its own netns; the supervisor's host-subprocess fallback surfaces egress signals when the spec declares any (src/backend.rs:141-148).
  • It does not populate kernel_digest_sha256, rootfs_digest_sha256, or firecracker_digest_sha256 on the returned CellHandle — there is no boot artifact manifest in this backend (src/backend.rs:146-149).
  • It does not build, ship, or vendor a runsc binary. The backend resolves the binary at every create() from $PATH (or the CELLOS_GVISOR_RUNSC_BIN override) and surfaces a CellosError::Host if the spawn fails — the supervisor then degrades to whatever fallback the operator wired (typically the stub backend on non-prod hosts).

Public API surface

Item Where
pub fn generate_bundle_config(&ExecutionCellDocument) -> Result<BundleConfig, BundleConfigError> src/bundle.rs:103
pub struct BundleConfig { oci_version, process, root, hostname, linux } src/bundle.rs:51
pub struct Process { terminal, args, cwd, env } src/bundle.rs:62
pub struct Root { path, readonly } src/bundle.rs:69
pub struct LinuxSection { namespaces } src/bundle.rs:77
pub struct Namespace { kind } src/bundle.rs:84
pub enum BundleConfigError { MissingCellId, MissingArgv } src/bundle.rs:26
pub struct GVisorCellBackend (Linux-only) src/backend.rs:64
impl CellBackend for GVisorCellBackend (Linux-only) src/backend.rs:95

The bundle generator requires spec.id non-empty (whitespace-only is also rejected) and spec.run.argv non-empty (src/bundle.rs:106-119). process.cwd defaults to / when spec.run.working_directory is None or an empty string (src/bundle.rs:121-125). The unshared namespace set is the same on every cell: pid, network, ipc, uts, mount (src/bundle.rs:143-159). root.path = "rootfs" and root.readonly = true by construction (src/bundle.rs:137-141).

process.env is intentionally empty — secrets and environment are layered by the supervisor's broker plumbing, not the bundle generator (src/bundle.rs:131-135). The crate enforces #![forbid(unsafe_code)] at the lib root (src/lib.rs:36).

Architecture / how it works

GVisorCellBackend holds a single Arc<Mutex<HashMap<String, TrackedCell>>> — no persistent state. Each tracked entry is the bundle dir we wrote and the tokio::process::Child for the runsc run call (src/backend.rs:52-65).

create(spec) (src/backend.rs:97-150):

  1. Calls generate_bundle_config(spec). Bundle-generation errors map to CellosError::InvalidSpec (src/backend.rs:98-99).
  2. Builds <bundle_root>/<cell_id>/ with a rootfs/ subdirectory and writes config.json (src/backend.rs:103-113).
  3. Spawns runsc run --bundle <bundle_dir> <cell_id> with detached stdin/stdout/stderr (src/backend.rs:115-128) and tracks the child in the in-memory map (src/backend.rs:130-136).

wait_for_in_vm_exit(cell_id) (src/backend.rs:153-177) drains the tracked entry, awaits the child, and returns Some(Ok(exit_code)) (or Some(Err(...)) if the wait() itself fails). Bundle-dir cleanup is owned by destroy(), deliberately — so post-mortem inspection still works between wait and destroy.

destroy(handle) (src/backend.rs:180-218) does best-effort runsc kill <cell_id> SIGKILL followed by runsc delete <cell_id>, removes the bundle dir, and returns a TeardownReport with destroyed: true and peers_tracked_after equal to the remaining in-memory entry count. runsc errors are logged at warn and never fail the teardown — gVisor's own state is the authority on whether the cell exists.

Configuration

Env var Default Effect
CELLOS_CELL_BACKEND proprietary Set to gvisor to select this backend in cellos-supervisor::composition. Non-Linux hosts fail-fast at startup when gvisor is selected (crates/cellos-supervisor/src/composition.rs:1097-1111).
CELLOS_GVISOR_RUNSC_BIN runsc Override the runsc binary path. Consulted at every create() so tests can inject a fake binary without rebuilding (src/backend.rs:45-46, resolved at src/backend.rs:81-83).
CELLOS_GVISOR_BUNDLE_ROOT ${TMPDIR:-/tmp}/cellos-gvisor Override the bundle staging root. The supervisor creates a per-cell subdirectory under this root (src/backend.rs:48-50, resolved at src/backend.rs:85-91).

The backend has no from_env() constructor; it is constructed with GVisorCellBackend::new() (or Default::default()) and reads the env vars lazily inside create(). This matches the supervisor composition call site (crates/cellos-supervisor/src/composition.rs:1103).

Examples

// Pure bundle generation — works on every host, no `runsc` required.
use cellos_host_gvisor::generate_bundle_config;
use cellos_core::ExecutionCellDocument;

fn build_config(doc: &ExecutionCellDocument) -> anyhow::Result<()> {
    let cfg = generate_bundle_config(doc)?;
    assert_eq!(cfg.root.path, "rootfs");
    assert!(cfg.root.readonly);
    Ok(())
}
// Linux-only — driving the real backend.
#[cfg(target_os = "linux")]
{
    use std::sync::Arc;
    use cellos_core::ports::CellBackend;
    use cellos_host_gvisor::GVisorCellBackend;

    let backend: Arc<dyn CellBackend> = Arc::new(GVisorCellBackend::new());
    // Supervisor drives create() / wait_for_in_vm_exit() / destroy()
    // through the trait object.
}

Testing

cargo test -p cellos-host-gvisor

All unit tests live in-source under #[cfg(test)] blocks:

  • src/bundle.rs — bundle generator: happy path, default cwd, empty-string cwd, empty / whitespace cell_id, missing run, empty argv, namespace set, JSON key shape, multi-arg argv round-trip (src/bundle.rs:165-318).
  • src/backend.rs — env-var override resolution for runsc binary and bundle root (src/backend.rs:225-251).

There is no tests/ directory and no #[ignore]-gated suite — the backend skeleton does not yet exercise a real runsc invocation under integration. The next slot (a follow-up to L2-06-5) wires real end-to-end runsc runs, at which point those tests will land gated by #[ignore] because they require Linux + the runsc binary on PATH.

Related crates

  • cellos-core — provides the CellBackend trait, ExecutionCellDocument, and CellosError types this backend implements and returns.
  • cellos-host-firecracker — the KVM-backed alternative selected when CELLOS_CELL_BACKEND=firecracker. Use that one when hardware virt is available; this one when it is not.
  • cellos-host-stub — the no-op backend used in tests where neither runsc nor Firecracker is appropriate.
  • cellos-supervisor — selects backends at startup; the gvisor arm lives at crates/cellos-supervisor/src/composition.rs:1089-1112.

ADRs