diff --git a/pkg/abi/linux/file.go b/pkg/abi/linux/file.go index a56ff8ee5..e9cd45ffc 100644 --- a/pkg/abi/linux/file.go +++ b/pkg/abi/linux/file.go @@ -193,6 +193,40 @@ var DirentType = abi.ValueSet{ DT_WHT: "DT_WHT", } +// Values for fs on-disk file types. +const ( + FT_UNKNOWN = 0 + FT_REG_FILE = 1 + FT_DIR = 2 + FT_CHRDEV = 3 + FT_BLKDEV = 4 + FT_FIFO = 5 + FT_SOCK = 6 + FT_SYMLINK = 7 + FT_MAX = 8 +) + +// Conversion from fs on-disk file type to dirent type. +var direntTypeByFileType = [FT_MAX]uint8{ + FT_UNKNOWN: DT_UNKNOWN, + FT_REG_FILE: DT_REG, + FT_DIR: DT_DIR, + FT_CHRDEV: DT_CHR, + FT_BLKDEV: DT_BLK, + FT_FIFO: DT_FIFO, + FT_SOCK: DT_SOCK, + FT_SYMLINK: DT_LNK, +} + +// FileTypeToDirentType converts the on-disk file type (FT_*) to the directory +// entry type (DT_*). +func FileTypeToDirentType(filetype uint8) uint8 { + if filetype >= FT_MAX { + return DT_UNKNOWN + } + return direntTypeByFileType[filetype] +} + // Values for preadv2/pwritev2. const ( // NOTE(b/120162627): gVisor does not implement the RWF_HIPRI feature, but diff --git a/pkg/erofs/BUILD b/pkg/erofs/BUILD new file mode 100644 index 000000000..8c033adf6 --- /dev/null +++ b/pkg/erofs/BUILD @@ -0,0 +1,31 @@ +load("//tools:defs.bzl", "go_library", "go_test") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +go_library( + name = "erofs", + srcs = ["erofs.go"], + marshal = True, + visibility = ["//visibility:public"], + deps = [ + "//pkg/abi/linux", + "//pkg/cleanup", + "//pkg/errors/linuxerr", + "//pkg/hostarch", + "//pkg/log", + "//pkg/marshal", + "//pkg/marshal/primitive", + "//pkg/safemem", + "@org_golang_x_sys//unix:go_default_library", + ], +) + +go_test( + name = "erofs_test", + size = "small", + srcs = ["erofs_test.go"], + library = ":erofs", +) diff --git a/pkg/erofs/erofs.go b/pkg/erofs/erofs.go new file mode 100644 index 000000000..1739c87e8 --- /dev/null +++ b/pkg/erofs/erofs.go @@ -0,0 +1,730 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package erofs provides the ability to access the contents in an EROFS [1] image. +// +// The design principle of this package is that, it will just provide the ability +// to access the contents in the image, and it will never cache any objects internally. +// The whole disk image is mapped via a read-only/shared mapping, and it relies on +// host kernel to cache the blocks/pages transparently. +// +// [1] https://docs.kernel.org/filesystems/erofs.html +package erofs + +import ( + "bytes" + "fmt" + "hash/crc32" + "os" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/cleanup" + "gvisor.dev/gvisor/pkg/errors/linuxerr" + "gvisor.dev/gvisor/pkg/hostarch" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/marshal" + "gvisor.dev/gvisor/pkg/marshal/primitive" + "gvisor.dev/gvisor/pkg/safemem" +) + +const ( + // Definitions for super block. + SuperBlockMagicV1 = 0xe0f5e1e2 + SuperBlockOffset = 1024 + + // Inode slot size in bit shift. + InodeSlotBits = 5 + + // Max file name length. + MaxNameLen = 255 +) + +// Bit definitions for Inode*::Format. +const ( + InodeLayoutBit = 0 + InodeLayoutBits = 1 + + InodeDataLayoutBit = 1 + InodeDataLayoutBits = 3 +) + +// Inode layouts. +const ( + InodeLayoutCompact = 0 + InodeLayoutExtended = 1 +) + +// Inode data layouts. +const ( + InodeDataLayoutFlatPlain = iota + InodeDataLayoutFlatCompressionLegacy + InodeDataLayoutFlatInline + InodeDataLayoutFlatCompression + InodeDataLayoutChunkBased + InodeDataLayoutMax +) + +// Features w/ backward compatibility. +// This is not exhaustive, unused features are not listed. +const ( + FeatureCompatSuperBlockChecksum = 0x00000001 +) + +// Features w/o backward compatibility. +// +// Any features that aren't in FeatureIncompatSupported are incompatible +// with this implementation. +// +// This is not exhaustive, unused features are not listed. +const ( + FeatureIncompatSupported = 0x0 +) + +// SuperBlock represents on-disk super block. +// +// +marshal +// +stateify savable +type SuperBlock struct { + Magic uint32 + Checksum uint32 + FeatureCompat uint32 + BlockSizeBits uint8 + ExtSlots uint8 + RootNid uint16 + Inodes uint64 + BuildTime uint64 + BuildTimeNsec uint32 + Blocks uint32 + MetaBlockAddr uint32 + XattrBlockAddr uint32 + UUID [16]uint8 + VolumeName [16]uint8 + FeatureIncompat uint32 + Union1 uint16 + ExtraDevices uint16 + DevTableSlotOff uint16 + Reserved [38]uint8 +} + +// BlockSize returns the block size. +func (sb *SuperBlock) BlockSize() uint32 { + return 1 << sb.BlockSizeBits +} + +// BlockAddrToOffset converts block addr to the offset in image file. +func (sb *SuperBlock) BlockAddrToOffset(addr uint32) uint64 { + return uint64(addr) << sb.BlockSizeBits +} + +// MetaOffset returns the offset of metadata area in image file. +func (sb *SuperBlock) MetaOffset() uint64 { + return sb.BlockAddrToOffset(sb.MetaBlockAddr) +} + +// NidToOffset converts inode number to the offset in image file. +func (sb *SuperBlock) NidToOffset(nid uint64) uint64 { + return sb.MetaOffset() + (nid << InodeSlotBits) +} + +// InodeCompact represents 32-byte reduced form of on-disk inode. +// +// +marshal +type InodeCompact struct { + Format uint16 + XattrCount uint16 + Mode uint16 + Nlink uint16 + Size uint32 + Reserved uint32 + RawBlockAddr uint32 + Ino uint32 + UID uint16 + GID uint16 + Reserved2 uint32 +} + +// InodeExtended represents 64-byte complete form of on-disk inode. +// +// +marshal +type InodeExtended struct { + Format uint16 + XattrCount uint16 + Mode uint16 + Reserved uint16 + Size uint64 + RawBlockAddr uint32 + Ino uint32 + UID uint32 + GID uint32 + Mtime uint64 + MtimeNsec uint32 + Nlink uint32 + Reserved2 [16]uint8 +} + +// Dirent represents on-disk directory entry. +// +// This struct is misaligned according to go_marshal, as it is only 12 bytes in size. The +// last field needs to be marked unaligned so the struct is marked unpacked and the +// generated methods behave correctly. +// +// +marshal +type Dirent struct { + Nid uint64 + NameOff uint16 + FileType uint8 + Reserved uint8 `marshal:"unaligned"` +} + +// Image represents an open EROFS image. +// +// +stateify savable +type Image struct { + src *os.File `state:"nosave"` + bytes []byte `state:"nosave"` + sb SuperBlock +} + +// OpenImage returns an Image providing access to the contents in the image file src. +// +// On success, the ownership of src is transferred to Image. +func OpenImage(src *os.File) (*Image, error) { + i := &Image{src: src} + + var cu cleanup.Cleanup + defer cu.Clean() + + stat, err := i.src.Stat() + if err != nil { + return nil, err + } + i.bytes, err = unix.Mmap(int(i.src.Fd()), 0, int(stat.Size()), unix.PROT_READ, unix.MAP_SHARED) + if err != nil { + return nil, err + } + cu.Add(func() { unix.Munmap(i.bytes) }) + + if err := i.initSuperBlock(); err != nil { + return nil, err + } + cu.Release() + return i, nil +} + +// Close closes the image. +func (i *Image) Close() { + unix.Munmap(i.bytes) + i.src.Close() +} + +// BlockSize returns the block size of this image. +func (i *Image) BlockSize() uint32 { + return i.sb.BlockSize() +} + +// Blocks returns the total blocks of this image. +func (i *Image) Blocks() uint32 { + return i.sb.Blocks +} + +// RootNid returns the root inode number of this image. +func (i *Image) RootNid() uint64 { + return uint64(i.sb.RootNid) +} + +// initSuperBlock initializes the super block of this image. +func (i *Image) initSuperBlock() error { + if err := i.UnmarshalAt(&i.sb, SuperBlockOffset); err != nil { + return fmt.Errorf("image size is too small") + } + + if i.sb.Magic != SuperBlockMagicV1 { + return fmt.Errorf("unknown magic: 0x%x", i.sb.Magic) + } + + if err := i.verifyChecksum(); err != nil { + return err + } + + if featureIncompat := i.sb.FeatureIncompat & ^uint32(FeatureIncompatSupported); featureIncompat != 0 { + return fmt.Errorf("unsupported incompatible features detected: 0x%x", featureIncompat) + } + + if i.BlockSize()%hostarch.PageSize != 0 { + return fmt.Errorf("unsupported block size: 0x%x", i.BlockSize()) + } + + return nil +} + +// verifyChecksum verifies the checksum of the super block. +func (i *Image) verifyChecksum() error { + if i.sb.FeatureCompat&FeatureCompatSuperBlockChecksum == 0 { + return nil + } + + sb := i.sb + sb.Checksum = 0 + table := crc32.MakeTable(crc32.Castagnoli) + checksum := crc32.Checksum(marshal.Marshal(&sb), table) + + off := SuperBlockOffset + uint64(i.sb.SizeBytes()) + if bytes, err := i.BytesAt(off, uint64(i.BlockSize())-off); err != nil { + return fmt.Errorf("image size is too small") + } else { + checksum = ^crc32.Update(checksum, table, bytes) + } + if checksum != i.sb.Checksum { + return fmt.Errorf("invalid checksum: 0x%x, expected: 0x%x", checksum, i.sb.Checksum) + } + + return nil +} + +// FD returns the host FD of underlying image file. +func (i *Image) FD() int { + return int(i.src.Fd()) +} + +// BytesAt returns the bytes at [off, off+n) of the image. +func (i *Image) BytesAt(off, n uint64) ([]byte, error) { + size := uint64(len(i.bytes)) + end := off + n + if off >= size || off > end || end > size { + log.Warningf("Invalid range (off: 0x%x, n: 0x%x) for image (size: 0x%x)", off, n, size) + return nil, linuxerr.EFAULT + } + return i.bytes[off:end], nil +} + +// UnmarshalAt deserializes data from the bytes at [off, off+n) of the image. +func (i *Image) UnmarshalAt(data marshal.Marshallable, off uint64) error { + bytes, err := i.BytesAt(off, uint64(data.SizeBytes())) + if err != nil { + log.Warningf("Failed to deserialize %T from 0x%x.", data, off) + return err + } + data.UnmarshalUnsafe(bytes) + return nil +} + +// Inode returns the inode identified by nid. +// +// TODO: Ideally, we should avoid escaping objects to heap when constructing +// objects from the image. +func (i *Image) Inode(nid uint64) (Inode, error) { + inode := Inode{ + image: i, + nid: nid, + } + + off := i.sb.NidToOffset(nid) + if err := i.UnmarshalAt(&inode.format, off); err != nil { + return Inode{}, err + } + + var ( + rawBlockAddr uint32 + inodeSize int + ) + + switch layout := inode.Layout(); layout { + case InodeLayoutCompact: + var ino InodeCompact + if err := i.UnmarshalAt(&ino, off); err != nil { + return Inode{}, err + } + + if ino.XattrCount != 0 { + log.Warningf("Unsupported xattr at inode (nid=%v)", nid) + return Inode{}, linuxerr.ENOTSUP + } + + rawBlockAddr = ino.RawBlockAddr + inodeSize = ino.SizeBytes() + + inode.size = uint64(ino.Size) + inode.nlink = uint32(ino.Nlink) + inode.mode = ino.Mode + inode.uid = uint32(ino.UID) + inode.gid = uint32(ino.GID) + inode.mtime = i.sb.BuildTime + inode.mtimeNsec = i.sb.BuildTimeNsec + + case InodeLayoutExtended: + var ino InodeExtended + if err := i.UnmarshalAt(&ino, off); err != nil { + return Inode{}, err + } + + if ino.XattrCount != 0 { + log.Warningf("Unsupported xattr at inode (nid=%v)", nid) + return Inode{}, linuxerr.ENOTSUP + } + + rawBlockAddr = ino.RawBlockAddr + inodeSize = ino.SizeBytes() + + inode.size = ino.Size + inode.nlink = ino.Nlink + inode.mode = ino.Mode + inode.uid = ino.UID + inode.gid = ino.GID + inode.mtime = ino.Mtime + inode.mtimeNsec = ino.MtimeNsec + + default: + log.Warningf("Unsupported layout 0x%x at inode (nid=%v)", layout, nid) + return Inode{}, linuxerr.ENOTSUP + } + + switch dataLayout := inode.DataLayout(); dataLayout { + case InodeDataLayoutFlatInline: + // Check that whether the file data in the last block fits into + // the remaining room of the metadata block. + blockSize := i.BlockSize() + tailSize := uint32(inode.size) & (blockSize - 1) + if tailSize == 0 || tailSize > blockSize-uint32(inodeSize) { + log.Warningf("Inline data not found or cross block boundary at inode (nid=%v)", nid) + return Inode{}, linuxerr.EUCLEAN + } + inode.idataOff = off + uint64(inodeSize) + fallthrough + + case InodeDataLayoutFlatPlain: + inode.dataOff = i.sb.BlockAddrToOffset(rawBlockAddr) + + default: + log.Warningf("Unsupported data layout 0x%x at inode (nid=%v)", dataLayout, nid) + return Inode{}, linuxerr.ENOTSUP + } + + return inode, nil +} + +// Inode represents in-memory inode object. +// +// +stateify savable +type Inode struct { + // image is the underlying image. Inode should not perform writable + // operations (e.g. Close()) on the image. + image *Image + + // dataOff points to the data of this inode in the data blocks. + dataOff uint64 + + // idataOff points to the tail packing inline data of this inode + // if it's not zero in the metadata block. + idataOff uint64 + + // format is the format of this inode. + format primitive.Uint16 + + // Metadata. + mode uint16 + nid uint64 + size uint64 + mtime uint64 + mtimeNsec uint32 + uid uint32 + gid uint32 + nlink uint32 +} + +func bitRange(value, bit, bits uint16) uint16 { + return (value >> bit) & ((1 << bits) - 1) +} + +// Layout returns the inode layout. +func (i *Inode) Layout() uint16 { + return bitRange(uint16(i.format), InodeLayoutBit, InodeLayoutBits) +} + +// DataLayout returns the inode data layout. +func (i *Inode) DataLayout() uint16 { + return bitRange(uint16(i.format), InodeDataLayoutBit, InodeDataLayoutBits) +} + +// IsRegular indicates whether i represents a regular file. +func (i *Inode) IsRegular() bool { + return i.mode&linux.S_IFMT == linux.S_IFREG +} + +// IsDir indicates whether i represents a directory. +func (i *Inode) IsDir() bool { + return i.mode&linux.S_IFMT == linux.S_IFDIR +} + +// IsCharDev indicates whether i represents a character device. +func (i *Inode) IsCharDev() bool { + return i.mode&linux.S_IFMT == linux.S_IFCHR +} + +// IsBlockDev indicates whether i represents a block device. +func (i *Inode) IsBlockDev() bool { + return i.mode&linux.S_IFMT == linux.S_IFBLK +} + +// IsFIFO indicates whether i represents a named pipe. +func (i *Inode) IsFIFO() bool { + return i.mode&linux.S_IFMT == linux.S_IFIFO +} + +// IsSocket indicates whether i represents a socket. +func (i *Inode) IsSocket() bool { + return i.mode&linux.S_IFMT == linux.S_IFSOCK +} + +// IsSymlink indicates whether i represents a symbolic link. +func (i *Inode) IsSymlink() bool { + return i.mode&linux.S_IFMT == linux.S_IFLNK +} + +// Nid returns the inode number. +func (i *Inode) Nid() uint64 { + return i.nid +} + +// Size returns the data size. +func (i *Inode) Size() uint64 { + return i.size +} + +// Nlink returns the number of hard links. +func (i *Inode) Nlink() uint32 { + return i.nlink +} + +// Mtime returns the time of last modification. +func (i *Inode) Mtime() uint64 { + return i.mtime +} + +// MtimeNsec returns the nano second part of Mtime. +func (i *Inode) MtimeNsec() uint32 { + return i.mtimeNsec +} + +// Mode returns the file type and permissions. +func (i *Inode) Mode() uint16 { + return i.mode +} + +// UID returns the user ID of the owner. +func (i *Inode) UID() uint32 { + return i.uid +} + +// GID returns the group ID of the owner. +func (i *Inode) GID() uint32 { + return i.gid +} + +// DataOffset returns the data offset of this inode in image file. +func (i *Inode) DataOffset() (uint64, error) { + // TODO: We don't support regular files with inline data yet, which means the image + // should be created with the "-E noinline_data" option. The "-E noinline_data" option + // was introduced for the DAX feature support in Linux [1]. + // [1] https://github.com/erofs/erofs-utils/commit/60549d52c3b636f0ddd1d51b0c1517c1dee22595 + if dataLayout := i.DataLayout(); dataLayout != InodeDataLayoutFlatPlain { + log.Warningf("Unsupported data layout 0x%x at inode (nid=%v)", dataLayout, i.Nid()) + return 0, linuxerr.ENOTSUP + } + return i.dataOff, nil +} + +// Data returns the read-only file data of this inode. +func (i *Inode) Data() (safemem.BlockSeq, error) { + switch dataLayout := i.DataLayout(); dataLayout { + case InodeDataLayoutFlatPlain: + bytes, err := i.image.BytesAt(i.dataOff, i.size) + if err != nil { + return safemem.BlockSeq{}, err + } + return safemem.BlockSeqOf(safemem.BlockFromSafeSlice(bytes)), nil + + case InodeDataLayoutFlatInline: + sl := make([]safemem.Block, 0, 2) + idataSize := i.size & (uint64(i.image.BlockSize()) - 1) + if i.size > idataSize { + if bytes, err := i.image.BytesAt(i.dataOff, i.size-idataSize); err != nil { + return safemem.BlockSeq{}, err + } else { + sl = append(sl, safemem.BlockFromSafeSlice(bytes)) + } + } + if bytes, err := i.image.BytesAt(i.idataOff, idataSize); err != nil { + return safemem.BlockSeq{}, err + } else { + sl = append(sl, safemem.BlockFromSafeSlice(bytes)) + } + return safemem.BlockSeqFromSlice(sl), nil + + default: + log.Warningf("Unsupported data layout 0x%x at inode (nid=%v)", dataLayout, i.Nid()) + return safemem.BlockSeq{}, linuxerr.ENOTSUP + } +} + +// blocks returns the number of blocks that contain data. It will count in the +// metadata block which contains the inline data. +func (i *Inode) blocks() uint64 { + blockSize := uint64(i.image.BlockSize()) + return (i.size + (blockSize - 1)) / blockSize +} + +// IterDirents invokes cb on each entry in the directory represented by this inode. +// The first two directory entries are "." and "..". The remaining directory entries +// will be iterated in alphabetical order. +// +// https://docs.kernel.org/filesystems/erofs.html#directories +// +// The on-disk format of one block looks like this: +// +// ___________________________ +// / | +// / ______________|________________ +// / / | nameoff1 | nameoffN-1 +// ____________.______________._______________v________________v__________ +// | dirent | dirent | ... | dirent | filename | filename | ... | filename | +// |___.0___|____1___|_____|___N-1__|____0_____|____1_____|_____|___N-1____| +// \ ^ +// \ | * could have +// \ | trailing '\0' +// \________________________| nameoff0 +// Directory block +// +// The on-disk format of one directory looks like this: +// +// [ (block 1) dirent 1 | dirent 2 | dirent 3 | name 1 | name 2 | name 3 | optional padding ] +// [ (block 2) dirent 4 | dirent 5 | name 4 | name 5 | optional padding ] +// ... +// [ (block N) dirent M | dirent M+1 | name M | name M+1 | optional padding ] +// +// [ (metadata block) inode | optional fields | dirent M+2 | dirent M+3 | name M+2 | name M+3 | optional padding ] +// +// All directory entries (except the first two: "." and "..") are _strictly_ recorded in alphabetical order. +func (i *Inode) IterDirents(cb func(name string, typ uint8, nid uint64) error) error { + if !i.IsDir() { + return linuxerr.ENOTDIR + } + + blocks := i.blocks() + blockSize := i.image.BlockSize() + + start := i.dataOff + if blocks == 1 && i.idataOff != 0 { + start = i.idataOff + } + + // Iterate all the blocks which contain dirents. + for blocks > 0 { + // Unmarshal the first dirent in the current block. + direntOff := start + d := &Dirent{} + if err := i.image.UnmarshalAt(d, direntOff); err != nil { + return err + } + // Apart from the offset of the first filename, nameOff0 also indicates + // the total number of dirents in this block. + nameOff0 := start + uint64(d.NameOff) + maxSize := blockSize + if blocks == 1 { + if tailSize := uint32(i.size) & (blockSize - 1); tailSize != 0 { + maxSize = tailSize + } + } + // Iterate all the dirents in this block. + for d != nil { + var ( + next *Dirent + nameLen uint32 + ) + direntOff += uint64(d.SizeBytes()) + lastDirent := direntOff >= nameOff0 + if lastDirent { + // There is no more dirent in this block, d is the last one. + next = nil + nameLen = maxSize - uint32(d.NameOff) + } else { + // Unmarshal the next adjacent dirent. + next = &Dirent{} + if err := i.image.UnmarshalAt(next, direntOff); err != nil { + return err + } + nameLen = uint32(next.NameOff - d.NameOff) + } + + buf, err := i.image.BytesAt(start+uint64(d.NameOff), uint64(nameLen)) + if err != nil { + return err + } + if lastDirent { + if n := bytes.IndexByte(buf, 0); n != -1 { + nameLen = uint32(n) + } + } + if nameLen > MaxNameLen { + log.Warningf("Corrupted dirent at inode (nid=%v)", i.Nid()) + return linuxerr.EUCLEAN + } + name := string(buf[:nameLen]) + if err := cb(name, d.FileType, d.Nid); err != nil { + return err + } + + d = next + } + + blocks-- + + if blocks == 1 && i.idataOff != 0 { + // If we have any inline data and this is the last block, we need to process it now. + start = i.idataOff + } else { + // Just go on to process the next adjacent block. + start += uint64(blockSize) + } + } + return nil +} + +// Readlink reads the link target. +func (i *Inode) Readlink() (string, error) { + if !i.IsSymlink() { + return "", linuxerr.EINVAL + } + off := i.dataOff + size := i.size + if i.idataOff != 0 { + // Inline symlink data shouldn't cross block boundary. + if i.blocks() > 1 { + log.Warningf("Inline data cross block boundary at inode (nid=%v)", i.Nid()) + return "", linuxerr.EUCLEAN + } + off = i.idataOff + } else { + // This matches Linux's behaviour in fs/namei.c:page_get_link() and + // include/linux/namei.h:nd_terminate_link(). + if size > hostarch.PageSize-1 { + size = hostarch.PageSize - 1 + } + } + target, err := i.image.BytesAt(off, size) + if err != nil { + return "", err + } + return string(target), nil +} diff --git a/pkg/erofs/erofs_test.go b/pkg/erofs/erofs_test.go new file mode 100644 index 000000000..28bb0e8fd --- /dev/null +++ b/pkg/erofs/erofs_test.go @@ -0,0 +1,37 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package erofs + +import ( + "testing" +) + +func TestOnDiskStructureSizes(t *testing.T) { + if sb := new(SuperBlock); sb.SizeBytes() != 128 { + t.Errorf("wrong super block size: want 128, got %d", sb.SizeBytes()) + } + + if i := new(InodeCompact); i.SizeBytes() != 32 { + t.Errorf("wrong compact inode size: want 32, got %d", i.SizeBytes()) + } + + if i := new(InodeExtended); i.SizeBytes() != 64 { + t.Errorf("wrong extended inode size: want 64, got %d", i.SizeBytes()) + } + + if d := new(Dirent); d.SizeBytes() != 12 { + t.Errorf("wrong dirent size: want 12, got %d", d.SizeBytes()) + } +} diff --git a/pkg/sentry/fsimpl/erofs/BUILD b/pkg/sentry/fsimpl/erofs/BUILD new file mode 100644 index 000000000..7fd92e831 --- /dev/null +++ b/pkg/sentry/fsimpl/erofs/BUILD @@ -0,0 +1,74 @@ +load("//tools:defs.bzl", "go_library") +load("//tools/go_generics:defs.bzl", "go_template_instance") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +go_template_instance( + name = "fstree", + out = "fstree.go", + package = "erofs", + prefix = "generic", + template = "//pkg/sentry/vfs/genericfstree:generic_fstree", + types = { + "Dentry": "dentry", + }, +) + +go_template_instance( + name = "dentry_refs", + out = "dentry_refs.go", + package = "erofs", + prefix = "dentry", + template = "//pkg/refs:refs_template", + types = { + "T": "dentry", + }, +) + +go_template_instance( + name = "inode_refs", + out = "inode_refs.go", + package = "erofs", + prefix = "inode", + template = "//pkg/refs:refs_template", + types = { + "T": "inode", + }, +) + +go_library( + name = "erofs", + srcs = [ + "dentry_refs.go", + "directory.go", + "erofs.go", + "filesystem.go", + "fstree.go", + "inode_refs.go", + "regular_file.go", + "save_restore.go", + ], + visibility = ["//pkg/sentry:internal"], + deps = [ + "//pkg/abi/linux", + "//pkg/atomicbitops", + "//pkg/cleanup", + "//pkg/context", + "//pkg/erofs", + "//pkg/errors/linuxerr", + "//pkg/fspath", + "//pkg/hostarch", + "//pkg/refs", + "//pkg/safemem", + "//pkg/sentry/fsimpl/lock", + "//pkg/sentry/fsutil", + "//pkg/sentry/kernel/auth", + "//pkg/sentry/memmap", + "//pkg/sentry/socket/unix/transport", + "//pkg/sentry/vfs", + "//pkg/usermem", + ], +) diff --git a/pkg/sentry/fsimpl/erofs/directory.go b/pkg/sentry/fsimpl/erofs/directory.go new file mode 100644 index 000000000..8792acf90 --- /dev/null +++ b/pkg/sentry/fsimpl/erofs/directory.go @@ -0,0 +1,172 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package erofs + +import ( + "sort" + "sync" + + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/errors/linuxerr" + "gvisor.dev/gvisor/pkg/sentry/vfs" +) + +func (i *inode) getDirents() ([]vfs.Dirent, error) { + // Fast path. + i.dirMu.RLock() + dirents := i.dirents + i.dirMu.RUnlock() + if dirents != nil { + return dirents, nil + } + + // Slow path. + i.dirMu.Lock() + defer i.dirMu.Unlock() + + off := int64(1) + if err := i.IterDirents(func(name string, typ uint8, nid uint64) error { + dirents = append(dirents, vfs.Dirent{ + Name: name, + Type: linux.FileTypeToDirentType(typ), + Ino: nid, + NextOff: off, + }) + off++ + return nil + }); err != nil { + return nil, err + } + + // "." and ".." should always be present. + if len(dirents) < 2 { + return nil, linuxerr.EUCLEAN + } + + i.dirents = dirents + return dirents, nil +} + +func (i *inode) lookup(name string) (uint64, error) { + // TODO: For simplicity, currently a lookup will cause all dirents to be + // read and cached. But it hurts the performance of large directories. + // We should do binary search on disk data directly (like Linux does). + dirents, err := i.getDirents() + if err != nil { + return 0, err + } + + // Skip "." and ".." + dirents = dirents[2:] + + // The dirents are sorted in alphabetical order. We do binary search + // to find the target. + idx := sort.Search(len(dirents), func(i int) bool { + return dirents[i].Name >= name + }) + if idx >= len(dirents) || dirents[idx].Name != name { + return 0, linuxerr.ENOENT + } + return dirents[idx].Ino, nil +} + +func (d *dentry) lookup(ctx context.Context, name string) (*dentry, error) { + // Fast path, dentry already exists. + d.dirMu.RLock() + child, ok := d.childMap[name] + d.dirMu.RUnlock() + if ok { + return child, nil + } + + // Slow path, create a new dentry. + d.dirMu.Lock() + defer d.dirMu.Unlock() + if child, ok := d.childMap[name]; ok { + return child, nil + } + + nid, err := d.inode.lookup(name) + if err != nil { + return nil, err + } + + if d.childMap == nil { + d.childMap = make(map[string]*dentry) + } + + child, err = d.inode.fs.newDentry(nid) + if err != nil { + return nil, err + } + child.parent.Store(d) + child.name = name + d.childMap[name] = child + return child, nil +} + +// +stateify savable +type directoryFD struct { + fileDescription + vfs.DirectoryFileDescriptionDefaultImpl + + // mu protects off. + mu sync.Mutex `state:"nosave"` + // +checklocks:mu + off int64 +} + +// IterDirents implements vfs.FileDescriptionImpl.IterDirents. +func (fd *directoryFD) IterDirents(ctx context.Context, cb vfs.IterDirentsCallback) error { + d := fd.dentry() + dirents, err := d.inode.getDirents() + if err != nil { + return err + } + + d.InotifyWithParent(ctx, linux.IN_ACCESS, 0, vfs.PathEvent) + + fd.mu.Lock() + defer fd.mu.Unlock() + + for fd.off < int64(len(dirents)) { + if err := cb.Handle(dirents[fd.off]); err != nil { + return err + } + fd.off++ + } + return nil +} + +// Seek implements vfs.FileDescriptionImpl.Seek. +func (fd *directoryFD) Seek(ctx context.Context, offset int64, whence int32) (int64, error) { + fd.mu.Lock() + defer fd.mu.Unlock() + + switch whence { + case linux.SEEK_SET: + // use offset as specified + case linux.SEEK_CUR: + offset += fd.off + default: + return 0, linuxerr.EINVAL + } + if offset < 0 { + return 0, linuxerr.EINVAL + } + fd.off = offset + return offset, nil +} diff --git a/pkg/sentry/fsimpl/erofs/erofs.go b/pkg/sentry/fsimpl/erofs/erofs.go new file mode 100644 index 000000000..1ec9fc4de --- /dev/null +++ b/pkg/sentry/fsimpl/erofs/erofs.go @@ -0,0 +1,523 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package erofs implements erofs. +package erofs + +import ( + "os" + "runtime" + "strconv" + "sync" + "sync/atomic" + + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/cleanup" + "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/erofs" + "gvisor.dev/gvisor/pkg/errors/linuxerr" + "gvisor.dev/gvisor/pkg/sentry/kernel/auth" + "gvisor.dev/gvisor/pkg/sentry/vfs" +) + +const Name = "erofs" + +// Mount option names for EROFS. +const ( + moptImageFD = "ifd" +) + +// FilesystemType implements vfs.FilesystemType. +// +// +stateify savable +type FilesystemType struct{} + +// filesystem implements vfs.FilesystemImpl. +// +// +stateify savable +type filesystem struct { + vfsfs vfs.Filesystem + + // Immutable options. + mopts string + + // devMinor is the filesystem's minor device number. devMinor is immutable. + devMinor uint32 + + // root is the root dentry. root is immutable. + root *dentry + + // image is the EROFS image. image is immutable. + image *erofs.Image + + // mf implements memmap.File for this image. + mf imageMemmapFile + + // inodeBuckets contains the inodes in use. Multiple buckets are used to + // reduce the lock contention. Bucket is chosen based on the hash calculation + // on nid in filesystem.inodeBucket. + inodeBuckets []inodeBucket +} + +// Name implements vfs.FilesystemType.Name. +func (FilesystemType) Name() string { + return Name +} + +// Release implements vfs.FilesystemType.Release. +func (FilesystemType) Release(ctx context.Context) {} + +// GetFilesystem implements vfs.FilesystemType.GetFilesystem. +func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.VirtualFilesystem, creds *auth.Credentials, source string, opts vfs.GetFilesystemOptions) (*vfs.Filesystem, *vfs.Dentry, error) { + mopts := vfs.GenericParseMountOptions(opts.Data) + + var cu cleanup.Cleanup + defer cu.Clean() + + fd, err := getFDFromMountOptionsMap(ctx, mopts) + if err != nil { + return nil, nil, err + } + + f := os.NewFile(uintptr(fd), "EROFS image file") + image, err := erofs.OpenImage(f) + if err != nil { + f.Close() + return nil, nil, err + } + cu.Add(func() { image.Close() }) + + devMinor, err := vfsObj.GetAnonBlockDevMinor() + if err != nil { + return nil, nil, err + } + + fs := &filesystem{ + mopts: opts.Data, + image: image, + devMinor: devMinor, + mf: imageMemmapFile{image: image}, + } + fs.vfsfs.Init(vfsObj, &fstype, fs) + cu.Add(func() { fs.vfsfs.DecRef(ctx) }) + + fs.inodeBuckets = make([]inodeBucket, runtime.GOMAXPROCS(0)) + for i := range fs.inodeBuckets { + fs.inodeBuckets[i].init() + } + + root, err := fs.newDentry(image.RootNid()) + if err != nil { + return nil, nil, err + } + + // Increase the root's reference count to 2. One reference is returned to + // the caller, and the other is held by fs. + root.IncRef() + fs.root = root + + cu.Release() + return &fs.vfsfs, &root.vfsd, nil +} + +func getFDFromMountOptionsMap(ctx context.Context, mopts map[string]string) (int, error) { + ifdstr, ok := mopts[moptImageFD] + if !ok { + ctx.Warningf("erofs.getFDFromMountOptionsMap: image FD must be specified as '%s='", moptImageFD) + return -1, linuxerr.EINVAL + } + delete(mopts, moptImageFD) + + ifd, err := strconv.Atoi(ifdstr) + if err != nil { + ctx.Warningf("erofs.getFDFromMountOptionsMap: invalid image FD: %s=%s", moptImageFD, ifdstr) + return -1, linuxerr.EINVAL + } + + return ifd, nil +} + +// Release implements vfs.FilesystemImpl.Release. +func (fs *filesystem) Release(ctx context.Context) { + // An extra reference was held by the filesystem on the root. + if fs.root != nil { + fs.root.DecRef(ctx) + } + fs.image.Close() + fs.vfsfs.VirtualFilesystem().PutAnonBlockDevMinor(fs.devMinor) +} + +func (fs *filesystem) statFS() linux.Statfs { + blockSize := int64(fs.image.BlockSize()) + return linux.Statfs{ + Type: erofs.SuperBlockMagicV1, + NameLength: erofs.MaxNameLen, + BlockSize: blockSize, + FragmentSize: blockSize, + Blocks: uint64(fs.image.Blocks()), + } +} + +// +stateify savable +type inodeBucket struct { + // mu protects inodeMap. + mu sync.RWMutex `state:"nosave"` + + // inodeMap contains the inodes indexed by nid. + // +checklocks:mu + inodeMap map[uint64]*inode +} + +func (ib *inodeBucket) init() { + ib.inodeMap = make(map[uint64]*inode) // +checklocksignore +} + +// getInode returns the inode identified by nid. A reference on inode is also +// returned to caller. +func (ib *inodeBucket) getInode(nid uint64) *inode { + ib.mu.RLock() + defer ib.mu.RUnlock() + i := ib.inodeMap[nid] + if i != nil { + i.IncRef() + } + return i +} + +// addInode adds the inode identified by nid into the bucket. It will first check +// whether the old inode exists. If not, it will call newInode() to get the new inode. +// The inode eventually saved in the bucket will be returned with a reference for caller. +func (ib *inodeBucket) addInode(nid uint64, newInode func() *inode) *inode { + ib.mu.Lock() + defer ib.mu.Unlock() + if i, ok := ib.inodeMap[nid]; ok { + i.IncRef() + return i + } + i := newInode() + ib.inodeMap[nid] = i + return i +} + +// removeInode removes the inode identified by nid. +func (ib *inodeBucket) removeInode(nid uint64) { + ib.mu.Lock() + delete(ib.inodeMap, nid) + ib.mu.Unlock() +} + +func (fs *filesystem) inodeBucket(nid uint64) *inodeBucket { + bucket := nid % uint64(len(fs.inodeBuckets)) + return &fs.inodeBuckets[bucket] +} + +// inode represents a filesystem object. +// +// Each dentry holds a reference on the inode it represents. An inode will +// be dropped once its reference count reaches zero. We do not cache inodes +// directly. The caching policy is implemented on top of dentries. +// +// +stateify savable +type inode struct { + erofs.Inode + + // inodeRefs is the reference count. + inodeRefs + + // fs is the owning filesystem. + fs *filesystem + + // dirMu protects dirents. dirents is immutable after creation. + dirMu sync.RWMutex `state:"nosave"` + // +checklocks:dirMu + dirents []vfs.Dirent `state:"nosave"` + + // locks supports POSIX and BSD style locks. + locks vfs.FileLocks + + // Inotify watches for this inode. + watches vfs.Watches +} + +// getInode returns the inode identified by nid. A reference on inode is also +// returned to caller. +func (fs *filesystem) getInode(nid uint64) (*inode, error) { + bucket := fs.inodeBucket(nid) + + // Fast path, inode already exists. + if i := bucket.getInode(nid); i != nil { + return i, nil + } + + // Slow path, create a new inode. + // + // Construct the underlying inode object from the image without taking + // the bucket lock first to reduce the contention. + ino, err := fs.image.Inode(nid) + if err != nil { + return nil, err + } + return bucket.addInode(nid, func() *inode { + i := &inode{ + Inode: ino, + fs: fs, + } + i.InitRefs() + return i + }), nil + +} + +// DecRef should be called when you're finished with an inode. +func (i *inode) DecRef(ctx context.Context) { + i.inodeRefs.DecRef(func() { + nid := i.Nid() + i.fs.inodeBucket(nid).removeInode(nid) + }) +} + +func (i *inode) checkPermissions(creds *auth.Credentials, ats vfs.AccessTypes) error { + return vfs.GenericCheckPermissions(creds, ats, linux.FileMode(i.Mode()), auth.KUID(i.UID()), auth.KGID(i.GID())) +} + +func (i *inode) statTo(stat *linux.Statx) { + stat.Mask = linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_NLINK | + linux.STATX_UID | linux.STATX_GID | linux.STATX_INO | linux.STATX_SIZE | + linux.STATX_BLOCKS | linux.STATX_ATIME | linux.STATX_CTIME | + linux.STATX_MTIME + stat.Blksize = i.fs.image.BlockSize() + stat.Nlink = i.Nlink() + stat.UID = i.UID() + stat.GID = i.GID() + stat.Mode = i.Mode() + stat.Ino = i.Nid() + stat.Size = i.Size() + stat.Blocks = (stat.Size + 511) / 512 + stat.Mtime = linux.StatxTimestamp{ + Sec: int64(i.Mtime()), + Nsec: i.MtimeNsec(), + } + stat.Atime = stat.Mtime + stat.Ctime = stat.Mtime + stat.DevMajor = linux.UNNAMED_MAJOR + stat.DevMinor = i.fs.devMinor +} + +func (i *inode) fileType() uint16 { + return i.Mode() & linux.S_IFMT +} + +// dentry implements vfs.DentryImpl. +// +// The filesystem is read-only and currently we never drop the cached dentries +// until the filesystem is unmounted. The reference model works like this: +// +// - The initial reference count of each dentry is one, which is the reference +// held by the parent (so when the reference count is one, it also means that +// this is a cached dentry, i.e. not in use). +// +// - When a dentry is used (e.g. opened by someone), its reference count will +// be increased and the new reference is held by caller. +// +// - The reference count of root dentry is two. One reference is returned to +// the caller of `GetFilesystem()`, and the other is held by `fs`. +// +// TODO: This can lead to unbounded memory growth in sentry due to the ever-growing +// dentry tree. We should have a dentry LRU cache, similar to what fsimpl/gofer does. +// +// +stateify savable +type dentry struct { + vfsd vfs.Dentry + + // dentryRefs is the reference count. + dentryRefs + + // parent is this dentry's parent directory. If this dentry is + // a file system root, parent is nil. + parent atomic.Pointer[dentry] `state:".(*dentry)"` + + // name is this dentry's name in its parent. If this dentry is + // a file system root, name is the empty string. + name string + + // inode is the inode represented by this dentry. + inode *inode + + // dirMu serializes changes to the dentry tree. + dirMu sync.RWMutex `state:"nosave"` + + // childMap contains the mappings of child names to dentries if this + // dentry represents a directory. + // +checklocks:dirMu + childMap map[string]*dentry +} + +// The caller is expected to handle dentry insertion into dentry tree. +func (fs *filesystem) newDentry(nid uint64) (*dentry, error) { + i, err := fs.getInode(nid) + if err != nil { + return nil, err + } + d := &dentry{ + inode: i, + } + d.InitRefs() + d.vfsd.Init(d) + return d, nil +} + +// DecRef implements vfs.DentryImpl.DecRef. +func (d *dentry) DecRef(ctx context.Context) { + d.dentryRefs.DecRef(func() { + d.dirMu.Lock() + for _, c := range d.childMap { + c.DecRef(ctx) + } + d.childMap = nil + d.dirMu.Unlock() + d.inode.DecRef(ctx) + }) +} + +// InotifyWithParent implements vfs.DentryImpl.InotifyWithParent. +func (d *dentry) InotifyWithParent(ctx context.Context, events, cookie uint32, et vfs.EventType) { + if d.inode.IsDir() { + events |= linux.IN_ISDIR + } + // The ordering below is important, Linux always notifies the parent first. + if parent := d.parent.Load(); parent != nil { + parent.inode.watches.Notify(ctx, d.name, events, cookie, et, false) + } + d.inode.watches.Notify(ctx, "", events, cookie, et, false) +} + +// Watches implements vfs.DentryImpl.Watches. +func (d *dentry) Watches() *vfs.Watches { + return &d.inode.watches +} + +// OnZeroWatches implements vfs.DentryImpl.OnZeroWatches. +func (d *dentry) OnZeroWatches(ctx context.Context) {} + +func (d *dentry) open(ctx context.Context, rp *vfs.ResolvingPath, opts *vfs.OpenOptions) (*vfs.FileDescription, error) { + ats := vfs.AccessTypesForOpenFlags(opts) + if err := d.inode.checkPermissions(rp.Credentials(), ats); err != nil { + return nil, err + } + + switch d.inode.fileType() { + case linux.S_IFREG: + if ats&vfs.MayWrite != 0 { + return nil, linuxerr.EROFS + } + var fd regularFileFD + fd.LockFD.Init(&d.inode.locks) + if err := fd.vfsfd.Init(&fd, opts.Flags, rp.Mount(), &d.vfsd, &vfs.FileDescriptionOptions{AllowDirectIO: true}); err != nil { + return nil, err + } + return &fd.vfsfd, nil + + case linux.S_IFDIR: + // Can't open directories with O_CREAT. + if opts.Flags&linux.O_CREAT != 0 { + return nil, linuxerr.EISDIR + } + // Can't open directories writably. + if ats&vfs.MayWrite != 0 { + return nil, linuxerr.EISDIR + } + if opts.Flags&linux.O_DIRECT != 0 { + return nil, linuxerr.EINVAL + } + var fd directoryFD + fd.LockFD.Init(&d.inode.locks) + if err := fd.vfsfd.Init(&fd, opts.Flags, rp.Mount(), &d.vfsd, &vfs.FileDescriptionOptions{AllowDirectIO: true}); err != nil { + return nil, err + } + return &fd.vfsfd, nil + + case linux.S_IFLNK: + // Can't open symlinks without O_PATH, which is handled at the VFS layer. + return nil, linuxerr.ELOOP + + default: + return nil, linuxerr.ENXIO + } +} + +// +stateify savable +type fileDescription struct { + vfsfd vfs.FileDescription + vfs.FileDescriptionDefaultImpl + vfs.LockFD + + lockLogging sync.Once `state:"nosave"` +} + +func (fd *fileDescription) filesystem() *filesystem { + return fd.vfsfd.Mount().Filesystem().Impl().(*filesystem) +} + +func (fd *fileDescription) dentry() *dentry { + return fd.vfsfd.Dentry().Impl().(*dentry) +} + +func (fd *fileDescription) inode() *inode { + return fd.dentry().inode +} + +// Stat implements vfs.FileDescriptionImpl.Stat. +func (fd *fileDescription) Stat(ctx context.Context, opts vfs.StatOptions) (linux.Statx, error) { + var stat linux.Statx + fd.inode().statTo(&stat) + return stat, nil +} + +// SetStat implements vfs.FileDescriptionImpl.SetStat. +func (fd *fileDescription) SetStat(ctx context.Context, opts vfs.SetStatOptions) error { + return linuxerr.EROFS +} + +// StatFS implements vfs.FileDescriptionImpl.StatFS. +func (fd *fileDescription) StatFS(ctx context.Context) (linux.Statfs, error) { + return fd.filesystem().statFS(), nil +} + +// ListXattr implements vfs.FileDescriptionImpl.ListXattr. +func (fd *fileDescription) ListXattr(ctx context.Context, size uint64) ([]string, error) { + return nil, linuxerr.ENOTSUP +} + +// GetXattr implements vfs.FileDescriptionImpl.GetXattr. +func (fd *fileDescription) GetXattr(ctx context.Context, opts vfs.GetXattrOptions) (string, error) { + return "", linuxerr.ENOTSUP +} + +// SetXattr implements vfs.FileDescriptionImpl.SetXattr. +func (fd *fileDescription) SetXattr(ctx context.Context, opts vfs.SetXattrOptions) error { + return linuxerr.EROFS +} + +// RemoveXattr implements vfs.FileDescriptionImpl.RemoveXattr. +func (fd *fileDescription) RemoveXattr(ctx context.Context, name string) error { + return linuxerr.EROFS +} + +// Sync implements vfs.FileDescriptionImpl.Sync. +func (*fileDescription) Sync(context.Context) error { + return nil +} + +// Release implements vfs.FileDescriptionImpl.Release. +func (*fileDescription) Release(ctx context.Context) {} diff --git a/pkg/sentry/fsimpl/erofs/filesystem.go b/pkg/sentry/fsimpl/erofs/filesystem.go new file mode 100644 index 000000000..e2441faf3 --- /dev/null +++ b/pkg/sentry/fsimpl/erofs/filesystem.go @@ -0,0 +1,444 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package erofs + +import ( + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/erofs" + "gvisor.dev/gvisor/pkg/errors/linuxerr" + "gvisor.dev/gvisor/pkg/fspath" + "gvisor.dev/gvisor/pkg/sentry/kernel/auth" + "gvisor.dev/gvisor/pkg/sentry/socket/unix/transport" + "gvisor.dev/gvisor/pkg/sentry/vfs" +) + +// step resolves rp.Component() to an existing file, starting from the given directory. +// +// step is loosely analogous to fs/namei.c:walk_component(). +// +// Preconditions: +// - !rp.Done(). +func step(ctx context.Context, rp *vfs.ResolvingPath, d *dentry) (*dentry, bool, error) { + if !d.inode.IsDir() { + return nil, false, linuxerr.ENOTDIR + } + if err := d.inode.checkPermissions(rp.Credentials(), vfs.MayExec); err != nil { + return nil, false, err + } + name := rp.Component() + if name == "." { + rp.Advance() + return d, false, nil + } + if name == ".." { + parent := d.parent.Load() + if isRoot, err := rp.CheckRoot(ctx, &d.vfsd); err != nil { + return nil, false, err + } else if isRoot || parent == nil { + rp.Advance() + return d, false, nil + } + if err := rp.CheckMount(ctx, &parent.vfsd); err != nil { + return nil, false, err + } + rp.Advance() + return parent, false, nil + } + if len(name) > erofs.MaxNameLen { + return nil, false, linuxerr.ENAMETOOLONG + } + child, err := d.lookup(ctx, name) + if err != nil { + return nil, false, err + } + if err := rp.CheckMount(ctx, &child.vfsd); err != nil { + return nil, false, err + } + if child.inode.IsSymlink() && rp.ShouldFollowSymlink() { + target, err := child.inode.Readlink() + if err != nil { + return nil, false, err + } + followedSymlink, err := rp.HandleSymlink(target) + return d, followedSymlink, err + } + rp.Advance() + return child, false, nil +} + +// walkParentDir resolves all but the last path component of rp to an existing +// directory, starting from the gvien directory. It does not check that the +// returned directory is searchable by the provider of rp. +// +// walkParentDir is loosely analogous to Linux's fs/namei.c:path_parentat(). +// +// Preconditions: +// - !rp.Done(). +func walkParentDir(ctx context.Context, rp *vfs.ResolvingPath, d *dentry) (*dentry, error) { + for !rp.Final() { + next, _, err := step(ctx, rp, d) + if err != nil { + return nil, err + } + d = next + } + if !d.inode.IsDir() { + return nil, linuxerr.ENOTDIR + } + return d, nil +} + +// resolve resolves rp to an existing file. +// +// resolve is loosely analogous to Linux's fs/namei.c:path_lookupat(). +func resolve(ctx context.Context, rp *vfs.ResolvingPath) (*dentry, error) { + d := rp.Start().Impl().(*dentry) + for !rp.Done() { + next, _, err := step(ctx, rp, d) + if err != nil { + return nil, err + } + d = next + } + if rp.MustBeDir() && !d.inode.IsDir() { + return nil, linuxerr.ENOTDIR + } + return d, nil +} + +// doCreateAt checks that creating a file at rp is permitted. +// +// doCreateAt is loosely analogous to a conjunction of Linux's +// fs/namei.c:filename_create() and done_path_create(). +// +// Preconditions: +// - !rp.Done(). +// - For the final path component in rp, !rp.ShouldFollowSymlink(). +func (fs *filesystem) doCreateAt(ctx context.Context, rp *vfs.ResolvingPath, dir bool) error { + parentDir, err := walkParentDir(ctx, rp, rp.Start().Impl().(*dentry)) + if err != nil { + return err + } + // Order of checks is important. First check if parent directory can be + // executed, then check for existence, and lastly check if mount is writable. + if err := parentDir.inode.checkPermissions(rp.Credentials(), vfs.MayExec); err != nil { + return err + } + name := rp.Component() + if name == "." || name == ".." { + return linuxerr.EEXIST + } + if len(name) > erofs.MaxNameLen { + return linuxerr.ENAMETOOLONG + } + if _, err := parentDir.lookup(ctx, name); err == nil { + return linuxerr.EEXIST + } else if !linuxerr.Equals(linuxerr.ENOENT, err) { + return err + } + if !dir && rp.MustBeDir() { + return linuxerr.ENOENT + } + return linuxerr.EROFS +} + +// Sync implements vfs.FilesystemImpl.Sync. +func (fs *filesystem) Sync(ctx context.Context) error { + return nil +} + +// AccessAt implements vfs.FilesystemImpl.AccessAt. +func (fs *filesystem) AccessAt(ctx context.Context, rp *vfs.ResolvingPath, creds *auth.Credentials, ats vfs.AccessTypes) error { + d, err := resolve(ctx, rp) + if err != nil { + return err + } + if ats.MayWrite() { + return linuxerr.EROFS + } + return d.inode.checkPermissions(creds, ats) +} + +// GetDentryAt implements vfs.FilesystemImpl.GetDentryAt. +func (fs *filesystem) GetDentryAt(ctx context.Context, rp *vfs.ResolvingPath, opts vfs.GetDentryOptions) (*vfs.Dentry, error) { + d, err := resolve(ctx, rp) + if err != nil { + return nil, err + } + if opts.CheckSearchable { + if !d.inode.IsDir() { + return nil, linuxerr.ENOTDIR + } + if err := d.inode.checkPermissions(rp.Credentials(), vfs.MayExec); err != nil { + return nil, err + } + } + d.IncRef() + return &d.vfsd, nil +} + +// GetParentDentryAt implements vfs.FilesystemImpl.GetParentDentryAt. +func (fs *filesystem) GetParentDentryAt(ctx context.Context, rp *vfs.ResolvingPath) (*vfs.Dentry, error) { + dir, err := walkParentDir(ctx, rp, rp.Start().Impl().(*dentry)) + if err != nil { + return nil, err + } + dir.IncRef() + return &dir.vfsd, nil +} + +// LinkAt implements vfs.FilesystemImpl.LinkAt. +func (fs *filesystem) LinkAt(ctx context.Context, rp *vfs.ResolvingPath, vd vfs.VirtualDentry) error { + return fs.doCreateAt(ctx, rp, false /* dir */) +} + +// MkdirAt implements vfs.FilesystemImpl.MkdirAt. +func (fs *filesystem) MkdirAt(ctx context.Context, rp *vfs.ResolvingPath, opts vfs.MkdirOptions) error { + return fs.doCreateAt(ctx, rp, true /* dir */) +} + +// MknodAt implements vfs.FilesystemImpl.MknodAt. +func (fs *filesystem) MknodAt(ctx context.Context, rp *vfs.ResolvingPath, opts vfs.MknodOptions) error { + return fs.doCreateAt(ctx, rp, false /* dir */) +} + +// OpenAt implements vfs.FilesystemImpl.OpenAt. +func (fs *filesystem) OpenAt(ctx context.Context, rp *vfs.ResolvingPath, opts vfs.OpenOptions) (*vfs.FileDescription, error) { + if opts.Flags&linux.O_TMPFILE != 0 { + return nil, linuxerr.EOPNOTSUPP + } + + if opts.Flags&linux.O_CREAT == 0 { + d, err := resolve(ctx, rp) + if err != nil { + return nil, err + } + return d.open(ctx, rp, &opts) + } + + mustCreate := opts.Flags&linux.O_EXCL != 0 + start := rp.Start().Impl().(*dentry) + if rp.Done() { + // Reject attempts to open mount root directory with O_CREAT. + if rp.MustBeDir() { + return nil, linuxerr.EISDIR + } + if mustCreate { + return nil, linuxerr.EEXIST + } + return start.open(ctx, rp, &opts) + } +afterTrailingSymlink: + parentDir, err := walkParentDir(ctx, rp, start) + if err != nil { + return nil, err + } + // Check for search permission in the parent directory. + if err := parentDir.inode.checkPermissions(rp.Credentials(), vfs.MayExec); err != nil { + return nil, err + } + // Reject attempts to open directories with O_CREAT. + if rp.MustBeDir() { + return nil, linuxerr.EISDIR + } + child, followedSymlink, err := step(ctx, rp, parentDir) + if followedSymlink { + if mustCreate { + // EEXIST must be returned if an existing symlink is opened with O_EXCL. + return nil, linuxerr.EEXIST + } + if err != nil { + // If followedSymlink && err != nil, then this symlink resolution error + // must be handled by the VFS layer. + return nil, err + } + start = parentDir + goto afterTrailingSymlink + } + if linuxerr.Equals(linuxerr.ENOENT, err) { + return nil, linuxerr.EROFS + } + if err != nil { + return nil, err + } + if mustCreate { + return nil, linuxerr.EEXIST + } + if rp.MustBeDir() && !child.inode.IsDir() { + return nil, linuxerr.ENOTDIR + } + return child.open(ctx, rp, &opts) +} + +// ReadlinkAt implements vfs.FilesystemImpl.ReadlinkAt. +func (fs *filesystem) ReadlinkAt(ctx context.Context, rp *vfs.ResolvingPath) (string, error) { + d, err := resolve(ctx, rp) + if err != nil { + return "", err + } + return d.inode.Readlink() +} + +// RenameAt implements vfs.FilesystemImpl.RenameAt. +func (fs *filesystem) RenameAt(ctx context.Context, rp *vfs.ResolvingPath, oldParentVD vfs.VirtualDentry, oldName string, opts vfs.RenameOptions) error { + // Resolve newParent first to verify that it's on this Mount. + newParentDir, err := walkParentDir(ctx, rp, rp.Start().Impl().(*dentry)) + if err != nil { + return err + } + newName := rp.Component() + if len(newName) > erofs.MaxNameLen { + return linuxerr.ENAMETOOLONG + } + mnt := rp.Mount() + if mnt != oldParentVD.Mount() { + return linuxerr.EXDEV + } + if err := newParentDir.inode.checkPermissions(rp.Credentials(), vfs.MayWrite|vfs.MayExec); err != nil { + return err + } + oldParentDir := oldParentVD.Dentry().Impl().(*dentry) + if err := oldParentDir.inode.checkPermissions(rp.Credentials(), vfs.MayWrite|vfs.MayExec); err != nil { + return err + } + return linuxerr.EROFS +} + +// RmdirAt implements vfs.FilesystemImpl.RmdirAt. +func (fs *filesystem) RmdirAt(ctx context.Context, rp *vfs.ResolvingPath) error { + parentDir, err := walkParentDir(ctx, rp, rp.Start().Impl().(*dentry)) + if err != nil { + return err + } + if err := parentDir.inode.checkPermissions(rp.Credentials(), vfs.MayExec); err != nil { + return err + } + name := rp.Component() + if name == "." { + return linuxerr.EINVAL + } + if name == ".." { + return linuxerr.ENOTEMPTY + } + return linuxerr.EROFS +} + +// SetStatAt implements vfs.FilesystemImpl.SetStatAt. +func (fs *filesystem) SetStatAt(ctx context.Context, rp *vfs.ResolvingPath, opts vfs.SetStatOptions) error { + if _, err := resolve(ctx, rp); err != nil { + return err + } + return linuxerr.EROFS +} + +// StatAt implements vfs.FilesystemImpl.StatAt. +func (fs *filesystem) StatAt(ctx context.Context, rp *vfs.ResolvingPath, opts vfs.StatOptions) (linux.Statx, error) { + d, err := resolve(ctx, rp) + if err != nil { + return linux.Statx{}, err + } + var stat linux.Statx + d.inode.statTo(&stat) + return stat, nil +} + +// StatFSAt implements vfs.FilesystemImpl.StatFSAt. +func (fs *filesystem) StatFSAt(ctx context.Context, rp *vfs.ResolvingPath) (linux.Statfs, error) { + if _, err := resolve(ctx, rp); err != nil { + return linux.Statfs{}, err + } + 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 */) +} + +// UnlinkAt implements vfs.FilesystemImpl.UnlinkAt. +func (fs *filesystem) UnlinkAt(ctx context.Context, rp *vfs.ResolvingPath) error { + parentDir, err := walkParentDir(ctx, rp, rp.Start().Impl().(*dentry)) + if err != nil { + return err + } + if err := parentDir.inode.checkPermissions(rp.Credentials(), vfs.MayExec); err != nil { + return err + } + name := rp.Component() + if name == "." || name == ".." { + return linuxerr.EISDIR + } + return linuxerr.EROFS +} + +// BoundEndpointAt implements vfs.FilesystemImpl.BoundEndpointAt. +func (fs *filesystem) BoundEndpointAt(ctx context.Context, rp *vfs.ResolvingPath, opts vfs.BoundEndpointOptions) (transport.BoundEndpoint, error) { + d, err := resolve(ctx, rp) + if err != nil { + return nil, err + } + if err := d.inode.checkPermissions(rp.Credentials(), vfs.MayWrite); err != nil { + return nil, err + } + return nil, linuxerr.ECONNREFUSED +} + +// ListXattrAt implements vfs.FilesystemImpl.ListXattrAt. +func (fs *filesystem) ListXattrAt(ctx context.Context, rp *vfs.ResolvingPath, size uint64) ([]string, error) { + if _, err := resolve(ctx, rp); err != nil { + return nil, err + } + return nil, linuxerr.ENOTSUP +} + +// GetXattrAt implements vfs.FilesystemImpl.GetXattrAt. +func (fs *filesystem) GetXattrAt(ctx context.Context, rp *vfs.ResolvingPath, opts vfs.GetXattrOptions) (string, error) { + if _, err := resolve(ctx, rp); err != nil { + return "", err + } + return "", linuxerr.ENOTSUP +} + +// SetXattrAt implements vfs.FilesystemImpl.SetXattrAt. +func (fs *filesystem) SetXattrAt(ctx context.Context, rp *vfs.ResolvingPath, opts vfs.SetXattrOptions) error { + if _, err := resolve(ctx, rp); err != nil { + return err + } + return linuxerr.EROFS +} + +// RemoveXattrAt implements vfs.FilesystemImpl.RemoveXattrAt. +func (fs *filesystem) RemoveXattrAt(ctx context.Context, rp *vfs.ResolvingPath, name string) error { + if _, err := resolve(ctx, rp); err != nil { + return err + } + return linuxerr.EROFS +} + +// PrependPath implements vfs.FilesystemImpl.PrependPath. +func (fs *filesystem) PrependPath(ctx context.Context, vfsroot, vd vfs.VirtualDentry, b *fspath.Builder) error { + return genericPrependPath(vfsroot, vd.Mount(), vd.Dentry().Impl().(*dentry), b) +} + +// MountOptions implements vfs.FilesystemImpl.MountOptions. +func (fs *filesystem) MountOptions() string { + return fs.mopts +} + +// IsDescendant implements vfs.FilesystemImpl.IsDescendant. +func (fs *filesystem) IsDescendant(vfsroot, vd vfs.VirtualDentry) bool { + return genericIsDescendant(vfsroot.Dentry(), vd.Dentry().Impl().(*dentry)) +} diff --git a/pkg/sentry/fsimpl/erofs/regular_file.go b/pkg/sentry/fsimpl/erofs/regular_file.go new file mode 100644 index 000000000..a2e541e2f --- /dev/null +++ b/pkg/sentry/fsimpl/erofs/regular_file.go @@ -0,0 +1,207 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package erofs + +import ( + "io" + "sync" + + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/erofs" + "gvisor.dev/gvisor/pkg/errors/linuxerr" + "gvisor.dev/gvisor/pkg/hostarch" + "gvisor.dev/gvisor/pkg/safemem" + "gvisor.dev/gvisor/pkg/sentry/memmap" + "gvisor.dev/gvisor/pkg/sentry/vfs" + "gvisor.dev/gvisor/pkg/usermem" +) + +// +stateify savable +type regularFileFD struct { + fileDescription + + // offMu protects off. + offMu sync.Mutex `state:"nosave"` + + // off is the file offset. + // +checklocks:offMu + off int64 +} + +// PRead implements vfs.FileDescriptionImpl.PRead. +func (fd *regularFileFD) PRead(ctx context.Context, dst usermem.IOSequence, offset int64, opts vfs.ReadOptions) (int64, error) { + if offset < 0 { + return 0, linuxerr.EINVAL + } + + // Check that flags are supported. + // + // TODO(gvisor.dev/issue/2601): Support select preadv2 flags. + if opts.Flags&^linux.RWF_HIPRI != 0 { + return 0, linuxerr.EOPNOTSUPP + } + + if dst.NumBytes() == 0 { + return 0, nil + } + + data, err := fd.inode().Data() + if err != nil { + return 0, err + } + r := ®ularFileReader{ + data: data, + off: uint64(offset), + } + return dst.CopyOutFrom(ctx, r) +} + +type regularFileReader struct { + data safemem.BlockSeq + off uint64 +} + +// ReadToBlocks implements safemem.Reader.ReadToBlocks. +func (r *regularFileReader) ReadToBlocks(dsts safemem.BlockSeq) (uint64, error) { + if r.off >= r.data.NumBytes() { + return 0, io.EOF + } + cp, err := safemem.CopySeq(dsts, r.data.DropFirst(int(r.off))) + r.off += cp + return cp, err +} + +// Read implements vfs.FileDescriptionImpl.Read. +func (fd *regularFileFD) Read(ctx context.Context, dst usermem.IOSequence, opts vfs.ReadOptions) (int64, error) { + fd.offMu.Lock() + n, err := fd.PRead(ctx, dst, fd.off, opts) + fd.off += n + fd.offMu.Unlock() + return n, err +} + +// PWrite implements vfs.FileDescriptionImpl.PWrite. +func (fd *regularFileFD) PWrite(ctx context.Context, src usermem.IOSequence, offset int64, opts vfs.WriteOptions) (int64, error) { + return 0, linuxerr.EROFS +} + +// Write implements vfs.FileDescriptionImpl.Write. +func (fd *regularFileFD) Write(ctx context.Context, src usermem.IOSequence, opts vfs.WriteOptions) (int64, error) { + return 0, linuxerr.EROFS +} + +// Seek implements vfs.FileDescriptionImpl.Seek. +func (fd *regularFileFD) Seek(ctx context.Context, offset int64, whence int32) (int64, error) { + fd.offMu.Lock() + defer fd.offMu.Unlock() + switch whence { + case linux.SEEK_SET: + // use offset as specified + case linux.SEEK_CUR: + offset += fd.off + case linux.SEEK_END: + offset += int64(fd.inode().Size()) + default: + return 0, linuxerr.EINVAL + } + if offset < 0 { + return 0, linuxerr.EINVAL + } + fd.off = offset + return offset, nil +} + +// ConfigureMMap implements vfs.FileDescriptionImpl.ConfigureMMap. +func (fd *regularFileFD) ConfigureMMap(ctx context.Context, opts *memmap.MMapOpts) error { + return vfs.GenericConfigureMMap(&fd.vfsfd, fd.inode(), opts) +} + +// AddMapping implements memmap.Mappable.AddMapping. +func (i *inode) AddMapping(ctx context.Context, ms memmap.MappingSpace, ar hostarch.AddrRange, offset uint64, writable bool) error { + return nil +} + +// RemoveMapping implements memmap.Mappable.RemoveMapping. +func (i *inode) RemoveMapping(ctx context.Context, ms memmap.MappingSpace, ar hostarch.AddrRange, offset uint64, writable bool) { +} + +// CopyMapping implements memmap.Mappable.CopyMapping. +func (i *inode) CopyMapping(ctx context.Context, ms memmap.MappingSpace, srcAR, dstAR hostarch.AddrRange, offset uint64, writable bool) error { + return nil +} + +// Translate implements memmap.Mappable.Translate. +func (i *inode) Translate(ctx context.Context, required, optional memmap.MappableRange, at hostarch.AccessType) ([]memmap.Translation, error) { + pgend, _ := hostarch.PageRoundUp(i.Size()) + if required.End > pgend { + if required.Start >= pgend { + return nil, &memmap.BusError{io.EOF} + } + required.End = pgend + } + if optional.End > pgend { + optional.End = pgend + } + if at.Write { + return nil, &memmap.BusError{linuxerr.EROFS} + } + offset, err := i.DataOffset() + if err != nil { + return nil, &memmap.BusError{err} + } + mr := optional + return []memmap.Translation{ + { + Source: mr, + File: &i.fs.mf, + Offset: mr.Start + offset, + Perms: at, + }, + }, nil +} + +// InvalidateUnsavable implements memmap.Mappable.InvalidateUnsavable. +func (i *inode) InvalidateUnsavable(ctx context.Context) error { + return nil +} + +// +stateify savable +type imageMemmapFile struct { + image *erofs.Image +} + +// IncRef implements memmap.File.IncRef. +func (mf *imageMemmapFile) IncRef(fr memmap.FileRange, memCgID uint32) {} + +// DecRef implements memmap.File.DecRef. +func (mf *imageMemmapFile) DecRef(fr memmap.FileRange) {} + +// MapInternal implements memmap.File.MapInternal. +func (mf *imageMemmapFile) MapInternal(fr memmap.FileRange, at hostarch.AccessType) (safemem.BlockSeq, error) { + if at.Write { + return safemem.BlockSeq{}, &memmap.BusError{linuxerr.EROFS} + } + bytes, err := mf.image.BytesAt(fr.Start, fr.Length()) + if err != nil { + return safemem.BlockSeq{}, &memmap.BusError{err} + } + return safemem.BlockSeqOf(safemem.BlockFromSafeSlice(bytes)), nil +} + +// FD implements memmap.File.FD. +func (mf *imageMemmapFile) FD() int { + return mf.image.FD() +} diff --git a/pkg/sentry/fsimpl/erofs/save_restore.go b/pkg/sentry/fsimpl/erofs/save_restore.go new file mode 100644 index 000000000..d979881d6 --- /dev/null +++ b/pkg/sentry/fsimpl/erofs/save_restore.go @@ -0,0 +1,27 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package erofs + +// TODO: support checkpoint/restore. + +// saveParent is called by stateify. +func (d *dentry) saveParent() *dentry { + return d.parent.Load() +} + +// loadParent is called by stateify. +func (d *dentry) loadParent(parent *dentry) { + d.parent.Store(parent) +} diff --git a/runsc/boot/BUILD b/runsc/boot/BUILD index 6dea1367c..2654913b9 100644 --- a/runsc/boot/BUILD +++ b/runsc/boot/BUILD @@ -60,6 +60,7 @@ go_library( "//pkg/sentry/fsimpl/cgroupfs", "//pkg/sentry/fsimpl/devpts", "//pkg/sentry/fsimpl/devtmpfs", + "//pkg/sentry/fsimpl/erofs", "//pkg/sentry/fsimpl/fuse", "//pkg/sentry/fsimpl/gofer", "//pkg/sentry/fsimpl/host", diff --git a/runsc/boot/vfs.go b/runsc/boot/vfs.go index b3e66b552..a8ede4cf5 100644 --- a/runsc/boot/vfs.go +++ b/runsc/boot/vfs.go @@ -39,6 +39,7 @@ import ( "gvisor.dev/gvisor/pkg/sentry/fsimpl/cgroupfs" "gvisor.dev/gvisor/pkg/sentry/fsimpl/devpts" "gvisor.dev/gvisor/pkg/sentry/fsimpl/devtmpfs" + "gvisor.dev/gvisor/pkg/sentry/fsimpl/erofs" "gvisor.dev/gvisor/pkg/sentry/fsimpl/fuse" "gvisor.dev/gvisor/pkg/sentry/fsimpl/gofer" "gvisor.dev/gvisor/pkg/sentry/fsimpl/mqfs" @@ -101,6 +102,9 @@ func registerFilesystems(k *kernel.Kernel, info *containerInfo) error { AllowUserMount: true, AllowUserList: true, }) + vfsObj.MustRegisterFilesystemType(erofs.Name, &erofs.FilesystemType{}, &vfs.RegisterFilesystemTypeOptions{ + AllowUserList: true, + }) vfsObj.MustRegisterFilesystemType(fuse.Name, &fuse.FilesystemType{}, &vfs.RegisterFilesystemTypeOptions{ AllowUserMount: true, AllowUserList: true,