Repository navigation
Description
mount.Mount(src, dst, "none", "bind,rprivate") performs a recursive bind mount instead of a plain bind mount. The same happens with bind,rslave, bind,rshared, and bind,runbindable.
Cause
parseOptionsORs every option into a singleintmask, so"bind,rprivate"producesMS_BIND|MS_PRIVATE|MS_REC(0x45000).- In
mount()(mount/mounter_linux.go),oflags := flags &^ ptypesstrips the propagation type bits before the firstmount(2)call, but leavesMS_REC(0x4000) set. - The initial
mount(2)call runs withMS_BIND|MS_REC, which the kernel treats asrbind, copying all submounts undersrcintodstbefore the secondmount(2)call appliesrprivate.
We hit this via Moby local volumes (volume/local), which pass o= directly to mount.Mount. A local volume with type=none,device=<dir>,o=bind,rprivate exposes every submount under <dir> inside the container; with o=bind,rshared, docker volume rm also unmounts those submounts on the host because the volume became a shared recursive bind.
Details and dockerd reproduction matrix
Tested on Linux with dockerd built from moby/moby master using local volumes type=none,device=/src,o=<opts>, where /src is a shared tmpfs with a tmpfs submount at /src/sub:
o= |
/src/sub visible in container |
host /src/sub after docker volume rm |
|---|---|---|
bind, bind,private |
no | kept |
bind,rprivate, bind,rslave |
yes (expected: no) | kept |
bind,rshared |
yes (expected: no) | unmounted (expected: kept) |
rbind, rbind,r* |
yes | rbind/rbind,rshared: unmounted; rprivate/rslave: kept |
Minimal standalone Go reproducer (run as root or under unshare -Urm --propagation private):
src, _ := os.MkdirTemp("", "src")
dst, _ := os.MkdirTemp("", "dst")
_ = mount.Mount("tmpfs", src, "tmpfs", "shared")
_ = os.Mkdir(filepath.Join(src, "sub"), 0o755)
_ = mount.Mount("tmpfs", filepath.Join(src, "sub"), "tmpfs", "")
_ = mount.Mount(src, dst, "none", "bind,rprivate")
mounted, _ := mountinfo.Mounted(filepath.Join(dst, "sub"))
fmt.Println("dst/sub mounted:", mounted) // prints true (expected: false)Proposed fix
Split propagation options out of parseOptions (flag, propFlag, data) so the initial mount(2) call receives flag and the propagation mount(2) call receives propFlag. MakeMount and ensureMountedAs are updated to match, and the exported API stays unchanged.
Callers passing bind,r<prop> that relied on submounts being copied would now get a non-recursive bind (matching mount --bind --make-r<prop>). Callers that want submounts would need rbind,r<prop>.
Does this approach look good to maintainers? Happy to open a PR with unit and root mount tests if so.
AI disclosure: Used Antigravity (Gemini) to help trace the MS_REC bitmask interaction, build the dockerd reproducer, and draft this issue; reviewed and verified by the author.
Activity