gVisor - Root Container
In gVisor, a root container is a fundamental structural concept that stems from how gVisor handles multi-container pods (like those in Kubernetes) inside a single sandbox environment.
To understand what a root container is, you have to look at the relationship between a gVisor Sandbox and the OCI (Open Container Initiative) runtime spec.
The Core Architecture: Sandbox vs. Container
In traditional runtimes like runc, every single container gets its own isolated namespaces on the host, but they all talk directly to the shared host Linux kernel.
gVisor (runsc) works differently. Instead of giving every container its own kernel, gVisor spins up an instance of The Sentry (the Go-based user-space guest kernel). This Sentry acts as an isolated sandbox boundary.
Inside a single gVisor sandbox, you can actually run multiple containers (for instance, a Kubernetes Pod containing an application container, a logging sidecar, and an init container).
- The Sandbox: The overall security boundary managed by one Sentry instance.
- The Root Container: The very first container created inside that sandbox. It defines the lifespan, the Sandbox ID, and the initial shared environment.
- Subcontainers: Any subsequent containers injected into that same sandbox later on.
Key Characteristics of the Root Container
A. It Dictates the Sandbox ID
When a container engine tells gVisor to spin up a workload (e.g., runsc create <container-id>), gVisor creates the Sentry process. The ID of this very first container (the root container) becomes the Sandbox ID. Any subcontainers added to this sandbox later will have their own unique container IDs, but they will all share the root container's Sandbox ID.
B. Shared Infrastructure Initializer
The root container acts as the baseline infrastructure layer for the sandbox. It initializes the core namespaces that the Sentry will emulate for the rest of the containers in that Pod, such as:
- The Network Stack (Netstack): The network interfaces and loopback setup are initialized with the root container.
- IPC Namespaces: Inter-process communication channels that sidecars might use to talk to the main app.
C. The Lifecycle Anchor
In gVisor, the lifecycle of the sandbox is heavily tied to the root container. If the root container exits or is destroyed, the Sentry kills off the remaining subcontainers and tears down the entire sandbox.
How It Operates Under the Hood
When a multi-container Pod deployment is triggered (like in Kubernetes), the workflow splits the root container from subcontainers:
- Creating the Root Container:
runscis called to create the pause container (or the primary app container if running standalone). gVisor launches the Sentry process and a Gofer process to serve the root container's filesystem. - Creating Subcontainers: When a sidecar needs to spin up,
runscis called again with a special flag indicating it should join an existing sandbox. The Sentry handles this internally by creating a new distinct set of processes inside its own virtualized environment, spawning a new Gofer process on the host to serve the sidecar's unique filesystem, and mounting it into the Sentry.
How it works with CRI?
To understand exactly how the Container Runtime Interface (CRI) and different runtimes interact, we can break down the execution paths into two distinct comparisons.
1. gVisor Standalone (No CRI) vs. gVisor with CRI (Kubernetes)
This comparison highlights how the presence of an orchestrator completely changes what gVisor considers its "Root Container."
Scenario A: gVisor Standalone (e.g., Plain Docker / runsc directly)
- Who is in charge: The Docker daemon or a direct user command (
docker run --runtime=runsc). - The Root Container: Your actual application (e.g., NGINX, Node.js).
- The Workflow: 1. Docker tells
runscto start a container using your application image. 2. gVisor boots the Sentry (user-space kernel). 3. The Sentry immediately executes your application process as PID 1. - Lifecycle: The application is the root container. The moment your application process exits, the Sentry sandbox tears itself down and terminates. There is no pause container.
Scenario B: gVisor with CRI (Kubernetes / containerd / CRI-O)
- Who is in charge: The Kubelet via the CRI layer.
- The Root Container: The standard Kubernetes
pausecontainer image (registry.k8s.io/pause). - The Workflow:
- The CRI tells
runscto create a "Pod Sandbox" using thepauseimage. - gVisor boots the Sentry and runs the
pausebinary inside it. This becomes the root container. - The
pauseprocess goes to sleep, holding open the emulated network and IPC namespaces. - The CRI subsequently tells
runscto inject your actual application and sidecar images into that active Sentry as subcontainers.
- The CRI tells
- Lifecycle: The Sentry’s lifecycle is tied to the
pausecontainer. If your application container crashes or restarts, thepausecontainer (the root container) keeps running, ensuring the Sentry sandbox and its network stack stay alive.
CRI Running runc vs. CRI Running runsc (gVisor)
This comparison highlights how the underlying security abstraction changes when the CRI issues identical commands to a traditional runtime versus a sandboxed runtime.
Scenario A: CRI running runc (Standard Linux Containers)
When Kubernetes tells containerd to spin up a Pod, and containerd invokes runc:
- Isolation Layer: Host-level Linux kernel primitives (Namespaces and Cgroups).
- The Pause Container Execution:
runccreates new Linux namespaces on the host machine and runs thepausebinary inside them. - The Application Containers: When the CRI adds your application containers,
runccallssetns()to join those exact same host namespaces created for thepausecontainer. - Kernel Visibility: Every process (the
pausebinary, your app, your sidecars) runs directly on the shared host Linux kernel. They are visible viapson the host machine as standard processes.
Scenario B: CRI running runsc (gVisor Sandbox)
When Kubernetes tells containerd to spin up a Pod, and containerd invokes gVisor (runsc):
- Isolation Layer: A dedicated user-space guest kernel (The Sentry).
- The Pause Container Execution:
runscboots a brand new instance of the Sentry process on the host. Thepausebinary is executed inside this virtualized Sentry kernel environment. - The Application Containers: When the CRI adds your application containers,
runscintercepts the request and instructs the already running Sentry to spawn a new process tree internally. The Sentry mounts the new application filesystem (served via a separate Gofer proxy process) alongside the pause container. - Kernel Visibility: The application processes do not interact with the host kernel. They interact exclusively with the Sentry. To the host machine, the entire Kubernetes Pod appears simply as a handful of Sentry and Gofer processes.
- Note: The pause container is still the first thing started (becoming the "root container"), but it doesn't need to hold namespaces open in the same way. In gVisor, the Sentry (the guest kernel) is the entity that actually owns and manages the virtualized network stack, IPC, and PID namespaces for the Pod. The root container is simply the first guest process spawned inside the Sentry.