summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorCraig Chi <craigchi@google.com>2020-08-10 18:15:32 -0700
committerCraig Chi <craigchi@google.com>2020-08-10 18:15:32 -0700
commit51e64d2fc590b0271d4e0cbbc75882cf81ada182 (patch)
tree1e99952f3b06cb290a414816c21d9a4e698bb1e9
parentb404b5c255214a37d7f787f9fe24bb8e22509eb4 (diff)
Implement FUSE_GETATTR
FUSE_GETATTR is called when a stat(2), fstat(2), or lstat(2) is issued from VFS2 layer to a FUSE filesystem. Fixes #3175
-rw-r--r--pkg/abi/linux/fuse.go55
-rw-r--r--pkg/sentry/fsimpl/fuse/fusefs.go96
-rw-r--r--test/fuse/BUILD8
-rw-r--r--test/fuse/linux/BUILD21
-rw-r--r--test/fuse/linux/fuse_base.cc15
-rw-r--r--test/fuse/linux/fuse_base.h6
-rw-r--r--test/fuse/linux/stat_test.cc169
-rw-r--r--test/runner/defs.bzl45
8 files changed, 375 insertions, 40 deletions
diff --git a/pkg/abi/linux/fuse.go b/pkg/abi/linux/fuse.go
index 5c6ffe4a3..7e30483ee 100644
--- a/pkg/abi/linux/fuse.go
+++ b/pkg/abi/linux/fuse.go
@@ -246,3 +246,58 @@ type FUSEInitOut struct {
_ [8]uint32
}
+
+// FUSEGetAttrIn is the request sent by the kernel to the daemon,
+// to get the attribute of a inode.
+//
+// +marshal
+type FUSEGetAttrIn struct {
+ // GetAttrFlags specifies whether getattr request is sent with a nodeid or
+ // with a file handle.
+ GetAttrFlags uint32
+
+ _ uint32
+
+ // Fh is the file handler when GetAttrFlags has FUSE_GETATTR_FH bit. If
+ // used, the operation is analogous to fstat(2).
+ Fh uint64
+}
+
+// FUSEAttr is the struct used in the reponse FUSEGetAttrOut.
+//
+// +marshal
+type FUSEAttr struct {
+ Ino uint64
+ Size uint64
+ Blocks uint64
+ Atime uint64
+ Mtime uint64
+ Ctime uint64
+ AtimeNsec uint32
+ MtimeNsec uint32
+ CtimeNsec uint32
+ Mode uint32
+ Nlink uint32
+ UID uint32
+ GID uint32
+ Rdev uint32
+ BlkSize uint32
+ _ uint32
+}
+
+// FUSEGetAttrOut is the reply sent by the daemon to the kernel
+// for FUSEGetAttrIn.
+//
+// +marshal
+type FUSEGetAttrOut struct {
+ // AttrValid and AttrValidNsec describe the attribute cache duration
+ AttrValid uint64
+
+ // AttrValidNsec is the nanosecond part of the attribute cache duration
+ AttrValidNsec uint32
+
+ _ uint32
+
+ // Attr contains the metadata returned from the FUSE server
+ Attr FUSEAttr
+}
diff --git a/pkg/sentry/fsimpl/fuse/fusefs.go b/pkg/sentry/fsimpl/fuse/fusefs.go
index a1405f7c3..83c24ec25 100644
--- a/pkg/sentry/fsimpl/fuse/fusefs.go
+++ b/pkg/sentry/fsimpl/fuse/fusefs.go
@@ -226,3 +226,99 @@ func (i *inode) Open(ctx context.Context, rp *vfs.ResolvingPath, vfsd *vfs.Dentr
}
return fd.VFSFileDescription(), nil
}
+
+// statFromFUSEAttr makes attributes from linux.FUSEAttr to linux.Statx. The
+// opts.Sync attribute is ignored since the synchronization is handled by the
+// FUSE server.
+func statFromFUSEAttr(attr linux.FUSEAttr, mask, devMinor uint32) linux.Statx {
+ var stat linux.Statx
+ stat.Blksize = attr.BlkSize
+ stat.DevMajor, stat.DevMinor = linux.UNNAMED_MAJOR, devMinor
+
+ rdevMajor, rdevMinor := linux.DecodeDeviceID(attr.Rdev)
+ stat.RdevMajor, stat.RdevMinor = uint32(rdevMajor), rdevMinor
+
+ if mask&linux.STATX_MODE != 0 {
+ stat.Mode = uint16(attr.Mode)
+ }
+ if mask&linux.STATX_NLINK != 0 {
+ stat.Nlink = attr.Nlink
+ }
+ if mask&linux.STATX_UID != 0 {
+ stat.UID = attr.UID
+ }
+ if mask&linux.STATX_GID != 0 {
+ stat.GID = attr.GID
+ }
+ if mask&linux.STATX_ATIME != 0 {
+ stat.Atime = linux.StatxTimestamp{
+ Sec: int64(attr.Atime),
+ Nsec: attr.AtimeNsec,
+ }
+ }
+ if mask&linux.STATX_MTIME != 0 {
+ stat.Mtime = linux.StatxTimestamp{
+ Sec: int64(attr.Mtime),
+ Nsec: attr.MtimeNsec,
+ }
+ }
+ if mask&linux.STATX_CTIME != 0 {
+ stat.Ctime = linux.StatxTimestamp{
+ Sec: int64(attr.Ctime),
+ Nsec: attr.CtimeNsec,
+ }
+ }
+ if mask&linux.STATX_INO != 0 {
+ stat.Ino = attr.Ino
+ }
+ if mask&linux.STATX_SIZE != 0 {
+ stat.Size = attr.Size
+ }
+ if mask&linux.STATX_BLOCKS != 0 {
+ stat.Blocks = attr.Blocks
+ }
+ return stat
+}
+
+// Stat implements kernfs.Inode.Stat.
+func (i *inode) Stat(ctx context.Context, fs *vfs.Filesystem, opts vfs.StatOptions) (linux.Statx, error) {
+ fusefs := fs.Impl().(*filesystem)
+ conn := fusefs.conn
+ task, creds := kernel.TaskFromContext(ctx), auth.CredentialsFromContext(ctx)
+ if task == nil {
+ log.Warningf("couldn't get kernel task from context")
+ return linux.Statx{}, syserror.EINVAL
+ }
+
+ var in linux.FUSEGetAttrIn
+ // We don't set any attribute in the request, because in VFS2 fstat(2) will
+ // finally be translated into vfs.FilesystemImpl.StatAt() (see
+ // pkg/sentry/syscalls/linux/vfs2/stat.go), resulting in the same flow
+ // as stat(2). Thus GetAttrFlags and Fh variable will never be used in VFS2.
+ req, err := conn.NewRequest(creds, uint32(task.ThreadID()), i.Ino(), linux.FUSE_GETATTR, &in)
+ if err != nil {
+ return linux.Statx{}, err
+ }
+
+ res, err := conn.Call(task, req)
+ if err != nil {
+ return linux.Statx{}, err
+ }
+ if err := res.Error(); err != nil {
+ return linux.Statx{}, err
+ }
+
+ var out linux.FUSEGetAttrOut
+ if err := res.UnmarshalPayload(&out); err != nil {
+ return linux.Statx{}, err
+ }
+
+ // Set all metadata into kernfs.InodeAttrs.
+ if err := i.SetStat(ctx, fs, creds, vfs.SetStatOptions{
+ Stat: statFromFUSEAttr(out.Attr, linux.STATX_ALL, fusefs.devMinor),
+ }); err != nil {
+ return linux.Statx{}, err
+ }
+
+ return statFromFUSEAttr(out.Attr, opts.Mask, fusefs.devMinor), nil
+}
diff --git a/test/fuse/BUILD b/test/fuse/BUILD
index 34b950644..56157c96b 100644
--- a/test/fuse/BUILD
+++ b/test/fuse/BUILD
@@ -1 +1,9 @@
+load("//test/runner:defs.bzl", "syscall_test")
+
package(licenses = ["notice"])
+
+syscall_test(
+ fuse = "True",
+ test = "//test/fuse/linux:stat_test",
+ vfs2 = "True",
+)
diff --git a/test/fuse/linux/BUILD b/test/fuse/linux/BUILD
index 49dc96c20..4871bb531 100644
--- a/test/fuse/linux/BUILD
+++ b/test/fuse/linux/BUILD
@@ -1,20 +1,31 @@
-load("//tools:defs.bzl", "cc_library", "gtest")
+load("//tools:defs.bzl", "cc_binary", "cc_library", "gtest")
package(
default_visibility = ["//:sandbox"],
licenses = ["notice"],
)
+cc_binary(
+ name = "stat_test",
+ testonly = 1,
+ srcs = ["stat_test.cc"],
+ deps = [
+ gtest,
+ ":fuse_base",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
+
cc_library(
name = "fuse_base",
testonly = 1,
- srcs = [
- "fuse_base.cc",
- "fuse_base.h",
- ],
+ srcs = ["fuse_base.cc"],
+ hdrs = ["fuse_base.h"],
deps = [
gtest,
"//test/util:posix_error",
+ "//test/util:temp_path",
"//test/util:test_util",
"@com_google_absl//absl/strings:str_format",
],
diff --git a/test/fuse/linux/fuse_base.cc b/test/fuse/linux/fuse_base.cc
index ce69276c9..4a2c64998 100644
--- a/test/fuse/linux/fuse_base.cc
+++ b/test/fuse/linux/fuse_base.cc
@@ -12,23 +12,23 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-#include "fuse_base.h"
+#include "test/fuse/linux/fuse_base.h"
#include <fcntl.h>
#include <linux/fuse.h>
-#include <stdio.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
+#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
#include <iostream>
-#include "gmock/gmock.h"
-#include "gtest/gtest.h"
#include "absl/strings/str_format.h"
+#include "gtest/gtest.h"
#include "test/util/posix_error.h"
+#include "test/util/temp_path.h"
#include "test/util/test_util.h"
namespace gvisor {
@@ -78,13 +78,14 @@ void FuseTest::MountFuse() {
EXPECT_THAT(dev_fd_ = open("/dev/fuse", O_RDWR), SyscallSucceeds());
std::string mount_opts = absl::StrFormat("fd=%d,%s", dev_fd_, kMountOpts);
- EXPECT_THAT(mount("fuse", kMountPoint, "fuse", MS_NODEV | MS_NOSUID,
- mount_opts.c_str()),
+ mount_point_ = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ EXPECT_THAT(mount("fuse", mount_point_.path().c_str(), "fuse",
+ MS_NODEV | MS_NOSUID, mount_opts.c_str()),
SyscallSucceedsWithValue(0));
}
void FuseTest::UnmountFuse() {
- EXPECT_THAT(umount(kMountPoint), SyscallSucceeds());
+ EXPECT_THAT(umount(mount_point_.path().c_str()), SyscallSucceeds());
// TODO(gvisor.dev/issue/3330): ensure the process is terminated successfully.
}
diff --git a/test/fuse/linux/fuse_base.h b/test/fuse/linux/fuse_base.h
index b008778de..3a2f255a9 100644
--- a/test/fuse/linux/fuse_base.h
+++ b/test/fuse/linux/fuse_base.h
@@ -20,14 +20,13 @@
#include <vector>
-#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "test/util/posix_error.h"
+#include "test/util/temp_path.h"
namespace gvisor {
namespace testing {
-constexpr char kMountPoint[] = "/mnt";
constexpr char kMountOpts[] = "rootmode=755,user_id=0,group_id=0";
class FuseTest : public ::testing::Test {
@@ -55,6 +54,9 @@ class FuseTest : public ::testing::Test {
// complains if the FUSE server responds failure during tests.
void WaitCompleted();
+ protected:
+ TempPath mount_point_;
+
private:
void MountFuse();
void UnmountFuse();
diff --git a/test/fuse/linux/stat_test.cc b/test/fuse/linux/stat_test.cc
new file mode 100644
index 000000000..172e09867
--- /dev/null
+++ b/test/fuse/linux/stat_test.cc
@@ -0,0 +1,169 @@
+// 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 <vector>
+
+#include "gtest/gtest.h"
+#include "test/fuse/linux/fuse_base.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+class StatTest : public FuseTest {
+ public:
+ bool CompareRequest(void* expected_mem, size_t expected_len, void* real_mem,
+ size_t real_len) override {
+ if (expected_len != real_len) return false;
+ struct fuse_in_header* real_header =
+ reinterpret_cast<fuse_in_header*>(real_mem);
+
+ if (real_header->opcode != FUSE_GETATTR) {
+ std::cerr << "expect header opcode " << FUSE_GETATTR << " but got "
+ << real_header->opcode << std::endl;
+ return false;
+ }
+ return true;
+ }
+
+ bool StatsAreEqual(struct stat expected, struct stat actual) {
+ // device number will be dynamically allocated by kernel, we cannot know
+ // in advance
+ actual.st_dev = expected.st_dev;
+ return memcmp(&expected, &actual, sizeof(struct stat)) == 0;
+ }
+};
+
+TEST_F(StatTest, StatNormal) {
+ struct iovec iov_in[2];
+ struct iovec iov_out[2];
+
+ struct fuse_in_header in_header = {
+ .len = sizeof(struct fuse_in_header) + sizeof(struct fuse_getattr_in),
+ .opcode = FUSE_GETATTR,
+ .unique = 4,
+ .nodeid = 1,
+ .uid = 0,
+ .gid = 0,
+ .pid = 4,
+ .padding = 0,
+ };
+ struct fuse_getattr_in in_payload = {0};
+ iov_in[0].iov_len = sizeof(in_header);
+ iov_in[0].iov_base = &in_header;
+ iov_in[1].iov_len = sizeof(in_payload);
+ iov_in[1].iov_base = &in_payload;
+
+ mode_t expected_mode = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH;
+ struct timespec atime = {.tv_sec = 1595436289, .tv_nsec = 134150844};
+ struct timespec mtime = {.tv_sec = 1595436290, .tv_nsec = 134150845};
+ struct timespec ctime = {.tv_sec = 1595436291, .tv_nsec = 134150846};
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_attr_out),
+ .error = 0,
+ .unique = 4,
+ };
+ struct fuse_attr attr = {
+ .ino = 1,
+ .size = 512,
+ .blocks = 4,
+ .atime = static_cast<uint64_t>(atime.tv_sec),
+ .mtime = static_cast<uint64_t>(mtime.tv_sec),
+ .ctime = static_cast<uint64_t>(ctime.tv_sec),
+ .atimensec = static_cast<uint32_t>(atime.tv_nsec),
+ .mtimensec = static_cast<uint32_t>(mtime.tv_nsec),
+ .ctimensec = static_cast<uint32_t>(ctime.tv_nsec),
+ .mode = expected_mode,
+ .nlink = 2,
+ .uid = 1234,
+ .gid = 4321,
+ .rdev = 12,
+ .blksize = 4096,
+ };
+ struct fuse_attr_out out_payload = {
+ .attr = attr,
+ };
+ iov_out[0].iov_len = sizeof(out_header);
+ iov_out[0].iov_base = &out_header;
+ iov_out[1].iov_len = sizeof(out_payload);
+ iov_out[1].iov_base = &out_payload;
+
+ SetExpected(iov_in, 2, iov_out, 2);
+
+ struct stat stat_buf;
+ EXPECT_THAT(stat(mount_point_.path().c_str(), &stat_buf), SyscallSucceeds());
+
+ struct stat expected_stat = {
+ .st_ino = attr.ino,
+ .st_nlink = attr.nlink,
+ .st_mode = expected_mode,
+ .st_uid = attr.uid,
+ .st_gid = attr.gid,
+ .st_rdev = attr.rdev,
+ .st_size = static_cast<off_t>(attr.size),
+ .st_blksize = attr.blksize,
+ .st_blocks = static_cast<blkcnt_t>(attr.blocks),
+ .st_atim = atime,
+ .st_mtim = mtime,
+ .st_ctim = ctime,
+ };
+ EXPECT_TRUE(StatsAreEqual(stat_buf, expected_stat));
+ WaitCompleted();
+}
+
+TEST_F(StatTest, StatNotFound) {
+ struct iovec iov_in[2];
+ struct iovec iov_out[2];
+
+ struct fuse_in_header in_header = {
+ .len = sizeof(struct fuse_in_header) + sizeof(struct fuse_getattr_in),
+ .opcode = FUSE_GETATTR,
+ .unique = 4,
+ };
+ struct fuse_getattr_in in_payload = {0};
+ iov_in[0].iov_len = sizeof(in_header);
+ iov_in[0].iov_base = &in_header;
+ iov_in[1].iov_len = sizeof(in_payload);
+ iov_in[1].iov_base = &in_payload;
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header),
+ .error = -ENOENT,
+ .unique = 4,
+ };
+ iov_out[0].iov_len = sizeof(out_header);
+ iov_out[0].iov_base = &out_header;
+
+ SetExpected(iov_in, 2, iov_out, 1);
+
+ struct stat stat_buf;
+ EXPECT_THAT(stat(mount_point_.path().c_str(), &stat_buf),
+ SyscallFailsWithErrno(ENOENT));
+ WaitCompleted();
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/runner/defs.bzl b/test/runner/defs.bzl
index ba4732ca6..1ae13a4a5 100644
--- a/test/runner/defs.bzl
+++ b/test/runner/defs.bzl
@@ -150,31 +150,12 @@ def syscall_test(
if not tags:
tags = []
- _syscall_test(
- test = test,
- shard_count = shard_count,
- size = size,
- platform = "native",
- use_tmpfs = False,
- add_uds_tree = add_uds_tree,
- tags = list(tags),
- )
-
- for (platform, platform_tags) in platforms.items():
- _syscall_test(
- test = test,
- shard_count = shard_count,
- size = size,
- platform = platform,
- use_tmpfs = use_tmpfs,
- add_uds_tree = add_uds_tree,
- tags = platform_tags + tags,
- )
-
vfs2_tags = list(tags)
if vfs2:
# Add tag to easily run VFS2 tests with --test_tag_filters=vfs2
vfs2_tags.append("vfs2")
+ if fuse:
+ vfs2_tags.append("fuse")
else:
# Don't automatically run tests tests not yet passing.
@@ -191,19 +172,31 @@ def syscall_test(
add_uds_tree = add_uds_tree,
tags = platforms[default_platform] + vfs2_tags,
vfs2 = True,
+ fuse = fuse,
+ )
+ if fuse:
+ # Only generate *_vfs2_fuse target if fuse parameter is enabled.
+ return
+
+ _syscall_test(
+ test = test,
+ shard_count = shard_count,
+ size = size,
+ platform = "native",
+ use_tmpfs = False,
+ add_uds_tree = add_uds_tree,
+ tags = list(tags),
)
- if vfs2 and fuse:
+ for (platform, platform_tags) in platforms.items():
_syscall_test(
test = test,
shard_count = shard_count,
size = size,
- platform = default_platform,
+ platform = platform,
use_tmpfs = use_tmpfs,
add_uds_tree = add_uds_tree,
- tags = platforms[default_platform] + vfs2_tags + ["fuse"],
- vfs2 = True,
- fuse = True,
+ tags = platform_tags + tags,
)
# TODO(gvisor.dev/issue/1487): Enable VFS2 overlay tests.