Virtualization · Sandboxing
Unprivileged User Namespaces and the Attack Surface They Open
Why allowing any process to create a user namespace turns privileged kernel code paths into reachable ones, which subsystems are most exposed, and how the trade is bounded in practice.
Before user namespaces, an unprivileged process could reach a small fraction of the kernel: the syscalls any user may call, with the permission checks they carry. A user namespace changes the reachable set, because it converts “requires CAP_SYS_ADMIN” into “requires a namespace of one’s own”. Every code path that was gated on that capability now has a caller, and the code behind those gates was written under the assumption that its callers were trusted.
The gate that moves
The kernel’s permission checks are written against the namespace that owns the object, not against an absolute notion of privilege. mount checks CAP_SYS_ADMIN in the mount namespace’s owning user namespace. sethostname checks CAP_SYS_ADMIN in the UTS namespace’s owner. Creating a device node checks it against the mount’s owner. When a process creates a new user namespace, it becomes the owner of everything it subsequently creates inside that namespace, and it holds the full capability set with respect to them.
The operations that follow are real: mount of a filesystem image the process can construct, mount of proc, sysfs, tmpfs, overlayfs, fuse, and a long list of others; mknod for device nodes; setsockopt and interface configuration in a network namespace; ioctl on some devices. None of these affect the host, because the objects belong to the child namespace. All of them execute host kernel code with attacker-influenced arguments, and the difference between “does not affect the host” and “cannot corrupt the host” is the correctness of the kernel’s namespace plumbing.
Where the plumbing is thin
The historical vulnerability pattern in this area is not a missing capability check. It is a check that is correct for the object’s own namespace but wrong about a transition between namespaces: a filesystem that stores a user namespace reference in a superblock, a socket whose owning namespace differs from the namespace of the process that created it, a device whose ioctl handler assumes the caller’s CAP_SYS_ADMIN implies host authority.
Three subsystems have carried most of this load:
- Stacked and network filesystems.
overlayfs,fuse,9p, and the network filesystems were written for the case where the mounting process is privileged on the host. Under a user namespace, the mount is unprivileged but the filesystem’s internal operations still run with the kernel’s authority. Bugs in how such a filesystem copies up metadata, follows a dentry, or attributes a write to the wrong user produce privilege escalation rather than a confined failure. Overlayfs in particular has been the subject of repeated fixes in this area, because copy-up crosses between two layers whose ownership can differ. - The network stack. A network namespace gives an unprivileged process control over interfaces, routing, and packet sockets within it. Packet socket handling,
tcconfiguration, and thenetfiltertargets reachable from a network namespace are large code paths that were not always written to treat their caller as hostile. - The
ioctlsurface. Any device node the process can create inside its own namespace can be opened, and the driver’sioctlhandler runs. Whether that is safe depends on the driver checking the namespace of the file it was opened through, which many do and some historically did not.
None of this is an argument that user namespaces are broken. It is an argument about arithmetic: widening the set of reachable kernel code paths by a large factor, for every unprivileged process on the machine, multiplies the exposure of every latent bug in those paths.
The asymmetry that makes it worse
A kernel bug reached from inside a user namespace does not fail inside the namespace. The exploit’s effect is on the host kernel’s state, and the confinement that was supposed to contain it is enforced by the same kernel the exploit is now running in. A namespace escape is not a separate step; there is nothing to escape from once the kernel’s own memory is writable.
The second asymmetry is that the process needs no special setup. Creating a user namespace requires only unshare(CLONE_NEWUSER) or clone with that flag, and the capability set arrives automatically. There is no configuration file to audit, no setuid binary to check, and no administrator action to observe. Any process that can execute code can attempt it, including one reached through a parser bug in an unrelated program.
The third is that the namespace is inherited and composable. A process inside one can create a nested one, and the nesting is deep enough that the kernel’s recursion limits, not the design, bound it. Each level is another opportunity for a check to be evaluated against the wrong namespace.
Bounding the trade without removing the capability
The capability is load-bearing. Rootless containers, unprivileged sandboxes for build systems, browser renderer confinement on some platforms, and podman-style workflows all depend on creating a user namespace without root. Removing it returns those workloads to setuid helpers with their own attack surface.
The bounds that are actually available:
Restrict who may create one. user.max_user_namespaces sets a per-user-namespace limit on the number of nested user namespaces; setting it to 0 for a user disables creation for that user. Some distributions have carried a separate toggle for unprivileged creation, and its presence and default vary by distribution and version, so a policy that depends on it should verify the sysctl exists rather than assume it.
Restrict which programs may create one. An LSM can attach a rule to the unshare and clone paths for specific executables, permitting the operation only for a named binary with a known profile. That converts a system-wide capability into a per-program one, at the cost of a policy that has to be maintained as those programs change.
Deny it in the sandbox itself. A seccomp filter can return EPERM for unshare and clone when the flags contain CLONE_NEWUSER, which is the standard element of a container’s default profile. The check is a scalar comparison on the flags word, exactly the kind of argument a filter handles well, as described in What a Seccomp Filter Can See of a Syscall. Denying it inside a container prevents a compromised payload from acquiring a fresh capability set, which is the specific escalation path that matters once the container’s own privileges have been reduced by the sequence in How Capability Sets Split Root Authority.
Limit what the namespace can reach. A user namespace on its own gives no filesystem or network objects; it gives the ability to create them. A sandbox that permits user namespace creation but denies mount and mknod by seccomp removes most of the reachable surface while keeping the namespace for its intended purpose. The two mechanisms compose by intersection, so a denial at either layer holds.
The interaction with other sandbox layers
A user namespace resets the bounding capability set to full inside itself, which means the drop sequence used outside is not preserved across the transition. A process that carefully dropped capabilities from its bounding set and then creates a user namespace gets them all back, scoped to the new namespace. This is by design, and it is why the ordering in a setup sequence matters: if a user namespace is part of the design, capability drops performed before it are undone, and the drops must be performed after.
Similarly, no_new_privs does not prevent user namespace creation. It prevents gaining privilege through execve of a privileged binary, which is a different mechanism. A sandbox that sets no_new_privs and considers itself done has not addressed the namespace path at all.
Landlock and seccomp restrictions survive into a new user namespace, because they are attached to the process rather than to a namespace. That is the useful direction: a payload that creates a user namespace after being restricted is still restricted, and the capabilities it gains are capabilities with respect to objects it has not yet been able to create.
What to conclude about the boundary
The honest statement is that a user namespace is a correctness mechanism for building sandboxes and a large increase in kernel attack surface for everything else. Its security value comes from the layers stacked on top: a reduced capability set outside, a seccomp filter that denies the operations the payload does not need, a Landlock ruleset that bounds the filesystem, and a mount namespace that makes the host tree unreachable rather than merely renamed. Without those, a user namespace is a container in the sense of a wrapper, not in the sense of a boundary.
The measurement that matters when assessing exposure is not how many namespaces a system has but how many distinct kernel code paths an unprivileged process can reach. User namespaces raise that number substantially and, unlike a new syscall, they do so without requiring any new interface to be audited, since the paths they open are old ones whose assumptions no longer hold.
References
- user_namespaces(7) — Linux manual page — man-pages project
- Linux kernel documentation — User namespace sysctls — Linux kernel project
- capabilities(7) — Linux manual page — man-pages project