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