Operating Systems · Isolation and Namespaces

User Namespaces and the Mapping of UIDs

How a process can hold full privilege over a subtree of the process hierarchy while holding none outside it, and what the uid_map translation table does at each syscall boundary.

The root account is not a boolean. Privilege in a user namespace is relative to that namespace, so a process can be numerically uid 0, hold every capability in its own namespace, and still be unable to touch a host file or signal a host process. The mechanism that produces this is a translation table applied at the boundary of every operation, and the table is what makes containers possible without a separate kernel: the container’s root is root over a subtree and nobody over everything else.

What the namespace contains

A user namespace owns a set of credentials — the effective, permitted and inheritable capability sets, the securebits, and a uid and gid translation map. clone with CLONE_NEWUSER creates a new namespace and a new cred structure for the child. The creator of the namespace receives a full capability set within it, including CAP_SYS_ADMIN, regardless of what it held outside. That grant is not a privilege escalation because every check that consults the capability set also consults the namespace in which the capability must be held: to modify a file, the kernel asks whether the caller has the capability in the user namespace that owns the file’s superblock.

The ownership relation is recorded when a resource is created. A superblock mounted by a process in a user namespace is owned by that namespace; so is a process, an IPC object, and a network namespace. Operations on such an object are checked against the caller’s capabilities in the object’s owning namespace, not against the caller’s position in the hierarchy. A process in the parent namespace has capabilities over the child’s objects as well, because the parent owns the child; the reverse does not hold.

The translation table

uid_map and gid_map in /proc/<pid>/uid_map are lists of three-number ranges: the uid inside the namespace, the uid outside it, and the length of the range. The kernel permits up to 340 lines, which is a consequence of the page-size buffer used to read the file rather than a design target.

         0       1000          1
         1     100000      65536

The first line maps namespace uid 0 to host uid 1000. The second maps namespace uids 1 through 65536 to host uids 100000 through 165535. A process inside the namespace that calls getuid sees 0. When it creates a file, the kernel stores the host-side value 1000 as the on-disk owner, so a host ls shows the file as owned by uid 1000. When the process reads a file owned by host uid 1000, the kernel translates back and the process sees itself as the owner, so permission checks on that file succeed with owner permissions.

The map is written once. A process may write its own map, or a parent may write the map of a child, and after the write the file is immutable for the lifetime of the namespace. The reason is that the map defines the namespace’s identity: allowing it to change would let a process reinterpret existing credentials, and an ownership check that succeeded before the change might apply to a different real uid after it. Rewriting is possible only by creating a new namespace.

Unmapped identifiers need a representation, because the kernel still has to return something from stat and getuid. The overflow uid and gid, 65534 by default and settable through /proc/sys/kernel/overflowuid, fill that role. A file owned by a host uid outside the map appears inside the namespace as owned by 65534, and a process whose real uid is unmapped appears as 65534 to itself and to everything in the namespace.

/* Read the host-side uid first: once the process is in the new
   namespace its uid is unmapped, and getuid() reports 65534. */
uid_t uid = getuid();
if (unshare(CLONE_NEWUSER) < 0)
        die("unshare");

int fd = open("/proc/self/setgroups", O_WRONLY);
write(fd, "deny", 4);                 /* required before gid_map */
close(fd);

fd = open("/proc/self/uid_map", O_WRONLY);
dprintf(fd, "0 %u 1\n", (unsigned)uid);  /* namespace 0 -> the caller's uid */
close(fd);

The setgroups write is not optional. Without it, a process could join a supplementary group it does not belong to outside the namespace, which would be an escape from the mapping’s intent. The kernel requires deny before an unprivileged process may write gid_map, and the requirement was added after the omission was found to be exploitable.

Where the boundary is enforced

Credential checks are not a single function. The kernel compares kuid_t values, which are global identifiers, so a check such as “is the caller the file owner” runs on the global value after translation. Capability checks run through ns_capable, which takes the target namespace and walks upward: a process holds a capability in namespace N if it holds it in N or in any ancestor of N, and the walk stops at the first namespace where the capability is present. That asymmetry is what gives the parent namespace authority over the child while denying the child authority over the parent.

Filesystem access adds a second layer. Each mount namespace is owned by a user namespace, and a mount operation is checked against CAP_SYS_ADMIN in that namespace rather than against the caller’s position in the hierarchy. Holding the capability in a container’s own user namespace is therefore sufficient to mount there, which is what makes rootless containers work at all. Two further constraints bound what can be mounted. Only filesystem types that opt in to being mounted from a non-initial user namespace are permitted, so a container with CAP_SYS_ADMIN in its own namespace can mount tmpfs and procfs but not the disk filesystems, which return EPERM regardless of the capability. And mounts inherited from the parent namespace are marked locked: they cannot be unmounted from inside, and a new mount stacked on one may not reveal content that the parent namespace hid. The second rule is the one that prevents a container from bind-mounting a path above a locked mount and walking into the host tree.

Device access follows the same ownership logic. mknod requires the capability in the user namespace that owns the containing filesystem, so a container that mounts its own tmpfs on /dev can create nodes there, while the same call on the image’s read-only root, owned by the host namespace, fails. Creating a node is not the same as using one, and the nodes a container creates on its own filesystem are what /dev inside that container is made of.

What the mapping does not cover

Several identifiers are deliberately outside the translation. The RLIMIT_NPROC limit counts processes per real user ID within the user namespace, so a container that maps 65536 uids can exceed a host per-user process limit without the host noticing, and a container that maps a single uid inherits the host’s count for that uid. The RLIMIT_MEMLOCK limit is per-process and unaffected. SysV IPC objects keyed by numeric ID are namespaced separately, through the IPC namespace, and a user namespace alone does not isolate them. Where a limit must bind a group of processes rather than a uid, the mechanism is a control group rather than a namespace, as described in Control Groups v2: How Memory and CPU Limits Are Enforced.

/proc/sys writes are restricted by namespace ownership as well: a process may write a sysctl only if it has CAP_SYS_ADMIN in the user namespace that owns the network namespace or the IPC namespace the sysctl belongs to. Many sysctls are not namespaced at all and remain writable only from the initial namespace, which is a frequent source of confusion when a container’s init script tries to tune a global parameter.

Nested user namespaces compound the translation. A uid that is 0 in the innermost namespace may be 1000 in its parent and 100000 in the initial namespace; each level’s map is applied in turn, and a uid unmapped at any level becomes overflow at that level. The kernel caps nesting at 32 levels, matching the PID namespace limit, and the cost of a translation is proportional to the nesting depth rather than constant.

The interaction with PID isolation is worth stating explicitly, because the two namespaces are often created together and are not equivalent. PID Namespaces and the Special Status of PID 1 explains that PID 1 in a namespace cannot be killed from inside; the user namespace is what allows a process to become that PID 1 without holding real privilege. Without a user namespace, unshare(CLONE_NEWPID) requires CAP_SYS_ADMIN in the current namespace, so an unprivileged process cannot create one at all.

Creation of a user namespace is unprivileged by design, and that is the security-relevant property: any process may call unshare(CLONE_NEWUSER) and obtain a namespace in which it holds CAP_SYS_ADMIN. The kernel’s attack surface exposed to that capability is correspondingly large, which is why the operation is restricted in some deployment configurations through the kernel.unprivileged_userns_clone sysctl on distributions that carry the patch, and why filesystem mounting by unprivileged user namespaces was for a period the subject of a separate restriction. The namespace grants authority over a subtree; the size of the subtree, and therefore the value of the grant, is a policy decision made outside the namespace mechanism.

References

  1. user_namespaces(7) — Linux manual page — man7.org
  2. Linux Credentials — kernel documentation — Linux kernel documentation
  3. capabilities(7) — Linux manual page — man7.org