aboutsummaryrefslogtreecommitdiff
path: root/tests/sys/fs/fusefs/setattr.cc
diff options
context:
space:
mode:
Diffstat (limited to 'tests/sys/fs/fusefs/setattr.cc')
-rw-r--r--tests/sys/fs/fusefs/setattr.cc862
1 files changed, 862 insertions, 0 deletions
diff --git a/tests/sys/fs/fusefs/setattr.cc b/tests/sys/fs/fusefs/setattr.cc
new file mode 100644
index 000000000000..79559db33b12
--- /dev/null
+++ b/tests/sys/fs/fusefs/setattr.cc
@@ -0,0 +1,862 @@
+/*-
+ * 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.
+ */
+
+extern "C" {
+#include <sys/types.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+
+#include <fcntl.h>
+#include <semaphore.h>
+#include <signal.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Setattr : public FuseTest {
+public:
+static sig_atomic_t s_sigxfsz;
+};
+
+class RofsSetattr: public Setattr {
+public:
+virtual void SetUp() {
+ s_sigxfsz = 0;
+ m_ro = true;
+ Setattr::SetUp();
+}
+};
+
+class Setattr_7_8: public Setattr {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 8;
+ Setattr::SetUp();
+}
+};
+
+
+sig_atomic_t Setattr::s_sigxfsz = 0;
+
+void sigxfsz_handler(int __unused sig) {
+ Setattr::s_sigxfsz = 1;
+}
+
+/*
+ * If setattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs
+ * should use the cached attributes, rather than query the daemon
+ */
+TEST_F(Setattr, attr_cache)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ struct stat sb;
+ const mode_t newmode = 0644;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillRepeatedly(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 = ino;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino);
+ }, 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_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_GETATTR);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+
+ /* Set an attribute with SETATTR */
+ ASSERT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
+
+ /* The stat(2) should use cached attributes */
+ ASSERT_EQ(0, stat(FULLPATH, &sb));
+ EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
+}
+
+/* Change the mode of a file */
+TEST_F(Setattr, chmod)
+{
+ 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_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFREG | oldmode;
+ out.body.entry.nodeid = ino;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ uint32_t valid = FATTR_MODE;
+ 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;
+ })));
+ EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
+}
+
+/*
+ * Chmod a multiply-linked file with cached attributes. Check that both files'
+ * attributes have changed.
+ */
+TEST_F(Setattr, chmod_multiply_linked)
+{
+ const char FULLPATH0[] = "mountpoint/some_file.txt";
+ const char RELPATH0[] = "some_file.txt";
+ const char FULLPATH1[] = "mountpoint/other_file.txt";
+ const char RELPATH1[] = "other_file.txt";
+ struct stat sb;
+ const uint64_t ino = 42;
+ const mode_t oldmode = 0777;
+ const mode_t newmode = 0666;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH0)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFREG | oldmode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 2;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH1)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFREG | oldmode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 2;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ uint32_t valid = FATTR_MODE;
+ 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;
+ out.body.attr.attr.mode = S_IFREG | newmode;
+ out.body.attr.attr.nlink = 2;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+
+ /* For a lookup of the 2nd file to get it into the cache*/
+ ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno);
+ EXPECT_EQ(S_IFREG | oldmode, sb.st_mode);
+
+ ASSERT_EQ(0, chmod(FULLPATH0, newmode)) << strerror(errno);
+ ASSERT_EQ(0, stat(FULLPATH0, &sb)) << strerror(errno);
+ EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
+ ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno);
+ EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
+}
+
+
+/* Change the owner and group of a file */
+TEST_F(Setattr, chown)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const gid_t oldgroup = 66;
+ const gid_t newgroup = 99;
+ const uid_t olduser = 33;
+ const uid_t newuser = 44;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .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 = ino;
+ out.body.entry.attr.gid = oldgroup;
+ out.body.entry.attr.uid = olduser;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ uint32_t valid = FATTR_GID | FATTR_UID;
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.valid == valid &&
+ in.body.setattr.uid == newuser &&
+ in.body.setattr.gid == newgroup);
+ }, 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 | 0644;
+ out.body.attr.attr.uid = newuser;
+ out.body.attr.attr.gid = newgroup;
+ })));
+ EXPECT_EQ(0, chown(FULLPATH, newuser, newgroup)) << strerror(errno);
+}
+
+
+
+/*
+ * FUSE daemons are allowed to check permissions however they like. If the
+ * daemon returns EPERM, even if the file permissions "should" grant access,
+ * then fuse(4) should return EPERM too.
+ */
+TEST_F(Setattr, eperm)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFREG | 0777;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.uid = in.header.uid;
+ out.body.entry.attr.gid = in.header.gid;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(EPERM)));
+ EXPECT_NE(0, truncate(FULLPATH, 10));
+ EXPECT_EQ(EPERM, errno);
+}
+
+/* Change the mode of an open file, by its file descriptor */
+TEST_F(Setattr, fchmod)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+ const mode_t oldmode = 0755;
+ const mode_t newmode = 0644;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFREG | oldmode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPEN &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ out.header.len = sizeof(out.header);
+ SET_OUT_HEADER_LEN(out, open);
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ uint32_t valid = FATTR_MODE;
+ 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;
+ })));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno);
+ leak(fd);
+}
+
+/* Change the size of an open file, by its file descriptor */
+TEST_F(Setattr, ftruncate)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+ uint64_t fh = 0xdeadbeef1a7ebabe;
+ const off_t oldsize = 99;
+ const off_t newsize = 12345;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFREG | 0755;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.attr.size = oldsize;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPEN &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ out.header.len = sizeof(out.header);
+ SET_OUT_HEADER_LEN(out, open);
+ out.body.open.fh = fh;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ uint32_t valid = FATTR_SIZE | FATTR_FH;
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.valid == valid &&
+ in.body.setattr.fh == fh);
+ }, 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 | 0755;
+ out.body.attr.attr.size = newsize;
+ })));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, ftruncate(fd, newsize)) << strerror(errno);
+ leak(fd);
+}
+
+/* Change the size of the file */
+TEST_F(Setattr, truncate) {
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const uint64_t oldsize = 100'000'000;
+ const uint64_t newsize = 20'000'000;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .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 = ino;
+ out.body.entry.attr.size = oldsize;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ uint32_t valid = FATTR_SIZE;
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.valid == valid &&
+ in.body.setattr.size == newsize);
+ }, 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 | 0644;
+ out.body.attr.attr.size = newsize;
+ })));
+ EXPECT_EQ(0, truncate(FULLPATH, newsize)) << strerror(errno);
+}
+
+/*
+ * Truncating a file should discard cached data past the truncation point.
+ * This is a regression test for bug 233783.
+ *
+ * There are two distinct failure modes. The first one is a failure to zero
+ * the portion of the file's final buffer past EOF. It can be reproduced by
+ * fsx -WR -P /tmp -S10 fsx.bin
+ *
+ * The second is a failure to drop buffers beyond that. It can be reproduced by
+ * fsx -WR -P /tmp -S18 -n fsx.bin
+ * Also reproducible in sh with:
+ * $> /path/to/libfuse/build/example/passthrough -d /tmp/mnt
+ * $> cd /tmp/mnt/tmp
+ * $> dd if=/dev/random of=randfile bs=1k count=192
+ * $> truncate -s 1k randfile && truncate -s 192k randfile
+ * $> xxd randfile | less # xxd will wrongly show random data at offset 0x8000
+ */
+TEST_F(Setattr, truncate_discards_cached_data) {
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ char *w0buf, *r0buf, *r1buf, *expected;
+ off_t w0_offset = 0;
+ size_t w0_size = 0x30000;
+ off_t r0_offset = 0;
+ off_t r0_size = w0_size;
+ size_t trunc0_size = 0x400;
+ size_t trunc1_size = w0_size;
+ off_t r1_offset = trunc0_size;
+ off_t r1_size = w0_size - trunc0_size;
+ size_t cur_size = 0;
+ const uint64_t ino = 42;
+ mode_t mode = S_IFREG | 0644;
+ int fd, r;
+ bool should_have_data = false;
+
+ w0buf = new char[w0_size];
+ memset(w0buf, 'X', w0_size);
+
+ r0buf = new char[r0_size];
+ r1buf = new char[r1_size];
+
+ expected = new char[r1_size]();
+
+ expect_lookup(RELPATH, ino, mode, 0, 1);
+ expect_open(ino, O_RDWR, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([&](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = ino;
+ out.body.attr.attr.mode = mode;
+ out.body.attr.attr.size = cur_size;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_WRITE);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) {
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.attr.attr.ino = ino;
+ out.body.write.size = in.body.write.size;
+ cur_size = std::max(static_cast<uint64_t>(cur_size),
+ in.body.write.size + in.body.write.offset);
+ })));
+
+ 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)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) {
+ auto trunc_size = in.body.setattr.size;
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = ino;
+ out.body.attr.attr.mode = mode;
+ out.body.attr.attr.size = trunc_size;
+ cur_size = trunc_size;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) {
+ auto osize = std::min(
+ static_cast<uint64_t>(cur_size) - in.body.read.offset,
+ static_cast<uint64_t>(in.body.read.size));
+ assert(osize <= sizeof(out.body.bytes));
+ out.header.len = sizeof(struct fuse_out_header) + osize;
+ if (should_have_data)
+ memset(out.body.bytes, 'X', osize);
+ else
+ bzero(out.body.bytes, osize);
+ })));
+
+ fd = open(FULLPATH, O_RDWR, 0644);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /* Fill the file with Xs */
+ ASSERT_EQ(static_cast<ssize_t>(w0_size),
+ pwrite(fd, w0buf, w0_size, w0_offset));
+ should_have_data = true;
+ /* Fill the cache */
+ ASSERT_EQ(static_cast<ssize_t>(r0_size),
+ pread(fd, r0buf, r0_size, r0_offset));
+ /* 1st truncate should discard cached data */
+ EXPECT_EQ(0, ftruncate(fd, trunc0_size)) << strerror(errno);
+ should_have_data = false;
+ /* 2nd truncate extends file into previously cached data */
+ EXPECT_EQ(0, ftruncate(fd, trunc1_size)) << strerror(errno);
+ /* Read should return all zeros */
+ ASSERT_EQ(static_cast<ssize_t>(r1_size),
+ pread(fd, r1buf, r1_size, r1_offset));
+
+ r = memcmp(expected, r1buf, r1_size);
+ ASSERT_EQ(0, r);
+
+ delete[] expected;
+ delete[] r1buf;
+ delete[] r0buf;
+ delete[] w0buf;
+
+ leak(fd);
+}
+
+/* truncate should fail if it would cause the file to exceed RLIMIT_FSIZE */
+TEST_F(Setattr, truncate_rlimit_rsize)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct rlimit rl;
+ const uint64_t ino = 42;
+ const uint64_t oldsize = 0;
+ const uint64_t newsize = 100'000'000;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .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 = ino;
+ out.body.entry.attr.size = oldsize;
+ })));
+
+ rl.rlim_cur = newsize / 2;
+ rl.rlim_max = 10 * newsize;
+ ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno);
+ ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno);
+
+ EXPECT_EQ(-1, truncate(FULLPATH, newsize));
+ EXPECT_EQ(EFBIG, errno);
+ EXPECT_EQ(1, s_sigxfsz);
+}
+
+/* Change a file's timestamps */
+TEST_F(Setattr, utimensat) {
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const timespec oldtimes[2] = {
+ {.tv_sec = 1, .tv_nsec = 2},
+ {.tv_sec = 3, .tv_nsec = 4},
+ };
+ const timespec newtimes[2] = {
+ {.tv_sec = 5, .tv_nsec = 6},
+ {.tv_sec = 7, .tv_nsec = 8},
+ };
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .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 = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.attr.atime = oldtimes[0].tv_sec;
+ out.body.entry.attr.atimensec = oldtimes[0].tv_nsec;
+ out.body.entry.attr.mtime = oldtimes[1].tv_sec;
+ out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ uint32_t valid = FATTR_ATIME | FATTR_MTIME;
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.valid == valid &&
+ (time_t)in.body.setattr.atime ==
+ newtimes[0].tv_sec &&
+ (long)in.body.setattr.atimensec ==
+ newtimes[0].tv_nsec &&
+ (time_t)in.body.setattr.mtime ==
+ newtimes[1].tv_sec &&
+ (long)in.body.setattr.mtimensec ==
+ newtimes[1].tv_nsec);
+ }, 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 | 0644;
+ out.body.attr.attr.atime = newtimes[0].tv_sec;
+ out.body.attr.attr.atimensec = newtimes[0].tv_nsec;
+ out.body.attr.attr.mtime = newtimes[1].tv_sec;
+ out.body.attr.attr.mtimensec = newtimes[1].tv_nsec;
+ })));
+ EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0))
+ << strerror(errno);
+}
+
+/* Change a file mtime but not its atime */
+TEST_F(Setattr, utimensat_mtime_only) {
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const timespec oldtimes[2] = {
+ {.tv_sec = 1, .tv_nsec = 2},
+ {.tv_sec = 3, .tv_nsec = 4},
+ };
+ const timespec newtimes[2] = {
+ {.tv_sec = 5, .tv_nsec = UTIME_OMIT},
+ {.tv_sec = 7, .tv_nsec = 8},
+ };
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .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 = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.attr.atime = oldtimes[0].tv_sec;
+ out.body.entry.attr.atimensec = oldtimes[0].tv_nsec;
+ out.body.entry.attr.mtime = oldtimes[1].tv_sec;
+ out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ uint32_t valid = FATTR_MTIME;
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.valid == valid &&
+ (time_t)in.body.setattr.mtime ==
+ newtimes[1].tv_sec &&
+ (long)in.body.setattr.mtimensec ==
+ newtimes[1].tv_nsec);
+ }, 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 | 0644;
+ out.body.attr.attr.atime = oldtimes[0].tv_sec;
+ out.body.attr.attr.atimensec = oldtimes[0].tv_nsec;
+ out.body.attr.attr.mtime = newtimes[1].tv_sec;
+ out.body.attr.attr.mtimensec = newtimes[1].tv_nsec;
+ })));
+ EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0))
+ << strerror(errno);
+}
+
+/*
+ * Set a file's mtime and atime to now
+ *
+ * The design of FreeBSD's VFS does not allow fusefs to set just one of atime
+ * or mtime to UTIME_NOW; it's both or neither.
+ */
+TEST_F(Setattr, utimensat_utime_now) {
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const timespec oldtimes[2] = {
+ {.tv_sec = 1, .tv_nsec = 2},
+ {.tv_sec = 3, .tv_nsec = 4},
+ };
+ const timespec newtimes[2] = {
+ {.tv_sec = 0, .tv_nsec = UTIME_NOW},
+ {.tv_sec = 0, .tv_nsec = UTIME_NOW},
+ };
+ /* "now" is whatever the server says it is */
+ const timespec now[2] = {
+ {.tv_sec = 5, .tv_nsec = 7},
+ {.tv_sec = 6, .tv_nsec = 8},
+ };
+ struct stat sb;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .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 = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr.atime = oldtimes[0].tv_sec;
+ out.body.entry.attr.atimensec = oldtimes[0].tv_nsec;
+ out.body.entry.attr.mtime = oldtimes[1].tv_sec;
+ out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ uint32_t valid = FATTR_ATIME | FATTR_ATIME_NOW |
+ FATTR_MTIME | FATTR_MTIME_NOW;
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.valid == valid);
+ }, 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 | 0644;
+ out.body.attr.attr.atime = now[0].tv_sec;
+ out.body.attr.attr.atimensec = now[0].tv_nsec;
+ out.body.attr.attr.mtime = now[1].tv_sec;
+ out.body.attr.attr.mtimensec = now[1].tv_nsec;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+ ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0))
+ << strerror(errno);
+ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
+ EXPECT_EQ(now[0].tv_sec, sb.st_atim.tv_sec);
+ EXPECT_EQ(now[0].tv_nsec, sb.st_atim.tv_nsec);
+ EXPECT_EQ(now[1].tv_sec, sb.st_mtim.tv_sec);
+ EXPECT_EQ(now[1].tv_nsec, sb.st_mtim.tv_nsec);
+}
+
+/*
+ * FUSE_SETATTR returns a different file type, even though the entry cache
+ * hasn't expired. This is a server bug! It probably means that the server
+ * removed the file and recreated it with the same inode but a different vtyp.
+ * The best thing fusefs can do is return ENOENT to the caller. After all, the
+ * entry must not have existed recently.
+ */
+TEST_F(Setattr, vtyp_conflict)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ uid_t newuser = 12345;
+ sem_t sem;
+
+ ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFREG | 0777;
+ out.body.entry.nodeid = ino;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino);
+ }, 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_IFDIR | 0777; // Changed!
+ out.body.attr.attr.uid = newuser;
+ })));
+ // We should reclaim stale vnodes
+ expect_forget(ino, 1, &sem);
+
+ EXPECT_NE(0, chown(FULLPATH, newuser, -1));
+ EXPECT_EQ(ENOENT, errno);
+
+ sem_wait(&sem);
+ sem_destroy(&sem);
+}
+
+/* On a read-only mount, no attributes may be changed */
+TEST_F(RofsSetattr, erofs)
+{
+ 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_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFREG | oldmode;
+ out.body.entry.nodeid = ino;
+ })));
+
+ ASSERT_EQ(-1, chmod(FULLPATH, newmode));
+ ASSERT_EQ(EROFS, errno);
+}
+
+/* Change the mode of a file */
+TEST_F(Setattr_7_8, chmod)
+{
+ 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_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry_7_8);
+ out.body.entry.attr.mode = S_IFREG | oldmode;
+ out.body.entry.nodeid = ino;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ uint32_t valid = FATTR_MODE;
+ 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_7_8);
+ out.body.attr.attr.ino = ino; // Must match nodeid
+ out.body.attr.attr.mode = S_IFREG | newmode;
+ })));
+ EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
+}