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 #include -#include #include #include #include +#include #include #include #include -#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 -#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 +#include +#include +#include +#include +#include +#include + +#include + +#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(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(atime.tv_sec), + .mtime = static_cast(mtime.tv_sec), + .ctime = static_cast(ctime.tv_sec), + .atimensec = static_cast(atime.tv_nsec), + .mtimensec = static_cast(mtime.tv_nsec), + .ctimensec = static_cast(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(attr.size), + .st_blksize = attr.blksize, + .st_blocks = static_cast(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,6 +150,34 @@ def syscall_test( if not 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. + vfs2_tags.append("manual") + vfs2_tags.append("noguitar") + vfs2_tags.append("notap") + + _syscall_test( + test = test, + shard_count = shard_count, + size = size, + platform = default_platform, + use_tmpfs = use_tmpfs, + 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, @@ -171,41 +199,6 @@ def syscall_test( 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") - - else: - # Don't automatically run tests tests not yet passing. - vfs2_tags.append("manual") - vfs2_tags.append("noguitar") - vfs2_tags.append("notap") - - _syscall_test( - test = test, - shard_count = shard_count, - size = size, - platform = default_platform, - use_tmpfs = use_tmpfs, - add_uds_tree = add_uds_tree, - tags = platforms[default_platform] + vfs2_tags, - vfs2 = True, - ) - - if vfs2 and fuse: - _syscall_test( - test = test, - shard_count = shard_count, - size = size, - platform = default_platform, - use_tmpfs = use_tmpfs, - add_uds_tree = add_uds_tree, - tags = platforms[default_platform] + vfs2_tags + ["fuse"], - vfs2 = True, - fuse = True, - ) - # TODO(gvisor.dev/issue/1487): Enable VFS2 overlay tests. if add_overlay: _syscall_test(