From 7bc837137cff470e384d8d4e879990c5661879ba Mon Sep 17 00:00:00 2001 From: Shambhavi Srivastava Date: Mon, 18 Apr 2022 18:48:19 -0700 Subject: [PATCH] Enable tmpfs size mount option. When tmpfs is mounted with `size` option it imposes a limit for the tmpfs mount. The total size of all files in that mount must not exceed this limit. See tmpfs(5) for more details. When this option was not supported and specifying it caused mount(2) to return EINVAL instead of ENOSPC. In Linux, tmpfs charges symlinks, regular files and directories against this `size` limit. All accounting is done on the granularity of page size. The `size` limit and all individual file sizes are rounded up to number of pages while performing calculations. gVisor has aimed to replicate the same behavior. One difference from Linux is that if the `size` option is not specified, then Linux uses 50% of physical RAM as the `size`. We don't replicate this behavior in gVisor as the actual size of host physical RAM should not be exposed to application. In gVisor, if `size` option is not specified, then no limit is imposed. This is consistent with the behavior before this change. Added syscall tests for regular files and symlink. PiperOrigin-RevId: 442686814 --- pkg/hostarch/addr.go | 10 ++++ pkg/sentry/fsimpl/tmpfs/filesystem.go | 76 ++++++++++++++++++++++++- pkg/sentry/fsimpl/tmpfs/regular_file.go | 24 +++++++- pkg/sentry/fsimpl/tmpfs/tmpfs.go | 74 ++++++++++++++++++------ test/syscalls/linux/mount.cc | 70 ++++++++++++++++++++--- 5 files changed, 228 insertions(+), 26 deletions(-) diff --git a/pkg/hostarch/addr.go b/pkg/hostarch/addr.go index 0cf0f3c81..cb2e11637 100644 --- a/pkg/hostarch/addr.go +++ b/pkg/hostarch/addr.go @@ -123,3 +123,13 @@ func PageRoundUp(x uint64) (addr uint64, ok bool) { ok = addr >= x return } + +// ToPages returns number of Pages for x. +// x is rounded up to the nearest page boundary. +func ToPages(x uint64) (uint64, bool) { + xRoundedUp, ok := PageRoundUp(x) + if !ok { + return 0, false + } + return xRoundedUp / PageSize, true +} diff --git a/pkg/sentry/fsimpl/tmpfs/filesystem.go b/pkg/sentry/fsimpl/tmpfs/filesystem.go index 0b093cfb3..5fff5709e 100644 --- a/pkg/sentry/fsimpl/tmpfs/filesystem.go +++ b/pkg/sentry/fsimpl/tmpfs/filesystem.go @@ -22,12 +22,23 @@ import ( "gvisor.dev/gvisor/pkg/context" "gvisor.dev/gvisor/pkg/errors/linuxerr" "gvisor.dev/gvisor/pkg/fspath" + "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/sentry/fsmetric" "gvisor.dev/gvisor/pkg/sentry/kernel/auth" "gvisor.dev/gvisor/pkg/sentry/socket/unix/transport" "gvisor.dev/gvisor/pkg/sentry/vfs" ) +const ( + // direntSize is the size of each directory entry + // that Linux uses for computing directory size. + // "20" is mm/shmem.c:BOGO_DIRENT_SIZE. + direntSize = 20 + // Linux implementation uses a SHORT_SYMLINK_LEN 128. + // It accounts size for only SYMLINK with size >= 128. + shortSymlinkLen = 128 +) + // Sync implements vfs.FilesystemImpl.Sync. func (fs *filesystem) Sync(ctx context.Context) error { // All filesystem state is in-memory. @@ -733,12 +744,17 @@ func (fs *filesystem) StatFSAt(ctx context.Context, rp *vfs.ResolvingPath) (linu if _, err := resolveLocked(ctx, rp); err != nil { return linux.Statfs{}, err } - return globalStatfs, nil + return fs.statFS(), nil } // SymlinkAt implements vfs.FilesystemImpl.SymlinkAt. func (fs *filesystem) SymlinkAt(ctx context.Context, rp *vfs.ResolvingPath, target string) error { return fs.doCreateAt(ctx, rp, false /* dir */, func(parentDir *directory, name string) error { + if len(target) >= shortSymlinkLen { + if err := fs.updatePagesUsed(0, uint64(len(target))); err != nil { + return err + } + } creds := rp.Credentials() child := fs.newDentry(fs.newSymlink(creds.EffectiveKUID, creds.EffectiveKGID, 0777, target, parentDir)) parentDir.insertChildLocked(child, name) @@ -785,12 +801,29 @@ func (fs *filesystem) UnlinkAt(ctx context.Context, rp *vfs.ResolvingPath) error if err := vfsObj.PrepareDeleteDentry(mntns, &child.vfsd); err != nil { return err } + // Remove pages used if child being removed is a SymLink or Regular File. + switch impl := child.inode.impl.(type) { + case *symlink: + if len(impl.target) >= shortSymlinkLen { + if err := fs.updatePagesUsed(uint64(len(impl.target)), 0); err != nil { + vfsObj.AbortDeleteDentry(&child.vfsd) + return err + } + } + case *regularFile: + impl.inode.mu.Lock() + if err := fs.updatePagesUsed(impl.size.Load(), 0); err != nil { + impl.inode.mu.Unlock() + vfsObj.AbortDeleteDentry(&child.vfsd) + return err + } + impl.inode.mu.Unlock() + } // Generate inotify events. Note that this must take place before the link // count of the child is decremented, or else the watches may be dropped // before these events are added. vfs.InotifyRemoveChild(ctx, &child.inode.watches, &parentDir.inode.watches, name) - parentDir.removeChildLocked(child) child.inode.decLinksLocked(ctx) vfsObj.CommitDeleteDentry(ctx, &child.vfsd) @@ -916,3 +949,42 @@ func (fs *filesystem) PrependPath(ctx context.Context, vfsroot, vd vfs.VirtualDe func (fs *filesystem) MountOptions() string { return fs.mopts } + +// updatePagesUsed updates the pagesUsed in filesystem struct +// if tmpfs is mounted with size option. +// Assumption: for all the int conversions overflow never occurs. +func (fs *filesystem) updatePagesUsed(oldFileSize, newFileSize uint64) error { + if fs.maxSizeInPages == 0 { + return nil + } + oldFileSizePages, ok := hostarch.ToPages(oldFileSize) + if !ok { + return linuxerr.EINVAL + } + newFileSizePages, ok := hostarch.ToPages(newFileSize) + if !ok { + return linuxerr.EINVAL + } + + pagesDelta := int64(newFileSizePages) - int64(oldFileSizePages) + if pagesDelta == 0 { + // No update required. + return nil + } + // Need to acquire fs.pagesUsedMu for fs.pagesUsed. + fs.pagesUsedMu.Lock() + defer fs.pagesUsedMu.Unlock() + pagesFree := fs.maxSizeInPages - fs.pagesUsed + + if int64(pagesFree) < pagesDelta { + return linuxerr.ENOSPC + } + + newPagesReqd := int64(fs.pagesUsed) + pagesDelta + if newPagesReqd < 0 { + panic("Deallocating more pages than allocated.") + } + + fs.pagesUsed = uint64(newPagesReqd) + return nil +} diff --git a/pkg/sentry/fsimpl/tmpfs/regular_file.go b/pkg/sentry/fsimpl/tmpfs/regular_file.go index ea97e8cac..ef2fc073d 100644 --- a/pkg/sentry/fsimpl/tmpfs/regular_file.go +++ b/pkg/sentry/fsimpl/tmpfs/regular_file.go @@ -139,6 +139,11 @@ func newUnlinkedRegularFileDescription(ctx context.Context, creds *auth.Credenti // Preconditions: mount must be a tmpfs mount. func NewZeroFile(ctx context.Context, creds *auth.Credentials, mount *vfs.Mount, size uint64) (*vfs.FileDescription, error) { // Compare mm/shmem.c:shmem_zero_setup(). + fs := mount.Filesystem().Impl().(*filesystem) + if err := fs.updatePagesUsed(0, size); err != nil { + return nil, err + } + fd, err := newUnlinkedRegularFileDescription(ctx, creds, mount, "dev/zero") if err != nil { return nil, err @@ -189,6 +194,10 @@ func (rf *regularFile) truncateLocked(newSize uint64) (bool, error) { return false, linuxerr.EPERM } // We only need to update the file size. + if err := rf.inode.fs.updatePagesUsed(rf.size.Load(), newSize); err != nil { + rf.dataMu.Unlock() + return false, err + } rf.size.Store(newSize) rf.dataMu.Unlock() return true, nil @@ -201,6 +210,10 @@ func (rf *regularFile) truncateLocked(newSize uint64) (bool, error) { } // Update the file size. + if err := rf.inode.fs.updatePagesUsed(rf.size.Load(), newSize); err != nil { + rf.dataMu.Unlock() + return false, err + } rf.size.Store(newSize) rf.dataMu.Unlock() @@ -441,9 +454,18 @@ func (fd *regularFileFD) pwrite(ctx context.Context, src usermem.IOSequence, off return 0, offset, err } src = src.TakeFirst64(srclen) - + reservedSize := f.size.Load() + uint64(srclen) + if err = f.inode.fs.updatePagesUsed(f.size.Load(), reservedSize); err != nil { + return 0, 0, err + } rw := getRegularFileReadWriter(f, offset) n, err := src.CopyInTo(ctx, rw) + if unwritten := srclen - n; unwritten != 0 { + if err := f.inode.fs.updatePagesUsed(reservedSize, f.size.Load()); err != nil { + return 0, 0, err + } + } + f.inode.touchCMtimeLocked() for { old := atomic.LoadUint32(&f.inode.mode) diff --git a/pkg/sentry/fsimpl/tmpfs/tmpfs.go b/pkg/sentry/fsimpl/tmpfs/tmpfs.go index d64046f45..deeac78e0 100644 --- a/pkg/sentry/fsimpl/tmpfs/tmpfs.go +++ b/pkg/sentry/fsimpl/tmpfs/tmpfs.go @@ -24,6 +24,7 @@ // regularFile.mapsMu // *** "memmap.Mappable locks taken by Translate" below this point // regularFile.dataMu +// fs.pagesUsedMu // directory.iterMu package tmpfs @@ -88,6 +89,15 @@ type filesystem struct { root *dentry maxFilenameLen int + + // maxSizeInPages is the maximum permissible size for the tmpfs in terms of pages. + // This field is immutable. + maxSizeInPages uint64 + + // pagesUsed is the pages used out of the tmpfs size. + // pagesUsed is protected by pagesUsedMu. + pagesUsedMu sync.Mutex `state:"nosave"` + pagesUsed uint64 } // Name implements vfs.FilesystemType.Name. @@ -189,6 +199,24 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt } rootKGID = kgid } + maxSizeStr, ok := mopts["size"] + var maxSizeInPages uint64 + if ok { + delete(mopts, "size") + maxSizeInBytes, err := strconv.ParseUint(maxSizeStr, 10, 64) + if err != nil { + ctx.Warningf("tmpfs.FilesystemType.GetFilesystem: invalid size: %q", maxSizeStr) + return nil, nil, linuxerr.EINVAL + } + // Convert size in bytes to nearest Page Size bytes + // as Linux allocates memory in terms of Page size. + maxSizeInPages, ok = hostarch.ToPages(maxSizeInBytes) + if !ok { + ctx.Warningf("tmpfs.FilesystemType.GetFilesystem: Pages RoundUp Overflow error: %q", ok) + return nil, nil, linuxerr.EINVAL + } + } + if len(mopts) != 0 { ctx.Warningf("tmpfs.FilesystemType.GetFilesystem: unknown options: %v", mopts) return nil, nil, linuxerr.EINVAL @@ -210,6 +238,7 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt mopts: opts.Data, usage: memUsage, maxFilenameLen: linux.NAME_MAX, + maxSizeInPages: maxSizeInPages, } fs.vfsfs.Init(vfsObj, newFSType, &fs) if tmpfsOptsOk && tmpfsOpts.MaxFilenameLen > 0 { @@ -273,23 +302,37 @@ func (d *dentry) releaseChildrenLocked(ctx context.Context) { } } -// immutable -var globalStatfs = linux.Statfs{ - Type: linux.TMPFS_MAGIC, - BlockSize: hostarch.PageSize, - FragmentSize: hostarch.PageSize, - NameLength: linux.NAME_MAX, +func (fs *filesystem) statFS() linux.Statfs { + st := linux.Statfs{ + Type: linux.TMPFS_MAGIC, + BlockSize: hostarch.PageSize, + FragmentSize: hostarch.PageSize, + NameLength: linux.NAME_MAX, + } - // tmpfs currently does not support configurable size limits. In Linux, - // such a tmpfs mount will return f_blocks == f_bfree == f_bavail == 0 from - // statfs(2). However, many applications treat this as having a size limit + // tmpfs supports configurable size limits. + // In Linux, if tmpfs is mounted with size option, + // we return the block sizes as set by the user. + if fs.maxSizeInPages > 0 { + // If size is set for tmpfs return set values. + st.Blocks = fs.maxSizeInPages + fs.pagesUsedMu.Lock() + defer fs.pagesUsedMu.Unlock() + st.BlocksFree = fs.maxSizeInPages - fs.pagesUsed + st.BlocksAvailable = fs.maxSizeInPages - fs.pagesUsed + return st + } + // In Linux, if tmpfs is mounted with no size option, + // such a tmpfs mount will return + // f_blocks == f_bfree == f_bavail == 0 from statfs(2). + // However, many applications treat this as having a size limit // of 0. To work around this, claim to have a very large but non-zero size, // chosen to ensure that BlockSize * Blocks does not overflow int64 (which // applications may also handle incorrectly). - // TODO(b/29637826): allow configuring a tmpfs size and enforce it. - Blocks: math.MaxInt64 / hostarch.PageSize, - BlocksFree: math.MaxInt64 / hostarch.PageSize, - BlocksAvailable: math.MaxInt64 / hostarch.PageSize, + st.Blocks = math.MaxInt64 / hostarch.PageSize + st.BlocksFree = math.MaxInt64 / hostarch.PageSize + st.BlocksAvailable = math.MaxInt64 / hostarch.PageSize + return st } // dentry implements vfs.DentryImpl. @@ -528,8 +571,7 @@ func (i *inode) statTo(stat *linux.Statx) { // too expensive to compute here. Cache it in regularFile. stat.Blocks = allocatedBlocksForSize(stat.Size) case *directory: - // "20" is mm/shmem.c:BOGO_DIRENT_SIZE. - stat.Size = 20 * (2 + uint64(impl.numChildren.Load())) + stat.Size = direntSize * (2 + uint64(impl.numChildren.Load())) // stat.Blocks is 0. case *symlink: stat.Size = uint64(len(impl.target)) @@ -845,7 +887,7 @@ func (fd *fileDescription) SetStat(ctx context.Context, opts vfs.SetStatOptions) // StatFS implements vfs.FileDescriptionImpl.StatFS. func (fd *fileDescription) StatFS(ctx context.Context) (linux.Statfs, error) { - return globalStatfs, nil + return fd.filesystem().statFS(), nil } // ListXattr implements vfs.FileDescriptionImpl.ListXattr. diff --git a/test/syscalls/linux/mount.cc b/test/syscalls/linux/mount.cc index 668c0499f..36a90ad93 100644 --- a/test/syscalls/linux/mount.cc +++ b/test/syscalls/linux/mount.cc @@ -443,10 +443,8 @@ TEST(MountTest, MountInfo) { } } -// TODO(b/29637826): Enable this test on gVisor once tmpfs supports size option. TEST(MountTest, TmpfsSizeRoundUpSinglePageSize) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)) || - IsRunningOnGvisor()); + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); auto tmpfs_size_opt = absl::StrCat("size=", kPageSize / 2); auto const mount = ASSERT_NO_ERRNO_AND_VALUE( @@ -472,8 +470,7 @@ TEST(MountTest, TmpfsSizeRoundUpSinglePageSize) { } TEST(MountTest, TmpfsSizeRoundUpMultiplePages) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)) || - IsRunningOnGvisor()); + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); auto page_multiple = 2; auto size = kPageSize * page_multiple; @@ -501,8 +498,7 @@ TEST(MountTest, TmpfsSizeRoundUpMultiplePages) { } TEST(MountTest, TmpfsSizeMoreThanSinglePgSZMultipleFiles) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)) || - IsRunningOnGvisor()); + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); auto const page_multiple = 10; auto const size = kPageSize * page_multiple; @@ -526,6 +522,66 @@ TEST(MountTest, TmpfsSizeMoreThanSinglePgSZMultipleFiles) { ASSERT_THAT(fallocate(fd.get(), 0, 0, kPageSize), SyscallFailsWithErrno(ENOSPC)); } + +// Test shows directory does not take up any pages. +TEST(MountTest, TmpfsDirectoryAllocCheck) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + auto const dir_parent = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + + auto tmpfs_size_opt = absl::StrCat("size=", kPageSize); + auto const mount = ASSERT_NO_ERRNO_AND_VALUE( + Mount("", dir_parent.path(), "tmpfs", 0, tmpfs_size_opt, 0)); + + auto const dir_tmp = + ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(dir_parent.path())); + + // Creating only 1 regular file allocates 1 page size. + auto fd = ASSERT_NO_ERRNO_AND_VALUE( + Open(JoinPath(dir_parent.path(), "foo"), O_CREAT | O_RDWR, 0777)); + + // Check that it starts at size zero. + struct stat buf; + ASSERT_THAT(fstat(fd.get(), &buf), SyscallSucceeds()); + EXPECT_EQ(buf.st_size, 0); + + // Grow to 1 Page Size. + ASSERT_THAT(fallocate(fd.get(), 0, 0, kPageSize), SyscallSucceeds()); + ASSERT_THAT(fstat(fd.get(), &buf), SyscallSucceeds()); + EXPECT_EQ(buf.st_size, kPageSize); + + // Grow to beyond 1 Page Size. + ASSERT_THAT(fallocate(fd.get(), 0, 0, kPageSize + 1), + SyscallFailsWithErrno(ENOSPC)); +} + +// Tests memory allocation for symlinks. +TEST(MountTest, TmpfsSymlinkAllocCheck) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + auto const dir_parent = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + + auto tmpfs_size_opt = absl::StrCat("size=", kPageSize); + auto const mount = ASSERT_NO_ERRNO_AND_VALUE( + Mount("", dir_parent.path(), "tmpfs", 0, tmpfs_size_opt, 0)); + + const int target_size = 128; + auto target = std::string(target_size - 1, 'a'); + auto pathname = JoinPath(dir_parent.path(), "foo1"); + EXPECT_THAT(symlink(target.c_str(), pathname.c_str()), SyscallSucceeds()); + + target = std::string(target_size, 'a'); + pathname = absl::StrCat(dir_parent.path(), "/foo2"); + EXPECT_THAT(symlink(target.c_str(), pathname.c_str()), SyscallSucceeds()); + + target = std::string(target_size, 'a'); + pathname = absl::StrCat(dir_parent.path(), "/foo3"); + EXPECT_THAT(symlink(target.c_str(), pathname.c_str()), + SyscallFailsWithErrno(ENOSPC)); + + target = std::string(target_size - 1, 'a'); + pathname = absl::StrCat(dir_parent.path(), "/foo4"); + EXPECT_THAT(symlink(target.c_str(), pathname.c_str()), SyscallSucceeds()); +} + } // namespace } // namespace testing