Before you start#
Review namespace lifecycles, cgroup controllers, credentials, and application sandboxes. This chapter pins Podman v5.3.0 to 874bf2c301ecf0ba645f1bb45f81966cc755b7da and runc v1.2.0 to 0b9fa21be2bcba45f6d9d748b4bcf70cfbffbc19. These are independently selected source versions, not a tested runtime combination.
Learning goals#
Separate OCI image content from a runtime bundle, follow a conditional Podman/conmon/runc creation path, account for child and descriptor ownership on failure, and explain rootless isolation without changing host containers or cgroups.
Source walkthrough#
- Image
Manifest, config and layers identify stored content.
- Podman
Prepares container state and selects a runtime.
- runc
Configures process namespaces and credentials.
- Application
Executes with host-kernel authority constraints.
An OCI image is distribution content: an index can select a platform-specific manifest, which points to a configuration object and layers by digest. A digest identifies bytes under a particular hash algorithm; it does not establish that executing those bytes is safe. An OCI runtime bundle instead provides a root filesystem and runtime configuration for a process. The image's content and metadata can be used to prepare that bundle, but an image manifest is not itself a running process, namespace, cgroup or runtime configuration.
The first exact excerpt is from Podman 5.3.0 libpod/oci_conmon_common.go, createOCIContainer. Podman's repository license is Apache-2.0. The excerpt omits earlier command and pipe setup and later error handling:
err = cmd.Start()
// regardless of whether we errored or not, we no longer need the children pipes
childSyncPipe.Close()
childStartPipe.Close()
if err != nil {
return 0, err
}
The parent starts the configured conmon command, closes its copies of child-side pipe endpoints regardless of whether start succeeded, then returns the start error when present. Closing these copies matters for EOF and ownership: leaving an extra write end open can make a reader wait after the actual writer exits. A successful Start only means a process was started; the following code signals conmon, waits for initial setup and reads synchronization data. It can still fail and request runtime cleanup.
The second exact excerpt is from runc 1.2.0 libcontainer/standard_init_linux.go, linuxStandardInit.Init. runc's repository license is Apache-2.0. Surrounding namespace setup and later seccomp handling are omitted:
if err := syncParentReady(l.pipe); err != nil {
return fmt.Errorf("sync ready: %w", err)
}
The init process signals readiness through its parent synchronization channel and returns a wrapped error if that step fails. The source comment explains why this handshake occurs before seccomp rules that could prevent required socket I/O. Readiness for the next stage is not proof that the application has executed successfully. The later path sets a process label, handles seccomp according to NoNewPrivileges, finalizes namespace credentials and looks up the executable; any of these can still fail.
Image, bundle, and process state#
An image index can describe multiple platform variants. The selected manifest refers to a configuration and ordered filesystem layers. Layer application creates a root filesystem view and must honor deletion markers and the image format's rules; the resulting filesystem is not a single layer file. Content hashes can detect changed bytes when verified, but an image built from untrusted instructions can still intentionally contain harmful programs. Image signatures, registry policy and runtime isolation answer different questions. This chapter does not pull, unpack or run an image.
A runtime bundle's configuration specifies process arguments and environment, root filesystem, mounts, namespaces, credentials and other execution settings. A bundle can be invalid even if its root filesystem exists: the executable might be absent, a mount source inaccessible, a namespace request forbidden or a cgroup unavailable. Conversely, a syntactically valid JSON document is not evidence that the requested runtime policy is secure. The OCI image and runtime specifications are distinct contracts; their versions and the implementation used should be recorded for a real test.
Podman is a container management tool with a daemonless user-facing architecture in common operation; it is not the Docker daemon. Its libpod code prepares container state and invokes a configured OCI runtime through an integration such as conmon. The selected runtime can be runc, crun or another supported implementation depending on configuration. This lesson chooses the runc path as an example and does not claim all Podman containers use it. Conmon is a separate monitor process with its own version and process lifetime; the quoted Podman code starts it and coordinates startup, but this lesson does not claim an independently pinned conmon implementation.
The conditional runc path#
A runtime creates a child process and arranges namespaces, filesystem, credentials, cgroups and security settings before executing the requested program. In runc, init and re-execution machinery lets a process enter a carefully prepared context; a parent/child synchronization channel coordinates stages. The exact sequence depends on configuration and whether a process is an initial container process or a later exec. A namespace is a kernel object referenced by processes and sometimes namespace fds. A process exiting releases its own references, but a separate process or descriptor can keep a namespace alive.
The sequence below is a teaching model of the Podman path. Conmon and the runtime are separate processes with separate lifetimes, and the sync pipe carries only the stage results shown.
- Podmanconmon
Start conmon with child sync pipes
- Podmanconmon
Signal start pipe after cgroup move
- conmonOCI runtime
Run runtime for the container
- OCI runtimeContainer init
Create namespaces and init process
- Container initOCI runtime
Return. Sync ready on the pipe
- conmonPodman
Return. Container PID on parent sync pipe
Time runs downward. Dashed arrows show returns.
Read the text version
- Podman → conmon: Start conmon with child sync pipes
- Podman → conmon: Signal start pipe after cgroup move
- conmon → OCI runtime: Run runtime for the container
- OCI runtime → Container init: Create namespaces and init process
- Container init → OCI runtime (return): Sync ready on the pipe
- conmon → Podman (return): Container PID on parent sync pipe
The runc linuxStandardInit.Init source performs multiple checks and setup operations before and after its readiness handshake. It can configure network and other settings, set security labels, apply seccomp in a branch determined by NoNewPrivileges, finalize credentials and check the executable path. The error returned by syncParentReady is distinct from an error setting an execution label or finding an executable. A parent must interpret the actual stage result and clean up what that stage acquired. A readiness message is a protocol event, not a universal certificate that the process reached execve.
In libcontainer/standard_init_linux.go, runc 1.2.0, Apache-2.0 for the repository (the surrounding namespace and credential code is omitted):
// Without NoNewPrivileges seccomp is a privileged operation, so we need to
// do this before dropping capabilities; otherwise do it as late as possible
// just before execve so as few syscalls take place after it as possible.
if l.config.Config.Seccomp != nil && !l.config.NoNewPrivileges {
seccompFd, err := seccomp.InitSeccomp(l.config.Config.Seccomp)
if err != nil {
return err
The condition selects where the seccomp filter is loaded. With NoNewPrivileges unset, the comment says seccomp is a privileged operation that must be set up before capabilities are dropped; with it set, a later branch installs the filter as close to execve as possible. The excerpt does not show which branch a given bundle takes, and the return err line reports failure only.
In the chosen Podman path, the parent constructs a conmon command and child synchronization pipes. After the quoted Start and closure of child endpoints, it signals the monitor and waits for initial setup. The source has an EPIPE distinction when the monitor may already have exited; other signal errors can lead to killing the process. It calls Wait to reap the initial process and then reads the synchronization pipe. If reading that data fails, the code calls its runtime deletion path before returning the error. Each branch has a different set of live resources, so an audit must follow the actual parent and child references.
In libpod/oci_conmon_common.go, Podman 5.3.0, Apache-2.0 for the repository (earlier and later lines are omitted):
if err := r.moveConmonToCgroupAndSignal(ctr, cmd, parentStartPipe); err != nil {
// The child likely already exited in which case the cmd.Wait() below should return the proper error.
// EPIPE is expected if the child already exited so not worth to log and kill the process.
if !errors.Is(err, syscall.EPIPE) {
logrus.Errorf("Failed to signal conmon to start: %v", err)
if err := cmd.Process.Kill(); err != nil && !errors.Is(err, syscall.ESRCH) {
logrus.Errorf("Failed to kill conmon after error: %v", err)
}
}
}
/* Wait for initial setup and fork, and reap child */
err = cmd.Wait()
if err != nil {
return 0, fmt.Errorf("conmon failed: %w", err)
The code logs and kills conmon for a signal error other than EPIPE, which it treats as expected when the child has already exited. cmd.Wait then reaps conmon and reports the early exit. The excerpt does not show what conmon reports on its own, and a successful signal does not establish that the container started.
Rootless authority and resource limits#
Rootless execution maps a range of container UIDs and GIDs to identities available to an unprivileged host user. A process can appear as UID zero within its user namespace while remaining a mapped ordinary user outside it. Helpers and subordinate ID ranges may be needed for multi-ID mappings, depending on system configuration and policy. A failed mapping is not fixed by pretending that the container is privileged. Capabilities inside a user namespace have scoped meaning; they are not automatically capabilities in the initial host user namespace.
The mapping below is a teaching model with example IDs. A real rootless map depends on the host's subordinate-ID configuration and on the helpers in use.
Read the text version
- Container UID 0: Root inside the user namespace only → Host UID 1000: The ordinary user who runs the container (single-ID map entry)
- Container UID 1000: A second ID that needs its own map entry → Subordinate host IDs: A range from helper configuration, when multi-ID maps are used (mapped only if a range covers it)
Cgroup v2 can limit or account for CPU, memory, I/O and process counts when the relevant controller and delegation are available. A rootless manager may lack authority to create or configure a requested subtree. The runtime should report the failure or use its documented configuration; a shared kernel does not automatically provide a delegated controller. Network setups such as pasta or slirp variants are separate userspace projects and depend on their own versions and configuration. This lesson does not pin them or claim that either is available.
A container shares the host kernel. Namespaces, cgroups, seccomp, capabilities and LSM rules constrain different aspects of authority, but a writable bind mount or inherited descriptor can expose host data. A privileged configuration can grant substantially broader access and should not be treated as equivalent to a restricted rootless process. SELinux labels and mount options influence access where enabled; changing host labels is not a prerequisite for this source exercise. A VM has a different guest-kernel boundary, though it also depends on its VMM and host policy.
Worked startup failure and descriptor ledger#
The following case is synthetic, not a captured Podman or runc run. A manager has prepared a bundle and creates a synchronization pipe with parent read end R and child write end W. It starts the monitor, closes its own copy of W, and retains R to receive a startup result. Assume the monitor successfully starts a runtime child but the child fails while looking up a nonexistent executable before completing the intended application exec. The runtime reports an error through its appropriate channel and exits. The parent reads the error or EOF, waits for the relevant child and invokes cleanup for runtime state that was created.
Now introduce a bug in this hypothetical program: an unrelated helper retains an extra copy of W. The runtime child exits, but R may not receive EOF until that helper also closes W. A reader that waits forever for EOF can hang despite the original child having terminated. The correct ownership table lists every duplicate descriptor, the process holding it and the point at which it closes. A numerical fd value is process-local; fd 5 in the parent does not automatically name the same endpoint as fd 5 in the child.
The actual Podman excerpt demonstrates closure of two parent-side child endpoints after cmd.Start; it does not prove every hypothetical helper is leak-free. The later cmd.Wait reaps the initial monitor process, and the source's failed sync-data branch invokes runtime deletion. A namespace or mount can outlive one process if another process or fd retains a reference. A failure report should therefore distinguish process exit, child reaping, descriptor closure, runtime deletion and persistent storage cleanup. Do not claim that a host mount or cgroup was created in this model.
A separate invalid-bundle counterexample is a configuration with an empty process argument list. It can be valid JSON but cannot describe the intended executable launch. The exact error returned depends on which validator and runtime version receive it. A source-only model can flag the missing field; it cannot establish runc's runtime error without executing or tracing the precise validation path. No container is started here.
Security and operational interpretation#
An application can access a mounted host directory according to the actual mount mode, credentials and host security checks. A read-only mount limits writes through that mount but does not erase data already accessible through another mount, socket or inherited descriptor. An application with network access may transmit data it can read. Image provenance and sandbox configuration need separate review. A container is not a safe place to run untrusted code merely because its filesystem looks isolated.
An OCI lifecycle has multiple states and failure points. A management command can prepare storage but fail before a runtime child exists; a runtime can create a process that fails before exec; an application can start and exit with its own status. Those cases have different owners and cleanup obligations. Do not interpret one generic exit code as a complete root-cause analysis. A useful report records the selected runtime, bundle configuration, rootless mapping, cgroup availability and failing stage, without collecting unrelated host containers or secrets.
Reviewing a rootless configuration#
Consider a private hypothetical bundle whose process asks to run as UID zero inside a new user namespace. Its mapping assigns that inside identity to an ordinary outside UID. If it requests a second inside UID without a valid mapping, a credential transition can fail even though the first UID is representable. The configuration and helpers must establish the relevant map before the runtime can safely use those identities. A successful map does not grant host access beyond the mapped credentials, existing descriptors and applicable policy.
Now add a writable volume pointing at an authored host directory. Its contents are outside the image layer and can persist after the container process exits. A cleanup operation that removes runtime state does not necessarily delete or restore that directory, nor should an investigator assume it does. If the same directory is supplied read-only, the process may still have another writable route if an inherited descriptor or separate mount exposes it. The correct authority analysis inventories all routes rather than reading one flag in isolation. These are hypothetical configuration cases; no volume or namespace is created here.
Practice#
This owned JSON model checks a minimal authored configuration; it does not invoke Podman, runc, conmon, namespaces or cgroups:
python3 - <<'PY'
import json
for raw in ('{"process":{"args":["/bin/example"]}}', '{"process":{"args":[]}}'):
obj = json.loads(raw)
args = obj.get('process', {}).get('args', [])
print('synthetic executable present:', bool(args and args[0]))
PY
The first row is true and the second false. This only checks an authored field; it does not validate the OCI specification, filesystem or executable. A nonempty string naming a nonexistent program is a counterexample to treating this result as a successful container start. If Python is unavailable, inspect the two examples by hand.
For a source-only alternative, draw the synthetic parent/monitor/runtime pipe ledger. Explain why closing the parent's copy of W matters, what an extra helper copy changes, when the initial child must be waited for and why a namespace fd can outlive the child. Add the cited runc readiness error and Podman cleanup branch. No host container inventory or runtime execution is required.
Knowledge check#
- Why is an OCI image manifest not an OCI runtime bundle?
- Why does the Podman parent close child-side pipe endpoints after starting conmon?
- What does runc's ready handshake not prove?
- Why can a rootless container's UID zero lack host root authority?
- Why can EOF be delayed after the intended writer exits?
Answers#
- The image describes distributed content and configuration; the bundle supplies an execution root filesystem and runtime process configuration. They have different purposes and lifetimes.
- The parent no longer needs those copies. Keeping extra endpoints can leak descriptors and keep a pipe open, preventing expected EOF.
- It is one setup-stage synchronization event. Later credential, security, executable lookup or exec steps can still fail.
- UID mappings and capability checks are scoped to the relevant user namespace. The host sees mapped credentials and applies its own access policy.
- Another process or duplicate descriptor can still hold a write end. EOF requires all relevant writers to be closed.
Checkpoint#
Submit an image-versus-bundle table and the startup failure ledger with parent, child, pipe and namespace references. Pass when runtime selection is conditional and successful JSON parsing is not described as a running container. Revisit the cited error branches if a child exit is treated as proof that every descriptor and namespace reference is gone. This is not certification.