← Flight log

Escaping the guest without touching the host

Every isolation story is really a story about a boundary someone has to trust. A virtual machine promises that whatever happens inside the guest stays inside the guest. That promise is only ever as strong as the thinnest interface crossing the boundary — and there are more of those interfaces than the marketing admits.

The boundary is not the CPU

Hardware virtualization (VT-x, AMD-V) makes the instruction boundary cheap and strong. The guest runs real instructions; only the privileged ones trap. If that were the whole story, escapes would be exotic. They aren’t, because the interesting surface isn’t the CPU — it’s everything the guest is allowed to talk to:

  • Emulated devices. The virtual NIC, disk controller, and graphics adapter are C programs parsing attacker-controlled bytes. Historically this is where the escapes live.
  • Paravirtualized channels. virtio trades emulation overhead for a shared ring buffer. Faster, but now the guest and host share a carefully described memory region, and description bugs are exploitation primitives.
  • The hypercall interface. Every call the guest can make into the host is a syscall boundary wearing a different hat.

Rule of thumb: count the parsers reachable from the guest. That number is your real attack surface, not the number of vCPUs.

Containers move the boundary, they don’t remove it

A container shares the host kernel. The “boundary” is a bundle of namespaces, cgroups, capabilities, and a seccomp filter. That’s a lot of independent knobs, and the default posture of many runtimes is more permissive than people assume.

A minimal seccomp profile that denies the usual escape-adjacent syscalls looks like this:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    { "names": ["read", "write", "openat", "close", "mmap", "brk"],
      "action": "SCMP_ACT_ALLOW" }
  ]
}

Everything not listed returns EPERM. It breaks a lot of software — which is the point. You want to find out now which syscalls your workload actually needs, not after someone else finds out for you.

Where the strong isolation actually is

If you want VM-grade isolation with container-grade ergonomics, the interesting work is in the sandboxed runtimes:

Approach Boundary Cost
Namespaces (runc) Shared kernel Cheapest, weakest
gVisor User-space kernel reimplementation Syscall overhead
Kata Real lightweight VM per container Boot + memory overhead

gVisor is the one I keep coming back to: instead of forwarding guest syscalls to the host kernel, it answers them itself in a memory-safe user-space kernel. The host kernel surface the workload can reach shrinks dramatically. You pay for it in syscall latency, and for a lot of workloads that trade is obviously worth making.

The takeaway

There is no such thing as escaping the guest without touching the host — every escape is, definitionally, reaching the host through some interface you were allowed to touch. The engineering question is never “is this isolated?” It’s “how many interfaces cross the boundary, who wrote the parser on the other side, and what happens when I feed it garbage?”

Answer those three and you’ve done more for your security posture than any number of reassuring architecture diagrams.

← All entries Reply by email →