Security Notes
Hardening

Sandbox Isolation for Untrusted Workloads: gVisor vs Firecracker vs Kata Containers

35 min read 12 sections 9 model answers verified 2026-10 version 1 · updated 2026-10-15

Last verified2026-10 Also see: Build platform security (caches, BuildKit secrets, SSH into jobs) · Container runtimes overview · Linux hardening · macOS hardening


Start here: the basics in plain words

A normal container is just a Linux process with a restricted view of the system. It sees its own files, its own process list and its own network. But it still talks directly to the host's kernel, the same kernel every other container on that machine uses. If the code inside finds one kernel bug, it owns the machine and every other customer on it.

When you run code you didn't write (customer CI jobs, student code submissions, AI-agent tool calls, serverless functions), you need a second wall. There are three common designs:

gVisor

Google's "fake kernel" written in Go. It answers the program's system calls itself, so the program almost never reaches the real kernel.

Firecracker

Amazon's tiny virtual machine monitor (VMM) written in Rust. Each job gets its own real Linux kernel inside a hardware-isolated microVM.

Kata Containers

A runtime that makes each Kubernetes pod look like a container but actually runs it inside a lightweight VM (using QEMU, Cloud Hypervisor, Firecracker or Dragonball).

runc

The default container runtime. No second wall: the program talks to the host kernel directly. The baseline everything else is compared with.

A system call (syscall) is how a program asks the kernel to do something: open a file, send a packet, start a process. Linux has about 450 of them, and every one is a door into the kernel. Sandboxes differ in who answers those doors:

                runc                  gVisor                 Firecracker / Kata
            ┌──────────┐          ┌──────────┐            ┌──────────────────┐
            │   app    │          │   app    │            │   app            │
            └────┬─────┘          └────┬─────┘            │   guest kernel   │ ← its own Linux
                 │ ~450 syscalls       │ ~450 syscalls    └────────┬─────────┘
                 │                ┌────▼─────┐                     │ virtio devices,
                 │                │  Sentry  │ ← Go "kernel"       │ VM exits
                 │                └────┬─────┘                ┌────▼─────┐
                 │                     │ few dozen host       │   VMM    │ ← Rust (Firecracker)
                 │                     │ seccomp-filtered     └────┬─────┘   or C (QEMU)
            ┌────▼─────────────────────▼──────────────────────────▼─────┐
            │                      host Linux kernel (+ KVM)             │
            └────────────────────────────────────────────────────────────┘
Memory hook

"who answers the syscall?" runc: the host kernel. gVisor: a Go program pretending to be a kernel. Firecracker/Kata: a real guest kernel, with hardware keeping it in a box. Every security and performance difference below follows from that one question.


Threat Model and Boundary Architecture

What is this?The threat model says what the attacker controls and what they want. For untrusted workloads the attacker controls all code inside the sandbox, often as root, and wants one of three things: the host, another tenant's data, or the platform's credentials.

Why it mattersInterviewers use this topic to see if you know that "container" and "security boundary" aren't the same word. The strong answer names the attack surface each design exposes, not just "VMs are safer".

How it works

runc (namespaces + cgroups + seccomp)
  • Namespaces hide things; cgroups limit resources; seccomp blocks some syscalls.
  • Attack surface: the whole host kernel minus what seccomp blocks. Docker's default seccomp profile still allows over 300 syscalls.
gVisor (runsc)
  • The Sentry is a user-space kernel in Go. It implements Linux syscalls, the virtual filesystem, memory management and a full TCP/IP stack (Netstack).
  • The Gofer is a separate host process that opens files on the Sentry's behalf. It speaks LISAFS (which replaced 9P in 2022) over a Unix domain socket.
  • With directfs (default since 2023) the Gofer hands the Sentry file descriptors once at mount time, and the Sentry then opens files itself, under a seccomp filter, from inside an empty mount namespace.
  • The Sentry itself runs under a strict seccomp filter: by default it can't open host files or create host sockets.
Firecracker
  • One Firecracker process per microVM. It uses Linux KVM (Kernel-based Virtual Machine) so the CPU's virtualisation hardware separates the guest from the host.
  • It emulates very few devices: virtio-net, virtio-block, virtio-vsock, virtio-balloon, virtio-rng, a serial console and a minimal keyboard controller used only for reset. No USB, no graphics, no virtio-fs.
  • The jailer wraps each Firecracker process in a chroot, its own namespaces and cgroups, drops privileges to an unprivileged user, and Firecracker then installs its own seccomp filters per thread.
Kata Containers
  • Speaks the OCI (Open Container Initiative) runtime interface, so Kubernetes and containerd treat it like runc.
  • Inside each VM runs a guest kernel plus the kata-agent, which starts the actual containers. The host side talks to the agent over vsock.
  • Container files reach the guest either through virtio-fs (a host daemon, virtiofsd, serves the host directory) or, with Firecracker, through block devices (devmapper), because Firecracker has no virtio-fs.
🎯

On the job. When someone says "we sandbox it", ask three questions: what runs the syscalls, what host process sits on the other side of the wall, and what privileges does that host process have. The answer to the third one is usually where the next escape lives.


Attack Surface and Mediation Layers

What is this?A mediation layer is any code that receives input from the untrusted side and acts on the host side. Each one is an attack surface.

Why it matters"VMs are safer" is only true if the code on the host side of the wall is small and careful. QEMU's floppy controller (VENOM, CVE-2015-3456) proved that a big device model is a big target.

How it worksEach design's host-side surface, smallest first:

LayergVisorFirecrackerKata (QEMU + virtio-fs)
First thing the attacker hitsSentry (Go, memory-safe)Guest kernel (C, attacker gets root easily)Guest kernel
Second wallHost kernel via a few dozen filtered syscallsVMM device emulation (Rust) + KVMVMM (QEMU, C) + KVM
File access pathGofer / directfs (Go)virtio-block: raw blocks onlyvirtiofsd (Rust) serving a host directory
Network pathNetstack inside Sentryvirtio-net → host tap devicevirtio-net → host tap device
Host process privilegeUnprivileged, seccompJailer: unprivileged, chroot, seccompDepends on config; virtiofsd often root

Key ideas behind the table:

Memory safety matters on the host side.

gVisor's Sentry is Go and Firecracker is Rust, so the classic C bug classes (use-after-free, buffer overflow) are rarer. Not zero: Rust unsafe blocks and logic bugs still happen (see CVE-2026-5747 below).

Block devices are simpler than shared filesystems.

virtio-block passes raw sectors; the host never interprets a path. virtio-fs passes file operations with paths, symlinks and permissions, and the host daemon must resolve them safely. Path handling is where shared-filesystem escapes come from.

Two walls beat one strong wall.

gVisor inside a VM, or Firecracker under the jailer, means an attacker needs two unrelated bugs.

In practiceHow to tell, from inside a job, which sandbox you're in:

text
$ uname -r                         # inside gVisor
4.4.0                              ← gVisor reports a fixed, old-looking version
$ dmesg | head -3
[   0.000000] Starting gVisor...   ← the Sentry's own fake boot log
[   0.412345] Reticulating splines...
[   0.723456] Ready!

$ cat /sys/class/dmi/id/sys_vendor  # inside Firecracker: no DMI tables at all
cat: /sys/class/dmi/id/sys_vendor: No such file or directory
$ ls /sys/bus/virtio/devices/
virtio0  virtio1                   ← only a network and a block device
🎯

On the job. In a design review of a Kata deployment, check virtiofsd's flags: is --sandbox set (it chroots or namespaces itself), is seccomp on, and does it run as root? CVE-2026-47243 below was exactly "root, sandbox off, seccomp off".


Escape History and Vulnerability Case Studies

Last verified2026-10

What is this?A "sandbox escape" is when code that is supposed to stay inside the sandbox manages to do something outside it: read the host's files, run a program on the host, or reach another customer. This section walks through real escape bugs, one at a time, assuming you've never seen any of them.

Why it mattersIn an interview, "VMs are safer than containers" is a weak answer. A strong answer is "here is a real bug from this year, here is the exact piece of code that failed, and here is the setting that would have limited the damage". You can only say that if you understand each bug in plain terms.

First, the idea every escape uses

Think of the sandbox as a prison cell with a hatch in the door. The prisoner can't walk out, but they can pass notes through the hatch: "please give me this file", "please send this packet". A guard on the other side reads each note and acts on it.

  • The prisoner is the untrusted code.
  • The notes are requests: system calls, disk reads, file operations.
  • The guard is the host-side program that handles them: gVisor's Sentry or Gofer, Firecracker itself, or Kata's file server virtiofsd.

Nobody escapes by breaking the wall. They escape by writing a note the guard misunderstands. If the guard misreads a note and does something harmful, the prisoner gets whatever power the guard has. That's why the guard's own privileges matter so much: a guard who is locked in a small room themselves (an unprivileged user, in a chroot, with seccomp) can't do much harm even when fooled. A guard with the keys to the whole building (root, no sandbox) can.

  1. 1. Become root inside the sandboxattackusually easy
    • Inside a VM the attacker controls the guest's own kernel; this is the normal starting point, not an achievement
  2. 2. Find the guardattackthe host-side code that reads your requests
    • Firecracker reading device settings, virtiofsd reading file requests, the Sentry reading syscalls
  3. 3. Send a request the guard mishandlesattackthe bug
    • A number it doesn't check (memory corruption), or a file path it follows to the wrong place
  4. 4. Act with the guard's powerattackthe escape
    • Guard is an unprivileged, jailed user → little gained; guard is root with no sandbox → the whole host

Firecracker: a device setting changed at the wrong time (CVE-2026-5747)

The pieces you need to know

  • A virtual machine needs a network card and a disk, but they're not real hardware. Firecracker pretends to be them. Those pretend devices use a standard design called virtio.
  • The guest and Firecracker exchange data through shared queues: lists in memory saying "here's a packet to send", "here's a block to read". Before using a device, the guest tells Firecracker where each queue is and how big it is, by writing to the device's configuration registers (think of them as the device's settings form).
  • There are two ways for the guest to reach those settings, called transports. The old default is MMIO (memory-mapped I/O). Firecracker 1.13 added an optional, faster one: PCI (the same bus real network cards use), switched on with --enable-pci.
  • An out-of-bounds write means a program writes data past the end of the memory it set aside. It overwrites whatever happens to be next to it, which can crash the program or, if the attacker controls what's written, take over the program.

What went wrongThe rules say the guest sets up queues, then "activates" the device, and doesn't touch those settings again. With the PCI transport, Firecracker didn't stop a guest that broke that rule. A guest root user could activate the device and then change the queue settings, for example its size. Firecracker kept using its old assumptions about the queue, and ended up writing outside the memory it had reserved.

Why it's badThe bad write happens inside the Firecracker process, which is on the host side of the wall. At minimum the guest can crash its own VMM. With extra conditions (AWS named a custom guest kernel or specific snapshot setups) it could potentially run code on the host as the Firecracker process.

📰

Real incident — Firecracker virtio-pci out-of-bounds write (CVE-2026-5747, April 2026). AWS bulletin 2026-015: affected Firecracker 1.13.0–1.14.3 and 1.15.0 on x86_64 and aarch64; fixed in 1.14.4 and 1.15.1. Only the opt-in PCI transport was affected; the default MMIO one wasn't, and AWS said none of its own services were. Lesson: Firecracker is small and written in Rust, a language that prevents most memory bugs, and it still got one in a new, optional feature. Every feature you turn on is new code the guest can talk to. And this is where the jailer earns its keep: it runs each Firecracker process as an unprivileged user inside a chroot with seccomp, so even if the attacker takes over Firecracker, they're a user who can barely do anything on the host.

Kata: a settings field that let tenants pass orders to the host (CVE-2026-44210)

The pieces you need to know

  • In Kubernetes, anyone allowed to create a pod can attach annotations to it: free-form key: value labels. Kata reads some of them to let a pod tune its VM, for example "give my VM these extra kernel options".
  • virtiofsd is the program on the host that shares a folder with the VM (this is how the container's files appear inside the VM). Like any program, it takes command-line flags that decide its behaviour, including which host folder to share.
  • "Argument injection" means an attacker gets to add their own flags to a command that someone else runs.

What went wrongBefore Kata 3.31.0, the default configuration accepted an annotation called virtio_fs_extra_args, whose whole purpose was "add these flags to virtiofsd". So a tenant could write, in their own pod definition, flags that told the host's virtiofsd to share a different folder, the host's root folder /, instead of the container's files. A second default-allowed annotation, kernel_params, could switch on Kata's debug console inside the VM, giving an easy shell to use it.

Why it's badNo hacking skill was needed: no memory corruption, no exploit code, just a pod with two annotations. From inside the VM, the attacker could then read or change any host file, such as /etc/shadow (the host's password hashes).

📰

Real incident — Kata virtiofsd argument injection (CVE-2026-44210, May 2026). Fixed in Kata 3.31.0 by no longer allowing those annotations by default. Lesson: the wall between guest and host isn't only code, it's also every setting the tenant is allowed to choose. If a customer can type something that ends up as a flag on a root program on the host, that's a hole in the wall, however good the hypervisor is.

Kata: the file server followed a shortcut out of its folder (CVE-2026-47243)

The pieces you need to know

  • FUSE (Filesystem in Userspace) is a protocol for "please do this file operation": open, read, create, rename. virtiofsd receives FUSE requests from the VM and performs them on the host's disk.
  • A symlink (symbolic link) is a file that just points to another path, like a desktop shortcut. Programs that follow symlinks without care can be led to a place they never meant to touch.
  • Path traversal is the general name for tricking a program into working on a path outside the folder it should be limited to.
  • /etc/cron.d/ is a host folder where every file is a scheduled job that the host runs as root.

What went wrongIn Kata's newer Rust runtime (runtime-rs) with the "standalone" virtio-fs setup, virtiofsd ran as root, with its own sandbox switched off and seccomp switched off. A root user in the guest could hand-craft FUSE requests that made virtiofsd create symlinks pointing outside the shared folder, for example into /etc/cron.d. Writing through them dropped a scheduled job onto the host.

Why it's badThe host's cron then ran the attacker's command as root. That's a complete escape: from inside the VM to full control of the host and every VM on it.

📰

Real incident — Kata runtime-rs virtiofsd escape (CVE-2026-47243, May 2026). Fixed in Kata 3.31.0. Lesson: sharing a folder means the host follows file paths that the guest chose. Getting that perfectly right is hard, so you need a second line of defence. Had virtiofsd run with its sandbox on (locked into the shared folder) and seccomp on, the same mistake would have hit a wall.

gVisor: which kinds of bugs matter

gVisor has fewer published escapes, and its bugs fall into different shapes. In the prison analogy, gVisor has two guards, and both are locked in small rooms.

The Sentry (the fake kernel)

It's written in Go, which prevents most memory-corruption bugs, so its bugs are usually logic mistakes ("forgot to check permission X"). Even when an attacker takes over the Sentry, the Sentry is trapped by a seccomp filter that allows only a few dozen host system calls, and it sits in an empty view of the filesystem. The attacker then needs a second bug, in the host kernel, through that tiny opening.

The Gofer (the file helper)

It opens files on the host for the Sentry, so it can have the same kind of "followed a path to the wrong place" bug as virtiofsd above. The difference is in the guard's room: the Gofer is confined to the container's own root folder and filtered by seccomp, so a confused Gofer can't reach /etc/cron.d.

The KVM platform

In this mode gVisor uses the CPU's virtualisation hardware and the Sentry acts as a small guest kernel. Bugs in how it handles low-level CPU state matter here. gVisor's own security guide cites POPSS (CVE-2018-8897), a case where most operating systems mishandled a quirk of the MOV SS CPU instruction, as the kind of hardware-level flaw that affects sandboxes and hypervisors alike.

🎯

On the job. Two checks you can do today. First, for Kata: open configuration.toml and look at enable_annotations. That list is "settings tenants may change on the host"; virtio_fs_extra_args and kernel_params shouldn't be on it. Second, for every runtime: find the host-side "guard" process (firecracker, virtiofsd, runsc-gofer) with ps -o user,args and confirm it isn't root and is sandboxed. Watch each project's GitHub Security Advisories page.

In practiceThe Kata setting behind CVE-2026-44210:

toml
# /opt/kata/share/defaults/kata-containers/configuration-qemu.toml (pre-3.31-style defaults)
[hypervisor.qemu]
shared_fs = "virtio-fs"
enable_annotations = ["virtio_fs_extra_args", "kernel_params"]   ← tenants can pass host flags

# Hardened
enable_annotations = []                                          ← tenants tune nothing on the host
virtio_fs_extra_args = ["--thread-pool-size=1", "--announce-submounts"]

Tracing a Kernel Exploit Through Each Sandbox

What is this?A "kernel exploit" attacks a bug in Linux itself, the core program that controls the machine. Whoever controls the kernel controls everything on it. Here we take two real kernel bugs and ask, for each sandbox: which kernel does the attack actually hit, and what does the attacker get?

Why it mattersThis is the clearest way to show you understand the difference between the sandboxes. runc, gVisor and VMs give three different answers to the same attack, and the reasons are the core of this whole page.

Dirty Pipe (CVE-2022-0847)

The pieces you need to know

  • A pipe is a kernel feature for passing data between programs, as in cat file | grep x.
  • The page cache is the kernel's memory copy of files that were recently read. When anyone reads /usr/bin/sudo, the kernel keeps it in memory, and the next program to run sudo gets that in-memory copy. There's one page cache per kernel, shared by every program and every container on it.
  • splice() is a shortcut system call that moves file data into a pipe without copying it, by pointing the pipe straight at page-cache memory.

What went wrongA flag inside the pipe code wasn't reset properly. After a splice(), an attacker could write into the pipe and the kernel would write those bytes straight into the page-cache copy of a file, even a file the attacker only had permission to read. The file on disk didn't change, but every program using the in-memory copy saw the attacker's version.

Why it's badOverwrite the in-memory copy of a root-owned program, or of the password file, and you become root. From a container it's worse: the page cache belongs to the host kernel, so a container can change files the host and other containers are using.

  1. runc: works against the hostattackthe container's syscalls go to the host kernel
    • The page cache is the host's, shared with every container on the machine
    • Researchers showed container escapes by overwriting the host's runc binary this way
  2. gVisor: failsdefendthe Sentry has its own pipe code and its own page cache
    • The attacker's splice() is answered by Go code that never had the bug; the host's pipe code is never reached
  3. Firecracker / Kata: works, but only inside the VMattackthe guest has its own kernel
    • The guest kernel has the same bug, so the attacker becomes root in the VM
    • The host's page cache is untouched; escaping now needs a second bug, such as CVE-2026-5747, which starts from exactly this "guest root"

An RxRPC out-of-bounds write

The pieces you need to know

  • RxRPC is an old, rarely used network protocol (it carries the AFS network filesystem). Almost nobody needs it, but its code ships with Linux, and it has had several memory bugs over the years.
  • Linux loads optional code (modules) on demand. On many distributions, simply asking to open an RxRPC socket, socket(AF_RXRPC, …), makes the kernel load the RxRPC module for you.
  • So any program can drag a rarely tested piece of kernel code into memory and then send it crafted packets that trigger the out-of-bounds write (writing past the end of a buffer, as explained above).

Why it's a good exampleIt shows that a big kernel's danger is all the code you don't use but can still reach.

  1. runc: works unless blockedattackthe host kernel loads RxRPC on request
    • Stopped only if the container's seccomp profile blocks that socket type, or the host has disabled the module
  2. gVisor: fails at the first stepdefendgVisor's network stack has no RxRPC at all
    • socket() just returns an error ("address family not supported"); the host kernel never hears about it
  3. Firecracker / Kata: usually failsdefendsmall guest kernels are built without RxRPC
    • If the guest kernel does include it, the attacker only gets root inside the VM, as with Dirty Pipe
Memory hook

"gVisor removes the bug; a VM contains the bug." Under gVisor the vulnerable kernel code is simply not in the path, so most kernel exploits fail outright. Under a VM the exploit succeeds, but against a throwaway kernel inside a box. That's why the strongest setups combine both, like GKE Sandbox running gVisor on nodes that are themselves VMs.

📰

Real incident — runc /proc/self/exe (CVE-2019-5736). runc is the program that starts containers on the host. A malicious container could open the runc program itself through a special path (/proc/self/exe, which always points to "the program running me") and overwrite it. The next time anyone started a container, the host ran the attacker's code as root. Under gVisor or a microVM the same trick only reaches the sandbox's own files. Lesson: with plain containers, even the tool that builds the wall is within the attacker's reach.


Hardware Virtualisation and Platform Diversity

Last verified2026-10

What is this?The sandboxes depend on CPU features, and fleets mix CPUs: AMD EPYC and ARM64 servers (AmpereOne) on bare-metal racks, plus Apple Silicon Macs for macOS builds.

Why it matters"Just use Firecracker" fails if your hosts are cloud VMs without nested virtualisation, or Macs. The right answer depends on the hardware.

How it works

  • KVM on AMD EPYC (x86_64) — AMD-V with nested page tables. Firecracker, Kata and gVisor-KVM all work. Optional AMD SEV-SNP (Secure Encrypted Virtualization) encrypts guest memory so even the host can't read it; Kata supports it for confidential containers.
  • KVM on AmpereOne (ARM64) — Arm virtualisation extensions (the hypervisor runs at exception level EL2). Firecracker, Kata (QEMU, Cloud Hypervisor) and gVisor (systrap and KVM) all support arm64. AmpereOne cores are single-threaded (no SMT), which removes a whole class of cross-hyperthread leaks.
  • Cloud VMs as hosts (AWS/GCP) — Firecracker and Kata need /dev/kvm, so they need bare-metal instances (e.g. AWS .metal) or nested virtualisation (GCP supports it on many machine types). gVisor's systrap platform needs no virtualisation at all, which is why it fits ordinary cloud VMs.
  • Apple Silicon (M4 / M5 Max) with Apple Virtualization (vz)
    • Apple's Virtualization.framework sits on Hypervisor.framework. It runs each VM in a sandboxed per-VM helper process and offers mostly virtio devices.
    • macOS guests: Apple's licence allows at most two extra macOS VMs per Mac, and the framework enforces it. Density is therefore 2 macOS jobs per machine, no matter how much RAM it has.
    • Linux guests: no such limit; density is bounded by RAM and cores. Nested virtualisation (KVM inside a Linux guest) needs M3 or later with macOS 15, so M4/M5 can run Firecracker inside a Linux VM on a Mac.
    • Automation: image cloning uses APFS copy-on-write; VMs boot in seconds, but there is no Firecracker-style snapshot restore at fleet scale, and host re-imaging between tenants is part of the boundary.
Linux KVM (EPYC / AmpereOne)Apple vz (M4 / M5 Max)
Jobs per hostHundreds of microVMs2 macOS VMs; Linux VMs RAM-bound
Hypervisor surfaceKVM (open source) + small VMMApple's closed hypervisor + virtio devices
Patch pathYou patch the host kernel and VMMApple ships it in macOS updates
Boot / restore~125 ms boot, ms-scale snapshot restoreSeconds; clone-and-boot
Tenant reuseThrow away the microVMOften re-image the whole Mac between tenants
🎯

On the job. On a macOS fleet, the second VM slot is the multi-tenancy question. If two customers share one Mac's two slots, a hypervisor bug or a leftover file in a shared cache crosses tenants. Many platforms give each tenant the whole Mac for the lease and wipe it between leases.


Side Channels: What No Sandbox Stops

What is this?Every escape above needed a software bug. A side channel needs none. Two programs running on the same CPU share parts of it, and one can learn the other's secrets by watching how the shared hardware behaves, not by reading the data directly.

An everyday picture: you share an office kitchen with a stranger. You can't open their lunchbox, but if the microwave is warm and smells of curry, you know what they ate. CPUs have their own "warm microwaves":

Caches

small, fast memory that keeps recently used data. If your program can tell that reading some address was fast, it knows someone else recently used it.

Speculative execution

to go faster, a CPU guesses what the program will do next and runs ahead. Wrong guesses are thrown away, but they leave traces in the cache. Spectre (2018) showed an attacker can steer those guesses so the CPU briefly touches a secret, then read the trace back.

Hyperthreads (SMT, simultaneous multithreading)

one physical core pretends to be two "logical" CPUs that run at the same time and share almost everything inside the core. Two tenants on sibling hyperthreads are as close as two programs can get.

Why it mattersEvery sandbox here, gVisor, Firecracker and Kata, runs the untrusted code directly on the real CPU, so all of them share the CPU with their neighbours. gVisor's own security guide says plainly that it doesn't stop hardware side channels, and a VM boundary doesn't either. This is the one class of attack where "use a stronger sandbox" doesn't help; you have to change where code runs.

How it works

Same for all three

Defences live on the host: up-to-date CPU firmware (microcode), the kernel's built-in mitigations (names you'll see in output: retpoline, IBRS, IBPB, STIBP), and guest kernels with mitigations left on.

Sharing a core is the big risk

Zenbleed (CVE-2023-20593) is the clearest example. On AMD Zen 2 CPUs, a bug in how the core cleaned up after a wrong guess let one program read fragments of the CPU registers of whatever else ran on that core, including other VMs. Registers hold data being worked on right now: passwords, keys. Intel had similar problems (MDS, L1TF).

Each CPU family has its own variants

Branch History Injection (CVE-2022-0001) affected Intel; AMD had Inception (CVE-2023-20569). You don't need the details, just the pattern: new variants appear every year, so the host must be patched continuously.

What to configure:

AMD EPYC

Either turn hyperthreading (SMT) off (Firecracker's production host guide recommends it), or use core scheduling: a Linux feature that only lets threads from the same tenant share a physical core. Pin each microVM to whole cores (both siblings), and never place two tenants on sibling threads.

AmpereOne

No SMT, so sibling sharing doesn't exist. Still keep Spectre-v2 mitigations on, and still don't share last-level cache partitions across tenants if the workload is sensitive.

gVisor specifically

Under systrap, the Sentry and the app run as separate host processes, so host process-level mitigations apply. Its main extra benefit is that kernel-based exploits needing direct host syscalls get harder.

In practice

text
$ cat /sys/devices/system/cpu/smt/control
on                                  ← EPYC host with SMT on: needs core scheduling or pinning per tenant
$ grep . /sys/devices/system/cpu/vulnerabilities/* | head -4
…/vulnerabilities/retbleed:Mitigation: untrained return thunk; SMT enabled with STIBP protection
…/vulnerabilities/spec_rstack_overflow:Mitigation: Safe RET
…/vulnerabilities/spectre_v2:Mitigation: Retpolines; IBPB: conditional; STIBP: always-on
…/vulnerabilities/mds:Not affected

Performance Profiling and Bottlenecks

Last verified2026-10

What is this?Where each sandbox loses speed, and what that means for picking one.

Why it mattersSecurity people who pick a sandbox that makes builds three times slower lose the argument. You need to know which workloads suffer.

How it works

runcgVisor (systrap)gVisor (KVM)FirecrackerKata (QEMU)
Extra memory per sandbox~0Tens of MB (Sentry)Tens of MB< 5 MiB VMM + guest kernelTypically 30 MB+ (QEMU + guest + agent)
Start timems~100s of ms~100s of ms~125 ms boot; ms-scale snapshot restore~0.5–1 s typical
CPU-bound codeNativeNativeNativeNativeNative
Syscall-heavy codeNativeSlower: each syscall is trappedFaster on bare metalNative inside guestNative inside guest
Small-file I/ONativeSlowest case (VFS in Sentry)SameFast (block device)virtio-fs round trips
gVisor's penalty is per syscall, not per instruction.

Pure computation runs at native speed. A Docker BuildKit build compiling thousands of small C files, or npm install writing 50,000 files, makes millions of open/stat/write calls and is where gVisor hurts most.

Platforms

gVisor's original ptrace platform trapped each syscall with PTRACE_SYSEMU and was slow; it's deprecated. Systrap (default since mid-2023) uses seccomp to trap syscalls and needs no hardware support. The KVM platform runs the Sentry as a guest kernel and is fastest on bare metal, but slow under nested virtualisation.

When Firecracker is strictly better

I/O-heavy and syscall-heavy builds on bare metal, workloads needing full Linux behaviour (Docker-in-Docker, eBPF, unusual syscalls, kernel modules), and jobs where a 5 MiB footprint and snapshot restore matter.

When gVisor is better

hosts without /dev/kvm, mostly CPU-bound or network-light code, and when you want to drop entire kernel subsystems from the attack surface.

In practiceWhat a gVisor penalty looks like in a build (illustrative timings; measure your own workload):

text
$ time npm ci          # runc
real    0m21.4s
$ time npm ci          # runsc, systrap, overlay rootfs
real    0m48.9s        ← ~2× slower: ~1.2M file syscalls answered by the Sentry
$ time make -j32       # CPU-bound C compile, same tree
real    3m02s vs 3m09s ← almost no difference once the work is in the CPU

Network Isolation and Egress Filtering

What is this?How each sandbox's network traffic leaves the machine, and how you control where it goes.

Why it mattersMost real damage from untrusted code isn't an escape, it's the network: stealing cloud credentials from the metadata service, scanning the internal network, or sending stolen secrets home.

How it works

gVisor Netstack

TCP/IP runs inside the Sentry, in Go. The host kernel only moves raw packets, so host TCP/IP bugs aren't reachable. (--network=host hands sockets back to the host kernel and removes this benefit.)

Firecracker / Kata

The guest kernel's TCP/IP talks to a virtio-net device, which the VMM connects to a tap device on the host. Everything after that is normal host routing and firewalling (nftables, eBPF).

A good egress design for multi-tenant jobs:

  1. Job sends trafficattackany destination
    • Guest has a default route only to the platform's gateway
  2. Host firewall per VMnetworknftables on the tap device
    • Drop 169.254.169.254, host IPs, other VMs, internal ranges
    • Allow only the egress proxy
  3. Egress proxydefendper-tenant policy
    • Allow-list by hostname (CONNECT / TLS SNI); log every destination
  4. Dedicated egress IP poolserverper tenant or per plan
    • Customers allow-list these IPs in their own firewalls
    • One tenant's abuse doesn't get another tenant's IPs blocked

In practiceThe host-side rules for one Firecracker VM's tap device:

text
table inet vm_egress {
  chain fwd {
    type filter hook forward priority 0; policy drop;
    iifname "tap-job812" ip daddr 169.254.169.254 drop     ← no cloud metadata
    iifname "tap-job812" ip daddr 10.0.0.0/8 drop           ← no internal network
    iifname "tap-job812" ip daddr 10.255.0.10 tcp dport 3128 accept   ← egress proxy only
  }
}
🎯

On the job. From a test job, run curl -m 2 http://169.254.169.254/latest/meta-data/ and curl -m 2 https://example.com without the proxy. Both should fail. Then check the proxy's logs show your test job's tenant ID next to each destination: without that, you can't investigate an exfiltration.


In Practice: Hardening Multi-Tenant CI/CD Workloads

What is this?Putting it together for a platform that runs customers' GitHub Actions and Buildkite jobs, including Docker BuildKit and shell builds, on bare-metal Kubernetes.

Why it mattersThis is the system-design version of the question, and it's where interviewers check you can connect sandboxes, identity and network.

How it works

  1. Job arrivesserverGitHub Actions webhook / Buildkite job
    • Controller requests a just-in-time runner for this one job
  2. Mint an ephemeral runner tokentlssingle use
    • GitHub: generate-jitconfig API returns a config valid for one job
    • Buildkite: agent token + --disconnect-after-job; each job gets its own job token
  3. Start a pod with a locked-down RuntimeClassosone VM per job
    • kata-fc (Kata + Firecracker) on bare-metal nodes; gVisor where KVM isn't available
    • Dedicated node pool; never schedule two tenants into one VM
  4. Customer code runs as root in the VMattackBuildKit, shell, C toolchains
    • Rootless BuildKit inside the VM; no host Docker socket, no privileged entitlements
  5. Job needs cloud accesstlsOIDC, not stored keys
    • Job requests an OIDC token; customer's cloud exchanges it for minutes-long credentials
  6. TeardowndefendVM destroyed, token already expired
    • Unique OS credentials per job, nothing reused; logs and artifacts stored per tenant

In practiceThe Kubernetes side:

yaml
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: kata-fc
handler: kata-fc                     # containerd maps this to Kata + Firecracker
scheduling:
  nodeSelector: { pool: untrusted-metal }
  tolerations: [{ key: untrusted, operator: Exists, effect: NoSchedule }]
---
apiVersion: v1
kind: Pod
metadata: { name: job-812, namespace: tenant-acme }
spec:
  runtimeClassName: kata-fc          ← the line that makes this a VM, not a container
  automountServiceAccountToken: false  ← the job must not get a Kubernetes API token
  containers:
    - name: runner
      image: registry.internal/runner@sha256:4c1e…
      env: [{ name: RUNNER_JITCONFIG, valueFrom: { secretKeyRef: { name: job-812-jit, key: cfg } } }]
      resources: { limits: { cpu: "8", memory: 16Gi } }

Things that commonly go wrong:

Long-lived registration tokens

A reusable runner registration token lets an attacker register their own runner and receive other jobs (including secrets). Use just-in-time, single-job tokens.

Kubernetes API reachable from the job

A mounted service account token, or network access to the API server, turns a job into a cluster attacker. Turn off the token and block the API at the network layer.

"Debug SSH" sidecars

An sshd in the job VM for debugging must accept only the triggering user's registered keys, with a time limit (see build platform security).

Platform TLS

Certificates for the platform's own endpoints (for example, issued automatically with a library such as certmagic) must live on control-plane hosts, never on job nodes.

🎯

On the job. Review the runner controller's code (often Go): how is the JIT token passed to the pod, does it ever land in logs, and is the Kubernetes Secret deleted when the job ends? A token printed in a pod event or a debug log is a token anyone with log access can replay until it's used.


Runtime Decision Matrix

Last verified2026-10

What is this?A way to pick a sandbox per workload, instead of one answer for everything.

WorkloadRecommendedWhy
Your own trusted servicesrunc + seccomp + AppArmor/SELinuxSpeed; the code isn't hostile
Untrusted code on cloud VMs without KVMgVisor (systrap)No hardware virtualisation needed; big kernel surface removed
Hostile multi-tenant CI, bare metalFirecracker (directly or via Kata)Hardware boundary, small Rust VMM, fast boot
Kubernetes-native platform needing full LinuxKata (Cloud Hypervisor/Firecracker)Pod semantics with a VM per pod
AI-agent tool calls, short code snippetsgVisor or Firecracker snapshot restoreFast start, no network by default
macOS buildsApple vz VMs, one tenant per MacLicence caps density at 2; re-image between tenants
Regulated / highest value tenantsDedicated hosts, plus any of the aboveNo shared hardware means no side channels

Operational trade-offs to say out loud:

Prerequisites

Firecracker/Kata need /dev/kvm (bare metal or nested virt). gVisor doesn't.

Compatibility

gVisor implements most but not all syscalls; some tools (eBPF, unusual ioctls, kernel modules) won't work. VMs run a real kernel.

Who you're trusting

gVisor: Google's Sentry. Firecracker: a small Rust VMM plus KVM. Kata: whichever VMM you choose plus virtiofsd, and your own configuration.

Compliance

Auditors usually accept "hardware virtualisation per tenant" more easily than "user-space kernel", even where the risk analysis is similar. Know which story you need to tell.


Interview Questions

Q
How does gVisor's Sentry intercept and emulate Linux syscalls, and why is that a different attack surface from a hardware VMM like Firecracker?
Model answer

gVisor traps every syscall the app makes, using seccomp on the default systrap platform or VM exits on the KVM platform, and the Sentry, a kernel written in Go, answers it in user space, so the app never reaches the host kernel directly and the Sentry itself may only use a few dozen filtered host syscalls. Firecracker does the opposite: the app talks to a real guest Linux kernel, and the attack surface is the boundary below that kernel, meaning KVM and a handful of virtio devices emulated in Rust. So gVisor removes kernel code from reach, while Firecracker contains a fully exploitable kernel inside a hardware box; that's why Dirty Pipe fails outright under gVisor but works inside a Firecracker guest and gives only guest root.

Q
Compare Kata's virtio-fs attack surface with gVisor's Gofer. What has historically caused escapes?
Model answer

Both let the sandbox ask a host process to do file operations by path, so both are only as safe as that process's path handling and privileges. In Kata, CVE-2026-47243 had virtiofsd running as root with its sandbox and seccomp off, so a guest root user sent raw FUSE requests creating symlinks into /etc/cron.d and ran code as host root; CVE-2026-44210 let anyone creating a pod inject virtiofsd flags through a default-allowed annotation and expose the host root filesystem. gVisor's Gofer is chrooted to the container root and seccomp-filtered, and with directfs the Sentry opens files itself inside an empty mount namespace, so a path bug there gets far less. The fix pattern is the same everywhere: unprivileged, sandboxed file daemons, or block devices instead of shared filesystems.

Q
When does gVisor perform badly, how do the platform modes change that, and when is Firecracker strictly better?
Model answer

gVisor's cost is per syscall, so CPU-bound code runs at native speed but small-file I/O, like npm ci or a BuildKit build compiling thousands of C files, can be about twice as slow. The old ptrace platform was the worst and is deprecated; systrap, the default since 2023, traps syscalls with seccomp and works anywhere, and the KVM platform is fastest on bare metal but suffers under nested virtualisation. Firecracker is strictly better for syscall- and I/O-heavy builds on bare metal, or anything needing full Linux like eBPF or Docker-in-Docker, as long as you have /dev/kvm.

Q
How do CPU side channels affect gVisor versus Firecracker/Kata, and what scheduling do you need on EPYC and AmpereOne?
Model answer

Side channels hit all three equally, because they all run untrusted code natively on a shared CPU; gVisor's own guide says it doesn't stop them. The biggest risk is two tenants on sibling hyperthreads, which is how Zenbleed (CVE-2023-20593) leaked register contents across VMs on AMD Zen 2. On EPYC I'd disable SMT or use core scheduling and pin each microVM to whole cores, while AmpereOne has single-threaded cores so sibling sharing doesn't exist, and on both I'd keep microcode and Spectre-v2 mitigations current.

Q
Design runner isolation for a multi-tenant CI platform running arbitrary BuildKit and shell builds on bare-metal Kubernetes.
Model answer

Each job gets a just-in-time, single-use runner token, like GitHub's generate-jitconfig or Buildkite's disconnect-after-job agents, and runs in its own pod with a kata-fc RuntimeClass on a dedicated bare-metal node pool, so every job is a fresh Firecracker microVM that's destroyed afterwards. Inside, BuildKit runs rootless with no host Docker socket and no privileged entitlements, the pod has no Kubernetes service account token, and cloud access comes only through OIDC tokens that expire in minutes. Network leaves through nftables on the tap device that drops the metadata service and internal ranges, then an egress proxy enforcing a per-tenant allow-list, out of a dedicated egress IP pool so customers can allow-list us.

Q
How does sandboxing on Apple Silicon with Apple's Virtualization framework compare with Linux KVM sandboxes?
Model answer

Density is the big difference: Apple's licence and the framework cap a Mac at two extra macOS VMs, so a Linux host runs hundreds of microVMs while an M4 or M5 Max runs two macOS jobs. The hypervisor surface is Apple's closed code with mostly virtio devices, patched only through macOS updates, versus open-source KVM and a small VMM you patch yourself. Automation relies on cloning APFS images and booting in seconds rather than millisecond snapshot restore, so most platforms give one tenant the whole Mac and re-image it between tenants.

Q
A workload tries Dirty Pipe or an RxRPC out-of-bounds write. Trace it under runc, gVisor, Firecracker and Kata.
Model answer

Under runc both hit the host kernel directly, so Dirty Pipe can overwrite read-only files in the shared host page cache, even the runc binary, and RxRPC works unless seccomp or a module blacklist stops the AF_RXRPC socket. Under gVisor both fail, because the Sentry has its own pipe and splice code and Netstack has no RxRPC at all, so socket() just returns an error. Under Firecracker or Kata, Dirty Pipe succeeds against the guest kernel and gives guest root, and RxRPC usually fails because minimal guest kernels don't include it, but guest root is exactly where a VMM bug like CVE-2026-5747 starts, so the jailer and patching the VMM are what really hold the line.

Q
Why is Kata's annotation configuration a security control, not just a tuning knob?
Model answer

Annotations are set by whoever creates the pod, so every annotation Kata honours is input from the tenant that crosses to the host side of the VM wall. CVE-2026-44210 was exactly that: before 3.31.0 the default allowed virtio_fs_extra_args, so a pod creator could add virtiofsd flags and expose the host's root filesystem without any memory corruption at all. I'd set enable_annotations to an empty or minimal list and treat any change to it like a firewall rule change.

Q
Why put gVisor inside a VM, or run Firecracker under the jailer, if each is already a sandbox?
Model answer

Because escapes keep happening at every layer, and two unrelated walls mean an attacker needs two unrelated bugs. Firecracker's jailer drops the VMM to an unprivileged user in a chroot with seccomp, so a bug like CVE-2026-5747 turns into code execution as a user who can do almost nothing on the host. The same logic is why GKE Sandbox runs gVisor on nodes that are themselves VMs.