Introduction
runc is the reference implementation of the OCI (Open Container Initiative) standard and the low-level runtime behind Docker, containerd, Kubernetes, and the broader container ecosystem. When you run docker run, it is ultimately runc that creates and runs the container.
The best starting point for understanding runc’s source code is its libcontainer/SPEC.md — a document that defines the standard configuration for v1 containers, covering namespaces, filesystem setup, resource controls, security capabilities, and more. As the first article in this series, we focus on the most fundamental and critical part: Namespaces.
Container Specification v1 Overview
SPEC.md opens with the following description:
This is the standard configuration for version 1 containers. It includes namespaces, standard filesystem setup, a default Linux capability set, and information about resource reservations. It also has information about any populated environment settings for the processes running inside a container.
The specification also outlines system requirements:
- Minimum kernel version: 3.10 recommended, 2.6.2x minimum (with backported patches)
- Cgroup mounts: Each subsystem must be mounted in its own hierarchy
The v1 profile is meant to be able to accommodate the majority of applications with a strong security configuration.
The design goal of the v1 profile is to support the majority of applications while maintaining a strong security posture.
Namespaces: The Foundation of Container Isolation
The namespace definition in SPEC.md is remarkably concise:
| Flag | Enabled |
|---|---|
| CLONE_NEWPID | 1 |
| CLONE_NEWUTS | 1 |
| CLONE_NEWIPC | 1 |
| CLONE_NEWNET | 1 |
| CLONE_NEWNS | 1 |
| CLONE_NEWUSER | 1 |
| CLONE_NEWCGROUP | 1 |
Namespaces are created for the container via the
unsharesyscall.
Just a table and a single sentence — yet they define the entire core of container isolation. Let’s walk through each of the 7 namespaces.
1. PID Namespace (CLONE_NEWPID) — Process Isolation
The PID Namespace gives a container its own process ID number space. The first process inside the container always gets PID 1, acting as the init process.
OS context: On Linux, PID 1 is the init process (typically systemd on modern systems), responsible for reaping orphan processes. PID 1 inside a container carries the same responsibility — if the container’s PID 1 exits, the entire container terminates.
Host view: Container view:
PID 1 (systemd) PID 1 (nginx)
PID 2 (kthreadd) PID 2 (nginx worker)
...
PID 28431 (nginx) <- same process, different PIDs
PID 28432 (nginx)Processes inside the container cannot see any processes on the host, achieving process-level isolation.
2. UTS Namespace (CLONE_NEWUTS) — Hostname Isolation
The UTS (Unix Time-Sharing) Namespace isolates the hostname and domain name. Each container can have its own hostname without affecting the host.
OS context: The name "UTS" comes from the utsname struct returned by the uname syscall. In container environments, an independent hostname allows each container to identify itself, which is important for service discovery and log collection. In Kubernetes, a Pod’s hostname is implemented through the UTS Namespace.
3. IPC Namespace (CLONE_NEWIPC) — Inter-Process Communication Isolation
The IPC Namespace isolates System V IPC objects and POSIX message queues. Processes inside a container cannot communicate with the host or other containers via shared memory, semaphores, or message queues.
OS context: Linux IPC mechanisms include semaphores, message queues, and shared memory. These are efficient means of inter-process communication, but without isolation, containers could interfere with each other or even steal data.
4. Network Namespace (CLONE_NEWNET) — Network Isolation
The Network Namespace provides each container with an independent network stack, including network devices, IP addresses, ports, routing tables, and iptables rules.
OS context: Each Network Namespace has its own:
- Network interfaces (e.g.,
eth0,lo) - IP addresses and port space
- Routing table
- Firewall rules (iptables/nftables)
/proc/netdirectory
This is why multiple containers can listen on port 80 simultaneously without conflicts. Docker’s bridge networking connects a container’s Network Namespace to the host using veth pairs.
5. Mount Namespace (CLONE_NEWNS) — Filesystem Isolation
The Mount Namespace isolates filesystem mount points, giving each container its own directory tree view.
OS context: This was the first Namespace implemented in Linux (2002, kernel 2.4.19), which is why its flag is named CLONE_NEWNS (NS = Namespace) rather than the more intuitive CLONE_NEWMOUNT. Later namespaces adopted more descriptive naming.
The Mount Namespace is the foundation of container filesystem isolation. Combined with the pivot_root syscall, a container can set its own rootfs (typically from a container image) as the root directory, completely hiding the host filesystem. The pivot_root procedure described in SPEC.md:
put_old = mkdir(...);
pivot_root(rootfs, put_old);
chdir("/");
unmount(put_old, MS_DETACH);
rmdir(put_old);6. User Namespace (CLONE_NEWUSER) — User Isolation
The User Namespace isolates user and group IDs. Root (UID 0) inside a container can be mapped to an unprivileged user on the host.
OS context: This is a critical line of defense for container security. Without User Namespace, root inside a container is root on the host — if a container escape occurs, the attacker gains full host privileges. With User Namespace, root inside the container is actually an unprivileged user on the host (e.g., UID 65534), limiting the damage even in case of an escape.
SPEC.md mentions that the parent process must write uid/gid mappings before initialization begins:
This allows the parent to have time to set the new process inside a cgroup hierarchy and/or write any uid/gid mappings required for user namespaces.
This mapping is done through the /proc/<pid>/uid_map and /proc/<pid>/gid_map files.
7. Cgroup Namespace (CLONE_NEWCGROUP) — Resource View Isolation
The Cgroup Namespace isolates the cgroup root directory view, so a container can only see the resource limits assigned to it.
OS context: Cgroups (Control Groups) are the Linux kernel’s resource limiting mechanism, used to constrain CPU, memory, IO, and other resource usage for processes. The Cgroup Namespace does not isolate resources themselves (that’s the job of cgroup subsystems) — it isolates the cgroup filesystem view.
Without Cgroup Namespace, a container can see its full path in the host’s cgroup hierarchy via /proc/self/cgroup, which both leaks host information and could be exploited to tamper with global resource allocation.
unshare: The Syscall That Creates Namespaces
SPEC.md explicitly states that namespaces are created via the unshare syscall. Linux provides three key syscalls for working with namespaces:
| Syscall | Purpose | Use Case |
|---|---|---|
clone | Create a new process and place it in new namespaces | Creating the container’s init process |
unshare | Detach the current process from its existing namespaces into new ones | runc initialization phase |
setns | Join the current process to an existing namespace | docker exec into a running container |
runc uses unshare rather than clone to create namespaces because runc’s init process needs to complete a series of setup steps in the current process context (such as cgroup configuration and uid/gid mapping) before detaching into the new namespaces.
It’s worth noting that although unshare is the syscall being invoked, the flags use the CLONE_* prefix. This is a historical artifact — these flags were originally designed for the clone syscall, and unshare later reused the same definitions.
How Namespaces Work with the Container Runtime
With an understanding of namespaces, we can trace the core flow of how runc creates a container:
- Parent process preparation: runc creates a pipe for parent-child process synchronization, passed to the init process via FD 3
- Create namespaces: The
unsharesyscall is invoked with all 7CLONE_*flags to create all namespaces - Parent process configuration: While the init process blocks waiting on the pipe, the parent places it into the correct cgroup hierarchy and writes uid/gid mappings
- Init process startup: The parent closes the pipe, and the init process continues execution, completing filesystem mounts,
pivot_root, and other operations - Execute user process: Finally,
execthe user-specified application
In this flow, namespace creation is the first and most critical step — it provides the foundational isolated environment for all subsequent configuration.
Summary
runc’s SPEC.md defines all 7 container namespaces with a concise table and a single sentence. These 7 namespaces isolate processes, hostnames, inter-process communication, networking, filesystems, users, and resource views respectively — together forming what we call a "container".
Understanding these namespaces is the foundation for diving deeper into runc’s source code. In upcoming articles, we will continue exploring the filesystem configuration, cgroup resource controls, security capabilities, and other sections of SPEC.md, gradually revealing the complete picture of a container runtime.