mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
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
This commit is contained in:
committed by
gVisor bot
parent
ec44093c97
commit
7bc837137c
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user