Visitar URL original
mount: "bind" with recursive propagation (rprivate/rslave/rshared) performs a recursive bind (rbind) · Issue #279 · moby/sys · GitHub
Skip to content

mount: "bind" with recursive propagation (rprivate/rslave/rshared) performs a recursive bind (rbind) #279

Open

Description

@f-sommerauer

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

  1. parseOptions ORs every option into a single int mask, so "bind,rprivate" produces MS_BIND|MS_PRIVATE|MS_REC (0x45000).
  2. In mount() (mount/mounter_linux.go), oflags := flags &^ ptypes strips the propagation type bits before the first mount(2) call, but leaves MS_REC (0x4000) set.
  3. The initial mount(2) call runs with MS_BIND|MS_REC, which the kernel treats as rbind, copying all submounts under src into dst before the second mount(2) call applies rprivate.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions