Merge pull request #9308 from btw616:erofs-initial-support

PiperOrigin-RevId: 571416660
This commit is contained in:
gVisor bot
2023-10-06 13:14:45 -07:00
19 changed files with 2578 additions and 5 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y curl gnupg2 git \
apt-transport-https ca-certificates gnupg-agent \
software-properties-common \
pkg-config libffi-dev patch diffutils libssl-dev iptables kmod \
clang crossbuild-essential-amd64
clang crossbuild-essential-amd64 erofs-utils
# Install Docker client for the website build.
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
+34
View File
@@ -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
+31
View File
@@ -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",
)
+730
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -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())
}
}
+74
View File
@@ -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",
],
)
+172
View File
@@ -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
}
File diff suppressed because it is too large Load Diff
+444
View File
@@ -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))
}
+207
View File
@@ -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 := &regularFileReader{
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()
}
+27
View File
@@ -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)
}
+1
View File
@@ -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",
+100
View File
@@ -18,14 +18,19 @@ import (
"errors"
"fmt"
"os"
"path"
gtime "time"
specs "github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/cleanup"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/control/server"
"gvisor.dev/gvisor/pkg/fd"
"gvisor.dev/gvisor/pkg/fspath"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sentry/control"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/erofs"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/seccheck"
"gvisor.dev/gvisor/pkg/sentry/socket/netstack"
@@ -95,6 +100,9 @@ const (
// ContMgrProcfsDump dumps sandbox procfs state.
ContMgrProcfsDump = "containerManager.ProcfsDump"
// ContMgrMount mounts a filesystem in a container.
ContMgrMount = "containerManager.Mount"
)
const (
@@ -673,3 +681,95 @@ func (cm *containerManager) ProcfsDump(_ *struct{}, out *[]procfs.ProcessProcfsD
}
return nil
}
// MountArgs contains arguments to the Mount method.
type MountArgs struct {
// ContainerID is the container in which we will mount the filesystem.
ContainerID string
// Source is the mount source.
Source string
// Destination is the mount target.
Destination string
// FsType is the filesystem type.
FsType string
// FilePayload contains the source image FD, if required by the filesystem.
urpc.FilePayload
}
const initTID kernel.ThreadID = 1
// Mount mounts a filesystem in a container.
func (cm *containerManager) Mount(args *MountArgs, _ *struct{}) error {
log.Debugf("containerManager.Mount, cid: %s, args: %+v", args.ContainerID, args)
var cu cleanup.Cleanup
defer cu.Clean()
eid := execID{cid: args.ContainerID}
ep, ok := cm.l.processes[eid]
if !ok {
return fmt.Errorf("container %v is deleted", args.ContainerID)
}
if ep.tg == nil {
return fmt.Errorf("container %v isn't started", args.ContainerID)
}
t := ep.tg.PIDNamespace().TaskWithID(initTID)
if t == nil {
return fmt.Errorf("failed to find init process")
}
source := args.Source
dest := path.Clean(args.Destination)
fstype := args.FsType
if dest[0] != '/' {
return fmt.Errorf("absolute path must be provided for destination")
}
var opts vfs.MountOptions
switch fstype {
case erofs.Name:
if len(args.FilePayload.Files) != 1 {
return fmt.Errorf("exactly one image file must be provided")
}
imageFD, err := unix.Dup(int(args.FilePayload.Files[0].Fd()))
if err != nil {
return fmt.Errorf("failed to dup image FD: %v", err)
}
cu.Add(func() { unix.Close(imageFD) })
opts = vfs.MountOptions{
ReadOnly: true,
GetFilesystemOptions: vfs.GetFilesystemOptions{
Data: fmt.Sprintf("ifd=%d", imageFD),
},
InternalMount: true,
}
default:
return fmt.Errorf("unsupported filesystem type: %v", fstype)
}
ctx := context.Background()
root := t.FSContext().RootDirectory()
defer root.DecRef(ctx)
pop := vfs.PathOperation{
Root: root,
Start: root,
Path: fspath.Parse(dest),
}
if _, err := t.Kernel().VFS().MountAt(ctx, t.Credentials(), source, &pop, fstype, &opts); err != nil {
return err
}
log.Infof("Mounted %q to %q type: %s, internal-options: %q, in container %q", source, dest, fstype, opts.GetFilesystemOptions.Data, args.ContainerID)
cu.Release()
return nil
}
+4
View File
@@ -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,
+14
View File
@@ -49,6 +49,7 @@ type Debug struct {
delay time.Duration
duration time.Duration
ps bool
mount string
}
// Name implements subcommands.Command.
@@ -82,6 +83,7 @@ func (d *Debug) SetFlags(f *flag.FlagSet) {
f.StringVar(&d.logLevel, "log-level", "", "The log level to set: warning (0), info (1), or debug (2).")
f.StringVar(&d.logPackets, "log-packets", "", "A boolean value to enable or disable packet logging: true or false.")
f.BoolVar(&d.ps, "ps", false, "lists processes")
f.StringVar(&d.mount, "mount", "", "Mount a filesystem (-mount fstype:source:destination).")
}
// Execute implements subcommands.Command.Execute.
@@ -224,6 +226,18 @@ func (d *Debug) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomm
}
util.Infof("%s", o)
}
if d.mount != "" {
opts := strings.Split(d.mount, ":")
if len(opts) != 3 {
util.Fatalf("Mount failed: invalid option: %v", d.mount)
}
fstype := opts[0]
src := opts[1]
dest := opts[2]
if err := c.Sandbox.Mount(c.ID, fstype, src, dest); err != nil {
util.Fatalf(err.Error())
}
}
// Open profiling files.
var (
+133
View File
@@ -21,6 +21,7 @@ import (
"io/ioutil"
"math"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
@@ -3135,3 +3136,135 @@ func TestOverlayByMountAnnotation(t *testing.T) {
t.Fatalf("overlay filestore at %q was not deleted after container.Destroy()", filestoreFile)
}
}
// TestMountEROFS checks that the checksums from the target directory in container
// are identical with the ones from the source directory on host.
func TestMountEROFS(t *testing.T) {
// Skip this test if mkfs.erofs is not available.
mkfs, err := exec.LookPath("mkfs.erofs")
if err != nil {
t.Skipf("mkfs.erofs is not available: %v", err)
}
// Create a temporary directory to save the test files.
assetsDir, err := ioutil.TempDir(testutil.TmpDir(), "erofs-assets")
if err != nil {
t.Fatalf("ioutil.TempDir() failed: %v", err)
}
// Create a temporary directory with some random files in it, which will
// be used as the source directory to create the EROFS images.
sourceDir := filepath.Join(assetsDir, "source")
if err := os.Mkdir(sourceDir, 0755); err != nil {
t.Fatalf("os.Mkdir() failed: %v", err)
}
testApp, err := testutil.FindFile("test/cmd/test_app/test_app")
if err != nil {
t.Fatalf("error finding test_app: %v", err)
}
// Source directory is a small directory. Let's create a big directory in it.
// So we can cover both cases.
cmd := fmt.Sprintf("%s fsTreeCreate --target-dir=%s --create-symlink --depth=1 --file-per-level=500 --file-size=5000", testApp, filepath.Join(sourceDir, "big-directory"))
if out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput(); err != nil {
t.Fatalf("exec: sh -c %q, err: %v, out: %s", cmd, err, out)
}
// Create a test script which can be used to get the checksums
// from a specified directory.
scriptFile := filepath.Join(assetsDir, "test-script")
if err := os.WriteFile(scriptFile, []byte(`#!/bin/bash
set -u -e -o pipefail
dir=$1
find $dir -printf "%P\n" | sort | md5sum
find $dir -type l | sort | xargs -L 1 readlink | md5sum
find $dir -type l -o -type f | sort | xargs cat | md5sum`), 0755); err != nil {
t.Fatalf("os.WriteFile() failed: %v", err)
}
// Get the checksums from the source directory on host.
var checksums string
if out, err := exec.Command(scriptFile, sourceDir).CombinedOutput(); err != nil {
t.Fatalf("exec: %s %s, err: %v, out: %s", scriptFile, sourceDir, err, out)
} else {
checksums = string(out)
}
images := []struct {
name string
options string
}{
{
// Generate extended inodes. Inline regular files if possible.
name: "image1",
options: "-E force-inode-extended",
},
{
// Generate extended inodes. Do not inline regular files.
name: "image2",
options: "-E force-inode-extended -E noinline_data",
},
{
// Generate compact inodes. Inline regular files if possible.
name: "image3",
options: "-E force-inode-compact",
},
{
// Generate compact inodes. Do not inline regular files.
name: "image4",
options: "-E force-inode-compact -E noinline_data",
},
}
// Create the EROFS images.
for _, i := range images {
cmd := fmt.Sprintf("%s %s %s %s", mkfs, i.options, filepath.Join(assetsDir, i.name), sourceDir)
if out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput(); err != nil {
t.Fatalf("exec: sh -c %q, err: %v, out: %s", cmd, err, out)
}
}
spec, _ := sleepSpecConf(t)
conf := testutil.TestConfig(t)
_, bundleDir, cleanup, err := testutil.SetupContainer(spec, conf)
if err != nil {
t.Fatalf("error setting up container: %v", err)
}
defer cleanup()
// Create and start the container.
args := Args{
ID: testutil.RandomContainerID(),
Spec: spec,
BundleDir: bundleDir,
}
c, err := New(conf, args)
if err != nil {
t.Fatalf("error creating container: %v", err)
}
defer c.Destroy()
if err := c.Start(conf); err != nil {
t.Fatalf("error starting container: %v", err)
}
targetDir := "/mnt"
for _, i := range images {
// Mount the EROFS image in container.
imageFile := filepath.Join(assetsDir, i.name)
if err := c.Sandbox.Mount(c.ID, "erofs", imageFile, targetDir); err != nil {
t.Fatalf("error mounting EROFS image %q to %q, err: %v", imageFile, targetDir, err)
}
// Get the checksums from the target directory in container, and check if they are
// identical with the ones got from the source directory on host.
if out, err := executeCombinedOutput(conf, c, nil, scriptFile, targetDir); err != nil {
t.Fatalf("exec: %s %s, err: %v, out: %s", scriptFile, targetDir, err, out)
} else if checksums != string(out) {
t.Errorf("checksums do not match, got: %s from %s, expected: %s", out, imageFile, checksums)
}
// Unmount the EROFS image in container.
if out, err := executeCombinedOutput(conf, c, nil, "/bin/umount", targetDir); err != nil {
t.Fatalf("exec: umount %q, err: %v, out: %s", targetDir, err, out)
}
}
}
+1
View File
@@ -28,6 +28,7 @@ go_library(
"//pkg/metric:metric_go_proto",
"//pkg/prometheus",
"//pkg/sentry/control",
"//pkg/sentry/fsimpl/erofs",
"//pkg/sentry/platform",
"//pkg/sentry/seccheck",
"//pkg/state/statefile",
+26
View File
@@ -44,6 +44,7 @@ import (
metricpb "gvisor.dev/gvisor/pkg/metric/metric_go_proto"
"gvisor.dev/gvisor/pkg/prometheus"
"gvisor.dev/gvisor/pkg/sentry/control"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/erofs"
"gvisor.dev/gvisor/pkg/sentry/platform"
"gvisor.dev/gvisor/pkg/sentry/seccheck"
"gvisor.dev/gvisor/pkg/state/statefile"
@@ -1669,3 +1670,28 @@ func SetUserMappings(spec *specs.Spec, pid int) error {
}
return nil
}
// Mount mounts a filesystem in a container.
func (s *Sandbox) Mount(cid, fstype, src, dest string) error {
var files []*os.File
switch fstype {
case erofs.Name:
if imageFile, err := os.Open(src); err != nil {
return fmt.Errorf("opening %s: %v", src, err)
} else {
files = append(files, imageFile)
}
default:
return fmt.Errorf("unsupported filesystem type: %v", fstype)
}
args := boot.MountArgs{
ContainerID: cid,
Source: src,
Destination: dest,
FsType: fstype,
FilePayload: urpc.FilePayload{Files: files},
}
return s.call(boot.ContMgrMount, &args, nil)
}
+19 -4
View File
@@ -64,6 +64,8 @@ type fsTreeCreator struct {
depth uint
numFilesPerLevel uint
fileSize uint
targetDir string
createSymlink bool
}
// Name implements subcommands.Command.Name.
@@ -73,7 +75,7 @@ func (*fsTreeCreator) Name() string {
// Synopsis implements subcommands.Command.Synopsys.
func (*fsTreeCreator) Synopsis() string {
return "creates a filesystem tree of a certain depth, with a certain number of files on each level and each file with a certain size. Some randomization is added on top of this"
return "creates a filesystem tree of a certain depth, with a certain number of files on each level and each file with a certain size and type, under a certain directory. Some randomization is added on top of this"
}
// Usage implements subcommands.Command.Usage.
@@ -86,6 +88,8 @@ func (c *fsTreeCreator) SetFlags(f *flag.FlagSet) {
f.UintVar(&c.depth, "depth", 10, "number of levels to create")
f.UintVar(&c.numFilesPerLevel, "file-per-level", 10, "number of files to create per level")
f.UintVar(&c.fileSize, "file-size", 4096, "size of each file")
f.StringVar(&c.targetDir, "target-dir", "/", "directory under which to create the filesystem tree")
f.BoolVar(&c.createSymlink, "create-symlink", false, "create symlinks other than the first file per level")
}
// Execute implements subcommands.Command.Execute.
@@ -93,15 +97,26 @@ func (c *fsTreeCreator) Execute(ctx context.Context, f *flag.FlagSet, args ...an
depth := c.depth + uint(rand.Uint32())%c.depth
numFilesPerLevel := c.numFilesPerLevel + uint(rand.Uint32())%c.numFilesPerLevel
fileSize := c.fileSize + uint(rand.Uint32())%c.fileSize
curDir := c.targetDir
if _, err := os.Stat(curDir); os.IsNotExist(err) {
if err := os.MkdirAll(curDir, 0777); err != nil {
log.Fatalf("error creating directory %q: %v", curDir, err)
}
}
curDir := "/"
data := make([]byte, fileSize)
rand.Read(data)
for i := uint(0); i < depth; i++ {
for j := uint(0); j < numFilesPerLevel; j++ {
filePath := filepath.Join(curDir, fmt.Sprintf("file%d", j))
if err := os.WriteFile(filePath, data, 0666); err != nil {
log.Fatalf("error writing file %q: %v", filePath, err)
if c.createSymlink && j > 0 {
if err := os.Symlink("file0", filePath); err != nil {
log.Fatalf("error creating symlink %q: %v", filePath, err)
}
} else {
if err := os.WriteFile(filePath, data, 0666); err != nil {
log.Fatalf("error writing file %q: %v", filePath, err)
}
}
}
nextDir := filepath.Join(curDir, "dir")