blob: 806905cd2ded194d9e3815a6f1382534347c951b [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <dirent.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/statfs.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <unistd.h>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <thread>
#include <vector>
#include <fbl/unique_fd.h>
#include <gtest/gtest.h>
#include <linux/capability.h>
#include <linux/fuse.h>
#include <linux/magic.h>
#include "src/lib/fxl/strings/string_printf.h"
#include "src/starnix/tests/syscalls/cpp/test_helper.h"
#define OK_OR_RETURN(x) \
{ \
auto result = (x); \
if (!result) { \
return result; \
} \
}
constexpr char kOverlayFsPath[] = "OVERLAYFS_PATH";
class FuseTest : public ::testing::Test {
public:
void SetUp() override {
if (!test_helper::HasSysAdmin()) {
GTEST_SKIP() << "Not running with sysadmin capabilities, skipping suite.";
}
}
void TearDown() override {
if (base_dir_) {
if (umount(GetMountDir().c_str()) != 0) {
FAIL() << "Unable to umount: " << strerror(errno);
}
base_dir_.reset();
}
}
protected:
std::string GetOverlayFsPath() {
if (getenv(kOverlayFsPath)) {
return getenv(kOverlayFsPath);
} else {
return "data/fuse-overlayfs";
}
}
std::string GetMountDir() { return *base_dir_ + "/merge"; }
testing::AssertionResult MkDir(const std::string& directory) {
if (mkdir(directory.c_str(), 0700) != 0) {
return testing::AssertionFailure()
<< "Unable to create '" << directory << "': " << strerror(errno);
}
return testing::AssertionSuccess();
}
testing::AssertionResult Mount() {
if (access(GetOverlayFsPath().c_str(), R_OK | X_OK) != 0) {
return testing::AssertionFailure()
<< "Unable to find fuse binary at: " << GetOverlayFsPath() << "(set OVERLAYFS_PATH)";
}
std::string base_dir = "/tmp/fuse_base_dir_XXXXXX";
if (mkdtemp(const_cast<char*>(base_dir.c_str())) == nullptr) {
return testing::AssertionFailure()
<< "Unable to create temporary directory: " << strerror(errno);
}
std::string lowerdir = base_dir + "/lower";
OK_OR_RETURN(MkDir(lowerdir));
std::string upperdir = base_dir + "/upper";
OK_OR_RETURN(MkDir(upperdir));
std::string workdir = base_dir + "/work";
OK_OR_RETURN(MkDir(workdir));
std::string mergedir = base_dir + "/merge";
OK_OR_RETURN(MkDir(mergedir));
std::string witness_name = "witness";
{
test_helper::ScopedFD witness(
open((lowerdir + "/" + witness_name).c_str(), O_RDWR | O_CREAT, 0600));
if (!witness.is_valid()) {
return testing::AssertionFailure() << "Unable to create witness file: " << strerror(errno);
}
if (write(witness.get(), "hello\n", 6) != 6) {
return testing::AssertionFailure()
<< "Unable to insert data in witness file: " << strerror(errno);
}
}
pid_t child_pid = fork_helper_.RunInForkedProcess([&] {
std::string configuration =
"lowerdir=" + lowerdir + ",upperdir=" + upperdir + ",workdir=" + workdir;
execl(GetOverlayFsPath().c_str(), GetOverlayFsPath().c_str(), "-f", "-o",
configuration.c_str(), mergedir.c_str(), NULL);
});
if (child_pid <= 0) {
return testing::AssertionFailure() << "Unable to fork to start the fuse server process";
}
base_dir_ = base_dir;
std::string witness = mergedir + "/" + witness_name;
for (int i = 0; i < 20 && access(witness.c_str(), R_OK) != 0; ++i) {
usleep(100000);
}
if (access(witness.c_str(), R_OK) != 0) {
return testing::AssertionFailure() << "Unable to see witness file. Mount failed?";
}
return testing::AssertionSuccess();
}
test_helper::ForkHelper fork_helper_;
std::optional<std::string> base_dir_;
};
class FuseServer {
public:
virtual ~FuseServer() {}
const test_helper::ScopedFD& fuse_fd() { return fuse_fd_; }
testing::AssertionResult Mount(const std::string& path) {
OK_OR_RETURN(OpenFuseDevice());
struct stat stat_buffer;
if (stat(path.c_str(), &stat_buffer) == -1) {
return testing::AssertionFailure() << "Failed to stat mount path: " << strerror(errno);
}
std::string options =
fxl::StringPrintf("fd=%i,rootmode=%o,user_id=%u,group_id=%u", fuse_fd_.get(),
stat_buffer.st_mode & S_IFMT, getuid(), getgid());
if (mount("fuse", path.c_str(), "fuse", 0, options.c_str()) == -1) {
return testing::AssertionFailure() << "Failed to mount fuse device: " << strerror(errno);
}
return testing::AssertionSuccess();
}
bool ServeOnce() {
std::vector<std::byte> buffer;
bool unmounted = false;
EXPECT_TRUE(ReadRequest(&buffer, &unmounted));
if (unmounted) {
return false;
}
EXPECT_TRUE(HandleFuseMessage(buffer));
return true;
}
protected:
virtual testing::AssertionResult HandleFuseMessage(const std::vector<std::byte>& message) {
const struct fuse_in_header* in_header =
reinterpret_cast<const struct fuse_in_header*>(message.data());
// The operation-specific payload for the fuse request begins after the header.
const void* in_payload = in_header + 1;
switch (in_header->opcode) {
case FUSE_INIT: {
struct fuse_init_in init_in = {};
memcpy(&init_in, in_payload, sizeof(init_in));
OK_OR_RETURN(HandleInit(in_header, &init_in, message));
break;
}
case FUSE_ACCESS: {
struct fuse_access_in access_in = {};
memcpy(&access_in, in_payload, sizeof(access_in));
OK_OR_RETURN(HandleAccess(in_header, &access_in, message));
break;
}
case FUSE_LOOKUP: {
OK_OR_RETURN(HandleLookup(in_header, message));
break;
}
case FUSE_OPEN: {
struct fuse_open_in open_in = {};
memcpy(&open_in, in_payload, sizeof(open_in));
OK_OR_RETURN(HandleOpen(in_header, &open_in, message));
break;
}
case FUSE_FLUSH: {
struct fuse_flush_in flush_in = {};
memcpy(&flush_in, in_payload, sizeof(flush_in));
OK_OR_RETURN(HandleFlush(in_header, &flush_in, message));
break;
}
case FUSE_RELEASE: {
struct fuse_release_in release_in = {};
memcpy(&release_in, in_payload, sizeof(release_in));
OK_OR_RETURN(HandleRelease(in_header, &release_in, message));
break;
}
default:
return testing::AssertionFailure() << "Unknown FUSE opcode: " << in_header->opcode;
}
return testing::AssertionSuccess();
}
virtual testing::AssertionResult HandleInit(const struct fuse_in_header* in_header,
const struct fuse_init_in* init_in,
const std::vector<std::byte>& message) {
struct fuse_init_out init_out = {};
init_out.major = FUSE_KERNEL_VERSION;
init_out.minor = FUSE_KERNEL_MINOR_VERSION;
return WriteStructResponse(in_header, init_out);
}
virtual testing::AssertionResult HandleAccess(const struct fuse_in_header* in_header,
const struct fuse_access_in* access_in,
const std::vector<std::byte>& message) {
return WriteAckResponse(in_header);
}
virtual testing::AssertionResult HandleLookup(const struct fuse_in_header* in_header,
const std::vector<std::byte>& message) {
struct fuse_entry_out entry_out = {};
entry_out.nodeid = next_nodeid_++;
entry_out.generation = 1;
entry_out.attr.ino = entry_out.nodeid;
entry_out.attr.mode = S_IFREG;
return WriteStructResponse(in_header, entry_out);
}
virtual testing::AssertionResult HandleOpen(const struct fuse_in_header* in_header,
const struct fuse_open_in* open_in,
const std::vector<std::byte>& message) {
struct fuse_open_out open_out = {};
open_out.fh = GetNextFileHandle();
return WriteStructResponse(in_header, open_out);
}
virtual testing::AssertionResult HandleFlush(const struct fuse_in_header* in_header,
const struct fuse_flush_in* flush_in,
const std::vector<std::byte>& message) {
return WriteAckResponse(in_header);
}
virtual testing::AssertionResult HandleRelease(const struct fuse_in_header* in_header,
const struct fuse_release_in* release_in,
const std::vector<std::byte>& message) {
return WriteAckResponse(in_header);
}
testing::AssertionResult WriteDataFreeResponse(const struct fuse_in_header* in_header,
int32_t error) {
fuse_out_header out_header = {
.len = sizeof(fuse_out_header),
.error = error,
.unique = in_header->unique,
};
auto data = reinterpret_cast<std::byte*>(&out_header);
std::vector<std::byte> response(data, data + sizeof(out_header));
return WriteResponse(response);
}
testing::AssertionResult WriteAckResponse(const struct fuse_in_header* in_header) {
return WriteDataFreeResponse(in_header, /* error= */ 0);
}
template <typename Data>
testing::AssertionResult WriteStructResponse(const struct fuse_in_header* in_header, Data data) {
struct fuse_out_header out_header = {};
uint32_t payload_len = sizeof(Data);
uint32_t response_len = payload_len + sizeof(out_header);
out_header.len = response_len;
out_header.unique = in_header->unique;
std::vector<std::byte> response(response_len);
memcpy(response.data(), &out_header, sizeof(out_header));
memcpy(response.data() + sizeof(out_header), &data, sizeof(Data));
return WriteResponse(response);
}
testing::AssertionResult WriteResponse(std::vector<std::byte> response) {
ssize_t actual = HANDLE_EINTR(write(fuse_fd_.get(), response.data(), response.size()));
if (actual != static_cast<ssize_t>(response.size())) {
return testing::AssertionFailure()
<< "Failed to write FUSE response: Got " << actual << " Expected " << response.size()
<< ": " << strerror(errno);
}
return testing::AssertionSuccess();
}
uint64_t GetNextFileHandle() { return next_fh_++; }
private:
testing::AssertionResult ReadRequest(std::vector<std::byte>* request, bool* unmounted) {
// There doesn't seem to be a good value to use for the max request size. We just pick
// something large that works for our cases.
const size_t kMaxRequestSize = 64ul * FUSE_MIN_READ_BUFFER;
request->resize(kMaxRequestSize);
ssize_t actual = HANDLE_EINTR(read(fuse_fd_.get(), request->data(), request->size()));
if (actual == -1) {
if (errno == ENODEV) {
request->clear();
*unmounted = true;
return testing::AssertionSuccess();
;
}
return testing::AssertionFailure() << "Failed to read FUSE request: " << strerror(errno);
}
request->resize(actual);
*unmounted = false;
return testing::AssertionSuccess();
}
testing::AssertionResult OpenFuseDevice() {
fuse_fd_ = test_helper::ScopedFD(open("/dev/fuse", O_RDWR));
if (!fuse_fd_.is_valid()) {
return testing::AssertionFailure() << "Failed to open /dev/fuse: " << strerror(errno);
}
return testing::AssertionSuccess();
}
test_helper::ScopedFD fuse_fd_;
uint64_t next_fh_ = 1;
uint64_t next_nodeid_ = 2; // 1 is reserved for the root.
};
class FuseServerTest : public ::testing::Test {
public:
void SetUp() override {
if (!test_helper::HasSysAdmin()) {
GTEST_SKIP() << "Not running with sysadmin capabilities, skipping suite.";
}
}
void TearDown() override {
if (mount_dir_) {
if (umount(mount_dir_->c_str()) != 0) {
FAIL() << "Unable to umount: " << strerror(errno);
}
mount_dir_.reset();
server_thread_.join();
}
}
protected:
std::string GetMountDir() { return *mount_dir_; }
testing::AssertionResult Mount(std::shared_ptr<FuseServer> server) {
std::string mount_dir = "/tmp/fuse_mount_dir_XXXXXX";
if (mkdtemp(const_cast<char*>(mount_dir.c_str())) == nullptr) {
return testing::AssertionFailure()
<< "Unable to create temporary directory: " << strerror(errno);
}
OK_OR_RETURN(server->Mount(mount_dir));
mount_dir_ = mount_dir;
server_ = std::move(server);
server_thread_ = std::thread([this] {
while (server_->ServeOnce()) {
}
});
return testing::AssertionSuccess();
}
private:
std::optional<std::string> mount_dir_;
std::shared_ptr<FuseServer> server_;
std::thread server_thread_;
};
TEST_F(FuseTest, ReadWriteUnMountedDevFuse) {
fbl::unique_fd fuse_fd(open("/dev/fuse", O_RDWR));
ASSERT_TRUE(fuse_fd.is_valid());
char buffer[1024];
ASSERT_EQ(read(fuse_fd.get(), buffer, 1024), -1);
ASSERT_EQ(errno, EPERM);
ASSERT_EQ(write(fuse_fd.get(), buffer, 1024), -1);
ASSERT_EQ(errno, EPERM);
}
TEST_F(FuseTest, Mount) { ASSERT_TRUE(Mount()); }
TEST_F(FuseTest, Stats) {
ASSERT_TRUE(Mount());
std::string mounted_witness = GetMountDir() + "/witness";
std::string original_witness = *base_dir_ + "/lower/witness";
test_helper::ScopedFD fd(open(mounted_witness.c_str(), O_RDONLY));
ASSERT_TRUE(fd.is_valid());
struct stat mounted_stats;
ASSERT_EQ(fstat(fd.get(), &mounted_stats), 0);
fd = test_helper::ScopedFD(open(original_witness.c_str(), O_RDONLY));
ASSERT_TRUE(fd.is_valid());
struct stat original_stats;
ASSERT_EQ(fstat(fd.get(), &original_stats), 0);
fd.reset();
// Check that the stat of the mounted file are the same as the origin one,
// except for the fs id.
ASSERT_NE(mounted_stats.st_dev, original_stats.st_dev);
// Clobber st_dev and check the rest of the data is the same.
mounted_stats.st_dev = 0;
original_stats.st_dev = 0;
ASSERT_EQ(memcmp(&mounted_stats, &original_stats, sizeof(struct stat)), 0);
}
TEST_F(FuseTest, Read) {
ASSERT_TRUE(Mount());
std::string mounted_witness = GetMountDir() + "/witness";
test_helper::ScopedFD fd(open(mounted_witness.c_str(), O_RDONLY));
ASSERT_TRUE(fd.is_valid());
char buffer[100];
ASSERT_EQ(read(fd.get(), buffer, 100), 6);
ASSERT_EQ(strncmp(buffer, "hello\n", 6), 0);
}
TEST_F(FuseTest, NoFile) {
ASSERT_TRUE(Mount());
std::string mounted_witness = GetMountDir() + "/unexistent";
test_helper::ScopedFD fd(open(mounted_witness.c_str(), O_RDONLY));
ASSERT_FALSE(fd.is_valid());
ASSERT_EQ(errno, ENOENT);
}
TEST_F(FuseTest, Mknod) {
ASSERT_TRUE(Mount());
std::string filename = GetMountDir() + "/file";
test_helper::ScopedFD fd(open(filename.c_str(), O_WRONLY | O_CREAT));
ASSERT_TRUE(fd.is_valid());
}
TEST_F(FuseTest, Write) {
ASSERT_TRUE(Mount());
std::string filename = GetMountDir() + "/file";
test_helper::ScopedFD fd(open(filename.c_str(), O_WRONLY | O_CREAT));
ASSERT_TRUE(fd.is_valid());
EXPECT_EQ(write(fd.get(), "hello\n", 6), 6);
}
TEST_F(FuseTest, Statfs) {
ASSERT_TRUE(Mount());
struct statfs stats;
ASSERT_EQ(statfs((GetMountDir() + "/witness").c_str(), &stats), 0);
ASSERT_EQ(stats.f_type, FUSE_SUPER_MAGIC);
}
TEST_F(FuseTest, Seek) {
ASSERT_TRUE(Mount());
test_helper::ScopedFD fd(open((GetMountDir() + "/witness").c_str(), O_RDONLY));
ASSERT_TRUE(fd.is_valid());
char buffer[100];
ASSERT_EQ(read(fd.get(), buffer, 100), 6);
ASSERT_EQ(strncmp(buffer, "hello\n", 6), 0);
ASSERT_EQ(lseek(fd.get(), 0, SEEK_CUR), 6);
ASSERT_EQ(lseek(fd.get(), -5, SEEK_END), 1);
ASSERT_EQ(read(fd.get(), buffer, 100), 5);
ASSERT_EQ(strncmp(buffer, "ello\n", 5), 0);
ASSERT_EQ(lseek(fd.get(), 1, SEEK_DATA), 1);
ASSERT_EQ(lseek(fd.get(), 7, SEEK_DATA), -1);
ASSERT_EQ(errno, ENXIO);
ASSERT_EQ(lseek(fd.get(), 0, SEEK_HOLE), 6);
ASSERT_EQ(lseek(fd.get(), 7, SEEK_HOLE), -1);
ASSERT_EQ(errno, ENXIO);
}
TEST_F(FuseTest, Poll) {
ASSERT_TRUE(Mount());
test_helper::ScopedFD fd(open((GetMountDir() + "/witness").c_str(), O_RDONLY));
ASSERT_TRUE(fd.is_valid());
struct pollfd poll_struct;
poll_struct.fd = fd.get();
poll_struct.events = -1;
ASSERT_EQ(poll(&poll_struct, 1, 0), 1);
ASSERT_EQ(poll_struct.revents, POLLIN | POLLOUT | POLLRDNORM | POLLWRNORM);
}
TEST_F(FuseTest, Mkdir) {
ASSERT_TRUE(Mount());
std::string dirname = GetMountDir() + "/dir";
ASSERT_EQ(open(dirname.c_str(), O_RDONLY), -1);
ASSERT_EQ(mkdir(dirname.c_str(), 0777), 0);
test_helper::ScopedFD fd(open(dirname.c_str(), O_RDONLY));
ASSERT_TRUE(fd.is_valid());
struct stat stats;
ASSERT_EQ(fstat(fd.get(), &stats), 0);
ASSERT_TRUE(S_ISDIR(stats.st_mode));
}
TEST_F(FuseTest, Symlink) {
ASSERT_TRUE(Mount());
std::string witness = GetMountDir() + "/witness";
std::string link = GetMountDir() + "/symlink";
ASSERT_EQ(symlink(witness.c_str(), link.c_str()), 0);
struct stat stats;
ASSERT_EQ(lstat(link.c_str(), &stats), 0);
ASSERT_TRUE(S_ISLNK(stats.st_mode));
std::vector<char> buffer;
buffer.resize(100);
ASSERT_EQ(readlink(link.c_str(), &buffer[0], buffer.size()),
static_cast<ssize_t>(witness.size()));
buffer.resize(witness.size());
ASSERT_EQ(memcmp(&buffer[0], &witness[0], witness.size()), 0);
}
TEST_F(FuseTest, Link) {
ASSERT_TRUE(Mount());
std::string witness = GetMountDir() + "/witness";
std::string linkname = GetMountDir() + "/link";
ASSERT_EQ(link(witness.c_str(), linkname.c_str()), 0);
struct stat stats;
ASSERT_EQ(lstat(linkname.c_str(), &stats), 0);
ASSERT_TRUE(S_ISREG(stats.st_mode));
ino_t ino = stats.st_ino;
ASSERT_EQ(lstat(witness.c_str(), &stats), 0);
ASSERT_EQ(ino, stats.st_ino);
}
TEST_F(FuseTest, Unlink) {
ASSERT_TRUE(Mount());
std::string witness = GetMountDir() + "/witness";
ASSERT_TRUE(test_helper::ScopedFD(open(witness.c_str(), O_RDONLY)).is_valid());
ASSERT_EQ(unlink(witness.c_str()), 0);
ASSERT_FALSE(test_helper::ScopedFD(open(witness.c_str(), O_RDONLY)).is_valid());
}
TEST_F(FuseTest, Truncate) {
ASSERT_TRUE(Mount());
std::string file = GetMountDir() + "/file";
test_helper::ScopedFD fd(open(file.c_str(), O_WRONLY | O_CREAT));
ASSERT_TRUE(fd.is_valid());
ASSERT_EQ(write(fd.get(), "hello", 5), 5);
fd.reset();
ASSERT_EQ(truncate(file.c_str(), 2), 0);
fd = test_helper::ScopedFD(open(file.c_str(), O_RDONLY));
char buffer[10];
ASSERT_EQ(read(fd.get(), buffer, 10), 2);
}
TEST_F(FuseTest, Readdir) {
// Create enough file to ensure more than one call to the fuse operation is
// needed to read the full content of a directory. Experimentally, the libc
// creates a buffer of 32k bytes when the user calls readdir.
const size_t kFileCount = (32768 / (sizeof(struct fuse_dirent) + 6)) + 1;
ASSERT_TRUE(Mount());
std::string root = GetMountDir();
for (size_t i = 0; i < kFileCount; ++i) {
std::string value = std::to_string(i / 2);
if (i % 2 == 0) {
ASSERT_EQ(mkdir((root + "/dir_" + value).c_str(), 0777), 0);
} else {
test_helper::ScopedFD fd(open((root + "/file" + value).c_str(), O_WRONLY | O_CREAT));
ASSERT_TRUE(fd.is_valid());
}
}
std::map<std::string, struct dirent> files;
DIR* dir = opendir(root.c_str());
ASSERT_TRUE(dir);
while (struct dirent* entry = readdir(dir)) {
std::string name = entry->d_name;
files[name] = *entry;
}
closedir(dir);
ASSERT_EQ(files.size(), 2u + kFileCount);
ASSERT_NE(files.find("witness"), files.end());
ASSERT_NE(files.find("."), files.end());
// fuse-overlayfs doesn't contain .. on root
ASSERT_EQ(files.find(".."), files.end());
files.clear();
std::string dir1 = GetMountDir() + "/dir_0";
dir = opendir(dir1.c_str());
ASSERT_TRUE(dir);
while (struct dirent* entry = readdir(dir)) {
std::string name = entry->d_name;
files[name] = *entry;
}
closedir(dir);
ASSERT_EQ(files.size(), 2u);
ASSERT_NE(files.find("."), files.end());
ASSERT_NE(files.find(".."), files.end());
}
TEST_F(FuseTest, Getdents) {
ASSERT_TRUE(Mount());
std::string root = GetMountDir();
test_helper::ScopedFD fd(open(root.c_str(), O_RDONLY));
ASSERT_TRUE(fd.is_valid());
char buffer[4096];
ASSERT_GT(syscall(SYS_getdents64, fd.get(), buffer, 4096), 0);
}
TEST_F(FuseTest, XAttr) {
const char attribute_name[] = "user.comment\0";
ASSERT_TRUE(Mount());
std::string filename = GetMountDir() + "/file";
test_helper::ScopedFD fd(open(filename.c_str(), O_RDWR | O_CREAT));
ASSERT_TRUE(fd.is_valid());
ASSERT_EQ(fgetxattr(fd.get(), attribute_name, nullptr, 0), -1);
ASSERT_EQ(errno, ENODATA);
ASSERT_EQ(fsetxattr(fd.get(), attribute_name, "hello", 5, XATTR_CREATE), 0);
ASSERT_EQ(fgetxattr(fd.get(), attribute_name, nullptr, 1), -1);
ASSERT_EQ(errno, ERANGE);
ASSERT_EQ(fgetxattr(fd.get(), attribute_name, nullptr, 0), 5);
char buffer[5];
ASSERT_EQ(fgetxattr(fd.get(), attribute_name, buffer, 5), 5);
ASSERT_EQ(memcmp(buffer, "hello", 5), 0);
ssize_t list_size = flistxattr(fd.get(), nullptr, 0);
ASSERT_GE(list_size, 0);
char list[list_size];
ASSERT_EQ(flistxattr(fd.get(), list, list_size), list_size);
ASSERT_EQ(list[list_size - 1], '\0');
std::set<std::string> attributes;
ssize_t index = 0;
while (index < list_size) {
std::string content = std::string(&list[index]);
attributes.insert(content);
index += content.size() + 1;
}
ASSERT_NE(attributes.find(attribute_name), attributes.end());
ASSERT_EQ(fremovexattr(fd.get(), attribute_name), 0);
ASSERT_EQ(fgetxattr(fd.get(), attribute_name, nullptr, 0), -1);
ASSERT_EQ(errno, ENODATA);
}
TEST_F(FuseServerTest, OpenAndClose) {
ASSERT_TRUE(Mount(std::make_shared<FuseServer>()));
std::string filename = GetMountDir() + "/file";
test_helper::ScopedFD fd(open(filename.c_str(), O_RDWR | O_CREAT));
ASSERT_TRUE(fd.is_valid());
fd.reset();
}
TEST_F(FuseServerTest, HeaderLengthUnderflow) {
class HeaderLengthUnderflowServer : public FuseServer {
testing::AssertionResult HandleAccess(const struct fuse_in_header* in_header,
const struct fuse_access_in* access_in,
const std::vector<std::byte>& message) override {
uint32_t response_len = sizeof(struct fuse_out_header);
std::vector<std::byte> response;
response.resize(response_len);
struct fuse_out_header* out_header =
reinterpret_cast<struct fuse_out_header*>(response.data());
out_header->len = 0;
out_header->unique = in_header->unique;
return WriteResponse(response);
}
};
ASSERT_TRUE(Mount(std::make_shared<HeaderLengthUnderflowServer>()));
std::string filename = GetMountDir() + "/file";
test_helper::ScopedFD fd(open(filename.c_str(), O_RDWR | O_CREAT));
ASSERT_TRUE(fd.is_valid());
fd.reset();
}
TEST_F(FuseServerTest, OverlongHeaderLength) {
class OverlongHeaderLengthServer : public FuseServer {
testing::AssertionResult HandleOpen(const struct fuse_in_header* in_header,
const struct fuse_open_in* open_in,
const std::vector<std::byte>& message) override {
const uint32_t kBogusHeaderLengthAddition = 1024;
struct fuse_open_out open_out = {};
open_out.fh = GetNextFileHandle();
struct fuse_out_header out_header = {};
uint32_t payload_len = sizeof(open_out);
uint32_t response_len = payload_len + sizeof(out_header);
out_header.len = response_len + kBogusHeaderLengthAddition;
out_header.unique = in_header->unique;
std::vector<std::byte> response(response_len);
memcpy(response.data(), &out_header, sizeof(out_header));
memcpy(response.data() + sizeof(out_header), &open_out, sizeof(open_out));
return WriteResponse(response);
}
};
ASSERT_TRUE(Mount(std::make_shared<OverlongHeaderLengthServer>()));
std::string filename = GetMountDir() + "/file";
test_helper::ScopedFD fd(open(filename.c_str(), O_RDWR | O_CREAT));
ASSERT_TRUE(fd.is_valid());
fd.reset();
}
TEST_F(FuseServerTest, BypassUnimplementedAccess) {
class BypassUnimplementedAccessServer : public FuseServer {
public:
uint64_t AccessCount() { return calls_to_access_.load(std::memory_order_relaxed); }
protected:
testing::AssertionResult HandleAccess(const struct fuse_in_header* in_header,
const struct fuse_access_in* access_in,
const std::vector<std::byte>& message) override {
calls_to_access_.fetch_add(1, std::memory_order_relaxed);
return WriteDataFreeResponse(in_header, -ENOSYS);
}
private:
std::atomic_uint64_t calls_to_access_;
};
std::shared_ptr<BypassUnimplementedAccessServer> server(new BypassUnimplementedAccessServer());
ASSERT_TRUE(Mount(server));
EXPECT_EQ(server->AccessCount(), 0u);
// Run the checks in a separate thread where we drop the |CAP_DAC_OVERRIDE|
// capability which bypasses file permission checks. See capabilities(7).
std::thread thrd([&]() {
test_helper::UnsetCapability(CAP_DAC_OVERRIDE);
auto check_access = [&](const char* file) {
const std::string filename = GetMountDir() + "/" + file;
ASSERT_EQ(access(filename.c_str(), R_OK), 0) << strerror(errno);
EXPECT_EQ(server->AccessCount(), 1u);
};
// No matter how many times we access a file, we should have only ever made
// the |FUSE_ACCESS| request once for the lifetime of the connection/server.
for (int i = 0; i < 3; ++i) {
ASSERT_NO_FATAL_FAILURE(check_access("somefile1"));
}
// Accessing another file shouldn't change the access count since it is still
// part of the same connection.
ASSERT_NO_FATAL_FAILURE(check_access("somefile2"));
});
thrd.join();
}