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:
parent
b404b5c255
commit
51e64d2fc5
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue