Skip to content
Menu

Virtualization7 min read

Sandboxed Runtimes

When shared kernel isn't secure enough — gVisor, Kata, Firecracker, and Wasm

The Security Gap: Shared Kernel

Warning

The fundamental problem: Traditional containers (runc) share the host kernel. A kernel exploit in any container = full host compromise.

Physical Host
Shared Linux Kernel (~300+ syscalls exposed)
Container A

Trusted workload

Container B

Untrusted code / attacker

Container C

Another tenant

Why Namespaces + cgroups Aren't Enough

  • Attack surface: the Linux kernel exposes ~300+ system calls. Many are complex, historically buggy subsystems (BPF, io_uring, futex, etc.)
  • Kernel exploits: a single vulnerability (e.g., Dirty COW, OverlayFS CVEs) can break out of namespaces entirely
  • Multi-tenant danger: if you run untrusted code from different customers on the same kernel, one exploit compromises everyone
  • seccomp helps but is not a security boundary — it filters syscalls but the kernel still processes them

Two Approaches to Solving This

Reduce attack surface

Intercept syscalls in user space so the host kernel sees only a tiny, well-audited set

Approach: gVisor (user-space kernel)

Eliminate kernel sharing

Give each container its own kernel inside a lightweight VM

Approach: Kata Containers, Firecracker

gVisor (runsc) — User-Space Kernel

What gVisor Is

  • Google's sandboxed container runtime (open-source)
  • Implements a subset of the Linux kernel in Go — runs entirely in user space
  • OCI-compatible: just swap runc for runsc (the gVisor binary)
  • Used by: Google Cloud Run, GKE Sandbox

Architecture

Host
Host Kernel (only ~20 syscalls used by Sentry)
Sentry (user-space kernel)

Intercepts and handles all container syscalls in user space. Written in Go. Runs as a regular unprivileged process.

Container

Application thinks it's talking to a real Linux kernel

Gofer (file proxy)

Handles filesystem access via 9P protocol. Isolated from Sentry — separate process, minimal privileges.

How Syscalls Flow

  1. Container App

    Calls read(), write(), open(), etc.

  2. Sentry

    Intercepts syscall, handles in user-space Go code

  3. Host Kernel

    Only ~20 syscalls (read, write, futex, etc.)

Container sees: ~300 Linux syscalls → Host kernel sees: ~20 syscalls from Sentry

Tradeoffs

Advantages
  • Massively reduced kernel attack surface (~20 vs ~300 syscalls)
  • Sentry is written in Go — memory-safe, no buffer overflows
  • No hardware virtualization needed (no KVM)
  • Drop-in replacement for runc (OCI-compatible)
  • Fine-grained syscall compatibility tracking
Disadvantages
  • 2-5x overhead for syscall-heavy workloads (each syscall goes through user-space)
  • Not all syscalls supported (e.g., no io_uring, limited eBPF)
  • Filesystem performance limited by 9P protocol to Gofer
  • Some applications may behave differently (kernel compatibility gaps)
  • Networking has higher latency (netstack in user-space)

Tip

Best for: untrusted code execution, serverless functions, multi-tenant platforms, CI/CD build runners — anywhere you run code you don't fully trust.

Kata Containers — Per-Container Lightweight VMs

What Kata Containers Does

  • Each container runs in its own lightweight virtual machine with its own kernel
  • Combines VM-level isolation with container-level ergonomics (OCI-compatible)
  • Uses QEMU or Firecracker as the VMM (Virtual Machine Monitor)
  • Guest kernel is minimal (~30MB) — just enough to run containers

Architecture

Physical Host + KVM
Lightweight VM (QEMU/Firecracker)
Guest Kernel (~30MB)
kata-agent
Container A

Runs inside VM with dedicated kernel

Lightweight VM (QEMU/Firecracker)
Guest Kernel (~30MB)
kata-agent
Container B

Completely isolated kernel

How It Works

  1. containerd / CRI-O

    CRI request to run container

  2. kata-runtime

    OCI runtime (replaces runc)

  3. QEMU / Firecracker

    Creates lightweight VM with KVM

  4. Guest Kernel

    Minimal Linux kernel boots

  5. kata-agent

    Manages container inside VM via vsock

Performance Characteristics

  • CPU: near-native (hardware virtualization via VT-x/AMD-V)
  • Memory: ~30MB overhead per VM for guest kernel + agent
  • I/O: small overhead for virtio passthrough
  • Startup: ~500ms (VM boot + kernel init + agent)
  • Networking: tap device + virtio-net into VM

Key Components

  • kata-runtime: OCI runtime binary (called by containerd-shim-kata-v2)
  • kata-agent: runs inside the VM, manages the container
  • vsock: host-to-VM communication (no network stack needed)
  • virtiofsd: filesystem sharing between host and guest
  • Guest kernel: stripped-down Linux (~30MB image)

Tip

Best for: workloads needing strong kernel-level isolation with container ergonomics — regulated industries, multi-tenant clouds, running untrusted kernel modules.

Firecracker — Minimal MicroVMs for Serverless

What Firecracker Is

  • Amazon's microVM manager — written in Rust
  • Powers AWS Lambda and AWS Fargate
  • NOT a full QEMU — stripped-down VMM with minimal device model
  • Designed for serverless: fast startup, high density

Key Specifications

Note

<125ms
Boot time

Note

~5MB
Memory overhead per microVM

Note

1000s
microVMs per host

Firecracker vs QEMU

QEMU (full VMM)
  • ~70 emulated devices
  • Legacy hardware support (IDE, PS/2, VGA, PCI bridges...)
  • Huge codebase (~1.4M lines of C)
  • Boot time: seconds
  • Memory overhead: ~100MB+ per VM
  • Attack surface: large
Firecracker (microVMM)
  • ~4 emulated devices only:
    • virtio-net (networking)
    • virtio-blk (block storage)
    • serial console
    • minimal legacy i8042 + RTC
  • ~50K lines of Rust (memory-safe)
  • Boot time: <125ms
  • Memory overhead: ~5MB

How It Integrates

  • Uses KVM under the hood (requires hardware virtualization)
  • Managed via REST API (not OCI directly)
  • Can be used as a Kata Containers backend (instead of QEMU)
  • firecracker-containerd: integration with containerd for running containers in Firecracker microVMs
  1. containerd

    CRI / gRPC API

  2. firecracker-containerd

    Shim that manages Firecracker VMs

  3. Firecracker

    Creates microVM via KVM

  4. Container

    Runs inside microVM

Tip

Best for: serverless platforms (Lambda-style), high-density multi-tenant workloads, any scenario needing thousands of isolated environments with fast cold starts.

WebAssembly (Wasm) Runtimes

Wasm as a Container Runtime

  • Wasm containers via runwasi — a containerd shim for Wasm runtimes
  • Not a VM, not a Linux process — a Wasm module runs in a sandboxed Wasm VM
  • Runtimes: wasmtime (Bytecode Alliance), WasmEdge (CNCF), Wasmer
  • Applications must be compiled to Wasm (from Rust, Go, C/C++, Python, etc.)

Key Characteristics

Strengths

  • Sub-millisecond cold start (<1ms)
  • Tiny footprint (~KB per module, not MB)
  • Sandboxed by design — no access to host unless explicitly granted
  • Portable — same binary runs on any OS/architecture
  • Near-native performance for compute (AOT compiled)

Limitations

  • No Linux syscalls — only WASI (WebAssembly System Interface) APIs
  • Limited filesystem access — must explicitly grant directory access
  • No raw sockets — networking is evolving (wasi-sockets)
  • Thread support — only recently standardized (wasm-threads proposal)
  • Ecosystem maturity — not all libraries/frameworks support Wasm yet

WASI — WebAssembly System Interface

WASI is the standardized system API for Wasm modules — it's what allows Wasm to interact with the outside world:

  • wasi-filesystem — pre-opened directory handles (capability-based)
  • wasi-sockets — TCP/UDP networking (in development)
  • wasi-http — HTTP request/response handling
  • wasi-cli — stdin/stdout/stderr, environment variables, args

Think of WASI as a capability-based POSIX — you only get access to what the host explicitly grants.

Integration with Kubernetes

  1. kubelet

    CRI request

  2. containerd

    Selects Wasm shim

  3. containerd-shim-runwasi

    Wasm-specific shim

  4. wasmtime / WasmEdge

    Executes Wasm module

Tip

Best for: edge computing, plugin systems, serverless functions, CDN workers (Cloudflare Workers, Fastly Compute), anywhere you need sub-millisecond startup with strong sandboxing.

Comparison Matrix — All Runtimes

Runtime Isolation Startup Overhead Syscall Support Best For
runc Namespaces + cgroups ~100ms Minimal Full Linux General purpose
gVisor (runsc) User-space kernel ~200ms 2-5x syscalls Partial (~70%) Untrusted code
Kata Containers MicroVM + own kernel ~500ms ~30MB + VM Full Linux Strong isolation
Firecracker MicroVM (minimal) <125ms ~5MB Full Linux Serverless, high density
Wasm (runwasi) Wasm sandbox <1ms ~KB WASI only Edge, plugins

Note

Tradeoff spectrum: As you move from runc → gVisor → Kata/Firecracker, you get stronger isolation but higher overhead. Wasm breaks the pattern entirely — incredible startup and density, but a fundamentally different programming model (no Linux ABI).

Isolation Models — Side by Side

runc — Shared Kernel
Container

NS + cgroups only

Container

NS + cgroups only

gVisor — User-Space Kernel
Sentry
Container

User-space syscall handling

Kata — Per-Container VM
VM
Guest Kernel
Container
VM
Guest Kernel
Container
Wasm — Sandboxed Module
wasmtime

Wasm sandbox, no kernel access, capability-based WASI

wasmtime

Each module fully isolated, ~KB footprint

Kubernetes RuntimeClass — Per-Pod Runtime Selection

How Kubernetes Selects a Runtime

The RuntimeClass resource lets you run different pods with different runtimes on the same cluster:

console
# Define a RuntimeClass for gVisor
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc   # matches containerd's runtime config name
scheduling:
  nodeSelector:
    runtime/gvisor: "true"   # only schedule on nodes that have gVisor installed
overhead:
  podFixed:
    memory: "64Mi"   # account for gVisor's memory overhead in scheduling
console
# Use it in a Pod spec
apiVersion: v1
kind: Pod
metadata:
  name: untrusted-workload
spec:
  runtimeClassName: gvisor   # use gVisor instead of default runc
  containers:
  - name: sandbox
    image: nginx:latest

Common RuntimeClass Configurations

RuntimeClass NameHandlerRuntimeUse Case
gvisorrunscgVisorUntrusted code, multi-tenant
katakataKata ContainersVM-level isolation
wasmwasmtimerunwasi + wasmtimeWasm workloads
(default / none)runcruncStandard containers

Note

scheduling.nodeSelector ensures pods land on nodes that actually have the requested runtime installed. Without this, a pod requesting gvisor could be scheduled on a node without gVisor, and it would fail to start.

containerd Runtime Configuration

To register alternative runtimes with containerd, add them to /etc/containerd/config.toml:

console
# /etc/containerd/config.toml

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
  runtime_type = "io.containerd.runc.v2"

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
  runtime_type = "io.containerd.runsc.v1"

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata]
  runtime_type = "io.containerd.kata.v2"

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.wasmtime]
  runtime_type = "io.containerd.wasmtime.v1"

Tip

Mix and match: On a single Kubernetes cluster, you can run trusted workloads with runc (lowest overhead), untrusted user code in gVisor (reduced kernel exposure), and regulated workloads in Kata (full kernel isolation) — all selected per-pod via RuntimeClass.

Solidnines — solidnines.com