From 06ec97a3f823f1f5d928fc9c2beb3a11c2c88487 Mon Sep 17 00:00:00 2001 From: Rahat Mahmood Date: Tue, 26 Mar 2019 16:15:55 -0700 Subject: Implement memfd_create. Memfds are simply anonymous tmpfs files with no associated mounts. Also implementing file seals, which Linux only implements for memfds at the moment. PiperOrigin-RevId: 240450031 Change-Id: I31de78b950101ae8d7a13d0e93fe52d98ea06f2f --- pkg/sentry/fs/tmpfs/inode_file.go | 151 ++++++++++++++++++++++++++++++++++ pkg/sentry/syscalls/linux/BUILD | 1 + pkg/sentry/syscalls/linux/linux64.go | 2 +- pkg/sentry/syscalls/linux/sys_file.go | 59 +++++++++++++ 4 files changed, 212 insertions(+), 1 deletion(-) (limited to 'pkg/sentry') diff --git a/pkg/sentry/fs/tmpfs/inode_file.go b/pkg/sentry/fs/tmpfs/inode_file.go index 25bf2b9dd..7c80d711b 100644 --- a/pkg/sentry/fs/tmpfs/inode_file.go +++ b/pkg/sentry/fs/tmpfs/inode_file.go @@ -15,10 +15,12 @@ package tmpfs import ( + "fmt" "io" "sync" "time" + "gvisor.googlesource.com/gvisor/pkg/abi/linux" "gvisor.googlesource.com/gvisor/pkg/metric" "gvisor.googlesource.com/gvisor/pkg/sentry/context" "gvisor.googlesource.com/gvisor/pkg/sentry/fs" @@ -29,6 +31,7 @@ import ( "gvisor.googlesource.com/gvisor/pkg/sentry/safemem" "gvisor.googlesource.com/gvisor/pkg/sentry/usage" "gvisor.googlesource.com/gvisor/pkg/sentry/usermem" + "gvisor.googlesource.com/gvisor/pkg/syserror" ) var ( @@ -42,6 +45,8 @@ var ( // These files are backed by pages allocated from a platform.Memory, and may be // directly mapped. // +// Lock order: attrMu -> mapsMu -> dataMu. +// // +stateify savable type fileInodeOperations struct { fsutil.InodeGenericChecker `state:"nosave"` @@ -74,6 +79,17 @@ type fileInodeOperations struct { // mappings is protected by mapsMu. mappings memmap.MappingSet + // writableMappingPages tracks how many pages of virtual memory are mapped + // as potentially writable from this file. If a page has multiple mappings, + // each mapping is counted separately. + // + // This counter is susceptible to overflow as we can potentially count + // mappings from many VMAs. We count pages rather than bytes to slightly + // mitigate this. + // + // Protected by mapsMu. + writableMappingPages uint64 + dataMu sync.RWMutex `state:"nosave"` // data maps offsets into the file to offsets into platform.Memory() that @@ -81,6 +97,11 @@ type fileInodeOperations struct { // // data is protected by dataMu. data fsutil.FileRangeSet + + // seals represents file seals on this inode. + // + // Protected by dataMu. + seals uint32 } var _ fs.InodeOperations = (*fileInodeOperations)(nil) @@ -91,9 +112,30 @@ func NewInMemoryFile(ctx context.Context, usage usage.MemoryKind, uattr fs.Unsta attr: uattr, kernel: kernel.KernelFromContext(ctx), memUsage: usage, + seals: linux.F_SEAL_SEAL, } } +// NewMemfdInode creates a new inode backing a memfd. Memory used by the memfd +// is backed by platform memory. +func NewMemfdInode(ctx context.Context, allowSeals bool) *fs.Inode { + // Per Linux, mm/shmem.c:__shmem_file_setup(), memfd inodes are set up with + // S_IRWXUGO. + perms := fs.PermMask{Read: true, Write: true, Execute: true} + iops := NewInMemoryFile(ctx, usage.Tmpfs, fs.UnstableAttr{ + Owner: fs.FileOwnerFromContext(ctx), + Perms: fs.FilePermissions{User: perms, Group: perms, Other: perms}}).(*fileInodeOperations) + if allowSeals { + iops.seals = 0 + } + return fs.NewInode(iops, fs.NewNonCachingMountSource(nil, fs.MountSourceFlags{}), fs.StableAttr{ + Type: fs.RegularFile, + DeviceID: tmpfsDevice.DeviceID(), + InodeID: tmpfsDevice.NextIno(), + BlockSize: usermem.PageSize, + }) +} + // Release implements fs.InodeOperations.Release. func (f *fileInodeOperations) Release(context.Context) { f.dataMu.Lock() @@ -170,6 +212,16 @@ func (f *fileInodeOperations) Truncate(ctx context.Context, _ *fs.Inode, size in f.dataMu.Lock() oldSize := f.attr.Size + + // Check if current seals allow truncation. + switch { + case size > oldSize && f.seals&linux.F_SEAL_GROW != 0: // Grow sealed + fallthrough + case oldSize > size && f.seals&linux.F_SEAL_SHRINK != 0: // Shrink sealed + f.dataMu.Unlock() + return syserror.EPERM + } + if oldSize != size { f.attr.Size = size // Update mtime and ctime. @@ -370,6 +422,34 @@ func (rw *fileReadWriter) WriteFromBlocks(srcs safemem.BlockSeq) (uint64, error) return 0, nil } + // Check if seals prevent either file growth or all writes. + switch { + case rw.f.seals&linux.F_SEAL_WRITE != 0: // Write sealed + return 0, syserror.EPERM + case end > rw.f.attr.Size && rw.f.seals&linux.F_SEAL_GROW != 0: // Grow sealed + // When growth is sealed, Linux effectively allows writes which would + // normally grow the file to partially succeed up to the current EOF, + // rounded down to the page boundary before the EOF. + // + // This happens because writes (and thus the growth check) for tmpfs + // files proceed page-by-page on Linux, and the final write to the page + // containing EOF fails, resulting in a partial write up to the start of + // that page. + // + // To emulate this behaviour, artifically truncate the write to the + // start of the page containing the current EOF. + // + // See Linux, mm/filemap.c:generic_perform_write() and + // mm/shmem.c:shmem_write_begin(). + if pgstart := int64(usermem.Addr(rw.f.attr.Size).RoundDown()); end > pgstart { + end = pgstart + } + if end <= rw.offset { + // Truncation would result in no data being written. + return 0, syserror.EPERM + } + } + defer func() { // If the write ends beyond the file's previous size, it causes the // file to grow. @@ -431,7 +511,27 @@ func (rw *fileReadWriter) WriteFromBlocks(srcs safemem.BlockSeq) (uint64, error) func (f *fileInodeOperations) AddMapping(ctx context.Context, ms memmap.MappingSpace, ar usermem.AddrRange, offset uint64, writable bool) error { f.mapsMu.Lock() defer f.mapsMu.Unlock() + + f.dataMu.RLock() + defer f.dataMu.RUnlock() + + // Reject writable mapping if F_SEAL_WRITE is set. + if f.seals&linux.F_SEAL_WRITE != 0 && writable { + return syserror.EPERM + } + f.mappings.AddMapping(ms, ar, offset, writable) + if writable { + pagesBefore := f.writableMappingPages + + // ar is guaranteed to be page aligned per memmap.Mappable. + f.writableMappingPages += uint64(ar.Length() / usermem.PageSize) + + if f.writableMappingPages < pagesBefore { + panic(fmt.Sprintf("Overflow while mapping potentially writable pages pointing to a tmpfs file. Before %v, after %v", pagesBefore, f.writableMappingPages)) + } + } + return nil } @@ -439,7 +539,19 @@ func (f *fileInodeOperations) AddMapping(ctx context.Context, ms memmap.MappingS func (f *fileInodeOperations) RemoveMapping(ctx context.Context, ms memmap.MappingSpace, ar usermem.AddrRange, offset uint64, writable bool) { f.mapsMu.Lock() defer f.mapsMu.Unlock() + f.mappings.RemoveMapping(ms, ar, offset, writable) + + if writable { + pagesBefore := f.writableMappingPages + + // ar is guaranteed to be page aligned per memmap.Mappable. + f.writableMappingPages -= uint64(ar.Length() / usermem.PageSize) + + if f.writableMappingPages > pagesBefore { + panic(fmt.Sprintf("Underflow while unmapping potentially writable pages pointing to a tmpfs file. Before %v, after %v", pagesBefore, f.writableMappingPages)) + } + } } // CopyMapping implements memmap.Mappable.CopyMapping. @@ -501,3 +613,42 @@ func (f *fileInodeOperations) Translate(ctx context.Context, required, optional func (f *fileInodeOperations) InvalidateUnsavable(ctx context.Context) error { return nil } + +// GetSeals returns the current set of seals on a memfd inode. +func GetSeals(inode *fs.Inode) (uint32, error) { + if f, ok := inode.InodeOperations.(*fileInodeOperations); ok { + f.dataMu.RLock() + defer f.dataMu.RUnlock() + return f.seals, nil + } + // Not a memfd inode. + return 0, syserror.EINVAL +} + +// AddSeals adds new file seals to a memfd inode. +func AddSeals(inode *fs.Inode, val uint32) error { + if f, ok := inode.InodeOperations.(*fileInodeOperations); ok { + f.mapsMu.Lock() + defer f.mapsMu.Unlock() + f.dataMu.Lock() + defer f.dataMu.Unlock() + + if f.seals&linux.F_SEAL_SEAL != 0 { + // Seal applied which prevents addition of any new seals. + return syserror.EPERM + } + + // F_SEAL_WRITE can only be added if there are no active writable maps. + if f.seals&linux.F_SEAL_WRITE == 0 && val&linux.F_SEAL_WRITE != 0 { + if f.writableMappingPages > 0 { + return syserror.EBUSY + } + } + + // Seals can only be added, never removed. + f.seals |= val + return nil + } + // Not a memfd inode. + return syserror.EINVAL +} diff --git a/pkg/sentry/syscalls/linux/BUILD b/pkg/sentry/syscalls/linux/BUILD index 846601881..6e2843b36 100644 --- a/pkg/sentry/syscalls/linux/BUILD +++ b/pkg/sentry/syscalls/linux/BUILD @@ -63,6 +63,7 @@ go_library( "//pkg/sentry/fs/anon", "//pkg/sentry/fs/lock", "//pkg/sentry/fs/timerfd", + "//pkg/sentry/fs/tmpfs", "//pkg/sentry/kernel", "//pkg/sentry/kernel/auth", "//pkg/sentry/kernel/epoll", diff --git a/pkg/sentry/syscalls/linux/linux64.go b/pkg/sentry/syscalls/linux/linux64.go index e855590e6..888b5aa9f 100644 --- a/pkg/sentry/syscalls/linux/linux64.go +++ b/pkg/sentry/syscalls/linux/linux64.go @@ -367,7 +367,7 @@ var AMD64 = &kernel.SyscallTable{ // 316: Renameat2, TODO 317: Seccomp, 318: GetRandom, - // 319: MemfdCreate, TODO + 319: MemfdCreate, 320: syscalls.CapError(linux.CAP_SYS_BOOT), // KexecFileLoad, infeasible to support 321: syscalls.CapError(linux.CAP_SYS_ADMIN), // Bpf, requires cap_sys_admin for all commands // 322: Execveat, TODO diff --git a/pkg/sentry/syscalls/linux/sys_file.go b/pkg/sentry/syscalls/linux/sys_file.go index cf6fdc190..3193718b5 100644 --- a/pkg/sentry/syscalls/linux/sys_file.go +++ b/pkg/sentry/syscalls/linux/sys_file.go @@ -23,6 +23,7 @@ import ( "gvisor.googlesource.com/gvisor/pkg/sentry/context" "gvisor.googlesource.com/gvisor/pkg/sentry/fs" "gvisor.googlesource.com/gvisor/pkg/sentry/fs/lock" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs/tmpfs" "gvisor.googlesource.com/gvisor/pkg/sentry/kernel" "gvisor.googlesource.com/gvisor/pkg/sentry/kernel/auth" "gvisor.googlesource.com/gvisor/pkg/sentry/kernel/fasync" @@ -933,6 +934,15 @@ func Fcntl(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.Syscall case linux.F_SETOWN: fSetOwn(t, file, args[2].Int()) return 0, nil, nil + case linux.F_GET_SEALS: + val, err := tmpfs.GetSeals(file.Dirent.Inode) + return uintptr(val), nil, err + case linux.F_ADD_SEALS: + if !file.Flags().Write { + return 0, nil, syserror.EPERM + } + err := tmpfs.AddSeals(file.Dirent.Inode, args[2].Uint()) + return 0, nil, err default: // Everything else is not yet supported. return 0, nil, syserror.EINVAL @@ -2066,3 +2076,52 @@ func Sendfile(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.Sysc // arbitrarily. return uintptr(n), nil, handleIOError(t, n != 0, err, kernel.ERESTARTSYS, "sendfile", inFile) } + +const ( + memfdPrefix = "/memfd:" + memfdAllFlags = uint32(linux.MFD_CLOEXEC | linux.MFD_ALLOW_SEALING) + memfdMaxNameLen = linux.NAME_MAX - len(memfdPrefix) + 1 +) + +// MemfdCreate implements the linux syscall memfd_create(2). +func MemfdCreate(t *kernel.Task, args arch.SyscallArguments) (uintptr, *kernel.SyscallControl, error) { + addr := args[0].Pointer() + flags := args[1].Uint() + + if flags&^memfdAllFlags != 0 { + // Unknown bits in flags. + return 0, nil, syserror.EINVAL + } + + allowSeals := flags&linux.MFD_ALLOW_SEALING != 0 + cloExec := flags&linux.MFD_CLOEXEC != 0 + + name, err := t.CopyInString(addr, syscall.PathMax-len(memfdPrefix)) + if err != nil { + return 0, nil, err + } + if len(name) > memfdMaxNameLen { + return 0, nil, syserror.EINVAL + } + name = memfdPrefix + name + + inode := tmpfs.NewMemfdInode(t, allowSeals) + dirent := fs.NewDirent(inode, name) + // Per Linux, mm/shmem.c:__shmem_file_setup(), memfd files are set up with + // FMODE_READ | FMODE_WRITE. + file, err := inode.GetFile(t, dirent, fs.FileFlags{Read: true, Write: true}) + if err != nil { + return 0, nil, err + } + + defer dirent.DecRef() + defer file.DecRef() + + fdFlags := kernel.FDFlags{CloseOnExec: cloExec} + newFD, err := t.FDMap().NewFDFrom(0, file, fdFlags, t.ThreadGroup().Limits()) + if err != nil { + return 0, nil, err + } + + return uintptr(newFD), nil, nil +} -- cgit v1.2.3