blob: 8c3412d35b18fc92e3ddd03d11b08df395373bcc [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 <lib/fit/defer.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 <zircon/compiler.h>
#include <algorithm>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <mutex>
#include <optional>
#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/capabilities_helper.h"
#include "src/starnix/tests/syscalls/cpp/test_helper.h"
#ifndef FUSE_SUPER_MAGIC
#define FUSE_SUPER_MAGIC 0x65735546
#endif // FUSE_SUPER_MAGIC
#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 {
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (!test_helper::HasSysAdmin()) {
GTEST_SKIP() << "Not running with sysadmin capabilities, skipping suite.";
}
}
void TearDown() override {
if (base_dir_) {
if (umount2(GetMountDir().c_str(), MNT_DETACH) != 0) {
FAIL() << "Unable to umount: " << strerror(errno);
}
base_dir_.reset();
}
}
protected:
std::string GetOverlayFsPath() {
if (getenv(kOverlayFsPath)) {
return getenv(kOverlayFsPath);
} else {
return "data/tests/deps/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";
{
fbl::unique_fd 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(), nullptr);
});
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 Node {
public:
virtual ~Node() {}
void SetPermissions(uint32_t perms) {
std::lock_guard guard(mtx_);
perms_ = perms;
}
void SetEntryValidDuration(uint64_t secs) {
std::lock_guard guard(mtx_);
entry_valid_secs_ = secs;
}
uint64_t UpdateNodeid(uint64_t id) {
std::lock_guard guard(mtx_);
std::swap(id, id_);
return id;
}
void IncrementGeneration() {
std::lock_guard guard(mtx_);
generation_++;
}
void PopulateAttrLocked(fuse_attr& attr) __TA_REQUIRES(mtx_) {
attr = {
.ino = id_,
.mode = type_ | perms_,
};
}
void PopulateAttr(fuse_attr& attr) {
std::lock_guard guard(mtx_);
PopulateAttrLocked(attr);
}
void PopulateEntry(fuse_entry_out& entry_out) {
std::lock_guard guard(mtx_);
entry_out = {
.nodeid = id_,
.generation = generation_,
.entry_valid = entry_valid_secs_,
};
PopulateAttrLocked(entry_out.attr);
}
protected:
Node(uint32_t type, uint64_t id) : type_(type), id_(id) {}
private:
uint32_t type_;
std::mutex mtx_;
uint64_t id_ __TA_GUARDED(mtx_);
uint64_t generation_ __TA_GUARDED(mtx_) = 1;
uint64_t entry_valid_secs_ __TA_GUARDED(mtx_) = 0;
uint32_t perms_ __TA_GUARDED(mtx_) = S_IRWXU | S_IRWXG | S_IRWXO;
};
class Directory : public Node {
public:
Directory(uint64_t id) : Node(S_IFDIR, id) {}
void AddChild(const std::string& name, std::shared_ptr<Node> node) { children_[name] = node; }
std::shared_ptr<Node> RemoveChild(const std::string& name) {
auto iter = children_.find(name);
if (iter == children_.end()) {
return nullptr;
} else {
std::shared_ptr<Node> node = iter->second;
children_.erase(iter);
return node;
}
}
std::shared_ptr<Node> Lookup(const std::string& name) {
auto it = children_.find(name);
if (it == children_.end()) {
return nullptr;
}
return it->second;
}
private:
std::unordered_map<std::string, std::shared_ptr<Node>> children_;
};
class File : public Node {
public:
File(uint64_t id) : Node(S_IFREG, id) {}
};
class FileSystem {
public:
FileSystem() {
root_ = std::shared_ptr<Directory>(new Directory(FUSE_ROOT_ID));
std::lock_guard guard(mtx_);
nodes_[FUSE_ROOT_ID] = root_;
}
void UpdateNodeId(const std::shared_ptr<Node>& node) {
std::lock_guard guard(mtx_);
uint64_t new_nodeid = AllocateNodeIdLocked();
nodes_[new_nodeid] = node;
uint64_t old_nodeid = node->UpdateNodeid(new_nodeid);
EXPECT_EQ(nodes_.erase(old_nodeid), 1u);
}
const std::shared_ptr<Directory>& RootDir() const { return root_; }
template <class T, typename F>
std::shared_ptr<T> AddNodeAt(const std::shared_ptr<Directory>& at, const std::string& name,
F builder) {
std::lock_guard guard(mtx_);
uint64_t nodeid = AllocateNodeIdLocked();
std::shared_ptr<T> node = builder(nodeid);
nodes_[nodeid] = node;
at->AddChild(name, node);
return node;
}
std::shared_ptr<Directory> AddDirAt(const std::shared_ptr<Directory>& at,
const std::string& name) {
return AddNodeAt<Directory>(
at, name, [](uint64_t nodeid) { return std::make_shared<Directory>(nodeid); });
}
std::shared_ptr<Directory> AddDirAtRoot(const std::string& name) {
return AddDirAt(RootDir(), name);
}
std::shared_ptr<File> AddFileAt(const std::shared_ptr<Directory>& at, const std::string& name) {
return AddNodeAt<File>(at, name,
[](uint64_t nodeid) { return std::make_shared<File>(nodeid); });
}
std::shared_ptr<File> AddFileAtRoot(const std::string& name) {
return AddFileAt(RootDir(), name);
}
std::shared_ptr<Node> Lookup(uint64_t nodeid) {
std::lock_guard guard(mtx_);
auto it = nodes_.find(nodeid);
if (it == nodes_.end()) {
return nullptr;
}
return it->second;
}
bool Rename(const std::shared_ptr<Directory>& source_dir, const std::string& name,
const std::shared_ptr<Directory>& target_dir, const std::string& target_name) {
auto maybe_node = source_dir->RemoveChild(name);
if (!maybe_node) {
return false;
}
target_dir->AddChild(target_name, maybe_node);
return true;
}
private:
uint64_t AllocateNodeIdLocked() __TA_REQUIRES(mtx_) { return next_nodeid_++; }
std::shared_ptr<Directory> root_;
std::mutex mtx_;
uint64_t next_nodeid_ __TA_GUARDED(mtx_) = FUSE_ROOT_ID + 1;
std::unordered_map<uint64_t, std::shared_ptr<Node>> nodes_ __TA_GUARDED(mtx_);
};
class FuseServer {
public:
FuseServer() : FuseServer(0) {}
FuseServer(uint32_t want_init_flags) : want_init_flags_(want_init_flags) {}
virtual ~FuseServer() {}
FileSystem& fs() { return fs_; }
const fbl::unique_fd& 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;
}
template <typename R = void>
R WaitForInit(std::function<R()> f = std::function<R()>()) {
std::unique_lock guard(init_mtx_);
init_cv_.wait(guard, [&]() { return init_done_; });
if (f) {
return f();
}
return R();
}
testing::AssertionResult SendInitResponse(const fuse_in_header& in_header, uint32_t flags) {
fuse_init_out init_out = {
.major = FUSE_KERNEL_VERSION,
.minor = FUSE_KERNEL_MINOR_VERSION,
.flags = flags,
};
return WriteStructResponse(in_header, init_out);
}
protected:
testing::AssertionResult HandleFuseMessage(const std::vector<std::byte>& message) {
if (message.size() < sizeof(fuse_in_header)) {
return testing::AssertionFailure() << "Message size too small; got " << message.size()
<< ", want at least " << sizeof(fuse_in_header);
}
// Copy out of |message| to make sure we access an aligned |fuse_in_header|.
struct fuse_in_header in_header;
memcpy(&in_header, message.data(), sizeof(in_header));
// The operation-specific payload for the fuse request begins after the header.
const void* in_payload = message.data() + sizeof(in_header);
std::shared_ptr<Node> node;
if (in_header.opcode != FUSE_INIT) {
node = fs_.Lookup(in_header.nodeid);
if (!node) {
return WriteDataFreeResponse(in_header, -ENOENT);
}
}
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));
break;
}
case FUSE_ACCESS: {
struct fuse_access_in access_in = {};
memcpy(&access_in, in_payload, sizeof(access_in));
OK_OR_RETURN(HandleAccess(node, in_header, &access_in));
break;
}
case FUSE_CREATE: {
struct fuse_create_in create_in = {};
memcpy(&create_in, in_payload, sizeof(create_in));
OK_OR_RETURN(HandleCreate(node, in_header, &create_in,
reinterpret_cast<const char*>(in_payload) + sizeof(create_in)));
break;
}
case FUSE_GETATTR: {
struct fuse_getattr_in getattr_in = {};
memcpy(&getattr_in, in_payload, sizeof(getattr_in));
OK_OR_RETURN(HandleGetAttr(node, in_header, &getattr_in));
break;
}
case FUSE_LOOKUP: {
OK_OR_RETURN(HandleLookup(node, in_header, reinterpret_cast<const char*>(in_payload)));
break;
}
case FUSE_MKNOD: {
struct fuse_mknod_in mknod_in = {};
memcpy(&mknod_in, in_payload, sizeof(mknod_in));
OK_OR_RETURN(HandleMknod(node, in_header, &mknod_in,
reinterpret_cast<const char*>(in_payload) + sizeof(mknod_in)));
break;
}
case FUSE_MKDIR: {
struct fuse_mkdir_in mkdir_in = {};
memcpy(&mkdir_in, in_payload, sizeof(mkdir_in));
OK_OR_RETURN(HandleMkdir(node, in_header, &mkdir_in,
reinterpret_cast<const char*>(in_payload) + sizeof(mkdir_in)));
break;
}
case FUSE_OPENDIR:
case FUSE_OPEN: {
struct fuse_open_in open_in = {};
memcpy(&open_in, in_payload, sizeof(open_in));
OK_OR_RETURN(HandleOpen(node, in_header, &open_in));
break;
}
case FUSE_FLUSH: {
struct fuse_flush_in flush_in = {};
memcpy(&flush_in, in_payload, sizeof(flush_in));
OK_OR_RETURN(HandleFlush(node, in_header, &flush_in));
break;
}
case FUSE_READ:
case FUSE_WRITE:
OK_OR_RETURN(WriteDataFreeResponse(in_header, -ENOTSUP));
break;
case FUSE_RELEASEDIR:
case FUSE_RELEASE: {
struct fuse_release_in release_in = {};
memcpy(&release_in, in_payload, sizeof(release_in));
OK_OR_RETURN(HandleRelease(node, in_header, &release_in));
break;
}
case FUSE_GETXATTR: {
OK_OR_RETURN(WriteDataFreeResponse(in_header, -ENOSYS));
break;
}
case FUSE_BATCH_FORGET:
case FUSE_FORGET:
// no-op; these don't expect a response.
break;
case FUSE_RENAME2: {
struct fuse_rename2_in rename_in = {};
memcpy(&rename_in, in_payload, sizeof(rename_in));
const char* name = reinterpret_cast<const char*>(in_payload) + sizeof(rename_in);
const char* target_name = name + strlen(name) + 1;
OK_OR_RETURN(HandleRename(node, in_header, &rename_in, name, target_name));
break;
}
default:
return testing::AssertionFailure() << "Unknown FUSE opcode: " << in_header.opcode;
}
return testing::AssertionSuccess();
}
void NotifyInitWaiters(std::function<void()> f = std::function<void()>()) {
std::unique_lock guard(init_mtx_);
init_done_ = true;
if (f) {
f();
}
init_cv_.notify_all();
}
virtual testing::AssertionResult HandleInit(const struct fuse_in_header& in_header,
const struct fuse_init_in* init_in) {
EXPECT_EQ(init_in->flags & want_init_flags_, want_init_flags_);
OK_OR_RETURN(SendInitResponse(in_header, want_init_flags_));
NotifyInitWaiters();
return testing::AssertionSuccess();
}
virtual testing::AssertionResult HandleAccess(const std::shared_ptr<Node>& node,
const struct fuse_in_header& in_header,
const struct fuse_access_in* access_in) {
return WriteAckResponse(in_header);
}
virtual testing::AssertionResult HandleGetAttr(const std::shared_ptr<Node>& node,
const struct fuse_in_header& in_header,
const struct fuse_getattr_in* getattr_in) {
fuse_attr_out attr_out = {};
node->PopulateAttr(attr_out.attr);
return WriteStructResponse(in_header, attr_out);
}
virtual testing::AssertionResult HandleLookup(const std::shared_ptr<Node>& node,
const struct fuse_in_header& in_header,
const char* name) {
fuse_entry_out entry_out;
if (HandleLookupInner(node, in_header, name, entry_out)) {
return WriteStructResponse(in_header, entry_out);
}
return testing::AssertionSuccess();
}
virtual testing::AssertionResult HandleMknod(const std::shared_ptr<Node>& dir_node,
const struct fuse_in_header& in_header,
const struct fuse_mknod_in* mknod_in,
const char* name) {
const std::shared_ptr dir = std::dynamic_pointer_cast<Directory>(dir_node);
if (!dir) {
return WriteDataFreeResponse(in_header, -ENOTDIR);
}
std::shared_ptr<File> node = fs_.AddFileAt(dir, std::string(name));
fuse_entry_out entry_out;
node->PopulateEntry(entry_out);
return WriteStructResponse(in_header, entry_out);
}
virtual testing::AssertionResult HandleCreate(const std::shared_ptr<Node>& dir_node,
const struct fuse_in_header& in_header,
const struct fuse_create_in* create_in,
const char* name) {
const std::shared_ptr dir = std::dynamic_pointer_cast<Directory>(dir_node);
if (!dir) {
return WriteDataFreeResponse(in_header, -ENOTDIR);
}
std::shared_ptr<File> node = fs_.AddFileAt(dir, std::string(name));
struct response {
fuse_entry_out entry_out;
fuse_open_out open_out;
} response = {};
node->PopulateEntry(response.entry_out);
response.open_out.fh = GetNextFileHandle();
return WriteStructResponse(in_header, response);
}
virtual testing::AssertionResult HandleMkdir(const std::shared_ptr<Node>& dir_node,
const struct fuse_in_header& in_header,
const struct fuse_mkdir_in* mkdir_in,
const char* name) {
const std::shared_ptr dir = std::dynamic_pointer_cast<Directory>(dir_node);
if (!dir) {
return WriteDataFreeResponse(in_header, -ENOTDIR);
}
std::shared_ptr<Directory> node = fs_.AddDirAt(dir, std::string(name));
fuse_entry_out entry_out;
node->PopulateEntry(entry_out);
return WriteStructResponse(in_header, entry_out);
}
virtual testing::AssertionResult HandleOpen(const std::shared_ptr<Node>& node,
const struct fuse_in_header& in_header,
const struct fuse_open_in* open_in) {
struct fuse_open_out open_out = {};
open_out.fh = GetNextFileHandle();
return WriteStructResponse(in_header, open_out);
}
virtual testing::AssertionResult HandleFlush(const std::shared_ptr<Node>& node,
const struct fuse_in_header& in_header,
const struct fuse_flush_in* flush_in) {
return WriteAckResponse(in_header);
}
virtual testing::AssertionResult HandleRelease(const std::shared_ptr<Node>& node,
const struct fuse_in_header& in_header,
const struct fuse_release_in* release_in) {
return WriteAckResponse(in_header);
}
virtual testing::AssertionResult HandleRename(const std::shared_ptr<Node>& source_dir_node,
const struct fuse_in_header& in_header,
const struct fuse_rename2_in* rename_in,
const char* name, const char* target_name) {
const std::shared_ptr source_dir = std::dynamic_pointer_cast<Directory>(source_dir_node);
if (!source_dir)
return WriteDataFreeResponse(in_header, -ENOTDIR);
auto node = fs_.Lookup(rename_in->newdir);
if (!node)
return WriteDataFreeResponse(in_header, -EINVAL);
std::shared_ptr<Directory> target_dir = std::dynamic_pointer_cast<Directory>(node);
if (!target_dir)
return WriteDataFreeResponse(in_header, -ENOTDIR);
if (!fs_.Rename(source_dir, name, target_dir, target_name))
return WriteDataFreeResponse(in_header, -ENOENT);
return WriteAckResponse(in_header);
}
virtual testing::AssertionResult HandleRmdir(const std::shared_ptr<Node>& dir_node,
const struct fuse_in_header& in_header,
const char* name) {
const std::shared_ptr dir = std::dynamic_pointer_cast<Directory>(dir_node);
if (!dir)
return WriteDataFreeResponse(in_header, -ENOTDIR);
if (dir->RemoveChild(name)) {
return WriteAckResponse(in_header);
} else {
return WriteDataFreeResponse(in_header, -ENOENT);
}
}
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_++; }
protected:
bool HandleLookupInner(const std::shared_ptr<Node>& dir_node,
const struct fuse_in_header& in_header, const char* name,
fuse_entry_out& entry_out) {
const std::shared_ptr dir = std::dynamic_pointer_cast<Directory>(dir_node);
if (!dir) {
EXPECT_TRUE(WriteDataFreeResponse(in_header, -ENOENT));
return false;
}
std::shared_ptr<Node> node = dir->Lookup(name);
if (!node) {
EXPECT_TRUE(WriteDataFreeResponse(in_header, -ENOENT));
return false;
}
node->PopulateEntry(entry_out);
return true;
}
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_ = fbl::unique_fd(open("/dev/fuse", O_RDWR));
if (!fuse_fd_.is_valid()) {
return testing::AssertionFailure() << "Failed to open /dev/fuse: " << strerror(errno);
}
return testing::AssertionSuccess();
}
fbl::unique_fd fuse_fd_;
uint64_t next_fh_ = 1;
std::mutex init_mtx_;
std::condition_variable init_cv_;
bool init_done_ = false;
uint32_t want_init_flags_;
FileSystem fs_;
};
class FuseServerTest : public ::testing::Test {
public:
void SetUp() override {
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (!test_helper::HasSysAdmin()) {
GTEST_SKIP() << "Not running with sysadmin capabilities, skipping suite.";
}
}
void TearDown() override {
if (mount_dir_) {
if (umount2(mount_dir_->c_str(), MNT_DETACH) != 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";
fbl::unique_fd 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 = fbl::unique_fd(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";
fbl::unique_fd 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";
fbl::unique_fd 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";
fbl::unique_fd 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";
fbl::unique_fd 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, HugeWrite) {
constexpr ssize_t kSize = 1024 * 1024;
ASSERT_TRUE(Mount());
std::string filename = GetMountDir() + "/file";
fbl::unique_fd fd(open(filename.c_str(), O_WRONLY | O_CREAT));
ASSERT_TRUE(fd.is_valid());
auto data = std::make_unique<char[]>(kSize);
memset(data.get(), 0, kSize);
ssize_t write_result = write(fd.get(), data.get(), kSize);
ASSERT_GT(write_result, 0);
ASSERT_LE(write_result, kSize);
}
TEST_F(FuseTest, WriteAppend) {
ASSERT_TRUE(Mount());
std::string filename = GetMountDir() + "/file";
fbl::unique_fd fd(open(filename.c_str(), O_WRONLY | O_CREAT | O_APPEND));
ASSERT_TRUE(fd.is_valid());
EXPECT_EQ(write(fd.get(), "hello\n", 6), 6);
fd.reset(open(filename.c_str(), O_WRONLY | O_APPEND));
ASSERT_TRUE(fd.is_valid());
EXPECT_EQ(write(fd.get(), "hello\n", 6), 6);
EXPECT_EQ(write(fd.get(), "hello\n", 6), 6);
fd.reset(open(filename.c_str(), O_RDONLY));
char buffer[1024];
EXPECT_EQ(read(fd.get(), buffer, 1024), 18);
EXPECT_EQ(strncmp(buffer, "hello\nhello\nhello\n", 18), 0);
}
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());
fbl::unique_fd 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());
fbl::unique_fd 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);
fbl::unique_fd 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(fbl::unique_fd(open(witness.c_str(), O_RDONLY)).is_valid());
ASSERT_EQ(unlink(witness.c_str()), 0) << strerror(errno);
ASSERT_FALSE(fbl::unique_fd(open(witness.c_str(), O_RDONLY)).is_valid());
}
TEST_F(FuseTest, Truncate) {
ASSERT_TRUE(Mount());
std::string file = GetMountDir() + "/file";
fbl::unique_fd 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 = fbl::unique_fd(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 {
fbl::unique_fd 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();
fbl::unique_fd 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";
fbl::unique_fd 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(FuseTest, Rename) {
ASSERT_TRUE(Mount());
std::string file = GetMountDir() + "/file";
fbl::unique_fd fd(open(file.c_str(), O_WRONLY | O_CREAT));
ASSERT_TRUE(fd.is_valid());
std::string dir = GetMountDir() + "/dir";
ASSERT_EQ(mkdir(dir.c_str(), 0777), 0);
std::string new_path = dir + "/new_name";
EXPECT_EQ(rename(file.c_str(), new_path.c_str()), 0);
struct stat stat_buf;
EXPECT_EQ(stat(file.c_str(), &stat_buf), -1);
EXPECT_EQ(errno, ENOENT);
EXPECT_EQ(stat(new_path.c_str(), &stat_buf), 0);
}
TEST_F(FuseTest, Rmdir) {
// Validate removal of empty dir
ASSERT_TRUE(Mount());
std::string dir = GetMountDir() + "/dir";
ASSERT_EQ(mkdir(dir.c_str(), 0777), 0);
EXPECT_EQ(rmdir(dir.c_str()), 0) << strerror(errno);
// Validate failure of non-empty dir
ASSERT_EQ(mkdir(dir.c_str(), 0777), 0);
std::string file_path = dir + "/new_name";
fbl::unique_fd fd(open(file_path.c_str(), O_WRONLY | O_CREAT));
ASSERT_TRUE(fd.is_valid());
ASSERT_NE(rmdir(dir.c_str()), 0);
}
TEST_F(FuseServerTest, NoReqsUntilInitResponse) {
class NoReqsUntilInitResponseServer : public FuseServer {
public:
fuse_in_header WaitForInitAndReturnRequestHeader() {
return WaitForInit(std::function<fuse_in_header()>([&]() { return init_hdr_; }));
}
protected:
testing::AssertionResult HandleInit(const struct fuse_in_header& in_header,
const struct fuse_init_in* init_in) {
// Don't actually complete the request, just store the init request's header so
// that we can respond to it later.
NotifyInitWaiters([&]() { init_hdr_ = in_header; });
return testing::AssertionSuccess();
}
private:
fuse_in_header init_hdr_;
};
std::shared_ptr<NoReqsUntilInitResponseServer> server(new NoReqsUntilInitResponseServer());
ASSERT_TRUE(server->fs().AddFileAtRoot("file"));
ASSERT_TRUE(Mount(server));
const fuse_in_header init_hdr = server->WaitForInitAndReturnRequestHeader();
// Create a new thread to perform a request against the FUSE server.
std::atomic_bool access_done = false;
std::thread thrd([&]() {
std::string filename = GetMountDir() + "/file";
EXPECT_EQ(access(filename.c_str(), R_OK), 0) << strerror(errno);
access_done = true;
});
// Make sure that the request is not completed.
sleep(1);
EXPECT_FALSE(access_done);
// Send our (delayed) response to the FUSE_INIT request and make sure that the
// access request is now completed.
ASSERT_TRUE(server->SendInitResponse(init_hdr, 0));
thrd.join();
EXPECT_TRUE(access_done);
}
TEST_F(FuseServerTest, OpenAndClose) {
std::shared_ptr<FuseServer> server(new FuseServer());
ASSERT_TRUE(server->fs().AddFileAtRoot("file"));
ASSERT_TRUE(Mount(server));
std::string filename = GetMountDir() + "/file";
fbl::unique_fd fd(open(filename.c_str(), O_RDWR | O_CREAT));
ASSERT_TRUE(fd.is_valid()) << strerror(errno);
fd.reset();
}
TEST_F(FuseServerTest, HeaderLengthUnderflow) {
class HeaderLengthUnderflowServer : public FuseServer {
testing::AssertionResult HandleAccess(const std::shared_ptr<Node>& node,
const struct fuse_in_header& in_header,
const struct fuse_access_in* access_in) 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);
}
};
std::shared_ptr<HeaderLengthUnderflowServer> server(new HeaderLengthUnderflowServer());
server->fs().AddFileAtRoot("file");
ASSERT_TRUE(Mount(server));
std::string filename = GetMountDir() + "/file";
fbl::unique_fd 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 std::shared_ptr<Node>& node,
const struct fuse_in_header& in_header,
const struct fuse_open_in* open_in) 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);
std::vector<std::byte> response(response_len);
out_header.unique = in_header.unique;
// Test an length longer than what we wil write.
out_header.len = response_len + kBogusHeaderLengthAddition;
memcpy(response.data(), &out_header, sizeof(out_header));
memcpy(response.data() + sizeof(out_header), &open_out, sizeof(open_out));
EXPECT_FALSE(WriteResponse(response));
// Test the right length.
out_header.len -= kBogusHeaderLengthAddition;
memcpy(response.data(), &out_header, sizeof(out_header));
EXPECT_TRUE(WriteResponse(response));
return testing::AssertionSuccess();
}
};
std::shared_ptr<OverlongHeaderLengthServer> server(new OverlongHeaderLengthServer());
server->fs().AddFileAtRoot("file");
ASSERT_TRUE(Mount(server));
std::string filename = GetMountDir() + "/file";
fbl::unique_fd fd(open(filename.c_str(), O_RDWR | O_CREAT));
ASSERT_TRUE(fd.is_valid());
fd.reset();
}
TEST_F(FuseServerTest, RevalidateEnoent) {
auto server = std::make_shared<FuseServer>();
ASSERT_TRUE(Mount(server));
std::string file = GetMountDir() + "/file";
{
fbl::unique_fd fd(open(file.c_str(), O_WRONLY | O_CREAT));
ASSERT_TRUE(fd.is_valid());
}
server->fs().RootDir()->RemoveChild("file");
// We removed `file` from behind Starnix's back; we should be able to recreate the file. Starnix
// should try and revalidate the file which will result in ENOENT, which should cause it to loop
// around.
{
fbl::unique_fd fd(open(file.c_str(), O_WRONLY | O_CREAT | O_EXCL));
ASSERT_TRUE(fd.is_valid());
}
}
// Run the checks in a separate thread where we drop the capabilities which bypasses discretionary
// file permission checks. See capabilities(7).
template <typename F>
void InThreadWithoutCapDacOverride(F f) {
std::thread thrd([&]() {
test_helper::UnsetCapability(CAP_DAC_OVERRIDE);
test_helper::UnsetCapability(CAP_DAC_READ_SEARCH);
f();
});
thrd.join();
}
struct BypassAccessTestCase {
uint32_t want_init_flags;
int access_reply;
uint64_t max_access_count;
};
class CountingFuseServer : public FuseServer {
public:
CountingFuseServer(uint32_t want_init_flags, int access_reply)
: FuseServer(want_init_flags), access_reply_(access_reply) {}
uint64_t LookupCount() { return calls_to_lookup_.load(std::memory_order_relaxed); }
uint64_t AccessCount() { return calls_to_access_.load(std::memory_order_relaxed); }
uint64_t NonRootGetAttrCount() {
return calls_to_non_root_getattr_.load(std::memory_order_relaxed);
}
protected:
testing::AssertionResult HandleLookup(const std::shared_ptr<Node>& node,
const struct fuse_in_header& in_header,
const char* name) override {
calls_to_lookup_.fetch_add(1, std::memory_order_relaxed);
return FuseServer::HandleLookup(node, in_header, name);
}
testing::AssertionResult HandleAccess(const std::shared_ptr<Node>& node,
const struct fuse_in_header& in_header,
const struct fuse_access_in* access_in) override {
calls_to_access_.fetch_add(1, std::memory_order_relaxed);
return WriteDataFreeResponse(in_header, access_reply_);
}
testing::AssertionResult HandleGetAttr(const std::shared_ptr<Node>& node,
const struct fuse_in_header& in_header,
const struct fuse_getattr_in* getattr_in) override {
if (in_header.nodeid != FUSE_ROOT_ID) {
calls_to_non_root_getattr_.fetch_add(1, std::memory_order_relaxed);
}
return FuseServer::HandleGetAttr(node, in_header, getattr_in);
}
private:
int access_reply_;
std::atomic_uint64_t calls_to_lookup_ = 0;
std::atomic_uint64_t calls_to_access_ = 0;
std::atomic_uint64_t calls_to_non_root_getattr_ = 0;
};
struct PermissionCheckTestCase {
std::optional<int> need_cap;
uint32_t want_init_flags;
uint32_t file_type;
std::function<void(const std::string&)> fn;
uint64_t expected_lookup_count;
uint64_t expected_access_count;
uint64_t expected_non_root_getattr_count;
};
class FuseServerPermissionCheck : public FuseServerTest,
public ::testing::WithParamInterface<PermissionCheckTestCase> {};
TEST_P(FuseServerPermissionCheck, PermissionCheck) {
const PermissionCheckTestCase& test_case = GetParam();
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (test_case.need_cap && !test_helper::HasCapability(test_case.need_cap.value())) {
GTEST_SKIP() << "Need extra capability " << test_case.need_cap.value();
}
std::shared_ptr<CountingFuseServer> server(new CountingFuseServer(test_case.want_init_flags, 0));
switch (test_case.file_type) {
case S_IFREG:
ASSERT_TRUE(server->fs().AddFileAtRoot("node"));
break;
case S_IFDIR:
ASSERT_TRUE(server->fs().AddDirAtRoot("node"));
break;
default:
FAIL() << "Unexpected file type = " << test_case.file_type;
}
ASSERT_TRUE(Mount(server));
server->WaitForInit();
EXPECT_EQ(server->LookupCount(), 0u);
EXPECT_EQ(server->AccessCount(), 0u);
EXPECT_EQ(server->NonRootGetAttrCount(), 0u);
std::string path = GetMountDir() + "/node";
ASSERT_NO_FATAL_FAILURE(test_case.fn(path));
// TODO(https://fxbug.dev/331965426): Don't perform an extra lookup.
const uint64_t lookup_count_offset = test_helper::IsStarnix() ? 1 : 0;
EXPECT_EQ(server->LookupCount(), test_case.expected_lookup_count + lookup_count_offset);
EXPECT_EQ(server->AccessCount(), test_case.expected_access_count);
EXPECT_EQ(server->NonRootGetAttrCount(), test_case.expected_non_root_getattr_count);
}
void TestChdir(const std::string& path) {
test_helper::ForkHelper fork_helper;
// Run in a forked process to not modify the state of the current
// process which may run other tests.
fork_helper.RunInForkedProcess([&] { ASSERT_EQ(chdir(path.c_str()), 0) << strerror(errno); });
ASSERT_TRUE(fork_helper.WaitForChildren());
}
void TestChroot(const std::string& path) {
test_helper::ForkHelper fork_helper;
// Run in a forked process to not modify the state of the current
// process which may run other tests.
fork_helper.RunInForkedProcess([&] { ASSERT_EQ(chroot(path.c_str()), 0) << strerror(errno); });
ASSERT_TRUE(fork_helper.WaitForChildren());
}
void TestAccess(const std::string& path) {
ASSERT_EQ(access(path.c_str(), R_OK), 0) << strerror(errno);
}
void TestExec(const std::string& path) {
int ret = execl(path.c_str(), path.c_str(), nullptr);
ASSERT_EQ(ret, -1);
// Access check passes but we don't exec because there is no memory for this file.
EXPECT_EQ(errno, ENOEXEC);
}
void TestStat(const std::string& path) {
struct stat s;
ASSERT_EQ(stat(path.c_str(), &s), 0) << strerror(errno);
}
void TestOpenWithFlags(const std::string& path, int flags) {
fbl::unique_fd fd(open(path.c_str(), flags));
ASSERT_TRUE(fd.is_valid());
}
INSTANTIATE_TEST_SUITE_P(FuseServerPermissionCheck, FuseServerPermissionCheck,
testing::Values(
// When performing a path walk, we should only use |FUSE_LOOKUP|
// for _initial_ permission/access checking.
PermissionCheckTestCase{
.want_init_flags = 0,
.file_type = S_IFREG,
.fn =
[](const std::string& path) {
ASSERT_NO_FATAL_FAILURE(TestOpenWithFlags(path, O_RDWR));
},
.expected_lookup_count = 1,
.expected_access_count = 0,
.expected_non_root_getattr_count = 0,
},
PermissionCheckTestCase{
.want_init_flags = 0,
.file_type = S_IFDIR,
.fn =
[](const std::string& path) {
ASSERT_NO_FATAL_FAILURE(TestOpenWithFlags(path, O_RDONLY));
},
.expected_lookup_count = 1,
.expected_access_count = 0,
.expected_non_root_getattr_count = 0,
},
PermissionCheckTestCase{
.want_init_flags = 0,
.file_type = S_IFREG,
.fn = TestStat,
.expected_lookup_count = 1,
.expected_access_count = 0,
.expected_non_root_getattr_count = 1,
},
PermissionCheckTestCase{
.want_init_flags = 0,
.file_type = S_IFDIR,
.fn = TestStat,
.expected_lookup_count = 1,
.expected_access_count = 0,
.expected_non_root_getattr_count = 1,
},
// These are the same as the above, but with the `FUSE_POSIX_ACL`
// init flag set.
PermissionCheckTestCase{
.want_init_flags = FUSE_POSIX_ACL,
.file_type = S_IFREG,
.fn =
[](const std::string& path) {
ASSERT_NO_FATAL_FAILURE(TestOpenWithFlags(path, O_RDWR));
},
.expected_lookup_count = 1,
.expected_access_count = 0,
.expected_non_root_getattr_count = 1,
},
PermissionCheckTestCase{
.want_init_flags = FUSE_POSIX_ACL,
.file_type = S_IFDIR,
.fn =
[](const std::string& path) {
ASSERT_NO_FATAL_FAILURE(TestOpenWithFlags(path, O_RDONLY));
},
.expected_lookup_count = 1,
.expected_access_count = 0,
.expected_non_root_getattr_count = 1,
},
PermissionCheckTestCase{
.want_init_flags = FUSE_POSIX_ACL,
.file_type = S_IFREG,
.fn = TestStat,
.expected_lookup_count = 1,
.expected_access_count = 0,
.expected_non_root_getattr_count = 1,
},
PermissionCheckTestCase{
.want_init_flags = FUSE_POSIX_ACL,
.file_type = S_IFDIR,
.fn = TestStat,
.expected_lookup_count = 1,
.expected_access_count = 0,
.expected_non_root_getattr_count = 1,
},
// Only the |access|, |chdir| and |chroot| family of
// syscalls may trigger |FUSE_ACCESS|.
PermissionCheckTestCase{
.want_init_flags = 0,
.file_type = S_IFREG,
.fn = TestAccess,
.expected_lookup_count = 1,
.expected_access_count = 1,
.expected_non_root_getattr_count = 0,
},
PermissionCheckTestCase{
.want_init_flags = 0,
.file_type = S_IFREG,
.fn = TestExec,
.expected_lookup_count = test_helper::IsStarnix() ? 1u : 2u,
// Importantly, we don't do any access checks during exec.
.expected_access_count = 0,
.expected_non_root_getattr_count = test_helper::IsStarnix() ? 1u
: 2u,
},
PermissionCheckTestCase{
.want_init_flags = 0,
.file_type = S_IFDIR,
.fn = TestAccess,
.expected_lookup_count = 1,
.expected_access_count = 1,
.expected_non_root_getattr_count = 0,
},
PermissionCheckTestCase{
.want_init_flags = 0,
.file_type = S_IFDIR,
.fn = TestChdir,
.expected_lookup_count = 1,
.expected_access_count = 1,
.expected_non_root_getattr_count = 0,
},
PermissionCheckTestCase{
.need_cap = CAP_SYS_CHROOT,
.want_init_flags = 0,
.file_type = S_IFDIR,
.fn = TestChroot,
.expected_lookup_count = 1,
.expected_access_count = 1,
.expected_non_root_getattr_count = 0,
},
// These are the same as the above, but with the `FUSE_POSIX_ACL`
// init flag set.
PermissionCheckTestCase{
.want_init_flags = FUSE_POSIX_ACL,
.file_type = S_IFREG,
.fn = TestAccess,
.expected_lookup_count = 1,
.expected_access_count = 0,
.expected_non_root_getattr_count = 1,
},
PermissionCheckTestCase{
.want_init_flags = FUSE_POSIX_ACL,
.file_type = S_IFDIR,
.fn = TestAccess,
.expected_lookup_count = 1,
.expected_access_count = 0,
.expected_non_root_getattr_count = 1,
},
PermissionCheckTestCase{
.want_init_flags = FUSE_POSIX_ACL,
.file_type = S_IFDIR,
.fn = TestChdir,
.expected_lookup_count = 1,
.expected_access_count = 0,
.expected_non_root_getattr_count = 1,
},
PermissionCheckTestCase{
.need_cap = CAP_SYS_CHROOT,
.want_init_flags = FUSE_POSIX_ACL,
.file_type = S_IFDIR,
.fn = TestChroot,
.expected_lookup_count = 1,
.expected_access_count = 0,
.expected_non_root_getattr_count = 1,
}));
class FuseServerBypassAccessTest : public FuseServerTest,
public ::testing::WithParamInterface<BypassAccessTestCase> {};
TEST_P(FuseServerBypassAccessTest, BypassAccess) {
constexpr char kSomeFile1Name[] = "somefile1";
constexpr char kSomeFile2Name[] = "somefile2";
const BypassAccessTestCase& test_case = GetParam();
std::shared_ptr<CountingFuseServer> server(
new CountingFuseServer(test_case.want_init_flags, test_case.access_reply));
FileSystem& fs = server->fs();
ASSERT_TRUE(fs.AddFileAtRoot(kSomeFile1Name));
ASSERT_TRUE(fs.AddFileAtRoot(kSomeFile2Name));
ASSERT_TRUE(Mount(server));
server->WaitForInit();
EXPECT_EQ(server->AccessCount(), 0u);
InThreadWithoutCapDacOverride([&]() {
auto check_access = [&, max_access_count = test_case.max_access_count](const char* file) {
const std::string filename = GetMountDir() + "/" + file;
ASSERT_EQ(access(filename.c_str(), R_OK), 0) << strerror(errno);
EXPECT_EQ(server->AccessCount(), max_access_count);
};
// No matter how many times we access a file, we should have only ever made
// the |FUSE_ACCESS| request |params.max_access_count| times for the lifetime
// of the connection/server.
for (int i = 0; i < 3; ++i) {
ASSERT_NO_FATAL_FAILURE(check_access(kSomeFile1Name));
}
// Accessing another file shouldn't change the access count since it is still
// part of the same connection.
ASSERT_NO_FATAL_FAILURE(check_access(kSomeFile2Name));
});
}
INSTANTIATE_TEST_SUITE_P(
FuseServerBypassAccessTest, FuseServerBypassAccessTest,
testing::Values(
// The kernel should stop sending |FUSE_ACCESS| requests once we send an
// |ENOSYS| response.
BypassAccessTestCase{.want_init_flags = 0, .access_reply = -ENOSYS, .max_access_count = 1},
// The kernel should never send a |FUSE_ACCESS| request if we set the
// |FUSE_POSIX_ACL| init flag.
BypassAccessTestCase{
.want_init_flags = FUSE_POSIX_ACL, .access_reply = 0, .max_access_count = 0}));
enum class ExpectedGetAttrBehaviour {
kNone,
kOncePerFile,
kOncePerAccess,
};
uint64_t ExpectedGetAttrsValue(ExpectedGetAttrBehaviour behaviour, uint64_t access_count) {
switch (behaviour) {
case ExpectedGetAttrBehaviour::kNone:
return 0;
case ExpectedGetAttrBehaviour::kOncePerFile:
return std::min(access_count, static_cast<uint64_t>(1));
case ExpectedGetAttrBehaviour::kOncePerAccess:
return access_count;
}
}
struct CacheAttributesTestCase {
uint64_t lookup_attr_timeout;
uint64_t getattr_attr_timeout;
ExpectedGetAttrBehaviour expected_getattr_behaviour;
};
class FuseServerCacheAttributesTest
: public FuseServerTest,
public ::testing::WithParamInterface<CacheAttributesTestCase> {};
TEST_P(FuseServerCacheAttributesTest, CacheAttributes) {
constexpr char kSomeFile1Name[] = "somefile1";
constexpr char kSomeFile2Name[] = "somefile2";
const CacheAttributesTestCase& test_case = GetParam();
class CacheAttributesServer : public FuseServer {
public:
CacheAttributesServer(uint64_t lookup_attr_valid, uint64_t getattr_attr_valid)
: FuseServer(FUSE_POSIX_ACL),
lookup_attr_valid_(lookup_attr_valid),
getattr_attr_valid_(getattr_attr_valid) {}
uint64_t NonRootGetAttrCount() {
return calls_to_non_root_getattr_.load(std::memory_order_relaxed);
}
protected:
testing::AssertionResult HandleLookup(const std::shared_ptr<Node>& node,
const struct fuse_in_header& in_header,
const char* name) override {
fuse_entry_out entry_out;
if (!HandleLookupInner(node, in_header, name, entry_out)) {
return testing::AssertionSuccess();
}
// Instruct the kernel to not immediately evict the |dcache| entry (|dentry|)
// for this node by setting a really high entry value. This value is used to
// determine when a FUSE-based |dentry| has gone stale. This test isn't focused
// on the |dcache| or |dentry| so this is ok. For more details, see:
// - https://www.halolinux.us/kernel-reference/the-dentry-cache.html
// - https://www.kernel.org/doc/html/latest/filesystems/path-lookup.html
// - https://lwn.net/Articles/649115/
// -
// https://www.infradead.org/~mchehab/kernel_docs/filesystems/path-walking.html#dcache-name-lookup
entry_out.entry_valid = std::numeric_limits<uint64_t>::max();
entry_out.attr_valid = lookup_attr_valid_;
return WriteStructResponse(in_header, entry_out);
}
testing::AssertionResult HandleGetAttr(const std::shared_ptr<Node>& node,
const struct fuse_in_header& in_header,
const struct fuse_getattr_in* getattr_in) override {
if (in_header.nodeid != FUSE_ROOT_ID) {
calls_to_non_root_getattr_.fetch_add(1, std::memory_order_relaxed);
}
fuse_attr_out attr_out = {
.attr_valid = getattr_attr_valid_,
};
node->PopulateAttr(attr_out.attr);
return WriteStructResponse(in_header, attr_out);
}
private:
uint64_t lookup_attr_valid_;
uint64_t getattr_attr_valid_;
std::atomic_uint64_t calls_to_non_root_getattr_ = 0;
};
std::shared_ptr<CacheAttributesServer> server(
new CacheAttributesServer(test_case.lookup_attr_timeout, test_case.getattr_attr_timeout));
FileSystem& fs = server->fs();
ASSERT_TRUE(fs.AddFileAtRoot(kSomeFile1Name));
ASSERT_TRUE(fs.AddFileAtRoot(kSomeFile2Name));
ASSERT_TRUE(Mount(server));
server->WaitForInit();
EXPECT_EQ(server->NonRootGetAttrCount(), 0u);
InThreadWithoutCapDacOverride([&]() {
auto check_getattr = [&](const char* file, uint64_t expected_getattrs) {
const std::string filename = GetMountDir() + "/" + file;
ASSERT_EQ(access(filename.c_str(), R_OK), 0) << strerror(errno);
EXPECT_EQ(server->NonRootGetAttrCount(), expected_getattrs);
};
uint64_t count_after_file1;
for (uint64_t i = 1; i <= 3; ++i) {
count_after_file1 = ExpectedGetAttrsValue(test_case.expected_getattr_behaviour, i);
ASSERT_NO_FATAL_FAILURE(check_getattr(kSomeFile1Name, count_after_file1));
}
for (uint64_t i = 1; i <= 5; ++i) {
ASSERT_NO_FATAL_FAILURE(check_getattr(
kSomeFile2Name,
count_after_file1 + ExpectedGetAttrsValue(test_case.expected_getattr_behaviour, i)));
}
});
}
INSTANTIATE_TEST_SUITE_P(
FuseServerCacheAttributesTest, FuseServerCacheAttributesTest,
testing::Values(
// When we don't cache the attributes, expect the kernel to refresh the
// attributes each call.
CacheAttributesTestCase{
.lookup_attr_timeout = 0,
.getattr_attr_timeout = 0,
.expected_getattr_behaviour = ExpectedGetAttrBehaviour::kOncePerAccess},
// When we respond to the lookup request with a cache timeout, it should
// be respected.
CacheAttributesTestCase{.lookup_attr_timeout = std::numeric_limits<uint64_t>::max(),
.getattr_attr_timeout = 0,
.expected_getattr_behaviour = ExpectedGetAttrBehaviour::kNone},
// When don't provide a cache timeout with lookup, but do for getattr,
// respect the cached attributes after the getattr request.
CacheAttributesTestCase{
.lookup_attr_timeout = 0,
.getattr_attr_timeout = std::numeric_limits<uint64_t>::max(),
.expected_getattr_behaviour = ExpectedGetAttrBehaviour::kOncePerFile}));
struct PathWalkRefreshDirEntryTestCase {
bool modify_nodeid;
bool modify_generation;
uint64_t entry_valid_forever;
uint64_t expected_extra_lookups;
};
class FusePathWalkRefreshDirEntryTest
: public FuseServerTest,
public testing::WithParamInterface<PathWalkRefreshDirEntryTestCase> {};
TEST_P(FusePathWalkRefreshDirEntryTest, PathWalkRefreshDirEntry) {
const PathWalkRefreshDirEntryTestCase& test_case = GetParam();
std::shared_ptr<CountingFuseServer> server(new CountingFuseServer(0, 0));
FileSystem& fs = server->fs();
std::shared_ptr<Directory> dir1 = fs.AddDirAtRoot("dir1");
std::shared_ptr<Directory> dir2 = fs.AddDirAt(dir1, "dir2");
std::shared_ptr<File> file = fs.AddFileAt(dir2, "file");
if (test_case.entry_valid_forever) {
dir1->SetEntryValidDuration(std::numeric_limits<uint64_t>::max());
dir2->SetEntryValidDuration(std::numeric_limits<uint64_t>::max());
file->SetEntryValidDuration(std::numeric_limits<uint64_t>::max());
}
ASSERT_TRUE(Mount(server));
EXPECT_EQ(server->LookupCount(), 0u);
constexpr uint64_t kNumberOfNodesInPath = 3;
// TODO(https://fxbug.dev/331965426): Don't perform an extra set of lookups each
// time we create a new DirEntry in starnix. Note that refreshing a DirEntry
// does not result in extra lookups, only the initial lookup to populate a new
// DirEntry does.
const uint64_t extra_initial_lookups_per_node =
(!test_case.entry_valid_forever && test_helper::IsStarnix()) ? 1 : 0;
const uint64_t extra_subsequent_lookups_per_node =
test_case.entry_valid_forever ? 0 : kNumberOfNodesInPath;
const uint64_t lookup_offset = extra_initial_lookups_per_node * kNumberOfNodesInPath;
const uint64_t expected_initial_lookup_count = kNumberOfNodesInPath + lookup_offset;
const uint64_t expected_post_update_lookup_extra_offset = extra_initial_lookups_per_node;
std::string filename = GetMountDir() + "/dir1/dir2/file";
auto check_open = [&]() {
fbl::unique_fd fd(open(filename.c_str(), O_RDWR));
ASSERT_TRUE(fd.is_valid()) << strerror(errno);
};
ASSERT_NO_FATAL_FAILURE(check_open());
EXPECT_EQ(server->LookupCount(), expected_initial_lookup_count);
ASSERT_NO_FATAL_FAILURE(check_open());
EXPECT_EQ(server->LookupCount(),
expected_initial_lookup_count + extra_subsequent_lookups_per_node);
// When the kernel attempts to refresh the entry and sees a node ID or generation
// different from what the kernel has cached for the same name, the kernel will
// discard the cached entry and perform a fresh lookup to create a new entry
// for the node with a different ID/generation pair but with the same name. Note
// that different ID/generation pairs are interpreted as a completely different
// nodes, but no two nodes may have the same ID, even if nodes have the same name
// in the same directory.
if (test_case.modify_nodeid) {
fs.UpdateNodeId(dir2);
}
if (test_case.modify_generation) {
dir2->IncrementGeneration();
}
ASSERT_NO_FATAL_FAILURE(check_open());
EXPECT_EQ(server->LookupCount(),
expected_initial_lookup_count + (2 * extra_subsequent_lookups_per_node) +
test_case.expected_extra_lookups * (1 + expected_post_update_lookup_extra_offset));
}
INSTANTIATE_TEST_SUITE_P(FusePathWalkRefreshDirEntryTest, FusePathWalkRefreshDirEntryTest,
testing::Values(
PathWalkRefreshDirEntryTestCase{
.modify_nodeid = false,
.modify_generation = false,
.entry_valid_forever = false,
.expected_extra_lookups = 0,
},
PathWalkRefreshDirEntryTestCase{
.modify_nodeid = false,
.modify_generation = true,
.entry_valid_forever = false,
.expected_extra_lookups = 1,
},
PathWalkRefreshDirEntryTestCase{
.modify_nodeid = true,
.modify_generation = false,
.entry_valid_forever = false,
.expected_extra_lookups = 1,
},
PathWalkRefreshDirEntryTestCase{
.modify_nodeid = true,
.modify_generation = true,
.entry_valid_forever = false,
.expected_extra_lookups = 1,
},
// Same as above but with entryies valid forever.
PathWalkRefreshDirEntryTestCase{
.modify_nodeid = false,
.modify_generation = false,
.entry_valid_forever = true,
.expected_extra_lookups = 0,
},
PathWalkRefreshDirEntryTestCase{
.modify_nodeid = false,
.modify_generation = true,
.entry_valid_forever = true,
.expected_extra_lookups = 0,
},
PathWalkRefreshDirEntryTestCase{
.modify_nodeid = true,
.modify_generation = false,
.entry_valid_forever = true,
.expected_extra_lookups = 0,
},
PathWalkRefreshDirEntryTestCase{
.modify_nodeid = true,
.modify_generation = true,
.entry_valid_forever = true,
.expected_extra_lookups = 0,
}));
struct DirPermissionCheckTestCase {
uint32_t want_init_flags;
uint32_t perms;
bool expect_open;
};
class FuseDirPermissionCheck : public FuseServerTest,
public ::testing::WithParamInterface<DirPermissionCheckTestCase> {};
TEST_P(FuseDirPermissionCheck, DirPermissionCheck) {
const DirPermissionCheckTestCase& test_case = GetParam();
std::shared_ptr<FuseServer> server(new FuseServer(test_case.want_init_flags));
FileSystem& fs = server->fs();
std::shared_ptr<Directory> dir = fs.AddDirAtRoot("dir");
ASSERT_TRUE(fs.AddFileAt(dir, "node"));
ASSERT_TRUE(Mount(server));
server->WaitForInit();
std::string path = GetMountDir() + "/dir/node";
dir->SetPermissions(test_case.perms);
InThreadWithoutCapDacOverride([&]() {
fbl::unique_fd fd(open(path.c_str(), O_RDONLY));
EXPECT_EQ(fd.is_valid(), test_case.expect_open);
});
}
INSTANTIATE_TEST_SUITE_P(FuseDirPermissionCheck, FuseDirPermissionCheck,
testing::Values(
DirPermissionCheckTestCase{
.want_init_flags = 0,
.perms = S_IRWXU | S_IRWXG | S_IRWXO,
.expect_open = true,
},
DirPermissionCheckTestCase{
.want_init_flags = 0,
.perms = 0,
.expect_open = true,
},
DirPermissionCheckTestCase{
.want_init_flags = FUSE_POSIX_ACL,
.perms = S_IRWXU | S_IRWXG | S_IRWXO,
.expect_open = true,
},
DirPermissionCheckTestCase{
.want_init_flags = FUSE_POSIX_ACL,
.perms = 0,
.expect_open = false,
}));
TEST_F(FuseServerTest, ExecAccessDenied) {
std::shared_ptr<FuseServer> server(new FuseServer(0));
FileSystem& fs = server->fs();
std::shared_ptr<Directory> dir = fs.AddDirAtRoot("dir");
auto file = fs.AddFileAt(dir, "node");
ASSERT_TRUE(file);
file->SetPermissions(S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
ASSERT_TRUE(Mount(server));
server->WaitForInit();
std::string path = GetMountDir() + "/dir/node";
InThreadWithoutCapDacOverride([&]() {
int ret = execl(path.c_str(), path.c_str(), nullptr);
EXPECT_EQ(ret, -1);
EXPECT_EQ(errno, EACCES);
});
}
struct MkPermissionCheckTestCase {
uint32_t want_init_flags;
uint32_t perms;
int expected_errno;
};
class FuseMkPermissionCheck
: public FuseServerTest,
public ::testing::WithParamInterface<
std::tuple<std::function<int(const std::string&)>, MkPermissionCheckTestCase>> {};
TEST_P(FuseMkPermissionCheck, MkPermissionCheck) {
const std::tuple<std::function<int(const std::string&)>, MkPermissionCheckTestCase>& param =
GetParam();
const std::function<int(const std::string&)>& test_fn = std::get<0>(param);
const MkPermissionCheckTestCase& test_case = std::get<1>(param);
std::shared_ptr<FuseServer> server(new FuseServer(test_case.want_init_flags));
FileSystem& fs = server->fs();
std::shared_ptr<Directory> dir = fs.AddDirAtRoot("dir");
ASSERT_TRUE(Mount(server));
server->WaitForInit();
std::string path = GetMountDir() + "/dir/node";
dir->SetPermissions(test_case.perms);
InThreadWithoutCapDacOverride([&]() {
int ret = test_fn(path);
if (test_case.expected_errno == 0) {
EXPECT_EQ(ret, 0) << strerror(errno);
} else {
ASSERT_EQ(ret, -1);
EXPECT_EQ(errno, test_case.expected_errno);
}
});
}
INSTANTIATE_TEST_SUITE_P(
FuseMkPermissionCheck, FuseMkPermissionCheck,
testing::Combine(
testing::Values([](const std::string& path) { return mknod(path.c_str(), 0, 0); },
[](const std::string& path) { return mkdir(path.c_str(), 0); }),
testing::Values(
MkPermissionCheckTestCase{
.want_init_flags = 0,
.perms = S_IRWXU | S_IRWXG | S_IRWXO,
.expected_errno = 0,
},
MkPermissionCheckTestCase{
.want_init_flags = 0,
.perms = 0,
.expected_errno = 0,
},
MkPermissionCheckTestCase{
.want_init_flags = FUSE_POSIX_ACL,
.perms = S_IRWXU | S_IRWXG | S_IRWXO,
.expected_errno = 0,
},
MkPermissionCheckTestCase{
.want_init_flags = FUSE_POSIX_ACL,
.perms = 0,
.expected_errno = EACCES,
})));
TEST_F(FuseServerTest, InvalidateMountDir) {
std::shared_ptr<FuseServer> parent_server(new FuseServer());
std::shared_ptr<Directory> mount_dir = parent_server->fs().AddDirAtRoot("mount_dir");
ASSERT_TRUE(mount_dir);
ASSERT_TRUE(Mount(parent_server));
std::shared_ptr<FuseServer> child_server(new FuseServer());
ASSERT_TRUE(child_server->fs().AddFileAtRoot("node"));
const std::string child_mount_dir = GetMountDir() + "/mount_dir";
ASSERT_TRUE(child_server->Mount(child_mount_dir));
std::thread child_mount_thread([&] {
while (child_server->ServeOnce()) {
}
});
auto cleanup_child_mount = fit::defer([&]() {
umount2(child_mount_dir.c_str(), MNT_DETACH);
child_mount_thread.join();
});
const std::string node_path = child_mount_dir + "/node";
ASSERT_NO_FATAL_FAILURE(TestOpenWithFlags(node_path, O_RDONLY));
// The following open operation will trigger a revalidation on the child
// mount directory which will fail because we update the generation. This
// revalidation failure should trigger the implicit unmounting of the child
// FUSE server.
mount_dir->IncrementGeneration();
ASSERT_EQ(open(node_path.c_str(), O_RDONLY), -1);
EXPECT_EQ(errno, ENOENT);
ASSERT_EQ(umount2(child_mount_dir.c_str(), MNT_DETACH), -1);
EXPECT_EQ(errno, EINVAL);
}
TEST_F(FuseServerTest, ReadDir) {
std::shared_ptr<FuseServer> parent_server(new FuseServer());
std::shared_ptr<Directory> mount_dir = parent_server->fs().AddDirAtRoot("dir");
ASSERT_TRUE(mount_dir);
ASSERT_TRUE(Mount(parent_server));
const std::string dir_path = GetMountDir() + "/dir";
fbl::unique_fd fd(open(dir_path.c_str(), O_RDONLY));
ASSERT_TRUE(fd.is_valid());
char buf[4096];
ASSERT_EQ(read(fd.get(), buf, 4096), -1);
ASSERT_EQ(errno, EISDIR);
}