Operating Systems · Isolation and Namespaces
Mount Namespaces and the Propagation of Mount Events
How each process gets its own mount table, why a mount made in one namespace can appear in another, and what the shared, slave, private and unbindable propagation flags actually control.
Two processes on the same machine can disagree about what /proc contains, and both can be right. That is the visible consequence of a mount namespace: the mapping from pathnames to mounted filesystems is per-namespace state, not global state. The surprising part is not that the tables can differ but that they can still be linked, so that a mount performed in one namespace silently appears in another at a position the second namespace never chose. Propagation is the mechanism that produces both the isolation and the leakage, and it is configured per mount, not per namespace.
What a namespace owns
The mount namespace holds a tree of struct mount objects. Each one pairs a mount point — a (mount, dentry) position in the parent mount — with a mounted filesystem, a set of flags, and a list of child mounts. Every process carries a pointer to its namespace through task_struct->nsproxy->mnt_ns, and clone with CLONE_NEWNS gives the child a copy of the parent’s tree. The copy is not a deep copy of the filesystems; it is a copy of the mount objects, which is why creating a namespace costs roughly the number of mounts in the source tree rather than the amount of data those mounts cover.
After the copy, the two trees are independent in the sense that a mount or unmount in one does not alter the other’s tree structure. They are not independent in the sense that they may be linked by propagation. A mount object carries a peer group identifier and a propagation type; two mounts with the same peer group are members of one group, and an event applied to any member is forwarded to the others.
Propagation is a property of a mount, not a switch
Four types are defined, and their names describe what happens to events arriving from a peer:
- Shared. The mount belongs to a peer group and both sends and receives events. A mount performed below it is replicated below every peer.
- Slave. The mount receives events from its master but does not send any upward. A mount below it stays local.
- Private. Neither sends nor receives. This is what a namespace’s root becomes after it is explicitly marked, and what containers set on the whole tree to obtain real isolation.
- Unbindable. Private, and additionally impossible to bind-mount, so the subtree cannot be grafted into another namespace even by a process with the capability.
A new mount namespace inherits its propagation types from the parent, and on a system whose init process has remounted the tree shared, as systemd does at startup, the initial namespace’s root is shared. That inheritance is the detail that makes containers leak. clone(CLONE_NEWNS) alone produces a namespace whose root mount remains a member of the parent’s peer group, so a mount performed on the host below that root is propagated into the container’s namespace. Container runtimes therefore follow the namespace creation with a recursive remount of / as MS_PRIVATE, using mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL). The MS_REC is essential; without it only the root mount changes type and every child mount retains its inherited type.
/* Make the new namespace's whole tree private before building anything. */
if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0)
die("remount private");
/* Now bind the container root and build the image tree under it. */
if (mount("/var/lib/images/rootfs", "/run/c", NULL, MS_BIND | MS_REC, NULL) < 0)
die("bind rootfs");
Event forwarding and the peer group walk
When a mount is attached, the kernel walks the propagation graph. For the mount’s parent, it consults the parent’s propagation type: if shared, the event is queued for every peer of the parent; if slave, it is queued for the master; if private, the walk stops. Each queued event names the source mount and the destination mount, and the whole walk runs synchronously inside the mount syscall under the namespace lock, so by the time the syscall returns to the caller the new mount exists at every peer where the event was forwarded. Propagation is not deferred; a process in a peer namespace that walks the tree after the syscall returns sees the replicated mount as already present.
The destination of a propagated mount is computed by matching path components relative to the peer mounts’ roots. Two mounts in the same peer group have the same root dentry and the same position relative to their parents — that is what makes them peers — so the relative position of the new mount is unambiguous. The two ways a mount joins an existing peer group are replication during the creation of a namespace and the creation of a bind mount from a member, which is how a bind mount can start forwarding events to namespaces that never mounted the source tree themselves.
Unmount propagation has an extra rule. A propagated unmount removes the corresponding mount from every peer, but a mount that was created locally in a peer namespace and never propagated upward has no counterpart, so it survives. The asymmetry is deliberate: a container may mount and unmount freely inside its own tree without the host noticing, provided the tree was made private first.
The lock that keeps the tree consistent
The mount tree is protected by a single global read-write lock, namespace_lock, taken for writing across a mount or unmount and for reading where a path walk needs to examine mount topology. Ordinary lookups do not take it; they rely on per-mount locks and on the mount’s own reference count, which is what keeps the common path cheap. The granularity of the global lock is coarse by design: mount operations are rare relative to lookups, and correctness of the tree under concurrent modification is hard to obtain with finer locking. The practical consequence is that a burst of mount operations serializes across every namespace on the machine, not just the one being modified, and that a filesystem whose mount method blocks — a network filesystem waiting on a server — holds the lock for the duration and stalls mount operations elsewhere.
Path walking takes a reference on each mount it passes through, using mntget, and releases it at the end. This is what allows a mount to be unmounted while a path through it is still being resolved: the unmount detaches the mount from the tree, but the walking process keeps its reference and finishes with the old tree. Lazy unmount, umount2 with MNT_DETACH, exploits this directly — it detaches the tree immediately and defers the actual teardown until the last reference drops, which for a busy mount can be indefinitely.
Where isolation still fails
A private mount namespace prevents new mounts from crossing into it, but it does not hide what was already there. Every namespace created from a parent starts with a full copy of the parent’s mounts, including any host filesystem the parent had mounted. Building a container image tree means unmounting or over-mounting those, and an over-mount is the usual approach because unmounting shared subtrees is restricted. The result is a tree whose visible contents come from the image, with the host mounts still present underneath and reachable if the over-mount is ever removed by a process with CAP_SYS_ADMIN in that namespace.
/proc/self/mountinfo and /proc/self/mountstats expose the tree to the namespace’s processes, including the peer group and propagation type of each mount in the optional fields. Reading them from inside a container is the direct way to check whether a runtime actually made the tree private, and a mount listed as shared:1 at the root indicates it did not.
The mount namespace also interacts with the root-directory mechanism described in Chroot Confines Path Resolution, Not the Process. chroot changes the process root within a namespace; the namespace changes what the tree contains. pivot_root, covered in Assembling a Container Root with pivot_root and Overlay Mounts, requires both: it swaps the namespace’s root mount and the process root together, and fails if the new root is not a mount point in the caller’s namespace.
A namespace outlives the processes in it as long as something holds a reference — an open file in /proc/<pid>/ns/mnt, a bind mount of that file, or a process in the namespace. This is how a container’s mount tree can be inspected after the container exits, and also how a leaked reference keeps a filesystem from being torn down long after the workload is gone.
References
- mount_namespaces(7) — Linux manual page — man7.org
- Shared Subtrees — Linux kernel filesystem documentation — Linux kernel documentation
- clone(2) — Linux manual page — man7.org