UID and GID Mapping

Inside a user namespace, the user and group IDs a process sees are not the host’s. UID/GID mapping is the kernel mechanism that translates between them: each user namespace carries a uid_map and gid_map table, exposed as the files /proc/[pid]/uid_map and /proc/[pid]/gid_map, and every line is a contiguous-range mapping written as three numbers — inside-id outside-id length (user_namespaces(7)). This is what lets a container’s root (uid 0 inside) be an ordinary, unprivileged uid (e.g. 100000) on the host: a breakout lands as a powerless user, not as host root. The kernel guards these tables with a deliberately strict protocol — a single write, capability checks against the parent namespace, and a setgroups-must-be-denied rule that closed a real privilege-escalation hole. For unprivileged users, the setuid-root helpers newuidmap/newgidmap, backed by /etc/subuid and /etc/subgid, hand out pre-authorized ID ranges so that rootless containers can map more than one ID without any host privilege.

This note owns the mapping mechanism — the file format, the translation arithmetic, the write protocol, and the helpers. The privilege model it rests on (why being root inside a userns is scoped, capable() vs ns_capable(), the owner/parent hierarchy) is the sibling note User Namespaces. The escalation CVEs are catalogued in User Namespaces and Privilege Escalation.

Mental Model: A Translation Table Per Namespace

Think of each user namespace as carrying a small lookup table that the kernel consults whenever an ID crosses the namespace boundary. When code inside the namespace asks “who am I?” the kernel maps the internal kuid_t down to what that namespace should see; when an inside ID must be checked against a host resource, the kernel maps it up to the parent’s ID space. The table is a list of extents, each extent being one contiguous range described by (first, lower_first, count)first is the start of the range inside this namespace, lower_first is the corresponding start in the parent, and count is how many consecutive IDs the extent covers (v6.12 struct uid_gid_extent, include/linux/user_namespace.h).

flowchart LR
  subgraph INSIDE["Inside the user namespace"]
    I0["uid 0 (root)"]
    I1["uid 1"]
    I65535["uid 65535"]
  end
  subgraph OUTSIDE["On the host (parent ns)"]
    O100000["uid 100000"]
    O100001["uid 100001"]
    O165535["uid 165535"]
  end
  I0 -->|"0 100000 65536"| O100000
  I1 --> O100001
  I65535 --> O165535
  MAP["uid_map line:<br/>first=0  lower_first=100000  count=65536"]

A single-extent UID map, 0 100000 65536. What it shows: inside-uid 0 maps to host-uid 100000, inside-uid 1 to 100001, and so on for 65536 IDs; the arithmetic is simply host_id = inside_id - first + lower_first. The insight to take: the container’s all-powerful “root” (inside uid 0) is, on the host, just unprivileged uid 100000 — owning no host files and holding no host capabilities. The mapping is the security boundary that makes a rooted-looking container harmless on the host.

The File Format, Field by Field

Each line of uid_map (and gid_map) is “three numbers delimited by white space” (user_namespaces(7)). The man page defines the fields in this exact order:

  1. The start of the range of IDs in the user namespace of the process pid — the inside starting ID.
  2. The start of the range of IDs to which the field-1 IDs map — the outside (parent) starting ID.
  3. The length of the range — how many consecutive IDs this line maps.

So the line 0 100000 65536 reads: “inside-ID 0 maps to outside-ID 100000, for 65536 consecutive IDs.” A multi-line map can stitch together several disjoint ranges, e.g.:

0     1000    1
1     100000  65536

This maps inside-uid 0 to the host uid that created the namespace (1000) — so that the creating user owns the container’s root-owned files on the host — and inside-uids 1..65536 to a delegated subordinate range starting at 100000. This two-line shape is exactly what podman writes for a rootless container.

There is a hard ceiling on how many lines a map may contain. In v6.12 the base extent array holds 5 extents inline (UID_GID_MAP_MAX_BASE_EXTENTS = 5), and beyond that the kernel promotes to a sorted, binary-searchable array up to 340 extents (UID_GID_MAP_MAX_EXTENTS = 340) (include/linux/user_namespace.h). The man page confirms the userspace-visible history: “Up to Linux 4.14, this limit was (arbitrarily) set at 5 lines. Since Linux 4.16, the limit is 340 lines.” Five lines or fewer are stored inline for cache-friendliness; the kernel only allocates the larger sorted structure when a map genuinely needs more than five extents.

The Translation Arithmetic

The down-mapping is concrete and worth seeing. In v6.12, map_id_range_down() dispatches to a linear scan over the inline extents (map_id_range_down_base) or a binary search over the promoted array, then computes (kernel/user_namespace.c):

/* Map the id or note failure */
if (extent)
	id = (id - extent->first) + extent->lower_first;
else
	id = (u32) -1;

That single line — id = (id - extent->first) + extent->lower_first — is the whole translation: take the offset of the ID within the extent’s inside-range (id - first) and add it to the extent’s outside-base (lower_first). If no extent contains the ID, the result is (u32) -1, i.e. 65534/nobody-style overflow id — an unmapped inside-ID appears on the host as the unprivileged overflow UID, never as something privileged. The reverse, map_id_up(), does the symmetric computation to translate a host ID into the namespace’s view. The extent-matching helper makes the containment check explicit:

first = map->extent[idx].first;
last  = first + map->extent[idx].count - 1;
if (id >= first && id <= last && (id2 >= first && id2 <= last))
	return &map->extent[idx];

An ID (or an entire requested range [id, id2]) must lie wholly within one extent to be mapped; partial overlaps do not match.

The Write Protocol — How the Kernel Guards the Map

The mapping files are write-once and heavily checked. The enforcement lives in map_write() and new_idmap_permitted() in v6.12 kernel/user_namespace.c. The rules, in the order the kernel applies them:

1. The target must not be the initial namespace. proc_uid_map_write() returns -EPERM immediately if !ns->parent — you cannot remap the root user namespace. It also requires the writer’s user namespace to be either the target namespace or its direct parent (if ((seq_ns != ns) && (seq_ns != ns->parent)) return -EPERM;).

2. Single write only. Inside map_write():

ret = -EPERM;
/* Only allow one successful write to the map */
if (map->nr_extents != 0)
	goto out;

Once any extent exists, every further write fails with EPERM. The man page states it plainly: “An attempt to write more than once to a uid_map file in a user namespace fails with the error EPERM.” This is why tools build the entire multi-line map and write it in a single write() call — there is no appending. A userns_state_mutex serializes the write so two racing writers cannot both “win.”

3. The write must be one write() of less than a page. if ((*ppos != 0) || (count >= PAGE_SIZE)) return -EINVAL; — writes must start at offset 0 and fit in under a page (4 KiB on most architectures).

4. Capability and parent-mapping checks (new_idmap_permitted). For the privileged path the kernel requires CAP_SETUID (for uid_map; CAP_SETGID for gid_map) in the parent user namespace, checked against both the current process and the credentials that opened the file:

/* Allow the specified ids if we have the appropriate capability
 * (CAP_SETUID or CAP_SETGID) over the parent user namespace. */
if (ns_capable(ns->parent, cap_setid) &&
    file_ns_capable(file, ns->parent, cap_setid))
	return true;

proc_uid_map_write() passes CAP_SETUID, proc_gid_map_write() passes CAP_SETGID — wiring the right capability per file. The use of file_ns_capable() (which checks the file opener’s credentials, not the current task’s) is deliberate: it lets a privileged parent open the map file and hand the descriptor to a less-privileged child without the child inheriting the write power.

5. The mapped outside IDs must themselves be mapped in the parent. A namespace can only map to IDs that its parent can actually represent — you cannot conjure an outside ID the parent has no name for. This is why nested namespaces can only ever narrow the available ID set, never widen it.

The Unprivileged Single-ID Path

There is a special concession for the unprivileged case. If a process has no CAP_SETUID/CAP_SETGID in the parent, it may still write a map — but only a single line that maps exactly its own effective ID:

if ((new_map->nr_extents == 1) && (new_map->extent[0].count == 1) &&
    uid_eq(ns->owner, cred->euid)) {
	... /* allow mapping the writer's own euid to one inside id */
}

The man page describes the same restriction: the data “must consist of a single line that maps the writing process’s effective user ID (group ID) in the parent user namespace to a user ID (group ID) in the user namespace,” and “the writing process must have the same effective user ID as the process that created the user namespace.” This is exactly the unshare --map-root-user case — an unprivileged user mapping only their own uid to inside-uid 0. To map more than one ID (a whole 65536-range), the unprivileged path is insufficient and the newuidmap helper is required (below).

Mapping UID 0 Requires CAP_SETFCAP (Linux 5.12+)

A subtle hardening: if a uid_map write would map UID 0 of the parent into the new namespace, the kernel additionally requires CAP_SETFCAP. v6.12’s verify_root_map() enforces it — when a process writes to its own map after unshare, the kernel checks file_ns->parent_could_setfcap; when one process writes another’s map, it checks file_ns_capable(file, map_ns->parent, CAP_SETFCAP). The capabilities(7) man page records the rationale: “Since Linux 5.12, this capability is also needed to map user ID 0 in a new user namespace.” This prevents an unprivileged user from gaining the ability to forge file capabilities that the parent’s root would honor.

The setgroups Rule — and the CVE Behind It

Before a gid_map may be written by an unprivileged process, that process must first write the string deny to /proc/[pid]/setgroups, permanently disabling the setgroups(2) system call inside the namespace. This is not arbitrary friction; it closes CVE-2014-8989, a real local-privilege-escalation flaw (cvedetails; LWN: “User namespaces and setgroups()”).

The bug, in Michael Kerrisk’s words on LWN, is the “negative groups” problem. A file (or POSIX ACL) can grant fewer permissions to a group than to “other” — e.g. mode rwx---rwx, which denies the group while allowing everyone else. Normally, calling setgroups() to drop a group membership is privileged. But a process that is root inside its own user namespace could call setgroups() there to drop a group it was a member of — thereby gaining access to a file it was previously denied (because it now falls into the more-permissive “other” category instead of the restricted “group” category). The man page summarizes the danger: “with the introduction of user namespaces, it became possible for an unprivileged process to create a new namespace in which the user had all privileges.”

The fix (Eric Biederman’s patches, merged in Linux 3.19 and backported to stable) disables setgroups() in a user namespace until a gid_map is established, and adds the setgroups control file:

  • Writing deny to /proc/[pid]/setgroups turns setgroups() off in the namespace; this must happen before gid_map is written. v6.12’s proc_setgroups_write() enforces the ordering — once ns->gid_map.nr_extents != 0, attempting to write deny fails (goto out_unlock), because the gid map is already set.
  • An unprivileged process can only set the gid_map once setgroups has been denied — the new_idmap_permitted single-ID path requires !(ns->flags & USERNS_SETGROUPS_ALLOWED) for the CAP_SETGID case.

So the unprivileged rootless sequence is rigid: write deny to setgroups, then write gid_map, and never the other way around. A privileged writer (holding CAP_SETGID in the parent) is exempt and may leave setgroups as allow.

newuidmap / newgidmap and /etc/subuid

The single-ID unprivileged path is too restrictive for real containers, which need a whole range (typically 65536 IDs) so the container can have its own uids 1..65535. The solution is a pair of setuid-root helper binaries, newuidmap(1) and newgidmap(1), shipped by shadow-utils. Because they run with root’s authority, they can write a multi-line map — but they only do so within ranges an administrator has pre-authorized.

newuidmap “sets /proc/[pid]/uid_map based on its command line arguments and the uids allowed,” taking “sets of 3 integers: uid (Beginning of the range of UIDs inside the user namespace), loweruid (Beginning of the range of UIDs outside the user namespace), count (Length of the ranges)” — the same inside outside length triple as the file itself (newuidmap(1)). Critically, it “verifies that the caller is the owner of the process indicated by pid and that … each of the UIDs in the range [loweruid, loweruid+count) is allowed to the caller according to /etc/subuid before setting /proc/[pid]/uid_map.”

/etc/subuid is the authorization database. Each line is three colon-separated fields — “login name or UID”, “numerical subordinate user ID” (range start), “numerical subordinate user ID count” — and the file “specifies the user IDs that ordinary users can use, with the newuidmap command, to configure uid mapping in a user namespace” (subuid(5)). A typical entry:

alice:100000:65536

This authorizes user alice to map host uids 100000..165535 into her user namespaces. newgidmap and /etc/subgid are exactly symmetric for groups. So the rootless container flow is: the runtime forks a child into a new user namespace, then the parent calls newuidmap <pid> 0 1000 1 1 100000 65536 (mapping inside-0 to alice’s own uid, and inside-1..65536 to her subordinate range), and newgidmap likewise — all without alice ever holding host root.

Failure Modes and Common Misunderstandings

EPERM on the second write. The map is write-once. Code that writes 0 100000 65536, then tries to append 100000 200000 1, gets EPERM on the second write. Build the complete map and write it once.

gid_map write fails with EPERM for an unprivileged user. Almost always because deny was not written to setgroups first. The ordering is mandatory and one-directional.

Mapping more than one ID without newuidmap. A bare unshare(CLONE_NEWUSER) followed by a direct write of a 65536-range fails for an unprivileged user — the kernel’s unprivileged path allows only a single-ID line. You need newuidmap/subuid, or CAP_SETUID in the parent.

Files owned by unmapped IDs show as nobody. Inside the namespace, any host file owned by a uid with no entry in uid_map displays as the overflow uid (commonly 65534/nobody) — the (u32) -1 result of a failed map_id_up. This surprises users who ls -l a bind-mounted host directory inside a rootless container.

subuid/subgid range overlap or exhaustion. If two users’ subordinate ranges overlap, or a user’s range is too small for the container’s needs, the runtime errors out at newuidmap time. Distributions size the default ranges (often 65536 per user starting at 100000, stepping by 65536) to avoid overlap.

Alternatives and When to Choose Them

  • Direct write via CAP_SETUID (rooted/userns-remap). A privileged daemon writes the multi-line map directly, no helper needed. Used by rooted Docker with userns-remap and by Kubernetes’ user-namespace support, where the kubelet allocates ranges.
  • newuidmap/subuid (rootless). The only way for a daemonless, unprivileged user to map a full range. Required by rootless Podman and rootless Docker. The cost is the setuid-root helpers and an /etc/subuid provisioning step per user.
  • Single-ID --map-root-user. The simplest, no-helper path — but maps exactly one ID, so the container has only uid 0 mapped and everything else is nobody. Fine for trivial sandboxes, inadequate for anything that needs non-root users inside.

Production Notes

Rootless Podman relies on newuidmap/newgidmap and /etc/subuid//etc/subgid; a common first-run failure is a missing or empty subuid entry, producing cannot set up namespace … newuidmap: write to uid_map failed. The fix is usermod --add-subuids 100000-165535 <user> (and the subgid equivalent), then podman system migrate. Kubernetes’ user-namespace feature (stabilizing across recent releases) maps each pod into a distinct host ID range precisely so that container root cannot touch another pod’s files — the same boundary, applied at cluster scale.

The single-write rule is also why you cannot “adjust” a running container’s mapping: the map is sealed the moment the runtime writes it, so any re-mapping requires a fresh user namespace. And the setgroups-deny requirement is why rootless containers, by default, cannot use setgroups() and therefore cannot drop or gain supplementary groups inside the namespace — a behavioral difference that occasionally surprises software expecting full group control.

See Also