Bind Mounts and Mount Propagation
A bind mount makes an existing file or directory subtree appear at a second location in the directory hierarchy — “after this call the same contents are accessible in two places” (mount(8)). It is not a copy, a link, or a special node: a bind creates a new in-kernel
struct mountwhose root dentry points at the source subtree, sharing the source’s superblock, so both locations are views onto the very same files. Mount propagation (the kernel’s shared-subtree machinery) is the orthogonal feature that controls whether mount and unmount events replicate between related mounts: a mount can be shared (events flow to and from its peer group), slave (events flow in but not out), private (no propagation), or unbindable (private and refuses to be bind-mounted) (sharedsubtree.rst, v6.12). Together these two mechanisms are what let one filesystem appear in many places and what makes a CD mounted on the host automatically show up inside a container — or, when misconfigured, lets a container’s mounts leak onto the host.
This note is pinned to Linux 6.12 LTS (released 2024-11-17). The classic sharedsubtree.txt documentation (Ram Pai, ~2005) describes the propagation semantics accurately, but its data-structure section is stale: it names the fields on struct vfsmount and calls the unbindable flag MNT_UNCLONABLE. At 6.12 those propagation fields live on struct mount (after Al Viro’s 2011 struct vfsmount→struct mount split) and the flag is MNT_UNBINDABLE — both verified directly against the v6.12 source below. The complementary mechanism — how a whole mount tree is copied when a namespace is cloned — is owned by Mount Namespaces; this note owns the propagation taxonomy that Mount Namespaces defers to, and the bind operation itself. The underlying struct mount/vfsmount topology is detailed in The Mount Tree and vfsmount.
Mental Model
Hold two ideas separately, because conflating them is the source of most confusion. The mount tree is the topology of where filesystems are attached — a graph of struct mount objects, each grafted onto a directory of some parent mount. The propagation tree is a second, orthogonal graph laid over the same mounts that says which mounts echo each other’s mount/unmount events. A bind mount adds a node to the first graph (a new place the subtree appears); a propagation type controls the second graph (whether that new place stays in sync with others). The kernel’s own documentation stresses this: “the propagation tree is orthogonal to the mount tree.”
A peer group is the unit of propagation: a set of mounts that propagate events to each other (a shared, bidirectional clique). A slave mount hangs off a peer group as a one-way receiver — it gets the group’s events but sends nothing back. Private mounts are in no peer group and are deaf and mute. Unbindable mounts are private and additionally refuse to be cloned by a bind.
flowchart LR subgraph pg["Peer group 7 (shared, bidirectional)"] A["/mnt<br/>shared:7"] B["/tmp (bind of /mnt)<br/>shared:7"] A <==> B end C["/view<br/>master:7 (slave)"] D["/box<br/>(private)"] A == "mount /dev/sdb /mnt/x<br/>propagates" ==> B A == "and one-way to slave" ==> C C -. "mount under /view<br/>does NOT propagate back" .-> A D -.- D
The two propagation directions. What it shows: /mnt and /tmp are peers in shared group 7, so a mount under either appears under both (bidirectional <==>). /view is a slave of group 7 (master:7): it receives the group’s mounts (one-way ==>) but its own mounts never flow back. /box is private — isolated. The insight to take: “shared” means a symmetric clique (a peer group); “slave” means a one-way subscription to a clique; the asymmetry of slave is exactly what containers exploit — a container can be a slave that sees host mounts appear but can never push a mount onto the host.
Bind Mounts — the Mechanism
What a bind actually is
The mount(2) flag for a bind is MS_BIND: “A bind mount makes a file or a directory subtree visible at another point within the single directory hierarchy. … the filesystem type and data arguments are ignored, and the bind mount has the same mount options as the underlying mount” (mount(2)). The mount(8) man page is emphatic that nothing magical is created: “It is important to understand that ‘bind’ does not create any second-class or special node in the kernel VFS. The ‘bind’ is just another operation to attach a filesystem” (mount(8)).
Concretely, binding /src onto /dst allocates a fresh struct mount whose embedded vfsmount’s mnt_root is set to the dentry of /src (not the root of /src’s filesystem) and whose mnt_sb (superblock) is the same superblock as /src. Because the superblock — and therefore the inodes, the page cache, the on-disk data — is shared, the two locations are not copies; they are two windows onto one filesystem subtree. Editing a file through /dst/foo changes the same inode you’d reach through /src/foo. This is fundamentally different from a symlink: a symlink is a name that may go stale if its target moves, whereas, per the docs’ FAQ, “bind mounts continue to exist even if the other mount is unmounted or moved.”
Plain bind versus recursive bind (rbind)
A plain bind replicates only the named mount. “By default, when a directory is bind mounted, only that directory is mounted; if there are any submounts under the directory tree, they are not bind mounted” (mount(2)). If /src has another filesystem mounted at /src/data, a plain mount --bind /src /dst will show /dst but /dst/data will be empty (you’ll see the underlying directory, not the mounted filesystem).
A recursive bind, MS_BIND | MS_REC (mount --rbind), fixes this: “all submounts under the source subtree (other than unbindable mounts) are also bind mounted at the corresponding location in the target subtree.” So mount --rbind /src /dst replicates /src/data’s mount under /dst/data too. The kernel docs put it crisply: “Bind replicates the specified mount. Rbind replicates all the mounts in the tree belonging to the specified mount.” Unbindable submounts are silently pruned from the copy.
Bind mounts and options — the read-only gotcha
A bind cannot change mount options in the same call. Because MS_BIND ignores the data argument and inherits the source’s options, the naive mount --bind -o ro /src /dst does not produce a read-only bind — the ro is ignored, and /dst is read-write like the source. You must follow with a remount: “the alternative (classic) way to create a read-only bind mount is to use the remount operation,” e.g.:
# mount --bind /src /dst # (1) bind, inherits rw from /src
# mount -o remount,bind,ro /dst # (2) flip THIS mount to read-onlyThe remount in step (2) sets the per-mount flag MNT_READONLY on /dst alone, leaving /src read-write — a crucial property, because per-mount flags (ro, nosuid, nodev, noexec, noatime) are independently settable per mount point, distinct from the per-superblock flags (mount(2)). Modern kernels and util-linux also accept the combined mount -o bind,ro as a convenience that performs both steps. This per-mount read-only-bind idiom is exactly how container runtimes expose a host directory into a container read-only without affecting the host’s view.
Mount Propagation — the Four Types
A propagation event is, per the kernel docs, “an event generated on a vfsmount that leads to mount or unmount actions in other vfsmounts.” Each mount carries one propagation state controlling its participation.
Shared (MS_SHARED). “This mount shares events with members of a peer group. mount(2) and umount(2) events immediately under this mount will propagate to the other mounts that are members of the peer group” (mount_namespaces(7)). Sharing is symmetric: every peer both sends and receives. The canonical example from the docs: mount --make-shared /mnt then mount --bind /mnt /tmp puts /mnt and /tmp in one peer group, so mount /dev/sd0 /tmp/a makes the device visible at /mnt/a too, and vice versa.
Slave (MS_SLAVE). “mount(2) and umount(2) events propagate into this mount from a (master) shared peer group. mount(2) and umount(2) events under this mount do not propagate to any peer.” A slave is a one-way subscriber: it has a master peer group from which it receives, but its own mounts go nowhere. In the docs’ worked example, after mount --make-slave /tmp, mounting on /mnt/a still appears at /tmp/a (master→slave), but mounting on /tmp/b does not appear at /mnt/b (slave→master is blocked).
Private (MS_PRIVATE). “This mount is private; it does not have a peer group. mount(2) and umount(2) events do not propagate into or out of this mount.” This is the default for a brand-new mount, and the familiar behavior from before shared subtrees existed.
Unbindable (MS_UNBINDABLE). “This is like a private mount, and in addition this mount can’t be bind mounted.” Attempting mount --bind of an unbindable mount fails. Its purpose is to prevent the combinatorial explosion that recursive binds of a tree onto itself would cause — marking the inner copy point unbindable prunes it from every recursive bind, so replicating a tree at N points stays linear instead of exponential (the docs work through the exact V[i] = i*V[i-1] blow-up that unbindable mounts defeat).
Changing propagation type
Propagation type is changed with a dedicated mount(2) call carrying one of the four flags (specifying more than one is an error), or via mount(8)’s --make-shared / --make-slave / --make-private / --make-unbindable. By default the change “affects only the target mount”; adding MS_REC (the --make-r* variants: --make-rshared, --make-rslave, --make-rprivate, --make-runbindable) changes “the propagation type of all mounts under target” (mount(2)). Note that, unlike the actual mount operations, a propagation-type change is not itself a propagating event — it just relabels mounts.
The state transitions have subtleties worth knowing. Per the v6.12 docs’ state table: making a private or unbindable mount a slave has no effect (there is no master to slave to — “slaving a non-shared mount has no effect”). And if a shared mount is “the only mount in its peer group, making it a slave automatically makes it private” — there is no master to receive from, so slave collapses to private. A mount can also be both shared and slave (“shared and slave”): it receives from a master peer group and forwards to its own peer group and slaves — the state that arises when you bind a slave into a shared mount, and the mechanism behind layered, cascading propagation.
Verifying Against the v6.12 Kernel Source
The propagation graph is built from four list fields the kernel adds to each mount. The 2005 documentation places them on struct vfsmount; that is wrong for any modern kernel. Directly inspecting fs/mount.h at the v6.12 tag confirms they live on struct mount:
/* fs/mount.h, Linux v6.12 */
struct mount {
struct vfsmount mnt; /* the embedded "public" vfsmount */
...
struct list_head mnt_share; /* circular list of shared mounts (the peer group) */
struct list_head mnt_slave_list; /* list of slave mounts hanging off this master */
struct list_head mnt_slave; /* this mount's entry in its master's slave_list */
struct mount *mnt_master; /* the master from which this slave receives */
struct mnt_namespace *mnt_ns; /* the namespace this mount belongs to */
...
int mnt_id; /* mount identifier, reused after umount */
u64 mnt_id_unique; /* unique mount id until reboot */
int mnt_group_id; /* peer group identifier (the shared:N number) */
...
};Walking the fields (v6.12 fs/mount.h): mnt_share is the circular list threading all peers of a shared mount — every member of peer group N is on this list, which is why the docs say “all the shared vfsmounts in a peer group form a cyclic list through mnt_share.” mnt_slave_list is the list of slaves receiving from this mount (as master); mnt_slave is a slave’s own entry on its master’s mnt_slave_list; and mnt_master points back at the master. mnt_group_id is the integer you see as shared:N / master:N in /proc/<pid>/mountinfo. The flag that marks unbindability is, per v6.12 include/linux/mount.h:
#define MNT_SHARED 0x1000 /* if the vfsmount is a shared mount */
#define MNT_UNBINDABLE 0x2000 /* if the vfsmount is a unbindable mount */
#define MNT_SHARED_MASK (MNT_UNBINDABLE) /* cleared when a mount is cloned */So the documented MNT_UNCLONABLE is a historical name; the live flag is MNT_UNBINDABLE. All four list fields are protected by the namespace_sem lock (exclusive for modification, shared for reading), and the heavy lifting of building propagation trees during bind/move lives in fs/pnode.c (propagate_mnt, attach_recursive_mnt), structured as a prepare / commit / abort three-phase operation so a partial failure can be cleanly rolled back.
Uncertain
Verify: the attribution that the
struct vfsmount→struct mountsplit (which movedmnt_share/mnt_slave*/mnt_masteroffvfsmountontostruct mount) was Al Viro’s work landing around kernel 3.3 (2011). Reason: the fact that these fields are onstruct mountat v6.12 is confirmed by direct source inspection; the attribution and exact release of the split is from general knowledge, not a primary source consulted here. To resolve: locate the merge commit that introducedstruct mountinfs/mount.hand read its merge-window version. uncertain
Why systemd and Containers Need Propagation
Mount propagation is not an academic feature — it is load-bearing infrastructure for both init systems and containers.
The /cdrom use case the docs open with is the original motivation: a process clones a new mount namespace but still wants removable media mounted later on the host to appear inside it. Solution: make /cdrom shared on the host, and every cloned namespace gets a peer at /cdrom that receives the host’s mount when a CD is inserted. Without shared subtrees, the namespaces would be frozen snapshots, blind to anything mounted after they were created.
systemd makes the host’s entire mount tree shared at boot (it issues the equivalent of mount --make-rshared / early in PID 1) precisely so that mounts performed later propagate sensibly across the services it spawns. When it then hardens a service with PrivateTmp=, ProtectSystem=, or ReadWritePaths=, it unshares a mount namespace for that service and reshapes propagation so the service’s private mounts don’t leak back to the host.
Container runtimes depend on the slave asymmetry for safety. A runtime like runc typically makes the container’s root recursively slave (MS_SLAVE | MS_REC) or recursively private before building the container’s filesystem. Recursive-slave is the interesting choice: the container receives host mount events (so a volume the host mounts later can appear inside) but can never propagate a mount back onto the host — the one-way property is the security boundary. This is also why the kernel automatically demotes shared mounts to slave when crossing into a less-privileged mount namespace: “When creating 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)) — the kernel-level guarantee detailed from the copy side in Mount Namespaces.
Kubernetes surfaces these three states directly in its volume API: mountPropagation: None maps to private, HostToContainer maps to slave (rslave — the container sees host mounts but can’t push back), and Bidirectional maps to shared (rshared — mounts flow both ways, which is why it requires privilege and is reserved for things like CSI node plugins that must surface mounts onto the host).
Worked Examples
A shared peer group, observed
# mount --bind /mnt /mnt # (1) make /mnt a mount of itself (so we can mark it)
# mount --make-shared /mnt # (2) it now belongs to a fresh peer group
# mount --bind /mnt /tmp # (3) /tmp joins the SAME peer group
# grep -E ' /mnt | /tmp ' /proc/self/mountinfo
56 24 259:2 /mnt /mnt rw,relatime shared:7 - ext4 /dev/nvme0n1p2 rw
71 24 259:2 /mnt /tmp rw,relatime shared:7 - ext4 /dev/nvme0n1p2 rw
# mount -t tmpfs none /tmp/x # (4) mount under /tmp ...
# ls /mnt/x # (5) ... and it appears under /mnt too (propagated!)Reading it: (1) you can only set a propagation type on an actual mount, so we self-bind /mnt first. (2) --make-shared allocates a new mnt_group_id — note the shared:7 tag. (3) binding to /tmp clones the mount into the same peer group (same shared:7), because the source was shared. (4)–(5) a mount under one peer propagates to the other: the tmpfs at /tmp/x materializes at /mnt/x. The two shared:7 tags in mountinfo are the userspace-visible proof of peer-group membership.
Slave: receive but don’t send
# mount --make-shared /mnt # /mnt is shared (peer group)
# mount --bind /mnt /tmp # /tmp joins it
# mount --make-slave /tmp # /tmp now RECEIVES from /mnt but sends nothing
# grep ' /tmp ' /proc/self/mountinfo
71 24 259:2 /mnt /tmp rw,relatime master:7 - ext4 ... # note: master:7, not shared:7
# mount -t tmpfs none /mnt/a ; ls /tmp/a # master -> slave: APPEARS
# mount -t tmpfs none /tmp/b ; ls /mnt/b # slave -> master: does NOT appearThe mountinfo tag flips from shared:7 to master:7, meaning “I am a slave receiving from peer group 7.” Mounts under /mnt reach /tmp; mounts under /tmp go nowhere. This is the exact configuration a container runtime wants for a host-volume mount point.
Decoding the mountinfo propagation field
A single line of /proc/<pid>/mountinfo encodes both the bind source and the propagation state (proc_pid_mountinfo(5)):
71 24 259:2 /mnt /tmp rw,relatime master:7 - ext4 /dev/nvme0n1p2 rw
└┬ └┬ └─┬─ └─┬ └─┬ └────┬───── └──┬── │ └┬─ └──────┬───── └┬
│ │ │ │ │ │ │ │ │ │ per-superblock opts
│ │ │ │ │ │ │ │ │ mount source
│ │ │ │ │ │ │ │ filesystem type
│ │ │ │ │ │ │ separator ending optional fields
│ │ │ │ │ │ PROPAGATION: master:7 (slave of peer group 7)
│ │ │ │ │ per-mount options
│ │ │ │ mount point (relative to the reader's root)
│ │ │ ROOT: which subtree of the fs is mounted — for a BIND, the SOURCE subdir (/mnt, not /)
│ │ st_dev major:minor of the backing device
│ parent mount ID
mount ID (unique, reused after umount)Two columns matter most here. The root field (/mnt) reveals the bind: for a normal mount it is /, but for a bind it is the source subdirectory, so a /mnt-rooted mount whose device matches another mount is a tell-tale bind. The optional propagation field (master:7) is the propagation state: shared:N = shared in peer group N, master:N = slave receiving from group N, shared:N master:M = both shared and slave, unbindable = unbindable, and nothing = private.
Failure Modes and Common Misunderstandings
- “Read-only bind didn’t work.”
mount --bind -o ro /src /dstignores thero(a bind inherits the source’s options); you mustmount -o remount,bind,ro /dstafterward, or usemount -o bind,ro. The symptom is a writable/dstyou expected to be read-only. - Bind shows an empty subdirectory where a filesystem should be. You used a plain
--bindover a source that has submounts; plain bind doesn’t replicate submounts. Use--rbind(MS_REC) to pull the whole subtree. - Mounts mysteriously leaking onto the host (or host mounts vanishing). The mount point is shared and an unmount/mount in another namespace propagated. The fix runtimes apply is
mount --make-rslaveor--make-rprivateon the relevant subtree. The classic disaster is a container with aBidirectional/shared volume unmounting and tearing down the host’s mount. mount --bindof an unbindable mount fails. By design: “Binding a unbindable mount is an invalid operation.” Check forunbindableinmountinfo.--make-slavedid nothing. The target was private or unbindable, or was the only member of its peer group (which silently becomes private). Slave requires an existing master peer group.- A monitoring/security tool sees only
/proc/self/mountinfo. That is its own namespace’s view; it is blind to other namespaces’ mounts and to propagation across them. Read the target process’s/proc/<pid>/mountinfoinstead (see Mount Namespaces). - Reasoning about propagation as if it were the mount tree. The two are orthogonal: a mount can be deep in the tree but in a peer group that spans unrelated parts of it. Always read the
shared:/master:tag, not the path nesting.
Alternatives and Boundaries
A bind mount is the right tool when you need the same files at two paths within one directory hierarchy and want it to survive the source being moved or unmounted; a symbolic link is lighter but goes stale and is a different inode. For exposing files across a network, an export protocol (NFS) is the analog, but the kernel docs note that “exportfs is a heavyweight way of accomplishing part of what shared subtree can do” and cannot express slave semantics at all. Propagation has no real “alternative” — it is the mechanism — but you choose among its four states: private for full isolation (the default, and the right choice when you want no surprises), slave for the container “see-but-don’t-touch” boundary, shared for genuine bidirectional sync (init systems, CSI plugins), and unbindable to prune recursive-bind explosions. The new mount API offers open_tree(OPEN_TREE_CLONE) + move_mount() as a more controllable, fd-based way to clone and place a subtree than the legacy mount --bind/--move pair — covered in The New Mount API.
The scope line: this note and this MOC own the propagation and bind mechanism; how mount namespaces copy a tree and how container runtimes and Kubernetes compose all of it are owned elsewhere. The CL_SHARED_TO_SLAVE demotion that crosses a namespace boundary is described from the copy side in Mount Namespaces; the propagation vocabulary it relies on is defined here.
Production Notes
The propagation default is a recurring production footgun. systemd’s choice to make / recursively shared at boot means that unless a runtime explicitly intervenes, mounts a container makes can propagate back to the host. Every serious runtime therefore reshapes propagation immediately after creating the namespace: Docker historically mounts its data root (/var/lib/docker) as a slave to contain its bind-mount churn; runc makes the container rootfs rslave or rprivate per the OCI spec’s rootfsPropagation. The Kubernetes mountPropagation field is the same machinery exposed to application authors, and its misuse is a frequent incident class — a CSI driver that needs Bidirectional to surface device mounts onto the node, set to HostToContainer by mistake, silently fails to make volumes visible; conversely an application volume left Bidirectional can escape onto the node. The diagnostic in all these cases is the same: read /proc/<pid>/mountinfo for the shared:/master: tags and confirm the propagation matches intent. The deepest documented pathology — recursive binds of a tree onto itself exploding into thousands of mounts — is precisely what the unbindable type exists to defuse, and remains the textbook reason that type is in the kernel at all.
See Also
- Mount Namespaces — how a whole mount tree is copied on
clone(CLONE_NEWNS); theCL_SHARED_TO_SLAVEdemotion (this note owns the propagation taxonomy that note defers to) - The Mount Tree and vfsmount — the
struct mount/vfsmounttopology a bind adds a node to - The New Mount API —
open_tree(OPEN_TREE_CLONE)+move_mount(), the fd-based modern alternative tomount --bind/--move - pivot_root and Changing the Root — uses recursive binds and propagation changes when assembling a container root
- overlayfs Union Filesystem — the merged overlay mount, and bypass volumes, are grafted with binds
- Mount Point Traversal During Lookup — how path resolution crosses these mounts
- Linux Filesystems and VFS MOC — parent MOC
- Linux Containers and Isolation MOC — runtime composition of bind mounts and propagation (scope boundary: this MOC owns the mechanism)