mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Plumb VFS2 inside the Sentry
- Added fsbridge package with interface that can be used to open and read from VFS1 and VFS2 files. - Converted ELF loader to use fsbridge - Added VFS2 types to FSContext - Added vfs.MountNamespace to ThreadGroup Updates #1623 PiperOrigin-RevId: 295183950
This commit is contained in:
@@ -16,10 +16,13 @@ go_library(
|
||||
],
|
||||
deps = [
|
||||
"//pkg/abi/linux",
|
||||
"//pkg/context",
|
||||
"//pkg/fd",
|
||||
"//pkg/fspath",
|
||||
"//pkg/log",
|
||||
"//pkg/sentry/fs",
|
||||
"//pkg/sentry/fs/host",
|
||||
"//pkg/sentry/fsbridge",
|
||||
"//pkg/sentry/kernel",
|
||||
"//pkg/sentry/kernel/auth",
|
||||
"//pkg/sentry/kernel/time",
|
||||
@@ -27,8 +30,10 @@ go_library(
|
||||
"//pkg/sentry/state",
|
||||
"//pkg/sentry/strace",
|
||||
"//pkg/sentry/usage",
|
||||
"//pkg/sentry/vfs",
|
||||
"//pkg/sentry/watchdog",
|
||||
"//pkg/sync",
|
||||
"//pkg/syserror",
|
||||
"//pkg/tcpip/link/sniffer",
|
||||
"//pkg/urpc",
|
||||
],
|
||||
|
||||
+117
-10
@@ -18,19 +18,26 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/abi/linux"
|
||||
"gvisor.dev/gvisor/pkg/context"
|
||||
"gvisor.dev/gvisor/pkg/fspath"
|
||||
"gvisor.dev/gvisor/pkg/log"
|
||||
"gvisor.dev/gvisor/pkg/sentry/fs"
|
||||
"gvisor.dev/gvisor/pkg/sentry/fs/host"
|
||||
"gvisor.dev/gvisor/pkg/sentry/fsbridge"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
|
||||
ktime "gvisor.dev/gvisor/pkg/sentry/kernel/time"
|
||||
"gvisor.dev/gvisor/pkg/sentry/limits"
|
||||
"gvisor.dev/gvisor/pkg/sentry/usage"
|
||||
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
||||
"gvisor.dev/gvisor/pkg/syserror"
|
||||
"gvisor.dev/gvisor/pkg/urpc"
|
||||
)
|
||||
|
||||
@@ -60,6 +67,12 @@ type ExecArgs struct {
|
||||
// process's MountNamespace.
|
||||
MountNamespace *fs.MountNamespace
|
||||
|
||||
// MountNamespaceVFS2 is the mount namespace to execute the new process in.
|
||||
// A reference on MountNamespace must be held for the lifetime of the
|
||||
// ExecArgs. If MountNamespace is nil, it will default to the init
|
||||
// process's MountNamespace.
|
||||
MountNamespaceVFS2 *vfs.MountNamespace
|
||||
|
||||
// WorkingDirectory defines the working directory for the new process.
|
||||
WorkingDirectory string `json:"wd"`
|
||||
|
||||
@@ -150,6 +163,7 @@ func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadI
|
||||
Envv: args.Envv,
|
||||
WorkingDirectory: args.WorkingDirectory,
|
||||
MountNamespace: args.MountNamespace,
|
||||
MountNamespaceVFS2: args.MountNamespaceVFS2,
|
||||
Credentials: creds,
|
||||
FDTable: fdTable,
|
||||
Umask: 0022,
|
||||
@@ -166,24 +180,53 @@ func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadI
|
||||
// be donated to the new process in CreateProcess.
|
||||
initArgs.MountNamespace.IncRef()
|
||||
}
|
||||
if initArgs.MountNamespaceVFS2 != nil {
|
||||
// initArgs must hold a reference on MountNamespaceVFS2, which will
|
||||
// be donated to the new process in CreateProcess.
|
||||
initArgs.MountNamespaceVFS2.IncRef()
|
||||
}
|
||||
ctx := initArgs.NewContext(proc.Kernel)
|
||||
|
||||
if initArgs.Filename == "" {
|
||||
// Get the full path to the filename from the PATH env variable.
|
||||
paths := fs.GetPath(initArgs.Envv)
|
||||
mns := initArgs.MountNamespace
|
||||
if mns == nil {
|
||||
mns = proc.Kernel.GlobalInit().Leader().MountNamespace()
|
||||
if kernel.VFS2Enabled {
|
||||
// Get the full path to the filename from the PATH env variable.
|
||||
if initArgs.MountNamespaceVFS2 == nil {
|
||||
// Set initArgs so that 'ctx' returns the namespace.
|
||||
//
|
||||
// MountNamespaceVFS2 adds a reference to the namespace, which is
|
||||
// transferred to the new process.
|
||||
initArgs.MountNamespaceVFS2 = proc.Kernel.GlobalInit().Leader().MountNamespaceVFS2()
|
||||
}
|
||||
|
||||
paths := fs.GetPath(initArgs.Envv)
|
||||
vfsObj := proc.Kernel.VFS
|
||||
file, err := ResolveExecutablePath(ctx, vfsObj, initArgs.WorkingDirectory, initArgs.Argv[0], paths)
|
||||
if err != nil {
|
||||
return nil, 0, nil, fmt.Errorf("error finding executable %q in PATH %v: %v", initArgs.Argv[0], paths, err)
|
||||
}
|
||||
initArgs.File = fsbridge.NewVFSFile(file)
|
||||
} else {
|
||||
// Get the full path to the filename from the PATH env variable.
|
||||
paths := fs.GetPath(initArgs.Envv)
|
||||
if initArgs.MountNamespace == nil {
|
||||
// Set initArgs so that 'ctx' returns the namespace.
|
||||
initArgs.MountNamespace = proc.Kernel.GlobalInit().Leader().MountNamespace()
|
||||
|
||||
// initArgs must hold a reference on MountNamespace, which will
|
||||
// be donated to the new process in CreateProcess.
|
||||
initArgs.MountNamespaceVFS2.IncRef()
|
||||
}
|
||||
f, err := initArgs.MountNamespace.ResolveExecutablePath(ctx, initArgs.WorkingDirectory, initArgs.Argv[0], paths)
|
||||
if err != nil {
|
||||
return nil, 0, nil, fmt.Errorf("error finding executable %q in PATH %v: %v", initArgs.Argv[0], paths, err)
|
||||
}
|
||||
initArgs.Filename = f
|
||||
}
|
||||
f, err := mns.ResolveExecutablePath(ctx, initArgs.WorkingDirectory, initArgs.Argv[0], paths)
|
||||
if err != nil {
|
||||
return nil, 0, nil, fmt.Errorf("error finding executable %q in PATH %v: %v", initArgs.Argv[0], paths, err)
|
||||
}
|
||||
initArgs.Filename = f
|
||||
}
|
||||
|
||||
mounter := fs.FileOwnerFromContext(ctx)
|
||||
|
||||
// TODO(gvisor.dev/issue/1623): Use host FD when supported in VFS2.
|
||||
var ttyFile *fs.File
|
||||
for appFD, hostFile := range args.FilePayload.Files {
|
||||
var appFile *fs.File
|
||||
@@ -411,3 +454,67 @@ func ttyName(tty *kernel.TTY) string {
|
||||
}
|
||||
return fmt.Sprintf("pts/%d", tty.Index)
|
||||
}
|
||||
|
||||
// ResolveExecutablePath resolves the given executable name given a set of
|
||||
// paths that might contain it.
|
||||
func ResolveExecutablePath(ctx context.Context, vfsObj *vfs.VirtualFilesystem, wd, name string, paths []string) (*vfs.FileDescription, error) {
|
||||
root := vfs.RootFromContext(ctx)
|
||||
defer root.DecRef()
|
||||
creds := auth.CredentialsFromContext(ctx)
|
||||
|
||||
// Absolute paths can be used directly.
|
||||
if path.IsAbs(name) {
|
||||
return openExecutable(ctx, vfsObj, creds, root, name)
|
||||
}
|
||||
|
||||
// Paths with '/' in them should be joined to the working directory, or
|
||||
// to the root if working directory is not set.
|
||||
if strings.IndexByte(name, '/') > 0 {
|
||||
if len(wd) == 0 {
|
||||
wd = "/"
|
||||
}
|
||||
if !path.IsAbs(wd) {
|
||||
return nil, fmt.Errorf("working directory %q must be absolute", wd)
|
||||
}
|
||||
return openExecutable(ctx, vfsObj, creds, root, path.Join(wd, name))
|
||||
}
|
||||
|
||||
// Otherwise, we must lookup the name in the paths, starting from the
|
||||
// calling context's root directory.
|
||||
for _, p := range paths {
|
||||
if !path.IsAbs(p) {
|
||||
// Relative paths aren't safe, no one should be using them.
|
||||
log.Warningf("Skipping relative path %q in $PATH", p)
|
||||
continue
|
||||
}
|
||||
|
||||
binPath := path.Join(p, name)
|
||||
f, err := openExecutable(ctx, vfsObj, creds, root, binPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f == nil {
|
||||
continue // Not found/no access.
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
return nil, syserror.ENOENT
|
||||
}
|
||||
|
||||
func openExecutable(ctx context.Context, vfsObj *vfs.VirtualFilesystem, creds *auth.Credentials, root vfs.VirtualDentry, path string) (*vfs.FileDescription, error) {
|
||||
pop := vfs.PathOperation{
|
||||
Root: root,
|
||||
Start: root, // binPath is absolute, Start can be anything.
|
||||
Path: fspath.Parse(path),
|
||||
FollowFinalSymlink: true,
|
||||
}
|
||||
opts := &vfs.OpenOptions{
|
||||
Flags: linux.O_RDONLY,
|
||||
FileExec: true,
|
||||
}
|
||||
f, err := vfsObj.OpenAt(ctx, creds, &pop, opts)
|
||||
if err == syserror.ENOENT || err == syserror.EACCES {
|
||||
return nil, nil
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ go_library(
|
||||
"//pkg/sentry/fs/proc/device",
|
||||
"//pkg/sentry/fs/proc/seqfile",
|
||||
"//pkg/sentry/fs/ramfs",
|
||||
"//pkg/sentry/fsbridge",
|
||||
"//pkg/sentry/inet",
|
||||
"//pkg/sentry/kernel",
|
||||
"//pkg/sentry/kernel/auth",
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/sentry/fs/proc/device"
|
||||
"gvisor.dev/gvisor/pkg/sentry/fs/proc/seqfile"
|
||||
"gvisor.dev/gvisor/pkg/sentry/fs/ramfs"
|
||||
"gvisor.dev/gvisor/pkg/sentry/fsbridge"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel"
|
||||
"gvisor.dev/gvisor/pkg/sentry/limits"
|
||||
"gvisor.dev/gvisor/pkg/sentry/mm"
|
||||
@@ -249,7 +250,7 @@ func newExe(t *kernel.Task, msrc *fs.MountSource) *fs.Inode {
|
||||
return newProcInode(t, exeSymlink, msrc, fs.Symlink, t)
|
||||
}
|
||||
|
||||
func (e *exe) executable() (d *fs.Dirent, err error) {
|
||||
func (e *exe) executable() (file fsbridge.File, err error) {
|
||||
e.t.WithMuLocked(func(t *kernel.Task) {
|
||||
mm := t.MemoryManager()
|
||||
if mm == nil {
|
||||
@@ -262,8 +263,8 @@ func (e *exe) executable() (d *fs.Dirent, err error) {
|
||||
// The MemoryManager may be destroyed, in which case
|
||||
// MemoryManager.destroy will simply set the executable to nil
|
||||
// (with locks held).
|
||||
d = mm.Executable()
|
||||
if d == nil {
|
||||
file = mm.Executable()
|
||||
if file == nil {
|
||||
err = syserror.ENOENT
|
||||
}
|
||||
})
|
||||
@@ -283,15 +284,7 @@ func (e *exe) Readlink(ctx context.Context, inode *fs.Inode) (string, error) {
|
||||
}
|
||||
defer exec.DecRef()
|
||||
|
||||
root := fs.RootFromContext(ctx)
|
||||
if root == nil {
|
||||
// This doesn't correspond to anything in Linux because the vfs is
|
||||
// global there.
|
||||
return "", syserror.EINVAL
|
||||
}
|
||||
defer root.DecRef()
|
||||
n, _ := exec.FullName(root)
|
||||
return n, nil
|
||||
return exec.PathnameWithDeleted(ctx), nil
|
||||
}
|
||||
|
||||
// namespaceSymlink represents a symlink in the namespacefs, such as the files
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
load("//tools:defs.bzl", "go_library")
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
go_library(
|
||||
name = "fsbridge",
|
||||
srcs = [
|
||||
"bridge.go",
|
||||
"fs.go",
|
||||
"vfs.go",
|
||||
],
|
||||
visibility = ["//pkg/sentry:internal"],
|
||||
deps = [
|
||||
"//pkg/abi/linux",
|
||||
"//pkg/context",
|
||||
"//pkg/fspath",
|
||||
"//pkg/sentry/fs",
|
||||
"//pkg/sentry/kernel/auth",
|
||||
"//pkg/sentry/memmap",
|
||||
"//pkg/sentry/vfs",
|
||||
"//pkg/syserror",
|
||||
"//pkg/usermem",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2020 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 fsbridge provides common interfaces to bridge between VFS1 and VFS2
|
||||
// files.
|
||||
package fsbridge
|
||||
|
||||
import (
|
||||
"gvisor.dev/gvisor/pkg/abi/linux"
|
||||
"gvisor.dev/gvisor/pkg/context"
|
||||
"gvisor.dev/gvisor/pkg/sentry/memmap"
|
||||
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
||||
"gvisor.dev/gvisor/pkg/usermem"
|
||||
)
|
||||
|
||||
// File provides a common interface to bridge between VFS1 and VFS2 files.
|
||||
type File interface {
|
||||
// PathnameWithDeleted returns an absolute pathname to vd, consistent with
|
||||
// Linux's d_path(). In particular, if vd.Dentry() has been disowned,
|
||||
// PathnameWithDeleted appends " (deleted)" to the returned pathname.
|
||||
PathnameWithDeleted(ctx context.Context) string
|
||||
|
||||
// ReadFull read all contents from the file.
|
||||
ReadFull(ctx context.Context, dst usermem.IOSequence, offset int64) (int64, error)
|
||||
|
||||
// ConfigureMMap mutates opts to implement mmap(2) for the file.
|
||||
ConfigureMMap(context.Context, *memmap.MMapOpts) error
|
||||
|
||||
// Type returns the file type, e.g. linux.S_IFREG.
|
||||
Type(context.Context) (linux.FileMode, error)
|
||||
|
||||
// IncRef increments reference.
|
||||
IncRef()
|
||||
|
||||
// DecRef decrements reference.
|
||||
DecRef()
|
||||
}
|
||||
|
||||
// Lookup provides a common interface to open files.
|
||||
type Lookup interface {
|
||||
// OpenPath opens a file.
|
||||
OpenPath(ctx context.Context, path string, opts vfs.OpenOptions, remainingTraversals *uint, resolveFinal bool) (File, error)
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// Copyright 2020 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 fsbridge
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/abi/linux"
|
||||
"gvisor.dev/gvisor/pkg/context"
|
||||
"gvisor.dev/gvisor/pkg/sentry/fs"
|
||||
"gvisor.dev/gvisor/pkg/sentry/memmap"
|
||||
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
||||
"gvisor.dev/gvisor/pkg/syserror"
|
||||
"gvisor.dev/gvisor/pkg/usermem"
|
||||
)
|
||||
|
||||
// fsFile implements File interface over fs.File.
|
||||
//
|
||||
// +stateify savable
|
||||
type fsFile struct {
|
||||
file *fs.File
|
||||
}
|
||||
|
||||
var _ File = (*fsFile)(nil)
|
||||
|
||||
// NewFSFile creates a new File over fs.File.
|
||||
func NewFSFile(file *fs.File) File {
|
||||
return &fsFile{file: file}
|
||||
}
|
||||
|
||||
// PathnameWithDeleted implements File.
|
||||
func (f *fsFile) PathnameWithDeleted(ctx context.Context) string {
|
||||
root := fs.RootFromContext(ctx)
|
||||
if root == nil {
|
||||
// This doesn't correspond to anything in Linux because the vfs is
|
||||
// global there.
|
||||
return ""
|
||||
}
|
||||
defer root.DecRef()
|
||||
|
||||
name, _ := f.file.Dirent.FullName(root)
|
||||
return name
|
||||
}
|
||||
|
||||
// ReadFull implements File.
|
||||
func (f *fsFile) ReadFull(ctx context.Context, dst usermem.IOSequence, offset int64) (int64, error) {
|
||||
var total int64
|
||||
for dst.NumBytes() > 0 {
|
||||
n, err := f.file.Preadv(ctx, dst, offset+total)
|
||||
total += n
|
||||
if err == io.EOF && total != 0 {
|
||||
return total, io.ErrUnexpectedEOF
|
||||
} else if err != nil {
|
||||
return total, err
|
||||
}
|
||||
dst = dst.DropFirst64(n)
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// ConfigureMMap implements File.
|
||||
func (f *fsFile) ConfigureMMap(ctx context.Context, opts *memmap.MMapOpts) error {
|
||||
return f.file.ConfigureMMap(ctx, opts)
|
||||
}
|
||||
|
||||
// Type implements File.
|
||||
func (f *fsFile) Type(context.Context) (linux.FileMode, error) {
|
||||
return linux.FileMode(f.file.Dirent.Inode.StableAttr.Type.LinuxType()), nil
|
||||
}
|
||||
|
||||
// IncRef implements File.
|
||||
func (f *fsFile) IncRef() {
|
||||
f.file.IncRef()
|
||||
}
|
||||
|
||||
// DecRef implements File.
|
||||
func (f *fsFile) DecRef() {
|
||||
f.file.DecRef()
|
||||
}
|
||||
|
||||
// fsLookup implements Lookup interface using fs.File.
|
||||
//
|
||||
// +stateify savable
|
||||
type fsLookup struct {
|
||||
mntns *fs.MountNamespace
|
||||
|
||||
root *fs.Dirent
|
||||
workingDir *fs.Dirent
|
||||
}
|
||||
|
||||
var _ Lookup = (*fsLookup)(nil)
|
||||
|
||||
// NewFSLookup creates a new Lookup using VFS1.
|
||||
func NewFSLookup(mntns *fs.MountNamespace, root, workingDir *fs.Dirent) Lookup {
|
||||
return &fsLookup{
|
||||
mntns: mntns,
|
||||
root: root,
|
||||
workingDir: workingDir,
|
||||
}
|
||||
}
|
||||
|
||||
// OpenPath implements Lookup.
|
||||
func (l *fsLookup) OpenPath(ctx context.Context, path string, opts vfs.OpenOptions, remainingTraversals *uint, resolveFinal bool) (File, error) {
|
||||
var d *fs.Dirent
|
||||
var err error
|
||||
if resolveFinal {
|
||||
d, err = l.mntns.FindInode(ctx, l.root, l.workingDir, path, remainingTraversals)
|
||||
} else {
|
||||
d, err = l.mntns.FindLink(ctx, l.root, l.workingDir, path, remainingTraversals)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer d.DecRef()
|
||||
|
||||
if !resolveFinal && fs.IsSymlink(d.Inode.StableAttr) {
|
||||
return nil, syserror.ELOOP
|
||||
}
|
||||
|
||||
fsPerm := openOptionsToPermMask(&opts)
|
||||
if err := d.Inode.CheckPermission(ctx, fsPerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If they claim it's a directory, then make sure.
|
||||
if strings.HasSuffix(path, "/") {
|
||||
if d.Inode.StableAttr.Type != fs.Directory {
|
||||
return nil, syserror.ENOTDIR
|
||||
}
|
||||
}
|
||||
|
||||
if opts.FileExec && d.Inode.StableAttr.Type != fs.RegularFile {
|
||||
ctx.Infof("%q is not a regular file: %v", path, d.Inode.StableAttr.Type)
|
||||
return nil, syserror.EACCES
|
||||
}
|
||||
|
||||
f, err := d.Inode.GetFile(ctx, d, flagsToFileFlags(opts.Flags))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &fsFile{file: f}, nil
|
||||
}
|
||||
|
||||
func openOptionsToPermMask(opts *vfs.OpenOptions) fs.PermMask {
|
||||
mode := opts.Flags & linux.O_ACCMODE
|
||||
return fs.PermMask{
|
||||
Read: mode == linux.O_RDONLY || mode == linux.O_RDWR,
|
||||
Write: mode == linux.O_WRONLY || mode == linux.O_RDWR,
|
||||
Execute: opts.FileExec,
|
||||
}
|
||||
}
|
||||
|
||||
func flagsToFileFlags(flags uint32) fs.FileFlags {
|
||||
return fs.FileFlags{
|
||||
Direct: flags&linux.O_DIRECT != 0,
|
||||
DSync: flags&(linux.O_DSYNC|linux.O_SYNC) != 0,
|
||||
Sync: flags&linux.O_SYNC != 0,
|
||||
NonBlocking: flags&linux.O_NONBLOCK != 0,
|
||||
Read: (flags & linux.O_ACCMODE) != linux.O_WRONLY,
|
||||
Write: (flags & linux.O_ACCMODE) != linux.O_RDONLY,
|
||||
Append: flags&linux.O_APPEND != 0,
|
||||
Directory: flags&linux.O_DIRECTORY != 0,
|
||||
Async: flags&linux.O_ASYNC != 0,
|
||||
LargeFile: flags&linux.O_LARGEFILE != 0,
|
||||
Truncate: flags&linux.O_TRUNC != 0,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright 2020 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 fsbridge
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/abi/linux"
|
||||
"gvisor.dev/gvisor/pkg/context"
|
||||
"gvisor.dev/gvisor/pkg/fspath"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
|
||||
"gvisor.dev/gvisor/pkg/sentry/memmap"
|
||||
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
||||
"gvisor.dev/gvisor/pkg/usermem"
|
||||
)
|
||||
|
||||
// fsFile implements File interface over vfs.FileDescription.
|
||||
//
|
||||
// +stateify savable
|
||||
type vfsFile struct {
|
||||
file *vfs.FileDescription
|
||||
}
|
||||
|
||||
var _ File = (*vfsFile)(nil)
|
||||
|
||||
// NewVFSFile creates a new File over fs.File.
|
||||
func NewVFSFile(file *vfs.FileDescription) File {
|
||||
return &vfsFile{file: file}
|
||||
}
|
||||
|
||||
// PathnameWithDeleted implements File.
|
||||
func (f *vfsFile) PathnameWithDeleted(ctx context.Context) string {
|
||||
root := vfs.RootFromContext(ctx)
|
||||
defer root.DecRef()
|
||||
|
||||
vfsObj := f.file.VirtualDentry().Mount().Filesystem().VirtualFilesystem()
|
||||
name, _ := vfsObj.PathnameWithDeleted(ctx, root, f.file.VirtualDentry())
|
||||
return name
|
||||
}
|
||||
|
||||
// ReadFull implements File.
|
||||
func (f *vfsFile) ReadFull(ctx context.Context, dst usermem.IOSequence, offset int64) (int64, error) {
|
||||
var total int64
|
||||
for dst.NumBytes() > 0 {
|
||||
n, err := f.file.PRead(ctx, dst, offset+total, vfs.ReadOptions{})
|
||||
total += n
|
||||
if err == io.EOF && total != 0 {
|
||||
return total, io.ErrUnexpectedEOF
|
||||
} else if err != nil {
|
||||
return total, err
|
||||
}
|
||||
dst = dst.DropFirst64(n)
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// ConfigureMMap implements File.
|
||||
func (f *vfsFile) ConfigureMMap(ctx context.Context, opts *memmap.MMapOpts) error {
|
||||
return f.file.ConfigureMMap(ctx, opts)
|
||||
}
|
||||
|
||||
// Type implements File.
|
||||
func (f *vfsFile) Type(ctx context.Context) (linux.FileMode, error) {
|
||||
stat, err := f.file.Stat(ctx, vfs.StatOptions{})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return linux.FileMode(stat.Mode).FileType(), nil
|
||||
}
|
||||
|
||||
// IncRef implements File.
|
||||
func (f *vfsFile) IncRef() {
|
||||
f.file.IncRef()
|
||||
}
|
||||
|
||||
// DecRef implements File.
|
||||
func (f *vfsFile) DecRef() {
|
||||
f.file.DecRef()
|
||||
}
|
||||
|
||||
// fsLookup implements Lookup interface using fs.File.
|
||||
//
|
||||
// +stateify savable
|
||||
type vfsLookup struct {
|
||||
mntns *vfs.MountNamespace
|
||||
|
||||
root vfs.VirtualDentry
|
||||
workingDir vfs.VirtualDentry
|
||||
}
|
||||
|
||||
var _ Lookup = (*vfsLookup)(nil)
|
||||
|
||||
// NewVFSLookup creates a new Lookup using VFS2.
|
||||
func NewVFSLookup(mntns *vfs.MountNamespace, root, workingDir vfs.VirtualDentry) Lookup {
|
||||
return &vfsLookup{
|
||||
mntns: mntns,
|
||||
root: root,
|
||||
workingDir: workingDir,
|
||||
}
|
||||
}
|
||||
|
||||
// OpenPath implements Lookup.
|
||||
//
|
||||
// remainingTraversals is not configurable in VFS2, all callers are using the
|
||||
// default anyways.
|
||||
//
|
||||
// TODO(gvisor.dev/issue/1623): Check mount has read and exec permission.
|
||||
func (l *vfsLookup) OpenPath(ctx context.Context, path string, opts vfs.OpenOptions, _ *uint, resolveFinal bool) (File, error) {
|
||||
vfsObj := l.mntns.Root().Mount().Filesystem().VirtualFilesystem()
|
||||
creds := auth.CredentialsFromContext(ctx)
|
||||
pop := &vfs.PathOperation{
|
||||
Root: l.root,
|
||||
Start: l.root,
|
||||
Path: fspath.Parse(path),
|
||||
FollowFinalSymlink: resolveFinal,
|
||||
}
|
||||
fd, err := vfsObj.OpenAt(ctx, creds, pop, &opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &vfsFile{file: fd}, nil
|
||||
}
|
||||
@@ -28,6 +28,9 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/sync"
|
||||
)
|
||||
|
||||
// Name is the default filesystem name.
|
||||
const Name = "devtmpfs"
|
||||
|
||||
// FilesystemType implements vfs.FilesystemType.
|
||||
type FilesystemType struct {
|
||||
initOnce sync.Once
|
||||
@@ -107,6 +110,7 @@ func (a *Accessor) wrapContext(ctx context.Context) *accessorContext {
|
||||
func (ac *accessorContext) Value(key interface{}) interface{} {
|
||||
switch key {
|
||||
case vfs.CtxMountNamespace:
|
||||
ac.a.mntns.IncRef()
|
||||
return ac.a.mntns
|
||||
case vfs.CtxRoot:
|
||||
ac.a.root.IncRef()
|
||||
|
||||
@@ -400,6 +400,7 @@ func (fs *filesystem) unlinkAt(ctx context.Context, rp *vfs.ResolvingPath, dir b
|
||||
}
|
||||
vfsObj := rp.VirtualFilesystem()
|
||||
mntns := vfs.MountNamespaceFromContext(ctx)
|
||||
defer mntns.DecRef()
|
||||
parent.dirMu.Lock()
|
||||
defer parent.dirMu.Unlock()
|
||||
childVFSD := parent.vfsd.Child(name)
|
||||
@@ -934,7 +935,9 @@ func (fs *filesystem) RenameAt(ctx context.Context, rp *vfs.ResolvingPath, oldPa
|
||||
if oldParent == newParent && oldName == newName {
|
||||
return nil
|
||||
}
|
||||
if err := vfsObj.PrepareRenameDentry(vfs.MountNamespaceFromContext(ctx), &renamed.vfsd, replacedVFSD); err != nil {
|
||||
mntns := vfs.MountNamespaceFromContext(ctx)
|
||||
defer mntns.DecRef()
|
||||
if err := vfsObj.PrepareRenameDentry(mntns, &renamed.vfsd, replacedVFSD); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := renamed.file.rename(ctx, newParent.file, newName); err != nil {
|
||||
|
||||
@@ -52,6 +52,9 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/usermem"
|
||||
)
|
||||
|
||||
// Name is the default filesystem name.
|
||||
const Name = "9p"
|
||||
|
||||
// FilesystemType implements vfs.FilesystemType.
|
||||
type FilesystemType struct{}
|
||||
|
||||
|
||||
@@ -544,6 +544,7 @@ func (fs *Filesystem) RenameAt(ctx context.Context, rp *vfs.ResolvingPath, oldPa
|
||||
}
|
||||
|
||||
mntns := vfs.MountNamespaceFromContext(ctx)
|
||||
defer mntns.DecRef()
|
||||
virtfs := rp.VirtualFilesystem()
|
||||
|
||||
srcDirDentry := srcDirVFSD.Impl().(*Dentry)
|
||||
@@ -595,7 +596,10 @@ func (fs *Filesystem) RmdirAt(ctx context.Context, rp *vfs.ResolvingPath) error
|
||||
parentDentry := vfsd.Parent().Impl().(*Dentry)
|
||||
parentDentry.dirMu.Lock()
|
||||
defer parentDentry.dirMu.Unlock()
|
||||
if err := virtfs.PrepareDeleteDentry(vfs.MountNamespaceFromContext(ctx), vfsd); err != nil {
|
||||
|
||||
mntns := vfs.MountNamespaceFromContext(ctx)
|
||||
defer mntns.DecRef()
|
||||
if err := virtfs.PrepareDeleteDentry(mntns, vfsd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := parentDentry.inode.RmDir(ctx, rp.Component(), vfsd); err != nil {
|
||||
@@ -697,7 +701,9 @@ func (fs *Filesystem) UnlinkAt(ctx context.Context, rp *vfs.ResolvingPath) error
|
||||
parentDentry := vfsd.Parent().Impl().(*Dentry)
|
||||
parentDentry.dirMu.Lock()
|
||||
defer parentDentry.dirMu.Unlock()
|
||||
if err := virtfs.PrepareDeleteDentry(vfs.MountNamespaceFromContext(ctx), vfsd); err != nil {
|
||||
mntns := vfs.MountNamespaceFromContext(ctx)
|
||||
defer mntns.DecRef()
|
||||
if err := virtfs.PrepareDeleteDentry(mntns, vfsd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := parentDentry.inode.Unlink(ctx, rp.Component(), vfsd); err != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ go_library(
|
||||
"tasks_net.go",
|
||||
"tasks_sys.go",
|
||||
],
|
||||
visibility = ["//pkg/sentry:internal"],
|
||||
deps = [
|
||||
"//pkg/abi/linux",
|
||||
"//pkg/context",
|
||||
|
||||
@@ -26,15 +26,18 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
||||
)
|
||||
|
||||
// procFSType is the factory class for procfs.
|
||||
// Name is the default filesystem name.
|
||||
const Name = "proc"
|
||||
|
||||
// FilesystemType is the factory class for procfs.
|
||||
//
|
||||
// +stateify savable
|
||||
type procFSType struct{}
|
||||
type FilesystemType struct{}
|
||||
|
||||
var _ vfs.FilesystemType = (*procFSType)(nil)
|
||||
var _ vfs.FilesystemType = (*FilesystemType)(nil)
|
||||
|
||||
// GetFilesystem implements vfs.FilesystemType.
|
||||
func (ft *procFSType) GetFilesystem(ctx context.Context, vfsObj *vfs.VirtualFilesystem, creds *auth.Credentials, source string, opts vfs.GetFilesystemOptions) (*vfs.Filesystem, *vfs.Dentry, error) {
|
||||
func (ft *FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.VirtualFilesystem, creds *auth.Credentials, source string, opts vfs.GetFilesystemOptions) (*vfs.Filesystem, *vfs.Dentry, error) {
|
||||
k := kernel.KernelFromContext(ctx)
|
||||
if k == nil {
|
||||
return nil, nil, fmt.Errorf("procfs requires a kernel")
|
||||
@@ -47,12 +50,13 @@ func (ft *procFSType) GetFilesystem(ctx context.Context, vfsObj *vfs.VirtualFile
|
||||
procfs := &kernfs.Filesystem{}
|
||||
procfs.VFSFilesystem().Init(vfsObj, procfs)
|
||||
|
||||
var data *InternalData
|
||||
var cgroups map[string]string
|
||||
if opts.InternalData != nil {
|
||||
data = opts.InternalData.(*InternalData)
|
||||
data := opts.InternalData.(*InternalData)
|
||||
cgroups = data.Cgroups
|
||||
}
|
||||
|
||||
_, dentry := newTasksInode(procfs, k, pidns, data.Cgroups)
|
||||
_, dentry := newTasksInode(procfs, k, pidns, cgroups)
|
||||
return procfs.VFSFilesystem(), dentry.VFSDentry(), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -90,8 +90,7 @@ func setup(t *testing.T) *testutil.System {
|
||||
ctx := k.SupervisorContext()
|
||||
creds := auth.CredentialsFromContext(ctx)
|
||||
|
||||
vfsObj := vfs.New()
|
||||
vfsObj.MustRegisterFilesystemType("procfs", &procFSType{}, &vfs.RegisterFilesystemTypeOptions{
|
||||
k.VFS.MustRegisterFilesystemType(Name, &FilesystemType{}, &vfs.RegisterFilesystemTypeOptions{
|
||||
AllowUserMount: true,
|
||||
})
|
||||
fsOpts := vfs.GetFilesystemOptions{
|
||||
@@ -102,11 +101,11 @@ func setup(t *testing.T) *testutil.System {
|
||||
},
|
||||
},
|
||||
}
|
||||
mntns, err := vfsObj.NewMountNamespace(ctx, creds, "", "procfs", &fsOpts)
|
||||
mntns, err := k.VFS.NewMountNamespace(ctx, creds, "", Name, &fsOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("NewMountNamespace(): %v", err)
|
||||
}
|
||||
return testutil.NewSystem(ctx, t, vfsObj, mntns)
|
||||
return testutil.NewSystem(ctx, t, k.VFS, mntns)
|
||||
}
|
||||
|
||||
func TestTasksEmpty(t *testing.T) {
|
||||
@@ -131,7 +130,7 @@ func TestTasks(t *testing.T) {
|
||||
var tasks []*kernel.Task
|
||||
for i := 0; i < 5; i++ {
|
||||
tc := k.NewThreadGroup(nil, k.RootPIDNamespace(), kernel.NewSignalHandlers(), linux.SIGCHLD, k.GlobalInit().Limits())
|
||||
task, err := testutil.CreateTask(s.Ctx, fmt.Sprintf("name-%d", i), tc)
|
||||
task, err := testutil.CreateTask(s.Ctx, fmt.Sprintf("name-%d", i), tc, s.MntNs, s.Root, s.Root)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTask(): %v", err)
|
||||
}
|
||||
@@ -213,7 +212,7 @@ func TestTasksOffset(t *testing.T) {
|
||||
k := kernel.KernelFromContext(s.Ctx)
|
||||
for i := 0; i < 3; i++ {
|
||||
tc := k.NewThreadGroup(nil, k.RootPIDNamespace(), kernel.NewSignalHandlers(), linux.SIGCHLD, k.GlobalInit().Limits())
|
||||
if _, err := testutil.CreateTask(s.Ctx, fmt.Sprintf("name-%d", i), tc); err != nil {
|
||||
if _, err := testutil.CreateTask(s.Ctx, fmt.Sprintf("name-%d", i), tc, s.MntNs, s.Root, s.Root); err != nil {
|
||||
t.Fatalf("CreateTask(): %v", err)
|
||||
}
|
||||
}
|
||||
@@ -337,7 +336,7 @@ func TestTask(t *testing.T) {
|
||||
|
||||
k := kernel.KernelFromContext(s.Ctx)
|
||||
tc := k.NewThreadGroup(nil, k.RootPIDNamespace(), kernel.NewSignalHandlers(), linux.SIGCHLD, k.GlobalInit().Limits())
|
||||
_, err := testutil.CreateTask(s.Ctx, "name", tc)
|
||||
_, err := testutil.CreateTask(s.Ctx, "name", tc, s.MntNs, s.Root, s.Root)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTask(): %v", err)
|
||||
}
|
||||
@@ -352,7 +351,7 @@ func TestProcSelf(t *testing.T) {
|
||||
|
||||
k := kernel.KernelFromContext(s.Ctx)
|
||||
tc := k.NewThreadGroup(nil, k.RootPIDNamespace(), kernel.NewSignalHandlers(), linux.SIGCHLD, k.GlobalInit().Limits())
|
||||
task, err := testutil.CreateTask(s.Ctx, "name", tc)
|
||||
task, err := testutil.CreateTask(s.Ctx, "name", tc, s.MntNs, s.Root, s.Root)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTask(): %v", err)
|
||||
}
|
||||
@@ -433,7 +432,7 @@ func TestTree(t *testing.T) {
|
||||
var tasks []*kernel.Task
|
||||
for i := 0; i < 5; i++ {
|
||||
tc := k.NewThreadGroup(nil, k.RootPIDNamespace(), kernel.NewSignalHandlers(), linux.SIGCHLD, k.GlobalInit().Limits())
|
||||
task, err := testutil.CreateTask(s.Ctx, fmt.Sprintf("name-%d", i), tc)
|
||||
task, err := testutil.CreateTask(s.Ctx, fmt.Sprintf("name-%d", i), tc, s.MntNs, s.Root, s.Root)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTask(): %v", err)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ go_library(
|
||||
srcs = [
|
||||
"sys.go",
|
||||
],
|
||||
visibility = ["//pkg/sentry:internal"],
|
||||
deps = [
|
||||
"//pkg/abi/linux",
|
||||
"//pkg/context",
|
||||
|
||||
@@ -28,6 +28,9 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/syserror"
|
||||
)
|
||||
|
||||
// Name is the default filesystem name.
|
||||
const Name = "sysfs"
|
||||
|
||||
// FilesystemType implements vfs.FilesystemType.
|
||||
type FilesystemType struct{}
|
||||
|
||||
|
||||
@@ -34,16 +34,15 @@ func newTestSystem(t *testing.T) *testutil.System {
|
||||
}
|
||||
ctx := k.SupervisorContext()
|
||||
creds := auth.CredentialsFromContext(ctx)
|
||||
v := vfs.New()
|
||||
v.MustRegisterFilesystemType("sysfs", sys.FilesystemType{}, &vfs.RegisterFilesystemTypeOptions{
|
||||
k.VFS.MustRegisterFilesystemType(sys.Name, sys.FilesystemType{}, &vfs.RegisterFilesystemTypeOptions{
|
||||
AllowUserMount: true,
|
||||
})
|
||||
|
||||
mns, err := v.NewMountNamespace(ctx, creds, "", "sysfs", &vfs.GetFilesystemOptions{})
|
||||
mns, err := k.VFS.NewMountNamespace(ctx, creds, "", sys.Name, &vfs.GetFilesystemOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new mount namespace: %v", err)
|
||||
}
|
||||
return testutil.NewSystem(ctx, t, v, mns)
|
||||
return testutil.NewSystem(ctx, t, k.VFS, mns)
|
||||
}
|
||||
|
||||
func TestReadCPUFile(t *testing.T) {
|
||||
|
||||
@@ -16,7 +16,7 @@ go_library(
|
||||
"//pkg/cpuid",
|
||||
"//pkg/fspath",
|
||||
"//pkg/memutil",
|
||||
"//pkg/sentry/fs",
|
||||
"//pkg/sentry/fsimpl/tmpfs",
|
||||
"//pkg/sentry/kernel",
|
||||
"//pkg/sentry/kernel/auth",
|
||||
"//pkg/sentry/kernel/sched",
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/context"
|
||||
"gvisor.dev/gvisor/pkg/cpuid"
|
||||
"gvisor.dev/gvisor/pkg/memutil"
|
||||
"gvisor.dev/gvisor/pkg/sentry/fs"
|
||||
"gvisor.dev/gvisor/pkg/sentry/fsimpl/tmpfs"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel/sched"
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/sentry/pgalloc"
|
||||
"gvisor.dev/gvisor/pkg/sentry/platform"
|
||||
"gvisor.dev/gvisor/pkg/sentry/time"
|
||||
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
||||
|
||||
// Platforms are plugable.
|
||||
_ "gvisor.dev/gvisor/pkg/sentry/platform/kvm"
|
||||
@@ -99,26 +100,27 @@ func Boot() (*kernel.Kernel, error) {
|
||||
return nil, fmt.Errorf("initializing kernel: %v", err)
|
||||
}
|
||||
|
||||
ctx := k.SupervisorContext()
|
||||
kernel.VFS2Enabled = true
|
||||
|
||||
vfsObj := vfs.New()
|
||||
vfsObj.MustRegisterFilesystemType(tmpfs.Name, &tmpfs.FilesystemType{}, &vfs.RegisterFilesystemTypeOptions{
|
||||
AllowUserMount: true,
|
||||
AllowUserList: true,
|
||||
})
|
||||
k.VFS = vfsObj
|
||||
|
||||
// Create mount namespace without root as it's the minimum required to create
|
||||
// the global thread group.
|
||||
mntns, err := fs.NewMountNamespace(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ls, err := limits.NewLinuxLimitSet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tg := k.NewThreadGroup(mntns, k.RootPIDNamespace(), kernel.NewSignalHandlers(), linux.SIGCHLD, ls)
|
||||
tg := k.NewThreadGroup(nil, k.RootPIDNamespace(), kernel.NewSignalHandlers(), linux.SIGCHLD, ls)
|
||||
k.TestOnly_SetGlobalInit(tg)
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// CreateTask creates a new bare bones task for tests.
|
||||
func CreateTask(ctx context.Context, name string, tc *kernel.ThreadGroup) (*kernel.Task, error) {
|
||||
func CreateTask(ctx context.Context, name string, tc *kernel.ThreadGroup, mntns *vfs.MountNamespace, root, cwd vfs.VirtualDentry) (*kernel.Task, error) {
|
||||
k := kernel.KernelFromContext(ctx)
|
||||
config := &kernel.TaskConfig{
|
||||
Kernel: k,
|
||||
@@ -129,6 +131,8 @@ func CreateTask(ctx context.Context, name string, tc *kernel.ThreadGroup) (*kern
|
||||
UTSNamespace: kernel.UTSNamespaceFromContext(ctx),
|
||||
IPCNamespace: kernel.IPCNamespaceFromContext(ctx),
|
||||
AbstractSocketNamespace: kernel.NewAbstractSocketNamespace(),
|
||||
MountNamespaceVFS2: mntns,
|
||||
FSContext: kernel.NewFSContextVFS2(root, cwd, 0022),
|
||||
}
|
||||
return k.TaskSet().NewTask(config)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user