Mount Namespaces
A mount namespace is a kernel object that isolates the list of mounts seen by a set of processes — its own copy of the mount tree, its own answer to the question “what is mounted where.” Two processes in different mount namespaces can look at the same path
/dataand see two entirely different filesystems, or one see a mount and the other see an empty directory. It was the first namespace type added to Linux, appearing in 2002 in Linux 2.4.19 (Kerrisk, LWN 2016; the same date is given as theHISTORYof mount_namespaces(7)); itsclone(2)/unshare(2)flag is the terseCLONE_NEWNS(“new namespace”) rather than the more descriptiveCLONE_NEWMNTthat later namespace types’ flags resemble.
Uncertain
Verify: the common explanation that the flag is named
CLONE_NEWNS(notCLONE_NEWMNT) because mount namespaces were the only namespace type when the flag was added, so a generic “NS” name sufficed. Reason: this rationale is widely repeated but was not found in any primary source consulted — it is absent from the raw text of namespaces(7), clone(2), and the LWN mount-namespaces article, all of which confirm only that mount namespaces were first (2.4.19, 2002) without commenting on the flag name. To resolve: find a kernel commit message or maintainer statement tying theNEWNSname to the “only namespace at the time” history. uncertain
A new mount namespace does not start empty — it is born as a copy of the mount list of the namespace that created it, after which the two diverge independently (mount_namespaces(7)). This per-process mount tree is one of the three pillars containers stand on, alongside pivot_root and overlayfs.
This note is pinned to Linux 6.12, which is a maintained long-term-support (LTS) release (released 2024-11-17); every structure and function quoted below was read from the v6.12 tree during this write-up. Where a fact changed after 6.12 — notably the listmount(2) request struct — the later version is named explicitly. Manual-page quotations come from man-pages 6.18 as published on man7.org.
The mechanism for what propagates between namespaces — the shared, slave, private, and unbindable subtree taxonomy in full — is owned by Bind Mounts and Mount Propagation; this note draws the taxonomy from the namespace-crossing angle only, because propagation is exactly what determines whether the isolation you asked for is the isolation you got. Its own distinct contribution is the copy semantics: how a namespace is cloned, what the kernel silently changes during that clone, and how the resulting object lives and dies.
Mental Model — Isolated Topology With Controlled Leakage
Think of the mount tree (see The Mount Tree and vfsmount) as a graph of struct mount objects — each one is a filesystem instance grafted onto a directory of some other mount. A mount namespace (struct mnt_namespace in the kernel) is simply a container for one such tree: it holds a pointer to the tree’s root mount and the collection of every mount that belongs to it. Every process has exactly one mount namespace, reachable through task->nsproxy->mnt_ns. When you resolve a path, the kernel walks your namespace’s tree; a mount that is not in your namespace is, for path-resolution purposes, invisible — it does not exist.
The crucial subtlety is that namespaces are not independent universes by default. Linux’s shared-subtree machinery lets a mount be marked shared, so that when a new mount appears under it, that mount event propagates to peer copies in other namespaces. The kernel’s own documentation opens with exactly this motivation: “A process wants to clone its own namespace, but still wants to access the CD that got mounted recently. Shared subtree semantics provide the necessary mechanism to accomplish the above” (sharedsubtree.rst). The feature arrived in Linux 2.6.15, added because “after the implementation of mount namespaces was completed, experience showed that the isolation that they provided was, in some cases, too great” (mount_namespaces(7)).
So a mount namespace is isolated topology with controlled leakage: a private copy of the tree, plus optional propagation edges that selectively reconnect it to its origin.
flowchart TB subgraph initns["Initial mount namespace (host)"] R1["/ (root mount)<br/>shared:1"] C1["/cdrom<br/>shared:5"] R1 --> C1 end subgraph newns["New namespace (after unshare -m)"] R2["/ (copy)<br/>shared:1"] C2["/cdrom (copy)<br/>shared:5 — same peer group"] R2 --> C2 end initns -. "clone(CLONE_NEWNS)<br/>copy_mnt_ns -> copy_tree" .-> newns C1 == "mount /dev/sr0 /cdrom<br/>propagates via peer group 5" ==> C2
How a new mount namespace relates to its parent. What it shows: clone(CLONE_NEWNS) runs copy_mnt_ns, which deep-copies the parent’s mount tree into a fresh namespace; because / and /cdrom were shared (peer-group tags shared:1, shared:5), the copies join the same peer groups, so a later mount under /cdrom in the host propagates into the child. The insight to take: isolation and sharing coexist — the namespace gives each process its own tree, but shared-subtree peer groups are the seams along which mount events still cross, which is exactly why a careless container can leak host mounts (and why runtimes deliberately turn propagation off — see pivot_root and Changing the Root).
The Kernel Object: struct mnt_namespace
In Linux 6.12, a mount namespace is struct mnt_namespace, defined in fs/mount.h. Reproduced verbatim from that tag:
struct mnt_namespace {
struct ns_common ns; /* generic ns header: inode #, ops, refcount */
struct mount * root; /* the root mount of this namespace's tree */
struct rb_root mounts; /* Protected by namespace_sem */
struct user_namespace *user_ns; /* owning user namespace */
struct ucounts *ucounts; /* per-user-ns mnt-namespace budget */
u64 seq; /* Sequence number to prevent loops */
wait_queue_head_t poll; /* waiters on mount-table change */
u64 event; /* mount-table generation counter */
unsigned int nr_mounts; /* # of mounts in the namespace */
unsigned int pending_mounts;
struct rb_node mnt_ns_tree_node; /* node in the mnt_ns_tree */
refcount_t passive; /* references not pinning @mounts */
} __randomize_layout;Walking the load-bearing fields:
nsis the generic header every namespace type embeds. It carries the namespace’s inode number — the thing you actually see as the target of/proc/<pid>/ns/mnt— the operation tablemntns_operations, and the active reference count. Two identical inode numbers mean two processes are in the same namespace; that is the cheapest possible “are these in the same container?” test.rootpoints at thestruct mountthat is the top of this namespace’s tree. Path resolution for a task in this namespace ultimately bottoms out here.mountsis the collection of every mount in the namespace, held as a red-black tree keyed bymnt_id_unique. This is not how it always looked: at v6.7 and earlier the field was a plainstruct list_head list, and the rbtree arrived in v6.8 — verified by fetchingfs/mount.hat the v6.6, v6.7, v6.8, v6.10, v6.11 and v6.12 tags and comparing. The switch is what makeslistmount(2)able to resume a listing from an arbitrary mount ID in logarithmic time instead of re-walking a list.user_ns/ucountstie the namespace to an owning user namespace and to that namespace’s budget.alloc_mnt_ns()callsinc_mnt_namespaces()→inc_ucount(ns, current_euid(), UCOUNT_MNT_NAMESPACES)and returns-ENOSPCif the user is at their cap (fs/namespace.c). That cap is the writable file/proc/sys/user/max_mnt_namespaces, which “defines a per-user limit on the number of mount namespaces that may be created in the user namespace” (namespaces(7)). It is the reason a fork bomb ofunshare -mfails withENOSPCrather than exhausting kernel memory.seqis a globally monotonic 64-bit id. The comment abovemnt_ns_seqinfs/namespace.cstates the purpose outright, and it is more specific than the terse field comment suggests: “Assign a sequence number so we can detect when we attempt to bind mount a reference to an older mount namespace into the current mount namespace, preventing reference counting loops. A 64bit number incrementing at 10Ghz will take 12,427 years to wrap which is effectively never, so we can ignore the possibility.” Because a namespace can be pinned by a bind mount of/proc/<pid>/ns/mnt, and that bind mount lives inside some namespace, a naive design lets namespace A pin B while B pins A — an uncollectable cycle. Monotonic ids make “is this the older one?” a single comparison, and the kernel refuses the direction that would close the loop.nr_mounts/pending_mountsare the accounting behind the per-namespace mount cap (sysctl fs.mount-max), which stops a propagation storm from creating unbounded mounts.mnt_ns_tree_nodesplices the namespace into a global rbtree of namespaces,mnt_ns_tree, so a namespace can be looked up by id without walking every task. It landed in v6.11 (absent at v6.10, present at v6.11 and v6.12) together with themnt_ns_idfield in thelistmount/statmountrequest struct — the two are one feature.passiveis a second, weaker reference count: “number references not pinning @mounts”. A passive reference keeps themnt_namespaceallocation alive so it can be inspected (bystatmount/listmount) without keeping its mounts alive.
classDiagram class task_struct { +nsproxy } class nsproxy { +mnt_ns +uts_ns, ipc_ns, net_ns, pid_ns } class mnt_namespace { +ns_common ns +mount* root +rb_root mounts +u64 seq +uint nr_mounts +refcount passive } class user_namespace { +owns the privilege model } class ucounts { +UCOUNT_MNT_NAMESPACES budget } class mount { +mnt_parent +mnt_mountpoint (dentry) +mnt_id, mnt_id_unique +mnt_share (peer list) +mnt_slave_list } class vfsmount { +mnt_root (dentry) +mnt_sb (superblock) +mnt_flags } task_struct --> nsproxy nsproxy --> mnt_namespace mnt_namespace --> user_namespace : owned by mnt_namespace --> ucounts : budgeted by mnt_namespace "1" --> "*" mount : rb_root mounts mount --> vfsmount : embeds mount --> mount : mnt_parent / mnt_share / mnt_slave_list
The object graph a mount namespace sits in, as of v6.12 fs/mount.h. What it shows: a namespace is reached from a task only indirectly, through nsproxy — which is why setns(2) is a pointer swap and not a data copy — and it owns nothing but a root pointer, an rbtree of mounts, and two pieces of policy state (owning user namespace, ucounts budget). The insight to take: the namespace is a thin index; all the interesting structure lives in struct mount, and the peer/slave lists on mount cross namespace boundaries freely. Isolation is a property of which mounts are in your rbtree, not a wall around them.
Creating a Namespace: copy_mnt_ns, Line by Line
Every new mount namespace is created by one function: copy_mnt_ns(), called from create_new_namespaces() during clone(2)/fork(2)/unshare(2). Its v6.12 logic (fs/namespace.c) is the heart of this note. Abridged, with the error paths removed and step numbers added:
struct mnt_namespace *copy_mnt_ns(unsigned long flags, struct mnt_namespace *ns,
struct user_namespace *user_ns, struct fs_struct *new_fs)
{
...
if (likely(!(flags & CLONE_NEWNS))) { /* (1) common case: no new ns requested */
get_mnt_ns(ns); /* just bump refcount, share parent's ns */
return ns;
}
old = ns->root;
new_ns = alloc_mnt_ns(user_ns, false); /* (2) allocate a fresh, empty mnt_namespace */
namespace_lock();
/* First pass: copy the tree topology */
copy_flags = CL_COPY_UNBINDABLE | CL_EXPIRE; /* (3) tree-copy flags */
if (user_ns != ns->user_ns)
copy_flags |= CL_SHARED_TO_SLAVE; /* (4) less-privileged: demote shared */
new = copy_tree(old, old->mnt.mnt_root, copy_flags); /* (5) deep-copy the whole tree */
if (user_ns != ns->user_ns) {
lock_mount_hash();
lock_mnt_tree(new); /* (6) freeze flags, forbid unmount */
unlock_mount_hash();
}
new_ns->root = new;
p = old; q = new;
while (p) { /* (7) walk both trees in lockstep */
mnt_add_to_ns(new_ns, q); /* attach each copied mount to new ns */
new_ns->nr_mounts++;
if (new_fs) { /* (8) re-point this task's root and cwd */
if (&p->mnt == new_fs->root.mnt) new_fs->root.mnt = mntget(&q->mnt);
if (&p->mnt == new_fs->pwd.mnt) new_fs->pwd.mnt = mntget(&q->mnt);
}
p = next_mnt(p, old);
q = next_mnt(q, new);
...
}
mnt_ns_tree_add(new_ns); /* (9) register ns in the global rbtree */
namespace_unlock();
return new_ns;
}-
The fast path. If
CLONE_NEWNSis not in the flags, no new namespace is wanted: the function bumps the parent namespace’s refcount withget_mnt_nsand returns it. The child shares the parent’s mount namespace. This is the overwhelmingly common case —fork()withoutCLONE_NEWNSdoes not create a new mount view, and thelikely()annotation says the kernel expects exactly that. -
Allocate. Otherwise
alloc_mnt_nskzallocs a freshmnt_namespacewithGFP_KERNEL_ACCOUNT— meaning the allocation is itself charged to the creator’s memory cgroup — assigns a newseqfrommnt_ns_seq, allocates an inode number vians_alloc_inumfor/proc/<pid>/ns/mnt, setsmounts = RB_ROOT, and takes theucountsbudget slot described above. -
Copy flags.
CL_COPY_UNBINDABLEsays: when deep-copying, also copy mounts markedunbindable. This is a deliberate asymmetry — a recursive bind prunes unbindable subtrees (that is the whole point ofMS_UNBINDABLE), but a full namespace clone must replicate them, or the child would silently lose mounts.CL_EXPIREpreserves expiry marks so auto-expiring mounts (NFS automounts) keep behaving. -
The privilege demotion — the key line. If the new namespace is owned by a different user namespace than the source (
user_ns != ns->user_ns), the kernel addsCL_SHARED_TO_SLAVE. This is the code-level realization of the man page’s rule that, in a less privileged mount namespace, “shared mounts are reduced to slave mounts. This ensures that mappings performed in less privileged mount namespaces will not propagate to more privileged mount namespaces” (mount_namespaces(7)). A shared mount in the parent would otherwise let the child push mount events back to the host; demoting it to slave means the child still receives the host’s propagated mounts but can never propagate out. This is precisely what makes unprivileged user-namespace containers safe to hand a copy of the host’s shared mounts. -
copy_tree. The deep copy: it allocates a newstruct mountfor every mount in the source subtree, cloning thevfsmountflags and re-grafting children, and — for shared mounts — joins the copies into the same peer groups as the originals (subject to the slave demotion above). That peer-group membership is exactly the propagation edge in the mental-model diagram. Note what is not copied: the superblock. Both trees point at the samestruct super_block, so writing a file through the container’s copy of/etcwrites the host’s/etc. A mount namespace virtualizes topology, never content. -
Locking the tree — the other half of the demotion. Also gated on the user-namespace mismatch, and easy to miss:
lock_mnt_tree(new)walks every copied mount and turns its current flags into locks. Verbatim from v6.12:for (p = mnt; p; p = next_mnt(p, mnt)) { int flags = p->mnt.mnt_flags; /* Don't allow unprivileged users to change mount flags */ flags |= MNT_LOCK_ATIME; if (flags & MNT_READONLY) flags |= MNT_LOCK_READONLY; if (flags & MNT_NODEV) flags |= MNT_LOCK_NODEV; if (flags & MNT_NOSUID) flags |= MNT_LOCK_NOSUID; if (flags & MNT_NOEXEC) flags |= MNT_LOCK_NOEXEC; /* Don't allow unprivileged users to reveal what is under a mount */ if (list_empty(&p->mnt_expire)) flags |= MNT_LOCKED; p->mnt.mnt_flags = flags; }Those two comments are the entire security argument for unprivileged containers’ mount handling, and the section below unpacks them.
-
The lockstep walk. With the topology copied, the function walks the old tree (
p) and the new tree (q) together vianext_mnt, attaching each new mount to the new namespace withmnt_add_to_ns(which inserts it intons->mountskeyed bymnt_id_uniqueand sets theMNT_ONRBflag) and counting it innr_mounts. -
Re-rooting the task. If a
fs_structwas passed (the task’s root/cwd holder), the loop swaps the task’sroot.mntandpwd.mntfrom the old mounts to their freshly-copied counterparts withmntget, so the new process’s/and current directory point into its tree, not the parent’s. This is whyclone(2)rejectsCLONE_NEWNS | CLONE_FS: the function must own thefs_structit is rewriting. -
Register. Finally
mnt_ns_tree_addinserts the namespace into the globalmnt_ns_treerbtree keyed by itsseq, solistmount(2)/statmount(2)can find it by id.
The takeaway: a new mount namespace is structurally identical to its parent at the instant of creation — same mounts at same paths, same superblocks — and only diverges as each side mounts and unmounts independently. The two asymmetries baked in at birth both fire only when the user namespace changes: the shared→slave demotion, and the flag locking.
flowchart TD START["clone / fork / unshare<br/>calls copy_mnt_ns()"] Q1{"CLONE_NEWNS<br/>in flags?"} FAST["get_mnt_ns(ns)<br/>child shares parent's namespace"] ALLOC["alloc_mnt_ns()<br/>ucounts budget, seq, ns inode<br/>ENOSPC if over max_mnt_namespaces"] Q2{"user_ns != ns->user_ns ?<br/>(crossing into a less<br/>privileged namespace)"} FLAGS1["copy_flags =<br/>CL_COPY_UNBINDABLE | CL_EXPIRE"] FLAGS2["+ CL_SHARED_TO_SLAVE"] COPY["copy_tree()<br/>new struct mount per mount<br/>same superblocks<br/>joins peer groups"] LOCK["lock_mnt_tree()<br/>MNT_LOCK_ATIME / _READONLY / _NODEV<br/>MNT_LOCK_NOSUID / _NOEXEC / MNT_LOCKED"] WALK["lockstep walk:<br/>mnt_add_to_ns(), nr_mounts++,<br/>re-point fs->root and fs->pwd"] REG["mnt_ns_tree_add()<br/>discoverable by listmount/statmount"] START --> Q1 Q1 -- no --> FAST Q1 -- yes --> ALLOC --> Q2 Q2 -- no --> FLAGS1 --> COPY Q2 -- yes --> FLAGS2 --> COPY --> LOCKQ{"same user_ns?"} LOCKQ -- no --> LOCK --> WALK LOCKQ -- yes --> WALK WALK --> REG
The copy_mnt_ns decision path in v6.12. What it shows: there is exactly one branch in the whole function that changes behaviour, and it is asked twice — “did we cross a user-namespace boundary?” If no, the child gets a faithful clone; if yes, it gets a demoted and frozen clone. The insight to take: unshare -m and unshare -Urm produce structurally different namespaces even though both “just” copy the tree. Every surprise about rootless containers not being able to unmount things traces back to the two boxes on the right-hand branch.
Propagation, From the Namespace’s Point of View
The single most common way to be wrong about mount namespaces is to assume the copy is disconnected. It is not: peer-group membership survives the copy, and on a modern systemd system nearly everything is in a peer group. The four propagation types decide, per mount, which direction events cross.
| Type | Flag | Sends events out? | Receives events in? | mountinfo tag | Namespace-crossing meaning |
|---|---|---|---|---|---|
| shared | MS_SHARED | yes, to all peers | yes, from all peers | shared:X | Fully two-way. A mount inside the container appears on the host; a host unmount tears down the container’s copy. |
| slave | MS_SLAVE | no | yes, from master group | master:X | One-way in. The container sees host mounts appear, but cannot push anything out. The safe default for containers. |
| private | MS_PRIVATE | no | no | (none) | Fully cut. Nothing crosses in either direction. What unshare(1) gives you by default. |
| unbindable | MS_UNBINDABLE | no | no | unbindable | Private, and refuses to be bind-mounted or replicated by a recursive bind — the cure for the “mount explosion” problem. |
A mount may be both slave and shared at once (master:X shared:Y): it receives from peer group X and shares with peer group Y. mountinfo prints both tags, and that combination is how systemd builds one-way propagation chains between a unit and the host.
flowchart LR subgraph HOST["Host namespace"] HS["/data — shared:7"] end subgraph SHARED["Container A — copy is shared:7"] CS["/data"] end subgraph SLAVE["Container B — copy is master:7"] CL["/data"] end subgraph PRIV["Container C — copy is private"] CP["/data"] end HS == "host mounts /data/vol<br/>propagates" ==> CS HS == "host mounts /data/vol<br/>propagates" ==> CL HS -. "nothing crosses" .-> CP CS == "container mounts /data/tmp<br/>propagates BACK to host" ==> HS CL -. "container mount stays inside" .-> HS
The same host mount, copied into three containers with three propagation types. What it shows: the direction of the arrows is the entire semantic difference. Shared is bidirectional and therefore dangerous; slave is receive-only; private is deaf. The insight to take: a container runtime’s choice here is a security boundary, not a tuning knob. Leaving the copy shared means a process inside the container can mount something the host will then see — and can umount something the host then loses.
Why naive unshare -m surprises people
Two facts collide. First, the kernel’s rule for a brand-new mount is: “if the mount has a parent … and the propagation type of the parent is MS_SHARED, then the propagation type of the new mount is also MS_SHARED. Otherwise … MS_PRIVATE.” Second — and this is the part everybody forgets — “systemd(1) automatically remounts all mounts as MS_SHARED on system startup. Thus, on most modern systems, the default propagation type is in practice MS_SHARED” (mount_namespaces(7)).
So on any systemd host, the tree you clone is shared top to bottom, and a raw unshare(CLONE_NEWNS) gives you a namespace whose every mount is still wired to the host. The unshare(1) command papers over this: “since util-linux version 2.27 [it] automatically sets propagation to private in a new mount namespace to make sure that the new namespace is really unshared,” equivalent to running mount --make-rprivate /, and --propagation unchanged turns that off (unshare(1)). The unshare(2) syscall does no such thing.
This is the single most consequential asymmetry between the tool and the system call, and it is the reason a C program that calls unshare(CLONE_NEWNS) behaves differently from the shell command that appears to do the same thing.
Note the two man pages appear to disagree and do not:
unshare(1)says “private is the kernel default”, which is true of a mount created under a non-shared parent;mount_namespaces(7)says shared is the practical default, which is true because systemd made every parent shared. Both statements are correct about different things.
flowchart TD Q0["I am creating a mount namespace.<br/>What propagation should the copy have?"] Q1{"Do I need host mounts<br/>(new disks, volumes)<br/>to appear inside?"} Q2{"Do I want mounts made inside<br/>to appear on the host?"} Q3{"Will this subtree be<br/>recursively bind-mounted<br/>somewhere under itself?"} PRIV["MS_REC | MS_PRIVATE<br/>total isolation<br/>(unshare -m default; runc rootfs)"] SLAVE["MS_REC | MS_SLAVE<br/>receive-only<br/>(systemd PrivateMounts=;<br/>k8s HostToContainer)"] SHARED["MS_SHARED<br/>two-way — audit this<br/>(k8s Bidirectional;<br/>CSI node plugins only)"] UNBIND["MS_UNBINDABLE<br/>private + unreplicable<br/>(stops mount explosion)"] Q0 --> Q1 Q1 -- no --> Q3 Q3 -- yes --> UNBIND Q3 -- no --> PRIV Q1 -- yes --> Q2 Q2 -- no --> SLAVE Q2 -- yes --> SHARED
Choosing a propagation type, as a decision tree. What it shows: only two questions matter — which direction do events need to flow — plus one special case for the replication blow-up. The insight to take: shared is the only answer that hands privilege outward, so it should be the answer you have to justify. Kubernetes agrees: Bidirectional volume propagation requires a privileged container.
The four types’ full semantics — the transition table for mount --make-*, the bind and move semantics tables, and the propagate_from:X tag — belong to Bind Mounts and Mount Propagation; go there for the taxonomy, stay here for what it means at a namespace boundary.
Less-Privileged Namespaces and Locked Mounts
unshare(CLONE_NEWNS) requires CAP_SYS_ADMIN. Rootless containers get it by creating a user namespace first (unshare -Urm), which grants a full capability set over that new user namespace. The kernel then treats the resulting mount namespace as less privileged, and applies three restrictions from mount_namespaces(7) — all three implemented by the lock_mnt_tree code quoted above.
| Restriction | Kernel mechanism (v6.12) | Concrete consequence |
|---|---|---|
| Shared mounts demoted to slave | CL_SHARED_TO_SLAVE in copy_mnt_ns | The container cannot push a mount out to the host. |
| Mounts that arrived as a unit are locked together and cannot be individually unmounted | MNT_LOCKED set on every copied mount whose mnt_expire list is empty | umount(2) returns EINVAL — the kernel’s “this mount is locked” error. |
MS_RDONLY, MS_NOSUID, MS_NOEXEC and the atime flags become locked | MNT_LOCK_READONLY, MNT_LOCK_NOSUID, MNT_LOCK_NOEXEC, MNT_LOCK_ATIME | mount -o remount,rw fails with EPERM. |
The man page’s worked example is the clearest statement of why. In a privileged namespace, an administrator hides the shadow password file:
# mount --bind /dev/null /etc/shadow
# cat /etc/shadow # produces no outputIf a less-privileged namespace could umount /etc/shadow, it would reveal the file. So it cannot:
# unshare --user --map-root-user --mount strace -o /tmp/log umount /etc/shadow
umount: /etc/shadow: not mounted.
# grep '^umount' /tmp/log
umount2("/etc/shadow", 0) = -1 EINVAL (Invalid argument)The mount(8) error message is misleading; the strace(1) output shows the real result. Similarly, a read-only bind cannot be made writable inside the child namespace — mount -o remount,rw returns “permission denied” — because MNT_LOCK_READONLY is set.
Two escapes are deliberately allowed, and knowing them prevents a lot of wasted debugging. First, you can stack a new mount on top of a locked one: mount --bind /tmp/a /etc/shadow inside the child works, because it hides rather than reveals. Second, you can umount an entire propagated subtree at once even though you cannot unmount a part of it — umount -l /mnt/ppp succeeds where umount /mnt/ppp/y fails, because removing the whole unit reveals nothing that was not already reachable.
ID-mapped mounts — the other half of rootless
Locking solves “don’t let the container reveal things.” The complementary problem for rootless containers is ownership: a container running as UID 100000 on the host wants files owned by host UID 100000 to look like they are owned by UID 0 inside. Historically this needed chown -R over the whole tree, or a FUSE shim.
ID-mapped mounts, added with mount_setattr(2) in Linux 5.12, solve it in the VFS. Setting MOUNT_ATTR_IDMAP with a userns_fd attaches an ID mapping to a mount, so that “whenever callers interact with the filesystem through an ID-mapped mount, the ID mapping of the mount will be applied to user and group IDs associated with filesystem objects” — including the security.capability xattr in VFS_CAP_REVISION_3 format and system.posix_acl_access/_default entries (mount_setattr(2)). The change is localized (visible only through that mount) and temporary (tied to the mount’s lifetime).
Three constraints bind it to the modern mount API and therefore to this note’s neighbours:
- The mount must be detached — “created by calling
open_tree(2)with theOPEN_TREE_CLONEflag and it must not already have been visible in a mount namespace.” You build it, map it, and only thenmove_mount(2)it into the tree. See The New Mount API. - The mapping cannot be changed afterwards;
MOUNT_ATTR_IDMAPinattr_clrisEINVAL. - The filesystem must support it, and support arrived staggered: xfs, ext4 and FAT in 5.12; btrfs and ntfs3 in 5.15; f2fs in 5.18; erofs and overlayfs (lower and upper layers) in 5.19; squashfs in 6.2; tmpfs in 6.3; cephfs in 6.7; hugetlbfs in 6.9 (mount_setattr(2), man-pages 6.18).
EINVALon an ID-mapped mount attempt usually means “this filesystem, on this kernel, is not on that list.”
Lifetime, Persistence, and setns
A mount namespace is not owned by a process; it is referenced by things. It stays alive while any of the following hold:
- a task’s
nsproxypoints at it; - an open file descriptor refers to
/proc/<pid>/ns/mnt; - a bind mount of that magic symlink exists somewhere (
unshare --mount=/root/namespaces/mntdoes exactly this).
The last one is how a namespace outlives every process in it. unshare(1) documents the sharp edge: the file you bind onto “must be located on a mount whose propagation type is not shared (or an error results)” — otherwise pinning the namespace would propagate the pin.
stateDiagram-v2 [*] --> Allocated: alloc_mnt_ns()<br/>ucounts slot, seq, ns inode Allocated --> Populated: copy_tree() + mnt_add_to_ns()<br/>nr_mounts set Populated --> Registered: mnt_ns_tree_add()<br/>visible to listmount by id Registered --> Pinned: extra refs<br/>open fd on /proc/PID/ns/mnt<br/>or bind mount of it Pinned --> Registered: last extra ref dropped Registered --> Draining: last task exits<br/>ns.count -> 0 Draining --> Freed: umount_tree() on all mounts,<br/>ns_free_inum(), dec_mnt_namespaces(),<br/>mnt_ns_tree_remove() Freed --> [*] Registered --> Joined: setns(fd, CLONE_NEWNS)<br/>via mntns_install() Joined --> Registered note right of Pinned A pinned namespace with zero processes still exists and still holds its mounts - this is how "unshare --mount=FILE" works end note note right of Draining Anonymous namespaces (is_anon_ns) never get an inode, never enter mnt_ns_tree, and cannot be joined with setns - they back detached trees from open_tree(OPEN_TREE_CLONE) end note
The life of a struct mnt_namespace. What it shows: creation is three distinct steps (allocate, populate, register), and destruction only begins when every reference class is gone — tasks, fds, and bind mounts alike. The insight to take: “the container exited but the mount is still there” is almost always a pinned namespace: some process still holds an fd on /proc/<pid>/ns/mnt, or a bind mount of it survives. Look for the reference, not for the process.
setns(2) lets a process join an existing mount namespace given such an fd — nsenter --mount=/proc/<pid>/ns/mnt is the userspace front-end, and it is how docker exec and kubectl exec drop a new process into a running container’s mount view without re-creating it. The kernel side is mntns_install(), and reading it explains three otherwise-cryptic failures (fs/namespace.c):
if (!ns_capable(mnt_ns->user_ns, CAP_SYS_ADMIN) ||
!ns_capable(user_ns, CAP_SYS_CHROOT) ||
!ns_capable(user_ns, CAP_SYS_ADMIN))
return -EPERM;
if (is_anon_ns(mnt_ns))
return -EINVAL;
if (fs->users != 1)
return -EINVAL;EPERMneeds three capability checks, not one:CAP_SYS_ADMINin the target namespace’s owning user namespace, plusCAP_SYS_CHROOTandCAP_SYS_ADMINin the caller’s. Joining a container’s mount view is as privileged as changing root, because it effectively is.EINVALon an anonymous namespace. Detached trees produced byopen_tree(OPEN_TREE_CLONE)live in an anonymous namespace with no inode number; there is nothing to join.EINVALwhenfs->users != 1. The task’sfs_structmust be unshared, because installing the namespace rewrites root and cwd (set_fs_root,set_fs_pwd). A multithreaded process sharing onefs_structcannot have one thread jump namespaces — the same constraint that makesCLONE_NEWNS | CLONE_FSillegal, seen from the other end.
Reading the Boundary: /proc/<pid>/mountinfo
/proc/<pid>/mounts, /proc/<pid>/mountinfo, and /proc/<pid>/mountstats each show the mount list of the namespace that process belongs to (mount_namespaces(7)). mountinfo is the richest; here is one real line:
72 1 259:2 / / rw,relatime shared:1 - ext4 /dev/nvme0n1p2 rw,seclabel
| Position | Value here | Field | What to read from it |
|---|---|---|---|
| 1 | 72 | mount ID | Unique id for this mount (mnt_id); recycled after umount, so never use it as a stable key across time. |
| 2 | 1 | parent mount ID | The mount this one is grafted onto. Fields 1 and 2 together reconstruct the whole tree. |
| 3 | 259:2 | st_dev major:minor | The backing device. Two mounts with the same value share a superblock. |
| 4 | / | root | Which subtree of the filesystem is mounted here. For a bind mount this is the source subdirectory, not / — the single best way to spot a bind. |
| 5 | / | mount point | Path relative to the reading process’s root, so it differs inside a chroot or after pivot_root. |
| 6 | rw,relatime | per-mount options | Per-vfsmount flags, not per-superblock. |
| 7… | shared:1 | optional fields | The propagation tags: shared:X, master:X, propagate_from:X, unbindable. Zero or more of them. |
| — | - | separator | Marks the end of the variable-length optional fields. |
| n+1 | ext4 | filesystem type | |
| n+2 | /dev/nvme0n1p2 | mount source | |
| n+3 | rw,seclabel | per-superblock options | Shared by every mount of this superblock. |
Walking a mountinfo record, per proc_pid_mountinfo(5). What it shows: the record is fixed-prefix, variable-middle, fixed-suffix — which is why every correct parser splits on the - separator rather than counting fields from the left. The insight to take: field 7 is the namespace-relevant one. shared:N means this mount will cross a namespace boundary; no tag at all means it will not. That single column tells you whether your isolation is real.
To compare a container’s view to the host’s, cat /proc/<container-pid>/mountinfo from the host and diff it against your own — divergent mount IDs, missing lines, and changed propagation tags are exactly the namespace boundary made visible.
The modern alternative: statmount(2) and listmount(2)
Parsing mountinfo has two structural problems: it is a text blob that must be re-read in full to observe one change, and its mount ID field is recycled. The statmount(2)/listmount(2) pair replaces it with a binary, resumable interface — listmount “returns a list of mount IDs under the req.mnt_id… meant to be used in conjunction with statmount(2) in order to provide a way to iterate and discover mounted file systems,” starting from the pseudo-id LSMT_ROOT for the caller’s namespace root (listmount(2)). It requires CAP_SYS_ADMIN in the user namespace.
For this note the interesting part is that the request struct can name a different mount namespace, and that field has moved:
| Kernel | struct mnt_id_req shape | Foreign-namespace query |
|---|---|---|
| ≤ v6.10 | (struct absent) | — |
| v6.11 | size, spare, mnt_id, param, mnt_ns_id | By namespace id (the seq value), looked up in the global mnt_ns_tree. |
| v6.12 | same shape | Same. In v6.12 grab_requested_mnt_ns() also interprets the spare field as a namespace file descriptor — CLASS(fd, f)(kreq->spare), requiring proc_ns_file() and ops->type == CLONE_NEWNS. |
| v6.18 | size, mnt_ns_fd, mnt_id, param, mnt_ns_id | The spare field is renamed to mnt_ns_fd, making the fd path an official part of the UAPI. |
Uncertain
Verify: whether the
spare-as-fd behaviour visible in v6.12grab_requested_mnt_ns()was intended as public UAPI at that release or was an in-flight implementation later formalized by the v6.18 rename. Reason: I read bothinclude/uapi/linux/mount.handfs/namespace.cat the v6.12 tag and they disagree in naming — the header calls the fieldspare, the implementation treats it as a namespace fd — and I could not reach the patch thread that would settle intent (lore.kernel.orgis behind an Anubis proof-of-work challenge and returns a JavaScript interstitial tocurl;lwn.netreturned HTTP 429 throughout this session). To resolve: read the commit that renamedsparetomnt_ns_fdand its cover letter onlore.kernel.orgfrom a browser. uncertain
Either way, the earlier claim that foreign-namespace querying is new in 6.18 is wrong in substance: id-based cross-namespace statmount/listmount has existed since v6.11, guarded by an explicit permission check in SYSCALL_DEFINE4(listmount, …) — if the request names a namespace id other than your own and you lack CAP_SYS_ADMIN in that namespace’s user namespace, the call returns -ENOENT rather than -EPERM, deliberately refusing to confirm the namespace exists.
Worked Examples
unshare from the shell
$ sudo unshare --mount --propagation private bash
# mount -t tmpfs none /mnt # (A) mount a tmpfs only this namespace sees
# grep /mnt /proc/self/mountinfo
142 96 0:54 / /mnt rw,relatime - tmpfs none rw,...
# # in another terminal on the host:
$ grep /mnt /proc/self/mountinfo # (B) host sees nothing
$ echo $?
1--mount is unshare(2) with CLONE_NEWNS; --propagation private runs the equivalent of mount --make-rprivate / inside the new namespace immediately after creation, so subsequent mounts neither propagate to nor from the host (A vs B). Note in line (A) that the record has no optional field between the options and the - — that absence is what “private” looks like on the wire.
CLONE_NEWNS requires CAP_SYS_ADMIN and, per clone(2), cannot be combined with CLONE_FS: a new mount namespace forces a private fs_struct (its own root and cwd) (clone(2), unshare(2)).
Creating one in C
#define _GNU_SOURCE
#include <sched.h>
#include <sys/mount.h>
#include <unistd.h>
int main(void) {
if (unshare(CLONE_NEWNS) == -1) /* (1) detach into a private mount namespace */
return 1;
/* keep our mounts from leaking back to the parent */
mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL); /* (2) recursively make private */
mount("none", "/mnt", "tmpfs", 0, NULL); /* (3) invisible to the parent */
execlp("bash", "bash", NULL); /* (4) explore the new view */
return 1;
}Line 1 creates the namespace (the process keeps a copy of the parent’s mount list, still wired into the host’s peer groups on a systemd machine). Line 2 is the idiom every container runtime performs, and the one unshare(2) will not do for you: MS_REC | MS_PRIVATE on / strips shared-subtree peer-group membership from the whole copied tree, severing the propagation edges. The man page suggests MS_SLAVE | MS_REC instead where you still want to receive host mounts:
mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL); /* receive-only */Line 3 mounts a tmpfs that, thanks to line 2, only this namespace and its descendants will ever see. Line 4 hands control to a shell to inspect it.
The full container sequence
Creating the namespace is step one of four. The sequence below is what every OCI runtime performs; the re-rooting half belongs to pivot_root and Changing the Root and is drawn here only so the boundary between the two notes is legible.
sequenceDiagram autonumber participant P as Runtime (parent) participant K as Kernel participant C as Container process P->>K: clone(CLONE_NEWNS | CLONE_NEWUSER | ...) K->>K: copy_mnt_ns(): alloc_mnt_ns + copy_tree K->>K: user_ns differs -> CL_SHARED_TO_SLAVE K->>K: lock_mnt_tree(): MNT_LOCKED, MNT_LOCK_RDONLY/NOSUID/NOEXEC/NODEV/ATIME K-->>C: child running, tree identical to host's C->>K: mount(NULL, "/", NULL, MS_REC|MS_SLAVE, NULL) Note over C,K: cut outbound propagation<br/>(the step unshare(2) does NOT do) C->>K: mount overlay on /rootfs (lower/upper/work) C->>K: bind-mount volumes, /proc, /sys, /dev into /rootfs C->>K: pivot_root("/rootfs", "/rootfs/old") K->>K: swap fs->root; old root reparented under put_old C->>K: umount2("/old", MNT_DETACH) Note over C,K: host tree now unreachable<br/>from inside the container C->>K: chdir("/"); execve(entrypoint)
What unshare(CLONE_NEWNS) plus re-rooting actually does, step by step. What it shows: the namespace clone (steps 1–5) gives you a copy, not isolation; isolation is manufactured by steps 6 through 11. The insight to take: steps 6 and 11 are the security-critical ones. Skip step 6 and the container’s mounts leak to the host; skip step 11 (or use chroot instead of pivot_root) and the old root stays reachable in the mount tree, which is the classic container escape.
MNT_DETACH, Lazy Unmount, and the Propagation Trap
umount2(target, MNT_DETACH) performs a lazy unmount: it makes “the mount unavailable for new accesses, immediately disconnect[s] the filesystem and all filesystems mounted below it from each other and from the mount table, and actually perform[s] the unmount when the mount ceases to be busy” (umount(2), available since Linux 2.4.11). In v6.12’s do_umount() this is a single branch: with MNT_DETACH the code calls umount_tree(mnt, UMOUNT_PROPAGATE) unconditionally, whereas the normal path first calls shrink_submounts() and refuses with -EBUSY if propagate_mount_busy(mnt, 2) says anyone still holds it.
MNT_DETACH is why a container teardown can succeed while processes still have files open inside it, and it is what pivot_root-based runtimes use to discard the old root. It is also a loaded gun on a systemd host, because of a footgun umount(2) spells out explicitly:
“Shared mounts cause any mount activity on a mount, including
umount()operations, to be forwarded to every shared mount in the peer group and every slave mount of that peer group… recursively bind mounting the root directory of the filesystem onto a subdirectory and then later unmounting that subdirectory withMNT_DETACHwill cause every mount in the mount namespace to be lazily unmounted.”
flowchart TD A["umount2(target, MNT_DETACH)"] B{"Is target's parent<br/>mount shared?"} C["Detach only this subtree.<br/>Freed when last user closes."] D["umount_tree(UMOUNT_PROPAGATE)<br/>walks the peer group"] E["Every peer's copy at that<br/>mountpoint is unmounted too"] F["Every slave of those peers<br/>is unmounted too"] G["On a systemd host where / is shared,<br/>an rbind of / onto a subdir means<br/>THE WHOLE NAMESPACE unmounts"] H["Prevention: mount(target, MS_REC|MS_PRIVATE)<br/>before umount"] A --> B B -- no --> C B -- yes --> D --> E --> F --> G G -.-> H C -.-> H
How a lazy unmount turns into a system-wide one. What it shows: MNT_DETACH says nothing about scope; scope is decided entirely by the propagation type of the parent mount, and propagation walks peers and then slaves. The insight to take: the fix named by the man page is to MS_REC | MS_PRIVATE the subtree immediately before unmounting it. Runtimes and backup tools that rbind / somewhere and clean up later must do this, or a cleanup path becomes an outage.
Failure Modes and Common Misunderstandings
- “A new namespace is empty.” No — it is a full copy of the creator’s mount list. A freshly
unshared namespace can still see/,/proc,/home, everything; isolation comes from what you change afterward (re-root with pivot_root, unmount, or overmount), not from starting blank. - “A new namespace is isolated.” Also no, on any systemd host: the copy inherits
shared:Ntags and stays in the host’s peer groups until you change them. Isolation is an action, not a state you are given. - Mounts mysteriously leaking to the host. If you
unshare -mwithout making/private or slave, mounts you create propagate back, and unmounts you do tear down the host’s mounts. The symptom is a host mount vanishing when a container exits, or a stray/var/lib/docker/...mount appearing on the node. Diagnose withfindmnt -o TARGET,PROPAGATIONon both sides. unshare(CLONE_NEWNS)fails withEPERM.CAP_SYS_ADMINis required. Inside a user namespace you can gain it over that user namespace (unshare -Urm) — but then the shared→slave demotion and thelock_mnt_treeflag freezing both apply.umountinside a rootless container fails withEINVAL, andmount(8)says “not mounted”. That isMNT_LOCKED, not a missing mount.straceit; the syscall returnsEINVAL. Workaround: overmount rather than unmount, or unmount the whole propagated unit lazily.mount -o remount,rwfails withEPERMin a rootless container.MNT_LOCK_READONLYwas set at namespace-creation time. There is no way to relax it from inside; the mount must be created writable in the more-privileged namespace.EINVALcombiningCLONE_NEWNSwithCLONE_FS. Disallowed by design — a new mount namespace must own a privatefs_struct(step 8 ofcopy_mnt_nsrewrites it).setnsfails withEINVALfrom a threaded program.mntns_install()requiresfs->users == 1. Do thesetnsbefore creating threads, or in a fresh single-threaded child. (Go programs hit this constantly; this is whyrunchas a C constructor that runs before the Go runtime spawns threads.)- Expecting
/proc/mountsto reflect another namespace./proc/self/mountsshows your namespace. To see another process’s view you must read its/proc/<pid>/mountinfo, or usensenter/setns, or usestatmount/listmountwith a namespace id. A monitoring tool that reads only/proc/self/mountsis blind to every container on the host — a classic observability bug. - Treating the
mount IDas stable. Field 1 ofmountinfois recycled after unmount. Usemnt_id_unique(viastatx(STATX_MNT_ID_UNIQUE)orstatmount) when you need an identity that is not reused. - Assuming a namespace dies with its last process. It does not if anything holds a reference — an open fd on
/proc/<pid>/ns/mnt, or a bind mount of it.
Alternatives and Boundaries
A mount namespace isolates the mount tree. It is one of eight namespace types (mount, PID, network, IPC, UTS, user, cgroup, time); the others isolate process IDs, the network stack, and so on (namespaces(7)). Mount namespaces compose with user namespaces (which own the privilege model and enable rootless containers) and with pivot_root (which replaces the root rather than merely copying the tree).
| Mechanism | What it changes | Escapable? | Cost | Use when |
|---|---|---|---|---|
chroot(2) | Apparent root directory only; the mount tree is unchanged and still fully present | Yes, by a process with CAP_SYS_CHROOT | Nearly free | Build sandboxes and rescue shells where the threat model is “mistakes”, not “attackers” |
| Mount namespace | The whole set of mounts visible | No — an invisible mount cannot be reached | One struct mount per mount, per namespace | You need a genuinely different filesystem view |
Mount ns + pivot_root | Visible mounts and the root, with the old root discardable | No, once the old root is MNT_DETACHed | Same, plus one syscall | Containers. This is what OCI runtimes do |
| Mount ns + ID-mapped mounts | The above, plus per-mount ownership remapping | No | One open_tree+mount_setattr per mount | Rootless containers sharing host data without chown -R |
| Landlock / LSM path rules | Which paths a process may access, not what is mounted | No | Policy evaluation per access | You want to restrict access without restructuring the tree |
Mount namespaces are the kernel mechanism; how container runtimes and orchestrators compose them is the province of Linux Containers and Isolation MOC and Kubernetes MOC — this note (and this MOC) owns the mechanism, those own the composition.
Production Notes
Every OCI container runtime creates a mount namespace per container. runc clones with CLONE_NEWNS, makes the propagation explicit (recursively slaving or privatizing the root so the container’s mounts never bork the host), builds the container’s rootfs out of overlay and bind mounts inside that namespace, and then pivot_roots into it — the sequence detailed in that sibling note from runc’s own rootfs_linux.go.
systemd’s per-service hardening is mount namespaces underneath. ProtectSystem=, PrivateTmp=, ReadOnlyPaths=, ProtectHome=, BindPaths= and friends “also enable file system namespacing.” The manual spells out the exact three-step recipe behind PrivateMounts= (systemd.exec(5)):
“a new
CLONE_NEWNSnamespace is created, after which all existing mounts are remounted toMS_SLAVEto disable propagation from the unit’s processes to the host (but leaving propagation in the opposite direction in effect). Finally, the mounts are remounted again to the propagation mode configured withMountFlags=.”
Two details worth internalizing. First, the slave step happens first and cannot be undone by MountFlags=shared — “Setting this option to shared does not reestablish propagation in that case.” systemd will let a unit receive host mounts but never silently let it push them out. Second, namespaces are per forked process, not per unit: “Mounts established in the namespace of the process created by ExecStartPre= … will not be available to subsequent processes forked off for ExecStart=.” A setup command that mounts something for the main process is a bug, not a pattern.
ProtectSystem=strict is a mount-namespace remount, and it has a documented hole. systemd notes that for ReadWritePaths=/ReadOnlyPaths=, “mounts created on the host generally appear in the unit processes’ namespace … even when propagated below a path marked with ReadOnlyPaths=! … the lock-down offered by that setting is not complete.” That is inbound propagation doing exactly what the slave setting asks it to do — the hardening is real but is not a capability boundary.
Kubernetes exposes propagation as a one-word YAML field, and the mapping is direct:
volumeMounts[].mountPropagation | Kernel propagation | Effect | Requires |
|---|---|---|---|
None (default) | rprivate (MS_REC | MS_PRIVATE) | No mounts cross in either direction after the container starts | — |
HostToContainer | rslave (MS_REC | MS_SLAVE) | Host mounts under the volume appear inside; container mounts stay in | — |
Bidirectional | rshared (MS_REC | MS_SHARED) | Mounts cross both ways; a container mount lands on the node and in every pod sharing the volume | Privileged container |
Two details the Kubernetes documentation is explicit about and that are routinely misremembered. First, None is rprivate, not plain private — but “the CRI runtime may choose rslave mount propagation … when rprivate propagation is not applicable,” and cri-dockerd is named as doing so “when the mount source contains the Docker daemon’s root directory (/var/lib/docker).” So None is a request, not a guarantee. Second, only Bidirectional requires a privileged container; HostToContainer does not. The docs justify the asymmetry bluntly: “Bidirectional mount propagation can be dangerous. It can damage the host operating system, and therefore, it is allowed only in privileged containers… any volume mounts created by containers in Pods must be destroyed (unmounted) by the containers on termination” (Kubernetes: Volumes, read 2026-08-29).
That last sentence is the recurring production incident in one line: a CSI or storage plugin uses Bidirectional, does not unmount on termination, and the mount survives the container. Repeat across restarts and a node either runs out of mounts (fs.mount-max, which “denotes the maximum number of mounts that may exist in a mount namespace” — Documentation/admin-guide/sysctl/fs.rst) or a cleanup path lazily unmounts far more than intended. The CL_SHARED_TO_SLAVE / MS_SLAVE machinery this note describes is the same code path, seen from a YAML field.
Debugging checklist for “my mount is/isn’t visible”:
readlink /proc/<pid>/ns/mnton both processes — same inode number means same namespace, and the question is not about namespaces at all.findmnt -o TARGET,PROPAGATION,ID,PARENTinside each namespace (nsenter -t <pid> -m findmnt …) — compare the propagation column first.- If tags say
shared:Non both sides with the sameN, they are peers and events will cross; if one saysmaster:N, it is receive-only. grep -c '' /proc/<pid>/mountinfoagainstsysctl fs.mount-maxwhen mounts are failing withENOSPC.- If a namespace outlives its container, hunt references:
lsof /proc/*/ns/mntandfindmnt | grep ns/mnt.
See Also
- Bind Mounts and Mount Propagation — the shared/slave/private/unbindable taxonomy in full: transition tables, bind and move semantics, the
propagate_fromtag, and thefs/pnode.cinternals - pivot_root and Changing the Root — how a namespace replaces its root (the next step after creating one);
chrootvspivot_rootin depth - The Mount Tree and vfsmount — the
struct mount/vfsmounttopology a namespace contains, andmnt_idvsmnt_id_unique - The New Mount API —
fsopen/fsconfig/fsmount/move_mount/open_tree, the modern way to build a mount (and the only way to build an ID-mapped one) before placing it into a namespace - User Namespaces — the privilege model that decides whether a mount namespace is “less privileged”
- overlayfs Union Filesystem — what a container’s root filesystem is usually made of
- Mount Point Traversal During Lookup — how path resolution crosses a mount within the namespace’s tree
- Linux Containers and Isolation MOC — how runtimes compose namespaces (mechanism boundary: this MOC owns the mount mechanism)
- Linux Filesystems and VFS MOC — parent MOC