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 /data and 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 the HISTORY of mount_namespaces(7)); its clone(2)/unshare(2) flag is the terse CLONE_NEWNS (“new namespace”) rather than the more descriptive CLONE_NEWMNT that later namespace types’ flags resemble.

Uncertain

Verify: the common explanation that the flag is named CLONE_NEWNS (not CLONE_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 the NEWNS name 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:

  • ns is 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 table mntns_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.
  • root points at the struct mount that is the top of this namespace’s tree. Path resolution for a task in this namespace ultimately bottoms out here.
  • mounts is the collection of every mount in the namespace, held as a red-black tree keyed by mnt_id_unique. This is not how it always looked: at v6.7 and earlier the field was a plain struct list_head list, and the rbtree arrived in v6.8 — verified by fetching fs/mount.h at the v6.6, v6.7, v6.8, v6.10, v6.11 and v6.12 tags and comparing. The switch is what makes listmount(2) able to resume a listing from an arbitrary mount ID in logarithmic time instead of re-walking a list.
  • user_ns / ucounts tie the namespace to an owning user namespace and to that namespace’s budget. alloc_mnt_ns() calls inc_mnt_namespaces()inc_ucount(ns, current_euid(), UCOUNT_MNT_NAMESPACES) and returns -ENOSPC if 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 of unshare -m fails with ENOSPC rather than exhausting kernel memory.
  • seq is a globally monotonic 64-bit id. The comment above mnt_ns_seq in fs/namespace.c states 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_mounts are the accounting behind the per-namespace mount cap (sysctl fs.mount-max), which stops a propagation storm from creating unbounded mounts.
  • mnt_ns_tree_node splices 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 the mnt_ns_id field in the listmount/statmount request struct — the two are one feature.
  • passive is a second, weaker reference count: “number references not pinning @mounts”. A passive reference keeps the mnt_namespace allocation alive so it can be inspected (by statmount/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;
}
  1. The fast path. If CLONE_NEWNS is not in the flags, no new namespace is wanted: the function bumps the parent namespace’s refcount with get_mnt_ns and returns it. The child shares the parent’s mount namespace. This is the overwhelmingly common case — fork() without CLONE_NEWNS does not create a new mount view, and the likely() annotation says the kernel expects exactly that.

  2. Allocate. Otherwise alloc_mnt_ns kzallocs a fresh mnt_namespace with GFP_KERNEL_ACCOUNT — meaning the allocation is itself charged to the creator’s memory cgroup — assigns a new seq from mnt_ns_seq, allocates an inode number via ns_alloc_inum for /proc/<pid>/ns/mnt, sets mounts = RB_ROOT, and takes the ucounts budget slot described above.

  3. Copy flags. CL_COPY_UNBINDABLE says: when deep-copying, also copy mounts marked unbindable. This is a deliberate asymmetry — a recursive bind prunes unbindable subtrees (that is the whole point of MS_UNBINDABLE), but a full namespace clone must replicate them, or the child would silently lose mounts. CL_EXPIRE preserves expiry marks so auto-expiring mounts (NFS automounts) keep behaving.

  4. 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 adds CL_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.

  5. copy_tree. The deep copy: it allocates a new struct mount for every mount in the source subtree, cloning the vfsmount flags 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 same struct super_block, so writing a file through the container’s copy of /etc writes the host’s /etc. A mount namespace virtualizes topology, never content.

  6. 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.

  7. The lockstep walk. With the topology copied, the function walks the old tree (p) and the new tree (q) together via next_mnt, attaching each new mount to the new namespace with mnt_add_to_ns (which inserts it into ns->mounts keyed by mnt_id_unique and sets the MNT_ONRB flag) and counting it in nr_mounts.

  8. Re-rooting the task. If a fs_struct was passed (the task’s root/cwd holder), the loop swaps the task’s root.mnt and pwd.mnt from the old mounts to their freshly-copied counterparts with mntget, so the new process’s / and current directory point into its tree, not the parent’s. This is why clone(2) rejects CLONE_NEWNS | CLONE_FS: the function must own the fs_struct it is rewriting.

  9. Register. Finally mnt_ns_tree_add inserts the namespace into the global mnt_ns_tree rbtree keyed by its seq, so listmount(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.

TypeFlagSends events out?Receives events in?mountinfo tagNamespace-crossing meaning
sharedMS_SHAREDyes, to all peersyes, from all peersshared:XFully two-way. A mount inside the container appears on the host; a host unmount tears down the container’s copy.
slaveMS_SLAVEnoyes, from master groupmaster:XOne-way in. The container sees host mounts appear, but cannot push anything out. The safe default for containers.
privateMS_PRIVATEnono(none)Fully cut. Nothing crosses in either direction. What unshare(1) gives you by default.
unbindableMS_UNBINDABLEnonounbindablePrivate, 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.

RestrictionKernel mechanism (v6.12)Concrete consequence
Shared mounts demoted to slaveCL_SHARED_TO_SLAVE in copy_mnt_nsThe container cannot push a mount out to the host.
Mounts that arrived as a unit are locked together and cannot be individually unmountedMNT_LOCKED set on every copied mount whose mnt_expire list is emptyumount(2) returns EINVAL — the kernel’s “this mount is locked” error.
MS_RDONLY, MS_NOSUID, MS_NOEXEC and the atime flags become lockedMNT_LOCK_READONLY, MNT_LOCK_NOSUID, MNT_LOCK_NOEXEC, MNT_LOCK_ATIMEmount -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 output

If 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 the OPEN_TREE_CLONE flag and it must not already have been visible in a mount namespace.” You build it, map it, and only then move_mount(2) it into the tree. See The New Mount API.
  • The mapping cannot be changed afterwards; MOUNT_ATTR_IDMAP in attr_clr is EINVAL.
  • 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). EINVAL on 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 nsproxy points 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/mnt does 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;
  • EPERM needs three capability checks, not one: CAP_SYS_ADMIN in the target namespace’s owning user namespace, plus CAP_SYS_CHROOT and CAP_SYS_ADMIN in the caller’s. Joining a container’s mount view is as privileged as changing root, because it effectively is.
  • EINVAL on an anonymous namespace. Detached trees produced by open_tree(OPEN_TREE_CLONE) live in an anonymous namespace with no inode number; there is nothing to join.
  • EINVAL when fs->users != 1. The task’s fs_struct must be unshared, because installing the namespace rewrites root and cwd (set_fs_root, set_fs_pwd). A multithreaded process sharing one fs_struct cannot have one thread jump namespaces — the same constraint that makes CLONE_NEWNS | CLONE_FS illegal, 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
PositionValue hereFieldWhat to read from it
172mount IDUnique id for this mount (mnt_id); recycled after umount, so never use it as a stable key across time.
21parent mount IDThe mount this one is grafted onto. Fields 1 and 2 together reconstruct the whole tree.
3259:2st_dev major:minorThe backing device. Two mounts with the same value share a superblock.
4/rootWhich 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 pointPath relative to the reading process’s root, so it differs inside a chroot or after pivot_root.
6rw,relatimeper-mount optionsPer-vfsmount flags, not per-superblock.
7…shared:1optional fieldsThe propagation tags: shared:X, master:X, propagate_from:X, unbindable. Zero or more of them.
-separatorMarks the end of the variable-length optional fields.
n+1ext4filesystem type
n+2/dev/nvme0n1p2mount source
n+3rw,seclabelper-superblock optionsShared 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:

Kernelstruct mnt_id_req shapeForeign-namespace query
≤ v6.10(struct absent)
v6.11size, spare, mnt_id, param, mnt_ns_idBy namespace id (the seq value), looked up in the global mnt_ns_tree.
v6.12same shapeSame. In v6.12 grab_requested_mnt_ns() also interprets the spare field as a namespace file descriptorCLASS(fd, f)(kreq->spare), requiring proc_ns_file() and ops->type == CLONE_NEWNS.
v6.18size, mnt_ns_fd, mnt_id, param, mnt_ns_idThe 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.12 grab_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 both include/uapi/linux/mount.h and fs/namespace.c at the v6.12 tag and they disagree in naming — the header calls the field spare, the implementation treats it as a namespace fd — and I could not reach the patch thread that would settle intent (lore.kernel.org is behind an Anubis proof-of-work challenge and returns a JavaScript interstitial to curl; lwn.net returned HTTP 429 throughout this session). To resolve: read the commit that renamed spare to mnt_ns_fd and its cover letter on lore.kernel.org from 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 with MNT_DETACH will 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:N tags 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 -m without 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 with findmnt -o TARGET,PROPAGATION on both sides.
  • unshare(CLONE_NEWNS) fails with EPERM. CAP_SYS_ADMIN is required. Inside a user namespace you can gain it over that user namespace (unshare -Urm) — but then the shared→slave demotion and the lock_mnt_tree flag freezing both apply.
  • umount inside a rootless container fails with EINVAL, and mount(8) says “not mounted”. That is MNT_LOCKED, not a missing mount. strace it; the syscall returns EINVAL. Workaround: overmount rather than unmount, or unmount the whole propagated unit lazily.
  • mount -o remount,rw fails with EPERM in a rootless container. MNT_LOCK_READONLY was 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.
  • EINVAL combining CLONE_NEWNS with CLONE_FS. Disallowed by design — a new mount namespace must own a private fs_struct (step 8 of copy_mnt_ns rewrites it).
  • setns fails with EINVAL from a threaded program. mntns_install() requires fs->users == 1. Do the setns before creating threads, or in a fresh single-threaded child. (Go programs hit this constantly; this is why runc has a C constructor that runs before the Go runtime spawns threads.)
  • Expecting /proc/mounts to reflect another namespace. /proc/self/mounts shows your namespace. To see another process’s view you must read its /proc/<pid>/mountinfo, or use nsenter/setns, or use statmount/listmount with a namespace id. A monitoring tool that reads only /proc/self/mounts is blind to every container on the host — a classic observability bug.
  • Treating the mount ID as stable. Field 1 of mountinfo is recycled after unmount. Use mnt_id_unique (via statx(STATX_MNT_ID_UNIQUE) or statmount) 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).

MechanismWhat it changesEscapable?CostUse when
chroot(2)Apparent root directory only; the mount tree is unchanged and still fully presentYes, by a process with CAP_SYS_CHROOTNearly freeBuild sandboxes and rescue shells where the threat model is “mistakes”, not “attackers”
Mount namespaceThe whole set of mounts visibleNo — an invisible mount cannot be reachedOne struct mount per mount, per namespaceYou need a genuinely different filesystem view
Mount ns + pivot_rootVisible mounts and the root, with the old root discardableNo, once the old root is MNT_DETACHedSame, plus one syscallContainers. This is what OCI runtimes do
Mount ns + ID-mapped mountsThe above, plus per-mount ownership remappingNoOne open_tree+mount_setattr per mountRootless containers sharing host data without chown -R
Landlock / LSM path rulesWhich paths a process may access, not what is mountedNoPolicy evaluation per accessYou 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_NEWNS namespace is created, after which all existing mounts are remounted to MS_SLAVE to 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 with MountFlags=.”

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[].mountPropagationKernel propagationEffectRequires
None (default)rprivate (MS_REC | MS_PRIVATE)No mounts cross in either direction after the container starts
HostToContainerrslave (MS_REC | MS_SLAVE)Host mounts under the volume appear inside; container mounts stay in
Bidirectionalrshared (MS_REC | MS_SHARED)Mounts cross both ways; a container mount lands on the node and in every pod sharing the volumePrivileged 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”:

  1. readlink /proc/<pid>/ns/mnt on both processes — same inode number means same namespace, and the question is not about namespaces at all.
  2. findmnt -o TARGET,PROPAGATION,ID,PARENT inside each namespace (nsenter -t <pid> -m findmnt …) — compare the propagation column first.
  3. If tags say shared:N on both sides with the same N, they are peers and events will cross; if one says master:N, it is receive-only.
  4. grep -c '' /proc/<pid>/mountinfo against sysctl fs.mount-max when mounts are failing with ENOSPC.
  5. If a namespace outlives its container, hunt references: lsof /proc/*/ns/mnt and findmnt | grep ns/mnt.

See Also