Est.

Linux Namespaces as Sandbox Isolation Primitives

Namespaces control visibility, not security—a distinction that explains most container escapes.

Senior Writer · · 14 min read
Cover illustration for “Linux Namespaces as Sandbox Isolation Primitives”
Sandbox Isolation Primitives · August 6, 2026 · 14 min read · 3,259 words

Linux namespaces are often described as "the foundation of container security." That framing is both true and dangerously incomplete. Having spent years working close to the kernel, watching containers move from curiosity to critical infrastructure, I have come to think the more useful framing is this: namespaces are a composable set of visibility controls, and confusing visibility with security has caused a significant portion of the container escapes the industry keeps re-learning from. What follows is an attempt to think through that distinction carefully, from the syscalls up.

The three syscalls that create, join, and move between namespaces

The entire namespace machinery is accessible through exactly three syscalls, and that constraint is worth pausing on. You cannot stumble into namespace composition through some ambient API; every namespace boundary is the product of an explicit, deliberate syscall.

clone() is the starting point. It spawns a new process and, depending on which flags you pass, places that process inside newly created namespaces. CLONENEWPID, CLONENEWNET, CLONE_NEWUSER: each flag creates a new namespace of the corresponding type and hands the child process an isolated view of that resource. The flags compose freely; you can pass as many as your privilege level permits in a single call.

unshare() operates differently. It is called by an already-running process that wants to leave its current namespace and enter a new one, without forking. No new process is required. This is how a shell script can isolate its own mount namespace mid-execution, or how an init system can narrow a service's network visibility before executing the service binary.

setns() is the join primitive. Given a file descriptor pointing to a namespace pseudo-file under /proc/[pid]/ns/, a process can enter any existing namespace that it has permission to reach. This is what nsenter(1) uses under the hood. It is also, notably, the mechanism by which a debugging tool or container runtime can inspect or manipulate a running container's environment from outside.

The filesystem representation matters here. Each namespace a process inhabits is exposed as a symlink under /proc/[pid]/ns/, and those symlinks persist as long as at least one process inhabits the namespace or a file descriptor holds it open. Since Linux 4.9, /proc/sys/user/ exposes per-user limits on namespace creation counts, providing a lever against resource exhaustion.

Understanding these three syscalls clarifies something that trips up practitioners who approach namespaces through container tooling: composing multiple namespace types is not automatic. Every type requires an explicit flag. A runtime that forgets to pass CLONE_NEWNET leaves network isolation out of the composition entirely, and there is no default that compensates.

What each of the eight namespace types actually seals off

The mount namespace arrived first. Its kernel flag is CLONE_NEWNS, deliberately generic, because when Al Viro's work landed in kernel 2.4.19, no one had yet planned for seven more types to follow. Mount namespaces isolate the set of filesystem mount points: processes in different mount namespaces see different filesystem hierarchies, with changes to one hierarchy invisible to the other. This is the foundation of container rootfs isolation, the mechanism that makes / inside a container refer to a different tree than / on the host.

UTS and IPC namespaces arrived together in subsequent releases. UTS isolation is narrow: it separates hostname and NIS domain name, so each container can present its own identity to uname() and gethostname() without contaminating the host's values. IPC isolation is broader in consequence: it seals off System V shared memory segments, semaphores, and POSIX message queues. Without IPC namespace isolation, a process inside a container could attach to a shared memory segment created by a host process, simply by knowing its key. That cross-boundary communication channel is not obvious, which is partly why IPC namespaces tend to be included by default even in lighter sandboxes.

PID namespaces introduce a conceptual wrinkle worth dwelling on. The first process inside a PID namespace becomes PID 1 inside that namespace, but it has a different PID from the perspective of the parent namespace. One process, two valid numeric identities depending on which namespace is doing the looking. /proc visibility is scoped accordingly: a process inside a PID namespace cannot see /proc entries for processes outside it. Signals respect this boundary too. The container's init-equivalent is PID 1 to everything running alongside it, but just another numbered process to the host scheduler.

Network namespaces provide the most complete isolation of any single type in terms of observable state. A new network namespace contains its own routing table, IP addresses, socket table, connection tracking, and firewall rules. It starts with only a loopback interface. Connecting a network namespace to the outside world requires explicit plumbing, typically a virtual ethernet pair (veth). That pair must be created, one end placed in the namespace, addressed, and routed. Nothing is implicit. This makes network namespaces both the most powerful isolation tool in the set and the most operationally complex to configure correctly.

User namespaces are the most consequential type, both for capability and for risk. A user namespace maps UIDs and GIDs between the namespace's internal view and the host's global view. A process can hold UID 0 inside the namespace while mapping to an unprivileged UID on the host. This mapping is the mechanism behind unprivileged containers: if a process escapes, the host observes only a restricted UID. User namespaces also grant capabilities scoped to the new namespace, and that scoping has attack-surface implications that deserve their own treatment later.

Cgroup namespaces isolate the cgroup root directory. Without this, a process could traverse upward through the cgroup hierarchy and observe the host's resource topology: how many containers exist, how resources are allocated across them. The cgroup namespace presents a virtualized root, cutting off that upward visibility.

The time namespace, the youngest of the eight, arrived in Linux 5.6. It isolates CLOCKMONOTONIC and CLOCKBOOTTIME, allowing a container to present a different clock offset from the host. This is primarily useful for checkpoint-restore and live migration scenarios, where a container resumed on a different host would otherwise observe a jarring discontinuity in monotonic time.

As of Linux 6.1, these eight types, cgroup, ipc, mnt, net, pid, time, user, and uts, constitute the complete set, per the Linux namespaces(7) man page. Each is independently composable. A sandbox can combine any subset. The absence of a type is not a default; it is a deliberate choice, with real consequences for what the sandboxed process can reach.

How namespaces combine in practice across containers, browsers, and sandboxing tools

Container runtimes such as Docker, LXC, and Podman combine all eight namespace types by default, and they pair that combination with cgroups for resource limits. The distinction between the two mechanisms is important and often elided in documentation: namespaces handle visibility, cgroups handle consumption. A namespace prevents a process from seeing another process's network stack; a cgroup prevents a process from exhausting the host's CPU or memory. Neither substitutes for the other, and a configuration that deploys one without the other is incomplete in a specific, describable way.

Google Chrome on Linux illustrates selective composition more visibly. Its renderer sandbox uses user namespaces and, where a setuid sandbox is not available, network and PID namespaces to isolate renderer processes. That constitutes the first layer. The second layer is Seccomp-BPF, which restricts which syscalls a renderer may issue to the kernel. The two layers address different threat surfaces: the namespace layer limits what a renderer can reach in terms of system resources; the seccomp layer limits how it can communicate with the kernel itself. Chrome's architecture makes explicit what container tooling sometimes obscures: namespaces and syscall filtering are not redundant; they are complementary, each covering a gap the other leaves open.

ChromeOS Minijail applies namespace composition at the per-service level, configured through init scripts. Rather than a single container-wide policy, each service receives a namespace configuration calibrated to its specific privilege requirements. A service that needs no network access is placed in a fresh network namespace with no interfaces. One that needs no filesystem writes operates within a restrictive mount namespace.

Bubblewrap exposes namespace composition directly to application developers who need isolation without a full container runtime. Flatpak uses Bubblewrap as its sandboxing primitive, composing mount, network, PID, and user namespaces to constrain application access to host resources.

The pattern that emerges across these deployments is not "use all eight." It is: choose the namespace types that match the resources a sandboxed process needs to be denied, then layer syscall filtering on top. The composition is functional, not ceremonial.

What namespaces do not isolate — the kernel surface they all share

Venn diagram: Namespaces vs. Kernel Surface in Container Security. Compares Namespace Isolation and Kernel Attack Surface; overlap: Shared Concerns.

This is the point that the "foundation of container security" framing obscures, and it is the point I find myself returning to most often when reviewing sandbox designs.

Every namespaced process still issues syscalls to the same host kernel. A container's mount namespace may present a completely separate filesystem hierarchy, but when the container process calls open(), the kernel that handles that call is the same kernel handling calls from every other process on the host. A bug in the kernel's handling of any syscall is reachable regardless of namespace configuration. The Linux kernel exposes roughly 350 system calls; one exploitable bug in any of them can produce a container escape. Namespaces change what a process sees, not which kernel serves its requests.

This is not a design flaw. It is a design scope. Namespaces were built to isolate resource visibility. They were not designed to filter kernel access. Treating them as the latter creates a category error that shows up, repeatedly, in CVE retrospectives.

What this implies for sandbox design is specific. Namespaces are necessary but not sufficient. Syscall filtering via Seccomp-BPF restricts the kernel interface directly, reducing the attack surface regardless of namespace configuration. Mandatory access control systems, AppArmor and SELinux principally, operate independently of namespace boundaries. Capability bounding sets limit what a process can request even if it holds capabilities within a namespace.

There are also gaps that none of these tools fully address. Shared kernel data structures that do not sit behind any namespace boundary remain accessible. Hardware-level side channels, such as cache-timing attacks, are orthogonal to namespace isolation entirely. And bugs in the runtime that configures the namespace, the runc process, the container engine, the orchestrator, exist outside the namespace boundary by construction.

How user namespaces expand kernel attack surface for unprivileged processes

Diagram: User Namespaces: One Syscall, 3.4× More Kernel Surface. Visualizes: Visualize the dramatic expansion of reachable kernel operations that occurs when an unprivileged process calls unshare(CLONE_NEWUSER).

User namespaces are the type most responsible for making unprivileged containers practical, and they are also the type most responsible for expanding the kernel surface available to unprivileged processes. These two facts are not in tension so much as they are the same fact viewed from different angles.

The mechanism is direct. A call to unshare(CLONENEWUSER) requires no privileges. The kernel responds by granting the calling process capabilities, including CAPSYSADMIN and CAPNET_ADMIN, scoped to the new namespace. Those capabilities unlock access to kernel subsystems that were previously unreachable to an unprivileged process: netfilter, the traffic control subsystem (net/sched), various filesystem operations. Edera's 2026 analysis found that without user namespaces, an unprivileged process can reach 8 of the 40 kernel operations they catalogued; with user namespaces enabled, that same process reaches 27 of those 40. That is a 3.4× expansion of reachable kernel surface triggered by a single syscall requiring no privileges whatsoever.

A peer-reviewed analysis presented at ACISP 2025, examining 51 recent exploits, found that 32 of them, exploits that capabilities were theoretically positioned to stop, successfully escalated privileges by creating a new user namespace. The capability model assumes that capabilities are held by processes that legitimately need them; user namespaces allow any process to acquire them on demand, breaking that assumption structurally.

User namespaces are enabled by default on Ubuntu, Debian, Fedora, and most distributions used in container-hosting environments. The expanded surface is not an edge case; it is the default posture.

The tension the feature was not designed to resolve is worth stating plainly. User namespaces make unprivileged containers possible, which is a substantial benefit, particularly in multi-tenant environments where granting users root to spin up containers is not acceptable. Simultaneously, they make kernel subsystems reachable by any process that calls a single unprivileged syscall. No subsequent mitigation has fully resolved this structural duality.

Where namespace isolation fails at runtime — recent container escape CVEs

Two distinct failure modes show up in recent CVE history, and they are worth separating carefully because they require different responses.

The first is the runtime-layer failure, where the namespace itself works as designed but the software configuring it does not. CVE-2024-21626, disclosed in January 2024 and dubbed "Leaky Vessels," is the clearest example. A file descriptor leak in runc allowed container processes to access the host filesystem. The mount namespace was intact and functioning correctly. The escape path was a leaked file descriptor that runc failed to close before handing control to the container. Namespace enforcement never came into play because the break happened before the boundary was fully established. Wiz reported that 80% of cloud environments were vulnerable at the time of disclosure. The namespace configuration offered no defense. The fix required updating runc.

Three runc CVEs disclosed in November 2025 illustrate the same failure mode with different mechanics. CVE-2025-31133 involved an attacker replacing /dev/null with a symlink to a procfs file, causing runc to bind-mount the symlink's target read-write and enabling breakout or host crash. CVE-2025-52565 involved a symlink replacement of a /dev/pts device causing runc to bind-mount a sensitive procfs file over /dev/console. CVE-2025-52881 exploited procfs write redirects to produce arbitrary write primitives; a successful exploit could reach the node's kubelet and extend control to all other pods on that node. All three required runc updates, to versions 1.2.8+, 1.3.3+, or 1.4.0-rc.3+ depending on the branch. In each case, the namespace boundary was not breached; the runtime implementing it was.

The second failure mode is the kernel-layer failure, where namespace configuration is simply irrelevant because the exploit operates below any namespace boundary. CVE-2024-1086 is the current exemplar. A use-after-free bug in the Linux kernel's netfilter subsystem provided a path to privilege escalation from inside a container regardless of its namespace configuration. CISA confirmed active exploitation in ransomware campaigns in October 2025, associating the vulnerability with groups including RansomHub, Akira, and LockBit. Required kernel updates ranged to 5.15.149+, 6.1.76+, or 6.6.15+ depending on the kernel branch.

The pattern across all of these cases is consistent: the namespace boundary held, or was irrelevant, and the escape traveled through the layer that configures the namespace or through the kernel beneath it. This is precisely what the design scope of namespaces predicts.

Mitigating the user namespace attack surface and the limits of that mitigation

The primary control is kernel.unprivilegedusernsclone=0, a sysctl that disables unprivileged user namespace creation system-wide. It can be applied temporarily or made persistent via /etc/sysctl.d/. This was among the vendor-recommended mitigations for CVE-2024-1086, and for good reason: disabling unprivileged user namespace creation closes the entry point to the netfilter and net/sched subsystems that the vulnerability exploited. The logic is direct: if unprivileged processes cannot create user namespaces, they cannot acquire the capabilities that unlock those subsystems.

Ubuntu's response after Pwn2Own in April 2024 attempted a more surgical approach. Rather than a blanket disable, the approach permitted only specific allow-listed applications to create unprivileged user namespaces, blocking untrusted processes while preserving functionality for Chrome, rootless containers, and Flatpak. The intent was sound. DEVCORE's published analysis from June 2025 found the implementation contained bypassing issues; the restriction proved less durable than intended, with researchers identifying paths around the allow-list enforcement.

The structural problem any mitigation faces here is that the software requiring unprivileged user namespaces is not marginal. Chrome's renderer sandbox uses them. Rootless Docker and Podman require them. Flatpak requires them. A blanket disable breaks these applications for all users of a system. That is not a theoretical cost; it is an operational one that most environments cannot accept.

No current mitigation fully resolves the tension. Allow-listing is the most targeted approach available, but it adds its own implementation surface and, as DEVCORE's research suggests, that surface can be exploited.

Complementary controls operate independently of the user namespace question and should be treated as mandatory layers rather than fallbacks. Seccomp-BPF restricts syscall access directly, reducing the reachable kernel interface regardless of what namespace a process inhabits. AppArmor and SELinux provide mandatory access control through LSM hooks, enforcing policy that persists even when namespace configuration is incomplete or misconfigured. Capability bounding sets constrain what capabilities a namespaced process can hold or pass to children, limiting the blast radius of a capability escalation.

Building an effective sandbox: what namespace composition does and does not give you

After working through the syscalls, the type-by-type isolation boundaries, the real-world deployment patterns, the shared kernel problem, and the CVE record, the picture that emerges is more specific than either the optimistic framing ("namespaces are the foundation of container security") or the pessimistic one ("namespaces are security theater").

Namespace composition gives you resource visibility isolation. It gives you a controlled, explicit interface for defining what a sandboxed process can observe: which filesystem, which network stack, which PIDs, which IPC objects. This is valuable. It prevents a large class of lateral movement that would otherwise be trivial. A process that cannot see the host network stack cannot trivially probe host services. A process that cannot see host PIDs cannot easily enumerate running processes. These are real constraints on attacker freedom of movement.

What namespace composition does not give you is kernel attack surface reduction. Every sandboxed process still reaches the same kernel. A narrowly configured sandbox with only mount, PID, and network namespaces is not meaningfully more protected against a kernel exploit than one with all eight types combined. The namespace boundary sits above the kernel; kernel vulnerabilities sit below it.

The practical implication for sandbox design is layered and specific. Start with namespace composition calibrated to the process's actual resource requirements, not a default-all-eight configuration adopted without reflection. Layer Seccomp-BPF to restrict the syscall interface; this is the most direct mechanism for reducing kernel attack surface. Apply AppArmor or SELinux profiles to enforce mandatory access control that survives misconfiguration at higher layers. Keep the runtime that configures the namespaces, runc or its equivalent, patched aggressively; CVE-2024-21626 and the November 2025 runc CVEs demonstrate that the runtime is an attack surface in its own right, independent of the kernel. And wherever user namespaces are enabled, treat the expanded reachable kernel surface as a first-class threat model input, not a footnote.

The deeper observation, the one I keep arriving at, is that namespaces were not designed as a unified security system. They accumulated over roughly two decades, one resource isolation problem at a time, and the security properties they compose into were an emergent consequence rather than a stated design goal. That history matters because it explains why the gaps exist where they do. The shared kernel surface is not an oversight; it was never in scope. User namespace attack surface expansion was not anticipated; it was a consequence of a legitimate capability grant mechanism encountering an unprivileged entry point. Understanding the design intent behind each layer is what makes it possible to reason clearly about what each layer can and cannot defend.

Namespaces are, in the end, a well-designed tool for a specific problem. The risk is in reaching for them to solve problems they were not designed for, then being surprised when the scope of their protection turns out to match the scope of their design.

Sources

  1. baeldung.com
  2. blog.netbsd.org
  3. nixhacker.com
  4. linuxvox.com

More in Sandbox Isolation Primitives