all posts

How gVisor intercepts syscalls: the Sentry, the Gofer, and platforms explained

Ajay Kumar··9 min read

Most explanations of gVisor stop at "it's a user-space kernel that intercepts syscalls," which is true but hides the interesting part: how does a syscall a program makes get pulled out of the host kernel's hands and answered by a Go process instead? That mechanism — the trap, the redirect, and the deliberately tiny door gVisor leaves open to the real kernel — is the whole security thesis. This post takes the mechanism apart: the Sentry (the user-space kernel), the Gofer (the filesystem proxy), and the platforms (ptrace and KVM) that do the actual trapping. If you want the head-to-head with hardware virtualization, /blog/gvisor-vs-firecracker covers that; here we go a layer deeper on gVisor itself.

The Sentry: a Linux kernel rewritten in Go

gVisor's runtime is called runsc ("run sandboxed container"), an OCI runtime you can drop in where runc normally sits. Its heart is a component called the Sentry: a program, written mostly in Go, that re-implements a large fraction of the Linux system-call ABI in user space. When the sandboxed application calls open, read, socket, mmap, or one of a few hundred other syscalls, that call does not go to the host kernel. It goes to the Sentry, which implements the semantics itself — it maintains its own view of file descriptors, its own virtual memory bookkeeping, its own network stack (a user-space TCP/IP implementation), its own process and signal handling. From inside the sandbox, it looks and behaves like a Linux kernel, because functionally it is one — just one running as an ordinary, unprivileged process.

The point of doing this is the security thesis in one sentence: the application talks to the Sentry, and only the Sentry talks to the host kernel — through a small, seccomp-restricted set of host syscalls. Instead of exposing the full Linux ABI (300-plus syscalls, many of them obscure and exploit-favored) to the host kernel on behalf of untrusted code, gVisor interposes a second kernel that fields those calls and reaches out to the real one only through a narrow, audited door. Shrink the door, shrink the attack surface. The Sentry is the thing standing in the doorway.

The mental model: gVisor answers your syscalls so the host kernel doesn't have to. Your program still thinks it's talking to Linux — it is, just a Linux written in Go running next to it in user space. The real host kernel only ever hears from the Sentry, and only in the small vocabulary the Sentry is allowed to speak.

The platforms: how a syscall actually gets trapped

The Sentry can only field a syscall if it gets control the instant the application makes one. The mechanism that intercepts the application's syscalls and redirects them into the Sentry is called a platform, and gVisor ships more than one because there's no single free way to do it. The platform is responsible for two things: trapping every syscall (and page fault) the application makes, and switching address spaces between the application and the Sentry.

The original platform is ptrace. It uses the same kernel machinery a debugger uses — the Sentry acts as a tracer, and the kernel stops the application on every syscall entry and hands control to the Sentry to service it. This works anywhere ptrace works and needs no special privileges or virtualization, which made it the portable default. The cost is that a debugger-style stop-and-context-switch on every single syscall is expensive: each intercepted call is a round trip through the tracing machinery, so syscall-heavy workloads feel it.

The faster platform is KVM. Here gVisor uses the same CPU virtualization extensions a hypervisor uses, but for a different purpose: instead of booting a guest OS, the Sentry runs itself as a guest so that it sits at ring 0 (kernel privilege) with respect to the application, and the application's syscalls and page faults trap directly to the Sentry via the CPU's virtualization hardware rather than through the kernel's ptrace path. It's faster because the trap-and-switch is done by the hardware virtualization mechanism instead of a debugger stop. This is the detail that confuses reviewers: using KVM does not make gVisor a virtual machine. There is no guest kernel being booted — gVisor borrows the CPU's virtualization extensions purely to make syscall trapping and address-space switching fast and safe. The isolation model is still a user-space kernel intercepting syscalls, not a hardware-virtualized VM.

"gVisor on the KVM platform is basically a VM" is the single fastest way to get a security review handed back to you. KVM here is a trapping-and-context-switch accelerator for the Sentry, not a hypervisor booting a guest OS. Same isolation model as the ptrace platform (user-space kernel intercepting syscalls), just a faster trap path. Verify the current platform behavior against gVisor's own docs — the platform layer is exactly the kind of thing that evolves.

The Gofer: keeping filesystem access out of the Sentry

There's a subtlety in the security thesis. If the Sentry itself had broad access to the host filesystem, an attacker who compromised the Sentry would inherit that access. So gVisor pushes filesystem access out of the Sentry into a separate helper process: the Gofer. The Sentry, by design, has minimal direct access to the host filesystem; when the sandboxed app opens or reads a file, the Sentry proxies that request over a channel to the Gofer, which is the process that actually touches the host filesystem and hands file descriptors or data back.

That channel historically speaks a variant of the 9P protocol (the Plan 9 file protocol) — a small, well-defined message set for filesystem operations — though gVisor has evolved its host-file mechanism over time, so treat the exact protocol details as a point to check against current gVisor docs. The architectural intent is the durable part: separate the process that re-implements the kernel (the Sentry) from the process that holds host filesystem access (the Gofer), so that neither one alone has both the syscall-servicing logic and broad host reach. The Sentry is confined by seccomp to a tiny host-syscall vocabulary; the Gofer is the narrow, auditable conduit to the actual files. It's least-privilege applied to gVisor's own internals.

  • Application — the untrusted workload. Makes ordinary Linux syscalls, unaware anything is intercepting them.
  • Platform (ptrace or KVM) — traps every syscall and page fault the application makes and redirects control into the Sentry, plus switches address spaces between app and Sentry.
  • Sentry — the user-space Linux kernel (Go). Implements the syscall ABI, its own memory management, and a user-space network stack. Talks to the host kernel only through a small seccomp-filtered set of syscalls.
  • Gofer — a separate process that holds the (narrow) host filesystem access. The Sentry proxies file operations to it (historically over a 9P-style protocol) so the Sentry itself keeps minimal host reach.

runsc in action

Because runsc is an OCI runtime, using gVisor mostly looks like using a container — you point your runtime at runsc and everything above it is unchanged. The most common way in is Docker's --runtime flag once the runsc runtime is registered:

# runsc is an OCI runtime; register it, then containers can run under it.
# (Install per gVisor's docs, then it shows up in Docker's runtime list.)

# Run a container sandboxed by gVisor instead of the host kernel directly:
docker run --rm --runtime=runsc alpine \
  sh -c 'echo hello from inside the Sentry'

# Prove it: inside a gVisor sandbox, the "kernel" the app sees is the
# Sentry, not the host. The reported kernel identity differs from the host
# because a user-space kernel is answering uname(), not Linux directly.
docker run --rm --runtime=runsc alpine uname -a

# Inspect the platform runsc is using (ptrace vs KVM) and other config
# via runsc's own flags -- e.g. a container spec can request:
#   runsc --platform=kvm    (hardware-accelerated trap path)
#   runsc --platform=ptrace (portable, debugger-style trap path)
#
# Under the hood each container gets its own Sentry process (the user-space
# kernel) plus a Gofer process (the host-filesystem proxy). The app's
# syscalls are trapped by the platform and serviced by the Sentry; only the
# Sentry -- through a tight seccomp allowlist -- ever calls the host kernel.

# Verify all specifics (flags, platform names, defaults) against gVisor's
# current documentation -- the runtime evolves.

The operational feel is deliberately container-shaped: same images, same OCI spec, near-container startup because there's no guest kernel to boot. What changed is who answers the syscalls. That's the entire pitch — stronger-than-container isolation without a second operating system to manage.

The honest trade-offs

Re-implementing the kernel in user space buys a smaller host-kernel attack surface, but nothing is free, and the costs are worth stating plainly. There are three.

First, compatibility gaps. The Sentry re-implements the Linux syscall surface, and it does not implement all of it. Some syscalls are unimplemented, and some implemented ones differ in edge-case behavior from the real kernel. Ordinary code overwhelmingly runs fine, but the long tail — programs using unusual syscalls, exotic filesystem or networking semantics, or kernel features the Sentry hasn't reproduced — can misbehave or fail outright. When the workload is arbitrary and you can't predict what it will call, that long tail is exactly where surprises live. Check gVisor's own compatibility documentation for specifics rather than assuming.

Second, per-syscall interception overhead. Every syscall the application makes is trapped and serviced in user space instead of going straight to the kernel — that's the whole mechanism, and it isn't free. CPU-bound code that rarely calls into the kernel barely notices. Syscall-heavy and I/O-heavy workloads — lots of small reads and writes, filesystem churn, heavy networking — pay the interception cost on every call, and the filesystem proxying through the Gofer adds its own hop. The overhead is real and workload-dependent; describe it qualitatively and measure it against your own workload rather than trusting a single headline number.

Third, the Sentry is now part of your trusted computing base. It's a large, sophisticated Go codebase re-implementing kernel behavior, and it's software, so it can have bugs. gVisor moves the boundary the application probes from the host kernel to the Sentry — a smaller and more purpose-built surface than the full Linux kernel, which is the point — but it doesn't eliminate the boundary, it relocates it. You've traded "trust the whole host kernel's syscall surface" for "trust the Sentry plus the tiny host-syscall set it's allowed." That's usually a good trade; it's still a TCB you're accountable for.

Contrast: gVisor vs a hardware-virtualized microVM

The cleanest way to understand gVisor's model is to hold it next to the other serious answer — hardware virtualization, as in a Firecracker microVM. Firecracker doesn't intercept syscalls at all; it boots a real guest kernel isolated by CPU hardware virtualization (KVM), and the application's syscalls hit that real guest kernel normally. The boundary the host defends isn't a user-space kernel, it's the Virtual Machine Monitor plus the small virtio device model. Neither approach is strictly better — they make different trades:

  • Boundary — gVisor: software syscall interception by a user-space kernel (the Sentry), trapped via the ptrace or KVM platform. Firecracker: hardware virtualization (KVM) around a real guest kernel; the boundary is the VMM, not a user-space kernel.
  • What runs the syscalls — gVisor: the Sentry re-implements the Linux syscall ABI itself in user space. Firecracker: a full, real guest kernel per microVM runs them natively.
  • Compatibility — gVisor: high for ordinary code, but some syscalls are unimplemented or differ, so exotic or syscall-heavy workloads can break (verify against gVisor's docs). Firecracker: essentially that of a normal Linux box — if it runs on Linux, it runs in the microVM.
  • Overhead profile — gVisor: near-container for CPU-bound work; per-syscall interception cost plus Gofer filesystem proxying hits syscall- and I/O-heavy workloads. Firecracker: VM-class with a small per-guest memory cost, but no per-syscall interception tax — syscalls hit the guest kernel directly.
  • Attack surface — gVisor: the host kernel sees only the Sentry's small, seccomp-filtered syscall set; the Sentry itself joins the TCB. Firecracker: the host defends the VMM + KVM ioctl interface + minimal virtio surface, backstopped by seccomp on the VMM and a privilege-dropping jailer.

For arbitrary, model-generated code — the AI-agent case — the two costs that matter most are compatibility and the trapping tax, and both point the same way: a real guest kernel means "if it runs on Linux it runs here" and no per-syscall interception overhead. That's the trade PandaStack makes. Each sandbox is its own Firecracker microVM with its own guest kernel under KVM, created via snapshot-restore at a p50 of about 179ms (roughly 203ms p99; the snapshot restore itself is around 49ms), with only the first cold boot of a template taking a few seconds. You get a hardware boundary and full Linux compatibility for the price of a couple hundred milliseconds per create — no user-space kernel in the syscall path.

from pandastack import Sandbox

# A Firecracker microVM: its own real guest kernel under KVM, created via
# snapshot-restore (~179ms p50). Syscalls hit the guest kernel directly --
# no user-space kernel intercepting them, so "if it runs on Linux, it runs
# here" and there's no per-syscall interception tax.
with Sandbox.create(
    template="code-interpreter",
    ttl_seconds=300,
    metadata={"task": "agent-tool-call"},
) as sb:
    result = sb.exec("python3 -c 'import platform; print(platform.uname())'")
    print(result.stdout, result.exit_code)
# Context manager destroys the microVM here -- nothing survives to the next task.

The SDK reads PANDASTACK_API_KEY from the environment; the same flow exists in the TypeScript SDK and CLI, and because the PandaStack core is Apache-2.0 you can self-host the whole stack on your own Linux KVM hosts.

The bottom line

gVisor is a genuinely clever piece of engineering: a Linux kernel rewritten in Go (the Sentry), fed by a platform (ptrace or KVM) that traps every application syscall and redirects it, with host filesystem access exiled to a separate Gofer process so the Sentry keeps minimal host reach. The payoff is a much smaller host-kernel attack surface at a near-container operational weight. The costs are a compatibility long tail, per-syscall interception overhead on syscall-heavy work, and a large new TCB in the shape of the Sentry itself. For well-characterized workloads where a container UX matters, that's a fine trade. For arbitrary untrusted code where compatibility can't quietly fail and syscall overhead can't be assumed away, a hardware boundary with a real guest kernel — the microVM model — tends to fit better. Match the boundary to your threat and your workload, and verify any gVisor specifics against its own documentation, which is the honest thing to do for a runtime that evolves as fast as this one.

Frequently asked questions

How does gVisor actually intercept syscalls?

The sandboxed application makes ordinary Linux syscalls, but a gVisor component called a platform traps each one and redirects control into the Sentry — a user-space Linux kernel written in Go — which services the syscall itself instead of letting it hit the host kernel. gVisor has two main platforms: ptrace, which uses the same kernel machinery a debugger uses to stop the process on every syscall (portable but slower), and KVM, which uses CPU virtualization extensions so the Sentry runs at kernel privilege relative to the app and traps syscalls via hardware (faster). Only the Sentry then talks to the host kernel, through a tiny seccomp-filtered syscall set.

What are the Sentry and the Gofer in gVisor?

The Sentry is gVisor's user-space kernel: a Go program that re-implements a large fraction of the Linux syscall ABI, including its own memory management and a user-space network stack, so the sandboxed app talks to it instead of the host kernel. The Gofer is a separate helper process that holds the (narrow) host filesystem access; the Sentry proxies file operations to it — historically over a 9P-style protocol — so the Sentry itself keeps minimal direct host reach. Splitting them applies least-privilege to gVisor's own internals: neither process alone has both the syscall-servicing logic and broad host filesystem access.

Is gVisor a virtual machine when it uses the KVM platform?

No. On the KVM platform gVisor borrows the CPU's virtualization extensions to make syscall trapping and address-space switching fast — the Sentry runs at ring 0 relative to the app so traps go through hardware rather than a debugger-style ptrace stop. But there is no guest kernel being booted; the isolation model is still a user-space kernel intercepting syscalls, not a hardware-virtualized VM. Calling KVM-platform gVisor a VM is a common and consequential mistake in security reviews. A Firecracker microVM, by contrast, does boot a real guest kernel under KVM.

What are the downsides of gVisor's syscall interception?

Three main ones. Compatibility: the Sentry re-implements the kernel and doesn't implement all of it, so some syscalls are unimplemented or differ in edge cases and exotic or syscall-heavy workloads can break — check gVisor's compatibility docs. Overhead: every syscall is trapped and serviced in user space, so syscall- and I/O-heavy workloads pay per-call interception cost plus Gofer proxying (CPU-bound code barely notices). And the Sentry itself is a large Go codebase that becomes part of your trusted computing base — the boundary moves from the host kernel to the Sentry rather than disappearing.

How does gVisor differ from a Firecracker microVM for running untrusted code?

gVisor intercepts syscalls in a user-space kernel (the Sentry) and reaches the host only through a small seccomp-filtered set — a software boundary, near-container weight, but with a compatibility long tail and per-syscall interception overhead. Firecracker boots a real guest kernel isolated by CPU hardware virtualization (KVM); syscalls hit the guest kernel directly, so compatibility is essentially that of a normal Linux box and there's no interception tax, at the cost of a small per-guest memory footprint and a guest boot that snapshot-restore makes sub-second. For arbitrary, unpredictable code the hardware boundary and full Linux compatibility usually tip toward the microVM — which is what PandaStack runs, at a create p50 near 179ms.

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.