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