From 0b983ff832b175e406f4f9b1a3868457bb1ceb7b Mon Sep 17 00:00:00 2001 From: Lucas Manning Date: Tue, 25 Oct 2022 15:33:05 -0700 Subject: [PATCH] Allow for creation of shared mounts. This is the first in a series of changes that will enabled shared mount subtrees. This will emulate the Linux kernel implementation described here: https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt Mounts can now a be part of a mount groups. These groups are identified by ids assigned by the VFS. Group IDs can be reused after all mounts that are a member of that group are destroyed. Newly created mounts inherit the propagation type of their parent mount. A mount created on a shared mount will also be a shared mount, though it will be part of a different group. A shared mount 'A' bound to a shared mount 'B' will replicate that bind to all of 'B's peers. These new mounts will all be part of a new shared peer group. Unmounting a mount 'A' that is a direct child of a shared mount 'B' mounted at dentry 'b' will propagate that event to all its peers. So peers B1, B2, B3, etc will all unmount the mounts (A1, A2...) located at 'b'. However, if any of peers of A have children, they are skipped. If the original mount A has children, the mount is failed entirely. The initial root mount has propagation type MS_PRIVATE. This change only implements a basic version of mount groups. Notably it does not implement MS_SLAVE, MS_UNBINDABLE, or MS_REC. PiperOrigin-RevId: 483792757 --- pkg/bitmap/bitmap.go | 16 + pkg/bitmap/bitmap_test.go | 16 + pkg/sentry/syscalls/linux/vfs2/mount.go | 122 +++--- pkg/sentry/vfs/BUILD | 15 + pkg/sentry/vfs/dentry.go | 4 +- pkg/sentry/vfs/mount.go | 369 +++++++++++++++- pkg/sentry/vfs/vfs.go | 8 + test/syscalls/linux/mount.cc | 541 +++++++++++++++++++++++- 8 files changed, 1009 insertions(+), 82 deletions(-) diff --git a/pkg/bitmap/bitmap.go b/pkg/bitmap/bitmap.go index cd72a4472..e5dac45aa 100644 --- a/pkg/bitmap/bitmap.go +++ b/pkg/bitmap/bitmap.go @@ -50,6 +50,22 @@ func (b *Bitmap) IsEmpty() bool { return b.numOnes == 0 } +// Size returns the total number of bits in the bitmap. +func (b *Bitmap) Size() int { + return len(b.bitBlock) * 64 +} + +// Grow grows the bitmap by at least toGrow bits. +func (b *Bitmap) Grow(toGrow uint32) error { + newbitBlockSize := uint32(len(b.bitBlock)) + ((toGrow + 63) / 64) + if newbitBlockSize > MaxBitEntryLimit/8 { + return fmt.Errorf("requested bitmap size %d too large", newbitBlockSize*64) + } + bits := make([]uint64, (toGrow+63)/64) + b.bitBlock = append(b.bitBlock, bits...) + return nil +} + // Minimum return the smallest value in the Bitmap. func (b *Bitmap) Minimum() uint32 { for i := 0; i < len(b.bitBlock); i++ { diff --git a/pkg/bitmap/bitmap_test.go b/pkg/bitmap/bitmap_test.go index 1e0f40214..56b60567f 100644 --- a/pkg/bitmap/bitmap_test.go +++ b/pkg/bitmap/bitmap_test.go @@ -340,3 +340,19 @@ func TestFirstOne(t *testing.T) { } } } + +func TestGrow(t *testing.T) { + bitmap := New(uint32(64)) + bitmap.FlipRange(0, 64) + bitmap.Grow(64) + + want := make([]uint32, 64) + for i := 0; i < 128; i++ { + if i < 64 { + want[i] = uint32(i) + } + } + if !reflect.DeepEqual(bitmap.ToSlice(), want) { + t.Errorf("Grow() got: %v, want: %v", bitmap.ToSlice(), want) + } +} diff --git a/pkg/sentry/syscalls/linux/vfs2/mount.go b/pkg/sentry/syscalls/linux/vfs2/mount.go index 4fcd9e87c..88c8e89a3 100644 --- a/pkg/sentry/syscalls/linux/vfs2/mount.go +++ b/pkg/sentry/syscalls/linux/vfs2/mount.go @@ -16,6 +16,7 @@ package vfs2 import ( "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/bits" "gvisor.dev/gvisor/pkg/errors/linuxerr" "gvisor.dev/gvisor/pkg/fspath" "gvisor.dev/gvisor/pkg/hostarch" @@ -32,39 +33,6 @@ func Mount(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.Syscall flags := args[3].Uint64() dataAddr := args[4].Pointer() - // For null-terminated strings related to mount(2), Linux copies in at most - // a page worth of data. See fs/namespace.c:copy_mount_string(). - fsType, err := t.CopyInString(typeAddr, hostarch.PageSize) - if err != nil { - return 0, nil, err - } - source, err := t.CopyInString(sourceAddr, hostarch.PageSize) - if err != nil { - return 0, nil, err - } - - targetPath, err := copyInPath(t, targetAddr) - if err != nil { - return 0, nil, err - } - - data := "" - if dataAddr != 0 { - // In Linux, a full page is always copied in regardless of null - // character placement, and the address is passed to each file system. - // Most file systems always treat this data as a string, though, and so - // do all of the ones we implement. - data, err = t.CopyInString(dataAddr, hostarch.PageSize) - if err != nil { - return 0, nil, err - } - } - - // Ignore magic value that was required before Linux 2.4. - if flags&linux.MS_MGC_MSK == linux.MS_MGC_VAL { - flags = flags &^ linux.MS_MGC_MSK - } - // Must have CAP_SYS_ADMIN in the current mount namespace's associated user // namespace. creds := t.Credentials() @@ -72,38 +40,29 @@ func Mount(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.Syscall return 0, nil, linuxerr.EPERM } - const unsupportedOps = linux.MS_REMOUNT | linux.MS_SHARED | linux.MS_PRIVATE | - linux.MS_SLAVE | linux.MS_UNBINDABLE | linux.MS_MOVE + // Ignore magic value that was required before Linux 2.4. + if flags&linux.MS_MGC_MSK == linux.MS_MGC_VAL { + flags = flags &^ linux.MS_MGC_MSK + } - // Silently allow MS_NOSUID, since we don't implement set-id bits - // anyway. - const unsupportedFlags = linux.MS_NODIRATIME | linux.MS_STRICTATIME + // Silently allow MS_NOSUID, since we don't implement set-id bits anyway. + const unsupported = linux.MS_REMOUNT | linux.MS_SLAVE | + linux.MS_UNBINDABLE | linux.MS_MOVE | linux.MS_REC | linux.MS_NODIRATIME | + linux.MS_STRICTATIME // Linux just allows passing any flags to mount(2) - it won't fail when // unknown or unsupported flags are passed. Since we don't implement // everything, we fail explicitly on flags that are unimplemented. - if flags&(unsupportedOps|unsupportedFlags) != 0 { + if flags&(unsupported) != 0 { return 0, nil, linuxerr.EINVAL } - var opts vfs.MountOptions - if flags&linux.MS_NOATIME == linux.MS_NOATIME { - opts.Flags.NoATime = true + // For null-terminated strings related to mount(2), Linux copies in at most + // a page worth of data. See fs/namespace.c:copy_mount_string(). + targetPath, err := copyInPath(t, targetAddr) + if err != nil { + return 0, nil, err } - if flags&linux.MS_NOEXEC == linux.MS_NOEXEC { - opts.Flags.NoExec = true - } - if flags&linux.MS_NODEV == linux.MS_NODEV { - opts.Flags.NoDev = true - } - if flags&linux.MS_NOSUID == linux.MS_NOSUID { - opts.Flags.NoSUID = true - } - if flags&linux.MS_RDONLY == linux.MS_RDONLY { - opts.ReadOnly = true - } - opts.GetFilesystemOptions.Data = data - target, err := getTaskPathOperation(t, linux.AT_FDCWD, targetPath, disallowEmptyPath, nofollowFinalSymlink) if err != nil { return 0, nil, err @@ -123,9 +82,56 @@ func Mount(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.Syscall } defer sourceTpop.Release(t) _, err = t.Kernel().VFS().BindAt(t, creds, &sourceTpop.pop, &target.pop) - } else { - _, err = t.Kernel().VFS().MountAt(t, creds, source, &target.pop, fsType, &opts) + return 0, nil, err } + const propagationFlags = linux.MS_SHARED | linux.MS_PRIVATE | linux.MS_SLAVE | linux.MS_UNBINDABLE + if propFlag := flags & propagationFlags; propFlag != 0 { + // Check if flags is a power of 2. If not then more than one flag is set. + if !bits.IsPowerOfTwo64(propFlag) { + return 0, nil, linuxerr.EINVAL + } + propType := vfs.PropagationTypeFromLinux(propFlag) + return 0, nil, t.Kernel().VFS().SetMountPropagation(t, creds, &target.pop, propType) + } + + // Only copy in source, fstype, and data if we are doing a normal mount. + source, err := t.CopyInString(sourceAddr, hostarch.PageSize) + if err != nil { + return 0, nil, err + } + fsType, err := t.CopyInString(typeAddr, hostarch.PageSize) + if err != nil { + return 0, nil, err + } + data := "" + if dataAddr != 0 { + // In Linux, a full page is always copied in regardless of null + // character placement, and the address is passed to each file system. + // Most file systems always treat this data as a string, though, and so + // do all of the ones we implement. + data, err = t.CopyInString(dataAddr, hostarch.PageSize) + if err != nil { + return 0, nil, err + } + } + var opts vfs.MountOptions + if flags&linux.MS_NOATIME == linux.MS_NOATIME { + opts.Flags.NoATime = true + } + if flags&linux.MS_NOEXEC == linux.MS_NOEXEC { + opts.Flags.NoExec = true + } + if flags&linux.MS_NODEV == linux.MS_NODEV { + opts.Flags.NoDev = true + } + if flags&linux.MS_NOSUID == linux.MS_NOSUID { + opts.Flags.NoSUID = true + } + if flags&linux.MS_RDONLY == linux.MS_RDONLY { + opts.ReadOnly = true + } + opts.GetFilesystemOptions.Data = data + _, err = t.Kernel().VFS().MountAt(t, creds, source, &target.pop, fsType, &opts) return 0, nil, err } diff --git a/pkg/sentry/vfs/BUILD b/pkg/sentry/vfs/BUILD index f0b51ef31..0975abecf 100644 --- a/pkg/sentry/vfs/BUILD +++ b/pkg/sentry/vfs/BUILD @@ -51,6 +51,19 @@ go_template_instance( }, ) +go_template_instance( + name = "shared_list", + out = "shared_list.go", + package = "vfs", + prefix = "shared", + template = "//pkg/ilist:generic_list", + types = { + "Element": "*Mount", + "Linker": "*sharedEntry", + "ElementMapper": "sharedMapper", + }, +) + go_template_instance( name = "event_list", out = "event_list.go", @@ -129,6 +142,7 @@ go_library( "permissions.go", "resolving_path.go", "save_restore.go", + "shared_list.go", "vfs.go", "virtual_filesystem_mutex.go", ], @@ -136,6 +150,7 @@ go_library( deps = [ "//pkg/abi/linux", "//pkg/atomicbitops", + "//pkg/bitmap", "//pkg/context", "//pkg/errors/linuxerr", "//pkg/fd", diff --git a/pkg/sentry/vfs/dentry.go b/pkg/sentry/vfs/dentry.go index 508df80b8..e99111b67 100644 --- a/pkg/sentry/vfs/dentry.go +++ b/pkg/sentry/vfs/dentry.go @@ -336,7 +336,9 @@ func (vfs *VirtualFilesystem) forgetDeadMountpoint(ctx context.Context, d *Dentr vfs.mountMu.Lock() vfs.mounts.seq.BeginWrite() for mnt := range vfs.mountpoints[d] { - vdsToDecRef, mountsToDecRef = vfs.umountRecursiveLocked(mnt, &umountRecursiveOptions{}, vdsToDecRef, mountsToDecRef) + vds, mounts := vfs.umountAtRecursiveLocked(ctx, VirtualDentry{mnt, d}, &umountRecursiveOptions{}) + vdsToDecRef = append(vdsToDecRef, vds...) + mountsToDecRef = append(mountsToDecRef, mounts...) } vfs.mounts.seq.EndWrite() vfs.mountMu.Unlock() diff --git a/pkg/sentry/vfs/mount.go b/pkg/sentry/vfs/mount.go index bf5466588..c05ffb393 100644 --- a/pkg/sentry/vfs/mount.go +++ b/pkg/sentry/vfs/mount.go @@ -29,6 +29,42 @@ import ( "gvisor.dev/gvisor/pkg/sentry/kernel/auth" ) +// PropagationType is a propagation flavor as described in +// https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt. Child +// and Unbindable are currently unimplemented. +// TODO(b/249777195): Support MS_SLAVE and MS_UNBINDABLE propagation types. +type PropagationType int + +const ( + // Unknown represents an invalid/unknown propagation type. + Unknown PropagationType = iota + // Shared represents the shared propagation type. + Shared + // Private represents the private propagation type. + Private + // Child represents the child propagation type (MS_SLAVE). + Child + // Unbindable represents the unbindable propagation type. + Unbindable +) + +// PropagationTypeFromLinux returns the PropagationType corresponding to a +// linux mount flag, aka MS_SHARED. +func PropagationTypeFromLinux(propFlag uint64) PropagationType { + switch propFlag { + case linux.MS_SHARED: + return Shared + case linux.MS_PRIVATE: + return Private + case linux.MS_SLAVE: + return Child + case linux.MS_UNBINDABLE: + return Unbindable + default: + return Unknown + } +} + // A Mount is a replacement of a Dentry (Mount.key.point) from one Filesystem // (Mount.key.parent.fs) with a Dentry (Mount.root) from another Filesystem // (Mount.fs), which applies to path resolution in the context of a particular @@ -82,6 +118,23 @@ type Mount struct { // Mount. children is protected by VirtualFilesystem.mountMu. children map[*Mount]struct{} + // propagationType is propagation type of this mount. It can be shared or + // private. + propType PropagationType + + // sharedList is a list of mounts in the shared peer group. It is nil if + // propType is not Shared. All mounts in a shared peer group hold the same + // sharedList. The mounts in sharedList do not need an extra reference taken + // because it would be redundant with the taken for being attached to a + // parent mount. If a mount is in a shared list if and only if it is attached + // and has the shared propagation type. + sharedList *sharedList + sharedEntry sharedEntry + + // groupID is the ID for this mount's shared peer group. If the mount is not + // in a peer group, this is 0. + groupID uint32 + // umounted is true if VFS.umountRecursiveLocked() has been called on this // Mount. VirtualFilesystem does not hold a reference on Mounts for which // umounted is true. umounted is protected by VirtualFilesystem.mountMu. @@ -94,15 +147,20 @@ type Mount struct { writers atomicbitops.Int64 } +type sharedMapper struct{} + +func (sharedMapper) linkerFor(mnt *Mount) *sharedEntry { return &mnt.sharedEntry } + func newMount(vfs *VirtualFilesystem, fs *Filesystem, root *Dentry, mntns *MountNamespace, opts *MountOptions) *Mount { mnt := &Mount{ - ID: vfs.lastMountID.Add(1), - Flags: opts.Flags, - vfs: vfs, - fs: fs, - root: root, - ns: mntns, - refs: atomicbitops.FromInt64(1), + ID: vfs.lastMountID.Add(1), + Flags: opts.Flags, + vfs: vfs, + fs: fs, + root: root, + ns: mntns, + propType: Private, + refs: atomicbitops.FromInt64(1), } if opts.ReadOnly { mnt.setReadOnlyLocked(true) @@ -121,6 +179,60 @@ func (mnt *Mount) Options() MountOptions { } } +// addPeer adds oth to mnt's peer group. Both will have the same groupID +// and sharedList. vfs.mountMu must be locked. +// +// +checklocks:vfs.mountMu +func (vfs *VirtualFilesystem) addPeer(mnt *Mount, oth *Mount) { + mnt.sharedList.PushBack(oth) + oth.sharedList = mnt.sharedList + oth.propType = mnt.propType + oth.groupID = mnt.groupID +} + +// mergePeerGroup merges oth and all its peers into mnt's peer group. Oth +// must have propagation type shared and vfs.mountMu must be locked. +// +// +checklocks:vfs.mountMu +func (vfs *VirtualFilesystem) mergePeerGroup(mnt *Mount, oth *Mount) { + peer := oth.sharedList.Front() + for peer != nil { + next := peer.sharedEntry.Next() + vfs.setPropagation(peer, Private) + vfs.addPeer(mnt, peer) + peer = next + } +} + +// setPropagation sets the propagation on mnt for a propagation type. +// +// +checklocks:vfs.mountMu +func (vfs *VirtualFilesystem) setPropagation(mnt *Mount, ptype PropagationType) error { + switch ptype { + case Shared: + id, err := vfs.allocateGroupID() + if err != nil { + return err + } + mnt.groupID = id + mnt.sharedList = &sharedList{} + mnt.sharedList.PushBack(mnt) + case Private: + if mnt.propType == Shared { + mnt.sharedList.Remove(mnt) + if mnt.sharedList.Empty() { + vfs.freeGroupID(mnt.groupID) + } + mnt.sharedList = nil + mnt.groupID = 0 + } + default: + panic(fmt.Sprintf("unsupported propagation type: %v", ptype)) + } + mnt.propType = ptype + return nil +} + // A MountNamespace is a collection of Mounts.// // MountNamespaces are reference-counted. Unless otherwise specified, all // MountNamespace methods require that a reference is held. @@ -224,12 +336,95 @@ func (vfs *VirtualFilesystem) ConnectMountAt(ctx context.Context, creds *auth.Cr return err } vfs.mountMu.Lock() + defer vfs.mountMu.Unlock() + tree := vfs.preparePropagationTree(mnt, vd) + if err := vfs.connectMountAt(ctx, mnt, vd); err != nil { + vfs.abortPropagationTree(ctx, tree) + return err + } + vfs.commitPropagationTree(ctx, tree) + return nil +} + +// preparePropagationTree returns a mapping of propagated mounts to their future +// mountpoints. The new mounts are clones of mnt and are added to mnt's peer +// group if vd.mount and mnt are shared. All the cloned mounts and new +// mountpoints in the tree have an extra reference taken. +// +// +checklocks:vfs.mountMu +// +checklocksalias:mnt.vfs.mountMu=vfs.mountMu +func (vfs *VirtualFilesystem) preparePropagationTree(mnt *Mount, vd VirtualDentry) map[*Mount]VirtualDentry { + tree := map[*Mount]VirtualDentry{} + if vd.mount.propType == Private { + return tree + } + if mnt.propType == Private { + vfs.setPropagation(mnt, Shared) + } + var newPeerGroup []*Mount + for peer := vd.mount.sharedList.Front(); peer != nil; peer = peer.sharedEntry.Next() { + if peer == vd.mount { + continue + } + peerVd := VirtualDentry{ + mount: peer, + dentry: vd.dentry, + } + peerVd.IncRef() + clone := vfs.cloneMount(mnt, mnt.root) + tree[clone] = peerVd + newPeerGroup = append(newPeerGroup, clone) + } + for _, newPeer := range newPeerGroup { + vfs.addPeer(mnt, newPeer) + } + return tree +} + +// commitPropagationTree attaches to mounts in tree to the mountpoints they +// are mapped to. If there is an error attaching a mount, the method panics. +// +// +checklocks:vfs.mountMu +func (vfs *VirtualFilesystem) commitPropagationTree(ctx context.Context, tree map[*Mount]VirtualDentry) { + // The peer mounts should have no way of being dead if we've reached this + // point so its safe to connect without checks. + vfs.mounts.seq.BeginWrite() + for mnt, vd := range tree { + vd.dentry.mu.Lock() + mntns := vd.mount.ns + vfs.connectLocked(mnt, vd, mntns) + vd.dentry.mu.Unlock() + mnt.DecRef(ctx) + } + vfs.mounts.seq.EndWrite() +} + +// abortPropagationTree releases any references held by the mounts and +// mountpoints in the tree and removes the mounts from their peer groups. +// +// +checklocks:vfs.mountMu +func (vfs *VirtualFilesystem) abortPropagationTree(ctx context.Context, tree map[*Mount]VirtualDentry) { + for mnt, vd := range tree { + vd.DecRef(ctx) + vfs.setPropagation(mnt, Private) + mnt.DecRef(ctx) + } +} + +// connectMountAtLocked attaches mnt at vd. It returns the new mountpoint of mnt +// if no error occurred. +// +// Preconditions: +// - mnt must be disconnected. +// - vfs.mountMu must be locked. +// +// +checklocks:vfs.mountMu +func (vfs *VirtualFilesystem) connectMountAt(ctx context.Context, mnt *Mount, vd VirtualDentry) error { vdDentry := vd.dentry vdDentry.mu.Lock() for { if vd.mount.umounted || vdDentry.dead { vdDentry.mu.Unlock() - vfs.mountMu.Unlock() vd.DecRef(ctx) return linuxerr.ENOENT } @@ -267,28 +462,92 @@ func (vfs *VirtualFilesystem) ConnectMountAt(ctx context.Context, creds *auth.Cr vfs.connectLocked(mnt, vd, mntns) vfs.mounts.seq.EndWrite() vdDentry.mu.Unlock() - vfs.mountMu.Unlock() return nil } +// SetMountPropagation changes the propagation type of the mount pointed to by +// pop. +func (vfs *VirtualFilesystem) SetMountPropagation(ctx context.Context, creds *auth.Credentials, pop *PathOperation, propType PropagationType) error { + vd, err := vfs.GetDentryAt(ctx, creds, pop, &GetDentryOptions{}) + if err != nil { + return err + } + // See the similar defer in UmountAt for why this is in a closure. + defer func() { + vd.DecRef(ctx) + }() + if vd.dentry.isMounted() { + if realmnt := vfs.getMountAt(ctx, vd.mount, vd.dentry); realmnt != nil { + vd.mount.DecRef(ctx) + vd.mount = realmnt + } + } else if vd.dentry != vd.mount.root { + return linuxerr.EINVAL + } + + vfs.mountMu.Lock() + defer vfs.mountMu.Unlock() + mnt := vd.mount + if propType != mnt.propType { + switch propType { + case Shared, Private: + vfs.setPropagation(mnt, propType) + default: + panic(fmt.Sprintf("unsupported propagation type: %v", propType)) + } + } + mnt.propType = propType + return nil +} + +// cloneMount returns a new mount with mnt.fs as the filesystem and root as the +// root. The returned mount has an extra reference. +// +// +checklocks:vfs.mountMu +// +checklocksalias:mnt.vfs.mountMu=vfs.mountMu +func (vfs *VirtualFilesystem) cloneMount(mnt *Mount, root *Dentry) *Mount { + opts := MountOptions{ + Flags: mnt.Flags, + ReadOnly: mnt.ReadOnly(), + } + return vfs.NewDisconnectedMount(mnt.fs, root, &opts) +} + // BindAt creates a clone of the source path's parent mount and mounts it at // the target path. The new mount's root dentry is one pointed to by the source // path. // // TODO(b/249121230): Support recursive bind mounting. func (vfs *VirtualFilesystem) BindAt(ctx context.Context, creds *auth.Credentials, source, target *PathOperation) (*Mount, error) { - vd, err := vfs.GetDentryAt(ctx, creds, source, &GetDentryOptions{}) + sourceVd, err := vfs.GetDentryAt(ctx, creds, source, &GetDentryOptions{}) if err != nil { return nil, err } - defer vd.DecRef(ctx) - opts := vd.mount.Options() - mnt := vfs.NewDisconnectedMount(vd.mount.fs, vd.dentry, &opts) - defer mnt.DecRef(ctx) - if err := vfs.ConnectMountAt(ctx, creds, mnt, target); err != nil { + defer sourceVd.DecRef(ctx) + targetVd, err := vfs.GetDentryAt(ctx, creds, target, &GetDentryOptions{}) + if err != nil { return nil, err } - return mnt, nil + + vfs.mountMu.Lock() + defer vfs.mountMu.Unlock() + clone := vfs.cloneMount(sourceVd.mount, sourceVd.dentry) + defer clone.DecRef(ctx) + tree := vfs.preparePropagationTree(clone, targetVd) + if sourceVd.mount.propType == Shared { + if clone.propType == Private { + vfs.addPeer(sourceVd.mount, clone) + } else { + vfs.mergePeerGroup(sourceVd.mount, clone) + } + } + if err := vfs.connectMountAt(ctx, clone, targetVd); err != nil { + vfs.setPropagation(clone, Private) + vfs.abortPropagationTree(ctx, tree) + return nil, err + } + vfs.commitPropagationTree(ctx, tree) + return clone, nil } // MountAt creates and mounts a Filesystem configured by the given arguments. @@ -384,10 +643,10 @@ func (vfs *VirtualFilesystem) UmountAt(ctx context.Context, creds *auth.Credenti return linuxerr.EBUSY } } - vdsToDecRef, mountsToDecRef := vfs.umountRecursiveLocked(vd.mount, &umountRecursiveOptions{ + vdsToDecRef, mountsToDecRef := vfs.umountAtRecursiveLocked(ctx, vd, &umountRecursiveOptions{ eager: opts.Flags&linux.MNT_DETACH == 0, disconnectHierarchy: true, - }, nil, nil) + }) vfs.mounts.seq.EndWrite() vfs.mountMu.Unlock() for _, vd := range vdsToDecRef { @@ -399,6 +658,45 @@ func (vfs *VirtualFilesystem) UmountAt(ctx context.Context, creds *auth.Credenti return nil } +// umountAtRecursiveLocked marks the mount located at vd and its decendendents +// as umounted and does the same for mounts at the same dentry in peers of +// vd.mount's parent. +// +// Preconditions: +// - vd is a mountpoint. +// - vfs.mountMu must be locked. +// - vfs.mounts.seq must be in a writer critical section. +// +// +checklocks:vfs.mountMu +func (vfs *VirtualFilesystem) umountAtRecursiveLocked(ctx context.Context, vd VirtualDentry, opts *umountRecursiveOptions) ([]VirtualDentry, []*Mount) { + parent, mountpoint := vd.mount.parent(), vd.mount.point() + vdsToDecRef, mountsToDecRef := vfs.umountRecursiveLocked(vd.mount, opts, nil, nil) + if parent != nil && parent.propType == Shared { + for peer := parent.sharedList.Front(); peer != nil; peer = peer.sharedEntry.Next() { + if peer == parent { + continue + } + + vfs.mounts.seq.EndWrite() + mnt := vfs.mounts.Lookup(peer, mountpoint) + vfs.mounts.seq.BeginWrite() + if mnt == nil { + continue + } + + // From https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt: + // If any peer has some child mounts, then that mount is not unmounted, + // but all other mounts are unmounted. + if len(mnt.children) != 0 { + continue + } + + vdsToDecRef, mountsToDecRef = vfs.umountRecursiveLocked(mnt, opts, vdsToDecRef, mountsToDecRef) + } + } + return vdsToDecRef, mountsToDecRef +} + // +stateify savable type umountRecursiveOptions struct { // If eager is true, ensure that future calls to Mount.tryIncMountedRef() @@ -429,6 +727,8 @@ type umountRecursiveOptions struct { // Preconditions: // - vfs.mountMu must be locked. // - vfs.mounts.seq must be in a writer critical section. +// +// +checklocks:vfs.mountMu func (vfs *VirtualFilesystem) umountRecursiveLocked(mnt *Mount, opts *umountRecursiveOptions, vdsToDecRef []VirtualDentry, mountsToDecRef []*Mount) ([]VirtualDentry, []*Mount) { if !mnt.umounted { mnt.umounted = true @@ -436,6 +736,9 @@ func (vfs *VirtualFilesystem) umountRecursiveLocked(mnt *Mount, opts *umountRecu if parent := mnt.parent(); parent != nil && (opts.disconnectHierarchy || !parent.umounted) { vdsToDecRef = append(vdsToDecRef, vfs.disconnectLocked(mnt)) } + if mnt.propType == Shared { + vfs.setPropagation(mnt, Private) + } } if opts.eager { for { @@ -600,9 +903,9 @@ func (mntns *MountNamespace) DecRef(ctx context.Context) { mntns.MountNamespaceRefs.DecRef(func() { vfs.mountMu.Lock() vfs.mounts.seq.BeginWrite() - vdsToDecRef, mountsToDecRef := vfs.umountRecursiveLocked(mntns.root, &umountRecursiveOptions{ + vdsToDecRef, mountsToDecRef := vfs.umountAtRecursiveLocked(ctx, mntns.Root(), &umountRecursiveOptions{ disconnectHierarchy: true, - }, nil, nil) + }) vfs.mounts.seq.EndWrite() vfs.mountMu.Unlock() for _, vd := range vdsToDecRef { @@ -1062,6 +1365,9 @@ func (vfs *VirtualFilesystem) GenerateProcMountInfo(ctx context.Context, taskRoo fmt.Fprintf(buf, "%s ", opts) // (7) Optional fields: zero or more fields of the form "tag[:value]". + if mnt.propType == Shared { + fmt.Fprintf(buf, "shared:%d ", mnt.groupID) + } // (8) Separator: the end of the optional fields is marked by a single hyphen. fmt.Fprintf(buf, "- ") @@ -1116,3 +1422,26 @@ func superBlockOpts(mountPath string, mnt *Mount) string { return opts } + +// allocateGroupID returns a new mount group id if one is available, and +// error otherwise. If the group ID bitmap is full, double the size of the +// bitmap before allocating the new group id. +// +// +checklocks:vfs.mountMu +func (vfs *VirtualFilesystem) allocateGroupID() (uint32, error) { + groupID, err := vfs.groupIDBitmap.FirstZero(1) + if err != nil { + if err := vfs.groupIDBitmap.Grow(uint32(vfs.groupIDBitmap.Size())); err != nil { + return 0, err + } + } + vfs.groupIDBitmap.Add(groupID) + return groupID, nil +} + +// freeGroupID marks a groupID as available for reuse. +// +// +checklocks:vfs.mountMu +func (vfs *VirtualFilesystem) freeGroupID(id uint32) { + vfs.groupIDBitmap.Remove(id) +} diff --git a/pkg/sentry/vfs/vfs.go b/pkg/sentry/vfs/vfs.go index fd7a4da42..172eab429 100644 --- a/pkg/sentry/vfs/vfs.go +++ b/pkg/sentry/vfs/vfs.go @@ -41,6 +41,7 @@ import ( "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/atomicbitops" + "gvisor.dev/gvisor/pkg/bitmap" "gvisor.dev/gvisor/pkg/context" "gvisor.dev/gvisor/pkg/errors/linuxerr" "gvisor.dev/gvisor/pkg/fspath" @@ -123,6 +124,9 @@ type VirtualFilesystem struct { // filesystemsMu. filesystemsMu sync.Mutex `state:"nosave"` filesystems map[*Filesystem]struct{} + + // groupIDBitmap tracks which mount group IDs are available for allocation. + groupIDBitmap bitmap.Bitmap } // Init initializes a new VirtualFilesystem with no mounts or FilesystemTypes. @@ -138,6 +142,10 @@ func (vfs *VirtualFilesystem) Init(ctx context.Context) error { vfs.filesystems = make(map[*Filesystem]struct{}) vfs.mounts.Init() + vfs.mountMu.Lock() + vfs.groupIDBitmap = bitmap.New(1024) + vfs.mountMu.Unlock() + // Construct vfs.anonMount. anonfsDevMinor, err := vfs.GetAnonBlockDevMinor() if err != nil { diff --git a/test/syscalls/linux/mount.cc b/test/syscalls/linux/mount.cc index 3ce7cd29c..d46dadcde 100644 --- a/test/syscalls/linux/mount.cc +++ b/test/syscalls/linux/mount.cc @@ -34,6 +34,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "absl/strings/match.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_split.h" #include "absl/strings/string_view.h" @@ -791,9 +792,9 @@ TEST(MountTest, SimpleBind) { std::string output; ASSERT_NO_ERRNO(GetContents(dir1_filepath, &output)); - ASSERT_TRUE(output == contents); + ASSERT_EQ(output, contents); ASSERT_NO_ERRNO(GetContents(dir2_filepath, &output)); - ASSERT_TRUE(output == contents); + ASSERT_EQ(output, contents); } TEST(MountTest, BindToSelf) { @@ -804,7 +805,7 @@ TEST(MountTest, BindToSelf) { const std::vector mounts_before = ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountsEntries()); for (const auto& e : mounts_before) { - ASSERT_TRUE(e.mount_point != dir.path()); + ASSERT_NE(e.mount_point, dir.path()); } auto const mount = ASSERT_NO_ERRNO_AND_VALUE( @@ -821,6 +822,540 @@ TEST(MountTest, BindToSelf) { ASSERT_TRUE(found); } +// Tests that it is possible to make a shared mount. +TEST(MountTest, MakeShared) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount("", dir.path().c_str(), "tmpfs", 0, "", 0)); + ASSERT_THAT(mount("", dir.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + + const std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + for (const auto& e : mounts) { + if (e.mount_point == dir.path()) { + EXPECT_TRUE(absl::StrContains(e.optional, "shared:")); + break; + } + } +} + +// Tests that shared mounts have different group IDs. +TEST(MountTest, MakeMultipleShared) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + auto const dir1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const mount1 = + ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir1.path(), "tmpfs", 0, "", 0)); + ASSERT_THAT(mount("", dir1.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + + auto const dir2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const mount2 = + ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir2.path(), "tmpfs", 0, "", 0)); + ASSERT_THAT(mount("", dir2.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + + std::string optional1, optional2; + const std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + for (const auto& e : mounts) { + if (e.mount_point == dir1.path()) { + EXPECT_TRUE(absl::StrContains(e.optional, "shared:")); + optional1 = e.optional; + } else if (e.mount_point == dir2.path()) { + EXPECT_TRUE(absl::StrContains(e.optional, "shared:")); + optional2 = e.optional; + } + } + EXPECT_NE(optional1, optional2); +} + +// Tests that shared mounts reused group IDs from deleted groups. +TEST(MountTest, ReuseGroupIDs) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + auto const dir1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const mount1 = + ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir1.path(), "tmpfs", 0, "", 0)); + ASSERT_THAT(mount("", dir1.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + + auto const dir2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + std::string reused_optional; + { + auto const mount2 = + ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir2.path(), "tmpfs", 0, "", 0)); + ASSERT_THAT(mount("", dir2.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + + const std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + for (const auto& e : mounts) { + if (e.mount_point == dir2.path()) { + EXPECT_TRUE(absl::StrContains(e.optional, "shared:")); + reused_optional = e.optional; + } + } + } + + // Check that created a new shared mount reuses the ID 2. + auto const mount2 = + ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir2.path(), "tmpfs", 0, "", 0)); + ASSERT_THAT(mount("", dir2.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + const std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + for (const auto& e : mounts) { + if (e.mount_point == dir2.path()) { + EXPECT_EQ(e.optional, reused_optional); + break; + } + } +} + +// Tests that a child mount inherits the propagation type of its parent. +TEST(MountTest, InerheritPropagation) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + auto const dir1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const mount1 = + ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir1.path(), "tmpfs", 0, "", 0)); + ASSERT_THAT(mount("", dir1.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + + auto const dir2 = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(dir1.path())); + auto const mount2 = + ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir2.path(), "tmpfs", 0, "", 0)); + + const std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + for (const auto& e : mounts) { + if (e.mount_point == dir2.path()) { + EXPECT_TRUE(absl::StrContains(e.optional, "shared:")); + break; + } + } +} + +// Tests that it is possible to make a mount private again after it is shared. +TEST(MountTest, MakePrivate) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const mnt = + ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir.path(), "tmpfs", 0, "", 0)); + ASSERT_THAT(mount("", dir.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + ASSERT_THAT(mount("", dir.path().c_str(), "", MS_PRIVATE, 0), + SyscallSucceeds()); + + const std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + for (const auto& e : mounts) { + if (e.mount_point == dir.path()) { + EXPECT_EQ(e.optional, ""); + break; + } + } +} + +TEST(MountTest, ArgumentsAreIgnored) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + // These mounts should not fail even though string arguments are passed as + // NULL. + ASSERT_THAT( + mount(dir.path().c_str(), dir.path().c_str(), NULL, MS_BIND, NULL), + SyscallSucceeds()); + ASSERT_THAT(mount(NULL, dir.path().c_str(), NULL, MS_SHARED, NULL), + SyscallSucceeds()); + const std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + for (const auto& e : mounts) { + if (e.mount_point == dir.path()) { + EXPECT_TRUE(absl::StrContains(e.optional, "shared:")); + break; + } + } +} + +TEST(MountTest, MultiplePropagationFlagsFails) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const mnt = + ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir.path(), "tmpfs", 0, "", 0)); + EXPECT_THAT(mount("", dir.path().c_str(), "", MS_SHARED | MS_PRIVATE, 0), + SyscallFailsWithErrno(EINVAL)); +} + +TEST(MountTest, SetMountPropagationOfStackedMounts) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const mnt1 = ASSERT_NO_ERRNO_AND_VALUE( + Mount("", dir.path().c_str(), "tmpfs", 0, "", 0)); + + std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + int parent_mount_id; + for (const auto& e : mounts) { + if (e.mount_point == dir.path()) { + parent_mount_id = e.id; + } + } + // Only the topmost mount on the stack should be shared. + auto const mnt2 = ASSERT_NO_ERRNO_AND_VALUE( + Mount("", dir.path().c_str(), "tmpfs", 0, "", 0)); + ASSERT_THAT(mount("", dir.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + mounts = ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + for (const auto& e : mounts) { + if (e.mount_point == dir.path() && e.id != parent_mount_id) { + EXPECT_TRUE(absl::StrContains(e.optional, "shared:")); + } + if (e.mount_point == dir.path() && e.id == parent_mount_id) { + EXPECT_EQ(e.optional, ""); + } + } +} + +TEST(MountTest, MakePeer) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + auto const dir1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount("", dir1.path().c_str(), "tmpfs", 0, "", 0)); + ASSERT_THAT(mount("", dir1.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + + auto const dir2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + ASSERT_THAT(mount(dir1.path().c_str(), dir2.path().c_str(), "", MS_BIND, 0), + SyscallSucceeds()); + std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + std::string optional1, optional2; + for (const auto& e : mounts) { + if (e.mount_point == dir1.path()) { + EXPECT_TRUE(absl::StrContains(e.optional, "shared:")); + optional1 = e.optional; + } + if (e.mount_point == dir2.path()) { + EXPECT_TRUE(absl::StrContains(e.optional, "shared:")); + optional2 = e.optional; + } + } + ASSERT_EQ(optional1, optional2); +} + +TEST(MountTest, PropagateMountEvent) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + auto const dir1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount("", dir1.path().c_str(), "tmpfs", 0, "", 0)); + auto const child_dir = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(dir1.path())); + ASSERT_THAT(mount("", dir1.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + auto const dir2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + ASSERT_THAT(mount(dir1.path().c_str(), dir2.path().c_str(), "", MS_BIND, 0), + SyscallSucceeds()); + // This mount should propagate to dir2. + auto const child_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount("", child_dir.path().c_str(), "tmpfs", 0, "", 0)); + + const std::string child_path1 = + JoinPath(dir1.path(), Basename(child_dir.path())); + const std::string child_path2 = + JoinPath(dir2.path(), Basename(child_dir.path())); + + std::string child_opt1, child_opt2, parent_optional; + std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + for (const auto& e : mounts) { + if (e.mount_point == child_path1) { + EXPECT_TRUE(absl::StrContains(e.optional, "shared:")); + child_opt1 = e.optional; + } + if (e.mount_point == child_path2) { + EXPECT_TRUE(absl::StrContains(e.optional, "shared:")); + child_opt2 = e.optional; + } + if (e.mount_point == dir1.path() || e.mount_point == dir2.path()) { + EXPECT_TRUE(absl::StrContains(e.optional, "shared:")); + parent_optional = e.optional; + } + } + // Should be in the same peer group. + ASSERT_EQ(child_opt1, child_opt2); + ASSERT_NE(child_opt1, parent_optional); +} + +TEST(MountTest, PropagateUmountEvent) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + auto const dir1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount("", dir1.path().c_str(), "tmpfs", 0, "", 0)); + auto const child_dir = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(dir1.path())); + ASSERT_THAT(mount("", dir1.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + auto const dir2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + ASSERT_THAT(mount(dir1.path().c_str(), dir2.path().c_str(), "", MS_BIND, 0), + SyscallSucceeds()); + // This mount will propagate to dir2. Once the block ends it will be + // unmounted, which should also propagate to dir2. + { + auto const child_mnt = ASSERT_NO_ERRNO_AND_VALUE( + Mount("", child_dir.path().c_str(), "tmpfs", 0, "", 0)); + } + + const std::string child_path1 = + JoinPath(dir1.path(), Basename(child_dir.path())); + const std::string child_path2 = + JoinPath(dir2.path(), Basename(child_dir.path())); + + std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + for (const auto& e : mounts) { + ASSERT_NE(e.mount_point, child_path1); + ASSERT_NE(e.mount_point, child_path2); + } +} + +TEST(MountTest, UmountIgnoresPeersWithChildren) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + auto const dir1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + ASSERT_THAT(mount("", dir1.path().c_str(), "tmpfs", 0, ""), + SyscallSucceeds()); + ASSERT_THAT(mount("", dir1.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + auto const dir2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + ASSERT_THAT(mount(dir1.path().c_str(), dir2.path().c_str(), "", MS_BIND, 0), + SyscallSucceeds()); + + auto const child_dir = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(dir1.path())); + ASSERT_THAT(mount("", child_dir.path().c_str(), "tmpfs", 0, ""), + SyscallSucceeds()); + auto const grandchild_dir = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(child_dir.path())); + ASSERT_THAT(mount("", grandchild_dir.path().c_str(), "tmpfs", 0, ""), + SyscallSucceeds()); + + const std::string child_path1 = + JoinPath(dir1.path(), Basename(child_dir.path())); + const std::string child_path2 = + JoinPath(dir2.path(), Basename(child_dir.path())); + ASSERT_THAT(mount("", child_path2.c_str(), "", MS_PRIVATE, 0), + SyscallSucceeds()); + const std::string grandchild_path2 = + JoinPath(child_path2, Basename(grandchild_dir.path())); + ASSERT_THAT(umount2(grandchild_path2.c_str(), MNT_DETACH), SyscallSucceeds()); + + // This umount event should not propagate to the peer at dir1 because its + // child mount still has its own child mount. + ASSERT_THAT(umount2(child_path2.c_str(), MNT_DETACH), SyscallSucceeds()); + std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + bool found = false; + for (const auto& e : mounts) { + ASSERT_NE(e.mount_point, child_path2); + if (e.mount_point == child_path1) { + found = true; + break; + } + } + ASSERT_TRUE(found); +} + +TEST(MountTest, BindSharedOnShared) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + auto const dir1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const dir2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const dir3 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const dir4 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + // Dir 1 and 2 are part of peer group 'A', dir 3 and 4 are part of peer group + // 'B'. + ASSERT_THAT(mount("", dir1.path().c_str(), "tmpfs", 0, ""), + SyscallSucceeds()); + auto const dir5 = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(dir1.path())); + ASSERT_THAT(mount("", dir1.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + ASSERT_THAT(mount(dir1.path().c_str(), dir2.path().c_str(), "", MS_BIND, 0), + SyscallSucceeds()); + ASSERT_THAT(mount("", dir3.path().c_str(), "tmpfs", 0, ""), + SyscallSucceeds()); + ASSERT_THAT(mount("", dir3.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + ASSERT_THAT(mount(dir3.path().c_str(), dir4.path().c_str(), "", MS_BIND, 0), + SyscallSucceeds()); + + const std::string dir5_path2 = JoinPath(dir2.path(), Basename(dir5.path())); + + // Bind peer group 'A' to peer group 'B'. + ASSERT_THAT(mount(dir4.path().c_str(), dir5.path().c_str(), "", MS_BIND, 0), + SyscallSucceeds()); + + std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + // The new mounts should all be peers with the old ones. + // Optional string should be in the format shared:x. + + std::string opt1, opt2, opt3, opt4; + for (const auto& e : mounts) { + if (e.mount_point == dir3.path()) { + opt1 = e.optional; + } + if (e.mount_point == dir4.path()) { + opt2 = e.optional; + } + if (e.mount_point == dir5.path()) { + opt3 = e.optional; + } + if (e.mount_point == dir5_path2) { + opt4 = e.optional; + } + } + ASSERT_EQ(opt1, opt2); + ASSERT_EQ(opt2, opt3); + ASSERT_EQ(opt3, opt4); + ASSERT_TRUE(absl::StrContains(opt1, "shared:")); +} + +TEST(MountTest, BindSharedOnPrivate) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + auto const dir1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + ASSERT_THAT(mount("", dir1.path().c_str(), "tmpfs", 0, ""), + SyscallSucceeds()); + auto const dir2 = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(dir1.path())); + auto const dir3 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + auto const dir4 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + ASSERT_THAT(mount("", dir3.path().c_str(), "tmpfs", 0, ""), + SyscallSucceeds()); + ASSERT_THAT(mount("", dir3.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + ASSERT_THAT(mount(dir3.path().c_str(), dir4.path().c_str(), "", MS_BIND, 0), + SyscallSucceeds()); + + // bind to private mount. + ASSERT_THAT(mount(dir3.path().c_str(), dir2.path().c_str(), "", MS_BIND, 0), + SyscallSucceeds()); + + std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + std::string opt1, opt2, opt3; + for (const auto& e : mounts) { + if (e.mount_point == dir1.path()) { + ASSERT_EQ(e.optional, ""); + } + if (e.mount_point == dir2.path()) { + opt1 = e.optional; + ASSERT_TRUE(absl::StrContains(e.optional, "shared:")); + } + if (e.mount_point == dir3.path()) { + opt2 = e.optional; + ASSERT_TRUE(absl::StrContains(e.optional, "shared:")); + } + if (e.mount_point == dir4.path()) { + opt3 = e.optional; + ASSERT_TRUE(absl::StrContains(e.optional, "shared:")); + } + } + ASSERT_EQ(opt1, opt2); + ASSERT_EQ(opt2, opt3); +} + +TEST(MountTest, BindPeerGroupsWithChildren) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + auto const dir1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + ASSERT_THAT(mount("", dir1.path().c_str(), "tmpfs", 0, ""), + SyscallSucceeds()); + ASSERT_THAT(mount("", dir1.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + auto const dir2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + ASSERT_THAT(mount("", dir2.path().c_str(), "tmpfs", 0, ""), + SyscallSucceeds()); + ASSERT_THAT(mount("", dir2.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + // dir3 and dir4 are child mounts of dir1. + auto const dir3 = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(dir1.path())); + ASSERT_THAT(mount("", dir3.path().c_str(), "tmpfs", 0, ""), + SyscallSucceeds()); + auto const dir4 = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(dir1.path())); + ASSERT_THAT(mount("", dir4.path().c_str(), "tmpfs", 0, ""), + SyscallSucceeds()); + ASSERT_THAT(mount(dir1.path().c_str(), dir2.path().c_str(), "", MS_BIND, 0), + SyscallSucceeds()); + + const std::string dir3_path2 = JoinPath(dir2.path(), Basename(dir3.path())); + const std::string dir4_path2 = JoinPath(dir2.path(), Basename(dir4.path())); + + std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + std::string opt1, opt2, opt3; + for (const auto& e : mounts) { + if (e.mount_point == dir1.path()) { + ASSERT_TRUE(absl::StrContains(e.optional, "shared:")); + opt1 = e.optional; + } + if (e.mount_point == dir3.path()) { + ASSERT_TRUE(absl::StrContains(e.optional, "shared:")); + opt2 = e.optional; + } + if (e.mount_point == dir4.path()) { + ASSERT_TRUE(absl::StrContains(e.optional, "shared:")); + opt3 = e.optional; + } + ASSERT_NE(e.mount_point, dir3_path2); + ASSERT_NE(e.mount_point, dir4_path2); + } + ASSERT_NE(opt1, opt2); + ASSERT_NE(opt2, opt3); + ASSERT_NE(opt3, opt1); +} + +TEST(MountTest, BindParentToChild) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + auto const dir1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + ASSERT_THAT(mount(dir1.path().c_str(), dir1.path().c_str(), "", MS_BIND, ""), + SyscallSucceeds()); + ASSERT_THAT(mount("", dir1.path().c_str(), "", MS_SHARED, 0), + SyscallSucceeds()); + auto const dir2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + ASSERT_THAT(mount(dir1.path().c_str(), dir2.path().c_str(), "", MS_BIND, ""), + SyscallSucceeds()); + auto const child_dir = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(dir1.path())); + ASSERT_THAT( + mount(dir1.path().c_str(), child_dir.path().c_str(), "", MS_BIND, ""), + SyscallSucceeds()); + + std::vector mounts = + ASSERT_NO_ERRNO_AND_VALUE(ProcSelfMountInfoEntries()); + std::string opt1, opt2, opt3; + for (const auto& e : mounts) { + if (e.mount_point == dir1.path()) { + opt1 = e.optional; + } + if (e.mount_point == dir2.path()) { + opt2 = e.optional; + } + if (e.mount_point == child_dir.path()) { + opt3 = e.optional; + } + } + ASSERT_TRUE(absl::StrContains(opt1, "shared:")); + ASSERT_EQ(opt1, opt2); + ASSERT_EQ(opt2, opt3); +} + } // namespace } // namespace testing