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
This commit is contained in:
Craig Chi 2020-08-10 18:15:32 -07:00
parent b404b5c255
commit 51e64d2fc5
8 changed files with 384 additions and 49 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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",
)

View File

@ -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",
],

View File

@ -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.
}

View File

@ -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();

View File

@ -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

View File

@ -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(