Virtualization · Sandboxing

What a Namespace Isolates and What It Leaves Shared

The eight namespace types, the resources each one partitions, and the state that stays global no matter how many namespaces a process joins.

A namespace is a lookup table substitution. Where the kernel would resolve a name, whether a process ID, a mount point, a network interface, or a user ID, it resolves it in the namespace the calling process belongs to. Nothing about the resource itself changes; only the name by which it is reachable, and the set of names visible. That is a narrower statement than “isolation”, and the gap between the two is where most sandbox assumptions fail.

Names partition identifiers, not resources

Each namespace type replaces one mapping:

NamespaceWhat is partitionedKey consequence
PIDprocess ID numbersPID 1 inside; parent sees a different number
Mountmount table and rootseparate filesystem view
Networkinterfaces, routes, portsseparate port space, own loopback
IPCSystem V and POSIX message queuesseparate key namespace
UTShostname and domain namesethostname affects only the namespace
UserUID and GID mappingsroot inside maps to non-root outside
Cgroupvisible cgroup hierarchy/proc/self/cgroup differs
Timeboot and monotonic clocks offsetsseparate clock epoch

The PID namespace is the clearest example of the substitution model. A process created with clone(CLONE_NEWPID) is the first process in a new PID namespace and therefore has PID 1 there. It does not lose its identity in the parent namespace: the parent sees it with an ordinary PID, and /proc in the parent lists it. The child sees a process table containing only its namespace’s members. Both views are correct, which is why a sandbox cannot conclude from “PID 1 is mine” that it controls the machine.

PID 1 inside a namespace has the kernel’s special-case behaviour: it ignores signals for which it has no handler, and if it exits, the kernel kills every other process in the namespace. A container whose entry process exits takes its whole PID namespace with it, regardless of what the processes inside intended.

The mount namespace is a view, and views can propagate

A mount namespace holds a mount table. clone(CLONE_NEWNS) copies the caller’s table into a new namespace, and subsequent mount and umount calls in either namespace do not affect the other. This is the mechanism behind a container’s private root filesystem.

Propagation is the part that surprises people. Mounts are shared by default between a parent and a newly created namespace, because the kernel models peer groups: a mount event in one member of a peer group is replicated to the others. To get a genuinely private table, the new namespace must first call mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) to break the sharing, and only then perform its own mounts. A container runtime that omits that step has a namespace whose mounts appear in the host’s table, which is both an information leak and a way to alter the host’s view.

The reverse direction is intentional and used: MS_SHARED propagation lets a host mount a filesystem and have it appear inside every container sharing the peer group, which is how volume injection works without entering each namespace.

The user namespace changes what privilege means

A user namespace maps UIDs and GIDs. The creating process writes a mapping through /proc/self/uid_map and /proc/self/gid_map, one line per range, in the form inside_id outside_id length. Inside the namespace, the mapped ID is treated as a full-capability UID; outside, the process has exactly the outside ID’s authority.

This is the single most consequential namespace, because it makes the others available to unprivileged processes. Creating a mount, network, or PID namespace normally requires CAP_SYS_ADMIN in the current user namespace. A process that creates a new user namespace receives a full capability set within that namespace, and can then create the others. The privilege is real inside and empty outside, and the kernel’s checks are written in terms of the namespace a resource belongs to.

The mapping is not symmetric and not arbitrary. A process may map only IDs it is entitled to, which for an unprivileged caller means its own effective UID, or the subordinate ranges the system has allocated to it when the setuid newuidmap and newgidmap helpers write the mapping on its behalf. setgroups must be denied in the namespace before a mapping that does not include the caller’s groups can be written, controlled by writing deny to /proc/self/setgroups. Without that, a process could map a group it belongs to and use supplementary group membership to gain file access it should not have.

Inside a user namespace, CAP_SYS_ADMIN does not mean what it means outside. Operations that would affect the host, such as mounting a real block device, loading a kernel module, or changing the host clock, are refused regardless of the capability, because the capability is held only with respect to the namespace’s own resources. The check is on the resource’s owning user namespace, not on the caller’s capability set.

What stays global

Several things are outside every namespace, and a sandbox that assumes otherwise is wrong in ways that matter:

  • The kernel itself. One kernel image, one set of syscalls, one scheduler, one page cache. A namespace does not provide a second kernel, so a kernel bug reachable from inside is a bug reachable on the host.
  • The clock’s rate. The time namespace offsets the boot and monotonic clocks; it does not change their frequency or give a namespace its own timer hardware. CLOCK_REALTIME is not namespaced at all and is shared.
  • Kernel keyrings and some credential state. Session keyrings are per-session, but the kernel keyring facility’s behaviour across user namespaces has its own rules and is not simply partitioned.
  • /proc/sys and /sys contents. These are filesystem views of global kernel state. A mount namespace can hide them, but hiding is not partitioning: the values are the host’s, and writes that succeed affect the host unless the sysctl itself is namespaced. Many are; many are not, and the per-namespace ones are marked as such in the kernel documentation.
  • The network stack’s behaviour. A network namespace gets its own interfaces, routing table, and port space, but it does not get its own TCP implementation with different congestion control defaults or its own firewall unless one is configured. net.ipv4.* sysctls are per-network-namespace, which is why a container can set them without host impact; net.core.* are largely global.
  • Devices. A device node in a container’s /dev refers to the same driver instance as the host’s, unless a device cgroup or an LSM rule excludes it. The mount namespace determines whether the node is visible; it does not determine whether the driver is reachable through some other path.
  • CPU and memory as physical resources. A cgroup limits consumption; it does not give the sandbox separate hardware. Cache and memory-bandwidth contention, and side channels built on them, cross namespace boundaries freely.

Namespaces are combined, and the combination is the boundary

A container is not “a namespace”. It is a process that has joined a set of namespaces, had its capability sets reduced, had a seccomp filter attached, and been placed in a cgroup. Each mechanism covers a different axis, and the isolation claim is only as strong as the weakest axis that is relevant to the threat.

The user namespace deserves special attention in that composition, because it changes the meaning of every capability check that follows. A process in a new user namespace holding CAP_SYS_ADMIN can mount filesystems inside its own mount namespace, create device nodes, and configure its network stack. All of that is confined to resources owned by its user namespace. But the code paths those operations traverse are host kernel code, and the operations are permitted precisely because the kernel believes they are confined. The attack surface that opens is taken up in Unprivileged User Namespaces and the Attack Surface They Open; the design point here is that the user namespace converts “requires privilege” into “requires no privilege”, and moves the security question from access control to kernel correctness.

Where the boundary is drawn in practice: a mount namespace hides paths but does not restrict which paths a process may reach once it has a file descriptor that points outside; a PID namespace hides processes but does not prevent signals sent by a process in the parent that holds the right credentials; a network namespace gives a separate port space but not a separate packet path to the outside world. Each is a rename of the interface, and the mechanisms that constrain the underlying operations (capabilities, seccomp, and the path-based checks described in Why a Seccomp Filter Cannot Inspect Syscall Arguments in Memory) are doing the actual enforcement.

References

  1. namespaces(7) — Linux manual page — man-pages project
  2. user_namespaces(7) — Linux manual page — man-pages project
  3. mount_namespaces(7) — Linux manual page — man-pages project