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, orfirecracker_digest_sha256on the returnedCellHandle— there is no boot artifact manifest in this backend (src/backend.rs:146-149). - It does not build, ship, or vendor a
runscbinary. The backend resolves the binary at everycreate()from$PATH(or theCELLOS_GVISOR_RUNSC_BINoverride) and surfaces aCellosError::Hostif 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):
- Calls
generate_bundle_config(spec). Bundle-generation errors map toCellosError::InvalidSpec(src/backend.rs:98-99). - Builds
<bundle_root>/<cell_id>/with arootfs/subdirectory and writesconfig.json(src/backend.rs:103-113). - 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 generate_bundle_config;
use ExecutionCellDocument;
// Linux-only — driving the real backend.
Testing
cargo test -p cellos-host-gvisor
All unit tests live in-source under #[cfg(test)] blocks:
src/bundle.rs— bundle generator: happy path, defaultcwd, empty-stringcwd, empty / whitespacecell_id, missingrun, emptyargv, namespace set, JSON key shape, multi-arg argv round-trip (src/bundle.rs:165-318).src/backend.rs— env-var override resolution forrunscbinary 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 theCellBackendtrait,ExecutionCellDocument, andCellosErrortypes this backend implements and returns.cellos-host-firecracker— the KVM-backed alternative selected whenCELLOS_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 neitherrunscnor Firecracker is appropriate.cellos-supervisor— selects backends at startup; thegvisorarm lives atcrates/cellos-supervisor/src/composition.rs:1089-1112.
ADRs
- ADR-0001 — Rust + NATS/JetStream + proprietary host — the surrounding stack commitments that frame "swap the L2 backend per host without touching L1/L3".
- ADR-0005 — TLS termination design — TLS termination lives outside the cell; this backend does not attempt in-VM TLS termination because it has no in-VM concept.