User Namespaces
A user namespace (created with the
CLONE_NEWUSERflag toclone(2)orunshare(2)) is the kernel facility that decouples privilege inside a namespace from privilege outside it. A process can hold a complete set of capabilities — effectively be root — inside its user namespace while being an ordinary, unprivileged user in the parent. The kernel achieves this by making every capability check relative to a user namespace: the man page states that a process “has full privileges for operations inside the user namespace, but is unprivileged for operations outside the namespace” (user_namespaces(7)). Crucially, every other namespace type (PID, mount, network, …) is owned by a user namespace, and the privilege checks that govern that namespace are evaluated against its owning user namespace — not against the host. This is the single mechanism that lets an unprivileged user create all the other namespace types and so build a container with no root at all: the privilege pivot at the heart of Rootless Containers.
Version pin
All kernel source in this note is read from the Linux v6.12 LTS tree (
raw.githubusercontent.com/torvalds/linux/v6.12/…), verified 2026-08-29. v6.12 is a maintained long-term-support series; mainline had moved on by the time of writing. Where a fact was introduced in an earlier release, the introducing version is named. Distribution behaviour (Ubuntu, Debian) is dated separately because it changes on a distro cadence, not a kernel one.
This note is the canonical owner of the user-namespace privilege model: what a user namespace is, how the kernel decides whether you hold a capability in one, and exactly what that capability does and does not buy you. Three neighbours carry the detail this note deliberately compresses. The mechanics of the UID/GID translation tables — the uid_map/gid_map file format, the extent arithmetic, and the newuidmap/newgidmap helpers — live in UID and GID Mapping. The catalogue of privilege-escalation CVEs that this power has enabled lives in User Namespaces and Privilege Escalation. Capabilities themselves — the five sets, the execve transition rules, the 41 CAP_* constants — are owned by POSIX Capabilities in the Linux Security MOC.
Mental Model: Privilege Is Relative to a Namespace
The pre-namespace mental model of Unix privilege is a single global question: does this process have capability X? User namespaces replace that with a relative question: does this process have capability X in user namespace N? The kernel still tracks a process’s capability sets (permitted, effective, inheritable, bounding, ambient), but a capability is only meaningful against a particular user namespace — the one that owns the resource being touched.
The clearest way to see this is in the kernel source itself. The classic global check, capable(int cap), is in v6.12 defined as nothing more than a check against the initial user namespace (kernel/capability.c):
bool capable(int cap)
{
return ns_capable(&init_user_ns, cap);
}That is the whole idea in three lines. capable() is just ns_capable() asking about init_user_ns — the root user namespace at the top of the hierarchy. Every privilege check in the kernel is therefore secretly a namespace-relative check; the global one is the special case where the namespace is the initial one. When the kernel virtualizes a resource (a hostname, a network stack, a mount table), it stops calling capable() and starts calling ns_capable(owning_user_ns, cap), where owning_user_ns is the user namespace that owns the resource’s namespace. That substitution — capable() → ns_capable(owning_userns, …) — is the entire content of “privilege is relative.”
flowchart TB INIT["init_user_ns<br/>level = 0<br/>owner = GLOBAL_ROOT_UID<br/>uid_map: 0 0 4294967295"] U1["user ns U1<br/>level = 1<br/>owner = kuid 1000<br/>caps: FULL inside U1"] U2["user ns U2 child of U1<br/>level = 2<br/>caps inherited downward"] INIT -->|"parent"| U1 U1 -->|"parent"| U2 NETNS["net ns<br/>owning userns = U1"] MNTNS["mount ns<br/>owning userns = U1"] U1 -.->|"owns"| NETNS U1 -.->|"owns"| MNTNS CHECK{"sethostname / iptables /<br/>mount → ns_capable<br/>owning_userns, CAP_SYS_ADMIN?"} MNTNS --> CHECK NETNS --> CHECK CHECK -->|"yes — process is root in U1"| OK["operation allowed"] HOSTRES["host-owned resource:<br/>settimeofday, init_module,<br/>mknod on a block device"] HOSTRES --> HCHECK{"capable = ns_capable<br/>init_user_ns, cap?"} HCHECK -->|"no — process is uid 1000<br/>in init_user_ns"| DENY["EPERM"]
How privilege is scoped by the user-namespace hierarchy. What it shows: init_user_ns is the root of the tree, statically initialised in the kernel with an identity map covering the whole 32-bit ID space; an unprivileged uid 1000 creates child U1 and is granted a full capability set within U1. Other namespaces (net, mount) created from inside U1 record U1 as their owning user namespace, so their privileged operations resolve to ns_capable(U1, …) — where the process is root. Operations on resources that no namespace virtualises still route through plain capable(), i.e. ns_capable(&init_user_ns, …), where the process is nobody. The insight to take: the process is simultaneously all-powerful and powerless, and which one applies is decided entirely by which user namespace the kernel asks about — not by anything about the process itself. Power is real but scoped.
Two Meanings of “Owner”, and Why Conflating Them Breaks Your Reasoning
The word “owner” is overloaded in this area, and almost every confused explanation of user namespaces on the internet comes from collapsing the two meanings. Keep them apart:
| Concept | Kernel representation (v6.12) | Set when | What it grants |
|---|---|---|---|
| Owner UID of a user namespace | struct user_namespace.owner — a kuid_t | At creation: kuid_t owner = new->euid in create_user_ns() | A process in the parent whose effective UID equals this value has all capabilities in the namespace and, transitively, in all its descendants |
| Owning user namespace of a nonuser namespace | The user_ns pointer recorded in that namespace’s own struct (e.g. struct net.user_ns); reachable from userspace via the NS_GET_USERNS ioctl(2) | At creation of the nonuser namespace: the creating process’s current user namespace | Every privileged operation on that namespace’s resources is checked with ns_capable(that_userns, cap) |
| Parent user namespace | struct user_namespace.parent; reachable via NS_GET_PARENT | At creation | Defines the tree used by the capability-check walk; for a user namespace, the .owner field of struct proc_ns_operations returns parent, so “owning userns of a user namespace” is its parent |
The three ioctl(2) operations that expose these relationships to userspace on a /proc/[pid]/ns/* file descriptor are NS_GET_USERNS, NS_GET_PARENT, and NS_GET_OWNER_UID (ioctl_nsfs(2)). Their existence is the cleanest proof that these are three genuinely different edges in the graph: the kernel needed three separate calls to expose them.
flowchart LR subgraph HOST["init_user_ns (level 0)"] P0["process alice<br/>euid 1000"] end subgraph U1G["user ns U1 (level 1)"] P1["process alice<br/>uid 0 inside"] NET["net ns N"] UTS["uts ns T"] end P0 -->|"unshare(CLONE_NEWUSER)<br/>records owner = kuid 1000"| U1G P1 -->|"unshare(CLONE_NEWNET)<br/>records owning userns = U1"| NET P1 -->|"unshare(CLONE_NEWUTS)"| UTS P0 -.->|"euid == U1.owner ⇒<br/>ALL caps in U1 (rule 3)"| U1G NET -.->|"NS_GET_USERNS"| U1G U1G -.->|"NS_GET_PARENT"| HOST U1G -.->|"NS_GET_OWNER_UID → 1000"| P0
The three distinct edges the word “owner” hides. What it shows: alice on the host creates U1, so U1’s owner field is alice’s kuid; inside U1 she creates a net and a UTS namespace, both of which record U1 as their owning user namespace. The dotted arrows are the three nsfs ioctls that let userspace read each edge back. The insight to take: alice retains total control over U1 from outside it purely because her euid matches U1.owner — she never has to enter the namespace. That is the rule that makes newuidmap and container supervisors work, and it is a different rule from the one that makes sethostname succeed inside U1.
User namespaces form a strict tree: every one except the initial namespace has exactly one parent, namely the user namespace of the process that created it, and may have any number of children (user_namespaces(7)). The man page gives the canonical worked example of the second column above: a process calling sethostname(2) touches a resource governed by the UTS namespace, so “the kernel will determine which user namespace owns the process’s UTS namespace, and check whether the process has the required capability (CAP_SYS_ADMIN) in that user namespace.”
The pointer is not metaphorical. In v6.12, struct net — the network namespace — literally carries the field, comment and all (include/net/net_namespace.h):
struct user_namespace *user_ns; /* Owning user namespace */struct uts_namespace carries the same field (include/linux/utsname.h). Every nonuser namespace type does. “Owned by a user namespace” is a single pointer per namespace object, set once at creation and, as the man page notes, never changeable afterwards.
Anatomy of struct user_namespace
Everything the privilege model needs sits in one object. Reading its v6.12 definition is the fastest way to see the whole feature at once (include/linux/user_namespace.h):
struct user_namespace {
struct uid_gid_map uid_map; /* uid translation table */
struct uid_gid_map gid_map; /* gid translation table */
struct uid_gid_map projid_map; /* project-quota ids */
struct user_namespace *parent; /* the tree edge */
int level; /* depth below init_user_ns */
kuid_t owner; /* creator's euid */
kgid_t group; /* creator's egid */
struct ns_common ns; /* nsfs inode + refcount */
unsigned long flags; /* USERNS_SETGROUPS_ALLOWED */
/* parent_could_setfcap: true if the creator if this ns had CAP_SETFCAP
* in its effective capability set at the child ns creation time. */
bool parent_could_setfcap;
/* … keyrings, sysctl set, work_struct … */
struct ucounts *ucounts;
long ucount_max[UCOUNT_COUNTS];
long rlimit_max[UCOUNT_RLIMIT_COUNTS];
} __randomize_layout;Walking the fields that matter:
uid_map/gid_map/projid_map— three translation tables, each astruct uid_gid_map. That struct is deliberately sized to 64 bytes, one cache line, with a union: up toUID_GID_MAP_MAX_BASE_EXTENTS == 5extents stored inline, or, beyond that, two heap pointers (forwardandreverse) to sorted arrays for binary search. The hard ceiling isUID_GID_MAP_MAX_EXTENTS == 340. This is the source of the “5 lines until Linux 4.14, 340 lines since Linux 4.16” limit the man page documents; the inline-versus-sorted-array split is why the small case is fast. Details in UID and GID Mapping.parentandlevel—levelis the depth:init_user_nshaslevel == 0, andcreate_user_ns()setsns->level = parent_ns->level + 1.levelis not decorative; the capability-check loop uses it to terminate early (below).owner/group— the creating process’s effective UID and GID, stored as kernel IDs (kuid_t/kgid_t), i.e. already translated into the global ID space so that comparisons are unambiguous across namespaces.flags— currently exactly one bit,USERNS_SETGROUPS_ALLOWED, inherited from the parent at creation and only ever cleared, never set. This is thesetgroupsratchet.parent_could_setfcap— a snapshot boolean:ns->parent_could_setfcap = cap_raised(new->cap_effective, CAP_SETFCAP). The kernel records, at creation time, whether the creator heldCAP_SETFCAP, because a rule added in Linux 5.12 needs to know that fact later, when someone writesuid_map. A namespace that remembers one bit about its creator’s privilege is an unusual design and worth noticing.ucounts/ucount_max[]— the per-namespace resource accounting that replaced the rejected “disable user namespaces” sysctl (see the gating section).
classDiagram class user_namespace { +uid_gid_map uid_map +uid_gid_map gid_map +uid_gid_map projid_map +user_namespace* parent +int level +kuid_t owner +kgid_t group +ns_common ns +unsigned long flags +bool parent_could_setfcap +ucounts* ucounts +long ucount_max[UCOUNT_COUNTS] } class uid_gid_map { +uid_gid_extent extent[5] +u32 nr_extents +uid_gid_extent* forward +uid_gid_extent* reverse } class uid_gid_extent { +u32 first +u32 lower_first +u32 count } class ucounts { +user_namespace* ns +kuid_t uid +atomic_long_t ucount[] } user_namespace "1" *-- "3" uid_gid_map : uid, gid, projid uid_gid_map "1" *-- "1..340" uid_gid_extent user_namespace "1" --> "1" ucounts : accounting user_namespace "1" --> "0..1" user_namespace : parent
The object graph of a user namespace at v6.12. What it shows: one user_namespace owns three uid_gid_maps, each holding between 1 and 340 uid_gid_extent triples; it points at its parent, forming the tree, and at a ucounts record used for per-user resource limits. The insight to take: the entire “who am I allowed to be” question is answered by a handful of integer extents in a single cache line — ID translation is not a lookup in a table of users, it is range arithmetic over at most 340 intervals, which is why it is cheap enough to sit on every credential comparison in the kernel.
Creating One: create_user_ns(), Gate by Gate
Consider what actually happens in the kernel when an unprivileged process calls unshare(CLONE_NEWUSER). The entry point is unshare_userns(), which prepares a new credential and calls create_user_ns(cred); clone(CLONE_NEWUSER) reaches the same function via copy_creds(). Reading create_user_ns() in kernel/user_namespace.c shows that “no privilege required” is not the same as “no checks”:
flowchart TB START["unshare(CLONE_NEWUSER)<br/>or clone(CLONE_NEWUSER)"] G1{"parent_ns->level > 32 ?"} G2{"inc_user_namespaces()<br/>ucount limit at every<br/>ancestor OK?"} G3{"current_chrooted() ?"} G4{"kuid_has_mapping(parent, euid)<br/>&& kgid_has_mapping(...) ?"} G5{"security_create_user_ns(new)<br/>LSM hook allows?"} ALLOC["kmem_cache_zalloc + ns_alloc_inum<br/>record parent_could_setfcap<br/>= cap_raised(cap_effective, CAP_SETFCAP)<br/>level = parent->level + 1<br/>owner = euid, group = egid<br/>flags = parent->flags"] SETCRED["set_cred_user_ns():<br/>cap_permitted = CAP_FULL_SET<br/>cap_effective = CAP_FULL_SET<br/>cap_bset = CAP_FULL_SET<br/>cap_inheritable = EMPTY<br/>cap_ambient = EMPTY<br/>securebits = SECUREBITS_DEFAULT"] START --> G1 G1 -->|"yes"| E1["-ENOSPC (EUSERS before Linux 4.9)"] G1 -->|"no"| G2 G2 -->|"no"| E2["-ENOSPC"] G2 -->|"yes"| G3 G3 -->|"yes"| E3["-EPERM"] G3 -->|"no"| G4 G4 -->|"no"| E4["-EPERM"] G4 -->|"yes"| G5 G5 -->|"denied"| E5["-EPERM (AppArmor:<br/>denied=userns_create)"] G5 -->|"allowed"| ALLOC ALLOC --> SETCRED SETCRED --> DONE["caller is now root<br/>in the new namespace"]
The five gates between an unprivileged unshare() and a full capability set, in the order create_user_ns() applies them. What it shows: the nesting-depth check, the per-user resource limit, a chroot check, a requirement that the creator’s own IDs be mapped in the parent, and finally the LSM hook — only then does the kernel allocate the namespace and hand out CAP_FULL_SET. The insight to take: “creating a user namespace requires no privilege” is true of capabilities and false of everything else. Four of these five gates are the places distributions and security modules actually hook in to restrict the feature, and the fifth (current_chrooted()) is a security fix hiding in plain sight — a chrooted process may not create a user namespace, because it could otherwise escape the policy its root directory expresses.
Two of those gates deserve a closer look because they are rarely explained.
The chroot gate. The source comments it directly: “Verify that we can not violate the policy of which files may be accessed that is specified by the root directory, by verifying that the root directory is at the root of the mount namespace which allows all files to be accessed.” A process inside a chroot(2) jail that could create a user namespace would gain CAP_SYS_CHROOT in it and could then chroot("..") its way out — which is, essentially, CVE-2013-1858 in a different key.
The “creator must be mapped” gate. kuid_has_mapping(parent_ns, owner) requires the creator’s own effective UID to have a mapping in the parent namespace. The comment explains the motivation: “The creator needs a mapping in the parent user namespace or else we won’t be able to reasonably tell userspace who created a user_namespace.” Practically, this is what stops you from stacking namespaces whose owner IDs cannot be named.
And then the payoff — set_cred_user_ns():
/* Start with the same capabilities as init but useless for doing
* anything as the capabilities are bound to the new user namespace.
*/
cred->securebits = SECUREBITS_DEFAULT;
cred->cap_inheritable = CAP_EMPTY_SET;
cred->cap_permitted = CAP_FULL_SET;
cred->cap_effective = CAP_FULL_SET;
cred->cap_ambient = CAP_EMPTY_SET;
cred->cap_bset = CAP_FULL_SET;The kernel’s own comment is the entire privilege-pivot insight in one sentence: the capabilities are full “but useless for doing anything as the capabilities are bound to the new user namespace.” Note what is not granted: cap_inheritable and cap_ambient are explicitly emptied, and securebits is reset to the default. That last reset matters and is easy to miss — user_namespaces(7) points out that because the caller loses capabilities in its original namespace, “it is not possible for a process to reset its securebits flags while retaining its user namespace membership by using a pair of setns(2) calls.” The securebits reset is one-way in practice. CAP_FULL_SET itself is CAP_VALID_MASK, defined in include/linux/capability.h as BIT_ULL(CAP_LAST_CAP+1)-1 — at v6.12 that is 41 bits, 0x1fffffffff, exactly the CapEff value the man page’s own worked example prints inside a fresh namespace.
Why the User Namespace Must Be Created First
There is a subtlety every runtime depends on: when CLONE_NEWUSER is combined with other CLONE_NEW* flags in a single clone()/unshare() call, the user namespace must come into being before the others, or the caller would lack the privilege to create them. The kernel guarantees exactly this. Per user_namespaces(7):
“If
CLONE_NEWUSERis specified along with otherCLONE_NEW*flags in a singleclone(2)orunshare(2)call, the user namespace is guaranteed to be created first, giving the child (clone(2)) or caller (unshare(2)) privileges over the remaining namespaces created by the call. Thus, it is possible for an unprivileged caller to specify this combination of flags.”
The unshare(2) man page states the capability consequence directly: “creating a user namespace automatically confers a full set of capabilities, [so] creating both a user namespace and any other type of namespace in the same unshare() call does not require the CAP_SYS_ADMIN capability in the original namespace.” This ordering guarantee is why a rootless container runtime issues CLONE_NEWUSER together with the other flags and gets a working container in one trip into the kernel — see clone unshare and setns.
The Nesting Limit, and a Documentation Discrepancy
The first gate is a single line:
ret = -ENOSPC;
if (parent_ns->level > 32)
goto fail;Two things are worth pinning down here, because the documentation and the code do not agree.
The errno. user_namespaces(7) says calls that exceed the limit “fail with the error EUSERS.” That is stale. unshare(2) documents the change explicitly — “ENOSPC (since Linux 4.9; beforehand EUSERS)” and “EUSERS (from Linux 3.11 to Linux 4.8)” — and the v6.12 source confirms it: create_user_ns() returns -ENOSPC. On any modern kernel, expect ENOSPC. Code that tests for EUSERS has been wrong since 2016.
The depth. The man page states a “limit of 32 nested levels of user namespaces (since Linux 3.11)”. The code permits creation whenever parent_ns->level <= 32, and sets ns->level = parent_ns->level + 1, with init_user_ns at level 0 — which admits namespaces at levels 1 through 33.
Uncertain
Verify: whether the reachable maximum
user_namespace.levelon Linux v6.12 is 32 or 33. Reason: the prose inuser_namespaces(7)says “32 nested levels”, while the v6.12create_user_ns()guard readsif (parent_ns->level > 32) goto fail;withinit_user_ns.level == 0, which arithmetically allows a child at level 33. Both readings are self-consistent depending on whether “level” counts the initial namespace. To resolve: run a nesting loop (unshare -Urecursively) until it returnsENOSPCand readreadlink /proc/self/ns/usercounts, or instrumentns->level. The practical fact — that the depth is a small fixed constant around 32 and that exceeding it yieldsENOSPC— is not in doubt.#uncertain
The Capability Check Walk — cap_capable() Symbol by Symbol
ns_capable(ns, cap) asks the Linux Security Module (LSM) layer, via security_capable(current_cred(), ns, cap, opts), whether the current process’s credentials grant cap with respect to user namespace ns. With no module overriding the decision, that lands in cap_capable() in security/commoncap.c — a loop of four tests that is the user-namespace privilege model in executable form:
int cap_capable(const struct cred *cred, struct user_namespace *targ_ns,
int cap, unsigned int opts)
{
struct user_namespace *ns = targ_ns;
for (;;) {
/* Do we have the necessary capabilities? */
if (ns == cred->user_ns)
return cap_raised(cred->cap_effective, cap) ? 0 : -EPERM;
/* If we're already at a lower level than we're looking for,
* we're done searching. */
if (ns->level <= cred->user_ns->level)
return -EPERM;
/* The owner of the user namespace in the parent of the
* user namespace has all caps. */
if ((ns->parent == cred->user_ns) && uid_eq(ns->owner, cred->euid))
return 0;
/* If you have a capability in a parent user ns, then you have
* it over all children user namespaces as well. */
ns = ns->parent;
}
}Read it symbol by symbol. cred is the credentials being tested; cred->user_ns is the namespace those credentials live in. targ_ns is the namespace the operation is being checked against — the owning user namespace of whatever resource is being touched. The loop walks ns from targ_ns upward toward init_user_ns:
ns == cred->user_ns— we have walked up to the caller’s own namespace. The answer is now purely local: is the bit raised incap_effective? This is the only place the capability bitmask is ever consulted. Everything else in the capability machinery (Capability Sets and the Bounding Set, Capability Transitions Across execve) exists to decide whatcap_effectivecontains; this line is where it is spent.ns->level <= cred->user_ns->level— an early-exit. If we have walked up to a namespace at or above the caller’s own depth without having hit the caller’s namespace, the caller cannot be an ancestor: the two are on different branches of the tree. Return-EPERM. This is the line that makeslevelload-bearing rather than cosmetic, and it is why capabilities never flow sideways between sibling namespaces.ns->parent == cred->user_ns && uid_eq(ns->owner, cred->euid)— the owner-UID rule. A process sitting in the parent whose effective UID matches the child’s recordedownergets everything in the child, with no capability bit required at all. This is the rule that grants a container supervisor total control over the namespace it created.ns = ns->parent— otherwise climb one level and repeat, which implements the downward-inheritance rule: “If a process has a capability in a user namespace, then it has that capability in all child (and further removed descendant) namespaces as well.”
flowchart TB A["cap_capable(cred, targ_ns, cap)<br/>ns := targ_ns"] B{"ns == cred->user_ns ?"} C{"cap_raised(cred->cap_effective, cap)"} D{"ns->level <= cred->user_ns->level ?"} E{"ns->parent == cred->user_ns<br/>&& ns->owner == cred->euid ?"} F["ns := ns->parent"] A --> B B -->|"yes"| C C -->|"bit set"| OK["return 0 — ALLOWED"] C -->|"bit clear"| NO["return -EPERM"] B -->|"no"| D D -->|"yes — different branch<br/>or shallower"| NO D -->|"no"| E E -->|"yes — owner rule"| OK E -->|"no"| F F --> B
The capability decision as an upward walk of the user-namespace tree. What it shows: starting at the namespace that owns the resource, the kernel climbs toward init_user_ns, stopping when it reaches the caller’s own namespace (then consult cap_effective), when it can prove the caller is not an ancestor (deny), or when the caller is the parent-side owner (allow unconditionally). The insight to take: there is no global privilege table anywhere. “Am I allowed?” is answered by a pointer chase up a tree that is at most ~32 deep, and the answer is different for every resource you touch, because every resource contributes a different targ_ns.
The capable() Family — Which Variant Answers Which Question
Kernel code does not call cap_capable() directly. It calls one of a small family of wrappers, and picking the wrong one is a recurring source of security bugs. All are defined in kernel/capability.c unless noted.
| Function | Whose credentials | Against which namespace | Sets PF_SUPERPRIV? | Use it when |
|---|---|---|---|---|
capable(cap) | current | init_user_ns | yes | The resource is genuinely global — system time, module loading, raw device access |
ns_capable(ns, cap) | current | ns | yes | The resource is virtualised; pass the owning user namespace |
ns_capable_noaudit(ns, cap) | current | ns | yes | A speculative check whose failure is normal and should not be audited |
ns_capable_setid(ns, cap) | current | ns | yes | Inside setuid/setgid/setgroups; passes CAP_OPT_INSETID so LSMs can distinguish |
file_ns_capable(file, ns, cap) | file->f_cred — the opener’s, captured at open() | ns | no | A write to a /proc file must be judged by who opened it, not who is writing now |
has_capability(task, cap) | another task’s, under RCU | init_user_ns | no | Inspecting a different process; has_ns_capability(t, ns, cap) is the namespace-aware form |
capable_wrt_inode_uidgid(idmap, inode, cap) | current | current’s own user_ns | yes | Filesystem operations — adds the inode-mapping requirement (below) |
ptracer_capable(tsk, ns) | the tracer’s, via tsk->ptracer_cred | ns | no | Deciding whether a traced process may be trusted |
bpf_capable() / perfmon_capable() | current | init_user_ns | yes | Convenience wrappers: CAP_BPF/CAP_PERFMON or CAP_SYS_ADMIN |
checkpoint_restore_ns_capable(ns) | current | ns | yes | CAP_CHECKPOINT_RESTORE or CAP_SYS_ADMIN in ns |
The file_ns_capable() entry is the one worth memorising. Its kernel doc comment states the rule and the reason it deliberately skips the accounting flag: “This does not set PF_SUPERPRIV because the caller may not actually be privileged.” Writing to /proc/[pid]/uid_map is checked with file_ns_capable() precisely so that a privileged helper can open() the file, hand the descriptor to an unprivileged process, and have the kernel still judge the write against the opener’s credentials. Get this wrong and you have a confused-deputy vulnerability.
The last three rows also make a quiet point about the granularity problem: bpf_capable() and perfmon_capable() are literally capable(CAP_BPF) || capable(CAP_SYS_ADMIN). The capabilities carved out of CAP_SYS_ADMIN in Linux 5.8 did not shrink CAP_SYS_ADMIN — it still implies them, for backward compatibility. See CAP_SYS_ADMIN and the Capability Granularity Problem.
What Root-Inside Actually Buys You — and What It Does Not
This is the single most important idea in the note, and it is where most write-ups stop at “you’re root but not really root.” The boundary is precise, and it has two independent halves.
Half one: is the resource governed by a namespace you own? user_namespaces(7) puts it plainly: “Having a capability inside a user namespace permits a process to perform operations (that require privilege) only on resources governed by that namespace.” Operations on things that no namespace virtualises still route through plain capable(), i.e. against init_user_ns, and there you are nobody.
Half two, for filesystem operations only: are the object’s IDs mapped into your namespace? This is the half people miss. capable_wrt_inode_uidgid() in kernel/capability.c is an AND of two conditions:
bool capable_wrt_inode_uidgid(struct mnt_idmap *idmap,
const struct inode *inode, int cap)
{
struct user_namespace *ns = current_user_ns();
return ns_capable(ns, cap) &&
privileged_wrt_inode_uidgid(ns, idmap, inode);
}privileged_wrt_inode_uidgid() returns true only if vfsuid_has_mapping(ns, …) and vfsgid_has_mapping(ns, …) — that is, only if both the inode’s owning UID and its owning GID translate into the current user namespace. This is why CAP_DAC_OVERRIDE held inside a container does not let you read /etc/shadow on the host: that file is owned by host uid 0, and host uid 0 has no mapping in your namespace, so the second conjunct is false regardless of how many capability bits you hold. The man page documents the rule and one exception: CAP_FOWNER is “treated somewhat exceptionally” in that only the file’s UID needs a mapping, not its GID.
| Operation class | Check used | Works as root-in-userns? |
|---|---|---|
sethostname(2), setdomainname(2) | ns_capable(uts_ns->user_ns, CAP_SYS_ADMIN) | Yes, if you own the UTS namespace |
iptables/nftables, veth creation, routing | ns_capable(net->user_ns, CAP_NET_ADMIN) | Yes, if you own the network namespace |
Bind mounts; mounting proc, sysfs, devpts, tmpfs, ramfs, mqueue, bpf, overlayfs | ns_capable(mnt_ns->user_ns, CAP_SYS_ADMIN) | Yes — see the per-filesystem “since” table below |
| Mounting a block-backed filesystem (ext4, xfs, …) | capable(CAP_SYS_ADMIN) — initial namespace only | No |
chown/chmod/read-past-DAC on a host-owned file | capable_wrt_inode_uidgid() | No unless the file’s UID and GID are both mapped |
settimeofday(2), adjtimex(2) | capable(CAP_SYS_TIME) | No — wall-clock time is not namespaced |
init_module(2), finit_module(2) | capable(CAP_SYS_MODULE) | No |
mknod(2) of a device node | capable(CAP_MKNOD) | No |
reboot(2) | capable(CAP_SYS_BOOT) | No |
Raw I/O — ioperm, /dev/mem | capable(CAP_SYS_RAWIO) | No |
The privilege boundary as a lookup table. What it shows: every “yes” row is a resource that some namespace virtualises and whose owning user namespace you control; every “no” row is either a genuinely global resource or a filesystem object whose identity does not translate into your namespace. The insight to take: the man page’s own list of unnamespaced privileges — “changing the system time (CAP_SYS_TIME), loading a kernel module (CAP_SYS_MODULE), and creating a device (CAP_MKNOD)” — is the honest answer to “is root-in-a-userns dangerous?” Those three alone would be full system compromise, and they are exactly what the model withholds.
The filesystem-mount list is the part that has grown over time, and the growth is the feature’s real history:
Filesystem mountable with CAP_SYS_ADMIN in the owning userns | Since |
|---|---|
proc, sysfs | Linux 3.8 |
devpts, tmpfs, ramfs, mqueue | Linux 3.9 |
bpf | Linux 4.4 |
cgroup v2 and cgroup v1 named hierarchies (none,name=) | Linux 4.6 |
overlayfs | Linux 5.11 |
| Anything block-backed (ext4, XFS, btrfs, …) | Never — requires CAP_SYS_ADMIN in init_user_ns |
Source: user_namespaces(7), “Effect of capabilities within a user namespace”. The insight to take: each row is a deliberate decision that a given filesystem’s parsing code is safe enough to expose to untrusted input, and each has been the subject of security review. overlayfs arriving only in 5.11 — a decade after user namespaces — is the tell: overlayfs-in-userns was a repeated source of local privilege escalation, and its admission was contentious. This table is also, read the other way, a map of where to look for bugs.
There is one more scoping rule that catches people. Executing a set-user-ID binary inside a user namespace does not silently confer host privilege: “if either the user or the group ID of the file has no mapping inside the namespace, the set-user-ID (set-group-ID) bit is silently ignored: the new program is executed, but the process’s effective user (group) ID is left unchanged” — deliberately mirroring MS_NOSUID semantics. A host /usr/bin/sudo owned by unmapped uid 0 is, inside your namespace, just a program.
Unmapped IDs, and What getuid() Returns Before You Have a Map
A freshly created user namespace has no mappings at all. Until uid_map is written, every ID in the namespace is unmapped, and the kernel must return something to userspace. It returns the overflow ID, default 65534 (/proc/sys/kernel/overflowuid and overflowgid) — which is why a bare unshare -U without --map-root-user drops you into a shell reporting uid=65534(nobody).
stateDiagram-v2 [*] --> Unmapped: unshare(CLONE_NEWUSER) Unmapped --> Unmapped: getuid() → 65534 (overflowuid) note right of Unmapped uid_map empty, gid_map empty. setuid/setgid/setgroups all FAIL. Reading uid_map shows nothing. Second field of an unmapped entry displays as 4294967295, NOT overflowuid. end note Unmapped --> SetgroupsDenied: write "deny" to /proc/self/setgroups SetgroupsDenied --> GidMapped: write gid_map (no CAP_SETGID in parent needed) Unmapped --> GidMapped: write gid_map (needs CAP_SETGID in parent) GidMapped --> FullyMapped: write uid_map Unmapped --> UidMapped: write uid_map UidMapped --> FullyMapped: write gid_map FullyMapped --> FullyMapped: setuid/setgid within mapped range OK note right of FullyMapped Each map is write-once. A second write returns EPERM. setgroups(2) permitted only if gid_map written AND flag still "allow". end note
The identity lifecycle of a user namespace. What it shows: a namespace begins with both maps empty and every ID reporting as the overflow ID; the maps may then be written once each, in either order, with the setgroups “deny” write being a prerequisite for the unprivileged gid_map path. The insight to take: the “root inside” that people talk about is two independent things — a full capability set, granted unconditionally at creation, and a UID of 0, which requires a successful map write. You can have the capabilities without ever being uid 0, and a process in that state is genuinely strange: CapEff of 0000001fffffffff with Uid: 65534.
One asymmetry is worth flagging because it surprises everyone: unmapped IDs are converted to the overflow ID almost everywhere — getuid(2), stat(2), SCM_CREDENTIALS, siginfo_t.si_uid, /proc/[pid]/status — except when reading a uid_map/gid_map file itself, where an unmapped second field is displayed as 4294967295. The initial namespace’s own dummy map illustrates the reserved value directly. It is a static initializer in kernel/user.c:
struct user_namespace init_user_ns = {
.uid_map = { { .extent[0] = { .first = 0,
.lower_first = 0,
.count = 4294967295U, },
.nr_extents = 1, }, },
/* … gid_map and projid_map identical … */
.owner = GLOBAL_ROOT_UID,
.group = GLOBAL_ROOT_GID,
.flags = USERNS_INIT_FLAGS, /* USERNS_SETGROUPS_ALLOWED */
};count is 4294967295, not 4294967296: the identity map covers IDs 0 through 4294967294 and deliberately leaves (uid_t) -1 unmapped, because several interfaces — setreuid(2) among them — use -1 to mean “no user ID”. Leaving it unmappable guarantees the sentinel can never collide with a real identity. This is why cat /proc/$$/uid_map on the host prints 0 0 4294967295: you are reading a compile-time constant.
Who May Write uid_map — the Rules Where the Security Lives
The maps are the hinge of the whole design: a namespace with a full capability set but no mapping is inert, and a namespace mapped to an ID range you do not own would be a privilege-escalation primitive. The write rules are correspondingly intricate, and this is precisely the part most write-ups hand-wave. The file format and extent arithmetic belong to UID and GID Mapping; what follows is the authorisation logic, which belongs here.
Two functions decide it, both in kernel/user_namespace.c. First map_write() applies the structural preconditions:
/* Only allow < page size writes at the beginning of the file */
if ((*ppos != 0) || (count >= PAGE_SIZE))
return -EINVAL;
…
ret = -EPERM;
/* Only allow one successful write to the map */
if (map->nr_extents != 0)
goto out;
/* Adjusting namespace settings requires capabilities on the target. */
if (cap_valid(cap_setid) && !file_ns_capable(file, map_ns, CAP_SYS_ADMIN))
goto out;Three rules in nine lines: the write must be a single sub-page write at offset 0 (so lseek/pwrite cannot be used to build a map incrementally); nr_extents != 0 makes the map write-once, returning EPERM on any second attempt; and the writer must have CAP_SYS_ADMIN in the target namespace, judged via file_ns_capable() against the credentials captured when the file was opened.
Then new_idmap_permitted() decides whether the contents are allowed. It encodes three separate paths:
if (cap_setid == CAP_SETUID && !verify_root_map(file, ns, new_map))
return false;
/* Don't allow mappings that would allow anything that wouldn't
* be allowed without the establishment of unprivileged mappings. */
if ((new_map->nr_extents == 1) && (new_map->extent[0].count == 1) &&
uid_eq(ns->owner, cred->euid)) {
u32 id = new_map->extent[0].lower_first;
if (cap_setid == CAP_SETUID) {
kuid_t uid = make_kuid(ns->parent, id);
if (uid_eq(uid, cred->euid))
return true;
} else if (cap_setid == CAP_SETGID) {
kgid_t gid = make_kgid(ns->parent, id);
if (!(ns->flags & USERNS_SETGROUPS_ALLOWED) &&
gid_eq(gid, cred->egid))
return true;
}
}
…
/* Allow the specified ids if we have the appropriate capability
* (CAP_SETUID or CAP_SETGID) over the parent user namespace.
* And the opener of the id file also has the appropriate capability. */
if (ns_capable(ns->parent, cap_setid) &&
file_ns_capable(file, ns->parent, cap_setid))
return true;
return false;| Path | Conditions | What you may map |
|---|---|---|
| Privileged | ns_capable(ns->parent, CAP_SETUID) and file_ns_capable(file, ns->parent, CAP_SETUID) — held by the current writer and by the opener of the file | Arbitrary ranges, arbitrarily many extents (up to 340) |
| Unprivileged single-ID | Exactly one extent, count == 1, ns->owner == cred->euid, and the mapped-to ID equals the writer’s own effective UID in the parent | Exactly your own UID, to any single ID inside the namespace (commonly 0) |
| Unprivileged single-ID, GID | Same, plus setgroups must already be denied (!(ns->flags & USERNS_SETGROUPS_ALLOWED)) | Exactly your own GID |
| Denied | Anything else | EPERM |
The second path is what unshare --map-root-user uses: it writes the single line 0 1000 1, mapping the caller’s own UID and nothing else. The kernel’s comment states the design goal exactly — “Don’t allow mappings that would allow anything that wouldn’t be allowed without the establishment of unprivileged mappings.” You are permitted to rename yourself inside the namespace; you are not permitted to become anyone else. Mapping a range wider than one ID requires a delegated privileged helper — newuidmap/newgidmap, which are CAP_SETUID-endowed and consult /etc/subuid and /etc/subgid (subuid(5)) — covered in UID and GID Mapping and Rootless Containers.
The verify_root_map() guard is the Linux 5.12 addition, and its rationale is a nice miniature of how these bugs are found:
if (map_ns == file_ns) {
/* The process unshared its ns and is writing to its own
* /proc/self/uid_map. User already has full capabilites in
* the new namespace. Verify that the parent had CAP_SETFCAP
* when it unshared. */
if (!file_ns->parent_could_setfcap)
return false;
} else {
/* Process p1 is writing to uid_map of p2, who is in a child
* user namespace to p1's. Verify that the opener of the map
* file has CAP_SETFCAP against the parent of the new map
* namespace */
if (!file_ns_capable(file, map_ns->parent, CAP_SETFCAP))
return false;
}This runs only when the proposed map contains an extent with lower_first == 0 — i.e. when it maps parent-namespace UID 0 into the child. The bug it closes, spelled out in user_namespaces(7): a UID-0 process lacking CAP_SETFCAP (which is needed to write namespaced file capabilities) could create a user namespace with the identity mapping 0 0 …; inside, it holds CAP_SETFCAP unconditionally, so it could stamp a binary with file capabilities that — because root is the same root on both sides of the identity map — would then be effective in the parent. The fix is the parent_could_setfcap boolean recorded at creation: the namespace remembers whether its creator was allowed to do this, and refuses the identity map if not. That single stored bit is the entire fix.
sequenceDiagram autonumber participant U as unprivileged process<br/>(host uid 1000) participant K as kernel participant H as newuidmap<br/>(CAP_SETUID helper) participant P as /etc/subuid U->>K: unshare(CLONE_NEWUSER) K->>K: create_user_ns(): 5 gates, then<br/>cap_permitted = cap_effective = CAP_FULL_SET K-->>U: success — full caps in new ns, but uid = 65534 Note over U,K: Capabilities WITHOUT identity.<br/>setuid()/setgid() still fail: no map. alt Single-ID path (unshare --map-root-user) U->>K: write "deny" > /proc/self/setgroups K->>K: clear USERNS_SETGROUPS_ALLOWED (one-way) U->>K: write "0 1000 1" > /proc/self/uid_map K->>K: map_write: offset 0, <PAGE_SIZE, nr_extents==0,<br/>file_ns_capable(file, ns, CAP_SYS_ADMIN) K->>K: new_idmap_permitted: 1 extent, count 1,<br/>ns->owner == euid, maps to own euid → allow K-->>U: 4 bytes written else Range path (rootless container) U->>H: newuidmap PID 0 1000 1 1 100000 65535 H->>P: look up "alice:100000:65536" P-->>H: range authorised H->>K: write both lines > /proc/PID/uid_map K->>K: new_idmap_permitted: ns_capable(parent, CAP_SETUID)<br/>&& file_ns_capable(file, parent, CAP_SETUID) → allow K-->>H: written end U->>K: setuid(0) / setgid(0) K-->>U: now uid 0 inside; unshare(CLONE_NEWNS|NEWPID|NEWNET) succeeds
The full privilege pivot as a message sequence, showing both the unprivileged single-ID path and the delegated range path. What it shows: capabilities arrive first and unconditionally at unshare() time; identity arrives second and is the part that is actually policed — either by the narrow “map only yourself” rule, or by a setuid helper that checks /etc/subuid. The insight to take: the ordering is the design. Because the caps come first, the process can write its own map files (it holds CAP_SYS_ADMIN in the new namespace); because the contents are policed against the parent namespace, holding those caps buys it nothing it did not already have. The pivot is safe only because the two checks look in opposite directions.
Joining an Existing Namespace: setns() Grants a Full Set Too
Creation is not the only way to acquire in-namespace root. user_namespaces(7): “A single-threaded process can join another user namespace with setns(2) if it has the CAP_SYS_ADMIN in that namespace; upon doing so, it gains a full set of capabilities in that namespace.” The implementation, userns_install(), is four preconditions and then the same set_cred_user_ns() call used at creation:
/* Don't allow gaining capabilities by reentering
* the same user namespace. */
if (user_ns == current_user_ns())
return -EINVAL;
/* Tasks that share a thread group must share a user namespace */
if (!thread_group_empty(current))
return -EINVAL;
if (current->fs->users != 1)
return -EINVAL;
if (!ns_capable(user_ns, CAP_SYS_ADMIN))
return -EPERM;
…
set_cred_user_ns(cred, get_user_ns(user_ns));Each precondition closes a specific hole. Re-entering your own namespace is forbidden because it would otherwise be a free capability reset — drop your caps, setns() back into where you already are, get CAP_FULL_SET again. Multi-threaded processes are refused because credentials are per-thread but the user namespace is a property the whole thread group must agree on. current->fs->users != 1 rejects a process sharing its fs_struct (root and cwd) with another task — the same CLONE_FS sharing that produced CVE-2013-1858, where combining CLONE_NEWUSER with CLONE_FS let a process in one namespace chroot() a process in another. And the capability check is ns_capable(user_ns, CAP_SYS_ADMIN) — evaluated by the tree walk above, so it succeeds if you are already root in an ancestor of the target, or if your euid matches the target’s owner.
The practical consequence, which is what nsenter(1) and docker exec rely on: entering a container’s user namespace from the host as root works because host root holds CAP_SYS_ADMIN in init_user_ns, which by the downward-inheritance rule means it holds it everywhere below.
Worked Example: Becoming Root Without Being Root
The canonical demonstration uses unshare(1) from util-linux, which wraps unshare(2) and the map writes.
$ id
uid=1000(alice) gid=1000(alice) groups=1000(alice)
$ unshare --user sh -c 'id -u; cat /proc/self/uid_map'
65534 # (1) no map yet → overflowuid
# (2) uid_map is empty: nothing printed
$ unshare --user --map-root-user \
sh -c 'whoami; cat /proc/self/uid_map /proc/self/gid_map'
root # (3)
0 1000 1 # (4)
0 1000 1Step by step. (1) A bare --user creates the namespace and grants CAP_FULL_SET, but writes no map, so getuid() has no translation and returns the overflow UID, 65534. (2) uid_map is genuinely empty — this is the “capabilities without identity” state. (3) --map-root-user additionally writes the maps; whoami now resolves uid 0. Per unshare(1), this option “implies --setgroups=deny and --user” and “is equivalent to --map-user=0 --map-group=0” — the setgroups deny is not optional, it is the precondition for the unprivileged gid_map write. (4) The map is the single line the unprivileged path permits: ID 0 in here ← ID 1000 out there, for 1 ID.
Widening beyond one ID requires delegated subordinate ranges. With /etc/subuid containing 1000:100000:65536, unshare(1) documents this session:
$ cat /etc/subuid
1000:100000:65536
$ unshare --user --map-auto --map-root-user
# id -u
0
# cat /proc/self/uid_map
0 1000 1 # (1) yourself → root inside
1 100000 65535 # (2) the delegated range
# touch file; chown 1:1 file # (3) legal: uid 1 IS mapped
# ls -ln --time-style=+ file
-rw-r--r-- 1 1 1 0 file
# exit
$ ls -ln --time-style=+ file
-rw-r--r-- 1 100000 100000 0 file # (4) same inode, different name(1) and (2) are two extents in one write — legal only because newuidmap-style delegation authorised the second one. (3) chown 1:1 succeeds because capable_wrt_inode_uidgid() finds CAP_CHOWN in the current user namespace and a mapping for the target IDs. Try chown 70000:70000 and it fails: 70000 is outside the mapped range. (4) is the punchline of the whole feature — the same file, viewed from the host, is owned by 100000. Nothing about the file changed; the translation did. There is exactly one inode with exactly one kernel-global owner (kuid_t 100000), rendered differently to observers in different namespaces.
Now the boundary, made tangible:
# hostname container0 # OK — UTS ns owned by our userns (with --uts)
# ip link add veth0 type veth # OK — net ns owned by our userns (with --net)
# mount -t tmpfs none /mnt # OK since Linux 3.9 (with --mount)
# mount /dev/sda1 /mnt # EPERM — block-backed fs needs init_user_ns
# date -s "2020-01-01" # EPERM — CAP_SYS_TIME is not namespaced
# modprobe dummy # EPERM — CAP_SYS_MODULE is not namespaced
# cat /etc/shadow # EPERM — host uid 0 is not mapped hereThat contrast — root-for-some-things, powerless-for-others, with the dividing line drawn by which namespace owns the resource and whether its IDs are mapped — is the privilege pivot made concrete.
Resource Limits: the ucounts Chain
The knob that actually shipped upstream to bound user namespaces is not a global on/off switch but a per-user, per-namespace counter. Each user_namespace carries ucount_max[UCOUNT_COUNTS], exposed through a per-namespace sysctl set mounted at user. — user.max_user_namespaces, user.max_pid_namespaces, user.max_net_namespaces, and so on, one per entry of enum ucount_type (kernel/ucount.c).
The enforcement in inc_ucount() is what makes them useful:
for (iter = ucounts; iter; iter = tns->ucounts) {
long max;
tns = iter->ns;
max = READ_ONCE(tns->ucount_max[type]);
if (!atomic_long_inc_below(&iter->ucount[type], max))
goto fail;
}The loop walks from the creating namespace all the way up to init_user_ns, incrementing a per-(namespace, uid) counter at every level and failing if any ancestor’s limit is exceeded. So setting user.max_user_namespaces = 0 in the initial namespace blocks creation for everybody below it, no matter how deeply nested — which is exactly the “resource limit” design Eric Biederman argued for in preference to a boolean sysctl (LWN, “Controlling access to user namespaces”, 2016). The counters are per-UID within each namespace, so one user cannot exhaust another’s budget.
The default is not infinite and is rarely stated anywhere. In kernel/fork.c, fork_init() runs:
for (i = 0; i < UCOUNT_COUNTS; i++)
init_user_ns.ucount_max[i] = max_threads/2;So the initial namespace’s default for every ucount type, max_user_namespaces included, is max_threads/2 — a value derived at boot from physical memory, typically in the tens of thousands. Newly created namespaces, by contrast, start at INT_MAX (ns->ucount_max[i] = INT_MAX in create_user_ns()), because the ancestor chain already bounds them.
flowchart BT L3["ucounts(U2, alice)<br/>ucount[USER_NAMESPACES]++"] L2["ucounts(U1, alice)<br/>ucount[USER_NAMESPACES]++"] L1["ucounts(init_user_ns, alice)<br/>ucount[USER_NAMESPACES]++"] L3 -->|"check vs U2.ucount_max<br/>= INT_MAX"| L2 L2 -->|"check vs U1.ucount_max<br/>= INT_MAX"| L1 L1 -->|"check vs init_user_ns.ucount_max<br/>= max_threads/2 (or admin override)"| VERDICT{"any level over limit?"} VERDICT -->|"yes"| FAIL["roll back every increment<br/>already done, return NULL<br/>→ create_user_ns fails -ENOSPC"] VERDICT -->|"no"| PASS["namespace created"]
How user.max_user_namespaces is enforced. What it shows: creating a namespace increments a counter for the creating UID at every level of the ancestry chain, and any single level’s limit can veto; a failure unwinds the increments it already made. The insight to take: this is why setting user.max_user_namespaces=0 at the host level is an effective kill switch even though the sysctl exists separately inside every namespace — a nested namespace cannot raise a limit its ancestor imposes, only lower it further for its own children.
Failure Modes and Common Misunderstandings
“Root inside means root outside.” The most dangerous misconception. Inside the namespace you are uid 0 with CAP_FULL_SET; outside you are uid 1000 with nothing. The kernel’s own set_cred_user_ns() comment — capabilities that are “useless for doing anything” outside — is the literal truth, and the two-column table above is the precise boundary.
Confusing the owner UID with the owning namespace. struct user_namespace.owner is a kuid_t recording who created the namespace; the “owning user namespace” of a net namespace is a struct user_namespace *. Conflating them produces wrong conclusions about who can do what from where. See the disambiguation table above.
Assuming creation always succeeds. The kernel requires no capability, but there are five gates, and on Ubuntu 24.04 LTS and later the LSM gate denies it by default for unconfined processes. Tooling that treats unshare(CLONE_NEWUSER) as infallible breaks on those systems.
Testing for EUSERS. Since Linux 4.9 the nesting-limit error is ENOSPC. user_namespaces(7) still says EUSERS; unshare(2) documents the change. Trust the newer page and the source.
Expecting setgroups to work. In a namespace created by the unprivileged path, setgroups(2) is permanently denied — and it is denied before gid_map is written, irreversibly, because proc_setgroups_write() refuses to re-enable it (if (!(ns->flags & USERNS_SETGROUPS_ALLOWED)) goto out_unlock;) and the restriction “propagates down to all child user namespaces.” Software that calls setgroups(2) on startup to drop supplementary groups — a very common hardening idiom — fails with EPERM inside rootless containers.
Assuming a full capability set means the bounding set is unlimited across execve. It is (cap_bset = CAP_FULL_SET), but execve(2) still recomputes capabilities by the usual rules. user_namespaces(7) warns: “unless the process has a user ID of 0 within the namespace, or the executable file has a nonempty inheritable capabilities mask, the process will lose all capabilities.” This is the concrete reason a rootless runtime writes uid_map before exec’ing the container entrypoint — otherwise the entrypoint starts unprivileged. See Capability Transitions Across execve.
Dumpability and /proc ownership. A subtle one from the man page: “A task that changes one of its effective IDs will have its dumpability reset to the value in /proc/sys/fs/suid_dumpable,” which can leave a parent unable to write its child’s uid_map file because the /proc file’s ownership changed. The documented workaround is prctl(PR_SET_DUMPABLE, 1) in the parent before creating the child.
| Symptom | errno | Likely cause |
|---|---|---|
unshare(CLONE_NEWUSER) fails | EPERM | LSM denial (AppArmor userns_create), or the caller is chrooted, or its euid/egid is unmapped in the parent |
unshare(CLONE_NEWUSER) fails | ENOSPC | Nesting depth exceeded, or a user.max_*_namespaces limit hit at some ancestor |
unshare(CLONE_NEWUSER) fails | EINVAL | CONFIG_USER_NS not enabled in the kernel |
Second write to uid_map | EPERM | Maps are write-once (map->nr_extents != 0) |
uid_map write rejected | EINVAL | Bad syntax, count == 0, overlapping ranges, >340 lines, ≥ PAGE_SIZE bytes, or a nonzero file offset |
uid_map write rejected | EPERM | Range wider than one ID without CAP_SETUID in the parent; or gid_map written before denying setgroups; or the 5.12 CAP_SETFCAP rule on an identity map |
setgroups(2) fails inside | EPERM | setgroups denied, or gid_map not yet written |
setns(2) into a userns fails | EINVAL | Target is your current namespace, process is multi-threaded, or fs_struct is shared |
id reports 65534 | — | No map written; you are seeing overflowuid |
chown/chmod on a bind-mounted host file fails | EPERM | The file’s UID or GID has no mapping — capable_wrt_inode_uidgid() second conjunct |
Why a Feature Designed to Reduce Privilege Became an Attack Surface
This is the uncomfortable part of the story, and it deserves an honest telling rather than a footnote.
User namespaces were designed to remove the need for privilege: instead of a setuid-root helper or a root daemon, an ordinary user could build a container themselves. Eric Biederman, their author, has consistently defended that framing — in the 2022 LSM-hook thread he wrote that restricting them would undermine “the goal … to reduce reliance on setuid programs running as root,” and warned the restrictions “would break general availability for ordinary applications like Chromium” (LWN, “A security-module hook for user-namespace creation”, 2022).
The problem is second-order. Granting an unprivileged user a namespace in which they hold CAP_SYS_ADMIN and CAP_NET_ADMIN does not grant them any intended privilege on the host — but it does hand them a key to kernel code paths that, before 2013, only root could ever reach: the netfilter rule parser, the overlayfs mount path, the mount-option parsers of several filesystems, the network protocol stacks reachable only after creating a socket in a fresh net namespace. All of that code was written under the tacit assumption that its callers were already root and therefore not adversarial. Michael Kerrisk stated the structural risk in 2013, when the feature was one release old (LWN, “Anatomy of a user namespaces vulnerability”): user namespaces “and their interactions with other parts of the kernel are rather complex—probably too complex” for the small group of developers with a close interest to vet exhaustively, and widespread deployment would surface more bugs.
He was right, and the first example was already in hand. CVE-2013-1858, found by Sebastian Krahmer in Linux 3.8, came from allowing CLONE_NEWUSER | CLONE_FS in one clone(2): the child got a full capability set in a new user namespace while sharing its fs_struct — root directory and cwd — with the parent, so it could chroot() a process in a different user namespace and subvert a setuid-root binary’s dynamic linker. Note what the fix looks like in today’s source: the if (current->fs->users != 1) return -EINVAL; line in userns_install(), and the current_chrooted() gate in create_user_ns(), are both descendants of this class of bug. The vulnerability was not in user namespaces; it was in the interaction between user namespaces and a decade-old flag.
Ubuntu’s specification for its 23.10 restriction names six CVEs as motivation — CVE-2022-0185, CVE-2022-1015, CVE-2022-2078, CVE-2022-24122, CVE-2022-25636 and CVE-2020-14386 — where “unprivileged user namespace capabilities enabled exploitation that would otherwise require root privileges” (Ubuntu spec, 2023). The pattern in every one is the same: the bug is elsewhere in the kernel; the user namespace is the ladder that lets an unprivileged attacker reach it. The full catalogue, with the mechanism of each, belongs to User Namespaces and Privilege Escalation.
Uncertain
Verify: the individual technical details of the six CVEs listed above. Reason: they are enumerated here as cited by Ubuntu’s specification document, not independently confirmed against each CVE’s own advisory or fixing commit during this task. To resolve: read each entry at
cve.org/nvd.nist.govand the corresponding upstream fixing commit. The claim actually made here — that Ubuntu’s spec cites these six as its motivation — is verified against the spec itself.#uncertain
The Gating Knobs — Which Are Mainline, Which Are Not
Four distinct mechanisms get conflated constantly. They are not the same thing and they do not all exist upstream.
| Knob | Where it lives | Semantics | Verified at v6.12 |
|---|---|---|---|
kernel.unprivileged_userns_clone | Downstream Debian/Ubuntu patch, never merged | Boolean: 0 blocks unprivileged CLONE_NEWUSER outright | Not present in mainline. Kees Cook’s upstream proposal (kernel.userns_restrict) was blocked by Eric Biederman as “buggy, and poorly thought through” (LWN 2016) |
user.max_user_namespaces | Mainline, kernel/ucount.c | Per-user, per-namespace count limit; 0 effectively disables creation for that subtree | Present. Registered from user_table[]; default in init_user_ns is max_threads/2 from fork_init() |
LSM hook userns_create (security_create_user_ns()) | Mainline since Linux 6.1; hook declared in include/linux/lsm_hook_defs.h | Lets any LSM veto creation with an errno | Present. Called from create_user_ns() |
kernel.apparmor_restrict_unprivileged_userns | Ubuntu downstream patch | Enforces AppArmor’s userns, rule for unconfined processes; default 1 on Ubuntu 24.04 LTS | Not present in mainline v6.12. The AppArmor sysctls that are upstream are kernel.unprivileged_userns_apparmor_policy and kernel.apparmor_restrict_unprivileged_unconfined (security/apparmor/lsm.c) |
That last row is worth dwelling on, because it is routinely reported as an upstream feature. What is upstream at v6.12 is the mediation: apparmor_userns_create() implements the userns_create LSM hook, and aa_profile_ns_perm() checks the AA_USERNS_CREATE permission against the confining profile, emitting audit records with requested="userns_create" / denied="userns_create" (security/apparmor/lsm.c, security/apparmor/task.c). Note the guard in apparmor_userns_create():
label = begin_current_label_crit_section();
if (!unconfined(label)) {
error = fn_for_each(label, profile,
aa_profile_ns_perm(profile, &ad,
AA_USERNS_CREATE));
}Upstream, an unconfined process is not checked at all — the hook returns success. Ubuntu’s added sysctl is exactly what changes that: it extends the restriction to unconfined processes, which is what makes it a system-wide default rather than an opt-in per-profile rule.
timeline title Gating unprivileged user namespaces — a decade of attempts 2013 : Linux 3.8 ships unprivileged CLONE_NEWUSER : CVE-2013-1858 (CLONE_FS) found within weeks 2015 : Linux 3.19 adds /proc/PID/setgroups after the negative-groups issue : Debian ships downstream kernel.unprivileged_userns_clone 2016 : Kees Cook proposes kernel.userns_restrict upstream : Biederman NAKs it; proposes resource limits instead : Linux 4.9 ships ucounts — user.max_user_namespaces 2018 : Linux 4.16 raises the id-map limit from 5 to 340 extents 2021 : Linux 5.11 permits overlayfs mounts in a user namespace : Linux 5.12 adds the CAP_SETFCAP rule on identity maps 2022 : LSM hook security_create_user_ns() proposed; merged for Linux 6.1 2023 : Ubuntu 23.10 ships opt-in AppArmor userns restriction 2024 : Ubuntu 24.04 LTS enables it by default : Jonathan Calmels proposes a per-userns capability set upstream
Ten years of trying to bound a feature designed to be unbounded. What it shows: every mechanism that shipped upstream is either a resource limit (ucounts) or a policy hook (userns_create), never a boolean off-switch — because the boolean was repeatedly rejected as too coarse. Distributions shipped the boolean anyway, downstream, because their users needed something immediately. The insight to take: the upstream/downstream split here is not incidental; it is the visible outcome of an unresolved disagreement about whether user namespaces are a privilege-reduction feature (Biederman) or an attack-surface-expansion feature (distribution security teams). Both are true, which is why the argument never ended.
The argument is still live. In June 2024 Jonathan Calmels proposed adding a sixth capability set — the “userns set” — such that when a thread creates a user namespace, the effective, permitted, bounding and userns sets inside are all initialised from the creator’s userns set rather than to CAP_FULL_SET, plus a kernel.cap_userns_mask sysctl to clamp it system-wide (LWN, “A capability set for user namespaces”, 2024). Serge Hallyn, the capabilities maintainer, was enthusiastic; Paul Moore rejected the LSM-hook portion; Casey Schaufler objected to the complexity.
Uncertain
Verify: whether the per-user-namespace capability set (
kernel.cap_userns_mask) has since been merged, and into which release. Reason: as of the June 2024 LWN coverage read during this task the proposal was under discussion with the LSM-hook component rejected; no post-2024 status was confirmed, and this note is written against v6.12, where no such set exists instruct credorinclude/linux/capability.h. To resolve: checkgit log --oneline -- kernel/capability.c security/commoncap.con a current mainline tree, orgrep cap_usernson the newest release.#uncertain
Alternatives and When to Choose Them
User namespaces are the only mechanism that lets an unprivileged process gain administrative control over the other namespace types. The alternatives are not substitutes so much as different points on a trade-off curve.
| Approach | Who creates the namespaces | Container uid 0 maps to | Breakout lands you as | Choose when |
|---|---|---|---|---|
| Rooted containers (classic Docker) | A daemon already holding CAP_SYS_ADMIN in init_user_ns; no user namespace at all | Host uid 0 | Host root | Simplicity matters more than blast radius, and the host is already a dedicated container node |
userns-remap (rooted daemon + user namespace) | Root daemon, but IDs remapped into an unprivileged host range | An unprivileged host uid | An unprivileged host user | You must keep a privileged daemon but want defence in depth |
| Rootless containers (Podman, rootless Docker, Buildah) | The unprivileged user, via the pivot in this note | An unprivileged host uid | The same unprivileged user they already were | Multi-tenant build farms, CI, developer workstations, anywhere a privileged daemon is unacceptable |
unshare(1) / bwrap ad hoc sandboxes | The user, directly | Whatever the map says | The invoking user | One-off isolation; Flatpak’s bubblewrap is the widely deployed example |
| gVisor | A user-space kernel intercepts syscalls | N/A — the host kernel is barely reached | Still inside the sentry | Untrusted multi-tenant workloads where host-kernel syscall surface is the threat |
| Kata Containers / microVMs | A hypervisor boots a real guest kernel | Guest root | Inside a VM | Hard isolation required; you accept the boot-time and memory cost |
The honest summary: user namespaces trade a large reduction in the privilege a container runtime needs for a modest increase in the kernel surface an attacker can reach. On a single-tenant machine running containers you wrote, that is an excellent trade. On a shared machine running untrusted code, distributions have concluded the trade needs a policy layer on top — hence AppArmor profiles carrying userns, rules, or a heavier sandbox entirely.
Production Notes
Rootless Podman and rootless Docker depend entirely on this mechanism: with no privileged daemon, the only way an ordinary user can create the PID/mount/net namespaces a container needs is to be root inside a user namespace they created. The combined-flags ordering guarantee is what makes their single clone3() call work; /etc/subuid delegation via newuidmap is what makes a container able to run more than one distinct UID. Build tools (buildah, BuildKit’s rootless mode), Flatpak’s bubblewrap, and Chromium’s renderer sandbox all lean on the same pivot — which is precisely why blanket-disabling user namespaces breaks a desktop.
Operationally, the two things worth configuring deliberately:
- Do not disable the feature globally if anything on the box uses it. Chromium, Podman, Flatpak, systemd’s
PrivateUsers=, and most CI runners will all degrade or fail. Prefer the AppArmor-style approach — allow it for the applications that need it, deny it for the ones that do not — or theuser.max_user_namespaceslimit if you need a blunt instrument on a server that genuinely runs no containers. - Know that a containerised environment may itself disable it. Ubuntu’s own documentation notes that “if installed, LXD will completely disable the user namespace restriction feature when running, effectively making this sysctl irrelevant” (Ubuntu Community Hub) — a good reminder that a security default verified on a clean image may not hold on a real host.
When debugging, three files answer nearly every question: /proc/[pid]/uid_map and /proc/[pid]/gid_map show the identity translation, /proc/[pid]/status shows CapPrm/CapEff/CapBnd/CapAmb as 64-bit hex (a fresh namespace shows 0000001fffffffff — 41 bits, caps 0 through CAP_LAST_CAP), and readlink /proc/[pid]/ns/user gives the namespace’s inode number so you can tell whether two processes are actually in the same namespace. For the ownership edges, the NS_GET_USERNS, NS_GET_PARENT and NS_GET_OWNER_UID ioctls (ioctl_nsfs(2)) are the only supported way to walk the graph; lsns(8) uses them.
On systems with the AppArmor restriction active, a denied creation is not silent — it produces an audit record containing apparmor="DENIED" operation="userns_create", generated by audit_ns_cb() in security/apparmor/task.c. If unshare -U returns EPERM on Ubuntu 24.04 or later, check dmesg/auditd for that string before suspecting anything else.
See Also
- UID and GID Mapping — the translation tables in detail: the extent format, the arithmetic of
map_id_down/map_id_up, the 340-extent limit, and thenewuidmap/newgidmaphelpers this note’s authorisation rules govern - Rootless Containers — the application of the privilege pivot: containers with no daemon and no host root, plus the userspace networking and storage workarounds it forces
- User Namespaces and Privilege Escalation — the security history in full: the CVE catalogue, the exploitation patterns, and the distribution gating that resulted
- POSIX Capabilities — the 41
CAP_*constants and the model whose checks this note shows being evaluated relative to a namespace - Capability Sets and the Bounding Set — what
cap_permitted/cap_effective/cap_bset/cap_ambientmean, i.e. whatset_cred_user_ns()is setting - Capability Transitions Across execve — why a rootless runtime must write
uid_mapbefore exec’ing the entrypoint - Process Credentials and struct cred — the object that holds
user_nsalongside the capability sets - clone unshare and setns — the three syscalls that create and join namespaces, including the
CLONE_NEWUSERordering guarantee - Linux Namespaces Overview — the eight namespace types and the general model of a private view of a global resource
- AppArmor — the LSM that implements the
userns_createhook and theuserns,profile rule - Linux Containers and Isolation MOC — the parent MOC (section C is the canonical owner of the user-namespace leaves)
- Linux Security MOC — sibling MOC; the confinement half of container isolation