Sandbox Isolation for Untrusted Workloads: gVisor vs Firecracker vs Kata Containers
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:
Google's "fake kernel" written in Go. It answers the program's system calls itself, so the program almost never reaches the real kernel.
Amazon's tiny virtual machine monitor (VMM) written in Rust. Each job gets its own real Linux kernel inside a hardware-isolated microVM.
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).
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
- 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.
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.
- 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.
- 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:
| Layer | gVisor | Firecracker | Kata (QEMU + virtio-fs) |
|---|---|---|---|
| First thing the attacker hits | Sentry (Go, memory-safe) | Guest kernel (C, attacker gets root easily) | Guest kernel |
| Second wall | Host kernel via a few dozen filtered syscalls | VMM device emulation (Rust) + KVM | VMM (QEMU, C) + KVM |
| File access path | Gofer / directfs (Go) | virtio-block: raw blocks only | virtiofsd (Rust) serving a host directory |
| Network path | Netstack inside Sentry | virtio-net → host tap device | virtio-net → host tap device |
| Host process privilege | Unprivileged, seccomp | Jailer: unprivileged, chroot, seccomp | Depends on config; virtiofsd often root |
Key ideas behind the table:
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).
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.
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:
$ 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--sandboxset (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. 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. Find the guardattackthe host-side code that reads your requests
- Firecracker reading device settings, virtiofsd reading file requests, the Sentry reading syscalls
-
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. 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: valuelabels. 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.
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.
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.
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.tomland look atenable_annotations. That list is "settings tenants may change on the host";virtio_fs_extra_argsandkernel_paramsshouldn't be on it. Second, for every runtime: find the host-side "guard" process (firecracker,virtiofsd,runsc-gofer) withps -o user,argsand confirm it isn't root and is sandboxed. Watch each project's GitHub Security Advisories page.
In practiceThe Kata setting behind CVE-2026-44210:
# /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 runsudogets 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.
-
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
-
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
-
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.
-
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
-
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
-
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).runcis 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 host | Hundreds of microVMs | 2 macOS VMs; Linux VMs RAM-bound |
| Hypervisor surface | KVM (open source) + small VMM | Apple's closed hypervisor + virtio devices |
| Patch path | You patch the host kernel and VMM | Apple ships it in macOS updates |
| Boot / restore | ~125 ms boot, ms-scale snapshot restore | Seconds; clone-and-boot |
| Tenant reuse | Throw away the microVM | Often 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":
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.
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.
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
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.
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).
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:
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.
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.
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
$ 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 affectedPerformance 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
| runc | gVisor (systrap) | gVisor (KVM) | Firecracker | Kata (QEMU) | |
|---|---|---|---|---|---|
| Extra memory per sandbox | ~0 | Tens of MB (Sentry) | Tens of MB | < 5 MiB VMM + guest kernel | Typically 30 MB+ (QEMU + guest + agent) |
| Start time | ms | ~100s of ms | ~100s of ms | ~125 ms boot; ms-scale snapshot restore | ~0.5–1 s typical |
| CPU-bound code | Native | Native | Native | Native | Native |
| Syscall-heavy code | Native | Slower: each syscall is trapped | Faster on bare metal | Native inside guest | Native inside guest |
| Small-file I/O | Native | Slowest case (VFS in Sentry) | Same | Fast (block device) | virtio-fs round trips |
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.
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.
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.
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):
$ 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 CPUNetwork 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
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.)
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:
-
Job sends trafficattackany destination
- Guest has a default route only to the platform's gateway
-
Host firewall per VMnetworknftables on the tap device
- Drop 169.254.169.254, host IPs, other VMs, internal ranges
- Allow only the egress proxy
-
Egress proxydefendper-tenant policy
- Allow-list by hostname (CONNECT / TLS SNI); log every destination
-
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:
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/andcurl -m 2 https://example.comwithout 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
-
Job arrivesserverGitHub Actions webhook / Buildkite job
- Controller requests a just-in-time runner for this one job
-
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
-
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
-
Customer code runs as root in the VMattackBuildKit, shell, C toolchains
- Rootless BuildKit inside the VM; no host Docker socket, no privileged entitlements
-
Job needs cloud accesstlsOIDC, not stored keys
- Job requests an OIDC token; customer's cloud exchanges it for minutes-long credentials
-
TeardowndefendVM destroyed, token already expired
- Unique OS credentials per job, nothing reused; logs and artifacts stored per tenant
In practiceThe Kubernetes side:
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:
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.
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.
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).
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.
| Workload | Recommended | Why |
|---|---|---|
| Your own trusted services | runc + seccomp + AppArmor/SELinux | Speed; the code isn't hostile |
| Untrusted code on cloud VMs without KVM | gVisor (systrap) | No hardware virtualisation needed; big kernel surface removed |
| Hostile multi-tenant CI, bare metal | Firecracker (directly or via Kata) | Hardware boundary, small Rust VMM, fast boot |
| Kubernetes-native platform needing full Linux | Kata (Cloud Hypervisor/Firecracker) | Pod semantics with a VM per pod |
| AI-agent tool calls, short code snippets | gVisor or Firecracker snapshot restore | Fast start, no network by default |
| macOS builds | Apple vz VMs, one tenant per Mac | Licence caps density at 2; re-image between tenants |
| Regulated / highest value tenants | Dedicated hosts, plus any of the above | No shared hardware means no side channels |
Operational trade-offs to say out loud:
Firecracker/Kata need /dev/kvm (bare metal or nested virt). gVisor doesn't.
gVisor implements most but not all syscalls; some tools (eBPF, unusual ioctls, kernel modules) won't work. VMs run a real kernel.
gVisor: Google's Sentry. Firecracker: a small Rust VMM plus KVM. Kata: whichever VMM you choose plus virtiofsd, and your own configuration.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.