Operating Systems · Isolation and Namespaces
PID Namespaces and the Special Status of PID 1
How PID numbers become per-namespace identifiers, why the first process in a new namespace cannot be killed from inside it, and how namespace teardown depends on that process exiting.
A PID is not a number. It is a handle to a struct pid object, and the number a process sees is chosen from a per-namespace allocator over that object. Two processes on one machine can each hold PID 42, and each is correct within the namespace that issued the number. Once that is understood, most of the strange behaviour of container init processes follows: the process numbered 1 in a namespace is not privileged by virtue of the number, it is constrained by it, and the constraint is what keeps the namespace from being left behind.
One object, many numbers
struct pid is allocated once per process and holds an array of upid entries, one for each namespace level the process belongs to, sized when the object is allocated. The array is indexed by namespace nesting level, and each entry caches the number that namespace assigned. A process in a namespace nested three levels deep has four entries: one for its own namespace, one for the parent, one for the grandparent, and one for the initial namespace. Looking up a PID inside a namespace means hashing on (namespace, number); looking up the same process from the initial namespace means hashing on a different pair.
This design makes translation cheap in the direction that matters most. When a process in the initial namespace signals a container process, it uses the initial namespace’s number and the kernel resolves it in that namespace’s hash table; the resulting struct pid carries the numbers for every level, so the target is unambiguous. Translation in the other direction — asking what a descendant process’s number is in an ancestor namespace — requires walking the array and is only available to processes in the ancestor namespace, which is why /proc in the initial namespace can show container processes under host PIDs while /proc inside the container cannot show host processes at all.
CLONE_NEWPID has a peculiarity that catches nearly every implementation once. The flag applies to children, not to the caller: clone with CLONE_NEWPID places the new child in a fresh PID namespace, and the calling process stays where it was. unshare(CLONE_NEWPID) likewise changes only the namespace that subsequent children are born into. The reason is structural: a process cannot change the namespace its own struct pid was allocated in. The consequence is that the standard sequence is unshare followed immediately by fork, with the forked child numbered 1.
/* unshare moves no process; the next fork lands in the new namespace. */
if (unshare(CLONE_NEWPID | CLONE_NEWNS) < 0)
die("unshare");
pid_t child = fork();
if (child == 0) {
/* This process is PID 1 in its own namespace. */
mount("proc", "/proc", "proc", 0, NULL);
execve("/bin/sh", argv, envp);
}
Why 1 cannot be killed
Signals delivered to PID 1 in a namespace are filtered by the kernel before the default action is applied. Only a signal for which the init process has installed a handler is delivered at all; every other signal is discarded, whatever its default action would have been, and that includes SIGKILL and SIGSTOP, which have no handler and cannot be caught. This is the same rule that applies to the initial namespace’s init process, generalised to every namespace, and it exists for the same reason: the kernel has no way to cleanly shut down an arbitrary process tree, so it declines to try.
The filtering is asymmetric with respect to origin. A signal sent from inside the namespace reaches init only through a handler. A signal sent from an ancestor namespace is subject to the same handler requirement, with one exception: SIGKILL and SIGSTOP are forcibly delivered across that boundary, which is why a process in an ancestor namespace can terminate a descendant namespace’s init. The initial namespace’s kill is therefore the mechanism a container runtime uses to terminate a container, and it works on PID 1 where a signal from inside would be ignored.
The direction of that permission is the whole of the security story: authority flows from ancestor to descendant and never the reverse. A container process cannot signal a host process even if it learns the host PID, because the check runs on the namespace relationship recorded in the struct pid rather than on the number it was handed. The same relation governs ptrace attachment and the visibility of /proc/<pid> entries, so a host process can inspect a container’s processes while a container cannot see the host’s at all.
Teardown depends on init exiting
When the init process of a PID namespace terminates, the kernel sends SIGKILL to every other process in that namespace and to every process in namespaces descended from it. There is no way to survive this: the signal is generated inside the kernel, and the namespace’s members have no ancestor relationship that would let them escape. The namespace is destroyed when the last process in it exits and the last reference to the namespace object drops.
That mechanism is what makes a container’s lifetime well defined, and it is also a source of hangs. If PID 1 blocks in an uninterruptible sleep (waiting on a filesystem whose backing store is gone, for example), the SIGKILL cannot take effect, and the namespace cannot be torn down. A runtime that waits for the container to exit waits forever, and the process cannot be removed by ordinary means because the only lever it has is the same signal that is already pending. The usual recovery is to remove the underlying blockage, not to kill harder.
A related obligation falls on PID 1 by convention rather than by kernel enforcement: orphaned processes are reparented to the init of their namespace, so PID 1 must reap them. If it does not call wait, the zombies accumulate and consume PID slots. This is why minimal container images often ship a small init whose only job is to reap, and why a shell used as PID 1 in a container is a poor choice: an interactive shell handles SIGCHLD only in specific ways and does not reliably reap.
Limits and observability
Namespace nesting is capped at 32 levels, and because the array is sized by level, the cap bounds the number of translations a single lookup can require. The number of PIDs available inside a namespace is bounded by the pid_max value in force for that namespace — 32768 by default, with a hard ceiling of 4194304 — and by the namespace’s own accounting of live PIDs. A namespace that has exhausted its allocator fails fork with EAGAIN, which surfaces in a container as a failing process launch rather than as a memory error.
Visibility is one-directional. A process in an ancestor namespace sees every descendant process, with the ancestor’s numbering, through /proc. A process in a descendant namespace sees only its own namespace’s processes, and the numbers it sees are its own. The /proc filesystem enforces this at mount time by recording the PID namespace of the mounting process; a procfs mounted before entering a new PID namespace continues to show the old set, which is why the mount call in the example above happens after unshare and inside the child. The mount namespace must be unshared for the mount to be private, which is why CLONE_NEWNS accompanies CLONE_NEWPID in practice.
Numeric identities in /proc are also the input to many other checks. /proc/<pid>/status reports NSpid, a space-separated list of the process’s numbers from the outermost namespace inward, and it is the simplest way to confirm which namespaces a process belongs to. Resource limits keyed on a PID — RLIMIT_NPROC counts per real user ID within the user namespace, not per PID namespace — do not follow the same boundaries, a mismatch discussed in User Namespaces and the Mapping of UIDs.
None of this restricts what a process can do. PID isolation governs naming and signalling, and a container that shares no other namespace but PID still shares the filesystem, the network stack, and every resource limit with the host. The namespace is one axis of a partition, and the remaining axes are supplied by the mechanisms in Network Namespaces and Virtual Ethernet Pairs and Capabilities and the Decomposition of Root Privilege.
References
- pid_namespaces(7) — Linux manual page — man7.org
- namespaces(7) — Linux manual page — man7.org
- Linux Kernel Development, 3rd edition — Robert Love, Addison-Wesley, 2010