From eca83ac68cdac5846b3836f3e77c0a7e399491b5 Mon Sep 17 00:00:00 2001 From: Tiwei Bie Date: Tue, 3 Oct 2023 20:19:53 +0800 Subject: [PATCH] Add initial support for EROFS This patch adds initial support for EROFS [1]. Below is a brief summary of the supported features. Both inode formats are supported: - compact format (32 bytes); - extended format (64 bytes); Below data layouts are supported: - flat file data without data inline (no extent); - flat file data with tail packing data inline (no extent); Below file types are supported: - directory; - regular file; - symlink; Special files (e.g. fifo) can be listed, but cannot be accessed. With this patch, sentry will be able to mount the EROFS image created with below command and access the files on it. mkfs.erofs -E noinline_data [1] https://docs.kernel.org/filesystems/erofs.html Updates #8956 Signed-off-by: Tiwei Bie --- pkg/abi/linux/file.go | 34 ++ pkg/erofs/BUILD | 31 + pkg/erofs/erofs.go | 730 ++++++++++++++++++++++++ pkg/erofs/erofs_test.go | 37 ++ pkg/sentry/fsimpl/erofs/BUILD | 74 +++ pkg/sentry/fsimpl/erofs/directory.go | 172 ++++++ pkg/sentry/fsimpl/erofs/erofs.go | 523 +++++++++++++++++ pkg/sentry/fsimpl/erofs/filesystem.go | 444 ++++++++++++++ pkg/sentry/fsimpl/erofs/regular_file.go | 207 +++++++ pkg/sentry/fsimpl/erofs/save_restore.go | 27 + runsc/boot/BUILD | 1 + runsc/boot/vfs.go | 4 + 12 files changed, 2284 insertions(+) create mode 100644 pkg/erofs/BUILD create mode 100644 pkg/erofs/erofs.go create mode 100644 pkg/erofs/erofs_test.go create mode 100644 pkg/sentry/fsimpl/erofs/BUILD create mode 100644 pkg/sentry/fsimpl/erofs/directory.go create mode 100644 pkg/sentry/fsimpl/erofs/erofs.go create mode 100644 pkg/sentry/fsimpl/erofs/filesystem.go create mode 100644 pkg/sentry/fsimpl/erofs/regular_file.go create mode 100644 pkg/sentry/fsimpl/erofs/save_restore.go 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,