Introduction
In the preceding seven articles, we systematically read through runc/libcontainer/SPEC.md, grasping the theoretical foundations of the isolation, limitation, and security mechanisms relied upon by Linux containers.
Theory needs to be translated into engineering. In the Open Container Initiative (OCI) standard, the only communication protocol between the higher-level container engine (like Docker, containerd) and the low-level runtime (runc) is a configuration file named config.json.
In the runc source code, the Go language data structure corresponding to this JSON resides in a third-party dependency package: github.com/opencontainers/runtime-spec/specs-go/config.go. This file is over 1000 lines long. For clarity, we will deeply deconstruct it across several articles. This is the first piece, primarily focusing on the container’s top-level base structure (Spec) and the critical process environment (Process).
1. The Top-Level Structure: Spec
The Spec struct is the entry point for the entire config.json, outlining the complete picture of the container.
type Spec struct {
// Version of the Open Container Initiative Runtime Specification with which the bundle complies.
Version string `json:"ociVersion"`
// Process configures the container process.
Process *Process `json:"process,omitempty"`
// Root configures the container's root filesystem.
Root *Root `json:"root,omitempty"`
// Hostname configures the container's hostname (corresponds to UTS Namespace).
Hostname string `json:"hostname,omitempty"`
// Domainname configures the container's domainname.
Domainname string `json:"domainname,omitempty"`
// Mounts configures additional mounts (on top of Root) (corresponds to Mount Namespace).
Mounts []Mount `json:"mounts,omitempty"`
// Hooks configures callbacks for container lifecycle events.
Hooks *Hooks `json:"hooks,omitempty" platform:"linux,solaris,zos"`
// Annotations contains arbitrary metadata for the container (key-value pairs).
Annotations map[string]string `json:"annotations,omitempty"`
// Linux is platform-specific configuration for Linux based containers.
Linux *Linux `json:"linux,omitempty" platform:"linux"`
// ... Windows, Solaris, VM specific configs omitted here
}Analysis:
- Cross-platform Design: You can see that
Specabstracts all common attributes of containers at the top level (process, root directory, mounts, hostname), while encapsulating underlying system-level features (like Cgroups, Namespaces, Seccomp) into platform-specific fields (likeLinux,Windows). - Hooks (Lifecycle Callbacks): This is crucial. It allows external programs to intervene and execute custom logic before the container starts (PreStart), after creation (CreateRuntime), after startup (PostStart), and after stopping (PostStop). For example, allocating a virtual NIC for the host and pushing it into the container’s network namespace.
2. The Root Filesystem: Root
Root describes what the / (root directory) looks like from the perspective of the container process.
type Root struct {
// Path is the absolute path to the container's root filesystem.
Path string `json:"path"`
// Readonly specifies if the root filesystem should be mounted as readonly.
Readonly bool `json:"readonly,omitempty"`
}Linking back to our SPEC knowledge, runc utilizes this Path, combined with the pivot_root system call, to "jail" the container process within this path. If Readonly = true is configured, it effectively prevents malicious processes from tampering with base system files.
3. The Core Execution Unit: Process
If Root and Linux define the stage and the walls, then Process is the actor dancing on the stage. It defines the complete context of the container’s main process.
type Process struct {
// Terminal creates an interactive terminal for the container.
Terminal bool `json:"terminal,omitempty"`
// ConsoleSize specifies the size of the console.
ConsoleSize *Box `json:"consoleSize,omitempty"`
// User specifies user information for the process.
User User `json:"user"`
// Args specifies the binary and arguments for the application to execute.
Args []string `json:"args,omitempty"`
// Env populates the process environment for the container.
Env []string `json:"env,omitempty"`
// Cwd is the current working directory for the process and must be relative to the container's root.
Cwd string `json:"cwd"`
// Capabilities are linux capabilities that are kept for the container.
Capabilities *LinuxCapabilities `json:"capabilities,omitempty" platform:"linux"`
// Rlimits specifies rlimit options to apply to the process.
Rlimits []POSIXRlimit `json:"rlimits,omitempty" platform:"linux,solaris"`
// NoNewPrivileges controls whether additional privileges could be gained by processes in the container.
NoNewPrivileges bool `json:"noNewPrivileges,omitempty" platform:"linux"`
// ApparmorProfile specifies the apparmor profile for the container.
ApparmorProfile string `json:"apparmorProfile,omitempty" platform:"linux"`
// SelinuxLabel specifies the selinux context that the container process is run as.
SelinuxLabel string `json:"selinuxLabel,omitempty" platform:"linux"`
// OOMScoreAdj adjusts the oom-killer score.
OOMScoreAdj *int `json:"oomScoreAdj,omitempty" platform:"linux"`
}Deep Dive into Process Fields
User(Identity)gotype User struct { UID uint32 `json:"uid" platform:"linux,solaris"` GID uint32 `json:"gid" platform:"linux,solaris"` AdditionalGids []uint32 `json:"additionalGids,omitempty" platform:"linux,solaris"` // ... }The
UIDandGIDhere are identifiers inside the container. If User Namespaces are enabled, there will be a mapping table in the underlyingLinuxconfig, mapping the internal UID (like 0/root) to a normal unprivileged UID on the host (like 100000). This is the security cornerstone of Rootless containers.Capabilities(Privilege Control)gotype LinuxCapabilities struct { Bounding []string `json:"bounding,omitempty"` Effective []string `json:"effective,omitempty"` Inheritable []string `json:"inheritable,omitempty"` Permitted []string `json:"permitted,omitempty"` Ambient []string `json:"ambient,omitempty"` }Remember the default Capabilities list (like
CAP_NET_BIND_SERVICE) we saw in the Security post? Linux Capabilities are divided into 5 sets:Bounding: The upper limit; boundaries the process can never surpass.Effective: The capabilities currently active and taking effect for the process.Permitted: The capabilities the process is allowed to possess (and can activate into Effective at any time). These three sets precisely delineate the maximum blast radius should a hacker compromise the process.
NoNewPrivilegesThis maps to the Linuxprctl(PR_SET_NO_NEW_PRIVS)syscall. If set totrue, even if a process inside the container executes a binary with thesuidflag (like thesudocommand), the kernel will refuse to grant it higher privileges. This is a mandatory security option in multi-tenant clusters.Rlimits(Process-Level Hard/Soft Limits) Do not confuse this with Cgroups. Cgroups restrict the CPU and memory of a group of processes, whereasRlimits(via thesetrlimitcall) restricts system-level quotas for a single process, such as the famousRLIMIT_NOFILE(max open file descriptors) orRLIMIT_NPROC(max child processes).
Conclusion
Through the Spec and Process structs in config.go, we can clearly see how the OCI specification passes runtime instructions to runc via JSON data structures.
In the next article, we will delve into the most hardcore part of config.go—the Linux struct—and meticulously deconstruct the configuration definitions for Namespace isolation, Cgroups resource binding, and Seccomp syscall filtering.