Userspace contracts, systemd, distributions, and containers

Relate system managers, containers and distribution choices to kernel interfaces.

14 min readChapter 89 of 168Expert practice
In this chapter

Before you start#

Review init and userspace, exec, mount namespaces, credentials, and cgroup v2. Linux source is pinned to upstream v6.12. The optional systemd discussion refers specifically to systemd v256, a separate historical userspace project, not a component of that kernel tag.

Learning goals#

Explain the kernel-to-PID-1 boundary, distinguish systemd unit and socket policy from kernel mechanisms, and trace a hypothetical container's namespace, credential, filesystem and cgroup state with its failure and cleanup obligations.

Source walkthrough#

Userspace contracts, systemd, distributions, and containers
Kernel
PID 1 execution, namespaces, credentials and VFS
Userspace manager
Units, socket activation, logging and service policy
Container setup
Root tree, child namespaces, delegated cgroup and restrictions
Application
Inherited fds and actual runtime behavior
Conceptual model. Source-based conceptual overview, not a captured execution; architecture, policy, configuration and actual evidence determine behavior.

The kernel creates the initial userspace task after early initialization and executes an init program. PID 1 has special lifecycle and signal semantics: it adopts orphaned descendants when they become orphaned, and certain default signal behaviors are restricted for namespace init. A functioning init must arrange child reaping and service lifecycle; the kernel does not require that init to be systemd. A minimal custom init, another init system or an initramfs program can fulfill the relevant role. A container's PID 1 is PID 1 in its own PID namespace when configured that way, while it has another PID in an ancestor namespace. These rules do not make PID 1 the adopter of every orphan in the system: reparenting depends on the process tree and namespace.

In init/main.c, run_init_process prepares the argument vector and calls kernel_execve. The following exact v6.12 GPL-2.0-only excerpt is from try_to_run_init_process; surrounding declarations and diagnostic are omitted:

c
	ret = run_init_process(init_filename);

	if (ret && ret != -ENOENT) {

The first statement attempts to execute a candidate init. The condition distinguishes an execution error other than “not found” for its diagnostic; the function ultimately returns the error to its caller. An existing file can fail to execute because of permissions, interpreter or loader issues, architecture mismatch or other errors. The kernel's later init fallback and panic behavior depend on the selected init arguments and source path. A missing init in an initramfs is not automatically proof that systemd itself is defective.

The kernel supplies interfaces such as system calls, VFS, procfs, sysfs, device APIs and cgroup control. Userspace decides much of the policy: which root filesystem to mount, what services to launch, which device events to handle and how to collect logs. An initramfs is an early userspace archive with its own programs, libraries and configuration; it can find the real root filesystem and switch to the next init. A distribution's package set, bootloader, kernel config, modules and userspace ABI expectations must match sufficiently for that particular system. A successful kernel build alone does not create a bootable root filesystem or a working service manager.

systemd is a userspace implementation#

In systemd, units describe service and resource management objects. Dependency and ordering relationships are different: an ordering edge describes relative start or stop order, while requirement or wants relationships affect what jobs are pulled in and how failure is handled. The exact unit directives and versioned semantics are userspace policy. A dependency graph can be valid even when a service fails because its executable, permissions, filesystem, configuration or dependency is unavailable.

Socket activation lets a manager arrange a listening socket and pass an already-open descriptor to a service. This separates socket availability from service process startup and can queue work under the socket's actual limits, but it does not make service initialization instantaneous or eliminate overload. The descriptor is a kernel file reference; its meaning and handoff convention are defined by the userspace manager and service. The v256 socket unit source and socket documentation show a specific implementation, not a Linux v6.12 kernel API definition.

Socket activation handoff
  1. Socket owned

    The manager creates and keeps the listening socket under its unit policy.

  2. Descriptor passed

    The service receives an already-open descriptor under the manager activation protocol.

  3. Service accepts

    The service handles connections through the inherited descriptor; it need not bind the socket itself.

  4. Restart

    The listener can stay open while the service restarts, but the queue remains finite.

  5. Failure visible

    A service failure or journal line is a manager event, not proof that every request was handled.

Conceptual model. Conceptual model of socket activation as the chapter describes it, with the systemd v256 socket documentation as the reference. Not a systemd trace.

journald is a userspace log service; kernel messages can be read through appropriate interfaces and forwarded or stored according to configuration. A journal entry's timestamp, collection delay, rate limits and storage policy differ from the kernel printk ring's own behavior. udev is a userspace device-event and rule-processing component in the systemd project; the kernel device model and sysfs expose device state and events, while userspace names, permissions and actions are policy. Neither a device node nor a journal line alone proves the corresponding service or hardware is healthy.

Building a container as a sequence#

A container is a userspace construction over a shared kernel, commonly combining namespaces, a root filesystem, credentials, LSM policy, seccomp and cgroups. It is not a separate VM or kernel. A runtime's exact sequence is implementation-specific, but a useful hypothetical sequence is: create a child with selected namespace flags; establish permitted UID/GID mappings; arrange a mount tree and propagation; enter or pivot to a prepared root; set credentials and capability sets; apply selected LSM and seccomp restrictions; place tasks in delegated cgroups; and execute the application. Some steps must occur in a different order depending on privilege and the runtime's protocol. Each failure needs cleanup of acquired namespace fds, mounts, pipes, temporary directories and child processes.

The root filesystem can be an image, a bind-mounted tree or an overlay assembled by userspace. pivot_root and mount operations have namespace, capability, mount topology and propagation requirements. An opened directory fd or bind mount can retain access to an object even after a pathname view changes. The runtime must manage the lifetime of the mount tree and any shared layers until child tasks and references are gone. A rootless runtime has capabilities inside a child user namespace under mapped IDs, but does not acquire initial-user-namespace authority merely by displaying UID 0 inside the container. Mapping ranges, setgroups, filesystem support and host policy can prevent a requested setup.

The kernel rejects inconsistent clone combinations before creating a task. In kernel/fork.c, this exact v6.12 GPL-2.0-only excerpt is from copy_process; surrounding checks are omitted:

c
	if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
		return ERR_PTR(-EINVAL);

The condition tests whether the caller simultaneously requests a new mount namespace and sharing of the filesystem context under these flags. If both are present, the function returns an encoded -EINVAL error instead of producing a child. Nearby checks reject other inconsistent combinations, such as a new user namespace with CLONE_FS and a thread group without shared signal handlers. The caller must recognize the error and unwind its own prepared resources. Passing this check does not prove later namespace creation, security checks, allocation or exec succeeds. See the pinned clone(2) documentation for the userspace API and flag meanings.

A concrete synthetic container state#

Assume a runtime starts from a host task in user namespace U0 and cgroup /service. It creates a child user namespace U1 mapping child UID 0 to host UID 1000, a new mount namespace M1, and a new PID namespace for the child's descendants, while leaving the network namespace shared in this example. Inside U1 the child can display UID 0; on the host it maps to UID 1000. It does not gain U0's host-wide capabilities. M1 can expose a restricted root tree, but inherited mount propagation and retained file descriptors must be inspected separately. The shared network namespace means network interfaces and routes are not isolated merely because the root filesystem differs.

Suppose the manager then delegates /service/job and places the workload there under cgroup v2. The cgroup namespace can display a shorter path, but a view change alone does not enable controllers or apply limits. If the parent has enabled a memory controller and the delegated policy sets a high boundary, the workload can experience reclaim pressure; if not, no such limit follows from the container label. An LSM profile and seccomp filter can further restrict operations, subject to their own installation and inheritance rules. A process with an inherited privileged fd may retain authority to use it, subject to later checks, even after its pathname view and effective IDs change.

Now make the hypothetical clone request use CLONE_NEWNS | CLONE_FS. The quoted kernel branch returns -EINVAL; the runtime must not report a running container or proceed as though the child exists. If creation succeeds but a later mount or exec fails, the runtime must report that stage and clean up the already-created child and references. A process visible in a PID namespace is not proof that the intended seccomp filter or cgroup membership was installed. Record the actual stage and error instead of collapsing all failures into “container failed.”

Init, services and inherited resources#

A service process receives arguments, environment, credentials, open fds and namespace membership from its launcher. An exec replaces the process image but does not necessarily close every descriptor; close-on-exec and explicit handoff matter. A service can therefore receive a listening socket from a manager without itself binding that socket. It can also accidentally inherit an unintended descriptor if the launcher fails to close it. The kernel provides fd and exec semantics; the service manager's activation protocol and configuration define which descriptors are intentional.

Service ordering is not the same as readiness. A job can be ordered after another unit but still fail if the earlier service reports startup before its dependent resource is usable. Socket activation can help decouple startup, but queue limits and service failure still matter. A journal record that a service started is evidence of a manager event, not proof that every request was handled. Diagnose the actual unit state, exit status, dependencies, socket ownership and application logs under the pinned systemd version.

PID 1 also affects shutdown. A service manager needs to signal and reap children, manage remaining processes and coordinate mounts or storage. In a PID namespace, if the namespace's init exits, the kernel's PID-namespace lifecycle affects remaining tasks. A tiny container application that never reaps children can accumulate zombies; adding an init process changes the process tree and signal forwarding, so it should be a deliberate design choice. PID 1 special handling and namespace membership are kernel behavior; the choice of supervisor and policy remains userspace.

In kernel/pid_namespace.c, v6.12, SPDX GPL-2.0-only, the teardown path in zap_pid_ns_processes first makes the exiting thread ignore SIGCHLD, so terminated children are reaped automatically (the longer comment below this block and the later reaping loop are omitted):

c
	/*
	 * Ignore SIGCHLD causing any terminated children to autoreap.
	 * This speeds up the namespace shutdown, plus see the comment
	 * below.
	 */
	spin_lock_irq(&me->sighand->siglock);
	me->sighand->action[SIGCHLD - 1].sa.sa_handler = SIG_IGN;
	spin_unlock_irq(&me->sighand->siglock);

The function then sends SIGKILL to the remaining tasks in the namespace and waits for them. This excerpt does not show that every task was reaped, how tasks in a different namespace that were created with setns() and fork() are handled, or whether a container runtime's own supervisor cleaned up first.

PID namespace teardown
PID namespace teardown1. Namespace init thread to Remaining tasks: sets SIGCHLD to ignore, then sends SIGKILL to each remaining task. 2. Remaining tasks to Namespace init thread (return): tasks exit; the wait loop reaps until -ECHILD.Namespace init threadRemaining taskssets SIGCHLD to ignore, then sends SIGKILL to eachremaining tasktasks exit; the wait loop reaps until -ECHILD
  1. Namespace init threadRemaining tasks

    sets SIGCHLD to ignore, then sends SIGKILL to each remaining task

  2. Remaining tasksNamespace init thread

    Return. tasks exit; the wait loop reaps until -ECHILD

Time runs downward. Dashed arrows show returns.

Read the text version
  1. Namespace init thread → Remaining tasks: sets SIGCHLD to ignore, then sends SIGKILL to each remaining task
  2. Remaining tasks → Namespace init thread (return): tasks exit; the wait loop reaps until -ECHILD
Source map. Source map of zap_pid_ns_processes in kernel/pid_namespace.c (Linux v6.12), reduced to two actors. Not a captured container shutdown.

A distribution can choose a different initramfs generator, service manager, libc, package manager and kernel configuration. A container image typically supplies userspace binaries while using the host kernel's system-call implementation. A newer userspace can encounter an older kernel lacking an expected feature; a newer kernel can still be configured without an optional feature. ABI compatibility is a contract to test, not a guarantee that every feature or device exists. Record the image, runtime and kernel versions separately; this chapter makes no claim about a particular runc release.

A failed launch and the authority that remains#

Extend the synthetic container sequence with a parent runtime that has already opened a root-directory fd and created a communication pipe before cloning. If the clone fails validation, the parent remains responsible for closing those fds and removing any private temporary state it created. If the child starts but cannot execute its target, the parent must distinguish that exec error from successful application startup, reap the child and release namespace and mount references according to the runtime's design. A cleanup path that reports success while leaving a child or mount pinned can confuse later retries and resource accounting.

If exec succeeds, the new image inherits only the descriptors allowed by the launcher's fd setup and close-on-exec flags, but a surviving descriptor can still represent authority outside the visible root tree. The exact operation may perform further credential or LSM checks; neither a pathname restriction nor an fd alone tells the entire authorization story. A good runtime review lists each deliberately inherited fd, its owner and intended lifetime, then tests failures at mapping, mount, policy installation and exec in a disposable guest. That is a security and reliability exercise, not a reason to run a privileged container on the host.

The same discipline applies to system services. A unit may be ordered after a mount but still encounter an unavailable or read-only filesystem; socket activation can retain a listener while the service process restarts, yet the queue remains finite. The kernel reports concrete errors through syscalls, while the manager decides restart, dependency and logging policy. Distinguish the kernel's operation result from the userspace manager's reaction when diagnosing a boot or service failure.

Practice#

Read-only host or guest inventory

On an authorized system, inspect PID 1 and your own namespace and cgroup views without starting or changing services:

sh
uname -r
printf 'PID 1: '; cat /proc/1/comm 2>/dev/null || echo 'unavailable'
printf 'own cgroup:\n'; cat /proc/$$/cgroup
printf 'own UID/GID mappings:\n'; cat /proc/$$/uid_map /proc/$$/gid_map
for f in /proc/$$/ns/*; do [ -e "$f" ] && printf '%s -> %s\n' "${f##*/}" "$(readlink "$f")"; done
if command -v systemctl >/dev/null 2>&1; then systemctl show --property=Version 2>&1 || true; else echo 'systemctl unavailable'; fi

/proc/1/comm describes PID 1 visible in this procfs view, which may be a container's PID 1 rather than the host's. Namespace links and cgroup paths describe a view, not a complete security policy. systemctl may exist without a reachable systemd manager or may be denied; that is a valid observation. Do not start, restart or stop services, create containers, mount filesystems or escalate privilege. Avoid sharing identifiers or paths that reveal sensitive deployment details.

Source-only alternative: use the synthetic U0/U1, M1 and shared-network state above. Draw a table for displayed UID, parent mapping, root view, network sharing and cgroup membership; then apply the quoted invalid clone combination and identify two cleanup obligations after a later exec failure. Deliver the table or actual read-only inventory, marking the example as hypothetical and no container as launched.

Knowledge check#

  1. Why does UID 0 in U1 not automatically grant host-root authority in U0?
  2. What happens to the shown clone request with both CLONE_NEWNS and CLONE_FS?
  3. Why can a socket-activated service receive requests without itself binding the socket?
  4. Does a cgroup namespace path view itself enable a memory limit?
  5. Why is PID 1 responsible for more than simply starting an application?

Answers#

  1. Identity mappings and capability checks are scoped to user namespaces and resources. The child mapping does not grant capabilities in the initial namespace.
  2. copy_process returns ERR_PTR(-EINVAL) on that validation path. The runtime must handle the error rather than assume a child exists.
  3. A userspace manager can create and retain the listening socket, then pass an fd under its activation protocol. The kernel supplies fd and socket semantics, not the manager's policy.
  4. No. It changes the path view; controller enablement, membership and configured limits are separate hierarchy operations.
  5. PID 1 has special signal and namespace lifecycle behavior and must arrange appropriate child reaping and shutdown for its role; the exact supervisor is userspace policy.

Checkpoint#

Submit the synthetic container state table and clone-failure/cleanup timeline. Ready means user namespace authority, mount and network views, cgroup limits, PID 1 and inherited fds are distinct, with no claim that the example container ran. Retry the source branch if a displayed UID is treated as host-wide authority. This is not certification.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.