Implement FUSE_OPEN/OPENDIR

Fixes #3174
This commit is contained in:
Boyuan He
2020-09-16 12:19:30 -07:00
committed by Andrei Vagin
parent d6ee3ae6d7
commit 32044f94e9
13 changed files with 481 additions and 32 deletions
+38
View File
@@ -357,3 +357,41 @@ func (r *FUSELookupIn) MarshalUnsafe(buf []byte) {
func (r *FUSELookupIn) SizeBytes() int {
return len(r.Name) + 1
}
// MAX_NON_LFS indicates the maximum offset without large file support.
const MAX_NON_LFS = ((1 << 31) - 1)
// flags returned by OPEN request.
const (
// FOPEN_DIRECT_IO indicates bypassing page cache for this opened file.
FOPEN_DIRECT_IO = 1 << 0
// FOPEN_KEEP_CACHE avoids invalidate of data cache on open.
FOPEN_KEEP_CACHE = 1 << 1
// FOPEN_NONSEEKABLE indicates the file cannot be seeked.
FOPEN_NONSEEKABLE = 1 << 2
)
// FUSEOpenIn is the request sent by the kernel to the daemon,
// to negotiate flags and get file handle.
//
// +marshal
type FUSEOpenIn struct {
// Flags of this open request.
Flags uint32
_ uint32
}
// FUSEOpenOut is the reply sent by the daemon to the kernel
// for FUSEOpenIn.
//
// +marshal
type FUSEOpenOut struct {
// Fh is the file handler for opened file.
Fh uint64
// OpenFlag for the opened file.
OpenFlag uint32
_ uint32
}
+1
View File
@@ -31,6 +31,7 @@ go_library(
srcs = [
"connection.go",
"dev.go",
"file.go",
"fusefs.go",
"init.go",
"inode_refs.go",
+15 -1
View File
@@ -78,8 +78,13 @@ type Response struct {
type connection struct {
fd *DeviceFD
// mu protect access to struct memebers.
mu sync.Mutex
// attributeVersion is the version of connection's attributes.
attributeVersion uint64
// The following FUSE_INIT flags are currently unsupported by this implementation:
// - FUSE_ATOMIC_O_TRUNC: requires open(..., O_TRUNC)
// - FUSE_EXPORT_SUPPORT
// - FUSE_HANDLE_KILLPRIV
// - FUSE_POSIX_LOCKS: requires POSIX locks
@@ -113,6 +118,11 @@ type connection struct {
// TODO(gvisor.dev/issue/3185): abort all queued requests.
aborted bool
// atomicOTrunc is true when FUSE does not send a separate SETATTR request
// before open with O_TRUNC flag.
// Negotiated and only set in INIT.
atomicOTrunc bool
// connInitError if FUSE_INIT encountered error (major version mismatch).
// Only set in INIT.
connInitError bool
@@ -189,6 +199,10 @@ type connection struct {
// dontMask if filestestem does not apply umask to creation modes.
// Negotiated in INIT.
dontMask bool
// noOpen if FUSE server doesn't support open operation.
// This flag only influence performance, not correctness of the program.
noOpen bool
}
// newFUSEConnection creates a FUSE connection to fd.
+96
View File
@@ -0,0 +1,96 @@
// 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 fuse
import (
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs"
"gvisor.dev/gvisor/pkg/sentry/vfs"
"gvisor.dev/gvisor/pkg/usermem"
)
// fileDescription implements vfs.FileDescriptionImpl for fuse.
type fileDescription struct {
vfsfd vfs.FileDescription
vfs.FileDescriptionDefaultImpl
vfs.DentryMetadataFileDescriptionImpl
vfs.NoLockFD
// the file handle used in userspace.
Fh uint64
// Nonseekable is indicate cannot perform seek on a file.
Nonseekable bool
// DirectIO suggest fuse to use direct io operation.
DirectIO bool
// OpenFlag is the flag returned by open.
OpenFlag uint32
}
func (fd *fileDescription) dentry() *kernfs.Dentry {
return fd.vfsfd.Dentry().Impl().(*kernfs.Dentry)
}
func (fd *fileDescription) inode() *inode {
return fd.dentry().Inode().(*inode)
}
func (fd *fileDescription) filesystem() *vfs.Filesystem {
return fd.vfsfd.VirtualDentry().Mount().Filesystem()
}
// Release implements vfs.FileDescriptionImpl.Release.
func (fd *fileDescription) Release(ctx context.Context) {}
// PRead implements vfs.FileDescriptionImpl.PRead.
func (fd *fileDescription) PRead(ctx context.Context, dst usermem.IOSequence, offset int64, opts vfs.ReadOptions) (int64, error) {
return 0, nil
}
// Read implements vfs.FileDescriptionImpl.Read.
func (fd *fileDescription) Read(ctx context.Context, dst usermem.IOSequence, opts vfs.ReadOptions) (int64, error) {
return 0, nil
}
// PWrite implements vfs.FileDescriptionImpl.PWrite.
func (fd *fileDescription) PWrite(ctx context.Context, src usermem.IOSequence, offset int64, opts vfs.WriteOptions) (int64, error) {
return 0, nil
}
// Write implements vfs.FileDescriptionImpl.Write.
func (fd *fileDescription) Write(ctx context.Context, src usermem.IOSequence, opts vfs.WriteOptions) (int64, error) {
return 0, nil
}
// Seek implements vfs.FileDescriptionImpl.Seek.
func (fd *fileDescription) Seek(ctx context.Context, offset int64, whence int32) (int64, error) {
return 0, nil
}
// Stat implements FileDescriptionImpl.Stat.
// Stat implements vfs.FileDescriptionImpl.Stat.
func (fd *fileDescription) Stat(ctx context.Context, opts vfs.StatOptions) (linux.Statx, error) {
fs := fd.filesystem()
inode := fd.inode()
return inode.Stat(ctx, fs, opts)
}
// SetStat implements FileDescriptionImpl.SetStat.
func (fd *fileDescription) SetStat(ctx context.Context, opts vfs.SetStatOptions) error {
return nil
}
+92 -5
View File
@@ -17,6 +17,7 @@ package fuse
import (
"strconv"
"sync/atomic"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/context"
@@ -212,6 +213,18 @@ type inode struct {
// the owning filesystem. fs is immutable.
fs *filesystem
// size of the file.
size uint64
// attributeVersion is the version of inode's attributes.
attributeVersion uint64
// attributeTime is the remaining vaild time of attributes.
attributeTime uint64
// version of the inode.
version uint64
}
func (fs *filesystem) newRootInode(creds *auth.Credentials, mode linux.FileMode) *kernfs.Dentry {
@@ -237,13 +250,87 @@ func (fs *filesystem) newInode(nodeID uint64, attr linux.FUSEAttr) *kernfs.Dentr
// Open implements kernfs.Inode.Open.
func (i *inode) Open(ctx context.Context, rp *vfs.ResolvingPath, vfsd *vfs.Dentry, opts vfs.OpenOptions) (*vfs.FileDescription, error) {
fd, err := kernfs.NewGenericDirectoryFD(rp.Mount(), vfsd, &i.OrderedChildren, &i.locks, &opts, kernfs.GenericDirectoryFDOptions{
SeekEnd: kernfs.SeekEndStaticEntries,
})
if err != nil {
isDir := i.InodeAttrs.Mode().IsDir()
// return error if specified to open directory but inode is not a directory.
if !isDir && opts.Mode.IsDir() {
return nil, syserror.ENOTDIR
}
if opts.Flags&linux.O_LARGEFILE == 0 && atomic.LoadUint64(&i.size) > linux.MAX_NON_LFS {
return nil, syserror.EOVERFLOW
}
// FOPEN_KEEP_CACHE is the defualt flag for noOpen.
fd := fileDescription{OpenFlag: linux.FOPEN_KEEP_CACHE}
// Only send open request when FUSE server support open or is opening a directory.
if !i.fs.conn.noOpen || isDir {
kernelTask := kernel.TaskFromContext(ctx)
if kernelTask == nil {
log.Warningf("fusefs.Inode.Open: couldn't get kernel task from context")
return nil, syserror.EINVAL
}
var opcode linux.FUSEOpcode
if isDir {
opcode = linux.FUSE_OPENDIR
} else {
opcode = linux.FUSE_OPEN
}
in := linux.FUSEOpenIn{Flags: opts.Flags & ^uint32(linux.O_CREAT|linux.O_EXCL|linux.O_NOCTTY)}
if !i.fs.conn.atomicOTrunc {
in.Flags &= ^uint32(linux.O_TRUNC)
}
req, err := i.fs.conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(kernelTask.ThreadID()), i.NodeID, opcode, &in)
if err != nil {
return nil, err
}
res, err := i.fs.conn.Call(kernelTask, req)
if err != nil {
return nil, err
}
if err := res.Error(); err == syserror.ENOSYS && !isDir {
i.fs.conn.noOpen = true
} else if err != nil {
return nil, err
} else {
out := linux.FUSEOpenOut{}
if err := res.UnmarshalPayload(&out); err != nil {
return nil, err
}
fd.OpenFlag = out.OpenFlag
fd.Fh = out.Fh
}
}
if isDir {
fd.OpenFlag &= ^uint32(linux.FOPEN_DIRECT_IO)
}
// TODO(gvisor.dev/issue/3234): invalidate mmap after implemented it for FUSE Inode
fd.DirectIO = fd.OpenFlag&linux.FOPEN_DIRECT_IO != 0
fdOptions := &vfs.FileDescriptionOptions{}
if fd.OpenFlag&linux.FOPEN_NONSEEKABLE != 0 {
fdOptions.DenyPRead = true
fdOptions.DenyPWrite = true
fd.Nonseekable = true
}
// If we don't send SETATTR before open (which is indicated by atomicOTrunc)
// and O_TRUNC is set, update the inode's version number and clean existing data
// by setting the file size to 0.
if i.fs.conn.atomicOTrunc && opts.Flags&linux.O_TRUNC != 0 {
i.fs.conn.mu.Lock()
i.fs.conn.attributeVersion++
i.attributeVersion = i.fs.conn.attributeVersion
atomic.StoreUint64(&i.size, 0)
i.fs.conn.mu.Unlock()
i.attributeTime = 0
}
if err := fd.vfsfd.Init(&fd, opts.Flags, rp.Mount(), vfsd, fdOptions); err != nil {
return nil, err
}
return fd.VFSFileDescription(), nil
return &fd.vfsfd, nil
}
// Lookup implements kernfs.Inode.Lookup.
+5
View File
@@ -6,3 +6,8 @@ syscall_test(
fuse = "True",
test = "//test/fuse/linux:stat_test",
)
syscall_test(
fuse = "True",
test = "//test/fuse/linux:open_test",
)
+13
View File
@@ -18,6 +18,19 @@ cc_binary(
],
)
cc_binary(
name = "open_test",
testonly = 1,
srcs = ["open_test.cc"],
deps = [
gtest,
":fuse_base",
"//test/util:fuse_util",
"//test/util:test_main",
"//test/util:test_util",
],
)
cc_library(
name = "fuse_base",
testonly = 1,
+24 -26
View File
@@ -117,6 +117,16 @@ uint32_t FuseTest::GetServerTotalReceivedBytes() {
static_cast<uint32_t>(FuseTestCmd::kGetTotalReceivedBytes));
}
// Sends the `kSkipRequest` command to the FUSE server, which would skip
// current stored request data.
void FuseTest::SkipServerActualRequest() {
uint32_t cmd = static_cast<uint32_t>(FuseTestCmd::kSkipRequest);
EXPECT_THAT(RetryEINTR(write)(sock_[0], &cmd, sizeof(cmd)),
SyscallSucceedsWithValue(sizeof(cmd)));
WaitServerComplete();
}
// Sends the `kSetInodeLookup` command, expected mode, and the path of the
// inode to create under the mount point.
void FuseTest::SetServerInodeLookup(const std::string& path, mode_t mode) {
@@ -284,6 +294,9 @@ void FuseTest::ServerHandleCommand() {
case FuseTestCmd::kGetNumUnsentResponses:
ServerSendData(static_cast<uint32_t>(responses_.RemainingBlocks()));
break;
case FuseTestCmd::kSkipRequest:
ServerSkipReceivedRequest();
break;
default:
FAIL() << "Unknown FuseTestCmd " << cmd;
break;
@@ -314,32 +327,7 @@ void FuseTest::ServerReceiveInodeLookup() {
.len = out_len,
.error = 0,
};
struct fuse_entry_out out_payload = {
.nodeid = nodeid_,
.generation = 0,
.entry_valid = 0,
.attr_valid = 0,
.entry_valid_nsec = 0,
.attr_valid_nsec = 0,
.attr =
(struct fuse_attr){
.ino = nodeid_,
.size = 512,
.blocks = 4,
.atime = 0,
.mtime = 0,
.ctime = 0,
.atimensec = 0,
.mtimensec = 0,
.ctimensec = 0,
.mode = mode,
.nlink = 2,
.uid = 1234,
.gid = 4321,
.rdev = 12,
.blksize = 4096,
},
};
struct fuse_entry_out out_payload = DefaultEntryOut(mode, nodeid_);
// Since this is only used in test, nodeid_ is simply increased by 1 to
// comply with the unqiueness of different path.
++nodeid_;
@@ -363,6 +351,15 @@ void FuseTest::ServerSendReceivedRequest() {
SyscallSucceedsWithValue(mem_block.len));
}
// Skip the request pointed by current cursor.
void FuseTest::ServerSkipReceivedRequest() {
if (requests_.End()) {
FAIL() << "No more received request.";
return;
}
requests_.Next();
}
// Handles FUSE request. Reads request from /dev/fuse, checks if it has the
// same opcode as expected, and responds with the saved fake FUSE response.
// The FUSE request is copied to the serial buffer and can be retrieved one-
@@ -390,6 +387,7 @@ void FuseTest::ServerProcessFuseRequest() {
requests_.AddMemBlock(in_header->opcode, buf.data(), len);
if (in_header->opcode == FUSE_RELEASE) return;
// Check if there is a corresponding response.
if (responses_.End()) {
GTEST_NONFATAL_FAILURE_("No more FUSE response is expected");
+9
View File
@@ -42,6 +42,7 @@ enum class FuseTestCmd {
kGetNumUnconsumedRequests,
kGetNumUnsentResponses,
kGetTotalReceivedBytes,
kSkipRequest,
};
// Holds the information of a memory block in a serial buffer.
@@ -158,6 +159,10 @@ class FuseTest : public ::testing::Test {
// bytes from /dev/fuse.
uint32_t GetServerTotalReceivedBytes();
// Called by the testing thread to ask the FUSE server to skip stored
// request data.
void SkipServerActualRequest();
protected:
TempPath mount_point_;
@@ -211,6 +216,10 @@ class FuseTest : public ::testing::Test {
// Sends a uint32_t data via socket.
inline void ServerSendData(uint32_t data);
// The FUSE server side's corresponding code of `SkipServerActualRequest()`.
// Handles `kSkipRequest` command. Skip the request pointed by current cursor.
void ServerSkipReceivedRequest();
// Handles FUSE request sent to /dev/fuse by its saved responses.
void ServerProcessFuseRequest();
+124
View File
@@ -0,0 +1,124 @@
// 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.
#include <errno.h>
#include <fcntl.h>
#include <linux/fuse.h>
#include <sys/stat.h>
#include <sys/statfs.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>
#include "gtest/gtest.h"
#include "test/fuse/linux/fuse_base.h"
#include "test/util/fuse_util.h"
#include "test/util/test_util.h"
namespace gvisor {
namespace testing {
namespace {
class OpenTest : public FuseTest {
protected:
const std::string test_file_ = "test_file";
const mode_t regular_file_ = S_IFREG | S_IRWXU | S_IRWXG | S_IRWXO;
struct fuse_open_out out_payload_ = {
.fh = 1,
.open_flags = O_RDWR,
};
};
TEST_F(OpenTest, RegularFile) {
const std::string test_file_path =
JoinPath(mount_point_.path().c_str(), test_file_);
SetServerInodeLookup(test_file_, regular_file_);
struct fuse_out_header out_header = {
.len = sizeof(struct fuse_out_header) + sizeof(struct fuse_open_out),
};
auto iov_out = FuseGenerateIovecs(out_header, out_payload_);
SetServerResponse(FUSE_OPEN, iov_out);
FileDescriptor fd =
ASSERT_NO_ERRNO_AND_VALUE(Open(test_file_path.c_str(), O_RDWR));
struct fuse_in_header in_header;
struct fuse_open_in in_payload;
auto iov_in = FuseGenerateIovecs(in_header, in_payload);
GetServerActualRequest(iov_in);
EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload));
EXPECT_EQ(in_header.opcode, FUSE_OPEN);
EXPECT_EQ(in_payload.flags, O_RDWR);
EXPECT_THAT(fcntl(fd.get(), F_GETFL), SyscallSucceedsWithValue(O_RDWR));
}
TEST_F(OpenTest, SetNoOpen) {
const std::string test_file_path =
JoinPath(mount_point_.path().c_str(), test_file_);
SetServerInodeLookup(test_file_, regular_file_);
// ENOSYS indicates open is not implemented.
struct fuse_out_header out_header = {
.len = sizeof(struct fuse_out_header) + sizeof(struct fuse_open_out),
.error = -ENOSYS,
};
auto iov_out = FuseGenerateIovecs(out_header, out_payload_);
SetServerResponse(FUSE_OPEN, iov_out);
ASSERT_NO_ERRNO_AND_VALUE(Open(test_file_path.c_str(), O_RDWR));
SkipServerActualRequest();
// check open doesn't send new request.
uint32_t recieved_before = GetServerTotalReceivedBytes();
ASSERT_NO_ERRNO_AND_VALUE(Open(test_file_path.c_str(), O_RDWR));
EXPECT_EQ(GetServerTotalReceivedBytes(), recieved_before);
}
TEST_F(OpenTest, OpenFail) {
struct fuse_out_header out_header = {
.len = sizeof(struct fuse_out_header) + sizeof(struct fuse_open_out),
.error = -ENOENT,
};
auto iov_out = FuseGenerateIovecs(out_header, out_payload_);
SetServerResponse(FUSE_OPENDIR, iov_out);
ASSERT_THAT(open(mount_point_.path().c_str(), O_RDWR),
SyscallFailsWithErrno(ENOENT));
struct fuse_in_header in_header;
struct fuse_open_in in_payload;
auto iov_in = FuseGenerateIovecs(in_header, in_payload);
GetServerActualRequest(iov_in);
EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload));
EXPECT_EQ(in_header.opcode, FUSE_OPENDIR);
EXPECT_EQ(in_payload.flags, O_RDWR);
}
TEST_F(OpenTest, DirectoryFlagOnRegularFile) {
const std::string test_file_path =
JoinPath(mount_point_.path().c_str(), test_file_);
SetServerInodeLookup(test_file_, regular_file_);
ASSERT_THAT(open(test_file_path.c_str(), O_RDWR | O_DIRECTORY),
SyscallFailsWithErrno(ENOTDIR));
}
} // namespace
} // namespace testing
} // namespace gvisor
+1
View File
@@ -48,6 +48,7 @@ cc_library(
cc_library(
name = "fuse_util",
testonly = 1,
srcs = ["fuse_util.cc"],
hdrs = ["fuse_util.h"],
)
+59
View File
@@ -0,0 +1,59 @@
// 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.
#include "test/util/fuse_util.h"
#include <sys/stat.h>
#include <sys/types.h>
#include <string>
namespace gvisor {
namespace testing {
// Create response body with specified mode and nodeID.
fuse_entry_out DefaultEntryOut(mode_t mode, uint64_t node_id) {
const int time_sec = 1595436289;
const int time_nsec = 134150844;
struct fuse_entry_out default_entry_out = {
.nodeid = node_id,
.generation = 0,
.entry_valid = time_sec,
.attr_valid = time_sec,
.entry_valid_nsec = time_nsec,
.attr_valid_nsec = time_nsec,
.attr =
(struct fuse_attr){
.ino = node_id,
.size = 512,
.blocks = 4,
.atime = time_sec,
.mtime = time_sec,
.ctime = time_sec,
.atimensec = time_nsec,
.mtimensec = time_nsec,
.ctimensec = time_nsec,
.mode = mode,
.nlink = 2,
.uid = 1234,
.gid = 4321,
.rdev = 12,
.blksize = 4096,
},
};
return default_entry_out;
};
} // namespace testing
} // namespace gvisor
+4
View File
@@ -15,6 +15,7 @@
#ifndef GVISOR_TEST_UTIL_FUSE_UTIL_H_
#define GVISOR_TEST_UTIL_FUSE_UTIL_H_
#include <linux/fuse.h>
#include <sys/uio.h>
#include <string>
@@ -62,6 +63,9 @@ std::vector<struct iovec> FuseGenerateIovecs(T &first, Types &...args) {
return first_iovec;
}
// Return a fuse_entry_out FUSE server response body.
fuse_entry_out DefaultEntryOut(mode_t mode, uint64_t nodeId);
} // namespace testing
} // namespace gvisor
#endif // GVISOR_TEST_UTIL_FUSE_UTIL_H_