aboutsummaryrefslogtreecommitdiff
path: root/tests/sys/fs/fusefs/default_permissions.cc
diff options
context:
space:
mode:
Diffstat (limited to 'tests/sys/fs/fusefs/default_permissions.cc')
-rw-r--r--tests/sys/fs/fusefs/default_permissions.cc1644
1 files changed, 1644 insertions, 0 deletions
diff --git a/tests/sys/fs/fusefs/default_permissions.cc b/tests/sys/fs/fusefs/default_permissions.cc
new file mode 100644
index 000000000000..4b04297d97ac
--- /dev/null
+++ b/tests/sys/fs/fusefs/default_permissions.cc
@@ -0,0 +1,1644 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2019 The FreeBSD Foundation
+ *
+ * This software was developed by BFF Storage Systems, LLC under sponsorship
+ * from the FreeBSD Foundation.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * Tests for the "default_permissions" mount option. They must be in their own
+ * file so they can be run as an unprivileged user.
+ */
+
+extern "C" {
+#include <sys/types.h>
+#include <sys/extattr.h>
+
+#include <fcntl.h>
+#include <semaphore.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class DefaultPermissions: public FuseTest {
+
+virtual void SetUp() {
+ m_default_permissions = true;
+ FuseTest::SetUp();
+ if (HasFatalFailure() || IsSkipped())
+ return;
+
+ if (geteuid() == 0) {
+ GTEST_SKIP() << "This test requires an unprivileged user";
+ }
+
+ /* With -o default_permissions, FUSE_ACCESS should never be called */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_ACCESS);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+}
+
+public:
+void expect_chmod(uint64_t ino, mode_t mode, uint64_t size = 0)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.valid == FATTR_MODE &&
+ in.body.setattr.mode == mode);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = ino; // Must match nodeid
+ out.body.attr.attr.mode = S_IFREG | mode;
+ out.body.attr.attr.size = size;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+}
+
+void expect_create(const char *relpath, uint64_t ino)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes +
+ sizeof(fuse_create_in);
+ return (in.header.opcode == FUSE_CREATE &&
+ (0 == strcmp(relpath, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, create);
+ out.body.create.entry.attr.mode = S_IFREG | 0644;
+ out.body.create.entry.nodeid = ino;
+ out.body.create.entry.entry_valid = UINT64_MAX;
+ out.body.create.entry.attr_valid = UINT64_MAX;
+ })));
+}
+
+void expect_copy_file_range(uint64_t ino_in, uint64_t off_in, uint64_t ino_out,
+ uint64_t off_out, uint64_t len)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
+ in.header.nodeid == ino_in &&
+ in.body.copy_file_range.off_in == off_in &&
+ in.body.copy_file_range.nodeid_out == ino_out &&
+ in.body.copy_file_range.off_out == off_out &&
+ in.body.copy_file_range.len == len);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = len;
+ })));
+}
+
+void expect_getattr(uint64_t ino, mode_t mode, uint64_t attr_valid, int times,
+ uid_t uid = 0, gid_t gid = 0)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(times)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = ino; // Must match nodeid
+ out.body.attr.attr.mode = mode;
+ out.body.attr.attr.size = 0;
+ out.body.attr.attr.uid = uid;
+ out.body.attr.attr.gid = gid;
+ out.body.attr.attr_valid = attr_valid;
+ })));
+}
+
+void expect_lookup(const char *relpath, uint64_t ino, mode_t mode,
+ uint64_t attr_valid, uid_t uid = 0, gid_t gid = 0)
+{
+ FuseTest::expect_lookup(relpath, ino, mode, 0, 1, attr_valid, uid, gid);
+}
+
+};
+
+class Access: public DefaultPermissions {};
+class Chown: public DefaultPermissions {};
+class Chgrp: public DefaultPermissions {};
+class CopyFileRange: public DefaultPermissions {};
+class Fspacectl: public DefaultPermissions {};
+class Lookup: public DefaultPermissions {};
+class Open: public DefaultPermissions {};
+class PosixFallocate: public DefaultPermissions {};
+class Read: public DefaultPermissions {};
+class Setattr: public DefaultPermissions {};
+class Unlink: public DefaultPermissions {};
+class Utimensat: public DefaultPermissions {};
+class Write: public DefaultPermissions {};
+
+/*
+ * Test permission handling during create, mkdir, mknod, link, symlink, and
+ * rename vops (they all share a common path for permission checks in
+ * VOP_LOOKUP)
+ */
+class Create: public DefaultPermissions {};
+
+class Deleteextattr: public DefaultPermissions {
+public:
+void expect_removexattr()
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_REMOVEXATTR);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(0)));
+}
+};
+
+class Getextattr: public DefaultPermissions {
+public:
+void expect_getxattr(ProcessMockerT r)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETXATTR);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(r));
+}
+};
+
+class Listextattr: public DefaultPermissions {
+public:
+void expect_listxattr()
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LISTXATTR);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
+ out.body.listxattr.size = 0;
+ SET_OUT_HEADER_LEN(out, listxattr);
+ })));
+}
+};
+
+class Rename: public DefaultPermissions {
+public:
+ /*
+ * Expect a rename and respond with the given error. Don't both to
+ * validate arguments; the tests in rename.cc do that.
+ */
+ void expect_rename(int error)
+ {
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_RENAME);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(error)));
+ }
+};
+
+class Setextattr: public DefaultPermissions {
+public:
+void expect_setxattr(int error)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_SETXATTR);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(error)));
+}
+};
+
+/* Return a group to which this user does not belong */
+static gid_t excluded_group()
+{
+ int i, ngroups = 64;
+ gid_t newgid, groups[ngroups];
+
+ getgrouplist(getlogin(), getegid(), groups, &ngroups);
+ for (newgid = 0; ; newgid++) {
+ bool belongs = false;
+
+ for (i = 0; i < ngroups; i++) {
+ if (groups[i] == newgid)
+ belongs = true;
+ }
+ if (!belongs)
+ break;
+ }
+ /* newgid is now a group to which the current user does not belong */
+ return newgid;
+}
+
+TEST_F(Access, eacces)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ mode_t access_mode = X_OK;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);
+
+ ASSERT_NE(0, access(FULLPATH, access_mode));
+ ASSERT_EQ(EACCES, errno);
+}
+
+TEST_F(Access, eacces_no_cached_attrs)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ mode_t access_mode = X_OK;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, 0, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0);
+ expect_getattr(ino, S_IFREG | 0644, 0, 1);
+ /*
+ * Once default_permissions is properly implemented, there might be
+ * another FUSE_GETATTR or something in here. But there should not be
+ * a FUSE_ACCESS
+ */
+
+ ASSERT_NE(0, access(FULLPATH, access_mode));
+ ASSERT_EQ(EACCES, errno);
+}
+
+TEST_F(Access, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ mode_t access_mode = R_OK;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);
+ /*
+ * Once default_permissions is properly implemented, there might be
+ * another FUSE_GETATTR or something in here.
+ */
+
+ ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
+}
+
+/* Unprivileged users may chown a file to their own uid */
+TEST_F(Chown, chown_to_self)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const mode_t mode = 0755;
+ uid_t uid;
+
+ uid = geteuid();
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid);
+ expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid);
+ /* The OS may optimize chown by omitting the redundant setattr */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_SETATTR);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out){
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.mode = S_IFREG | mode;
+ out.body.attr.attr.uid = uid;
+ })));
+
+ EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno);
+}
+
+/*
+ * A successful chown by a non-privileged non-owner should clear a file's SUID
+ * bit
+ */
+TEST_F(Chown, clear_suid)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ const mode_t oldmode = 06755;
+ const mode_t newmode = 0755;
+ uid_t uid = geteuid();
+ uint32_t valid = FATTR_UID | FATTR_MODE;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid);
+ expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.valid == valid &&
+ in.body.setattr.mode == newmode);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = ino; // Must match nodeid
+ out.body.attr.attr.mode = S_IFREG | newmode;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+
+ EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno);
+}
+
+
+/* Only root may change a file's owner */
+TEST_F(Chown, eperm)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const mode_t mode = 0755;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, geteuid());
+ expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, geteuid());
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_SETATTR);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+
+ EXPECT_NE(0, chown(FULLPATH, 0, -1));
+ EXPECT_EQ(EPERM, errno);
+}
+
+/*
+ * A successful chgrp by a non-privileged non-owner should clear a file's SUID
+ * bit
+ */
+TEST_F(Chgrp, clear_suid)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ const mode_t oldmode = 06755;
+ const mode_t newmode = 0755;
+ uid_t uid = geteuid();
+ gid_t gid = getegid();
+ uint32_t valid = FATTR_GID | FATTR_MODE;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid);
+ expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.valid == valid &&
+ in.body.setattr.mode == newmode);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = ino; // Must match nodeid
+ out.body.attr.attr.mode = S_IFREG | newmode;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+
+ EXPECT_EQ(0, chown(FULLPATH, -1, gid)) << strerror(errno);
+}
+
+/* non-root users may only chgrp a file to a group they belong to */
+TEST_F(Chgrp, eperm)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const mode_t mode = 0755;
+ uid_t uid;
+ gid_t gid, newgid;
+
+ uid = geteuid();
+ gid = getegid();
+ newgid = excluded_group();
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid);
+ expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_SETATTR);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+
+ EXPECT_NE(0, chown(FULLPATH, -1, newgid));
+ EXPECT_EQ(EPERM, errno);
+}
+
+TEST_F(Chgrp, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const mode_t mode = 0755;
+ uid_t uid;
+ gid_t gid, newgid;
+
+ uid = geteuid();
+ gid = 0;
+ newgid = getegid();
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid);
+ expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid);
+ /* The OS may optimize chgrp by omitting the redundant setattr */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out){
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.mode = S_IFREG | mode;
+ out.body.attr.attr.uid = uid;
+ out.body.attr.attr.gid = newgid;
+ })));
+
+ EXPECT_EQ(0, chown(FULLPATH, -1, newgid)) << strerror(errno);
+}
+
+/* A write by a non-owner should clear a file's SGID bit */
+TEST_F(CopyFileRange, clear_sgid)
+{
+ const char FULLPATH_IN[] = "mountpoint/in.txt";
+ const char RELPATH_IN[] = "in.txt";
+ const char FULLPATH_OUT[] = "mountpoint/out.txt";
+ const char RELPATH_OUT[] = "out.txt";
+ struct stat sb;
+ uint64_t ino_in = 42;
+ uint64_t ino_out = 43;
+ mode_t oldmode = 02777;
+ mode_t newmode = 0777;
+ off_t fsize = 16;
+ off_t off_in = 0;
+ off_t off_out = 8;
+ off_t len = 8;
+ int fd_in, fd_out;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ FuseTest::expect_lookup(RELPATH_IN, ino_in, S_IFREG | oldmode, fsize, 1,
+ UINT64_MAX, 0, 0);
+ expect_open(ino_in, 0, 1);
+ FuseTest::expect_lookup(RELPATH_OUT, ino_out, S_IFREG | oldmode, fsize,
+ 1, UINT64_MAX, 0, 0);
+ expect_open(ino_out, 0, 1);
+ expect_copy_file_range(ino_in, off_in, ino_out, off_out, len);
+ expect_chmod(ino_out, newmode, fsize);
+
+ fd_in = open(FULLPATH_IN, O_RDONLY);
+ ASSERT_LE(0, fd_in) << strerror(errno);
+ fd_out = open(FULLPATH_OUT, O_WRONLY);
+ ASSERT_LE(0, fd_out) << strerror(errno);
+ ASSERT_EQ(len,
+ copy_file_range(fd_in, &off_in, fd_out, &off_out, len, 0))
+ << strerror(errno);
+ ASSERT_EQ(0, fstat(fd_out, &sb)) << strerror(errno);
+ EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
+ ASSERT_EQ(0, fstat(fd_in, &sb)) << strerror(errno);
+ EXPECT_EQ(S_IFREG | oldmode, sb.st_mode);
+
+ leak(fd_in);
+ leak(fd_out);
+}
+
+/* A write by a non-owner should clear a file's SUID bit */
+TEST_F(CopyFileRange, clear_suid)
+{
+ const char FULLPATH_IN[] = "mountpoint/in.txt";
+ const char RELPATH_IN[] = "in.txt";
+ const char FULLPATH_OUT[] = "mountpoint/out.txt";
+ const char RELPATH_OUT[] = "out.txt";
+ struct stat sb;
+ uint64_t ino_in = 42;
+ uint64_t ino_out = 43;
+ mode_t oldmode = 04777;
+ mode_t newmode = 0777;
+ off_t fsize = 16;
+ off_t off_in = 0;
+ off_t off_out = 8;
+ off_t len = 8;
+ int fd_in, fd_out;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ FuseTest::expect_lookup(RELPATH_IN, ino_in, S_IFREG | oldmode, fsize, 1,
+ UINT64_MAX, 0, 0);
+ expect_open(ino_in, 0, 1);
+ FuseTest::expect_lookup(RELPATH_OUT, ino_out, S_IFREG | oldmode, fsize,
+ 1, UINT64_MAX, 0, 0);
+ expect_open(ino_out, 0, 1);
+ expect_copy_file_range(ino_in, off_in, ino_out, off_out, len);
+ expect_chmod(ino_out, newmode, fsize);
+
+ fd_in = open(FULLPATH_IN, O_RDONLY);
+ ASSERT_LE(0, fd_in) << strerror(errno);
+ fd_out = open(FULLPATH_OUT, O_WRONLY);
+ ASSERT_LE(0, fd_out) << strerror(errno);
+ ASSERT_EQ(len,
+ copy_file_range(fd_in, &off_in, fd_out, &off_out, len, 0))
+ << strerror(errno);
+ ASSERT_EQ(0, fstat(fd_out, &sb)) << strerror(errno);
+ EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
+ ASSERT_EQ(0, fstat(fd_in, &sb)) << strerror(errno);
+ EXPECT_EQ(S_IFREG | oldmode, sb.st_mode);
+
+ leak(fd_in);
+ leak(fd_out);
+}
+
+TEST_F(Create, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_create(RELPATH, ino);
+
+ fd = open(FULLPATH, O_CREAT | O_EXCL, 0644);
+ ASSERT_LE(0, fd) << strerror(errno);
+ leak(fd);
+}
+
+TEST_F(Create, eacces)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ ASSERT_EQ(-1, open(FULLPATH, O_CREAT | O_EXCL, 0644));
+ EXPECT_EQ(EACCES, errno);
+}
+
+TEST_F(Deleteextattr, eacces)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
+
+ ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
+ ASSERT_EQ(EACCES, errno);
+}
+
+TEST_F(Deleteextattr, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
+ expect_removexattr();
+
+ ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))
+ << strerror(errno);
+}
+
+/* Delete system attributes requires superuser privilege */
+TEST_F(Deleteextattr, system)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_SYSTEM;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid());
+
+ ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
+ ASSERT_EQ(EPERM, errno);
+}
+
+/* Anybody with write permission can set both timestamps to UTIME_NOW */
+TEST_F(Utimensat, utime_now)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ /* Write permissions for everybody */
+ const mode_t mode = 0666;
+ uid_t owner = 0;
+ const timespec times[2] = {
+ {.tv_sec = 0, .tv_nsec = UTIME_NOW},
+ {.tv_sec = 0, .tv_nsec = UTIME_NOW},
+ };
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, owner);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.valid & FATTR_ATIME &&
+ in.body.setattr.valid & FATTR_MTIME);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.mode = S_IFREG | mode;
+ })));
+
+ ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &times[0], 0))
+ << strerror(errno);
+}
+
+/* Anybody can set both timestamps to UTIME_OMIT */
+TEST_F(Utimensat, utime_omit)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ /* Write permissions for no one */
+ const mode_t mode = 0444;
+ uid_t owner = 0;
+ const timespec times[2] = {
+ {.tv_sec = 0, .tv_nsec = UTIME_OMIT},
+ {.tv_sec = 0, .tv_nsec = UTIME_OMIT},
+ };
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, owner);
+
+ ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &times[0], 0))
+ << strerror(errno);
+}
+
+/* Deleting user attributes merely requires WRITE privilege */
+TEST_F(Deleteextattr, user)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0);
+ expect_removexattr();
+
+ ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))
+ << strerror(errno);
+}
+
+TEST_F(Getextattr, eacces)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ char data[80];
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0600, UINT64_MAX, 0);
+
+ ASSERT_EQ(-1,
+ extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)));
+ ASSERT_EQ(EACCES, errno);
+}
+
+TEST_F(Getextattr, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ char data[80];
+ const char value[] = "whatever";
+ ssize_t value_len = strlen(value) + 1;
+ int ns = EXTATTR_NAMESPACE_USER;
+ ssize_t r;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ /* Getting user attributes only requires read access */
+ expect_lookup(RELPATH, ino, S_IFREG | 0444, UINT64_MAX, 0);
+ expect_getxattr(
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ memcpy((void*)out.body.bytes, value, value_len);
+ out.header.len = sizeof(out.header) + value_len;
+ })
+ );
+
+ r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
+ ASSERT_EQ(value_len, r) << strerror(errno);
+ EXPECT_STREQ(value, data);
+}
+
+/* Getting system attributes requires superuser privileges */
+TEST_F(Getextattr, system)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ char data[80];
+ int ns = EXTATTR_NAMESPACE_SYSTEM;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid());
+
+ ASSERT_EQ(-1,
+ extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)));
+ ASSERT_EQ(EPERM, errno);
+}
+
+TEST_F(Listextattr, eacces)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0600, UINT64_MAX, 0);
+
+ ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
+ ASSERT_EQ(EACCES, errno);
+}
+
+TEST_F(Listextattr, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
+ /* Listing user extended attributes merely requires read access */
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
+ expect_listxattr();
+
+ ASSERT_EQ(0, extattr_list_file(FULLPATH, ns, NULL, 0))
+ << strerror(errno);
+}
+
+/* Listing system xattrs requires superuser privileges */
+TEST_F(Listextattr, system)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_SYSTEM;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
+ /* Listing user extended attributes merely requires read access */
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
+
+ ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
+ ASSERT_EQ(EPERM, errno);
+}
+
+/* A write by a non-owner should clear a file's SGID bit */
+TEST_F(Fspacectl, clear_sgid)
+{
+ const char FULLPATH[] = "mountpoint/file.txt";
+ const char RELPATH[] = "file.txt";
+ struct stat sb;
+ struct spacectl_range rqsr;
+ uint64_t ino = 42;
+ mode_t oldmode = 02777;
+ mode_t newmode = 0777;
+ off_t fsize = 16;
+ off_t off = 8;
+ off_t len = 8;
+ int fd;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize,
+ 1, UINT64_MAX, 0, 0);
+ expect_open(ino, 0, 1);
+ expect_fallocate(ino, off, len,
+ FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);
+ expect_chmod(ino, newmode, fsize);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ rqsr.r_len = len;
+ rqsr.r_offset = off;
+ EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
+ ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
+ EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
+
+ leak(fd);
+}
+
+/* A write by a non-owner should clear a file's SUID bit */
+TEST_F(Fspacectl, clear_suid)
+{
+ const char FULLPATH[] = "mountpoint/file.txt";
+ const char RELPATH[] = "file.txt";
+ struct stat sb;
+ struct spacectl_range rqsr;
+ uint64_t ino = 42;
+ mode_t oldmode = 04777;
+ mode_t newmode = 0777;
+ off_t fsize = 16;
+ off_t off = 8;
+ off_t len = 8;
+ int fd;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize,
+ 1, UINT64_MAX, 0, 0);
+ expect_open(ino, 0, 1);
+ expect_fallocate(ino, off, len,
+ FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);
+ expect_chmod(ino, newmode, fsize);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ rqsr.r_len = len;
+ rqsr.r_offset = off;
+ EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
+ ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
+ EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
+
+ leak(fd);
+}
+
+/*
+ * fspacectl() of a file without writable permissions should succeed as
+ * long as the file descriptor is writable. This is important when combined
+ * with O_CREAT
+ */
+TEST_F(Fspacectl, posix_fallocate_of_newly_created_file)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct spacectl_range rqsr;
+ const uint64_t ino = 42;
+ off_t off = 8;
+ off_t len = 8;
+ int fd;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_create(RELPATH, ino);
+ expect_fallocate(ino, off, len,
+ FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);
+
+ fd = open(FULLPATH, O_CREAT | O_RDWR, 0);
+ ASSERT_LE(0, fd) << strerror(errno);
+ rqsr.r_len = len;
+ rqsr.r_offset = off;
+ EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
+ leak(fd);
+}
+
+/* A component of the search path lacks execute permissions */
+TEST_F(Lookup, eacces)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/some_file.txt";
+ const char RELDIRPATH[] = "some_dir";
+ uint64_t dir_ino = 42;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELDIRPATH, dir_ino, S_IFDIR | 0700, UINT64_MAX, 0);
+
+ EXPECT_EQ(-1, access(FULLPATH, F_OK));
+ EXPECT_EQ(EACCES, errno);
+}
+
+TEST_F(Open, eacces)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);
+
+ EXPECT_EQ(-1, open(FULLPATH, O_RDWR));
+ EXPECT_EQ(EACCES, errno);
+}
+
+TEST_F(Open, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);
+ expect_open(ino, 0, 1);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ leak(fd);
+}
+
+/* A write by a non-owner should clear a file's SGID bit */
+TEST_F(PosixFallocate, clear_sgid)
+{
+ const char FULLPATH[] = "mountpoint/file.txt";
+ const char RELPATH[] = "file.txt";
+ struct stat sb;
+ uint64_t ino = 42;
+ mode_t oldmode = 02777;
+ mode_t newmode = 0777;
+ off_t fsize = 16;
+ off_t off = 8;
+ off_t len = 8;
+ int fd;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize,
+ 1, UINT64_MAX, 0, 0);
+ expect_open(ino, 0, 1);
+ expect_fallocate(ino, off, len, 0, 0);
+ expect_chmod(ino, newmode, fsize);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
+ EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
+
+ leak(fd);
+}
+
+/* A write by a non-owner should clear a file's SUID bit */
+TEST_F(PosixFallocate, clear_suid)
+{
+ const char FULLPATH[] = "mountpoint/file.txt";
+ const char RELPATH[] = "file.txt";
+ struct stat sb;
+ uint64_t ino = 42;
+ mode_t oldmode = 04777;
+ mode_t newmode = 0777;
+ off_t fsize = 16;
+ off_t off = 8;
+ off_t len = 8;
+ int fd;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize,
+ 1, UINT64_MAX, 0, 0);
+ expect_open(ino, 0, 1);
+ expect_fallocate(ino, off, len, 0, 0);
+ expect_chmod(ino, newmode, fsize);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
+ EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
+
+ leak(fd);
+}
+
+/*
+ * posix_fallocate() of a file without writable permissions should succeed as
+ * long as the file descriptor is writable. This is important when combined
+ * with O_CREAT
+ */
+TEST_F(PosixFallocate, posix_fallocate_of_newly_created_file)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t off = 8;
+ off_t len = 8;
+ int fd;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_create(RELPATH, ino);
+ expect_fallocate(ino, off, len, 0, 0);
+
+ fd = open(FULLPATH, O_CREAT | O_RDWR, 0);
+ ASSERT_LE(0, fd) << strerror(errno);
+ EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno);
+ leak(fd);
+}
+
+TEST_F(Rename, eacces_on_srcdir)
+{
+ const char FULLDST[] = "mountpoint/d/dst";
+ const char RELDST[] = "d/dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ uint64_t ino = 42;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, 0);
+ expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX);
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
+ .Times(AnyNumber())
+ .WillRepeatedly(Invoke(ReturnErrno(ENOENT)));
+
+ ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
+ ASSERT_EQ(EACCES, errno);
+}
+
+TEST_F(Rename, eacces_on_dstdir_for_creating)
+{
+ const char FULLDST[] = "mountpoint/d/dst";
+ const char RELDSTDIR[] = "d";
+ const char RELDST[] = "dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ uint64_t src_ino = 42;
+ uint64_t dstdir_ino = 43;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);
+ expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX);
+ expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX);
+ EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
+ ASSERT_EQ(EACCES, errno);
+}
+
+TEST_F(Rename, eacces_on_dstdir_for_removing)
+{
+ const char FULLDST[] = "mountpoint/d/dst";
+ const char RELDSTDIR[] = "d";
+ const char RELDST[] = "dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ uint64_t src_ino = 42;
+ uint64_t dstdir_ino = 43;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);
+ expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX);
+ expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX);
+ EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
+ ASSERT_EQ(EACCES, errno);
+}
+
+TEST_F(Rename, eperm_on_sticky_srcdir)
+{
+ const char FULLDST[] = "mountpoint/d/dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ uint64_t ino = 42;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0);
+ expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX);
+
+ ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
+ ASSERT_EQ(EPERM, errno);
+}
+
+/*
+ * A user cannot move out a subdirectory that he does not own, because that
+ * would require changing the subdirectory's ".." dirent
+ */
+TEST_F(Rename, eperm_for_subdirectory)
+{
+ const char FULLDST[] = "mountpoint/d/dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELDSTDIR[] = "d";
+ const char RELDST[] = "dst";
+ const char RELSRC[] = "src";
+ uint64_t ino = 42;
+ uint64_t dstdir_ino = 43;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);
+ expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0);
+ expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0777, UINT64_MAX, 0);
+ EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
+ ASSERT_EQ(EACCES, errno);
+}
+
+/*
+ * A user _can_ rename a subdirectory to which he lacks write permissions, if
+ * it will keep the same parent
+ */
+TEST_F(Rename, subdirectory_to_same_dir)
+{
+ const char FULLDST[] = "mountpoint/dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELDST[] = "dst";
+ const char RELSRC[] = "src";
+ uint64_t ino = 42;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);
+ expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0);
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_rename(0);
+
+ ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
+}
+
+TEST_F(Rename, eperm_on_sticky_dstdir)
+{
+ const char FULLDST[] = "mountpoint/d/dst";
+ const char RELDSTDIR[] = "d";
+ const char RELDST[] = "dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ uint64_t src_ino = 42;
+ uint64_t dstdir_ino = 43;
+ uint64_t dst_ino = 44;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);
+ expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX);
+ expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 01777, UINT64_MAX);
+ EXPECT_LOOKUP(dstdir_ino, RELDST)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ out.body.entry.nodeid = dst_ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr.uid = 0;
+ })));
+
+ ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
+ ASSERT_EQ(EPERM, errno);
+}
+
+/* Successfully rename a file, overwriting the destination */
+TEST_F(Rename, ok)
+{
+ const char FULLDST[] = "mountpoint/dst";
+ const char RELDST[] = "dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ // The inode of the already-existing destination file
+ uint64_t dst_ino = 2;
+ uint64_t ino = 42;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, geteuid());
+ expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX);
+ expect_lookup(RELDST, dst_ino, S_IFREG | 0644, UINT64_MAX);
+ expect_rename(0);
+
+ ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
+}
+
+TEST_F(Rename, ok_to_remove_src_because_of_stickiness)
+{
+ const char FULLDST[] = "mountpoint/dst";
+ const char RELDST[] = "dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ uint64_t ino = 42;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0);
+ expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_rename(0);
+
+ ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
+}
+
+// Don't update atime during close after read, if we lack permissions to write
+// that file.
+TEST_F(Read, atime_during_close)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = 100;
+ uint8_t buf[bufsize];
+ const char *CONTENTS = "abcdefgh";
+ ssize_t fsize = sizeof(CONTENTS);
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0755, fsize,
+ 1, UINT64_MAX, 0, 0);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, fsize, fsize, CONTENTS);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ return (in.header.opcode == FUSE_SETATTR);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+ expect_flush(ino, 1, ReturnErrno(0));
+ expect_release(ino, FuseTest::FH);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /* Ensure atime will be different than during lookup */
+ nap();
+
+ ASSERT_EQ(fsize, read(fd, buf, bufsize)) << strerror(errno);
+
+ close(fd);
+}
+
+TEST_F(Setattr, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const mode_t oldmode = 0755;
+ const mode_t newmode = 0644;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid());
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.mode == newmode);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.mode = S_IFREG | newmode;
+ })));
+
+ EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
+}
+
+TEST_F(Setattr, eacces)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const mode_t oldmode = 0755;
+ const mode_t newmode = 0644;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, 0);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_SETATTR);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+
+ EXPECT_NE(0, chmod(FULLPATH, newmode));
+ EXPECT_EQ(EPERM, errno);
+}
+
+/*
+ * ftruncate() of a file without writable permissions should succeed as long as
+ * the file descriptor is writable. This is important when combined with
+ * O_CREAT
+ */
+TEST_F(Setattr, ftruncate_of_newly_created_file)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const mode_t mode = 0000;
+ int fd;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_create(RELPATH, ino);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ (in.body.setattr.valid & FATTR_SIZE));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = ino;
+ out.body.attr.attr.mode = S_IFREG | mode;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+
+ fd = open(FULLPATH, O_CREAT | O_RDWR, 0);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, ftruncate(fd, 100)) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * Setting the sgid bit should fail for an unprivileged user who doesn't belong
+ * to the file's group
+ */
+TEST_F(Setattr, sgid_by_non_group_member)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const mode_t oldmode = 0755;
+ const mode_t newmode = 02755;
+ uid_t uid = geteuid();
+ gid_t gid = excluded_group();
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_SETATTR);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+
+ EXPECT_NE(0, chmod(FULLPATH, newmode));
+ EXPECT_EQ(EPERM, errno);
+}
+
+/* Only the superuser may set the sticky bit on a non-directory */
+TEST_F(Setattr, sticky_regular_file)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const mode_t oldmode = 0644;
+ const mode_t newmode = 01644;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid());
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_SETATTR);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+
+ EXPECT_NE(0, chmod(FULLPATH, newmode));
+ EXPECT_EQ(EFTYPE, errno);
+}
+
+TEST_F(Setextattr, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ const char value[] = "whatever";
+ ssize_t value_len = strlen(value) + 1;
+ int ns = EXTATTR_NAMESPACE_USER;
+ ssize_t r;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
+ expect_setxattr(0);
+
+ r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
+ value_len);
+ ASSERT_EQ(value_len, r) << strerror(errno);
+}
+
+TEST_F(Setextattr, eacces)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ const char value[] = "whatever";
+ ssize_t value_len = strlen(value) + 1;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
+
+ ASSERT_EQ(-1, extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
+ value_len));
+ ASSERT_EQ(EACCES, errno);
+}
+
+// Setting system attributes requires superuser privileges
+TEST_F(Setextattr, system)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ const char value[] = "whatever";
+ ssize_t value_len = strlen(value) + 1;
+ int ns = EXTATTR_NAMESPACE_SYSTEM;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid());
+
+ ASSERT_EQ(-1, extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
+ value_len));
+ ASSERT_EQ(EPERM, errno);
+}
+
+// Setting user attributes merely requires write privileges
+TEST_F(Setextattr, user)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ const char value[] = "whatever";
+ ssize_t value_len = strlen(value) + 1;
+ int ns = EXTATTR_NAMESPACE_USER;
+ ssize_t r;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0);
+ expect_setxattr(0);
+
+ r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
+ value_len);
+ ASSERT_EQ(value_len, r) << strerror(errno);
+}
+
+TEST_F(Unlink, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ sem_t sem;
+
+ ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
+ expect_unlink(FUSE_ROOT_ID, RELPATH, 0);
+ expect_forget(ino, 1, &sem);
+
+ ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno);
+
+ sem_wait(&sem);
+ sem_destroy(&sem);
+}
+
+/*
+ * Ensure that a cached name doesn't cause unlink to bypass permission checks
+ * in VOP_LOOKUP.
+ *
+ * This test should pass because lookup(9) purges the namecache entry by doing
+ * a vfs_cache_lookup with ~MAKEENTRY when nameiop == DELETE.
+ */
+TEST_F(Unlink, cached_unwritable_directory)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .Times(AnyNumber())
+ .WillRepeatedly(Invoke(
+ ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ out.body.entry.nodeid = ino;
+ out.body.entry.entry_valid = UINT64_MAX;
+ }))
+ );
+
+ /* Fill name cache */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+ /* Despite cached name , unlink should fail */
+ ASSERT_EQ(-1, unlink(FULLPATH));
+ ASSERT_EQ(EACCES, errno);
+}
+
+TEST_F(Unlink, unwritable_directory)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
+
+ ASSERT_EQ(-1, unlink(FULLPATH));
+ ASSERT_EQ(EACCES, errno);
+}
+
+TEST_F(Unlink, sticky_directory)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
+
+ ASSERT_EQ(-1, unlink(FULLPATH));
+ ASSERT_EQ(EPERM, errno);
+}
+
+/* A write by a non-owner should clear a file's SUID bit */
+TEST_F(Write, clear_suid)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct stat sb;
+ uint64_t ino = 42;
+ mode_t oldmode = 04777;
+ mode_t newmode = 0777;
+ char wbuf[1] = {'x'};
+ int fd;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX);
+ expect_open(ino, 0, 1);
+ expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf);
+ expect_chmod(ino, newmode, sizeof(wbuf));
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
+ EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
+ leak(fd);
+}
+
+/* A write by a non-owner should clear a file's SGID bit */
+TEST_F(Write, clear_sgid)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct stat sb;
+ uint64_t ino = 42;
+ mode_t oldmode = 02777;
+ mode_t newmode = 0777;
+ char wbuf[1] = {'x'};
+ int fd;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX);
+ expect_open(ino, 0, 1);
+ expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf);
+ expect_chmod(ino, newmode, sizeof(wbuf));
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
+ EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
+ leak(fd);
+}
+
+/* Regression test for a specific recurse-of-nonrecursive-lock panic
+ *
+ * With writeback caching, we can't call vtruncbuf from fuse_io_strategy, or it
+ * may panic. That happens if the FUSE_SETATTR response indicates that the
+ * file's size has changed since the write.
+ */
+TEST_F(Write, recursion_panic_while_clearing_suid)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ mode_t oldmode = 04777;
+ mode_t newmode = 0777;
+ char wbuf[1] = {'x'};
+ int fd;
+
+ expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
+ expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX);
+ expect_open(ino, 0, 1);
+ expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf);
+ /* XXX Return a smaller file size than what we just wrote! */
+ expect_chmod(ino, newmode, 0);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno);
+ leak(fd);
+}