Container runtimes - runc - Overview
Part 2 of the container runtimes series. Introduces runc and breaks down the init, create, and start lifecycle stages.
Series navigation:
- Part 1: container runtimes fundamentals
- Part 2: runc overview (this post)
- Part 3: runc init stage
- Part 4: runc create and start stages
What is runc?
runc is an OCI (Open Container Initiative) compliant CLI to create and run containers on Linux. It was introduced in 2015 by the Docker community and donated to the Open Container Project (later renamed the Open Container Initiative), with the goal of creating a lightweight, portable container runtime built from the low-level code Docker used to interact with Linux container primitives.
Its most common commands are:
runc spec: creates aconfig.jsonfile (the container spec)runc create: creates a containerrunc start: starts a previously created containerrunc delete: removes a stopped containerrunc run: creates and starts a container in one step
These commands orchestrate the overall container lifecycle, which we will break down in the next sections.
runc stages
The OCI Runtime Specification defines the container lifecycle in terms of three operations: create, start, and delete. runc implements them, and runc run is simply a convenience wrapper around create plus start.
There is, however, a fourth piece that is not a CLI command at all: runc init. This is the process that actually sets up the isolation, and it is where most of the important work happens. It is easy to misread runc as running “init, then create, then start” in sequence. It does not. runc init is a child process forked during create, and it sits blocked partway through its own setup until runc start releases it:
/proc/self/exe init"] B --> C["runc init
(namespaces, rootfs, security context)"] C --> D["blocks on exec.fifo"] D -. "container state: created" .-> E["runc start"] E --> F["writes to exec.fifo"] F --> G["runc init unblocks
and calls execve()"] G --> H["container state: running"] H --> I["runc delete
(teardown)"] classDef create fill:#e8f5e9,stroke:#43a047,stroke-width:2px,color:#14321a classDef init fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px,color:#311b3f classDef start fill:#fff3e0,stroke:#fb8c00,stroke-width:2px,color:#3e2600 classDef teardown fill:#eceff1,stroke:#607d8b,stroke-width:2px,color:#22313a class A,B create class C,D init class E,F,G,H start class I teardown
With that ordering in mind, the rest of this post walks through each phase: what runc init does, what create adds around it, and what start triggers.
The init phase
The init phase is where runc prepares the container environment. This is the phase that requires the most attention from a security standpoint, since it establishes namespaces and security contexts before the container process starts.
• PID • Mount • Network
• IPC • UTS • User • Cgroup"] B1 --> B2["Filesystem Setup
• Rootfs preparation
• Bind mounts
• Device nodes
• /proc, /sys"] B2 --> B3["Security Context
• SELinux/AppArmor
• Capabilities
• Seccomp profiles
• User mappings"] end classDef initStage fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px,color:#311b3f class B1,B2,B3 initStage style INIT fill:#faf4fc,stroke:#ce93d8,stroke-width:1px,color:#311b3f
In this phase, the following steps are performed:
-
Namespace creation:
runccreates new namespaces based on the container specification, usingclone(),unshare(), andsetns()with the appropriate flags (CLONE_NEWPID,CLONE_NEWNS, and so on). - Filesystem setup: the container’s root filesystem is prepared, including:
- setting up the rootfs from the container image
- creating bind mounts for volumes
- setting up device nodes
- configuring
/proc,/sys, and other special filesystems
- Security context: security-related configurations are applied:
- SELinux/AppArmor labels
- capabilities are dropped or added
- seccomp profiles are loaded
- user namespace mappings are established
Cgroups are a deliberate exception here. They are not configured by runc init. The parent runc process creates the cgroup and moves the init process into it, because a process confined by a restrictive cgroup cannot reliably set up its own limits. We will come back to this in Part 4.
The create phase
The create phase is where runc creates the container but does not start the user-specified process yet. At this point, the container exists in a created state: all namespaces and isolation are set up, but the main application has not started.
• Process prepared
• Not yet exec'd"] C1 --> C2["Cgroups Applied
• Created by parent runc
• Init process moved in"] C2 --> C3["Execute Hooks
• createRuntime
• createContainer"] C3 --> C4["Container State: CREATED
• All isolation ready
• Main process not started
• State persisted to disk"] end classDef createStage fill:#e8f5e9,stroke:#43a047,stroke-width:2px,color:#14321a class C1,C2,C3,C4 createStage style CREATE fill:#f4faf5,stroke:#a5d6a7,stroke-width:1px,color:#14321a
In this phase, the following steps are performed:
- Process preparation: the init process is forked and configured, but the container command is not yet
exec‘d - Cgroup setup: control groups are created and applied to limit and isolate resource usage (CPU, memory, I/O)
- Runtime hooks: OCI lifecycle hooks (
createRuntimeandcreateContainer) run during this phase - State transition: the container transitions to the
createdstate - State persistence: container state is written to disk for management tools
The create phase matters because it gives tooling a window for inspection and additional setup before the container actually starts running. It is also the window in which the createContainer hook runs, which is exactly the mechanism abused by CVE-2025-23266.
The start phase
The start phase is where the container begins executing the user-specified command or entrypoint.
• Write to exec.fifo
• Unblocks runc init"] D1 --> D2["execve()
• Replace init with
the container process"] D2 --> D3["Execute poststart Hooks
• Application-specific setup
• Health checks"] D3 --> D4["Container State: RUNNING
• Main application executing
• Monitoring begins"] end classDef startStage fill:#fff3e0,stroke:#fb8c00,stroke-width:2px,color:#3e2600 class D1,D2,D3,D4 startStage style START fill:#fffaf3,stroke:#ffcc80,stroke-width:1px,color:#3e2600
In this phase, the following steps are performed:
- Unblocking init:
runc startwrites to the container’sexec.fifo, releasing the blockedrunc initprocess - Process execution: the init process performs an
execve()system call to replace itself with the container’s main process - Hooks: the
startContainerhook runs inside the container namespace just beforeexecve(), andpoststarthooks run on the host afterwards - State transition: the container state changes from
createdtorunning
What’s next?
In the next part of this series, we will explore the runc init phase and how each of these steps is implemented in practice: Part 3 – runc init stage.