mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Add VFS2 support for device special files.
- Add FileDescriptionOptions.UseDentryMetadata, which reduces the amount of boilerplate needed for device FDs and the like between filesystems. - Switch back to having FileDescription.Init() take references on the Mount and Dentry; otherwise managing refcounts around failed calls to OpenDeviceSpecialFile() / Device.Open() is tricky. PiperOrigin-RevId: 287575574
This commit is contained in:
@@ -157,8 +157,6 @@ func (in *inode) open(rp *vfs.ResolvingPath, vfsd *vfs.Dentry, flags uint32) (*v
|
||||
switch in.impl.(type) {
|
||||
case *regularFile:
|
||||
var fd regularFileFD
|
||||
mnt.IncRef()
|
||||
vfsd.IncRef()
|
||||
fd.vfsfd.Init(&fd, flags, mnt, vfsd, &vfs.FileDescriptionOptions{})
|
||||
return &fd.vfsfd, nil
|
||||
case *directory:
|
||||
@@ -168,8 +166,6 @@ func (in *inode) open(rp *vfs.ResolvingPath, vfsd *vfs.Dentry, flags uint32) (*v
|
||||
return nil, syserror.EISDIR
|
||||
}
|
||||
var fd directoryFD
|
||||
mnt.IncRef()
|
||||
vfsd.IncRef()
|
||||
fd.vfsfd.Init(&fd, flags, mnt, vfsd, &vfs.FileDescriptionOptions{})
|
||||
return &fd.vfsfd, nil
|
||||
case *symlink:
|
||||
@@ -178,8 +174,6 @@ func (in *inode) open(rp *vfs.ResolvingPath, vfsd *vfs.Dentry, flags uint32) (*v
|
||||
return nil, syserror.ELOOP
|
||||
}
|
||||
var fd symlinkFD
|
||||
mnt.IncRef()
|
||||
vfsd.IncRef()
|
||||
fd.vfsfd.Init(&fd, flags, mnt, vfsd, &vfs.FileDescriptionOptions{})
|
||||
return &fd.vfsfd, nil
|
||||
default:
|
||||
|
||||
@@ -81,8 +81,6 @@ type DynamicBytesFD struct {
|
||||
|
||||
// Init initializes a DynamicBytesFD.
|
||||
func (fd *DynamicBytesFD) Init(m *vfs.Mount, d *vfs.Dentry, data vfs.DynamicBytesSource, flags uint32) {
|
||||
m.IncRef() // DecRef in vfs.FileDescription.vd.DecRef on final ref.
|
||||
d.IncRef() // DecRef in vfs.FileDescription.vd.DecRef on final ref.
|
||||
fd.inode = d.Impl().(*Dentry).inode
|
||||
fd.SetDataSource(data)
|
||||
fd.vfsfd.Init(fd, flags, m, d, &vfs.FileDescriptionOptions{})
|
||||
|
||||
@@ -44,8 +44,6 @@ type GenericDirectoryFD struct {
|
||||
|
||||
// Init initializes a GenericDirectoryFD.
|
||||
func (fd *GenericDirectoryFD) Init(m *vfs.Mount, d *vfs.Dentry, children *OrderedChildren, flags uint32) {
|
||||
m.IncRef() // DecRef in vfs.FileDescription.vd.DecRef on final ref.
|
||||
d.IncRef() // DecRef in vfs.FileDescription.vd.DecRef on final ref.
|
||||
fd.children = children
|
||||
fd.vfsfd.Init(fd, flags, m, d, &vfs.FileDescriptionOptions{})
|
||||
}
|
||||
|
||||
@@ -348,8 +348,6 @@ func (d *dentry) open(ctx context.Context, rp *vfs.ResolvingPath, flags uint32,
|
||||
}
|
||||
// mnt.EndWrite() is called by regularFileFD.Release().
|
||||
}
|
||||
mnt.IncRef()
|
||||
d.IncRef()
|
||||
fd.vfsfd.Init(&fd, flags, mnt, &d.vfsd, &vfs.FileDescriptionOptions{})
|
||||
if flags&linux.O_TRUNC != 0 {
|
||||
impl.mu.Lock()
|
||||
@@ -364,8 +362,6 @@ func (d *dentry) open(ctx context.Context, rp *vfs.ResolvingPath, flags uint32,
|
||||
return nil, syserror.EISDIR
|
||||
}
|
||||
var fd directoryFD
|
||||
mnt.IncRef()
|
||||
d.IncRef()
|
||||
fd.vfsfd.Init(&fd, flags, mnt, &d.vfsd, &vfs.FileDescriptionOptions{})
|
||||
return &fd.vfsfd, nil
|
||||
case *symlink:
|
||||
|
||||
@@ -55,8 +55,6 @@ func newNamedPipeFD(ctx context.Context, np *namedPipe, rp *vfs.ResolvingPath, v
|
||||
return nil, err
|
||||
}
|
||||
mnt := rp.Mount()
|
||||
mnt.IncRef()
|
||||
vfsd.IncRef()
|
||||
fd.vfsfd.Init(&fd, flags, mnt, vfsd, &vfs.FileDescriptionOptions{})
|
||||
return &fd.vfsfd, nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ go_library(
|
||||
"context.go",
|
||||
"debug.go",
|
||||
"dentry.go",
|
||||
"device.go",
|
||||
"file_description.go",
|
||||
"file_description_impl_util.go",
|
||||
"filesystem.go",
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright 2019 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 vfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/sentry/context"
|
||||
"gvisor.dev/gvisor/pkg/syserror"
|
||||
)
|
||||
|
||||
// DeviceKind indicates whether a device is a block or character device.
|
||||
type DeviceKind uint32
|
||||
|
||||
const (
|
||||
// BlockDevice indicates a block device.
|
||||
BlockDevice DeviceKind = iota
|
||||
|
||||
// CharDevice indicates a character device.
|
||||
CharDevice
|
||||
)
|
||||
|
||||
// String implements fmt.Stringer.String.
|
||||
func (kind DeviceKind) String() string {
|
||||
switch kind {
|
||||
case BlockDevice:
|
||||
return "block"
|
||||
case CharDevice:
|
||||
return "character"
|
||||
default:
|
||||
return fmt.Sprintf("invalid device kind %d", kind)
|
||||
}
|
||||
}
|
||||
|
||||
type devTuple struct {
|
||||
kind DeviceKind
|
||||
major uint32
|
||||
minor uint32
|
||||
}
|
||||
|
||||
// A Device backs device special files.
|
||||
type Device interface {
|
||||
// Open returns a FileDescription representing this device.
|
||||
Open(ctx context.Context, mnt *Mount, d *Dentry, opts OpenOptions) (*FileDescription, error)
|
||||
}
|
||||
|
||||
type registeredDevice struct {
|
||||
dev Device
|
||||
opts RegisterDeviceOptions
|
||||
}
|
||||
|
||||
// RegisterDeviceOptions contains options to
|
||||
// VirtualFilesystem.RegisterDevice().
|
||||
type RegisterDeviceOptions struct {
|
||||
// GroupName is the name shown for this device registration in
|
||||
// /proc/devices. If GroupName is empty, this registration will not be
|
||||
// shown in /proc/devices.
|
||||
GroupName string
|
||||
}
|
||||
|
||||
// RegisterDevice registers the given Device in vfs with the given major and
|
||||
// minor device numbers.
|
||||
func (vfs *VirtualFilesystem) RegisterDevice(kind DeviceKind, major, minor uint32, dev Device, opts *RegisterDeviceOptions) error {
|
||||
tup := devTuple{kind, major, minor}
|
||||
vfs.devicesMu.Lock()
|
||||
defer vfs.devicesMu.Unlock()
|
||||
if existing, ok := vfs.devices[tup]; ok {
|
||||
return fmt.Errorf("%s device number (%d, %d) is already registered to device type %T", kind, major, minor, existing.dev)
|
||||
}
|
||||
vfs.devices[tup] = ®isteredDevice{
|
||||
dev: dev,
|
||||
opts: *opts,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OpenDeviceSpecialFile returns a FileDescription representing the given
|
||||
// device.
|
||||
func (vfs *VirtualFilesystem) OpenDeviceSpecialFile(ctx context.Context, mnt *Mount, d *Dentry, kind DeviceKind, major, minor uint32, opts *OpenOptions) (*FileDescription, error) {
|
||||
tup := devTuple{kind, major, minor}
|
||||
vfs.devicesMu.RLock()
|
||||
defer vfs.devicesMu.RUnlock()
|
||||
rd, ok := vfs.devices[tup]
|
||||
if !ok {
|
||||
return nil, syserror.ENXIO
|
||||
}
|
||||
return rd.dev.Open(ctx, mnt, d, *opts)
|
||||
}
|
||||
@@ -61,11 +61,25 @@ type FileDescriptionOptions struct {
|
||||
// If AllowDirectIO is true, allow O_DIRECT to be set on the file. This is
|
||||
// usually only the case if O_DIRECT would actually have an effect.
|
||||
AllowDirectIO bool
|
||||
|
||||
// If UseDentryMetadata is true, calls to FileDescription methods that
|
||||
// interact with file and filesystem metadata (Stat, SetStat, StatFS,
|
||||
// Listxattr, Getxattr, Setxattr, Removexattr) are implemented by calling
|
||||
// the corresponding FilesystemImpl methods instead of the corresponding
|
||||
// FileDescriptionImpl methods.
|
||||
//
|
||||
// UseDentryMetadata is intended for file descriptions that are implemented
|
||||
// outside of individual filesystems, such as pipes, sockets, and device
|
||||
// special files. FileDescriptions for which UseDentryMetadata is true may
|
||||
// embed DentryMetadataFileDescriptionImpl to obtain appropriate
|
||||
// implementations of FileDescriptionImpl methods that should not be
|
||||
// called.
|
||||
UseDentryMetadata bool
|
||||
}
|
||||
|
||||
// Init must be called before first use of fd. It takes ownership of references
|
||||
// on mnt and d held by the caller. statusFlags is the initial file description
|
||||
// status flags, which is usually the full set of flags passed to open(2).
|
||||
// Init must be called before first use of fd. It takes references on mnt and
|
||||
// d. statusFlags is the initial file description status flags, which is
|
||||
// usually the full set of flags passed to open(2).
|
||||
func (fd *FileDescription) Init(impl FileDescriptionImpl, statusFlags uint32, mnt *Mount, d *Dentry, opts *FileDescriptionOptions) {
|
||||
fd.refs = 1
|
||||
fd.statusFlags = statusFlags | linux.O_LARGEFILE
|
||||
@@ -73,6 +87,7 @@ func (fd *FileDescription) Init(impl FileDescriptionImpl, statusFlags uint32, mn
|
||||
mount: mnt,
|
||||
dentry: d,
|
||||
}
|
||||
fd.vd.IncRef()
|
||||
fd.opts = *opts
|
||||
fd.impl = impl
|
||||
}
|
||||
@@ -140,7 +155,7 @@ func (fd *FileDescription) SetStatusFlags(ctx context.Context, creds *auth.Crede
|
||||
// sense. However, the check as actually implemented seems to be "O_APPEND
|
||||
// cannot be changed if the file is marked as append-only".
|
||||
if (flags^oldFlags)&linux.O_APPEND != 0 {
|
||||
stat, err := fd.impl.Stat(ctx, StatOptions{
|
||||
stat, err := fd.Stat(ctx, StatOptions{
|
||||
// There is no mask bit for stx_attributes.
|
||||
Mask: 0,
|
||||
// Linux just reads inode::i_flags directly.
|
||||
@@ -154,7 +169,7 @@ func (fd *FileDescription) SetStatusFlags(ctx context.Context, creds *auth.Crede
|
||||
}
|
||||
}
|
||||
if (flags&linux.O_NOATIME != 0) && (oldFlags&linux.O_NOATIME == 0) {
|
||||
stat, err := fd.impl.Stat(ctx, StatOptions{
|
||||
stat, err := fd.Stat(ctx, StatOptions{
|
||||
Mask: linux.STATX_UID,
|
||||
// Linux's inode_owner_or_capable() just reads inode::i_uid
|
||||
// directly.
|
||||
@@ -348,17 +363,47 @@ func (fd *FileDescription) OnClose(ctx context.Context) error {
|
||||
|
||||
// Stat returns metadata for the file represented by fd.
|
||||
func (fd *FileDescription) Stat(ctx context.Context, opts StatOptions) (linux.Statx, error) {
|
||||
if fd.opts.UseDentryMetadata {
|
||||
vfsObj := fd.vd.mount.vfs
|
||||
rp := vfsObj.getResolvingPath(auth.CredentialsFromContext(ctx), &PathOperation{
|
||||
Root: fd.vd,
|
||||
Start: fd.vd,
|
||||
})
|
||||
stat, err := fd.vd.mount.fs.impl.StatAt(ctx, rp, opts)
|
||||
vfsObj.putResolvingPath(rp)
|
||||
return stat, err
|
||||
}
|
||||
return fd.impl.Stat(ctx, opts)
|
||||
}
|
||||
|
||||
// SetStat updates metadata for the file represented by fd.
|
||||
func (fd *FileDescription) SetStat(ctx context.Context, opts SetStatOptions) error {
|
||||
if fd.opts.UseDentryMetadata {
|
||||
vfsObj := fd.vd.mount.vfs
|
||||
rp := vfsObj.getResolvingPath(auth.CredentialsFromContext(ctx), &PathOperation{
|
||||
Root: fd.vd,
|
||||
Start: fd.vd,
|
||||
})
|
||||
err := fd.vd.mount.fs.impl.SetStatAt(ctx, rp, opts)
|
||||
vfsObj.putResolvingPath(rp)
|
||||
return err
|
||||
}
|
||||
return fd.impl.SetStat(ctx, opts)
|
||||
}
|
||||
|
||||
// StatFS returns metadata for the filesystem containing the file represented
|
||||
// by fd.
|
||||
func (fd *FileDescription) StatFS(ctx context.Context) (linux.Statfs, error) {
|
||||
if fd.opts.UseDentryMetadata {
|
||||
vfsObj := fd.vd.mount.vfs
|
||||
rp := vfsObj.getResolvingPath(auth.CredentialsFromContext(ctx), &PathOperation{
|
||||
Root: fd.vd,
|
||||
Start: fd.vd,
|
||||
})
|
||||
statfs, err := fd.vd.mount.fs.impl.StatFSAt(ctx, rp)
|
||||
vfsObj.putResolvingPath(rp)
|
||||
return statfs, err
|
||||
}
|
||||
return fd.impl.StatFS(ctx)
|
||||
}
|
||||
|
||||
@@ -417,6 +462,16 @@ func (fd *FileDescription) Ioctl(ctx context.Context, uio usermem.IO, args arch.
|
||||
// Listxattr returns all extended attribute names for the file represented by
|
||||
// fd.
|
||||
func (fd *FileDescription) Listxattr(ctx context.Context) ([]string, error) {
|
||||
if fd.opts.UseDentryMetadata {
|
||||
vfsObj := fd.vd.mount.vfs
|
||||
rp := vfsObj.getResolvingPath(auth.CredentialsFromContext(ctx), &PathOperation{
|
||||
Root: fd.vd,
|
||||
Start: fd.vd,
|
||||
})
|
||||
names, err := fd.vd.mount.fs.impl.ListxattrAt(ctx, rp)
|
||||
vfsObj.putResolvingPath(rp)
|
||||
return names, err
|
||||
}
|
||||
names, err := fd.impl.Listxattr(ctx)
|
||||
if err == syserror.ENOTSUP {
|
||||
// Linux doesn't actually return ENOTSUP in this case; instead,
|
||||
@@ -431,18 +486,48 @@ func (fd *FileDescription) Listxattr(ctx context.Context) ([]string, error) {
|
||||
// Getxattr returns the value associated with the given extended attribute for
|
||||
// the file represented by fd.
|
||||
func (fd *FileDescription) Getxattr(ctx context.Context, name string) (string, error) {
|
||||
if fd.opts.UseDentryMetadata {
|
||||
vfsObj := fd.vd.mount.vfs
|
||||
rp := vfsObj.getResolvingPath(auth.CredentialsFromContext(ctx), &PathOperation{
|
||||
Root: fd.vd,
|
||||
Start: fd.vd,
|
||||
})
|
||||
val, err := fd.vd.mount.fs.impl.GetxattrAt(ctx, rp, name)
|
||||
vfsObj.putResolvingPath(rp)
|
||||
return val, err
|
||||
}
|
||||
return fd.impl.Getxattr(ctx, name)
|
||||
}
|
||||
|
||||
// Setxattr changes the value associated with the given extended attribute for
|
||||
// the file represented by fd.
|
||||
func (fd *FileDescription) Setxattr(ctx context.Context, opts SetxattrOptions) error {
|
||||
if fd.opts.UseDentryMetadata {
|
||||
vfsObj := fd.vd.mount.vfs
|
||||
rp := vfsObj.getResolvingPath(auth.CredentialsFromContext(ctx), &PathOperation{
|
||||
Root: fd.vd,
|
||||
Start: fd.vd,
|
||||
})
|
||||
err := fd.vd.mount.fs.impl.SetxattrAt(ctx, rp, opts)
|
||||
vfsObj.putResolvingPath(rp)
|
||||
return err
|
||||
}
|
||||
return fd.impl.Setxattr(ctx, opts)
|
||||
}
|
||||
|
||||
// Removexattr removes the given extended attribute from the file represented
|
||||
// by fd.
|
||||
func (fd *FileDescription) Removexattr(ctx context.Context, name string) error {
|
||||
if fd.opts.UseDentryMetadata {
|
||||
vfsObj := fd.vd.mount.vfs
|
||||
rp := vfsObj.getResolvingPath(auth.CredentialsFromContext(ctx), &PathOperation{
|
||||
Root: fd.vd,
|
||||
Start: fd.vd,
|
||||
})
|
||||
err := fd.vd.mount.fs.impl.RemovexattrAt(ctx, rp, name)
|
||||
vfsObj.putResolvingPath(rp)
|
||||
return err
|
||||
}
|
||||
return fd.impl.Removexattr(ctx, name)
|
||||
}
|
||||
|
||||
@@ -464,7 +549,7 @@ func (fd *FileDescription) MappedName(ctx context.Context) string {
|
||||
|
||||
// DeviceID implements memmap.MappingIdentity.DeviceID.
|
||||
func (fd *FileDescription) DeviceID() uint64 {
|
||||
stat, err := fd.impl.Stat(context.Background(), StatOptions{
|
||||
stat, err := fd.Stat(context.Background(), StatOptions{
|
||||
// There is no STATX_DEV; we assume that Stat will return it if it's
|
||||
// available regardless of mask.
|
||||
Mask: 0,
|
||||
@@ -480,7 +565,7 @@ func (fd *FileDescription) DeviceID() uint64 {
|
||||
|
||||
// InodeID implements memmap.MappingIdentity.InodeID.
|
||||
func (fd *FileDescription) InodeID() uint64 {
|
||||
stat, err := fd.impl.Stat(context.Background(), StatOptions{
|
||||
stat, err := fd.Stat(context.Background(), StatOptions{
|
||||
Mask: linux.STATX_INO,
|
||||
// fs/proc/task_mmu.c:show_map_vma() just reads inode::i_ino directly.
|
||||
Sync: linux.AT_STATX_DONT_SYNC,
|
||||
@@ -493,5 +578,5 @@ func (fd *FileDescription) InodeID() uint64 {
|
||||
|
||||
// Msync implements memmap.MappingIdentity.Msync.
|
||||
func (fd *FileDescription) Msync(ctx context.Context, mr memmap.MappableRange) error {
|
||||
return fd.impl.Sync(ctx)
|
||||
return fd.Sync(ctx)
|
||||
}
|
||||
|
||||
@@ -177,6 +177,21 @@ func (DirectoryFileDescriptionDefaultImpl) Write(ctx context.Context, src userme
|
||||
return 0, syserror.EISDIR
|
||||
}
|
||||
|
||||
// DentryMetadataFileDescriptionImpl may be embedded by implementations of
|
||||
// FileDescriptionImpl for which FileDescriptionOptions.UseDentryMetadata is
|
||||
// true to obtain implementations of Stat and SetStat that panic.
|
||||
type DentryMetadataFileDescriptionImpl struct{}
|
||||
|
||||
// Stat implements FileDescriptionImpl.Stat.
|
||||
func (DentryMetadataFileDescriptionImpl) Stat(ctx context.Context, opts StatOptions) (linux.Statx, error) {
|
||||
panic("illegal call to DentryMetadataFileDescriptionImpl.Stat")
|
||||
}
|
||||
|
||||
// SetStat implements FileDescriptionImpl.SetStat.
|
||||
func (DentryMetadataFileDescriptionImpl) SetStat(ctx context.Context, opts SetStatOptions) error {
|
||||
panic("illegal call to DentryMetadataFileDescriptionImpl.SetStat")
|
||||
}
|
||||
|
||||
// DynamicBytesFileDescriptionImpl may be embedded by implementations of
|
||||
// FileDescriptionImpl that represent read-only regular files whose contents
|
||||
// are backed by a bytes.Buffer that is regenerated when necessary, consistent
|
||||
|
||||
@@ -418,17 +418,38 @@ type FilesystemImpl interface {
|
||||
UnlinkAt(ctx context.Context, rp *ResolvingPath) error
|
||||
|
||||
// ListxattrAt returns all extended attribute names for the file at rp.
|
||||
//
|
||||
// Errors:
|
||||
//
|
||||
// - If extended attributes are not supported by the filesystem,
|
||||
// ListxattrAt returns nil. (See FileDescription.Listxattr for an
|
||||
// explanation.)
|
||||
ListxattrAt(ctx context.Context, rp *ResolvingPath) ([]string, error)
|
||||
|
||||
// GetxattrAt returns the value associated with the given extended
|
||||
// attribute for the file at rp.
|
||||
//
|
||||
// Errors:
|
||||
//
|
||||
// - If extended attributes are not supported by the filesystem, GetxattrAt
|
||||
// returns ENOTSUP.
|
||||
GetxattrAt(ctx context.Context, rp *ResolvingPath, name string) (string, error)
|
||||
|
||||
// SetxattrAt changes the value associated with the given extended
|
||||
// attribute for the file at rp.
|
||||
//
|
||||
// Errors:
|
||||
//
|
||||
// - If extended attributes are not supported by the filesystem, SetxattrAt
|
||||
// returns ENOTSUP.
|
||||
SetxattrAt(ctx context.Context, rp *ResolvingPath, opts SetxattrOptions) error
|
||||
|
||||
// RemovexattrAt removes the given extended attribute from the file at rp.
|
||||
//
|
||||
// Errors:
|
||||
//
|
||||
// - If extended attributes are not supported by the filesystem,
|
||||
// RemovexattrAt returns ENOTSUP.
|
||||
RemovexattrAt(ctx context.Context, rp *ResolvingPath, name string) error
|
||||
|
||||
// PrependPath prepends a path from vd to vd.Mount().Root() to b.
|
||||
|
||||
@@ -75,6 +75,11 @@ type VirtualFilesystem struct {
|
||||
// mountpoints is analogous to Linux's mountpoint_hashtable.
|
||||
mountpoints map[*Dentry]map[*Mount]struct{}
|
||||
|
||||
// devices contains all registered Devices. devices is protected by
|
||||
// devicesMu.
|
||||
devicesMu sync.RWMutex
|
||||
devices map[devTuple]*registeredDevice
|
||||
|
||||
// fsTypes contains all registered FilesystemTypes. fsTypes is protected by
|
||||
// fsTypesMu.
|
||||
fsTypesMu sync.RWMutex
|
||||
@@ -90,6 +95,7 @@ type VirtualFilesystem struct {
|
||||
func New() *VirtualFilesystem {
|
||||
vfs := &VirtualFilesystem{
|
||||
mountpoints: make(map[*Dentry]map[*Mount]struct{}),
|
||||
devices: make(map[devTuple]*registeredDevice),
|
||||
fsTypes: make(map[string]*registeredFilesystemType),
|
||||
filesystems: make(map[*Filesystem]struct{}),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user