# 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](../../LAYERS.md)
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
| `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
| `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
```rust
// 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(())
}
```
```rust
// 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`](../cellos-core) — provides the `CellBackend` trait,
`ExecutionCellDocument`, and `CellosError` types this backend implements
and returns.
- [`cellos-host-firecracker`](../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`](../cellos-host-stub) — the no-op backend used in
tests where neither `runsc` nor Firecracker is appropriate.
- [`cellos-supervisor`](../cellos-supervisor) — selects backends at
startup; the `gvisor` arm lives at
`crates/cellos-supervisor/src/composition.rs:1089-1112`.
## ADRs
- [ADR-0001 — Rust + NATS/JetStream + proprietary host](../../docs/adr/0001-rust-nats-jetstream-proprietary-host.md)
— the surrounding stack commitments that frame "swap the L2 backend
per host without touching L1/L3".
- [ADR-0005 — TLS termination design](../../docs/adr/0005-tls-termination-design.md)
— TLS termination lives outside the cell; this backend does not
attempt in-VM TLS termination because it has no in-VM concept.