Impose default tmpfs size limits correctly.

Syzkaller came up with workloads that fallocate(2) 1 TB in /tmp. The host
mlock(2) or madvise(2) syscalls on memfd(2) files end up hanging for multiple
minutes in such situations causing the watchdog to mark the calling goroutine
as stuck. memfd(2) files have not size limits.

Linux fails such fallocate(2) attempts in /tmp with ENOSPC.
In Linux tmpfs (shmem), when size= mount option is not specified, the default
size limit for the mount is set to 50% of physical RAM size. But in gVisor, it
is set to MaxInt64. Which is why Linux fails with ENOSPC and gVisor doesn't.

In runsc, the physcial RAM size is already exposed to the containerized
application via `/proc/meminfo` which uses `usage.*TotalMemoryBytes`. These
fields are configured using the `MemTotal:` field from host `/proc/meminfo`.
So use that information to set the default size limit correctly.

Reported-by: syzbot+4aa3d6d42b063a11c850@syzkaller.appspotmail.com
PiperOrigin-RevId: 548252095
This commit is contained in:
Ayush Ranjan
2023-07-14 16:44:25 -07:00
committed by gVisor bot
parent e7bd1b4c9c
commit e54e3668b0
6 changed files with 58 additions and 16 deletions
+29 -14
View File
@@ -140,6 +140,32 @@ type FilesystemOpts struct {
// FilestoreFD is the FD for the memory file that will be used to store file
// data. If this is nil, then MemoryFileProviderFromContext() is used.
FilestoreFD *fd.FD
// DisableDefaultSizeLimit disables setting a default size limit. In Linux,
// SB_KERNMOUNT has this effect on tmpfs mounts; see mm/shmem.c:shmem_fill_super().
DisableDefaultSizeLimit bool
}
// Default size limit mount option. It is immutable after initialization.
var defaultSizeLimit uint64
// SetDefaultSizeLimit configures the size limit to be used for tmpfs mounts
// that do not specify a size= mount option. This must be called only once,
// before any tmpfs filesystems are created.
func SetDefaultSizeLimit(sizeLimit uint64) {
defaultSizeLimit = sizeLimit
}
func getDefaultSizeLimit(disable bool) uint64 {
if disable || defaultSizeLimit == 0 {
// The size limit is used to populate statfs(2) results. If Linux tmpfs is
// mounted with no size option, then statfs(2) returns f_blocks == f_bfree
// == f_bavail == 0. However, many applications treat this as having a size
// limit of 0. To work around this, return a very large but non-zero size
// limit, chosen to ensure that it does not overflow int64.
return math.MaxInt64
}
return defaultSizeLimit
}
// GetFilesystem implements vfs.FilesystemType.GetFilesystem.
@@ -152,6 +178,7 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt
privateMF := false
rootFileType := uint16(linux.S_IFDIR)
disableDefaultSizeLimit := false
newFSType := vfs.FilesystemType(&fstype)
tmpfsOpts, tmpfsOptsOk := opts.InternalData.(FilesystemOpts)
if tmpfsOptsOk {
@@ -161,6 +188,7 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt
if tmpfsOpts.FilesystemType != nil {
newFSType = tmpfsOpts.FilesystemType
}
disableDefaultSizeLimit = tmpfsOpts.DisableDefaultSizeLimit
if tmpfsOpts.FilestoreFD != nil {
mfOpts := pgalloc.MemoryFileOpts{
// tmpfsOpts.FilestoreFD may be backed by a file on disk (not memfd),
@@ -231,15 +259,7 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt
}
rootKGID = kgid
}
// In Linux, the default size limit is set to 50% of physical RAM. Such a
// default is not suitable in gVisor, because we don't want to reveal the
// host's RAM size. If Linux tmpfs is mounted with no size option, then
// statfs(2) returns f_blocks == f_bfree == f_bavail == 0.
// However, many applications treat this as having a size limit
// of 0. To work around these, set a very large but non-zero default size
// limit, chosen to ensure that BlockSize * Blocks does not overflow int64
// (which applications may also handle incorrectly).
maxSizeInPages := uint64(math.MaxInt64 / hostarch.PageSize)
maxSizeInPages := getDefaultSizeLimit(disableDefaultSizeLimit) / hostarch.PageSize
maxSizeStr, ok := mopts["size"]
if ok {
delete(mopts, "size")
@@ -303,11 +323,6 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt
return &fs.vfsfs, &root.vfsd, nil
}
// NewFilesystem returns a new tmpfs filesystem.
func NewFilesystem(ctx context.Context, vfsObj *vfs.VirtualFilesystem, creds *auth.Credentials) (*vfs.Filesystem, *vfs.Dentry, error) {
return FilesystemType{}.GetFilesystem(ctx, vfsObj, creds, "", vfs.GetFilesystemOptions{})
}
// Release implements vfs.FilesystemImpl.Release.
func (fs *filesystem) Release(ctx context.Context) {
fs.vfsfs.VirtualFilesystem().PutAnonBlockDevMinor(fs.devMinor)
+9 -1
View File
@@ -439,7 +439,15 @@ func (k *Kernel) Init(args InitKernelArgs) error {
pipeMount := k.vfs.NewDisconnectedMount(pipeFilesystem, nil, &vfs.MountOptions{})
k.pipeMount = pipeMount
tmpfsFilesystem, tmpfsRoot, err := tmpfs.NewFilesystem(ctx, &k.vfs, auth.NewRootCredentials(k.rootUserNamespace))
tmpfsOpts := vfs.GetFilesystemOptions{
InternalData: tmpfs.FilesystemOpts{
// See mm/shmem.c:shmem_init() => vfs_kern_mount(flags=SB_KERNMOUNT).
// Note how mm/shmem.c:shmem_fill_super() does not provide a default
// value for sbinfo->max_blocks when SB_KERNMOUNT is set.
DisableDefaultSizeLimit: true,
},
}
tmpfsFilesystem, tmpfsRoot, err := tmpfs.FilesystemType{}.GetFilesystem(ctx, &k.vfs, auth.NewRootCredentials(k.rootUserNamespace), "", tmpfsOpts)
if err != nil {
return fmt.Errorf("failed to create tmpfs filesystem: %v", err)
}
+9
View File
@@ -40,6 +40,7 @@ import (
"gvisor.dev/gvisor/pkg/sentry/control"
"gvisor.dev/gvisor/pkg/sentry/fdimport"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/host"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/tmpfs"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/user"
"gvisor.dev/gvisor/pkg/sentry/inet"
"gvisor.dev/gvisor/pkg/sentry/kernel"
@@ -260,6 +261,8 @@ type Args struct {
// TotalMem is the initial amount of total memory to report back to the
// container.
TotalMem uint64
// TotalHostMem is the total memory reported by host /proc/meminfo.
TotalHostMem uint64
// UserLogFD is the file descriptor to write user logs to.
UserLogFD int
// ProductName is the value to show in
@@ -412,6 +415,12 @@ func New(args Args) (*Loader, error) {
log.Infof("CPUs: %d", args.NumCPU)
runtime.GOMAXPROCS(args.NumCPU)
if args.TotalHostMem > 0 {
// As per tmpfs(5), the default size limit is 50% of total physical RAM.
// See mm/shmem.c:shmem_default_max_blocks().
tmpfs.SetDefaultSizeLimit(args.TotalHostMem / 2)
}
if args.TotalMem > 0 {
// Adjust the total memory returned by the Sentry so that applications that
// use /proc/meminfo can make allocations based on this limit.
+3
View File
@@ -550,6 +550,9 @@ func (c *containerMounter) configureOverlay(ctx context.Context, conf *config.Co
tmpfsOpts := tmpfs.FilesystemOpts{
RootFileType: uint16(rootType),
FilestoreFD: filestoreFD,
// If a mount is being overlaid, it should not be limited by the default
// tmpfs size limit.
DisableDefaultSizeLimit: true,
}
upperOpts.GetFilesystemOptions.InternalData = tmpfsOpts
upper, err := c.k.VFS().MountDisconnected(ctx, creds, "" /* source */, tmpfs.Name, &upperOpts)
+5
View File
@@ -113,6 +113,9 @@ type Boot struct {
// container.
totalMem uint64
// totalHostMem is the total memory reported by host /proc/meminfo.
totalHostMem uint64
// userLogFD is the file descriptor to write user logs to.
userLogFD int
@@ -178,6 +181,7 @@ func (b *Boot) SetFlags(f *flag.FlagSet) {
f.IntVar(&b.procMountSyncFD, "proc-mount-sync-fd", -1, "file descriptor that has to be written to when /proc isn't needed anymore and can be unmounted")
f.IntVar(&b.syncUsernsFD, "sync-userns-fd", -1, "file descriptor used to synchronize rootless user namespace initialization.")
f.Uint64Var(&b.totalMem, "total-memory", 0, "sets the initial amount of total memory to report back to the container")
f.Uint64Var(&b.totalHostMem, "total-host-memory", 0, "total memory reported by host /proc/meminfo")
f.BoolVar(&b.attached, "attached", false, "if attached is true, kills the sandbox process when the parent process terminates")
f.StringVar(&b.productName, "product-name", "", "value to show in /sys/devices/virtual/dmi/id/product_name")
@@ -406,6 +410,7 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomma
OverlayMediums: b.overlayMediums.GetArray(),
NumCPU: b.cpuNum,
TotalMem: b.totalMem,
TotalHostMem: b.totalHostMem,
UserLogFD: b.userLogFD,
ProductName: b.productName,
PodInitConfigFD: b.podInitConfigFD,
+3 -1
View File
@@ -978,11 +978,13 @@ func (s *Sandbox) createSandboxProcess(conf *config.Config, args *Args, startSyn
// because it relies on stdin being the next FD donated.
donations.Donate("stdio-fds", stdios[:]...)
mem, err := totalSystemMemory()
totalSysMem, err := totalSystemMemory()
if err != nil {
return err
}
cmd.Args = append(cmd.Args, "--total-host-memory", strconv.FormatUint(totalSysMem, 10))
mem := totalSysMem
if s.CgroupJSON.Cgroup != nil {
cpuNum, err := s.CgroupJSON.Cgroup.NumCPU()
if err != nil {