Three runC vulnerabilities — CVE-2025-31133, CVE-2025-52565, and CVE-2025-52881 — were disclosed in November 2025 and were still being actively exploited in the wild as of June 2026. Every runC version back to 1.0.0-rc3 is affected. The bug class: a malicious container image can write to arbitrary /proc files on the host through a crafted mount configuration, and pivot straight from "one tenant's bad image" to "root on the node." In a single-tenant cluster that's bad. In a multi-tenant cluster — every other tenant scheduled on that node inherits the blast radius the moment one neighbor gets popped.
That's the argument for not running untrusted workloads on plain runC. It is not, by itself, an argument for which of the two runtimes teams actually reach for instead. Kata Containers gives every pod its own kernel inside a KVM microVM; gVisor intercepts syscalls in userspace and never gives the workload a kernel to exploit in the first place. Both close the runC gap. They do it at very different costs, and the costs don't move together:
| gVisor | Kata Containers | |
|---|---|---|
| Cold start | Single-digit milliseconds | 150–300ms |
| Steady-state CPU overhead | ~0% (native execution) | 8–12% |
| Raw syscall overhead | 2.2–2.8x slower | Near-native (guest kernel handles it) |
| I/O-heavy workload overhead | 10–30% slower | Near-native once running |
| Memory overhead per pod | ~50Mi | 130Mi (Firecracker) – 200Mi (containerd-shim) |
| Isolation boundary | Userspace syscall filter, shared host kernel | Hardware (KVM), separate guest kernel |
| Reported operational cost | — | 3.4x more on-call pages than containerd, per one 90-day study |
Those numbers don't just differ in magnitude — they point in opposite directions depending on what the pod actually does, which is the whole reason "just pick one" doesn't work for a fleet running mixed workloads.
Two ways to not trust the host kernel
gVisor's runsc sits between the container and the host kernel and re-implements a substantial subset of the Linux syscall surface in Go (the "Sentry"). A workload never gets direct syscall access to the real kernel — every open(), read(), or socket() call gets intercepted, validated, and either emulated or proxied. There's no hypervisor, no guest kernel, no second boot. That's why gVisor starts in milliseconds: there's nothing to boot. It's also why gVisor is explicitly framed by its own maintainers and by production users as good for defense-in-depth, not necessarily as a substitute for hardware isolation against a fully adversarial tenant — the Sentry itself is attack surface, just much smaller and much more scrutinized than the full Linux kernel.
Kata Containers takes the opposite bet. Each pod gets a real, separate guest kernel running inside a lightweight VM — QEMU, Cloud Hypervisor, or Firecracker as the VMM — talking to the CPU through KVM. A kernel exploit inside the guest stays inside the guest; it never reaches the host kernel, because there's a hardware-enforced boundary (Intel VT-x/AMD-V page tables, not a userspace filter) in between. That's the stronger guarantee. It's also why Kata pods take 150–300ms to schedule instead of milliseconds — you're booting a kernel, not attaching a filter.
Why the overhead doesn't move together
This is the part a single "X% slower" headline number always flattens, and it's the reason a per-workload decision framework exists instead of a single default: the two runtimes are slow at different things.
- Syscall-heavy workloads — package installs, compilers, build sandboxes, anything that makes thousands of small syscalls per second — hit gVisor's interception tax hardest. A USENIX benchmark measured simple syscalls at 2.2–2.8x slower than a native container under
runsc. Kata pays no equivalent tax here: once the guest kernel is up, syscalls resolve inside the guest exactly as they would on bare metal. - I/O-heavy workloads — anything pushing bytes across the sandbox boundary, network-bound services, database proxies — cost gVisor 10–30% versus native, since every read/write still crosses the userspace interception layer. Kata's I/O runs through virtio devices to the guest kernel and lands close to native once the VM is running.
- CPU-bound workloads — model inference, batch compute, anything that mostly spins on the CPU without touching the kernel — see close to zero overhead on either runtime, because neither one interferes with raw instruction execution.
- Bursty, short-lived workloads — a single AI-agent tool call, a one-shot code-eval request, anything scheduled and torn down in under a second — pay disproportionately for Kata's 150–300ms boot tax relative to the work actually done, and disproportionately reward gVisor's millisecond start.
- Density — the memory delta compounds fast at scale. On a node with, say, 32GB allocatable to sandboxes, gVisor's ~50Mi fixed overhead leaves room for roughly 600+ pods before memory becomes the packing constraint; Kata's 130–200Mi per pod caps the same node at roughly 150–240. That's a 3–4x difference in tenant density per node before either sandbox does a single byte of real work.
None of this makes one runtime strictly better — it makes them different tools that happen to share a RuntimeClass API.
A decision framework, not a default
The practical output of all of the above is a table, and it maps to a Kubernetes primitive you almost certainly already have wired up: RuntimeClass, paired with node taints/selectors so each tier actually lands on nodes provisioned for it.
| Workload trust tier | Example | Recommended RuntimeClass | Why |
|---|---|---|---|
| Trusted first-party | Your own control plane, ingress, internal services | runc (default) | No isolation tax to pay against workloads you wrote |
| Semi-trusted tenant app | A customer's web app or API, code you didn't write but that isn't adversarial by design | gvisor | Syscall filtering is enough against accidental misbehavior; density and cold-start matter more than a hardware boundary |
| Genuinely untrusted, short-lived | A single AI-agent tool call, one-shot code eval, a sandboxed npm install from an unverified package | gvisor | Millisecond start dominates; the workload is gone before Kata would even finish booting |
| Genuinely untrusted, longer-running or adversarial-by-design | Customer-submitted long-running compute, anything you'd assume is actively trying to escape | kata-qemu / kata-clh | Only a hardware boundary is credible against a motivated attacker with sustained access |
Wiring this up is two RuntimeClass objects and a nodeSelector:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
scheduling:
nodeSelector:
sandbox.tier: gvisor
---
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: kata-clh
handler: kata-clh
scheduling:
nodeSelector:
sandbox.tier: kataA tenant workload's runtimeClassName becomes a tier assignment, not a runtime detail — the same decision a platform team makes once, encoded as policy, rather than a judgment call an operator makes per incident.
This isn't a vendor's invented distinction
It's worth being clear-eyed that this split isn't one platform's opinion. agent-sandbox, a CNCF project under Kubernetes SIG Apps launched at KubeCon NA 2025, ships a declarative CRD specifically for isolated, stateful AI-agent workloads — and it natively supports both gvisor and kata as backends via runtimeClassName. The per-workload split is becoming a Kubernetes-native pattern, not a proprietary schema one vendor is pushing.
Northflank is a concrete commercial example already running this exact model in production: Kata with Cloud Hypervisor as the primary VMM for workloads that need VM-level isolation, gVisor for workloads where syscall filtering is sufficient, selected per workload rather than fleet-wide.
What a managed platform hides from you
Here's the gap that matters if you're comparing this to just using a hosted PaaS. Render, Heroku, and Vercel don't expose a RuntimeClass knob, because there isn't one to expose — they've already picked a single isolation model for their entire multi-tenant fleet, tuned for their own margins and density targets, and every tenant gets it whether their workload is a static site or an AI agent executing arbitrary customer-submitted code. If your workload is higher-risk than the platform's default assumption, you have no lever to pull. You can't ask Heroku to run your one adversarial workload under Kata instead of whatever it runs everything else under.
A self-hosted, Cluster API-managed fleet doesn't get to hide that decision — which is the point. Every pod scheduled onto owned hardware needs an explicit trust tier, because there's no platform vendor absorbing the isolation choice (and its cost) on your behalf. That's a real operational burden. It's also the only way to actually match isolation strength to threat model instead of paying one vendor's flat isolation tax on every workload, trusted or not.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with the isolation tier a matter of policy instead of vendor default. Star the repo on GitHub or deploy your first app today.
Sources
- CNCF: runC container breakout vulnerabilities — a technical overview
- Orca Security: New runC Vulnerabilities Expose Docker and Kubernetes to Container Escape Attacks
- gVisor Performance Guide
- Northflank: Kata Containers vs gVisor
- Northflank: How to sandbox AI agents in 2026
- Kubernetes Blog: Running Agents on Kubernetes with Agent Sandbox
- kubernetes-sigs/agent-sandbox
- OneUptime: Pod Overhead for Sandboxed and VM-Based Runtimes in Kubernetes