aboutsummaryrefslogtreecommitdiff
path: root/tests/sys/fs/fusefs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/sys/fs/fusefs')
-rw-r--r--tests/sys/fs/fusefs/Makefile100
-rw-r--r--tests/sys/fs/fusefs/access.cc301
-rw-r--r--tests/sys/fs/fusefs/allow_other.cc306
-rw-r--r--tests/sys/fs/fusefs/bad_server.cc108
-rw-r--r--tests/sys/fs/fusefs/bmap.cc353
-rw-r--r--tests/sys/fs/fusefs/cache.cc218
-rw-r--r--tests/sys/fs/fusefs/copy_file_range.cc802
-rw-r--r--tests/sys/fs/fusefs/create.cc493
-rw-r--r--tests/sys/fs/fusefs/ctl.sh69
-rw-r--r--tests/sys/fs/fusefs/default_permissions.cc1644
-rw-r--r--tests/sys/fs/fusefs/default_permissions_privileged.cc124
-rw-r--r--tests/sys/fs/fusefs/destroy.cc157
-rw-r--r--tests/sys/fs/fusefs/dev_fuse_poll.cc229
-rw-r--r--tests/sys/fs/fusefs/fallocate.cc779
-rw-r--r--tests/sys/fs/fusefs/fifo.cc211
-rw-r--r--tests/sys/fs/fusefs/flush.cc272
-rw-r--r--tests/sys/fs/fusefs/forget.cc177
-rw-r--r--tests/sys/fs/fusefs/fsync.cc285
-rw-r--r--tests/sys/fs/fusefs/fsyncdir.cc195
-rw-r--r--tests/sys/fs/fusefs/getattr.cc367
-rw-r--r--tests/sys/fs/fusefs/interrupt.cc796
-rw-r--r--tests/sys/fs/fusefs/io.cc609
-rw-r--r--tests/sys/fs/fusefs/last_local_modify.cc521
-rw-r--r--tests/sys/fs/fusefs/link.cc280
-rw-r--r--tests/sys/fs/fusefs/locks.cc730
-rw-r--r--tests/sys/fs/fusefs/lookup.cc662
-rw-r--r--tests/sys/fs/fusefs/lseek.cc518
-rw-r--r--tests/sys/fs/fusefs/mkdir.cc274
-rw-r--r--tests/sys/fs/fusefs/mknod.cc318
-rw-r--r--tests/sys/fs/fusefs/mockfs.cc1063
-rw-r--r--tests/sys/fs/fusefs/mockfs.hh446
-rw-r--r--tests/sys/fs/fusefs/mount.cc200
-rw-r--r--tests/sys/fs/fusefs/nfs.cc480
-rw-r--r--tests/sys/fs/fusefs/notify.cc602
-rw-r--r--tests/sys/fs/fusefs/open.cc307
-rw-r--r--tests/sys/fs/fusefs/opendir.cc197
-rw-r--r--tests/sys/fs/fusefs/pre-init.cc226
-rw-r--r--tests/sys/fs/fusefs/read.cc1497
-rw-r--r--tests/sys/fs/fusefs/readdir.cc516
-rw-r--r--tests/sys/fs/fusefs/readlink.cc162
-rw-r--r--tests/sys/fs/fusefs/release.cc289
-rw-r--r--tests/sys/fs/fusefs/releasedir.cc116
-rw-r--r--tests/sys/fs/fusefs/rename.cc330
-rw-r--r--tests/sys/fs/fusefs/rmdir.cc162
-rw-r--r--tests/sys/fs/fusefs/setattr.cc862
-rw-r--r--tests/sys/fs/fusefs/statfs.cc171
-rw-r--r--tests/sys/fs/fusefs/symlink.cc208
-rw-r--r--tests/sys/fs/fusefs/unlink.cc237
-rw-r--r--tests/sys/fs/fusefs/utils.cc673
-rw-r--r--tests/sys/fs/fusefs/utils.hh277
-rw-r--r--tests/sys/fs/fusefs/write.cc1662
-rw-r--r--tests/sys/fs/fusefs/xattr.cc891
52 files changed, 23472 insertions, 0 deletions
diff --git a/tests/sys/fs/fusefs/Makefile b/tests/sys/fs/fusefs/Makefile
new file mode 100644
index 000000000000..a21512798597
--- /dev/null
+++ b/tests/sys/fs/fusefs/Makefile
@@ -0,0 +1,100 @@
+.include <bsd.compiler.mk>
+
+PACKAGE= tests
+
+TESTSDIR= ${TESTSBASE}/sys/fs/fusefs
+
+ATF_TESTS_SH+= ctl
+
+# We could simply link all of these files into a single executable. But since
+# Kyua treats googletest programs as plain tests, it's better to separate them
+# out, so we get more granular reporting.
+GTESTS+= access
+GTESTS+= allow_other
+GTESTS+= bad_server
+GTESTS+= bmap
+GTESTS+= cache
+GTESTS+= copy_file_range
+GTESTS+= create
+GTESTS+= default_permissions
+GTESTS+= default_permissions_privileged
+GTESTS+= destroy
+GTESTS+= dev_fuse_poll
+GTESTS+= fallocate
+GTESTS+= fifo
+GTESTS+= flush
+GTESTS+= forget
+GTESTS+= fsync
+GTESTS+= fsyncdir
+GTESTS+= getattr
+GTESTS+= interrupt
+GTESTS+= io
+GTESTS+= last_local_modify
+GTESTS+= link
+GTESTS+= locks
+GTESTS+= lookup
+GTESTS+= lseek
+GTESTS+= mkdir
+GTESTS+= mknod
+GTESTS+= mount
+GTESTS+= nfs
+GTESTS+= notify
+GTESTS+= open
+GTESTS+= opendir
+GTESTS+= pre-init
+GTESTS+= read
+GTESTS+= readdir
+GTESTS+= readlink
+GTESTS+= release
+GTESTS+= releasedir
+GTESTS+= rename
+GTESTS+= rmdir
+GTESTS+= setattr
+GTESTS+= statfs
+GTESTS+= symlink
+GTESTS+= unlink
+GTESTS+= write
+GTESTS+= xattr
+
+.for p in ${GTESTS}
+SRCS.$p+= ${p}.cc
+SRCS.$p+= mockfs.cc
+SRCS.$p+= utils.cc
+.endfor
+
+TEST_METADATA.default_permissions+= required_user="unprivileged"
+TEST_METADATA.default_permissions_privileged+= required_user="root"
+TEST_METADATA.mknod+= required_user="root"
+TEST_METADATA.nfs+= required_user="root"
+# ctl must be exclusive because it disables/enables camsim
+TEST_METADATA.ctl+= is_exclusive="true"
+TEST_METADATA.ctl+= required_user="root"
+
+TEST_METADATA+= timeout=10
+TEST_METADATA+= required_kmods="fusefs"
+
+FUSEFS= ${SRCTOP}/sys/fs/fuse
+# Suppress warnings that GCC generates for the libc++ and gtest headers.
+CXXWARNFLAGS.gcc+= -Wno-placement-new -Wno-attributes
+# Suppress Wcast-align for readdir.cc, because it is unavoidable when using
+# getdirentries.
+CXXWARNFLAGS.readdir.cc+= -Wno-cast-align
+.if ${COMPILER_TYPE} == "gcc" && ${COMPILER_VERSION} >= 80000
+CXXWARNFLAGS+= -Wno-class-memaccess
+.endif
+# Suppress false warning about set but unused DNAME in inval_entry_below_root
+CXXWARNFLAGS.gcc+= -Wno-unused-but-set-variable
+# Suppress warnings about deprecated implicit copy constructors in gtest.
+CXXWARNFLAGS+= -Wno-deprecated-copy
+.if ${COMPILER_TYPE} == "clang" && ${COMPILER_VERSION} >= 180000
+# clang 18.0.0 introduces a new warning about variable length arrays in C++.
+CXXWARNFLAGS+= -Wno-vla-cxx-extension
+.endif
+CXXFLAGS+= -I${SRCTOP}/tests
+CXXFLAGS+= -I${FUSEFS}
+
+LIBADD+= pthread
+LIBADD+= gmock gtest
+LIBADD+= util
+
+.include <bsd.test.mk>
diff --git a/tests/sys/fs/fusefs/access.cc b/tests/sys/fs/fusefs/access.cc
new file mode 100644
index 000000000000..5762269fac7b
--- /dev/null
+++ b/tests/sys/fs/fusefs/access.cc
@@ -0,0 +1,301 @@
+/*-
+ * 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/extattr.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Access: public FuseTest {
+public:
+virtual void SetUp() {
+ FuseTest::SetUp();
+ // Clear the default FUSE_ACCESS expectation
+ Mock::VerifyAndClearExpectations(m_mock);
+}
+
+void expect_lookup(const char *relpath, uint64_t ino)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1);
+}
+
+/*
+ * Expect that FUSE_ACCESS will never be called for the given inode, with any
+ * bits in the supplied access_mask set
+ */
+void expect_noaccess(uint64_t ino, mode_t access_mask)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_ACCESS &&
+ in.header.nodeid == ino &&
+ in.body.access.mask & access_mask);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+}
+
+};
+
+class RofsAccess: public Access {
+public:
+virtual void SetUp() {
+ m_ro = true;
+ Access::SetUp();
+}
+};
+
+/*
+ * Change the mode of a file.
+ *
+ * There should never be a FUSE_ACCESS sent for this operation, except for
+ * search permissions on the parent directory.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689
+ */
+TEST_F(Access, chmod)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const mode_t newmode = 0644;
+
+ expect_access(FUSE_ROOT_ID, X_OK, 0);
+ expect_lookup(RELPATH, ino);
+ expect_noaccess(ino, 0);
+ 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;
+ })));
+
+ EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
+}
+
+/*
+ * Create a new file
+ *
+ * There should never be a FUSE_ACCESS sent for this operation, except for
+ * search permissions on the parent directory.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689
+ */
+TEST_F(Access, create)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = S_IFREG | 0755;
+ uint64_t ino = 42;
+
+ expect_access(FUSE_ROOT_ID, X_OK, 0);
+ expect_noaccess(FUSE_ROOT_ID, R_OK | W_OK);
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_noaccess(ino, 0);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_CREATE);
+ }, Eq(true)),
+ _)
+ ).WillOnce(ReturnErrno(EPERM));
+
+ EXPECT_EQ(-1, open(FULLPATH, O_CREAT | O_EXCL, mode));
+ EXPECT_EQ(EPERM, errno);
+}
+
+/* The error case of FUSE_ACCESS. */
+TEST_F(Access, eaccess)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ mode_t access_mode = X_OK;
+
+ expect_access(FUSE_ROOT_ID, X_OK, 0);
+ expect_lookup(RELPATH, ino);
+ expect_access(ino, access_mode, EACCES);
+
+ ASSERT_NE(0, access(FULLPATH, access_mode));
+ ASSERT_EQ(EACCES, errno);
+}
+
+/*
+ * If the filesystem returns ENOSYS, then it is treated as a permanent success,
+ * and subsequent VOP_ACCESS calls will succeed automatically without querying
+ * the daemon.
+ */
+TEST_F(Access, enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ mode_t access_mode = R_OK;
+
+ expect_access(FUSE_ROOT_ID, X_OK, ENOSYS);
+ FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
+
+ ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
+ ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
+}
+
+TEST_F(RofsAccess, erofs)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ mode_t access_mode = W_OK;
+
+ expect_access(FUSE_ROOT_ID, X_OK, 0);
+ expect_lookup(RELPATH, ino);
+
+ ASSERT_NE(0, access(FULLPATH, access_mode));
+ ASSERT_EQ(EROFS, errno);
+}
+
+
+/*
+ * Lookup an extended attribute
+ *
+ * There should never be a FUSE_ACCESS sent for this operation, except for
+ * search permissions on the parent directory.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689
+ */
+TEST_F(Access, Getxattr)
+{
+ 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;
+ ssize_t r;
+
+ expect_access(FUSE_ROOT_ID, X_OK, 0);
+ expect_lookup(RELPATH, ino);
+ expect_noaccess(ino, 0);
+ expect_getxattr(ino, "user.foo", ReturnErrno(ENOATTR));
+
+ r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
+ ASSERT_EQ(-1, r);
+ ASSERT_EQ(ENOATTR, errno);
+}
+
+/* The successful case of FUSE_ACCESS. */
+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_access(FUSE_ROOT_ID, X_OK, 0);
+ expect_lookup(RELPATH, ino);
+ expect_access(ino, access_mode, 0);
+
+ ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
+}
+
+/*
+ * Unlink a file
+ *
+ * There should never be a FUSE_ACCESS sent for this operation, except for
+ * search permissions on the parent directory.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689
+ */
+TEST_F(Access, unlink)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+
+ expect_access(FUSE_ROOT_ID, X_OK, 0);
+ expect_noaccess(FUSE_ROOT_ID, W_OK | R_OK);
+ expect_noaccess(ino, 0);
+ expect_lookup(RELPATH, ino);
+ expect_unlink(1, RELPATH, EPERM);
+
+ ASSERT_NE(0, unlink(FULLPATH));
+ ASSERT_EQ(EPERM, errno);
+}
+
+/*
+ * Unlink a file whose parent diretory's sticky bit is set
+ *
+ * There should never be a FUSE_ACCESS sent for this operation, except for
+ * search permissions on the parent directory.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689
+ */
+TEST_F(Access, unlink_sticky_directory)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+
+ expect_access(FUSE_ROOT_ID, X_OK, 0);
+ expect_noaccess(FUSE_ROOT_ID, W_OK | R_OK);
+ expect_noaccess(ino, 0);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == FUSE_ROOT_ID);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out)
+ {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = FUSE_ROOT_ID;
+ out.body.attr.attr.mode = S_IFDIR | 01777;
+ out.body.attr.attr.uid = 0;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_ACCESS &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+ expect_lookup(RELPATH, ino);
+ expect_unlink(FUSE_ROOT_ID, RELPATH, EPERM);
+
+ ASSERT_EQ(-1, unlink(FULLPATH));
+ ASSERT_EQ(EPERM, errno);
+}
diff --git a/tests/sys/fs/fusefs/allow_other.cc b/tests/sys/fs/fusefs/allow_other.cc
new file mode 100644
index 000000000000..24a161166a90
--- /dev/null
+++ b/tests/sys/fs/fusefs/allow_other.cc
@@ -0,0 +1,306 @@
+/*-
+ * 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 "allow_other" mount option. They must be in their own
+ * file so they can be run as root
+ */
+
+extern "C" {
+#include <sys/types.h>
+#include <sys/extattr.h>
+#include <sys/wait.h>
+#include <fcntl.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+const static char FULLPATH[] = "mountpoint/some_file.txt";
+const static char RELPATH[] = "some_file.txt";
+
+class NoAllowOther: public FuseTest {
+
+public:
+virtual void SetUp() {
+ if (geteuid() != 0) {
+ GTEST_SKIP() << "This test must be run as root";
+ }
+
+ FuseTest::SetUp();
+}
+};
+
+class AllowOther: public NoAllowOther {
+
+public:
+virtual void SetUp() {
+ m_allow_other = true;
+ NoAllowOther::SetUp();
+}
+};
+
+TEST_F(AllowOther, allowed)
+{
+ int status;
+
+ fork(true, &status, [&] {
+ uint64_t ino = 42;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_open(ino, 0, 1);
+ expect_flush(ino, 1, ReturnErrno(0));
+ expect_release(ino, FH);
+ }, []() {
+ int fd;
+
+ fd = open(FULLPATH, O_RDONLY);
+ if (fd < 0) {
+ perror("open");
+ return(1);
+ }
+
+ leak(fd);
+ return 0;
+ }
+ );
+ ASSERT_EQ(0, WEXITSTATUS(status));
+}
+
+/* Check that fusefs uses the correct credentials for FUSE operations */
+TEST_F(AllowOther, creds)
+{
+ int status;
+ uid_t uid;
+ gid_t gid;
+
+ get_unprivileged_id(&uid, &gid);
+ fork(true, &status, [=] {
+ EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LOOKUP &&
+ in.header.uid == uid &&
+ in.header.gid == gid);
+ }, Eq(true)),
+ _)
+ ).Times(1)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ }, []() {
+ eaccess(FULLPATH, F_OK);
+ return 0;
+ }
+ );
+ ASSERT_EQ(0, WEXITSTATUS(status));
+}
+
+/*
+ * A variation of the Open.multiple_creds test showing how the bug can lead to a
+ * privilege elevation. The first process is privileged and opens a file only
+ * visible to root. The second process is unprivileged and shouldn't be able
+ * to open the file, but does thanks to the bug
+ */
+TEST_F(AllowOther, privilege_escalation)
+{
+ int fd1, status;
+ const static uint64_t ino = 42;
+ const static uint64_t fh = 100;
+
+ /* Fork a child to open the file with different credentials */
+ fork(true, &status, [&] {
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0600, 0, 2);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPEN &&
+ in.header.pid == (uint32_t)getpid() &&
+ in.header.uid == (uint32_t)geteuid() &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(
+ ReturnImmediate([](auto in __unused, auto& out) {
+ out.body.open.fh = fh;
+ out.header.len = sizeof(out.header);
+ SET_OUT_HEADER_LEN(out, open);
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPEN &&
+ in.header.pid != (uint32_t)getpid() &&
+ in.header.uid != (uint32_t)geteuid() &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(AnyNumber())
+ .WillRepeatedly(Invoke(ReturnErrno(EPERM)));
+
+ fd1 = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd1) << strerror(errno);
+ }, [] {
+ int fd0;
+
+ fd0 = open(FULLPATH, O_RDONLY);
+ if (fd0 >= 0) {
+ fprintf(stderr, "Privilege escalation!\n");
+ return 1;
+ }
+ if (errno != EPERM) {
+ fprintf(stderr, "Unexpected error %s\n",
+ strerror(errno));
+ return 1;
+ }
+ leak(fd0);
+ return 0;
+ }
+ );
+ ASSERT_EQ(0, WEXITSTATUS(status));
+ leak(fd1);
+}
+
+TEST_F(NoAllowOther, disallowed)
+{
+ int status;
+
+ fork(true, &status, [] {
+ }, []() {
+ int fd;
+
+ fd = open(FULLPATH, O_RDONLY);
+ if (fd >= 0) {
+ fprintf(stderr, "open should've failed\n");
+ leak(fd);
+ return(1);
+ } else if (errno != EPERM) {
+ fprintf(stderr, "Unexpected error: %s\n",
+ strerror(errno));
+ return(1);
+ }
+ return 0;
+ }
+ );
+ ASSERT_EQ(0, WEXITSTATUS(status));
+}
+
+/*
+ * When -o allow_other is not used, users other than the owner aren't allowed
+ * to open anything inside of the mount point, not just the mountpoint itself
+ * This is a regression test for bug 237052
+ */
+TEST_F(NoAllowOther, disallowed_beneath_root)
+{
+ const static char RELPATH2[] = "other_dir";
+ const static uint64_t ino = 42;
+ const static uint64_t ino2 = 43;
+ int dfd, status;
+
+ expect_lookup(RELPATH, ino, S_IFDIR | 0755, 0, 1);
+ EXPECT_LOOKUP(ino, RELPATH2)
+ .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 = ino2;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ })));
+ expect_opendir(ino);
+ dfd = open(FULLPATH, O_DIRECTORY);
+ ASSERT_LE(0, dfd) << strerror(errno);
+
+ fork(true, &status, [] {
+ }, [&]() {
+ int fd;
+
+ fd = openat(dfd, RELPATH2, O_RDONLY);
+ if (fd >= 0) {
+ fprintf(stderr, "openat should've failed\n");
+ leak(fd);
+ return(1);
+ } else if (errno != EPERM) {
+ fprintf(stderr, "Unexpected error: %s\n",
+ strerror(errno));
+ return(1);
+ }
+ return 0;
+ }
+ );
+ ASSERT_EQ(0, WEXITSTATUS(status));
+
+ leak(dfd);
+}
+
+/*
+ * Provide coverage for the extattr methods, which have a slightly different
+ * code path
+ */
+TEST_F(NoAllowOther, setextattr)
+{
+ int ino = 42, status;
+
+ fork(true, &status, [&] {
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ out.body.entry.nodeid = ino;
+ })));
+
+ /*
+ * lookup the file to get it into the cache.
+ * Otherwise, the unprivileged lookup will fail with
+ * EACCES
+ */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+ }, [&]() {
+ const char value[] = "whatever";
+ ssize_t value_len = strlen(value) + 1;
+ int ns = EXTATTR_NAMESPACE_USER;
+ ssize_t r;
+
+ r = extattr_set_file(FULLPATH, ns, "foo",
+ (const void*)value, value_len);
+ if (r >= 0) {
+ fprintf(stderr, "should've failed\n");
+ return(1);
+ } else if (errno != EPERM) {
+ fprintf(stderr, "Unexpected error: %s\n",
+ strerror(errno));
+ return(1);
+ }
+ return 0;
+ }
+ );
+ ASSERT_EQ(0, WEXITSTATUS(status));
+}
diff --git a/tests/sys/fs/fusefs/bad_server.cc b/tests/sys/fs/fusefs/bad_server.cc
new file mode 100644
index 000000000000..825523cac2bb
--- /dev/null
+++ b/tests/sys/fs/fusefs/bad_server.cc
@@ -0,0 +1,108 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2023 Axcient
+ *
+ * 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 <fcntl.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class BadServer: public FuseTest {};
+
+/*
+ * If the server sends a response for an unknown request, the kernel should
+ * gracefully return EINVAL.
+ */
+TEST_F(BadServer, UnknownUnique)
+{
+ mockfs_buf_out out;
+
+ out.header.len = sizeof(out.header);
+ out.header.error = 0;
+ out.header.unique = 99999; // Invalid!
+ out.expected_errno = EINVAL;
+ m_mock->write_response(out);
+}
+
+/*
+ * If the server sends less than a header's worth of data, the kernel should
+ * gracefully return EINVAL.
+ */
+TEST_F(BadServer, ShortWrite)
+{
+ mockfs_buf_out out;
+
+ out.header.len = sizeof(out.header) - 1;
+ out.header.error = 0;
+ out.header.unique = 0; // Asynchronous notification
+ out.expected_errno = EINVAL;
+ /*
+ * Tell the event loop to quit. The kernel will disconnect us
+ * because of the short write.
+ */
+ m_mock->m_expect_unmount = true;
+ m_mock->write_response(out);
+}
+
+/*
+ * It is illegal to report an error, and also send back a payload.
+ */
+TEST_F(BadServer, ErrorWithPayload)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke([&](auto in, auto &out) {
+ // First send an invalid response
+ std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
+ out0->header.unique = in.header.unique;
+ out0->header.error = -ENOENT;
+ SET_OUT_HEADER_LEN(*out0, entry); // Invalid!
+ out0->expected_errno = EINVAL;
+ out.push_back(std::move(out0));
+
+ // Then, respond to the lookup so we can complete the test
+ std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
+ out1->header.unique = in.header.unique;
+ out1->header.error = -ENOENT;
+ out1->header.len = sizeof(out1->header);
+ out.push_back(std::move(out1));
+
+ // The kernel may disconnect us for bad behavior, so don't try
+ // to read or write any more.
+ m_mock->m_quit = true;
+ }));
+
+ EXPECT_NE(0, access(FULLPATH, F_OK));
+
+ EXPECT_EQ(ENOENT, errno);
+}
diff --git a/tests/sys/fs/fusefs/bmap.cc b/tests/sys/fs/fusefs/bmap.cc
new file mode 100644
index 000000000000..e61dadb6d79e
--- /dev/null
+++ b/tests/sys/fs/fusefs/bmap.cc
@@ -0,0 +1,353 @@
+/*-
+ * 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/param.h>
+#include <sys/ioctl.h>
+#include <sys/filio.h>
+
+#include <fcntl.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+const static char FULLPATH[] = "mountpoint/foo";
+const static char RELPATH[] = "foo";
+
+class Bmap: public FuseTest {
+public:
+virtual void SetUp() {
+ m_maxreadahead = UINT32_MAX;
+ FuseTest::SetUp();
+}
+void expect_bmap(uint64_t ino, uint64_t lbn, uint32_t blocksize, uint64_t pbn)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_BMAP &&
+ in.header.nodeid == ino &&
+ in.body.bmap.block == lbn &&
+ in.body.bmap.blocksize == blocksize);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, bmap);
+ out.body.bmap.block = pbn;
+ })));
+}
+
+void expect_lookup(const char *relpath, uint64_t ino, off_t size)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, size, 1,
+ UINT64_MAX);
+}
+};
+
+class BmapEof: public Bmap, public WithParamInterface<int> {};
+
+/*
+ * Test FUSE_BMAP
+ */
+TEST_F(Bmap, bmap)
+{
+ struct fiobmap2_arg arg;
+ /*
+ * Pick fsize and lbn large enough that max length runs won't reach
+ * either beginning or end of file
+ */
+ const off_t filesize = 1 << 30;
+ int64_t lbn = 100;
+ int64_t pbn = 12345;
+ const ino_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, 42, filesize);
+ expect_open(ino, 0, 1);
+ expect_bmap(ino, lbn, m_maxbcachebuf, pbn);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ arg.bn = lbn;
+ arg.runp = -1;
+ arg.runb = -1;
+ ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno);
+ EXPECT_EQ(arg.bn, pbn);
+ /*
+ * XXX The FUSE protocol does not include the runp and runb variables,
+ * so those must be guessed in-kernel. There's no "right" answer, so
+ * just check that they're within reasonable limits.
+ */
+ EXPECT_LE(arg.runb, lbn);
+ EXPECT_LE((unsigned long)arg.runb, m_maxreadahead / m_maxbcachebuf);
+ EXPECT_LE((unsigned long)arg.runb, m_maxphys / m_maxbcachebuf);
+ EXPECT_GT(arg.runb, 0);
+ EXPECT_LE(arg.runp, filesize / m_maxbcachebuf - lbn);
+ EXPECT_LE((unsigned long)arg.runp, m_maxreadahead / m_maxbcachebuf);
+ EXPECT_LE((unsigned long)arg.runp, m_maxphys / m_maxbcachebuf);
+ EXPECT_GT(arg.runp, 0);
+
+ leak(fd);
+}
+
+/*
+ * If the daemon does not implement VOP_BMAP, fusefs should return sensible
+ * defaults.
+ */
+TEST_F(Bmap, default_)
+{
+ struct fiobmap2_arg arg;
+ const off_t filesize = 1 << 30;
+ const ino_t ino = 42;
+ int64_t lbn;
+ int fd;
+
+ expect_lookup(RELPATH, 42, filesize);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_BMAP);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENOSYS)));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /* First block */
+ lbn = 0;
+ arg.bn = lbn;
+ arg.runp = -1;
+ arg.runb = -1;
+ ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno);
+ EXPECT_EQ(arg.bn, 0);
+ EXPECT_EQ((unsigned long )arg.runp, m_maxphys / m_maxbcachebuf - 1);
+ EXPECT_EQ(arg.runb, 0);
+
+ /* In the middle */
+ lbn = filesize / m_maxbcachebuf / 2;
+ arg.bn = lbn;
+ arg.runp = -1;
+ arg.runb = -1;
+ ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno);
+ EXPECT_EQ(arg.bn, lbn * m_maxbcachebuf / DEV_BSIZE);
+ EXPECT_EQ((unsigned long )arg.runp, m_maxphys / m_maxbcachebuf - 1);
+ EXPECT_EQ((unsigned long )arg.runb, m_maxphys / m_maxbcachebuf - 1);
+
+ /* Last block */
+ lbn = filesize / m_maxbcachebuf - 1;
+ arg.bn = lbn;
+ arg.runp = -1;
+ arg.runb = -1;
+ ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno);
+ EXPECT_EQ(arg.bn, lbn * m_maxbcachebuf / DEV_BSIZE);
+ EXPECT_EQ(arg.runp, 0);
+ EXPECT_EQ((unsigned long )arg.runb, m_maxphys / m_maxbcachebuf - 1);
+
+ leak(fd);
+}
+
+/*
+ * The server returns an error for some reason for FUSE_BMAP. fusefs should
+ * faithfully report that error up to the caller.
+ */
+TEST_F(Bmap, einval)
+{
+ struct fiobmap2_arg arg;
+ const off_t filesize = 1 << 30;
+ int64_t lbn = 100;
+ const ino_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, 42, filesize);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_BMAP &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(EINVAL)));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ arg.bn = lbn;
+ arg.runp = -1;
+ arg.runb = -1;
+ ASSERT_EQ(-1, ioctl(fd, FIOBMAP2, &arg));
+ EXPECT_EQ(EINVAL, errno);
+
+ leak(fd);
+}
+
+/*
+ * Even if the server returns EINVAL during VOP_BMAP, we should still be able
+ * to successfully read a block. This is a regression test for
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=264196 . The bug did not
+ * lie in fusefs, but this is a convenient place for a regression test.
+ */
+TEST_F(Bmap, spurious_einval)
+{
+ const off_t filesize = 4ull << 30;
+ const ino_t ino = 42;
+ int fd, r;
+ char buf[1];
+
+ expect_lookup(RELPATH, 42, filesize);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_BMAP &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnErrno(EINVAL)));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ &&
+ in.header.nodeid == ino &&
+ in.body.read.offset == 0 &&
+ in.body.read.size == (uint64_t)m_maxbcachebuf);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) {
+ size_t osize = in.body.read.size;
+
+ assert(osize < sizeof(out.body.bytes));
+ out.header.len = sizeof(struct fuse_out_header) + osize;
+ bzero(out.body.bytes, osize);
+ })));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /*
+ * Read the same block multiple times. On a system affected by PR
+ * 264196 , the second read will fail.
+ */
+ r = read(fd, buf, sizeof(buf));
+ EXPECT_EQ(r, 1) << strerror(errno);
+ r = read(fd, buf, sizeof(buf));
+ EXPECT_EQ(r, 1) << strerror(errno);
+ r = read(fd, buf, sizeof(buf));
+ EXPECT_EQ(r, 1) << strerror(errno);
+}
+
+/*
+ * VOP_BMAP should not query the server for the file's size, even if its cached
+ * attributes have expired.
+ * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=256937
+ */
+TEST_P(BmapEof, eof)
+{
+ /*
+ * Outline:
+ * 1) lookup the file, setting attr_valid=0
+ * 2) Read more than one block, causing the kernel to issue VOP_BMAP to
+ * plan readahead.
+ * 3) Nothing should panic
+ * 4) Repeat the tests, truncating the file after different numbers of
+ * GETATTR operations.
+ */
+ Sequence seq;
+ const off_t filesize = 2 * m_maxbcachebuf;
+ const ino_t ino = 42;
+ mode_t mode = S_IFREG | 0644;
+ char *buf;
+ int fd;
+ int ngetattrs;
+
+ ngetattrs = GetParam();
+ FuseTest::expect_lookup(RELPATH, ino, mode, filesize, 1, 0);
+ expect_open(ino, 0, 1);
+ // Depending on ngetattrs, FUSE_READ could be called with either
+ // filesize or filesize / 2 .
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ &&
+ in.header.nodeid == ino &&
+ in.body.read.offset == 0 &&
+ ( in.body.read.size == filesize ||
+ in.body.read.size == filesize / 2));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) {
+ size_t osize = in.body.read.size;
+
+ assert(osize < sizeof(out.body.bytes));
+ out.header.len = sizeof(struct fuse_out_header) + osize;
+ bzero(out.body.bytes, osize);
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(Between(ngetattrs - 1, ngetattrs))
+ .InSequence(seq)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr_valid = 0;
+ out.body.attr.attr.ino = ino;
+ out.body.attr.attr.mode = S_IFREG | 0644;
+ out.body.attr.attr.size = filesize;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr_valid = 0;
+ out.body.attr.attr.ino = ino;
+ out.body.attr.attr.mode = S_IFREG | 0644;
+ out.body.attr.attr.size = filesize / 2;
+ })));
+
+ buf = new char[filesize]();
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ read(fd, buf, filesize);
+
+ delete[] buf;
+ leak(fd);
+}
+
+INSTANTIATE_TEST_SUITE_P(BE, BmapEof,
+ Values(1, 2, 3)
+);
diff --git a/tests/sys/fs/fusefs/cache.cc b/tests/sys/fs/fusefs/cache.cc
new file mode 100644
index 000000000000..ea6d87674da2
--- /dev/null
+++ b/tests/sys/fs/fusefs/cache.cc
@@ -0,0 +1,218 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2020 Alan Somers
+ *
+ * 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/param.h>
+#include <fcntl.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+/*
+ * Tests for thorny cache problems not specific to any one opcode
+ */
+
+using namespace testing;
+
+/*
+ * Parameters
+ * - reopen file - If true, close and reopen the file between reads
+ * - cache lookups - If true, allow lookups to be cached
+ * - cache attrs - If true, allow file attributes to be cached
+ * - cache_mode - uncached, writeback, or writethrough
+ * - initial size - File size before truncation
+ * - truncated size - File size after truncation
+ */
+typedef tuple<tuple<bool, bool, bool>, cache_mode, ssize_t, ssize_t> CacheParam;
+
+class Cache: public FuseTest, public WithParamInterface<CacheParam> {
+public:
+bool m_direct_io;
+
+Cache(): m_direct_io(false) {};
+
+virtual void SetUp() {
+ int cache_mode = get<1>(GetParam());
+ switch (cache_mode) {
+ case Uncached:
+ m_direct_io = true;
+ break;
+ case WritebackAsync:
+ m_async = true;
+ /* FALLTHROUGH */
+ case Writeback:
+ m_init_flags |= FUSE_WRITEBACK_CACHE;
+ /* FALLTHROUGH */
+ case Writethrough:
+ break;
+ default:
+ FAIL() << "Unknown cache mode";
+ }
+ m_noatime = true; // To prevent SETATTR for atime on close
+
+ FuseTest::SetUp();
+ if (IsSkipped())
+ return;
+}
+
+void expect_getattr(uint64_t ino, int times, uint64_t size, uint64_t attr_valid)
+{
+ 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_valid = attr_valid;
+ out.body.attr.attr.ino = ino;
+ out.body.attr.attr.mode = S_IFREG | 0644;
+ out.body.attr.attr.size = size;
+ })));
+}
+
+void expect_lookup(const char *relpath, uint64_t ino,
+ uint64_t size, uint64_t entry_valid, uint64_t attr_valid)
+{
+ 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.attr.nlink = 1;
+ out.body.entry.attr_valid = attr_valid;
+ out.body.entry.attr.size = size;
+ out.body.entry.entry_valid = entry_valid;
+ })));
+}
+
+void expect_open(uint64_t ino, int times)
+{
+ FuseTest::expect_open(ino, m_direct_io ? FOPEN_DIRECT_IO: 0, times);
+}
+
+void expect_release(uint64_t ino, ProcessMockerT r)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_RELEASE &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(r));
+}
+
+};
+
+// If the server truncates the file behind the kernel's back, the kernel should
+// invalidate cached pages beyond the new EOF
+TEST_P(Cache, truncate_by_surprise_invalidates_cache)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefghijklmnopqrstuvwxyz";
+ uint64_t ino = 42;
+ uint64_t attr_valid, entry_valid;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+ bool reopen = get<0>(get<0>(GetParam()));
+ bool cache_lookups = get<1>(get<0>(GetParam()));
+ bool cache_attrs = get<2>(get<0>(GetParam()));
+ ssize_t osize = get<2>(GetParam());
+ ssize_t nsize = get<3>(GetParam());
+
+ ASSERT_LE(osize, bufsize);
+ ASSERT_LE(nsize, bufsize);
+ if (cache_attrs)
+ attr_valid = UINT64_MAX;
+ else
+ attr_valid = 0;
+ if (cache_lookups)
+ entry_valid = UINT64_MAX;
+ else
+ entry_valid = 0;
+
+ expect_lookup(RELPATH, ino, osize, entry_valid, attr_valid);
+ expect_open(ino, 1);
+ if (!cache_attrs)
+ expect_getattr(ino, 2, osize, attr_valid);
+ expect_read(ino, 0, osize, osize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(osize, read(fd, buf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, CONTENTS, osize));
+
+ // Now truncate the file behind the kernel's back. The next read
+ // should discard cache and fetch from disk again.
+ if (reopen) {
+ // Close and reopen the file
+ expect_flush(ino, 1, ReturnErrno(ENOSYS));
+ expect_release(ino, ReturnErrno(0));
+ ASSERT_EQ(0, close(fd));
+ expect_lookup(RELPATH, ino, nsize, entry_valid, attr_valid);
+ expect_open(ino, 1);
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ }
+
+ if (!cache_attrs)
+ expect_getattr(ino, 1, nsize, attr_valid);
+ expect_read(ino, 0, nsize, nsize, CONTENTS);
+ ASSERT_EQ(0, lseek(fd, 0, SEEK_SET));
+ ASSERT_EQ(nsize, read(fd, buf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, CONTENTS, nsize));
+
+ leak(fd);
+}
+
+INSTANTIATE_TEST_SUITE_P(Cache, Cache,
+ Combine(
+ /* Test every combination that:
+ * - does not cache at least one of entries and attrs
+ * - either doesn't cache attrs, or reopens the file
+ * In the other combinations, the kernel will never learn that
+ * the file's size has changed.
+ */
+ Values(
+ std::make_tuple(false, false, false),
+ std::make_tuple(false, true, false),
+ std::make_tuple(true, false, false),
+ std::make_tuple(true, false, true),
+ std::make_tuple(true, true, false)
+ ),
+ Values(Writethrough, Writeback),
+ /* Test both reductions and extensions to file size */
+ Values(20),
+ Values(10, 25)
+ )
+);
diff --git a/tests/sys/fs/fusefs/copy_file_range.cc b/tests/sys/fs/fusefs/copy_file_range.cc
new file mode 100644
index 000000000000..806ecf3c3653
--- /dev/null
+++ b/tests/sys/fs/fusefs/copy_file_range.cc
@@ -0,0 +1,802 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2020 Alan Somers
+ *
+ * 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/param.h>
+#include <sys/mman.h>
+#include <sys/time.h>
+#include <sys/resource.h>
+
+#include <fcntl.h>
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class CopyFileRange: public FuseTest {
+public:
+
+void expect_maybe_lseek(uint64_t ino)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(AtMost(1))
+ .WillRepeatedly(Invoke(ReturnErrno(ENOSYS)));
+}
+
+void expect_open(uint64_t ino, uint32_t flags, int times, uint64_t fh)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPEN &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(times)
+ .WillRepeatedly(Invoke(
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ out.header.len = sizeof(out.header);
+ SET_OUT_HEADER_LEN(out, open);
+ out.body.open.fh = fh;
+ out.body.open.open_flags = flags;
+ })));
+}
+
+void expect_write(uint64_t ino, uint64_t offset, uint64_t isize,
+ uint64_t osize, const void *contents)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *buf = (const char*)in.body.bytes +
+ sizeof(struct fuse_write_in);
+
+ return (in.header.opcode == FUSE_WRITE &&
+ in.header.nodeid == ino &&
+ in.body.write.offset == offset &&
+ in.body.write.size == isize &&
+ 0 == bcmp(buf, contents, isize));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = osize;
+ })));
+}
+
+};
+
+
+class CopyFileRange_7_27: public CopyFileRange {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 27;
+ CopyFileRange::SetUp();
+}
+};
+
+class CopyFileRangeNoAtime: public CopyFileRange {
+public:
+virtual void SetUp() {
+ m_noatime = true;
+ CopyFileRange::SetUp();
+}
+};
+
+class CopyFileRangeRlimitFsize: public CopyFileRange {
+public:
+static sig_atomic_t s_sigxfsz;
+struct rlimit m_initial_limit;
+
+virtual void SetUp() {
+ s_sigxfsz = 0;
+ getrlimit(RLIMIT_FSIZE, &m_initial_limit);
+ CopyFileRange::SetUp();
+}
+
+void TearDown() {
+ struct sigaction sa;
+
+ setrlimit(RLIMIT_FSIZE, &m_initial_limit);
+
+ bzero(&sa, sizeof(sa));
+ sa.sa_handler = SIG_DFL;
+ sigaction(SIGXFSZ, &sa, NULL);
+
+ FuseTest::TearDown();
+}
+
+};
+
+sig_atomic_t CopyFileRangeRlimitFsize::s_sigxfsz = 0;
+
+void sigxfsz_handler(int __unused sig) {
+ CopyFileRangeRlimitFsize::s_sigxfsz = 1;
+}
+
+TEST_F(CopyFileRange, eio)
+{
+ const char FULLPATH1[] = "mountpoint/src.txt";
+ const char RELPATH1[] = "src.txt";
+ const char FULLPATH2[] = "mountpoint/dst.txt";
+ const char RELPATH2[] = "dst.txt";
+ const uint64_t ino1 = 42;
+ const uint64_t ino2 = 43;
+ const uint64_t fh1 = 0xdeadbeef1a7ebabe;
+ const uint64_t fh2 = 0xdeadc0de88c0ffee;
+ off_t fsize1 = 1 << 20; /* 1 MiB */
+ off_t fsize2 = 1 << 19; /* 512 KiB */
+ off_t start1 = 1 << 18;
+ off_t start2 = 3 << 17;
+ ssize_t len = 65536;
+ int fd1, fd2;
+
+ expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1);
+ expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
+ expect_open(ino1, 0, 1, fh1);
+ expect_open(ino2, 0, 1, fh2);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
+ in.header.nodeid == ino1 &&
+ in.body.copy_file_range.fh_in == fh1 &&
+ (off_t)in.body.copy_file_range.off_in == start1 &&
+ in.body.copy_file_range.nodeid_out == ino2 &&
+ in.body.copy_file_range.fh_out == fh2 &&
+ (off_t)in.body.copy_file_range.off_out == start2 &&
+ in.body.copy_file_range.len == (size_t)len &&
+ in.body.copy_file_range.flags == 0);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(EIO)));
+
+ fd1 = open(FULLPATH1, O_RDONLY);
+ fd2 = open(FULLPATH2, O_WRONLY);
+ ASSERT_EQ(-1, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
+ EXPECT_EQ(EIO, errno);
+}
+
+/*
+ * copy_file_range should evict cached data for the modified region of the
+ * destination file.
+ */
+TEST_F(CopyFileRange, evicts_cache)
+{
+ const char FULLPATH1[] = "mountpoint/src.txt";
+ const char RELPATH1[] = "src.txt";
+ const char FULLPATH2[] = "mountpoint/dst.txt";
+ const char RELPATH2[] = "dst.txt";
+ char *buf0, *buf1, *buf;
+ const uint64_t ino1 = 42;
+ const uint64_t ino2 = 43;
+ const uint64_t fh1 = 0xdeadbeef1a7ebabe;
+ const uint64_t fh2 = 0xdeadc0de88c0ffee;
+ off_t fsize1 = 1 << 20; /* 1 MiB */
+ off_t fsize2 = 1 << 19; /* 512 KiB */
+ off_t start1 = 1 << 18;
+ off_t start2 = 3 << 17;
+ ssize_t len = m_maxbcachebuf;
+ int fd1, fd2;
+
+ buf0 = new char[m_maxbcachebuf];
+ memset(buf0, 42, m_maxbcachebuf);
+
+ expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1);
+ expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
+ expect_open(ino1, 0, 1, fh1);
+ expect_open(ino2, 0, 1, fh2);
+ expect_read(ino2, start2, m_maxbcachebuf, m_maxbcachebuf, buf0, -1,
+ fh2);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
+ in.header.nodeid == ino1 &&
+ in.body.copy_file_range.fh_in == fh1 &&
+ (off_t)in.body.copy_file_range.off_in == start1 &&
+ in.body.copy_file_range.nodeid_out == ino2 &&
+ in.body.copy_file_range.fh_out == fh2 &&
+ (off_t)in.body.copy_file_range.off_out == start2 &&
+ in.body.copy_file_range.len == (size_t)len &&
+ in.body.copy_file_range.flags == 0);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = len;
+ })));
+
+ fd1 = open(FULLPATH1, O_RDONLY);
+ fd2 = open(FULLPATH2, O_RDWR);
+
+ // Prime cache
+ buf = new char[m_maxbcachebuf];
+ ASSERT_EQ(m_maxbcachebuf, pread(fd2, buf, m_maxbcachebuf, start2))
+ << strerror(errno);
+ EXPECT_EQ(0, memcmp(buf0, buf, m_maxbcachebuf));
+
+ // Tell the FUSE server overwrite the region we just read
+ ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
+
+ // Read again. This should bypass the cache and read direct from server
+ buf1 = new char[m_maxbcachebuf];
+ memset(buf1, 69, m_maxbcachebuf);
+ start2 -= len;
+ expect_read(ino2, start2, m_maxbcachebuf, m_maxbcachebuf, buf1, -1,
+ fh2);
+ ASSERT_EQ(m_maxbcachebuf, pread(fd2, buf, m_maxbcachebuf, start2))
+ << strerror(errno);
+ EXPECT_EQ(0, memcmp(buf1, buf, m_maxbcachebuf));
+
+ delete[] buf1;
+ delete[] buf0;
+ delete[] buf;
+ leak(fd1);
+ leak(fd2);
+}
+
+/*
+ * If the server doesn't support FUSE_COPY_FILE_RANGE, the kernel should
+ * fallback to a read/write based implementation.
+ */
+TEST_F(CopyFileRange, fallback)
+{
+ const char FULLPATH1[] = "mountpoint/src.txt";
+ const char RELPATH1[] = "src.txt";
+ const char FULLPATH2[] = "mountpoint/dst.txt";
+ const char RELPATH2[] = "dst.txt";
+ const uint64_t ino1 = 42;
+ const uint64_t ino2 = 43;
+ const uint64_t fh1 = 0xdeadbeef1a7ebabe;
+ const uint64_t fh2 = 0xdeadc0de88c0ffee;
+ off_t fsize2 = 0;
+ off_t start1 = 0;
+ off_t start2 = 0;
+ const char *contents = "Hello, world!";
+ ssize_t len;
+ int fd1, fd2;
+
+ len = strlen(contents);
+
+ /*
+ * Ensure that we read to EOF, just so the buffer cache's read size is
+ * predictable.
+ */
+ expect_lookup(RELPATH1, ino1, S_IFREG | 0644, start1 + len, 1);
+ expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
+ expect_open(ino1, 0, 1, fh1);
+ expect_open(ino2, 0, 1, fh2);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
+ in.header.nodeid == ino1 &&
+ in.body.copy_file_range.fh_in == fh1 &&
+ (off_t)in.body.copy_file_range.off_in == start1 &&
+ in.body.copy_file_range.nodeid_out == ino2 &&
+ in.body.copy_file_range.fh_out == fh2 &&
+ (off_t)in.body.copy_file_range.off_out == start2 &&
+ in.body.copy_file_range.len == (size_t)len &&
+ in.body.copy_file_range.flags == 0);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENOSYS)));
+ expect_maybe_lseek(ino1);
+ expect_read(ino1, start1, len, len, contents, 0);
+ expect_write(ino2, start2, len, len, contents);
+
+ fd1 = open(FULLPATH1, O_RDONLY);
+ ASSERT_GE(fd1, 0);
+ fd2 = open(FULLPATH2, O_WRONLY);
+ ASSERT_GE(fd2, 0);
+ ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
+}
+
+/*
+ * Writes via mmap should not conflict with using copy_file_range. Any dirty
+ * pages that overlap with copy_file_range's input should be flushed before
+ * FUSE_COPY_FILE_RANGE is sent.
+ */
+TEST_F(CopyFileRange, mmap_write)
+{
+ const char FULLPATH[] = "mountpoint/src.txt";
+ const char RELPATH[] = "src.txt";
+ uint8_t *wbuf, *fbuf;
+ void *p;
+ size_t fsize = 0x6000;
+ size_t wsize = 0x3000;
+ ssize_t r;
+ off_t offset2_in = 0;
+ off_t offset2_out = wsize;
+ size_t copysize = wsize;
+ const uint64_t ino = 42;
+ const uint64_t fh = 0xdeadbeef1a7ebabe;
+ int fd;
+ const mode_t mode = 0644;
+
+ fbuf = new uint8_t[fsize]();
+ wbuf = new uint8_t[wsize];
+ memset(wbuf, 1, wsize);
+
+ expect_lookup(RELPATH, ino, S_IFREG | mode, fsize, 1);
+ expect_open(ino, 0, 1, fh);
+ /* This read is initiated by the mmap write */
+ expect_read(ino, 0, fsize, fsize, fbuf, -1, fh);
+ /* This write flushes the buffer filled by the mmap write */
+ expect_write(ino, 0, wsize, wsize, wbuf);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
+ (off_t)in.body.copy_file_range.off_in == offset2_in &&
+ (off_t)in.body.copy_file_range.off_out == offset2_out &&
+ in.body.copy_file_range.len == copysize
+ );
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([&](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = copysize;
+ })));
+
+ fd = open(FULLPATH, O_RDWR);
+
+ /* First, write some data via mmap */
+ p = mmap(NULL, wsize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ ASSERT_NE(MAP_FAILED, p) << strerror(errno);
+ memmove((uint8_t*)p, wbuf, wsize);
+ ASSERT_EQ(0, munmap(p, wsize)) << strerror(errno);
+
+ /*
+ * Then copy it around the file via copy_file_range. This should
+ * trigger a FUSE_WRITE to flush the pages written by mmap.
+ */
+ r = copy_file_range(fd, &offset2_in, fd, &offset2_out, copysize, 0);
+ ASSERT_EQ(copysize, (size_t)r) << strerror(errno);
+
+ delete[] wbuf;
+ delete[] fbuf;
+}
+
+
+/*
+ * copy_file_range should send SIGXFSZ and return EFBIG when the operation
+ * would exceed the limit imposed by RLIMIT_FSIZE.
+ */
+TEST_F(CopyFileRangeRlimitFsize, signal)
+{
+ const char FULLPATH1[] = "mountpoint/src.txt";
+ const char RELPATH1[] = "src.txt";
+ const char FULLPATH2[] = "mountpoint/dst.txt";
+ const char RELPATH2[] = "dst.txt";
+ struct rlimit rl;
+ const uint64_t ino1 = 42;
+ const uint64_t ino2 = 43;
+ const uint64_t fh1 = 0xdeadbeef1a7ebabe;
+ const uint64_t fh2 = 0xdeadc0de88c0ffee;
+ off_t fsize1 = 1 << 20; /* 1 MiB */
+ off_t fsize2 = 1 << 19; /* 512 KiB */
+ off_t start1 = 1 << 18;
+ off_t start2 = fsize2;
+ ssize_t len = 65536;
+ int fd1, fd2;
+
+ expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1);
+ expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
+ expect_open(ino1, 0, 1, fh1);
+ expect_open(ino2, 0, 1, fh2);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_COPY_FILE_RANGE);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+
+ rl.rlim_cur = fsize2;
+ rl.rlim_max = m_initial_limit.rlim_max;
+ ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno);
+ ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno);
+
+ fd1 = open(FULLPATH1, O_RDONLY);
+ fd2 = open(FULLPATH2, O_WRONLY);
+ ASSERT_EQ(-1, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
+ EXPECT_EQ(EFBIG, errno);
+ EXPECT_EQ(1, s_sigxfsz);
+}
+
+/*
+ * When crossing the RLIMIT_FSIZE boundary, writes should be truncated, not
+ * aborted.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=266611
+ */
+TEST_F(CopyFileRangeRlimitFsize, truncate)
+{
+ const char FULLPATH1[] = "mountpoint/src.txt";
+ const char RELPATH1[] = "src.txt";
+ const char FULLPATH2[] = "mountpoint/dst.txt";
+ const char RELPATH2[] = "dst.txt";
+ struct rlimit rl;
+ const uint64_t ino1 = 42;
+ const uint64_t ino2 = 43;
+ const uint64_t fh1 = 0xdeadbeef1a7ebabe;
+ const uint64_t fh2 = 0xdeadc0de88c0ffee;
+ off_t fsize1 = 1 << 20; /* 1 MiB */
+ off_t fsize2 = 1 << 19; /* 512 KiB */
+ off_t start1 = 1 << 18;
+ off_t start2 = fsize2;
+ ssize_t len = 65536;
+ off_t limit = start2 + len / 2;
+ int fd1, fd2;
+
+ expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1);
+ expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
+ expect_open(ino1, 0, 1, fh1);
+ expect_open(ino2, 0, 1, fh2);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
+ (off_t)in.body.copy_file_range.off_out == start2 &&
+ in.body.copy_file_range.len == (size_t)len / 2
+ );
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = len / 2;
+ })));
+
+ rl.rlim_cur = limit;
+ rl.rlim_max = m_initial_limit.rlim_max;
+ ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno);
+ ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno);
+
+ fd1 = open(FULLPATH1, O_RDONLY);
+ fd2 = open(FULLPATH2, O_WRONLY);
+ ASSERT_EQ(len / 2, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
+}
+
+TEST_F(CopyFileRange, ok)
+{
+ const char FULLPATH1[] = "mountpoint/src.txt";
+ const char RELPATH1[] = "src.txt";
+ const char FULLPATH2[] = "mountpoint/dst.txt";
+ const char RELPATH2[] = "dst.txt";
+ const uint64_t ino1 = 42;
+ const uint64_t ino2 = 43;
+ const uint64_t fh1 = 0xdeadbeef1a7ebabe;
+ const uint64_t fh2 = 0xdeadc0de88c0ffee;
+ off_t fsize1 = 1 << 20; /* 1 MiB */
+ off_t fsize2 = 1 << 19; /* 512 KiB */
+ off_t start1 = 1 << 18;
+ off_t start2 = 3 << 17;
+ ssize_t len = 65536;
+ int fd1, fd2;
+
+ expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1);
+ expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
+ expect_open(ino1, 0, 1, fh1);
+ expect_open(ino2, 0, 1, fh2);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
+ in.header.nodeid == ino1 &&
+ in.body.copy_file_range.fh_in == fh1 &&
+ (off_t)in.body.copy_file_range.off_in == start1 &&
+ in.body.copy_file_range.nodeid_out == ino2 &&
+ in.body.copy_file_range.fh_out == fh2 &&
+ (off_t)in.body.copy_file_range.off_out == start2 &&
+ in.body.copy_file_range.len == (size_t)len &&
+ in.body.copy_file_range.flags == 0);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = len;
+ })));
+
+ fd1 = open(FULLPATH1, O_RDONLY);
+ fd2 = open(FULLPATH2, O_WRONLY);
+ ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
+}
+
+/*
+ * copy_file_range can make copies within a single file, as long as the ranges
+ * don't overlap.
+ * */
+TEST_F(CopyFileRange, same_file)
+{
+ const char FULLPATH[] = "mountpoint/src.txt";
+ const char RELPATH[] = "src.txt";
+ const uint64_t ino = 4;
+ const uint64_t fh = 0xdeadbeefa7ebabe;
+ off_t fsize = 1 << 20; /* 1 MiB */
+ off_t off_in = 1 << 18;
+ off_t off_out = 3 << 17;
+ ssize_t len = 65536;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1, fh);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
+ in.header.nodeid == ino &&
+ in.body.copy_file_range.fh_in == fh &&
+ (off_t)in.body.copy_file_range.off_in == off_in &&
+ in.body.copy_file_range.nodeid_out == ino &&
+ in.body.copy_file_range.fh_out == fh &&
+ (off_t)in.body.copy_file_range.off_out == off_out &&
+ in.body.copy_file_range.len == (size_t)len &&
+ in.body.copy_file_range.flags == 0);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = len;
+ })));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_EQ(len, copy_file_range(fd, &off_in, fd, &off_out, len, 0));
+
+ leak(fd);
+}
+
+/*
+ * copy_file_range should update the destination's mtime and ctime, and
+ * the source's atime.
+ */
+TEST_F(CopyFileRange, timestamps)
+{
+ const char FULLPATH1[] = "mountpoint/src.txt";
+ const char RELPATH1[] = "src.txt";
+ const char FULLPATH2[] = "mountpoint/dst.txt";
+ const char RELPATH2[] = "dst.txt";
+ struct stat sb1a, sb1b, sb2a, sb2b;
+ const uint64_t ino1 = 42;
+ const uint64_t ino2 = 43;
+ const uint64_t fh1 = 0xdeadbeef1a7ebabe;
+ const uint64_t fh2 = 0xdeadc0de88c0ffee;
+ off_t fsize1 = 1 << 20; /* 1 MiB */
+ off_t fsize2 = 1 << 19; /* 512 KiB */
+ off_t start1 = 1 << 18;
+ off_t start2 = 3 << 17;
+ ssize_t len = 65536;
+ int fd1, fd2;
+
+ expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1);
+ expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
+ expect_open(ino1, 0, 1, fh1);
+ expect_open(ino2, 0, 1, fh2);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
+ in.header.nodeid == ino1 &&
+ in.body.copy_file_range.fh_in == fh1 &&
+ (off_t)in.body.copy_file_range.off_in == start1 &&
+ in.body.copy_file_range.nodeid_out == ino2 &&
+ in.body.copy_file_range.fh_out == fh2 &&
+ (off_t)in.body.copy_file_range.off_out == start2 &&
+ in.body.copy_file_range.len == (size_t)len &&
+ in.body.copy_file_range.flags == 0);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = len;
+ })));
+
+ fd1 = open(FULLPATH1, O_RDONLY);
+ ASSERT_GE(fd1, 0);
+ fd2 = open(FULLPATH2, O_WRONLY);
+ ASSERT_GE(fd2, 0);
+ ASSERT_EQ(0, fstat(fd1, &sb1a)) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd2, &sb2a)) << strerror(errno);
+
+ nap();
+
+ ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
+ ASSERT_EQ(0, fstat(fd1, &sb1b)) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd2, &sb2b)) << strerror(errno);
+
+ EXPECT_NE(sb1a.st_atime, sb1b.st_atime);
+ EXPECT_EQ(sb1a.st_mtime, sb1b.st_mtime);
+ EXPECT_EQ(sb1a.st_ctime, sb1b.st_ctime);
+ EXPECT_EQ(sb2a.st_atime, sb2b.st_atime);
+ EXPECT_NE(sb2a.st_mtime, sb2b.st_mtime);
+ EXPECT_NE(sb2a.st_ctime, sb2b.st_ctime);
+
+ leak(fd1);
+ leak(fd2);
+}
+
+/*
+ * copy_file_range can extend the size of a file
+ * */
+TEST_F(CopyFileRange, extend)
+{
+ const char FULLPATH[] = "mountpoint/src.txt";
+ const char RELPATH[] = "src.txt";
+ struct stat sb;
+ const uint64_t ino = 4;
+ const uint64_t fh = 0xdeadbeefa7ebabe;
+ off_t fsize = 65536;
+ off_t off_in = 0;
+ off_t off_out = 65536;
+ ssize_t len = 65536;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1, fh);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
+ in.header.nodeid == ino &&
+ in.body.copy_file_range.fh_in == fh &&
+ (off_t)in.body.copy_file_range.off_in == off_in &&
+ in.body.copy_file_range.nodeid_out == ino &&
+ in.body.copy_file_range.fh_out == fh &&
+ (off_t)in.body.copy_file_range.off_out == off_out &&
+ in.body.copy_file_range.len == (size_t)len &&
+ in.body.copy_file_range.flags == 0);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = len;
+ })));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(len, copy_file_range(fd, &off_in, fd, &off_out, len, 0));
+
+ /* Check that cached attributes were updated appropriately */
+ ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
+ EXPECT_EQ(fsize + len, sb.st_size);
+
+ leak(fd);
+}
+
+/* With older protocol versions, no FUSE_COPY_FILE_RANGE should be attempted */
+TEST_F(CopyFileRange_7_27, fallback)
+{
+ const char FULLPATH1[] = "mountpoint/src.txt";
+ const char RELPATH1[] = "src.txt";
+ const char FULLPATH2[] = "mountpoint/dst.txt";
+ const char RELPATH2[] = "dst.txt";
+ const uint64_t ino1 = 42;
+ const uint64_t ino2 = 43;
+ const uint64_t fh1 = 0xdeadbeef1a7ebabe;
+ const uint64_t fh2 = 0xdeadc0de88c0ffee;
+ off_t fsize2 = 0;
+ off_t start1 = 0;
+ off_t start2 = 0;
+ const char *contents = "Hello, world!";
+ ssize_t len;
+ int fd1, fd2;
+
+ len = strlen(contents);
+
+ /*
+ * Ensure that we read to EOF, just so the buffer cache's read size is
+ * predictable.
+ */
+ expect_lookup(RELPATH1, ino1, S_IFREG | 0644, start1 + len, 1);
+ expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
+ expect_open(ino1, 0, 1, fh1);
+ expect_open(ino2, 0, 1, fh2);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_COPY_FILE_RANGE);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+ expect_maybe_lseek(ino1);
+ expect_read(ino1, start1, len, len, contents, 0);
+ expect_write(ino2, start2, len, len, contents);
+
+ fd1 = open(FULLPATH1, O_RDONLY);
+ ASSERT_GE(fd1, 0);
+ fd2 = open(FULLPATH2, O_WRONLY);
+ ASSERT_GE(fd2, 0);
+ ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
+
+ leak(fd1);
+ leak(fd2);
+}
+
+/*
+ * With -o noatime, copy_file_range should update the destination's mtime and
+ * ctime, but not the source's atime.
+ */
+TEST_F(CopyFileRangeNoAtime, timestamps)
+{
+ const char FULLPATH1[] = "mountpoint/src.txt";
+ const char RELPATH1[] = "src.txt";
+ const char FULLPATH2[] = "mountpoint/dst.txt";
+ const char RELPATH2[] = "dst.txt";
+ struct stat sb1a, sb1b, sb2a, sb2b;
+ const uint64_t ino1 = 42;
+ const uint64_t ino2 = 43;
+ const uint64_t fh1 = 0xdeadbeef1a7ebabe;
+ const uint64_t fh2 = 0xdeadc0de88c0ffee;
+ off_t fsize1 = 1 << 20; /* 1 MiB */
+ off_t fsize2 = 1 << 19; /* 512 KiB */
+ off_t start1 = 1 << 18;
+ off_t start2 = 3 << 17;
+ ssize_t len = 65536;
+ int fd1, fd2;
+
+ expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1);
+ expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
+ expect_open(ino1, 0, 1, fh1);
+ expect_open(ino2, 0, 1, fh2);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
+ in.header.nodeid == ino1 &&
+ in.body.copy_file_range.fh_in == fh1 &&
+ (off_t)in.body.copy_file_range.off_in == start1 &&
+ in.body.copy_file_range.nodeid_out == ino2 &&
+ in.body.copy_file_range.fh_out == fh2 &&
+ (off_t)in.body.copy_file_range.off_out == start2 &&
+ in.body.copy_file_range.len == (size_t)len &&
+ in.body.copy_file_range.flags == 0);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = len;
+ })));
+
+ fd1 = open(FULLPATH1, O_RDONLY);
+ ASSERT_GE(fd1, 0);
+ fd2 = open(FULLPATH2, O_WRONLY);
+ ASSERT_GE(fd2, 0);
+ ASSERT_EQ(0, fstat(fd1, &sb1a)) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd2, &sb2a)) << strerror(errno);
+
+ nap();
+
+ ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
+ ASSERT_EQ(0, fstat(fd1, &sb1b)) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd2, &sb2b)) << strerror(errno);
+
+ EXPECT_EQ(sb1a.st_atime, sb1b.st_atime);
+ EXPECT_EQ(sb1a.st_mtime, sb1b.st_mtime);
+ EXPECT_EQ(sb1a.st_ctime, sb1b.st_ctime);
+ EXPECT_EQ(sb2a.st_atime, sb2b.st_atime);
+ EXPECT_NE(sb2a.st_mtime, sb2b.st_mtime);
+ EXPECT_NE(sb2a.st_ctime, sb2b.st_ctime);
+
+ leak(fd1);
+ leak(fd2);
+}
diff --git a/tests/sys/fs/fusefs/create.cc b/tests/sys/fs/fusefs/create.cc
new file mode 100644
index 000000000000..08126ffc9056
--- /dev/null
+++ b/tests/sys/fs/fusefs/create.cc
@@ -0,0 +1,493 @@
+/*-
+ * 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 <fcntl.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Create: public FuseTest {
+public:
+
+void expect_create(const char *relpath, mode_t mode, ProcessMockerT r)
+{
+ mode_t mask = umask(0);
+ (void)umask(mask);
+
+ 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 &&
+ in.body.create.mode == mode &&
+ in.body.create.umask == mask &&
+ (0 == strcmp(relpath, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(r));
+}
+
+};
+
+/* FUSE_CREATE operations for a protocol 7.8 server */
+class Create_7_8: public Create {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 8;
+ Create::SetUp();
+}
+
+void expect_create(const char *relpath, mode_t mode, ProcessMockerT r)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes +
+ sizeof(fuse_open_in);
+ return (in.header.opcode == FUSE_CREATE &&
+ in.body.create.mode == mode &&
+ (0 == strcmp(relpath, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(r));
+}
+
+};
+
+/* FUSE_CREATE operations for a server built at protocol <= 7.11 */
+class Create_7_11: public FuseTest {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 11;
+ FuseTest::SetUp();
+}
+
+void expect_create(const char *relpath, mode_t mode, ProcessMockerT r)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes +
+ sizeof(fuse_open_in);
+ return (in.header.opcode == FUSE_CREATE &&
+ in.body.create.mode == mode &&
+ (0 == strcmp(relpath, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(r));
+}
+
+};
+
+
+/*
+ * If FUSE_CREATE sets attr_valid, then subsequent GETATTRs should use the
+ * attribute cache
+ */
+TEST_F(Create, attr_cache)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = S_IFREG | 0755;
+ uint64_t ino = 42;
+ int fd;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_create(RELPATH, mode,
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, create);
+ out.body.create.entry.attr.mode = mode;
+ out.body.create.entry.nodeid = ino;
+ out.body.create.entry.entry_valid = UINT64_MAX;
+ out.body.create.entry.attr_valid = UINT64_MAX;
+ }));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+
+ fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
+ ASSERT_LE(0, fd) << strerror(errno);
+ leak(fd);
+}
+
+/* A successful CREATE operation should purge the parent dir's attr cache */
+TEST_F(Create, clear_attr_cache)
+{
+ const char FULLPATH[] = "mountpoint/src";
+ const char RELPATH[] = "src";
+ mode_t mode = S_IFREG | 0755;
+ uint64_t ino = 42;
+ int fd;
+ struct stat sb;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == FUSE_ROOT_ID);
+ }, Eq(true)),
+ _)
+ ).Times(2)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = FUSE_ROOT_ID;
+ out.body.attr.attr.mode = S_IFDIR | 0755;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+
+ expect_create(RELPATH, mode,
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, create);
+ out.body.create.entry.attr.mode = mode;
+ out.body.create.entry.nodeid = ino;
+ out.body.create.entry.entry_valid = UINT64_MAX;
+ out.body.create.entry.attr_valid = UINT64_MAX;
+ }));
+
+ EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
+ fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
+ ASSERT_LE(0, fd) << strerror(errno);
+ EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
+
+ leak(fd);
+}
+
+/*
+ * The fuse daemon fails the request with EEXIST. This usually indicates a
+ * race condition: some other FUSE client created the file in between when the
+ * kernel checked for it with lookup and tried to create it with create
+ */
+TEST_F(Create, eexist)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = S_IFREG | 0755;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_create(RELPATH, mode, ReturnErrno(EEXIST));
+ EXPECT_EQ(-1, open(FULLPATH, O_CREAT | O_EXCL, mode));
+ EXPECT_EQ(EEXIST, errno);
+}
+
+/*
+ * If the daemon doesn't implement FUSE_CREATE, then the kernel should fallback
+ * to FUSE_MKNOD/FUSE_OPEN
+ */
+TEST_F(Create, Enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = S_IFREG | 0755;
+ uint64_t ino = 42;
+ int fd;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_create(RELPATH, mode, ReturnErrno(ENOSYS));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes +
+ sizeof(fuse_mknod_in);
+ return (in.header.opcode == FUSE_MKNOD &&
+ in.body.mknod.mode == (S_IFREG | mode) &&
+ in.body.mknod.rdev == 0 &&
+ (0 == strcmp(RELPATH, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.entry_valid = UINT64_MAX;
+ 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);
+ })));
+
+ fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
+ ASSERT_LE(0, fd) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * Creating a new file after FUSE_LOOKUP returned a negative cache entry
+ */
+TEST_F(Create, entry_cache_negative)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = S_IFREG | 0755;
+ uint64_t ino = 42;
+ int fd;
+ /*
+ * Set entry_valid = 0 because this test isn't concerned with whether
+ * or not we actually cache negative entries, only with whether we
+ * interpret negative cache responses correctly.
+ */
+ struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0};
+
+ /* create will first do a LOOKUP, adding a negative cache entry */
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(ReturnNegativeCache(&entry_valid));
+ expect_create(RELPATH, mode,
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, create);
+ out.body.create.entry.attr.mode = mode;
+ out.body.create.entry.nodeid = ino;
+ out.body.create.entry.entry_valid = UINT64_MAX;
+ out.body.create.entry.attr_valid = UINT64_MAX;
+ }));
+
+ fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
+ ASSERT_LE(0, fd) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * Creating a new file should purge any negative namecache entries
+ */
+TEST_F(Create, entry_cache_negative_purge)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = S_IFREG | 0755;
+ uint64_t ino = 42;
+ int fd;
+ struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0};
+
+ /* create will first do a LOOKUP, adding a negative cache entry */
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH).Times(1)
+ .WillOnce(Invoke(ReturnNegativeCache(&entry_valid)))
+ .RetiresOnSaturation();
+
+ /* Then the CREATE should purge the negative cache entry */
+ expect_create(RELPATH, mode,
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, create);
+ out.body.create.entry.attr.mode = mode;
+ out.body.create.entry.nodeid = ino;
+ out.body.create.entry.attr_valid = UINT64_MAX;
+ }));
+
+ fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /* Finally, a subsequent lookup should query the daemon */
+ expect_lookup(RELPATH, ino, S_IFREG | mode, 0, 1);
+
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * The daemon is responsible for checking file permissions (unless the
+ * default_permissions mount option was used)
+ */
+TEST_F(Create, eperm)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = S_IFREG | 0755;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_create(RELPATH, mode, ReturnErrno(EPERM));
+
+ EXPECT_EQ(-1, open(FULLPATH, O_CREAT | O_EXCL, mode));
+ EXPECT_EQ(EPERM, errno);
+}
+
+TEST_F(Create, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = S_IFREG | 0755;
+ uint64_t ino = 42;
+ int fd;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_create(RELPATH, mode,
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, create);
+ out.body.create.entry.attr.mode = mode;
+ out.body.create.entry.nodeid = ino;
+ out.body.create.entry.entry_valid = UINT64_MAX;
+ out.body.create.entry.attr_valid = UINT64_MAX;
+ }));
+
+ fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
+ ASSERT_LE(0, fd) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * Nothing bad should happen if the server returns the parent's inode number
+ * for the newly created file. Regression test for bug 263662
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263662
+ */
+TEST_F(Create, parent_inode)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/some_file.txt";
+ const char RELDIRPATH[] = "some_dir";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = 0755;
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELDIRPATH, ino, S_IFDIR | mode, 0, 1);
+ EXPECT_LOOKUP(ino, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_create(RELPATH, S_IFREG | mode,
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, create);
+ out.body.create.entry.attr.mode = S_IFREG | mode;
+ /* Return the same inode as the parent dir */
+ out.body.create.entry.nodeid = ino;
+ out.body.create.entry.entry_valid = UINT64_MAX;
+ out.body.create.entry.attr_valid = UINT64_MAX;
+ }));
+ // FUSE_RELEASE happens asynchronously, so it may or may not arrive
+ // before the test completes.
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_RELEASE);
+ }, Eq(true)),
+ _)
+ ).Times(AtMost(1))
+ .WillOnce(Invoke([=](auto in __unused, auto &out __unused) { }));
+
+ fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
+ ASSERT_EQ(-1, fd);
+ EXPECT_EQ(EIO, errno);
+}
+
+/*
+ * A regression test for a bug that affected old FUSE implementations:
+ * open(..., O_WRONLY | O_CREAT, 0444) should work despite the seeming
+ * contradiction between O_WRONLY and 0444
+ *
+ * For example:
+ * https://bugs.launchpad.net/ubuntu/+source/sshfs-fuse/+bug/44886
+ */
+TEST_F(Create, wronly_0444)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = S_IFREG | 0444;
+ uint64_t ino = 42;
+ int fd;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_create(RELPATH, mode,
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, create);
+ out.body.create.entry.attr.mode = mode;
+ out.body.create.entry.nodeid = ino;
+ out.body.create.entry.entry_valid = UINT64_MAX;
+ out.body.create.entry.attr_valid = UINT64_MAX;
+ }));
+
+ fd = open(FULLPATH, O_CREAT | O_WRONLY, mode);
+ ASSERT_LE(0, fd) << strerror(errno);
+ leak(fd);
+}
+
+TEST_F(Create_7_8, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = S_IFREG | 0755;
+ uint64_t ino = 42;
+ int fd;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_create(RELPATH, mode,
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, create_7_8);
+ out.body.create_7_8.entry.attr.mode = mode;
+ out.body.create_7_8.entry.nodeid = ino;
+ out.body.create_7_8.entry.entry_valid = UINT64_MAX;
+ out.body.create_7_8.entry.attr_valid = UINT64_MAX;
+ out.body.create_7_8.open.fh = FH;
+ }));
+ expect_flush(ino, 1, ReturnErrno(0));
+ expect_release(ino, FH);
+
+ fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
+ ASSERT_LE(0, fd) << strerror(errno);
+ close(fd);
+}
+
+TEST_F(Create_7_11, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = S_IFREG | 0755;
+ uint64_t ino = 42;
+ int fd;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_create(RELPATH, mode,
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, create);
+ out.body.create.entry.attr.mode = mode;
+ out.body.create.entry.nodeid = ino;
+ out.body.create.entry.entry_valid = UINT64_MAX;
+ out.body.create.entry.attr_valid = UINT64_MAX;
+ }));
+
+ fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
+ ASSERT_LE(0, fd) << strerror(errno);
+ leak(fd);
+}
diff --git a/tests/sys/fs/fusefs/ctl.sh b/tests/sys/fs/fusefs/ctl.sh
new file mode 100644
index 000000000000..7d2e7593cbdc
--- /dev/null
+++ b/tests/sys/fs/fusefs/ctl.sh
@@ -0,0 +1,69 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 ConnectWise
+# All rights reserved.
+#
+# 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 DOCUMENTATION IS PROVIDED BY THE AUTHOR ``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 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.
+
+. $(atf_get_srcdir)/../../cam/ctl/ctl.subr
+
+# Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=283402
+#
+# Almost any fuse file system would work, but this tests uses fusefs-ext2
+# because it's simple and its download is very small.
+atf_test_case remove_lun_with_atime cleanup
+remove_lun_with_atime_head()
+{
+ atf_set "descr" "Remove a fuse-backed CTL LUN when atime is enabled"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-ext2 mkfs.ext2"
+}
+remove_lun_with_atime_body()
+{
+ MOUNTPOINT=$PWD/mnt
+ atf_check mkdir $MOUNTPOINT
+ atf_check truncate -s 1g ext2.img
+ atf_check mkfs.ext2 -q ext2.img
+ # Note: both default_permissions and atime must be enabled
+ atf_check fuse-ext2 -o default_permissions,allow_other,rw+ ext2.img \
+ $MOUNTPOINT
+
+ atf_check truncate -s 1m $MOUNTPOINT/file
+ create_block -o file=$MOUNTPOINT/file
+
+ # Force fusefs to open the file, and dirty its atime
+ atf_check dd if=/dev/$dev of=/dev/null count=1 status=none
+
+ # Finally, remove the LUN. Hopefully it won't panic.
+ atf_check -o ignore ctladm remove -b block -l $LUN
+
+ rm lun-create.txt # So we don't try to remove the LUN twice
+}
+remove_lun_with_atime_cleanup()
+{
+ cleanup
+ umount $PWD/mnt
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case remove_lun_with_atime
+}
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);
+}
diff --git a/tests/sys/fs/fusefs/default_permissions_privileged.cc b/tests/sys/fs/fusefs/default_permissions_privileged.cc
new file mode 100644
index 000000000000..43f2141c1984
--- /dev/null
+++ b/tests/sys/fs/fusefs/default_permissions_privileged.cc
@@ -0,0 +1,124 @@
+/*-
+ * 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 that require a privileged
+ * user.
+ */
+
+extern "C" {
+#include <sys/types.h>
+#include <sys/extattr.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class DefaultPermissionsPrivileged: public FuseTest {
+virtual void SetUp() {
+ m_default_permissions = true;
+ FuseTest::SetUp();
+ if (HasFatalFailure() || IsSkipped())
+ return;
+
+ if (geteuid() != 0) {
+ GTEST_SKIP() << "This test requires a privileged 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_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 Setattr: public DefaultPermissionsPrivileged {};
+
+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(1, 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)),
+ _)
+ ).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);
+}
+
+
diff --git a/tests/sys/fs/fusefs/destroy.cc b/tests/sys/fs/fusefs/destroy.cc
new file mode 100644
index 000000000000..45acb1f99724
--- /dev/null
+++ b/tests/sys/fs/fusefs/destroy.cc
@@ -0,0 +1,157 @@
+/*-
+ * 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 <fcntl.h>
+#include <pthread.h>
+#include <semaphore.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+/* Tests for orderly unmounts */
+class Destroy: public FuseTest {};
+
+/* Tests for unexpected deaths of the server */
+class Death: public FuseTest{};
+
+static void* open_th(void* arg) {
+ int fd;
+ const char *path = (const char*)arg;
+
+ fd = open(path, O_RDONLY);
+ EXPECT_EQ(-1, fd);
+ EXPECT_EQ(ENOTCONN, errno);
+ return 0;
+}
+
+/*
+ * The server dies with unsent operations still on the message queue.
+ * Check for any memory leaks like this:
+ * 1) kldunload fusefs, if necessary
+ * 2) kldload fusefs
+ * 3) ./destroy --gtest_filter=Death.unsent_operations
+ * 4) kldunload fusefs
+ * 5) check /var/log/messages for anything like this:
+Freed UMA keg (fuse_ticket) was not empty (31 items). Lost 2 pages of memory.
+Warning: memory type fuse_msgbuf leaked memory on destroy (68 allocations, 428800 bytes leaked).
+ */
+TEST_F(Death, unsent_operations)
+{
+ const char FULLPATH0[] = "mountpoint/some_file.txt";
+ const char FULLPATH1[] = "mountpoint/other_file.txt";
+ const char RELPATH0[] = "some_file.txt";
+ const char RELPATH1[] = "other_file.txt";
+ pthread_t th0, th1;
+ ino_t ino0 = 42, ino1 = 43;
+ sem_t sem;
+ mode_t mode = S_IFREG | 0644;
+
+ sem_init(&sem, 0, 0);
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH0)
+ .WillRepeatedly(Invoke(
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino0;
+ out.body.entry.attr.nlink = 1;
+ })));
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH1)
+ .WillRepeatedly(Invoke(
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino1;
+ out.body.entry.attr.nlink = 1;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ return (in.header.opcode == FUSE_OPEN);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([&](auto in __unused, auto &out __unused) {
+ sem_post(&sem);
+ pause();
+ }));
+
+ /*
+ * One thread's operation will be sent to the daemon and block, and the
+ * other's will be stuck in the message queue.
+ */
+ ASSERT_EQ(0, pthread_create(&th0, NULL, open_th,
+ __DECONST(void*, FULLPATH0))) << strerror(errno);
+ ASSERT_EQ(0, pthread_create(&th1, NULL, open_th,
+ __DECONST(void*, FULLPATH1))) << strerror(errno);
+
+ /* Wait for the first thread to block */
+ sem_wait(&sem);
+ /* Give the second thread time to block */
+ nap();
+
+ m_mock->kill_daemon();
+
+ pthread_join(th0, NULL);
+ pthread_join(th1, NULL);
+
+ sem_destroy(&sem);
+}
+
+/*
+ * On unmount the kernel should send a FUSE_DESTROY operation. It should also
+ * send FUSE_FORGET operations for all inodes with lookup_count > 0.
+ */
+TEST_F(Destroy, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
+ expect_forget(ino, 2);
+ expect_destroy(0);
+
+ /*
+ * access(2) the file to force a lookup. Access it twice to double its
+ * lookup count.
+ */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+
+ /*
+ * Unmount, triggering a FUSE_DESTROY and also causing a VOP_RECLAIM
+ * for every vnode on this mp, triggering FUSE_FORGET for each of them.
+ */
+ m_mock->unmount();
+}
diff --git a/tests/sys/fs/fusefs/dev_fuse_poll.cc b/tests/sys/fs/fusefs/dev_fuse_poll.cc
new file mode 100644
index 000000000000..181cd69de665
--- /dev/null
+++ b/tests/sys/fs/fusefs/dev_fuse_poll.cc
@@ -0,0 +1,229 @@
+/*-
+ * 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.
+ */
+
+/*
+ * This file tests different polling methods for the /dev/fuse device
+ */
+
+extern "C" {
+#include <fcntl.h>
+#include <semaphore.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+const char FULLPATH[] = "mountpoint/some_file.txt";
+const char RELPATH[] = "some_file.txt";
+const uint64_t ino = 42;
+const mode_t access_mode = R_OK;
+
+/*
+ * Translate a poll method's string representation to the enum value.
+ * Using strings with ::testing::Values gives better output with
+ * --gtest_list_tests
+ */
+enum poll_method poll_method_from_string(const char *s)
+{
+ if (0 == strcmp("BLOCKING", s))
+ return BLOCKING;
+ else if (0 == strcmp("KQ", s))
+ return KQ;
+ else if (0 == strcmp("POLL", s))
+ return POLL;
+ else
+ return SELECT;
+}
+
+class DevFusePoll: public FuseTest, public WithParamInterface<const char *> {
+ virtual void SetUp() {
+ m_pm = poll_method_from_string(GetParam());
+ FuseTest::SetUp();
+ }
+};
+
+class Kqueue: public FuseTest {
+ virtual void SetUp() {
+ m_pm = KQ;
+ FuseTest::SetUp();
+ }
+};
+
+TEST_P(DevFusePoll, access)
+{
+ expect_access(FUSE_ROOT_ID, X_OK, 0);
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_access(ino, access_mode, 0);
+
+ ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
+}
+
+/* Ensure that we wake up pollers during unmount */
+TEST_P(DevFusePoll, destroy)
+{
+ expect_destroy(0);
+
+ m_mock->unmount();
+}
+
+INSTANTIATE_TEST_SUITE_P(PM, DevFusePoll,
+ ::testing::Values("BLOCKING", "KQ", "POLL", "SELECT"));
+
+static void* statter(void* arg) {
+ const char *name;
+ struct stat sb;
+
+ name = (const char*)arg;
+ return ((void*)(intptr_t)stat(name, &sb));
+}
+
+/*
+ * A kevent's data field should contain the number of operations available to
+ * be immediately read.
+ */
+TEST_F(Kqueue, data)
+{
+ pthread_t th0, th1, th2;
+ sem_t sem0, sem1;
+ int nready0, nready1, nready2;
+ uint64_t foo_ino = 42;
+ uint64_t bar_ino = 43;
+ uint64_t baz_ino = 44;
+ Sequence seq;
+ void *th_ret;
+
+ ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
+ ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno);
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, "foo")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ out.body.entry.nodeid = foo_ino;
+ })));
+ EXPECT_LOOKUP(FUSE_ROOT_ID, "bar")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ out.body.entry.nodeid = bar_ino;
+ })));
+ EXPECT_LOOKUP(FUSE_ROOT_ID, "baz")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ out.body.entry.nodeid = baz_ino;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == foo_ino);
+ }, Eq(true)),
+ _)
+ )
+ .WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
+ nready0 = m_mock->m_nready;
+
+ sem_post(&sem0);
+ // Block the daemon so we can accumulate a few more ops
+ sem_wait(&sem1);
+
+ out.header.unique = in.header.unique;
+ out.header.error = -EIO;
+ out.header.len = sizeof(out.header);
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ (in.header.nodeid == bar_ino ||
+ in.header.nodeid == baz_ino));
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
+ nready1 = m_mock->m_nready;
+ out.header.unique = in.header.unique;
+ out.header.error = -EIO;
+ out.header.len = sizeof(out.header);
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ (in.header.nodeid == bar_ino ||
+ in.header.nodeid == baz_ino));
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
+ nready2 = m_mock->m_nready;
+ out.header.unique = in.header.unique;
+ out.header.error = -EIO;
+ out.header.len = sizeof(out.header);
+ })));
+
+ /*
+ * Create cached lookup entries for these files. It seems that only
+ * one thread at a time can be in VOP_LOOKUP for a given directory
+ */
+ access("mountpoint/foo", F_OK);
+ access("mountpoint/bar", F_OK);
+ access("mountpoint/baz", F_OK);
+ ASSERT_EQ(0, pthread_create(&th0, NULL, statter,
+ __DECONST(void*, "mountpoint/foo"))) << strerror(errno);
+ EXPECT_EQ(0, sem_wait(&sem0)) << strerror(errno);
+ ASSERT_EQ(0, pthread_create(&th1, NULL, statter,
+ __DECONST(void*, "mountpoint/bar"))) << strerror(errno);
+ ASSERT_EQ(0, pthread_create(&th2, NULL, statter,
+ __DECONST(void*, "mountpoint/baz"))) << strerror(errno);
+
+ nap(); // Allow th1 and th2 to send their ops to the daemon
+ EXPECT_EQ(0, sem_post(&sem1)) << strerror(errno);
+
+ pthread_join(th0, &th_ret);
+ ASSERT_EQ(-1, (intptr_t)th_ret);
+ pthread_join(th1, &th_ret);
+ ASSERT_EQ(-1, (intptr_t)th_ret);
+ pthread_join(th2, &th_ret);
+ ASSERT_EQ(-1, (intptr_t)th_ret);
+
+ EXPECT_EQ(1, nready0);
+ EXPECT_EQ(2, nready1);
+ EXPECT_EQ(1, nready2);
+
+ sem_destroy(&sem0);
+ sem_destroy(&sem1);
+}
diff --git a/tests/sys/fs/fusefs/fallocate.cc b/tests/sys/fs/fusefs/fallocate.cc
new file mode 100644
index 000000000000..4e5b047b78b7
--- /dev/null
+++ b/tests/sys/fs/fusefs/fallocate.cc
@@ -0,0 +1,779 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2021 Alan Somers
+ *
+ * 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/param.h>
+#include <sys/mount.h>
+#include <sys/resource.h>
+#include <sys/time.h>
+
+#include <fcntl.h>
+#include <mntopts.h> // for build_iovec
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+/* Is buf all zero? */
+static bool
+is_zero(const char *buf, uint64_t size)
+{
+ return buf[0] == 0 && !memcmp(buf, buf + 1, size - 1);
+}
+
+class Fallocate: public FuseTest {
+public:
+/*
+ * expect VOP_DEALLOCATE to be implemented by vop_stddeallocate.
+ */
+void expect_vop_stddeallocate(uint64_t ino, uint64_t off, uint64_t length)
+{
+ /* XXX read offset and size may depend on cache mode */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ &&
+ in.header.nodeid == ino &&
+ in.body.read.offset <= off &&
+ in.body.read.offset + in.body.read.size >=
+ off + length);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) {
+ assert(in.body.read.size <= sizeof(out.body.bytes));
+ out.header.len = sizeof(struct fuse_out_header) +
+ in.body.read.size;
+ memset(out.body.bytes, 'X', in.body.read.size);
+ }))).RetiresOnSaturation();
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *buf = (const char*)in.body.bytes +
+ sizeof(struct fuse_write_in);
+
+ assert(length <= sizeof(in.body.bytes) -
+ sizeof(struct fuse_write_in));
+ return (in.header.opcode == FUSE_WRITE &&
+ in.header.nodeid == ino &&
+ in.body.write.offset == off &&
+ in.body.write.size == length &&
+ is_zero(buf, length));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = length;
+ })));
+}
+};
+
+class Fspacectl: public Fallocate {};
+
+class Fspacectl_7_18: public Fspacectl {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 18;
+ Fspacectl::SetUp();
+}
+};
+
+class FspacectlCache: public Fspacectl, public WithParamInterface<cache_mode> {
+public:
+bool m_direct_io;
+
+FspacectlCache(): m_direct_io(false) {};
+
+virtual void SetUp() {
+ int cache_mode = GetParam();
+ switch (cache_mode) {
+ case Uncached:
+ m_direct_io = true;
+ break;
+ case WritebackAsync:
+ m_async = true;
+ /* FALLTHROUGH */
+ case Writeback:
+ m_init_flags |= FUSE_WRITEBACK_CACHE;
+ /* FALLTHROUGH */
+ case Writethrough:
+ break;
+ default:
+ FAIL() << "Unknown cache mode";
+ }
+
+ FuseTest::SetUp();
+ if (IsSkipped())
+ return;
+}
+};
+
+class PosixFallocate: public Fallocate {
+public:
+static sig_atomic_t s_sigxfsz;
+
+void SetUp() {
+ s_sigxfsz = 0;
+ FuseTest::SetUp();
+}
+
+void TearDown() {
+ struct sigaction sa;
+
+ bzero(&sa, sizeof(sa));
+ sa.sa_handler = SIG_DFL;
+ sigaction(SIGXFSZ, &sa, NULL);
+
+ Fallocate::TearDown();
+}
+
+};
+
+sig_atomic_t PosixFallocate::s_sigxfsz = 0;
+
+void sigxfsz_handler(int __unused sig) {
+ PosixFallocate::s_sigxfsz = 1;
+}
+
+class PosixFallocate_7_18: public PosixFallocate {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 18;
+ PosixFallocate::SetUp();
+}
+};
+
+
+/*
+ * If the server returns ENOSYS, it indicates that the server does not support
+ * FUSE_FALLOCATE. This and future calls should fall back to vop_stddeallocate.
+ */
+TEST_F(Fspacectl, enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ off_t fsize = 1 << 20;
+ off_t off0 = 100;
+ off_t len0 = 500;
+ struct spacectl_range rqsr = { .r_offset = off0, .r_len = len0 };
+ uint64_t ino = 42;
+ uint64_t off1 = fsize;
+ uint64_t len1 = 1000;
+ off_t off2 = fsize / 2;
+ off_t len2 = 500;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ expect_fallocate(ino, off0, len0,
+ FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, ENOSYS);
+ expect_vop_stddeallocate(ino, off0, len0);
+ expect_vop_stddeallocate(ino, off2, len2);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
+
+ /* Subsequent calls shouldn't query the daemon either */
+ rqsr.r_offset = off2;
+ rqsr.r_len = len2;
+ EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
+
+ /* Neither should posix_fallocate query the daemon */
+ EXPECT_EQ(EINVAL, posix_fallocate(fd, off1, len1));
+
+ leak(fd);
+}
+
+/*
+ * EOPNOTSUPP means "the file system does not support fallocate with the
+ * supplied mode on this particular file". So we should fallback, but not
+ * assume anything about whether the operation will fail on a different file or
+ * with a different mode.
+ */
+TEST_F(Fspacectl, eopnotsupp)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct spacectl_range rqsr;
+ uint64_t ino = 42;
+ uint64_t fsize = 1 << 20;
+ uint64_t off0 = 500;
+ uint64_t len = 1000;
+ uint64_t off1 = fsize / 2;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ expect_fallocate(ino, off0, len,
+ FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE,
+ EOPNOTSUPP);
+ expect_vop_stddeallocate(ino, off0, len);
+ expect_fallocate(ino, off1, len,
+ FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE,
+ EOPNOTSUPP);
+ expect_vop_stddeallocate(ino, off1, len);
+ expect_fallocate(ino, fsize, len, 0, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /*
+ * Though the FUSE daemon will reject the call, the kernel should fall
+ * back to a read-modify-write approach.
+ */
+ rqsr.r_offset = off0;
+ rqsr.r_len = len;
+ EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
+
+ /* Subsequent calls should still query the daemon */
+ rqsr.r_offset = off1;
+ rqsr.r_len = len;
+ EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
+
+ /* But subsequent posix_fallocate calls _should_ query the daemon */
+ EXPECT_EQ(0, posix_fallocate(fd, fsize, len));
+
+ leak(fd);
+}
+
+TEST_F(Fspacectl, erofs)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct statfs statbuf;
+ uint64_t fsize = 2000;
+ struct spacectl_range rqsr = { .r_offset = 0, .r_len = 1 };
+ struct iovec *iov = NULL;
+ int iovlen = 0;
+ uint64_t ino = 42;
+ int fd;
+ int newflags;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_STATFS);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
+ {
+ /*
+ * All of the fields except f_flags are don't care, and f_flags
+ * is set by the VFS
+ */
+ SET_OUT_HEADER_LEN(out, statfs);
+ })));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /* Remount read-only */
+ ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
+ newflags = statbuf.f_flags | MNT_UPDATE | MNT_RDONLY;
+ build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1);
+ build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1);
+ build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1);
+ ASSERT_EQ(0, nmount(iov, iovlen, newflags)) << strerror(errno);
+ free_iovec(&iov, &iovlen);
+
+ EXPECT_EQ(-1, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
+ EXPECT_EQ(EROFS, errno);
+
+ leak(fd);
+}
+
+/*
+ * If FUSE_GETATTR fails when determining the size of the file, fspacectl
+ * should fail gracefully. This failure mode is easiest to trigger when
+ * attribute caching is disabled.
+ */
+TEST_F(Fspacectl, getattr_fails)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ Sequence seq;
+ struct spacectl_range rqsr;
+ const uint64_t ino = 42;
+ const uint64_t fsize = 2000;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1, 0);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(1)
+ .InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = ino;
+ out.body.attr.attr.mode = S_IFREG | 0644;
+ out.body.attr.attr.size = fsize;
+ out.body.attr.attr_valid = 0;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(ReturnErrno(EIO));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ rqsr.r_offset = 500;
+ rqsr.r_len = 1000;
+ EXPECT_EQ(-1, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
+ EXPECT_EQ(EIO, errno);
+
+ leak(fd);
+}
+
+TEST_F(Fspacectl, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct spacectl_range rqsr, rmsr;
+ struct stat sb0, sb1;
+ uint64_t ino = 42;
+ uint64_t fsize = 2000;
+ uint64_t offset = 500;
+ uint64_t length = 1000;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ expect_fallocate(ino, offset, length,
+ FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb0)) << strerror(errno);
+ rqsr.r_offset = offset;
+ rqsr.r_len = length;
+ EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr));
+ EXPECT_EQ(0, rmsr.r_len);
+ EXPECT_EQ((off_t)(offset + length), rmsr.r_offset);
+
+ /*
+ * The file's attributes should not have been invalidated, so this fstat
+ * will not requery the daemon.
+ */
+ EXPECT_EQ(0, fstat(fd, &sb1));
+ EXPECT_EQ(fsize, (uint64_t)sb1.st_size);
+
+ /* mtime and ctime should be updated */
+ EXPECT_EQ(sb0.st_atime, sb1.st_atime);
+ EXPECT_NE(sb0.st_mtime, sb1.st_mtime);
+ EXPECT_NE(sb0.st_ctime, sb1.st_ctime);
+
+ leak(fd);
+}
+
+/* The returned rqsr.r_off should be clipped at EoF */
+TEST_F(Fspacectl, past_eof)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct spacectl_range rqsr, rmsr;
+ uint64_t ino = 42;
+ uint64_t fsize = 1000;
+ uint64_t offset = 1500;
+ uint64_t length = 1000;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ expect_fallocate(ino, offset, length,
+ FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ rqsr.r_offset = offset;
+ rqsr.r_len = length;
+ EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr));
+ EXPECT_EQ(0, rmsr.r_len);
+ EXPECT_EQ((off_t)fsize, rmsr.r_offset);
+
+ leak(fd);
+}
+
+/* The returned rqsr.r_off should be clipped at EoF */
+TEST_F(Fspacectl, spans_eof)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct spacectl_range rqsr, rmsr;
+ uint64_t ino = 42;
+ uint64_t fsize = 1000;
+ uint64_t offset = 500;
+ uint64_t length = 1000;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ expect_fallocate(ino, offset, length,
+ FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ rqsr.r_offset = offset;
+ rqsr.r_len = length;
+ EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr));
+ EXPECT_EQ(0, rmsr.r_len);
+ EXPECT_EQ((off_t)fsize, rmsr.r_offset);
+
+ leak(fd);
+}
+
+/*
+ * With older servers, no FUSE_FALLOCATE should be attempted. The kernel
+ * should fall back to vop_stddeallocate.
+ */
+TEST_F(Fspacectl_7_18, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct spacectl_range rqsr, rmsr;
+ char *buf;
+ uint64_t ino = 42;
+ uint64_t fsize = 2000;
+ uint64_t offset = 500;
+ uint64_t length = 1000;
+ int fd;
+
+ buf = new char[length];
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ expect_vop_stddeallocate(ino, offset, length);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ rqsr.r_offset = offset;
+ rqsr.r_len = length;
+ EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr));
+ EXPECT_EQ(0, rmsr.r_len);
+ EXPECT_EQ((off_t)(offset + length), rmsr.r_offset);
+
+ leak(fd);
+ delete[] buf;
+}
+
+/*
+ * A successful fspacectl should clear the zeroed data from the kernel cache.
+ */
+TEST_P(FspacectlCache, clears_cache)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefghijklmnopqrstuvwxyz";
+ struct spacectl_range rqsr, rmsr;
+ uint64_t ino = 42;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint64_t fsize = bufsize;
+ uint8_t buf[bufsize];
+ char zbuf[bufsize];
+ uint64_t offset = 0;
+ uint64_t length = bufsize;
+ int fd;
+
+ bzero(zbuf, bufsize);
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ /* NB: expectations are applied in LIFO order */
+ expect_read(ino, 0, fsize, fsize, zbuf);
+ expect_read(ino, 0, fsize, fsize, CONTENTS);
+ expect_fallocate(ino, offset, length,
+ FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /* Populate the cache */
+ ASSERT_EQ(fsize, (uint64_t)pread(fd, buf, bufsize, 0))
+ << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, CONTENTS, fsize));
+
+ /* Zero the file */
+ rqsr.r_offset = offset;
+ rqsr.r_len = length;
+ EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr));
+ EXPECT_EQ(0, rmsr.r_len);
+ EXPECT_EQ((off_t)(offset + length), rmsr.r_offset);
+
+ /* Read again. This should query the daemon */
+ ASSERT_EQ(fsize, (uint64_t)pread(fd, buf, bufsize, 0))
+ << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, zbuf, fsize));
+
+ leak(fd);
+}
+
+INSTANTIATE_TEST_SUITE_P(FspacectlCache, FspacectlCache,
+ Values(Uncached, Writethrough, Writeback)
+);
+
+/*
+ * If the server returns ENOSYS, it indicates that the server does not support
+ * FUSE_FALLOCATE. This and future calls should return EINVAL.
+ */
+TEST_F(PosixFallocate, enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ uint64_t off0 = 0;
+ uint64_t len0 = 1000;
+ off_t off1 = 100;
+ off_t len1 = 200;
+ uint64_t fsize = 500;
+ struct spacectl_range rqsr = { .r_offset = off1, .r_len = len1 };
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ expect_fallocate(ino, off0, len0, 0, ENOSYS);
+ expect_vop_stddeallocate(ino, off1, len1);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ EXPECT_EQ(EINVAL, posix_fallocate(fd, off0, len0));
+
+ /* Subsequent calls shouldn't query the daemon*/
+ EXPECT_EQ(EINVAL, posix_fallocate(fd, off0, len0));
+
+ /* Neither should VOP_DEALLOCATE query the daemon */
+ EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
+
+ leak(fd);
+}
+
+/*
+ * EOPNOTSUPP means "the file system does not support fallocate with the
+ * supplied mode on this particular file". So we should fallback, but not
+ * assume anything about whether the operation will fail on a different file or
+ * with a different mode.
+ */
+TEST_F(PosixFallocate, eopnotsupp)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct spacectl_range rqsr;
+ uint64_t ino = 42;
+ uint64_t fsize = 2000;
+ uint64_t offset = 0;
+ uint64_t length = 1000;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ expect_fallocate(ino, fsize, length, 0, EOPNOTSUPP);
+ expect_fallocate(ino, offset, length, 0, EOPNOTSUPP);
+ expect_fallocate(ino, offset, length,
+ FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ EXPECT_EQ(EINVAL, posix_fallocate(fd, fsize, length));
+
+ /* Subsequent calls should still query the daemon*/
+ EXPECT_EQ(EINVAL, posix_fallocate(fd, offset, length));
+
+ /* And subsequent VOP_DEALLOCATE calls should also query the daemon */
+ rqsr.r_len = length;
+ rqsr.r_offset = offset;
+ EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
+
+ leak(fd);
+}
+
+/* EIO is not a permanent error, and may be retried */
+TEST_F(PosixFallocate, eio)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ uint64_t offset = 0;
+ uint64_t length = 1000;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_open(ino, 0, 1);
+ expect_fallocate(ino, offset, length, 0, EIO);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ EXPECT_EQ(EIO, posix_fallocate(fd, offset, length));
+
+ expect_fallocate(ino, offset, length, 0, 0);
+
+ EXPECT_EQ(0, posix_fallocate(fd, offset, length));
+
+ leak(fd);
+}
+
+TEST_F(PosixFallocate, erofs)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct statfs statbuf;
+ struct iovec *iov = NULL;
+ int iovlen = 0;
+ uint64_t ino = 42;
+ uint64_t offset = 0;
+ uint64_t length = 1000;
+ int fd;
+ int newflags;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_STATFS);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
+ {
+ /*
+ * All of the fields except f_flags are don't care, and f_flags
+ * is set by the VFS
+ */
+ SET_OUT_HEADER_LEN(out, statfs);
+ })));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /* Remount read-only */
+ ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
+ newflags = statbuf.f_flags | MNT_UPDATE | MNT_RDONLY;
+ build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1);
+ build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1);
+ build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1);
+ ASSERT_EQ(0, nmount(iov, iovlen, newflags)) << strerror(errno);
+ free_iovec(&iov, &iovlen);
+
+ EXPECT_EQ(EROFS, posix_fallocate(fd, offset, length));
+
+ leak(fd);
+}
+
+TEST_F(PosixFallocate, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct stat sb0, sb1;
+ uint64_t ino = 42;
+ uint64_t offset = 0;
+ uint64_t length = 1000;
+ int fd;
+
+ 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.entry_valid = UINT64_MAX;
+ out.body.entry.attr_valid = UINT64_MAX;
+ })));
+ expect_open(ino, 0, 1);
+ expect_fallocate(ino, offset, length, 0, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb0)) << strerror(errno);
+ EXPECT_EQ(0, posix_fallocate(fd, offset, length));
+ /*
+ * Despite the originally cached file size of zero, stat should now
+ * return either the new size or requery the daemon.
+ */
+ EXPECT_EQ(0, stat(FULLPATH, &sb1));
+ EXPECT_EQ(length, (uint64_t)sb1.st_size);
+
+ /* mtime and ctime should be updated */
+ EXPECT_EQ(sb0.st_atime, sb1.st_atime);
+ EXPECT_NE(sb0.st_mtime, sb1.st_mtime);
+ EXPECT_NE(sb0.st_ctime, sb1.st_ctime);
+
+ leak(fd);
+}
+
+/* fusefs should respect RLIMIT_FSIZE */
+TEST_F(PosixFallocate, rlimit_fsize)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct rlimit rl;
+ uint64_t ino = 42;
+ uint64_t offset = 0;
+ uint64_t length = 1'000'000;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_open(ino, 0, 1);
+
+ rl.rlim_cur = length / 2;
+ rl.rlim_max = 10 * length;
+ ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno);
+ ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ EXPECT_EQ(EFBIG, posix_fallocate(fd, offset, length));
+ EXPECT_EQ(1, s_sigxfsz);
+
+ leak(fd);
+}
+
+/* With older servers, no FUSE_FALLOCATE should be attempted */
+TEST_F(PosixFallocate_7_18, einval)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ uint64_t offset = 0;
+ uint64_t length = 1000;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_open(ino, 0, 1);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ EXPECT_EQ(EINVAL, posix_fallocate(fd, offset, length));
+
+ leak(fd);
+}
diff --git a/tests/sys/fs/fusefs/fifo.cc b/tests/sys/fs/fusefs/fifo.cc
new file mode 100644
index 000000000000..3ec21f3f779b
--- /dev/null
+++ b/tests/sys/fs/fusefs/fifo.cc
@@ -0,0 +1,211 @@
+/*-
+ * 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/socket.h>
+#include <sys/un.h>
+#include <fcntl.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+const char FULLPATH[] = "mountpoint/some_fifo";
+const char RELPATH[] = "some_fifo";
+const char MESSAGE[] = "Hello, World!\n";
+const int msgsize = sizeof(MESSAGE);
+
+class Fifo: public FuseTest {
+public:
+pthread_t m_child;
+
+Fifo(): m_child(NULL) {};
+
+void TearDown() {
+ if (m_child != NULL) {
+ pthread_join(m_child, NULL);
+ }
+ FuseTest::TearDown();
+}
+};
+
+class Socket: public Fifo {};
+
+/* Writer thread */
+static void* writer(void* arg) {
+ ssize_t sent = 0;
+ int fd;
+
+ fd = *(int*)arg;
+ while (sent < msgsize) {
+ ssize_t r;
+
+ r = write(fd, MESSAGE + sent, msgsize - sent);
+ if (r < 0)
+ return (void*)(intptr_t)errno;
+ else
+ sent += r;
+
+ }
+ return 0;
+}
+
+/*
+ * Reading and writing FIFOs works. None of the I/O actually goes through FUSE
+ */
+TEST_F(Fifo, read_write)
+{
+ mode_t mode = S_IFIFO | 0755;
+ const int bufsize = 80;
+ char message[bufsize];
+ ssize_t recvd = 0, r;
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino, mode, 0, 1);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, pthread_create(&m_child, NULL, writer, &fd))
+ << strerror(errno);
+ while (recvd < msgsize) {
+ r = read(fd, message + recvd, bufsize - recvd);
+ ASSERT_LE(0, r) << strerror(errno);
+ ASSERT_LT(0, r) << "unexpected EOF";
+ recvd += r;
+ }
+ ASSERT_STREQ(message, MESSAGE);
+
+ leak(fd);
+}
+
+/* Writer thread */
+static void* socket_writer(void* arg __unused) {
+ ssize_t sent = 0;
+ int fd, err;
+ struct sockaddr_un sa;
+
+ fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (fd < 0) {
+ perror("socket");
+ return (void*)(intptr_t)errno;
+ }
+ sa.sun_family = AF_UNIX;
+ strlcpy(sa.sun_path, FULLPATH, sizeof(sa.sun_path));
+ sa.sun_len = sizeof(FULLPATH);
+ err = connect(fd, (struct sockaddr*)&sa, sizeof(sa));
+ if (err < 0) {
+ perror("connect");
+ return (void*)(intptr_t)errno;
+ }
+
+ while (sent < msgsize) {
+ ssize_t r;
+
+ r = write(fd, MESSAGE + sent, msgsize - sent);
+ if (r < 0)
+ return (void*)(intptr_t)errno;
+ else
+ sent += r;
+
+ }
+
+ FuseTest::leak(fd);
+ return 0;
+}
+
+/*
+ * Reading and writing unix-domain sockets works. None of the I/O actually
+ * goes through FUSE.
+ */
+TEST_F(Socket, read_write)
+{
+ mode_t mode = S_IFSOCK | 0755;
+ const int bufsize = 80;
+ char message[bufsize];
+ struct sockaddr_un sa;
+ ssize_t recvd = 0, r;
+ uint64_t ino = 42;
+ int fd, connected;
+ Sequence seq;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_MKNOD);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr_valid = UINT64_MAX;
+ })));
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ ASSERT_LE(0, fd) << strerror(errno);
+ sa.sun_family = AF_UNIX;
+ strlcpy(sa.sun_path, FULLPATH, sizeof(sa.sun_path));
+ sa.sun_len = sizeof(FULLPATH);
+ ASSERT_EQ(0, bind(fd, (struct sockaddr*)&sa, sizeof(sa)))
+ << strerror(errno);
+ listen(fd, 5);
+ ASSERT_EQ(0, pthread_create(&m_child, NULL, socket_writer, NULL))
+ << strerror(errno);
+ connected = accept(fd, 0, 0);
+ ASSERT_LE(0, connected) << strerror(errno);
+
+ while (recvd < msgsize) {
+ r = read(connected, message + recvd, bufsize - recvd);
+ ASSERT_LE(0, r) << strerror(errno);
+ ASSERT_LT(0, r) << "unexpected EOF";
+ recvd += r;
+ }
+ ASSERT_STREQ(message, MESSAGE);
+
+ leak(fd);
+}
diff --git a/tests/sys/fs/fusefs/flush.cc b/tests/sys/fs/fusefs/flush.cc
new file mode 100644
index 000000000000..7ba1218b3287
--- /dev/null
+++ b/tests/sys/fs/fusefs/flush.cc
@@ -0,0 +1,272 @@
+/*-
+ * 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 <fcntl.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Flush: public FuseTest {
+
+public:
+void
+expect_flush(uint64_t ino, int times, pid_t lo, ProcessMockerT r)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_FLUSH &&
+ in.header.nodeid == ino &&
+ in.body.flush.lock_owner == (uint64_t)lo &&
+ in.body.flush.fh == FH);
+ }, Eq(true)),
+ _)
+ ).Times(times)
+ .WillRepeatedly(Invoke(r));
+}
+
+void expect_lookup(const char *relpath, uint64_t ino, int times)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, times);
+}
+
+/*
+ * When testing FUSE_FLUSH, the FUSE_RELEASE calls are uninteresting. This
+ * expectation will silence googlemock warnings
+ */
+void expect_release()
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_RELEASE);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnErrno(0)));
+}
+};
+
+class FlushWithLocks: public Flush {
+ virtual void SetUp() {
+ m_init_flags = FUSE_POSIX_LOCKS;
+ Flush::SetUp();
+ }
+};
+
+/*
+ * If multiple file descriptors refer to the same file handle, closing each
+ * should send FUSE_FLUSH
+ */
+TEST_F(Flush, open_twice)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd, fd2;
+
+ expect_lookup(RELPATH, ino, 2);
+ expect_open(ino, 0, 1);
+ expect_flush(ino, 2, getpid(), ReturnErrno(0));
+ expect_release();
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ fd2 = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd2) << strerror(errno);
+
+ EXPECT_EQ(0, close(fd2)) << strerror(errno);
+ EXPECT_EQ(0, close(fd)) << strerror(errno);
+}
+
+/**
+ * Test for FOPEN_NOFLUSH: we expect that zero flush calls will be performed.
+ */
+TEST_F(Flush, open_noflush)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ uint64_t pid = (uint64_t)getpid();
+ int fd;
+
+ expect_lookup(RELPATH, ino, 1);
+ expect_open(ino, FOPEN_NOFLUSH, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_FLUSH &&
+ in.header.nodeid == ino &&
+ in.body.flush.lock_owner == pid &&
+ in.body.flush.fh == FH);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+ expect_release();
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ // close MUST not flush
+ EXPECT_EQ(0, close(fd)) << strerror(errno);
+}
+
+/*
+ * Some FUSE filesystem cache data internally and flush it on release. Such
+ * filesystems may generate errors during release. On Linux, these get
+ * returned by close(2). However, POSIX does not require close(2) to return
+ * this error. FreeBSD's fuse(4) should return EIO if it returns an error at
+ * all.
+ */
+/* http://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html */
+TEST_F(Flush, eio)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino, 1);
+ expect_open(ino, 0, 1);
+ expect_flush(ino, 1, getpid(), ReturnErrno(EIO));
+ expect_release();
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_TRUE(0 == close(fd) || errno == EIO) << strerror(errno);
+}
+
+/*
+ * If the filesystem returns ENOSYS, it will be treated as success and
+ * no more FUSE_FLUSH operations will be sent to the daemon
+ */
+TEST_F(Flush, enosys)
+{
+ 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";
+ uint64_t ino0 = 42;
+ uint64_t ino1 = 43;
+ int fd0, fd1;
+
+ expect_lookup(RELPATH0, ino0, 1);
+ expect_open(ino0, 0, 1);
+ /* On the 2nd close, FUSE_FLUSH won't be sent at all */
+ expect_flush(ino0, 1, getpid(), ReturnErrno(ENOSYS));
+ expect_release();
+
+ expect_lookup(RELPATH1, ino1, 1);
+ expect_open(ino1, 0, 1);
+ /* On the 2nd close, FUSE_FLUSH won't be sent at all */
+ expect_release();
+
+ fd0 = open(FULLPATH0, O_WRONLY);
+ ASSERT_LE(0, fd0) << strerror(errno);
+
+ fd1 = open(FULLPATH1, O_WRONLY);
+ ASSERT_LE(0, fd1) << strerror(errno);
+
+ EXPECT_EQ(0, close(fd0)) << strerror(errno);
+ EXPECT_EQ(0, close(fd1)) << strerror(errno);
+}
+
+/* A FUSE_FLUSH should be sent on close(2) */
+TEST_F(Flush, flush)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino, 1);
+ expect_open(ino, 0, 1);
+ expect_flush(ino, 1, getpid(), ReturnErrno(0));
+ expect_release();
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_TRUE(0 == close(fd)) << strerror(errno);
+}
+
+/*
+ * When closing a file with a POSIX file lock, flush should release the lock,
+ * _even_if_ it's not the process's last file descriptor for this file.
+ */
+TEST_F(FlushWithLocks, unlock_on_close)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd, fd2;
+ struct flock fl;
+ pid_t pid = getpid();
+
+ expect_lookup(RELPATH, ino, 2);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_SETLK &&
+ in.header.nodeid == ino &&
+ in.body.setlk.lk.type == F_RDLCK &&
+ in.body.setlk.fh == FH);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(0)));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_SETLK &&
+ in.header.nodeid == ino &&
+ in.body.setlk.lk.type == F_UNLCK &&
+ in.body.setlk.fh == FH);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(0)));
+ expect_flush(ino, 1, pid, ReturnErrno(0));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ fl.l_start = 0;
+ fl.l_len = 0;
+ fl.l_pid = pid;
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_SET;
+ fl.l_sysid = 0;
+ ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno);
+
+ fd2 = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd2) << strerror(errno);
+ ASSERT_EQ(0, close(fd2)) << strerror(errno);
+ leak(fd);
+ leak(fd2);
+}
diff --git a/tests/sys/fs/fusefs/forget.cc b/tests/sys/fs/fusefs/forget.cc
new file mode 100644
index 000000000000..1e7764ac4782
--- /dev/null
+++ b/tests/sys/fs/fusefs/forget.cc
@@ -0,0 +1,177 @@
+/*-
+ * 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/mount.h>
+
+#include <fcntl.h>
+#include <semaphore.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Forget: public FuseTest {
+public:
+void SetUp() {
+ if (geteuid() != 0)
+ GTEST_SKIP() << "Only root may use " << reclaim_mib;
+
+ FuseTest::SetUp();
+}
+
+};
+
+/*
+ * When a fusefs vnode is reclaimed, it should send a FUSE_FORGET operation.
+ */
+TEST_F(Forget, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ mode_t mode = S_IFREG | 0755;
+ sem_t sem;
+
+ ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .Times(3)
+ .WillRepeatedly(Invoke(
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ })));
+ expect_forget(ino, 3, &sem);
+
+ /*
+ * access(2) the file to force a lookup. Access it twice to double its
+ * lookup count.
+ */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+
+ reclaim_vnode(FULLPATH);
+
+ sem_wait(&sem);
+ sem_destroy(&sem);
+}
+
+/*
+ * When a directory is reclaimed, the names of its entries vanish from the
+ * namecache
+ */
+TEST_F(Forget, invalidate_names)
+{
+ const char FULLFPATH[] = "mountpoint/some_dir/some_file.txt";
+ const char FULLDPATH[] = "mountpoint/some_dir";
+ const char DNAME[] = "some_dir";
+ const char FNAME[] = "some_file.txt";
+ uint64_t dir_ino = 42;
+ uint64_t file_ino = 43;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, DNAME)
+ .Times(2)
+ .WillRepeatedly(Invoke(
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = dir_ino;
+ out.body.entry.attr.nlink = 2;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ /*
+ * Even though we don't reclaim FNAME and its entry is cacheable, we
+ * should get two lookups because the reclaim of DNAME will invalidate
+ * the cached FNAME entry.
+ */
+ EXPECT_LOOKUP(dir_ino, FNAME)
+ .Times(2)
+ .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 = file_ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ expect_forget(dir_ino, 1);
+
+ /* Access the file to cache its name */
+ ASSERT_EQ(0, access(FULLFPATH, F_OK)) << strerror(errno);
+
+ /* Reclaim the directory, invalidating its children from namecache */
+ reclaim_vnode(FULLDPATH);
+
+ /* Access the file again, causing another lookup */
+ ASSERT_EQ(0, access(FULLFPATH, F_OK)) << strerror(errno);
+}
+
+/*
+ * Reclaiming the root inode should not send a FUSE_FORGET request, nor should
+ * it interfere with further lookup operations.
+ */
+TEST_F(Forget, root)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ mode_t mode = S_IFREG | 0755;
+
+ 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 = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ /* access(2) the file to force a lookup. */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+
+ reclaim_vnode("mountpoint");
+ nap();
+
+ /* Access it again, to make sure it's still possible. */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+}
diff --git a/tests/sys/fs/fusefs/fsync.cc b/tests/sys/fs/fusefs/fsync.cc
new file mode 100644
index 000000000000..d6f4e6f70da0
--- /dev/null
+++ b/tests/sys/fs/fusefs/fsync.cc
@@ -0,0 +1,285 @@
+/*-
+ * 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 <aio.h>
+#include <fcntl.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+/*
+ * TODO: remove FUSE_FSYNC_FDATASYNC definition when upgrading to protocol 7.28.
+ * This bit was actually part of kernel protocol version 5.2, but never
+ * documented until after 7.28
+ */
+#ifndef FUSE_FSYNC_FDATASYNC
+#define FUSE_FSYNC_FDATASYNC 1
+#endif
+
+class Fsync: public FuseTest {
+public:
+void expect_fsync(uint64_t ino, uint32_t flags, int error, int times = 1)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_FSYNC &&
+ in.header.nodeid == ino &&
+ /*
+ * TODO: reenable pid check after fixing
+ * bug 236379
+ */
+ //(pid_t)in.header.pid == getpid() &&
+ in.body.fsync.fh == FH &&
+ in.body.fsync.fsync_flags == flags);
+ }, Eq(true)),
+ _)
+ ).Times(times)
+ .WillRepeatedly(Invoke(ReturnErrno(error)));
+}
+
+void expect_lookup(const char *relpath, uint64_t ino, int times = 1)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, times);
+}
+
+void expect_write(uint64_t ino, uint64_t size, const void *contents)
+{
+ FuseTest::expect_write(ino, 0, size, size, 0, 0, contents);
+}
+
+};
+
+class AioFsync: public Fsync {
+virtual void SetUp() {
+ if (!is_unsafe_aio_enabled())
+ GTEST_SKIP() <<
+ "vfs.aio.enable_unsafe must be set for this test";
+ FuseTest::SetUp();
+}
+};
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */
+TEST_F(AioFsync, aio_fsync)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ ssize_t bufsize = strlen(CONTENTS);
+ uint64_t ino = 42;
+ struct aiocb iocb, *piocb;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_write(ino, bufsize, CONTENTS);
+ expect_fsync(ino, 0, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+
+ bzero(&iocb, sizeof(iocb));
+ iocb.aio_fildes = fd;
+
+ ASSERT_EQ(0, aio_fsync(O_SYNC, &iocb)) << strerror(errno);
+ ASSERT_EQ(0, aio_waitcomplete(&piocb, NULL)) << strerror(errno);
+
+ leak(fd);
+}
+
+/*
+ * fuse(4) should NOT fsync during VOP_RELEASE or VOP_INACTIVE
+ *
+ * This test only really make sense in writeback caching mode, but it should
+ * still pass in any cache mode.
+ */
+TEST_F(Fsync, close)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ ssize_t bufsize = strlen(CONTENTS);
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_write(ino, bufsize, CONTENTS);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_SETATTR);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = ino; // Must match nodeid
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_FSYNC);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+ expect_flush(ino, 1, ReturnErrno(0));
+ expect_release(ino, FH);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ close(fd);
+}
+
+TEST_F(Fsync, eio)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ ssize_t bufsize = strlen(CONTENTS);
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_write(ino, bufsize, CONTENTS);
+ expect_fsync(ino, FUSE_FSYNC_FDATASYNC, EIO);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ ASSERT_NE(0, fdatasync(fd));
+ ASSERT_EQ(EIO, errno);
+
+ leak(fd);
+}
+
+/*
+ * If the filesystem returns ENOSYS, it will be treated as success and
+ * subsequent calls to VOP_FSYNC will succeed automatically without being sent
+ * to the filesystem daemon
+ */
+TEST_F(Fsync, enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ ssize_t bufsize = strlen(CONTENTS);
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_write(ino, bufsize, CONTENTS);
+ expect_fsync(ino, FUSE_FSYNC_FDATASYNC, ENOSYS);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ EXPECT_EQ(0, fdatasync(fd));
+
+ /* Subsequent calls shouldn't query the daemon*/
+ EXPECT_EQ(0, fdatasync(fd));
+ leak(fd);
+}
+
+
+TEST_F(Fsync, fdatasync)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ ssize_t bufsize = strlen(CONTENTS);
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_write(ino, bufsize, CONTENTS);
+ expect_fsync(ino, FUSE_FSYNC_FDATASYNC, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, fdatasync(fd)) << strerror(errno);
+
+ leak(fd);
+}
+
+TEST_F(Fsync, fsync)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ ssize_t bufsize = strlen(CONTENTS);
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_write(ino, bufsize, CONTENTS);
+ expect_fsync(ino, 0, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, fsync(fd)) << strerror(errno);
+
+ leak(fd);
+}
+
+/* If multiple FUSE file handles are active, we must fsync them all */
+TEST_F(Fsync, two_handles)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ ssize_t bufsize = strlen(CONTENTS);
+ uint64_t ino = 42;
+ int fd1, fd2;
+
+ expect_lookup(RELPATH, ino, 2);
+ expect_open(ino, 0, 2);
+ expect_write(ino, bufsize, CONTENTS);
+ expect_fsync(ino, 0, 0, 2);
+
+ fd1 = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd1) << strerror(errno);
+ fd2 = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd2) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd1, CONTENTS, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, fsync(fd1)) << strerror(errno);
+
+ leak(fd1);
+ leak(fd2);
+}
diff --git a/tests/sys/fs/fusefs/fsyncdir.cc b/tests/sys/fs/fusefs/fsyncdir.cc
new file mode 100644
index 000000000000..aef93a0fb1d5
--- /dev/null
+++ b/tests/sys/fs/fusefs/fsyncdir.cc
@@ -0,0 +1,195 @@
+/*-
+ * 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 <aio.h>
+#include <fcntl.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+/*
+ * TODO: remove FUSE_FSYNC_FDATASYNC definition when upgrading to protocol 7.28.
+ * This bit was actually part of kernel protocol version 5.2, but never
+ * documented until after 7.28
+ */
+#ifndef FUSE_FSYNC_FDATASYNC
+#define FUSE_FSYNC_FDATASYNC 1
+#endif
+
+class FsyncDir: public FuseTest {
+public:
+void expect_fsyncdir(uint64_t ino, uint32_t flags, int error)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_FSYNCDIR &&
+ in.header.nodeid == ino &&
+ /*
+ * TODO: reenable pid check after fixing
+ * bug 236379
+ */
+ //(pid_t)in.header.pid == getpid() &&
+ in.body.fsyncdir.fh == FH &&
+ in.body.fsyncdir.fsync_flags == flags);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(error)));
+}
+
+void expect_lookup(const char *relpath, uint64_t ino)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFDIR | 0755, 0, 1);
+}
+
+};
+
+class AioFsyncDir: public FsyncDir {
+virtual void SetUp() {
+ if (!is_unsafe_aio_enabled())
+ GTEST_SKIP() <<
+ "vfs.aio.enable_unsafe must be set for this test";
+ FuseTest::SetUp();
+}
+};
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */
+TEST_F(AioFsyncDir, aio_fsync)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ struct aiocb iocb, *piocb;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+ expect_fsyncdir(ino, 0, 0);
+
+ fd = open(FULLPATH, O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ bzero(&iocb, sizeof(iocb));
+ iocb.aio_fildes = fd;
+
+ ASSERT_EQ(0, aio_fsync(O_SYNC, &iocb)) << strerror(errno);
+ ASSERT_EQ(0, aio_waitcomplete(&piocb, NULL)) << strerror(errno);
+
+ leak(fd);
+}
+
+TEST_F(FsyncDir, eio)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+ expect_fsyncdir(ino, 0, EIO);
+
+ fd = open(FULLPATH, O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_NE(0, fsync(fd));
+ ASSERT_EQ(EIO, errno);
+
+ leak(fd);
+}
+
+/*
+ * If the filesystem returns ENOSYS, it will be treated as success and
+ * subsequent calls to VOP_FSYNC will succeed automatically without being sent
+ * to the filesystem daemon
+ */
+TEST_F(FsyncDir, enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+ expect_fsyncdir(ino, 0, ENOSYS);
+
+ fd = open(FULLPATH, O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ EXPECT_EQ(0, fsync(fd)) << strerror(errno);
+
+ /* Subsequent calls shouldn't query the daemon*/
+ EXPECT_EQ(0, fsync(fd)) << strerror(errno);
+
+ leak(fd);
+}
+
+TEST_F(FsyncDir, fsyncdata)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+ expect_fsyncdir(ino, FUSE_FSYNC_FDATASYNC, 0);
+
+ fd = open(FULLPATH, O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, fdatasync(fd)) << strerror(errno);
+
+ leak(fd);
+}
+
+/*
+ * Unlike regular files, the kernel doesn't know whether a directory is or
+ * isn't dirty, so fuse(4) should always send FUSE_FSYNCDIR on fsync(2)
+ */
+TEST_F(FsyncDir, fsync)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+ expect_fsyncdir(ino, 0, 0);
+
+ fd = open(FULLPATH, O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, fsync(fd)) << strerror(errno);
+
+ leak(fd);
+}
diff --git a/tests/sys/fs/fusefs/getattr.cc b/tests/sys/fs/fusefs/getattr.cc
new file mode 100644
index 000000000000..98a757fdff94
--- /dev/null
+++ b/tests/sys/fs/fusefs/getattr.cc
@@ -0,0 +1,367 @@
+/*-
+ * 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/param.h>
+
+#include <semaphore.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Getattr : public FuseTest {
+public:
+void expect_lookup(const char *relpath, uint64_t ino, mode_t mode,
+ uint64_t size, int times, uint64_t attr_valid, uint32_t attr_valid_nsec)
+{
+ EXPECT_LOOKUP(FUSE_ROOT_ID, relpath)
+ .Times(times)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = attr_valid;
+ out.body.entry.attr_valid_nsec = attr_valid_nsec;
+ out.body.entry.attr.size = size;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+}
+};
+
+class Getattr_7_8: public FuseTest {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 8;
+ FuseTest::SetUp();
+}
+};
+
+/*
+ * If getattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs
+ * should use the cached attributes, rather than query the daemon
+ */
+TEST_F(Getattr, attr_cache)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ struct stat sb;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .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;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr_valid = UINT64_MAX;
+ out.body.attr.attr.ino = ino; // Must match nodeid
+ out.body.attr.attr.mode = S_IFREG | 0644;
+ })));
+ EXPECT_EQ(0, stat(FULLPATH, &sb));
+ /* The second stat(2) should use cached attributes */
+ EXPECT_EQ(0, stat(FULLPATH, &sb));
+}
+
+/*
+ * If getattr returns a finite but non-zero cache timeout, then we should
+ * discard the cached attributes and requery the daemon after the timeout
+ * period passes.
+ */
+TEST_F(Getattr, attr_cache_timeout)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ struct stat sb;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1, 0, 0);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(2)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr_valid_nsec = NAP_NS / 2;
+ out.body.attr.attr_valid = 0;
+ out.body.attr.attr.ino = ino; // Must match nodeid
+ out.body.attr.attr.mode = S_IFREG | 0644;
+ })));
+
+ EXPECT_EQ(0, stat(FULLPATH, &sb));
+ nap();
+ /* Timeout has expired. stat(2) should requery the daemon */
+ EXPECT_EQ(0, stat(FULLPATH, &sb));
+}
+
+/*
+ * If attr.blksize is zero, then the kernel should use a default value for
+ * st_blksize
+ */
+TEST_F(Getattr, blksize_zero)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ struct stat sb;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 1, 1, 0, 0);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.mode = S_IFREG | 0644;
+ out.body.attr.attr.ino = ino; // Must match nodeid
+ out.body.attr.attr.blksize = 0;
+ out.body.attr.attr.size = 1;
+ })));
+
+ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
+ EXPECT_EQ((blksize_t)PAGE_SIZE, sb.st_blksize);
+}
+
+TEST_F(Getattr, enoent)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct stat sb;
+ const uint64_t ino = 42;
+ sem_t sem;
+
+ ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1, 0, 0);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENOENT)));
+ // Since FUSE_GETATTR returns ENOENT, the kernel will reclaim the vnode
+ // and send a FUSE_FORGET
+ expect_forget(ino, 1, &sem);
+
+ EXPECT_NE(0, stat(FULLPATH, &sb));
+ EXPECT_EQ(ENOENT, errno);
+
+ sem_wait(&sem);
+ sem_destroy(&sem);
+}
+
+TEST_F(Getattr, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ struct stat sb;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 1, 1, 0, 0);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.body.getattr.getattr_flags == 0 &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(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 = S_IFREG | 0644;
+ out.body.attr.attr.size = 1;
+ out.body.attr.attr.blocks = 2;
+ out.body.attr.attr.atime = 3;
+ out.body.attr.attr.mtime = 4;
+ out.body.attr.attr.ctime = 5;
+ out.body.attr.attr.atimensec = 6;
+ out.body.attr.attr.mtimensec = 7;
+ out.body.attr.attr.ctimensec = 8;
+ out.body.attr.attr.nlink = 9;
+ out.body.attr.attr.uid = 10;
+ out.body.attr.attr.gid = 11;
+ out.body.attr.attr.rdev = 12;
+ out.body.attr.attr.blksize = 12345;
+ })));
+
+ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
+ EXPECT_EQ(1, sb.st_size);
+ EXPECT_EQ(2, sb.st_blocks);
+ EXPECT_EQ(3, sb.st_atim.tv_sec);
+ EXPECT_EQ(6, sb.st_atim.tv_nsec);
+ EXPECT_EQ(4, sb.st_mtim.tv_sec);
+ EXPECT_EQ(7, sb.st_mtim.tv_nsec);
+ EXPECT_EQ(5, sb.st_ctim.tv_sec);
+ EXPECT_EQ(8, sb.st_ctim.tv_nsec);
+ EXPECT_EQ(9ull, sb.st_nlink);
+ EXPECT_EQ(10ul, sb.st_uid);
+ EXPECT_EQ(11ul, sb.st_gid);
+ EXPECT_EQ(12ul, sb.st_rdev);
+ EXPECT_EQ((blksize_t)12345, sb.st_blksize);
+ EXPECT_EQ(ino, sb.st_ino);
+ EXPECT_EQ(S_IFREG | 0644, sb.st_mode);
+
+ /*
+ * st_birthtim and st_flags are not supported by the fuse protocol.
+ * They're only supported as OS-specific extensions to OSX. For
+ * birthtime, the convention for "not supported" is "negative one
+ * second".
+ */
+ EXPECT_EQ(-1, sb.st_birthtim.tv_sec);
+ EXPECT_EQ(0, sb.st_birthtim.tv_nsec);
+ EXPECT_EQ(0u, sb.st_flags);
+}
+
+/*
+ * FUSE_GETATTR 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(Getattr, vtyp_conflict)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ struct stat sb;
+ 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 | 0644;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = 0;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.body.getattr.getattr_flags == 0 &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(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 = S_IFDIR | 0755; // Changed!
+ out.body.attr.attr.nlink = 2;
+ })));
+ // We should reclaim stale vnodes
+ expect_forget(ino, 1, &sem);
+
+ ASSERT_NE(0, stat(FULLPATH, &sb));
+ EXPECT_EQ(errno, ENOENT);
+
+ sem_wait(&sem);
+ sem_destroy(&sem);
+}
+
+TEST_F(Getattr_7_8, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ struct stat sb;
+
+ 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 | 0644;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr.size = 1;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([](auto i __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 | 0644;
+ out.body.attr.attr.size = 1;
+ out.body.attr.attr.blocks = 2;
+ out.body.attr.attr.atime = 3;
+ out.body.attr.attr.mtime = 4;
+ out.body.attr.attr.ctime = 5;
+ out.body.attr.attr.atimensec = 6;
+ out.body.attr.attr.mtimensec = 7;
+ out.body.attr.attr.ctimensec = 8;
+ out.body.attr.attr.nlink = 9;
+ out.body.attr.attr.uid = 10;
+ out.body.attr.attr.gid = 11;
+ out.body.attr.attr.rdev = 12;
+ })));
+
+ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
+ EXPECT_EQ(1, sb.st_size);
+ EXPECT_EQ(2, sb.st_blocks);
+ EXPECT_EQ(3, sb.st_atim.tv_sec);
+ EXPECT_EQ(6, sb.st_atim.tv_nsec);
+ EXPECT_EQ(4, sb.st_mtim.tv_sec);
+ EXPECT_EQ(7, sb.st_mtim.tv_nsec);
+ EXPECT_EQ(5, sb.st_ctim.tv_sec);
+ EXPECT_EQ(8, sb.st_ctim.tv_nsec);
+ EXPECT_EQ(9ull, sb.st_nlink);
+ EXPECT_EQ(10ul, sb.st_uid);
+ EXPECT_EQ(11ul, sb.st_gid);
+ EXPECT_EQ(12ul, sb.st_rdev);
+ EXPECT_EQ(ino, sb.st_ino);
+ EXPECT_EQ(S_IFREG | 0644, sb.st_mode);
+
+ //st_birthtim and st_flags are not supported by protocol 7.8. They're
+ //only supported as OS-specific extensions to OSX.
+}
diff --git a/tests/sys/fs/fusefs/interrupt.cc b/tests/sys/fs/fusefs/interrupt.cc
new file mode 100644
index 000000000000..3bfd4a834932
--- /dev/null
+++ b/tests/sys/fs/fusefs/interrupt.cc
@@ -0,0 +1,796 @@
+/*-
+ * 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/extattr.h>
+#include <sys/mman.h>
+#include <sys/wait.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <semaphore.h>
+#include <signal.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+/* Initial size of files used by these tests */
+const off_t FILESIZE = 1000;
+/* Access mode used by all directories in these tests */
+const mode_t MODE = 0755;
+const char FULLDIRPATH0[] = "mountpoint/some_dir";
+const char RELDIRPATH0[] = "some_dir";
+const char FULLDIRPATH1[] = "mountpoint/other_dir";
+const char RELDIRPATH1[] = "other_dir";
+
+static sem_t *blocked_semaphore;
+static sem_t *signaled_semaphore;
+
+static bool killer_should_sleep = false;
+
+/* Don't do anything; all we care about is that the syscall gets interrupted */
+void sigusr2_handler(int __unused sig) {
+ if (verbosity > 1) {
+ printf("Signaled! thread %p\n", pthread_self());
+ }
+
+}
+
+void* killer(void* target) {
+ /* Wait until the main thread is blocked in fdisp_wait_answ */
+ if (killer_should_sleep)
+ nap();
+ else
+ sem_wait(blocked_semaphore);
+ if (verbosity > 1)
+ printf("Signalling! thread %p\n", target);
+ pthread_kill((pthread_t)target, SIGUSR2);
+ if (signaled_semaphore != NULL)
+ sem_post(signaled_semaphore);
+
+ return(NULL);
+}
+
+class Interrupt: public FuseTest {
+public:
+pthread_t m_child;
+
+Interrupt(): m_child(NULL) {};
+
+void expect_lookup(const char *relpath, uint64_t ino)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, FILESIZE, 1);
+}
+
+/*
+ * Expect a FUSE_MKDIR but don't reply. Instead, just record the unique value
+ * to the provided pointer
+ */
+void expect_mkdir(uint64_t *mkdir_unique)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_MKDIR);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([=](auto in, auto &out __unused) {
+ *mkdir_unique = in.header.unique;
+ sem_post(blocked_semaphore);
+ }));
+}
+
+/*
+ * Expect a FUSE_READ but don't reply. Instead, just record the unique value
+ * to the provided pointer
+ */
+void expect_read(uint64_t ino, uint64_t *read_unique)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([=](auto in, auto &out __unused) {
+ *read_unique = in.header.unique;
+ sem_post(blocked_semaphore);
+ }));
+}
+
+/*
+ * Expect a FUSE_WRITE but don't reply. Instead, just record the unique value
+ * to the provided pointer
+ */
+void expect_write(uint64_t ino, uint64_t *write_unique)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_WRITE &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([=](auto in, auto &out __unused) {
+ *write_unique = in.header.unique;
+ sem_post(blocked_semaphore);
+ }));
+}
+
+void setup_interruptor(pthread_t target, bool sleep = false)
+{
+ ASSERT_NE(SIG_ERR, signal(SIGUSR2, sigusr2_handler)) << strerror(errno);
+ killer_should_sleep = sleep;
+ ASSERT_EQ(0, pthread_create(&m_child, NULL, killer, (void*)target))
+ << strerror(errno);
+}
+
+void SetUp() {
+ const int mprot = PROT_READ | PROT_WRITE;
+ const int mflags = MAP_ANON | MAP_SHARED;
+
+ signaled_semaphore = NULL;
+
+ blocked_semaphore = (sem_t*)mmap(NULL, sizeof(*blocked_semaphore),
+ mprot, mflags, -1, 0);
+ ASSERT_NE(MAP_FAILED, blocked_semaphore) << strerror(errno);
+ ASSERT_EQ(0, sem_init(blocked_semaphore, 1, 0)) << strerror(errno);
+ ASSERT_EQ(0, siginterrupt(SIGUSR2, 1));
+
+ FuseTest::SetUp();
+}
+
+void TearDown() {
+ struct sigaction sa;
+
+ if (m_child != NULL) {
+ pthread_join(m_child, NULL);
+ }
+ bzero(&sa, sizeof(sa));
+ sa.sa_handler = SIG_DFL;
+ sigaction(SIGUSR2, &sa, NULL);
+
+ sem_destroy(blocked_semaphore);
+ munmap(blocked_semaphore, sizeof(*blocked_semaphore));
+
+ FuseTest::TearDown();
+}
+};
+
+class Intr: public Interrupt {};
+
+class Nointr: public Interrupt {
+ void SetUp() {
+ m_nointr = true;
+ Interrupt::SetUp();
+ }
+};
+
+static void* mkdir0(void* arg __unused) {
+ ssize_t r;
+
+ r = mkdir(FULLDIRPATH0, MODE);
+ if (r >= 0)
+ return 0;
+ else
+ return (void*)(intptr_t)errno;
+}
+
+static void* read1(void* arg) {
+ const size_t bufsize = FILESIZE;
+ char buf[bufsize];
+ int fd = (int)(intptr_t)arg;
+ ssize_t r;
+
+ r = read(fd, buf, bufsize);
+ if (r >= 0)
+ return 0;
+ else
+ return (void*)(intptr_t)errno;
+}
+
+/*
+ * An interrupt operation that gets received after the original command is
+ * complete should generate an EAGAIN response.
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */
+TEST_F(Intr, already_complete)
+{
+ uint64_t ino = 42;
+ pthread_t self;
+ uint64_t mkdir_unique = 0;
+ Sequence seq;
+
+ self = pthread_self();
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
+ .InSequence(seq)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_mkdir(&mkdir_unique);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ return (in.header.opcode == FUSE_INTERRUPT &&
+ in.body.interrupt.unique == mkdir_unique);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([&](auto in, auto &out) {
+ // First complete the mkdir request
+ std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
+ out0->header.unique = mkdir_unique;
+ SET_OUT_HEADER_LEN(*out0, entry);
+ out0->body.create.entry.attr.mode = S_IFDIR | MODE;
+ out0->body.create.entry.nodeid = ino;
+ out.push_back(std::move(out0));
+
+ // Then, respond EAGAIN to the interrupt request
+ std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
+ out1->header.unique = in.header.unique;
+ out1->header.error = -EAGAIN;
+ out1->header.len = sizeof(out1->header);
+ out.push_back(std::move(out1));
+ }));
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
+ .InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | MODE;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 2;
+ })));
+
+ setup_interruptor(self);
+ EXPECT_EQ(0, mkdir(FULLDIRPATH0, MODE)) << strerror(errno);
+ /*
+ * The final syscall simply ensures that the test's main thread doesn't
+ * end before the daemon finishes responding to the FUSE_INTERRUPT.
+ */
+ EXPECT_EQ(0, access(FULLDIRPATH0, F_OK)) << strerror(errno);
+}
+
+/*
+ * If a FUSE file system returns ENOSYS for a FUSE_INTERRUPT operation, the
+ * kernel should not attempt to interrupt any other operations on that mount
+ * point.
+ */
+TEST_F(Intr, enosys)
+{
+ uint64_t ino0 = 42, ino1 = 43;;
+ uint64_t mkdir_unique;
+ pthread_t self, th0;
+ sem_t sem0, sem1;
+ void *thr0_value;
+ Sequence seq;
+
+ self = pthread_self();
+ ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
+ ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno);
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH1)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_mkdir(&mkdir_unique);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ return (in.header.opcode == FUSE_INTERRUPT &&
+ in.body.interrupt.unique == mkdir_unique);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(Invoke([&](auto in, auto &out) {
+ // reject FUSE_INTERRUPT and respond to the FUSE_MKDIR
+ std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
+ std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
+
+ out0->header.unique = in.header.unique;
+ out0->header.error = -ENOSYS;
+ out0->header.len = sizeof(out0->header);
+ out.push_back(std::move(out0));
+
+ SET_OUT_HEADER_LEN(*out1, entry);
+ out1->body.create.entry.attr.mode = S_IFDIR | MODE;
+ out1->body.create.entry.nodeid = ino1;
+ out1->header.unique = mkdir_unique;
+ out.push_back(std::move(out1));
+ }));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ return (in.header.opcode == FUSE_MKDIR);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(Invoke([&](auto in, auto &out) {
+ std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
+
+ sem_post(&sem0);
+ sem_wait(&sem1);
+
+ SET_OUT_HEADER_LEN(*out0, entry);
+ out0->body.create.entry.attr.mode = S_IFDIR | MODE;
+ out0->body.create.entry.nodeid = ino0;
+ out0->header.unique = in.header.unique;
+ out.push_back(std::move(out0));
+ }));
+
+ setup_interruptor(self);
+ /* First mkdir operation should finish synchronously */
+ ASSERT_EQ(0, mkdir(FULLDIRPATH1, MODE)) << strerror(errno);
+
+ ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL))
+ << strerror(errno);
+
+ sem_wait(&sem0);
+ /*
+ * th0 should be blocked waiting for the fuse daemon thread.
+ * Signal it. No FUSE_INTERRUPT should result
+ */
+ pthread_kill(th0, SIGUSR1);
+ /* Allow the daemon thread to proceed */
+ sem_post(&sem1);
+ pthread_join(th0, &thr0_value);
+ /* Second mkdir should've finished without error */
+ EXPECT_EQ(0, (intptr_t)thr0_value);
+}
+
+/*
+ * A FUSE filesystem is legally allowed to ignore INTERRUPT operations, and
+ * complete the original operation whenever it damn well pleases.
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */
+TEST_F(Intr, ignore)
+{
+ uint64_t ino = 42;
+ pthread_t self;
+ uint64_t mkdir_unique;
+
+ self = pthread_self();
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_mkdir(&mkdir_unique);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ return (in.header.opcode == FUSE_INTERRUPT &&
+ in.body.interrupt.unique == mkdir_unique);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([&](auto in __unused, auto &out) {
+ // Ignore FUSE_INTERRUPT; respond to the FUSE_MKDIR
+ std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
+ out0->header.unique = mkdir_unique;
+ SET_OUT_HEADER_LEN(*out0, entry);
+ out0->body.create.entry.attr.mode = S_IFDIR | MODE;
+ out0->body.create.entry.nodeid = ino;
+ out.push_back(std::move(out0));
+ }));
+
+ setup_interruptor(self);
+ ASSERT_EQ(0, mkdir(FULLDIRPATH0, MODE)) << strerror(errno);
+}
+
+/*
+ * A restartable operation (basically, anything except write or setextattr)
+ * that hasn't yet been sent to userland can be interrupted without sending
+ * FUSE_INTERRUPT, and will be automatically restarted.
+ */
+TEST_F(Intr, in_kernel_restartable)
+{
+ const char FULLPATH1[] = "mountpoint/other_file.txt";
+ const char RELPATH1[] = "other_file.txt";
+ uint64_t ino0 = 42, ino1 = 43;
+ int fd1;
+ pthread_t self, th0, th1;
+ sem_t sem0, sem1;
+ void *thr0_value, *thr1_value;
+
+ ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
+ ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno);
+ self = pthread_self();
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_lookup(RELPATH1, ino1);
+ expect_open(ino1, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_MKDIR);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([&](auto in __unused, auto& out) {
+ /* Let the next write proceed */
+ sem_post(&sem1);
+ /* Pause the daemon thread so it won't read the next op */
+ sem_wait(&sem0);
+
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.create.entry.attr.mode = S_IFDIR | MODE;
+ out.body.create.entry.nodeid = ino0;
+ })));
+ FuseTest::expect_read(ino1, 0, FILESIZE, 0, NULL);
+
+ fd1 = open(FULLPATH1, O_RDONLY);
+ ASSERT_LE(0, fd1) << strerror(errno);
+
+ /* Use a separate thread for each operation */
+ ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL))
+ << strerror(errno);
+
+ sem_wait(&sem1); /* Sequence the two operations */
+
+ ASSERT_EQ(0, pthread_create(&th1, NULL, read1, (void*)(intptr_t)fd1))
+ << strerror(errno);
+
+ setup_interruptor(self, true);
+
+ pause(); /* Wait for signal */
+
+ /* Unstick the daemon */
+ ASSERT_EQ(0, sem_post(&sem0)) << strerror(errno);
+
+ /* Wait awhile to make sure the signal generates no FUSE_INTERRUPT */
+ nap();
+
+ pthread_join(th1, &thr1_value);
+ pthread_join(th0, &thr0_value);
+ EXPECT_EQ(0, (intptr_t)thr1_value);
+ EXPECT_EQ(0, (intptr_t)thr0_value);
+ sem_destroy(&sem1);
+ sem_destroy(&sem0);
+
+ leak(fd1);
+}
+
+/*
+ * An operation that hasn't yet been sent to userland can be interrupted
+ * without sending FUSE_INTERRUPT. If it's a non-restartable operation (write
+ * or setextattr) it will return EINTR.
+ */
+TEST_F(Intr, in_kernel_nonrestartable)
+{
+ const char FULLPATH1[] = "mountpoint/other_file.txt";
+ const char RELPATH1[] = "other_file.txt";
+ const char value[] = "whatever";
+ ssize_t value_len = strlen(value) + 1;
+ uint64_t ino0 = 42, ino1 = 43;
+ int ns = EXTATTR_NAMESPACE_USER;
+ int fd1;
+ pthread_t self, th0;
+ sem_t sem0, sem1;
+ void *thr0_value;
+ ssize_t r;
+
+ ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
+ ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno);
+ self = pthread_self();
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_lookup(RELPATH1, ino1);
+ expect_open(ino1, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_MKDIR);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([&](auto in __unused, auto& out) {
+ /* Let the next write proceed */
+ sem_post(&sem1);
+ /* Pause the daemon thread so it won't read the next op */
+ sem_wait(&sem0);
+
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.create.entry.attr.mode = S_IFDIR | MODE;
+ out.body.create.entry.nodeid = ino0;
+ })));
+
+ fd1 = open(FULLPATH1, O_WRONLY);
+ ASSERT_LE(0, fd1) << strerror(errno);
+
+ /* Use a separate thread for the first write */
+ ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL))
+ << strerror(errno);
+
+ sem_wait(&sem1); /* Sequence the two operations */
+
+ setup_interruptor(self, true);
+
+ r = extattr_set_fd(fd1, ns, "foo", (const void*)value, value_len);
+ EXPECT_NE(0, r);
+ EXPECT_EQ(EINTR, errno);
+
+ /* Unstick the daemon */
+ ASSERT_EQ(0, sem_post(&sem0)) << strerror(errno);
+
+ /* Wait awhile to make sure the signal generates no FUSE_INTERRUPT */
+ nap();
+
+ pthread_join(th0, &thr0_value);
+ EXPECT_EQ(0, (intptr_t)thr0_value);
+ sem_destroy(&sem1);
+ sem_destroy(&sem0);
+
+ leak(fd1);
+}
+
+/*
+ * A syscall that gets interrupted while blocking on FUSE I/O should send a
+ * FUSE_INTERRUPT command to the fuse filesystem, which should then send EINTR
+ * in response to the _original_ operation. The kernel should ultimately
+ * return EINTR to userspace
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */
+TEST_F(Intr, in_progress)
+{
+ pthread_t self;
+ uint64_t mkdir_unique;
+
+ self = pthread_self();
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_mkdir(&mkdir_unique);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ return (in.header.opcode == FUSE_INTERRUPT &&
+ in.body.interrupt.unique == mkdir_unique);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([&](auto in __unused, auto &out) {
+ std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
+ out0->header.error = -EINTR;
+ out0->header.unique = mkdir_unique;
+ out0->header.len = sizeof(out0->header);
+ out.push_back(std::move(out0));
+ }));
+
+ setup_interruptor(self);
+ ASSERT_EQ(-1, mkdir(FULLDIRPATH0, MODE));
+ EXPECT_EQ(EINTR, errno);
+}
+
+/* Reads should also be interruptible */
+TEST_F(Intr, in_progress_read)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const size_t bufsize = 80;
+ char buf[bufsize];
+ uint64_t ino = 42;
+ int fd;
+ pthread_t self;
+ uint64_t read_unique;
+
+ self = pthread_self();
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_read(ino, &read_unique);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ return (in.header.opcode == FUSE_INTERRUPT &&
+ in.body.interrupt.unique == read_unique);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([&](auto in __unused, auto &out) {
+ std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
+ out0->header.error = -EINTR;
+ out0->header.unique = read_unique;
+ out0->header.len = sizeof(out0->header);
+ out.push_back(std::move(out0));
+ }));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ setup_interruptor(self);
+ ASSERT_EQ(-1, read(fd, buf, bufsize));
+ EXPECT_EQ(EINTR, errno);
+
+ leak(fd);
+}
+
+/*
+ * When mounted with -o nointr, fusefs will block signals while waiting for the
+ * server.
+ */
+TEST_F(Nointr, block)
+{
+ uint64_t ino = 42;
+ pthread_t self;
+ sem_t sem0;
+
+ ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
+ signaled_semaphore = &sem0;
+ self = pthread_self();
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_MKDIR);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([&](auto in __unused, auto& out) {
+ /* Let the killer proceed */
+ sem_post(blocked_semaphore);
+
+ /* Wait until after the signal has been sent */
+ sem_wait(signaled_semaphore);
+ /* Allow time for the mkdir thread to receive the signal */
+ nap();
+
+ /* Finally, complete the original op */
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.create.entry.attr.mode = S_IFDIR | MODE;
+ out.body.create.entry.nodeid = ino;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ return (in.header.opcode == FUSE_INTERRUPT);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+
+ setup_interruptor(self);
+ ASSERT_EQ(0, mkdir(FULLDIRPATH0, MODE)) << strerror(errno);
+
+ sem_destroy(&sem0);
+}
+
+/* FUSE_INTERRUPT operations should take priority over other pending ops */
+TEST_F(Intr, priority)
+{
+ Sequence seq;
+ uint64_t ino1 = 43;
+ uint64_t mkdir_unique;
+ pthread_t th0;
+ sem_t sem0, sem1;
+
+ ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
+ ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno);
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH1)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_MKDIR);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
+ mkdir_unique = in.header.unique;
+
+ /* Let the next mkdir proceed */
+ sem_post(&sem1);
+
+ /* Pause the daemon thread so it won't read the next op */
+ sem_wait(&sem0);
+
+ /* Finally, interrupt the original op */
+ out.header.error = -EINTR;
+ out.header.unique = mkdir_unique;
+ out.header.len = sizeof(out.header);
+ })));
+ /*
+ * FUSE_INTERRUPT should be received before the second FUSE_MKDIR,
+ * even though it was generated later
+ */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ return (in.header.opcode == FUSE_INTERRUPT &&
+ in.body.interrupt.unique == mkdir_unique);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(Invoke(ReturnErrno(EAGAIN)));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ return (in.header.opcode == FUSE_MKDIR);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.create.entry.attr.mode = S_IFDIR | MODE;
+ out.body.create.entry.nodeid = ino1;
+ })));
+
+ /* Use a separate thread for the first mkdir */
+ ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL))
+ << strerror(errno);
+
+ signaled_semaphore = &sem0;
+
+ sem_wait(&sem1); /* Sequence the two mkdirs */
+ setup_interruptor(th0, true);
+ ASSERT_EQ(0, mkdir(FULLDIRPATH1, MODE)) << strerror(errno);
+
+ pthread_join(th0, NULL);
+ sem_destroy(&sem1);
+ sem_destroy(&sem0);
+}
+
+/*
+ * If the FUSE filesystem receives the FUSE_INTERRUPT operation before
+ * processing the original, then it should wait for "some timeout" for the
+ * original operation to arrive. If not, it should send EAGAIN to the
+ * INTERRUPT operation, and the kernel should requeue the INTERRUPT.
+ *
+ * In this test, we'll pretend that the INTERRUPT arrives too soon, gets
+ * EAGAINed, then the kernel requeues it, and the second time around it
+ * successfully interrupts the original
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */
+TEST_F(Intr, too_soon)
+{
+ Sequence seq;
+ pthread_t self;
+ uint64_t mkdir_unique;
+
+ self = pthread_self();
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_mkdir(&mkdir_unique);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ return (in.header.opcode == FUSE_INTERRUPT &&
+ in.body.interrupt.unique == mkdir_unique);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(Invoke(ReturnErrno(EAGAIN)));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ return (in.header.opcode == FUSE_INTERRUPT &&
+ in.body.interrupt.unique == mkdir_unique);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(Invoke([&](auto in __unused, auto &out __unused) {
+ std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
+ out0->header.error = -EINTR;
+ out0->header.unique = mkdir_unique;
+ out0->header.len = sizeof(out0->header);
+ out.push_back(std::move(out0));
+ }));
+
+ setup_interruptor(self);
+ ASSERT_EQ(-1, mkdir(FULLDIRPATH0, MODE));
+ EXPECT_EQ(EINTR, errno);
+}
+
+
+// TODO: add a test where write returns EWOULDBLOCK
diff --git a/tests/sys/fs/fusefs/io.cc b/tests/sys/fs/fusefs/io.cc
new file mode 100644
index 000000000000..ced291836da0
--- /dev/null
+++ b/tests/sys/fs/fusefs/io.cc
@@ -0,0 +1,609 @@
+/*-
+ * 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/mman.h>
+
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+}
+
+#include <iomanip>
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+/*
+ * For testing I/O like fsx does, but deterministically and without a real
+ * underlying file system
+ */
+
+using namespace testing;
+
+const char FULLPATH[] = "mountpoint/some_file.txt";
+const char RELPATH[] = "some_file.txt";
+const uint64_t ino = 42;
+
+static void compare(const void *tbuf, const void *controlbuf, off_t baseofs,
+ ssize_t size)
+{
+ int i;
+
+ for (i = 0; i < size; i++) {
+ if (((const char*)tbuf)[i] != ((const char*)controlbuf)[i]) {
+ off_t ofs = baseofs + i;
+ FAIL() << "miscompare at offset "
+ << std::hex
+ << std::showbase
+ << ofs
+ << ". expected = "
+ << std::setw(2)
+ << (unsigned)((const uint8_t*)controlbuf)[i]
+ << " got = "
+ << (unsigned)((const uint8_t*)tbuf)[i];
+ }
+ }
+}
+
+typedef tuple<bool, uint32_t, cache_mode, uint32_t> IoParam;
+
+class Io: public FuseTest, public WithParamInterface<IoParam> {
+public:
+int m_backing_fd, m_control_fd, m_test_fd;
+off_t m_filesize;
+bool m_direct_io;
+
+Io(): m_backing_fd(-1), m_control_fd(-1), m_test_fd(-1), m_filesize(0),
+ m_direct_io(false) {};
+
+void SetUp()
+{
+ m_backing_fd = open("backing_file", O_RDWR | O_CREAT | O_TRUNC, 0644);
+ if (m_backing_fd < 0)
+ FAIL() << strerror(errno);
+ m_control_fd = open("control", O_RDWR | O_CREAT | O_TRUNC, 0644);
+ if (m_control_fd < 0)
+ FAIL() << strerror(errno);
+ srandom(22'9'1982); // Seed with my birthday
+
+ if (get<0>(GetParam()))
+ m_init_flags |= FUSE_ASYNC_READ;
+ m_maxwrite = get<1>(GetParam());
+ switch (get<2>(GetParam())) {
+ case Uncached:
+ m_direct_io = true;
+ break;
+ case WritebackAsync:
+ m_async = true;
+ /* FALLTHROUGH */
+ case Writeback:
+ m_init_flags |= FUSE_WRITEBACK_CACHE;
+ /* FALLTHROUGH */
+ case Writethrough:
+ break;
+ default:
+ FAIL() << "Unknown cache mode";
+ }
+ m_kernel_minor_version = get<3>(GetParam());
+ m_noatime = true; // To prevent SETATTR for atime on close
+
+ FuseTest::SetUp();
+ if (IsSkipped())
+ return;
+
+ if (verbosity > 0) {
+ printf("Test Parameters: init_flags=%#x maxwrite=%#x "
+ "%sasync cache=%s kernel_minor_version=%d\n",
+ m_init_flags, m_maxwrite, m_async? "" : "no",
+ cache_mode_to_s(get<2>(GetParam())),
+ m_kernel_minor_version);
+ }
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_open(ino, m_direct_io ? FOPEN_DIRECT_IO : 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_WRITE &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) {
+ const char *buf = (const char*)in.body.bytes +
+ sizeof(struct fuse_write_in);
+ ssize_t isize = in.body.write.size;
+ off_t iofs = in.body.write.offset;
+
+ assert((size_t)isize <= sizeof(in.body.bytes) -
+ sizeof(struct fuse_write_in));
+ ASSERT_EQ(isize, pwrite(m_backing_fd, buf, isize, iofs))
+ << strerror(errno);
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = isize;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) {
+ ssize_t isize = in.body.write.size;
+ off_t iofs = in.body.write.offset;
+ void *buf = out.body.bytes;
+ ssize_t osize;
+
+ assert((size_t)isize <= sizeof(out.body.bytes));
+ osize = pread(m_backing_fd, buf, isize, iofs);
+ ASSERT_LE(0, osize) << strerror(errno);
+ out.header.len = sizeof(struct fuse_out_header) + osize;
+ })));
+ 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) {
+ ASSERT_EQ(0, ftruncate(m_backing_fd, in.body.setattr.size))
+ << strerror(errno);
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = ino;
+ out.body.attr.attr.mode = S_IFREG | 0755;
+ out.body.attr.attr.size = in.body.setattr.size;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+ /* Any test that close()s will send FUSE_FLUSH and FUSE_RELEASE */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_FLUSH &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnErrno(0)));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_RELEASE &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnErrno(0)));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
+ in.header.nodeid == ino &&
+ in.body.copy_file_range.nodeid_out == ino &&
+ in.body.copy_file_range.flags == 0);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) {
+ off_t off_in = in.body.copy_file_range.off_in;
+ off_t off_out = in.body.copy_file_range.off_out;
+ ASSERT_EQ((ssize_t)in.body.copy_file_range.len,
+ copy_file_range(m_backing_fd, &off_in, m_backing_fd,
+ &off_out, in.body.copy_file_range.len, 0));
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = in.body.copy_file_range.len;
+ })));
+ /* Claim that we don't support FUSE_LSEEK */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnErrno(ENOSYS)));
+
+ m_test_fd = open(FULLPATH, O_RDWR );
+ EXPECT_LE(0, m_test_fd) << strerror(errno);
+}
+
+void TearDown()
+{
+ if (m_test_fd >= 0)
+ close(m_test_fd);
+ if (m_backing_fd >= 0)
+ close(m_backing_fd);
+ if (m_control_fd >= 0)
+ close(m_control_fd);
+ FuseTest::TearDown();
+ leak(m_test_fd);
+}
+
+void do_closeopen()
+{
+ ASSERT_EQ(0, close(m_test_fd)) << strerror(errno);
+ m_test_fd = open("backing_file", O_RDWR);
+ ASSERT_LE(0, m_test_fd) << strerror(errno);
+
+ ASSERT_EQ(0, close(m_control_fd)) << strerror(errno);
+ m_control_fd = open("control", O_RDWR);
+ ASSERT_LE(0, m_control_fd) << strerror(errno);
+}
+
+void do_copy_file_range(off_t off_in, off_t off_out, size_t size)
+{
+ ssize_t r;
+ off_t test_off_in = off_in;
+ off_t test_off_out = off_out;
+ off_t test_size = size;
+ off_t control_off_in = off_in;
+ off_t control_off_out = off_out;
+ off_t control_size = size;
+
+ while (test_size > 0) {
+ r = copy_file_range(m_test_fd, &test_off_in, m_test_fd,
+ &test_off_out, test_size, 0);
+ ASSERT_GT(r, 0) << strerror(errno);
+ test_size -= r;
+ }
+ while (control_size > 0) {
+ r = copy_file_range(m_control_fd, &control_off_in, m_control_fd,
+ &control_off_out, control_size, 0);
+ ASSERT_GT(r, 0) << strerror(errno);
+ control_size -= r;
+ }
+ m_filesize = std::max(m_filesize, off_out + (off_t)size);
+}
+
+void do_ftruncate(off_t offs)
+{
+ ASSERT_EQ(0, ftruncate(m_test_fd, offs)) << strerror(errno);
+ ASSERT_EQ(0, ftruncate(m_control_fd, offs)) << strerror(errno);
+ m_filesize = offs;
+}
+
+void do_mapread(off_t offs, ssize_t size)
+{
+ char *control_buf;
+ void *p;
+ off_t pg_offset, page_mask;
+ size_t map_size;
+
+ page_mask = getpagesize() - 1;
+ pg_offset = offs & page_mask;
+ map_size = pg_offset + size;
+
+ p = mmap(NULL, map_size, PROT_READ, MAP_FILE | MAP_SHARED, m_test_fd,
+ offs - pg_offset);
+ ASSERT_NE(p, MAP_FAILED) << strerror(errno);
+
+ control_buf = new char[size];
+
+ ASSERT_EQ(size, pread(m_control_fd, control_buf, size, offs))
+ << strerror(errno);
+
+ compare((void*)((char*)p + pg_offset), control_buf, offs, size);
+
+ ASSERT_EQ(0, munmap(p, map_size)) << strerror(errno);
+ delete[] control_buf;
+}
+
+void do_read(off_t offs, ssize_t size)
+{
+ char *test_buf, *control_buf;
+ ssize_t r;
+
+ test_buf = new char[size];
+ control_buf = new char[size];
+
+ errno = 0;
+ r = pread(m_test_fd, test_buf, size, offs);
+ ASSERT_NE(-1, r) << strerror(errno);
+ ASSERT_EQ(size, r) << "unexpected short read";
+ r = pread(m_control_fd, control_buf, size, offs);
+ ASSERT_NE(-1, r) << strerror(errno);
+ ASSERT_EQ(size, r) << "unexpected short read";
+
+ compare(test_buf, control_buf, offs, size);
+
+ delete[] control_buf;
+ delete[] test_buf;
+}
+
+void do_mapwrite(off_t offs, ssize_t size)
+{
+ char *buf;
+ void *p;
+ off_t pg_offset, page_mask;
+ size_t map_size;
+ long i;
+
+ page_mask = getpagesize() - 1;
+ pg_offset = offs & page_mask;
+ map_size = pg_offset + size;
+
+ buf = new char[size];
+ for (i=0; i < size; i++)
+ buf[i] = random();
+
+ if (offs + size > m_filesize) {
+ /*
+ * Must manually extend. vm_mmap_vnode will not implicitly
+ * extend a vnode
+ */
+ do_ftruncate(offs + size);
+ }
+
+ p = mmap(NULL, map_size, PROT_READ | PROT_WRITE,
+ MAP_FILE | MAP_SHARED, m_test_fd, offs - pg_offset);
+ ASSERT_NE(p, MAP_FAILED) << strerror(errno);
+
+ bcopy(buf, (char*)p + pg_offset, size);
+ ASSERT_EQ(size, pwrite(m_control_fd, buf, size, offs))
+ << strerror(errno);
+
+ delete[] buf;
+ ASSERT_EQ(0, munmap(p, map_size)) << strerror(errno);
+}
+
+void do_write(off_t offs, ssize_t size)
+{
+ char *buf;
+ long i;
+
+ buf = new char[size];
+ for (i=0; i < size; i++)
+ buf[i] = random();
+
+ ASSERT_EQ(size, pwrite(m_test_fd, buf, size, offs ))
+ << strerror(errno);
+ ASSERT_EQ(size, pwrite(m_control_fd, buf, size, offs))
+ << strerror(errno);
+ m_filesize = std::max(m_filesize, offs + size);
+
+ delete[] buf;
+}
+
+};
+
+class IoCacheable: public Io {
+public:
+virtual void SetUp() {
+ Io::SetUp();
+}
+};
+
+class IoCopyFileRange: public Io {
+public:
+virtual void SetUp() {
+ Io::SetUp();
+}
+};
+
+/*
+ * Extend a file with dirty data in the last page of the last block.
+ *
+ * fsx -WR -P /tmp -S8 -N3 fsx.bin
+ */
+TEST_P(Io, extend_from_dirty_page)
+{
+ off_t wofs = 0x21a0;
+ ssize_t wsize = 0xf0a8;
+ off_t rofs = 0xb284;
+ ssize_t rsize = 0x9b22;
+ off_t truncsize = 0x28702;
+
+ do_write(wofs, wsize);
+ do_ftruncate(truncsize);
+ do_read(rofs, rsize);
+}
+
+/*
+ * mapwrite into a newly extended part of a file.
+ *
+ * fsx -c 100 -i 100 -l 524288 -o 131072 -N5 -P /tmp -S19 fsx.bin
+ */
+TEST_P(IoCacheable, extend_by_mapwrite)
+{
+ do_mapwrite(0x29a3a, 0x849e); /* [0x29a3a, 0x31ed7] */
+ do_mapwrite(0x3c7d8, 0x3994); /* [0x3c7d8, 0x4016b] */
+ do_read(0x30c16, 0xf556); /* [0x30c16, 0x4016b] */
+}
+
+/*
+ * When writing the last page of a file, it must be written synchronously.
+ * Otherwise the cached page can become invalid by a subsequent extend
+ * operation.
+ *
+ * fsx -WR -P /tmp -S642 -N3 fsx.bin
+ */
+TEST_P(Io, last_page)
+{
+ do_write(0x1134f, 0xcc77); /* [0x1134f, 0x1dfc5] */
+ do_write(0x2096a, 0xdfa7); /* [0x2096a, 0x2e910] */
+ do_read(0x1a3aa, 0xb5b7); /* [0x1a3aa, 0x25960] */
+}
+
+/*
+ * Read a hole using mmap
+ *
+ * fsx -c 100 -i 100 -l 524288 -o 131072 -N11 -P /tmp -S14 fsx.bin
+ */
+TEST_P(IoCacheable, mapread_hole)
+{
+ do_write(0xf205, 0x123b7); /* [0xf205, 0x215bb] */
+ do_mapread(0x2f4c, 0xeeea); /* [0x2f4c, 0x11e35] */
+}
+
+/*
+ * Read a hole from a block that contains some cached data.
+ *
+ * fsx -WR -P /tmp -S55 fsx.bin
+ */
+TEST_P(Io, read_hole_from_cached_block)
+{
+ off_t wofs = 0x160c5;
+ ssize_t wsize = 0xa996;
+ off_t rofs = 0x472e;
+ ssize_t rsize = 0xd8d5;
+
+ do_write(wofs, wsize);
+ do_read(rofs, rsize);
+}
+
+/*
+ * Truncating a file into a dirty buffer should not causing anything untoward
+ * to happen when that buffer is eventually flushed.
+ *
+ * fsx -WR -P /tmp -S839 -d -N6 fsx.bin
+ */
+TEST_P(Io, truncate_into_dirty_buffer)
+{
+ off_t wofs0 = 0x3bad7;
+ ssize_t wsize0 = 0x4529;
+ off_t wofs1 = 0xc30d;
+ ssize_t wsize1 = 0x5f77;
+ off_t truncsize0 = 0x10916;
+ off_t rofs = 0xdf17;
+ ssize_t rsize = 0x29ff;
+ off_t truncsize1 = 0x152b4;
+
+ do_write(wofs0, wsize0);
+ do_write(wofs1, wsize1);
+ do_ftruncate(truncsize0);
+ do_read(rofs, rsize);
+ do_ftruncate(truncsize1);
+ close(m_test_fd);
+}
+
+/*
+ * Truncating a file into a dirty buffer should not causing anything untoward
+ * to happen when that buffer is eventually flushed, even when the buffer's
+ * dirty_off is > 0.
+ *
+ * Based on this command with a few steps removed:
+ * fsx -WR -P /tmp -S677 -d -N8 fsx.bin
+ */
+TEST_P(Io, truncate_into_dirty_buffer2)
+{
+ off_t truncsize0 = 0x344f3;
+ off_t wofs = 0x2790c;
+ ssize_t wsize = 0xd86a;
+ off_t truncsize1 = 0x2de38;
+ off_t rofs2 = 0x1fd7a;
+ ssize_t rsize2 = 0xc594;
+ off_t truncsize2 = 0x31e71;
+
+ /* Sets the file size to something larger than the next write */
+ do_ftruncate(truncsize0);
+ /*
+ * Creates a dirty buffer. The part in lbn 2 doesn't flush
+ * synchronously.
+ */
+ do_write(wofs, wsize);
+ /* Truncates part of the dirty buffer created in step 2 */
+ do_ftruncate(truncsize1);
+ /* XXX ?I don't know why this is necessary? */
+ do_read(rofs2, rsize2);
+ /* Truncates the dirty buffer */
+ do_ftruncate(truncsize2);
+ close(m_test_fd);
+}
+
+/*
+ * Regression test for a bug introduced in r348931
+ *
+ * Sequence of operations:
+ * 1) The first write reads lbn so it can modify it
+ * 2) The first write flushes lbn 3 immediately because it's the end of file
+ * 3) The first write then flushes lbn 4 because it's the end of the file
+ * 4) The second write modifies the cached versions of lbn 3 and 4
+ * 5) The third write's getblkx invalidates lbn 4's B_CACHE because it's
+ * extending the buffer. Then it flushes lbn 4 because B_DELWRI was set but
+ * B_CACHE was clear.
+ * 6) fuse_write_biobackend erroneously called vfs_bio_clrbuf, putting the
+ * buffer into a weird write-only state. All read operations would return
+ * 0. Writes were apparently still processed, because the buffer's contents
+ * were correct when examined in a core dump.
+ * 7) The third write reads lbn 4 because cache is clear
+ * 9) uiomove dutifully copies new data into the buffer
+ * 10) The buffer's dirty is flushed to lbn 4
+ * 11) The read returns all zeros because of step 6.
+ *
+ * Based on:
+ * fsx -WR -l 524388 -o 131072 -P /tmp -S6456 -q fsx.bin
+ */
+TEST_P(Io, resize_a_valid_buffer_while_extending)
+{
+ do_write(0x36ee6, 0x14530); /* [0x36ee6, 0x4b415] */
+ do_write(0x33256, 0x1507c); /* [0x33256, 0x482d1] */
+ do_write(0x4c03d, 0x175c); /* [0x4c03d, 0x4d798] */
+ do_read(0x3599c, 0xe277); /* [0x3599c, 0x43c12] */
+ close(m_test_fd);
+}
+
+/*
+ * mmap of a suitable region could trigger a panic. I'm not sure what
+ * combination of size and offset counts as "suitable". Regression test for
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=276191
+ */
+TEST_P(IoCacheable, vnode_pager_generic_putpage_clean_block_at_eof)
+{
+ do_mapwrite(0x3b4e0, 0x1bbc3);
+}
+
+/*
+ * A copy_file_range that follows an mmap write to the input area needs to
+ * flush the mmap buffer first.
+ */
+TEST_P(IoCopyFileRange, copy_file_range_from_mapped_write)
+{
+ do_mapwrite(0, 0x1000);
+ do_copy_file_range(0, 0x1000, 0x1000);
+ do_read(0x1000, 0x1000);
+}
+
+
+INSTANTIATE_TEST_SUITE_P(Io, Io,
+ Combine(Bool(), /* async read */
+ Values(0x1000, 0x10000, 0x20000), /* m_maxwrite */
+ Values(Uncached, Writethrough, Writeback, WritebackAsync),
+ Values(28) /* kernel_minor_vers */
+ )
+);
+
+INSTANTIATE_TEST_SUITE_P(Io, IoCacheable,
+ Combine(Bool(), /* async read */
+ Values(0x1000, 0x10000, 0x20000), /* m_maxwrite */
+ Values(Writethrough, Writeback, WritebackAsync),
+ Values(28) /* kernel_minor_vers */
+ )
+);
+
+INSTANTIATE_TEST_SUITE_P(Io, IoCopyFileRange,
+ Combine(Values(true), /* async read */
+ Values(0x10000), /* m_maxwrite */
+ Values(Writethrough, Writeback, WritebackAsync),
+ Values(27, 28) /* kernel_minor_vers */
+ )
+);
diff --git a/tests/sys/fs/fusefs/last_local_modify.cc b/tests/sys/fs/fusefs/last_local_modify.cc
new file mode 100644
index 000000000000..6b8c19f1efc7
--- /dev/null
+++ b/tests/sys/fs/fusefs/last_local_modify.cc
@@ -0,0 +1,521 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2021 Alan Somers
+ *
+ * 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/param.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+
+#include <fcntl.h>
+#include <pthread.h>
+#include <semaphore.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+/*
+ * "Last Local Modify" bugs
+ *
+ * This file tests a class of race conditions caused by one thread fetching a
+ * file's size with FUSE_LOOKUP while another thread simultaneously modifies it
+ * with FUSE_SETATTR, FUSE_WRITE, FUSE_COPY_FILE_RANGE or similar. It's
+ * possible for the second thread to start later yet finish first. If that
+ * happens, the first thread must not override the size set by the second
+ * thread.
+ *
+ * FUSE_GETATTR is not vulnerable to the same race, because it is always called
+ * with the vnode lock held.
+ *
+ * A few other operations like FUSE_LINK can also trigger the same race but
+ * with the file's ctime instead of size. However, the consequences of an
+ * incorrect ctime are much less disastrous than an incorrect size, so fusefs
+ * does not attempt to prevent such races.
+ */
+
+enum Mutator {
+ VOP_ALLOCATE,
+ VOP_COPY_FILE_RANGE,
+ VOP_SETATTR,
+ VOP_WRITE,
+};
+
+/*
+ * Translate a poll method's string representation to the enum value.
+ * Using strings with ::testing::Values gives better output with
+ * --gtest_list_tests
+ */
+enum Mutator writer_from_str(const char* s) {
+ if (0 == strcmp("VOP_ALLOCATE", s))
+ return VOP_ALLOCATE;
+ else if (0 == strcmp("VOP_COPY_FILE_RANGE", s))
+ return VOP_COPY_FILE_RANGE;
+ else if (0 == strcmp("VOP_SETATTR", s))
+ return VOP_SETATTR;
+ else
+ return VOP_WRITE;
+}
+
+uint32_t fuse_op_from_mutator(enum Mutator mutator) {
+ switch(mutator) {
+ case VOP_ALLOCATE:
+ return(FUSE_FALLOCATE);
+ case VOP_COPY_FILE_RANGE:
+ return(FUSE_COPY_FILE_RANGE);
+ case VOP_SETATTR:
+ return(FUSE_SETATTR);
+ case VOP_WRITE:
+ return(FUSE_WRITE);
+ }
+}
+
+class LastLocalModify: public FuseTest, public WithParamInterface<const char*> {
+public:
+virtual void SetUp() {
+ m_init_flags = FUSE_EXPORT_SUPPORT;
+
+ FuseTest::SetUp();
+}
+};
+
+static void* allocate_th(void* arg) {
+ int fd;
+ ssize_t r;
+ sem_t *sem = (sem_t*) arg;
+
+ if (sem)
+ sem_wait(sem);
+
+ fd = open("mountpoint/some_file.txt", O_RDWR);
+ if (fd < 0)
+ return (void*)(intptr_t)errno;
+
+ r = posix_fallocate(fd, 0, 15);
+ LastLocalModify::leak(fd);
+ if (r >= 0)
+ return 0;
+ else
+ return (void*)(intptr_t)errno;
+}
+
+static void* copy_file_range_th(void* arg) {
+ ssize_t r;
+ int fd;
+ sem_t *sem = (sem_t*) arg;
+ off_t off_in = 0;
+ off_t off_out = 10;
+ ssize_t len = 5;
+
+ if (sem)
+ sem_wait(sem);
+ fd = open("mountpoint/some_file.txt", O_RDWR);
+ if (fd < 0)
+ return (void*)(intptr_t)errno;
+
+ r = copy_file_range(fd, &off_in, fd, &off_out, len, 0);
+ if (r >= 0) {
+ LastLocalModify::leak(fd);
+ return 0;
+ } else
+ return (void*)(intptr_t)errno;
+}
+
+static void* setattr_th(void* arg) {
+ int fd;
+ ssize_t r;
+ sem_t *sem = (sem_t*) arg;
+
+ if (sem)
+ sem_wait(sem);
+
+ fd = open("mountpoint/some_file.txt", O_RDWR);
+ if (fd < 0)
+ return (void*)(intptr_t)errno;
+
+ r = ftruncate(fd, 15);
+ LastLocalModify::leak(fd);
+ if (r >= 0)
+ return 0;
+ else
+ return (void*)(intptr_t)errno;
+}
+
+static void* write_th(void* arg) {
+ ssize_t r;
+ int fd;
+ sem_t *sem = (sem_t*) arg;
+ const char BUF[] = "abcdefghijklmn";
+
+ if (sem)
+ sem_wait(sem);
+ /*
+ * Open the file in direct mode.
+ * The race condition affects both direct and non-direct writes, and
+ * they have separate code paths. However, in the non-direct case, the
+ * kernel updates last_local_modify _before_ sending FUSE_WRITE to the
+ * server. So the technique that this test program uses to invoke the
+ * race cannot work. Therefore, test with O_DIRECT only.
+ */
+ fd = open("mountpoint/some_file.txt", O_RDWR | O_DIRECT);
+ if (fd < 0)
+ return (void*)(intptr_t)errno;
+
+ r = write(fd, BUF, sizeof(BUF));
+ if (r >= 0) {
+ LastLocalModify::leak(fd);
+ return 0;
+ } else
+ return (void*)(intptr_t)errno;
+}
+
+/*
+ * VOP_LOOKUP should discard attributes returned by the server if they were
+ * modified by another VOP while the VOP_LOOKUP was in progress.
+ *
+ * Sequence of operations:
+ * * Thread 1 calls a mutator like ftruncate, which acquires the vnode lock
+ * exclusively.
+ * * Thread 2 calls stat, which does VOP_LOOKUP, which sends FUSE_LOOKUP to the
+ * server. The server replies with the old file length. Thread 2 blocks
+ * waiting for the vnode lock.
+ * * Thread 1 sends the mutator operation like FUSE_SETATTR that changes the
+ * file's size and updates the attribute cache. Then it releases the vnode
+ * lock.
+ * * Thread 2 acquires the vnode lock. At this point it must not add the
+ * now-stale file size to the attribute cache.
+ *
+ * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071
+ */
+TEST_P(LastLocalModify, lookup)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ Sequence seq;
+ uint64_t ino = 3;
+ uint64_t mutator_unique;
+ const uint64_t oldsize = 10;
+ const uint64_t newsize = 15;
+ pthread_t th0;
+ void *thr0_value;
+ struct stat sb;
+ static sem_t sem;
+ Mutator mutator;
+ uint32_t mutator_op;
+ size_t mutator_size;
+
+ mutator = writer_from_str(GetParam());
+ mutator_op = fuse_op_from_mutator(mutator);
+
+ ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ /* Called by the mutator, caches attributes but not entries */
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.size = oldsize;
+ out.body.entry.attr_valid_nsec = NAP_NS / 2;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ })));
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == mutator_op &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(Invoke([&](auto in, auto &out __unused) {
+ /*
+ * The mutator changes the file size, but in order to simulate
+ * a race, don't reply. Instead, just save the unique for
+ * later.
+ */
+ mutator_unique = in.header.unique;
+ switch(mutator) {
+ case VOP_WRITE:
+ mutator_size = in.body.write.size;
+ break;
+ case VOP_COPY_FILE_RANGE:
+ mutator_size = in.body.copy_file_range.len;
+ break;
+ default:
+ break;
+ }
+ /* Allow the lookup thread to proceed */
+ sem_post(&sem);
+ }));
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .InSequence(seq)
+ .WillOnce(Invoke([&](auto in __unused, auto& out) {
+ std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
+ std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
+
+ /* First complete the lookup request, returning the old size */
+ out0->header.unique = in.header.unique;
+ SET_OUT_HEADER_LEN(*out0, entry);
+ out0->body.entry.attr.mode = S_IFREG | 0644;
+ out0->body.entry.nodeid = ino;
+ out0->body.entry.attr.ino = ino;
+ out0->body.entry.entry_valid = UINT64_MAX;
+ out0->body.entry.attr_valid = UINT64_MAX;
+ out0->body.entry.attr.size = oldsize;
+ out.push_back(std::move(out0));
+
+ /* Then, respond to the mutator request */
+ out1->header.unique = mutator_unique;
+ switch(mutator) {
+ case VOP_ALLOCATE:
+ out1->header.error = 0;
+ out1->header.len = sizeof(out1->header);
+ break;
+ case VOP_COPY_FILE_RANGE:
+ SET_OUT_HEADER_LEN(*out1, write);
+ out1->body.write.size = mutator_size;
+ break;
+ case VOP_SETATTR:
+ SET_OUT_HEADER_LEN(*out1, attr);
+ out1->body.attr.attr.ino = ino;
+ out1->body.attr.attr.mode = S_IFREG | 0644;
+ out1->body.attr.attr.size = newsize; // Changed size
+ out1->body.attr.attr_valid = UINT64_MAX;
+ break;
+ case VOP_WRITE:
+ SET_OUT_HEADER_LEN(*out1, write);
+ out1->body.write.size = mutator_size;
+ break;
+ }
+ out.push_back(std::move(out1));
+ }));
+
+ /* Start the mutator thread */
+ switch(mutator) {
+ case VOP_ALLOCATE:
+ ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th,
+ NULL)) << strerror(errno);
+ break;
+ case VOP_COPY_FILE_RANGE:
+ ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th,
+ NULL)) << strerror(errno);
+ break;
+ case VOP_SETATTR:
+ ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th, NULL))
+ << strerror(errno);
+ break;
+ case VOP_WRITE:
+ ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, NULL))
+ << strerror(errno);
+ break;
+ }
+
+
+ /* Wait for FUSE_SETATTR to be sent */
+ sem_wait(&sem);
+
+ /* Lookup again, which will race with the mutator */
+ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
+ ASSERT_EQ((off_t)newsize, sb.st_size);
+
+ /* ftruncate should've completed without error */
+ pthread_join(th0, &thr0_value);
+ EXPECT_EQ(0, (intptr_t)thr0_value);
+}
+
+/*
+ * VFS_VGET should discard attributes returned by the server if they were
+ * modified by another VOP while the VFS_VGET was in progress.
+ *
+ * Sequence of operations:
+ * * Thread 1 calls fhstat, entering VFS_VGET, and issues FUSE_LOOKUP
+ * * Thread 2 calls a mutator like ftruncate, which acquires the vnode lock
+ * exclusively and issues a FUSE op like FUSE_SETATTR.
+ * * Thread 1's FUSE_LOOKUP returns with the old size, but the thread blocks
+ * waiting for the vnode lock.
+ * * Thread 2's FUSE op returns, and that thread sets the file's new size
+ * in the attribute cache. Finally it releases the vnode lock.
+ * * The vnode lock acquired, thread 1 must not overwrite the attr cache's size
+ * with the old value.
+ *
+ * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071
+ */
+TEST_P(LastLocalModify, vfs_vget)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ Sequence seq;
+ uint64_t ino = 3;
+ uint64_t lookup_unique;
+ const uint64_t oldsize = 10;
+ const uint64_t newsize = 15;
+ pthread_t th0;
+ void *thr0_value;
+ struct stat sb;
+ static sem_t sem;
+ fhandle_t fhp;
+ Mutator mutator;
+ uint32_t mutator_op;
+
+ if (geteuid() != 0)
+ GTEST_SKIP() << "This test requires a privileged user";
+
+ mutator = writer_from_str(GetParam());
+ mutator_op = fuse_op_from_mutator(mutator);
+
+ ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .Times(1)
+ .InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
+ {
+ /* Called by getfh, caches attributes but not entries */
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.size = oldsize;
+ out.body.entry.attr_valid_nsec = NAP_NS / 2;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ })));
+ EXPECT_LOOKUP(ino, ".")
+ .InSequence(seq)
+ .WillOnce(Invoke([&](auto in, auto &out __unused) {
+ /* Called by fhstat. Block to simulate a race */
+ lookup_unique = in.header.unique;
+ sem_post(&sem);
+ }));
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .Times(1)
+ .InSequence(seq)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
+ {
+ /* Called by VOP_SETATTR, caches attributes but not entries */
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.size = oldsize;
+ out.body.entry.attr_valid_nsec = NAP_NS / 2;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ })));
+
+ /* Called by the mutator thread */
+ expect_open(ino, 0, 1);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == mutator_op &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(Invoke([&](auto in __unused, auto& out) {
+ std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
+ std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
+
+ /* First complete the lookup request, returning the old size */
+ out0->header.unique = lookup_unique;
+ SET_OUT_HEADER_LEN(*out0, entry);
+ out0->body.entry.attr.mode = S_IFREG | 0644;
+ out0->body.entry.nodeid = ino;
+ out0->body.entry.attr.ino = ino;
+ out0->body.entry.entry_valid = UINT64_MAX;
+ out0->body.entry.attr_valid = UINT64_MAX;
+ out0->body.entry.attr.size = oldsize;
+ out.push_back(std::move(out0));
+
+ /* Then, respond to the mutator request */
+ out1->header.unique = in.header.unique;
+ switch(mutator) {
+ case VOP_ALLOCATE:
+ out1->header.error = 0;
+ out1->header.len = sizeof(out1->header);
+ break;
+ case VOP_COPY_FILE_RANGE:
+ SET_OUT_HEADER_LEN(*out1, write);
+ out1->body.write.size = in.body.copy_file_range.len;
+ break;
+ case VOP_SETATTR:
+ SET_OUT_HEADER_LEN(*out1, attr);
+ out1->body.attr.attr.ino = ino;
+ out1->body.attr.attr.mode = S_IFREG | 0644;
+ out1->body.attr.attr.size = newsize; // Changed size
+ out1->body.attr.attr_valid = UINT64_MAX;
+ break;
+ case VOP_WRITE:
+ SET_OUT_HEADER_LEN(*out1, write);
+ out1->body.write.size = in.body.write.size;
+ break;
+ }
+ out.push_back(std::move(out1));
+ }));
+
+ /* First get a file handle */
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+
+ /* Start the mutator thread */
+ switch(mutator) {
+ case VOP_ALLOCATE:
+ ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th,
+ (void*)&sem)) << strerror(errno);
+ break;
+ case VOP_COPY_FILE_RANGE:
+ ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th,
+ (void*)&sem)) << strerror(errno);
+ break;
+ case VOP_SETATTR:
+ ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th,
+ (void*)&sem)) << strerror(errno);
+ break;
+ case VOP_WRITE:
+ ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, (void*)&sem))
+ << strerror(errno);
+ break;
+ }
+
+ /* Lookup again, which will race with setattr */
+ ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
+
+ ASSERT_EQ((off_t)newsize, sb.st_size);
+
+ /* mutator should've completed without error */
+ pthread_join(th0, &thr0_value);
+ EXPECT_EQ(0, (intptr_t)thr0_value);
+}
+
+
+INSTANTIATE_TEST_SUITE_P(LLM, LastLocalModify,
+ Values(
+ "VOP_ALLOCATE",
+ "VOP_COPY_FILE_RANGE",
+ "VOP_SETATTR",
+ "VOP_WRITE"
+ )
+);
diff --git a/tests/sys/fs/fusefs/link.cc b/tests/sys/fs/fusefs/link.cc
new file mode 100644
index 000000000000..ac77347cd03b
--- /dev/null
+++ b/tests/sys/fs/fusefs/link.cc
@@ -0,0 +1,280 @@
+/*-
+ * 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 <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Link: public FuseTest {
+public:
+void expect_link(uint64_t ino, const char *relpath, mode_t mode, uint32_t nlink)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes
+ + sizeof(struct fuse_link_in);
+ return (in.header.opcode == FUSE_LINK &&
+ in.body.link.oldnodeid == ino &&
+ (0 == strcmp(name, relpath)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.mode = mode;
+ out.body.entry.attr.nlink = nlink;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+}
+
+void expect_lookup(const char *relpath, uint64_t ino)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1);
+}
+};
+
+class Link_7_8: public FuseTest {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 8;
+ FuseTest::SetUp();
+}
+
+void expect_link(uint64_t ino, const char *relpath, mode_t mode, uint32_t nlink)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes
+ + sizeof(struct fuse_link_in);
+ return (in.header.opcode == FUSE_LINK &&
+ in.body.link.oldnodeid == ino &&
+ (0 == strcmp(name, relpath)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry_7_8);
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.mode = mode;
+ out.body.entry.attr.nlink = nlink;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+}
+
+void expect_lookup(const char *relpath, uint64_t ino)
+{
+ FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, 0, 1);
+}
+};
+
+/*
+ * A successful link should clear the parent directory's attribute cache,
+ * because the fuse daemon should update its mtime and ctime
+ */
+TEST_F(Link, clear_attr_cache)
+{
+ const char FULLPATH[] = "mountpoint/src";
+ const char RELPATH[] = "src";
+ const char FULLDST[] = "mountpoint/dst";
+ const char RELDST[] = "dst";
+ const uint64_t ino = 42;
+ mode_t mode = S_IFREG | 0644;
+ struct stat sb;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == FUSE_ROOT_ID);
+ }, Eq(true)),
+ _)
+ ).Times(2)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = FUSE_ROOT_ID;
+ out.body.attr.attr.mode = S_IFDIR | 0755;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
+
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ expect_link(ino, RELPATH, mode, 2);
+
+ EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
+ EXPECT_EQ(0, link(FULLDST, FULLPATH)) << strerror(errno);
+ EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
+}
+
+TEST_F(Link, emlink)
+{
+ const char FULLPATH[] = "mountpoint/lnk";
+ const char RELPATH[] = "lnk";
+ const char FULLDST[] = "mountpoint/dst";
+ const char RELDST[] = "dst";
+ uint64_t dst_ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_lookup(RELDST, dst_ino);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes
+ + sizeof(struct fuse_link_in);
+ return (in.header.opcode == FUSE_LINK &&
+ in.body.link.oldnodeid == dst_ino &&
+ (0 == strcmp(name, RELPATH)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(EMLINK)));
+
+ EXPECT_EQ(-1, link(FULLDST, FULLPATH));
+ EXPECT_EQ(EMLINK, errno);
+}
+
+/*
+ * A hard link should always have the same inode as its source. If it doesn't,
+ * then it's not a hard link.
+ */
+TEST_F(Link, bad_inode)
+{
+ const char FULLPATH[] = "mountpoint/src";
+ const char RELPATH[] = "src";
+ const char FULLDST[] = "mountpoint/dst";
+ const char RELDST[] = "dst";
+ const uint64_t src_ino = 42;
+ const uint64_t dst_ino = 43;
+ mode_t mode = S_IFREG | 0644;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = dst_ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes
+ + sizeof(struct fuse_link_in);
+ return (in.header.opcode == FUSE_LINK &&
+ in.body.link.oldnodeid == dst_ino &&
+ (0 == strcmp(name, RELPATH)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = src_ino;
+ out.body.entry.attr.mode = mode;
+ out.body.entry.attr.nlink = 2;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ ASSERT_EQ(-1, link(FULLDST, FULLPATH));
+ ASSERT_EQ(EIO, errno);
+}
+
+TEST_F(Link, ok)
+{
+ const char FULLPATH[] = "mountpoint/src";
+ const char RELPATH[] = "src";
+ const char FULLDST[] = "mountpoint/dst";
+ const char RELDST[] = "dst";
+ const uint64_t ino = 42;
+ mode_t mode = S_IFREG | 0644;
+ struct stat sb;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ expect_link(ino, RELPATH, mode, 2);
+
+ ASSERT_EQ(0, link(FULLDST, FULLPATH)) << strerror(errno);
+ // Check that the original file's nlink count has increased.
+ ASSERT_EQ(0, stat(FULLDST, &sb)) << strerror(errno);
+ EXPECT_EQ(2ul, sb.st_nlink);
+}
+
+TEST_F(Link_7_8, ok)
+{
+ const char FULLPATH[] = "mountpoint/src";
+ const char RELPATH[] = "src";
+ const char FULLDST[] = "mountpoint/dst";
+ const char RELDST[] = "dst";
+ const uint64_t ino = 42;
+ mode_t mode = S_IFREG | 0644;
+ struct stat sb;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry_7_8);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ expect_link(ino, RELPATH, mode, 2);
+
+ ASSERT_EQ(0, link(FULLDST, FULLPATH)) << strerror(errno);
+ // Check that the original file's nlink count has increased.
+ ASSERT_EQ(0, stat(FULLDST, &sb)) << strerror(errno);
+ EXPECT_EQ(2ul, sb.st_nlink);
+}
diff --git a/tests/sys/fs/fusefs/locks.cc b/tests/sys/fs/fusefs/locks.cc
new file mode 100644
index 000000000000..222217c3cfbe
--- /dev/null
+++ b/tests/sys/fs/fusefs/locks.cc
@@ -0,0 +1,730 @@
+/*-
+ * 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/file.h>
+#include <fcntl.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+/* This flag value should probably be defined in fuse_kernel.h */
+#define OFFSET_MAX 0x7fffffffffffffffLL
+
+using namespace testing;
+
+/* For testing filesystems without posix locking support */
+class Fallback: public FuseTest {
+public:
+
+void expect_lookup(const char *relpath, uint64_t ino, uint64_t size = 0)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, size, 1);
+}
+
+};
+
+/* For testing filesystems with posix locking support */
+class Locks: public Fallback {
+ virtual void SetUp() {
+ m_init_flags = FUSE_POSIX_LOCKS;
+ Fallback::SetUp();
+ }
+};
+
+class Fcntl: public Locks {
+public:
+void expect_setlk(uint64_t ino, pid_t pid, uint64_t start, uint64_t end,
+ uint32_t type, int err)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_SETLK &&
+ in.header.nodeid == ino &&
+ in.body.setlk.fh == FH &&
+ in.body.setlk.owner == (uint32_t)pid &&
+ in.body.setlk.lk.start == start &&
+ in.body.setlk.lk.end == end &&
+ in.body.setlk.lk.type == type &&
+ in.body.setlk.lk.pid == (uint64_t)pid);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(err)));
+}
+void expect_setlkw(uint64_t ino, pid_t pid, uint64_t start, uint64_t end,
+ uint32_t type, int err)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_SETLKW &&
+ in.header.nodeid == ino &&
+ in.body.setlkw.fh == FH &&
+ in.body.setlkw.owner == (uint32_t)pid &&
+ in.body.setlkw.lk.start == start &&
+ in.body.setlkw.lk.end == end &&
+ in.body.setlkw.lk.type == type &&
+ in.body.setlkw.lk.pid == (uint64_t)pid);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(err)));
+}
+};
+
+class Flock: public Locks {
+public:
+void expect_setlk(uint64_t ino, uint32_t type, int err)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_SETLK &&
+ in.header.nodeid == ino &&
+ in.body.setlk.fh == FH &&
+ /*
+ * The owner should be set to the address of
+ * the vnode. That's hard to verify.
+ */
+ /* in.body.setlk.owner == ??? && */
+ in.body.setlk.lk.type == type);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(err)));
+}
+};
+
+class FlockFallback: public Fallback {};
+class GetlkFallback: public Fallback {};
+class Getlk: public Fcntl {};
+class SetlkFallback: public Fallback {};
+class Setlk: public Fcntl {};
+class SetlkwFallback: public Fallback {};
+class Setlkw: public Fcntl {};
+
+/*
+ * If the fuse filesystem does not support flock locks, then the kernel should
+ * fall back to local locks.
+ */
+TEST_F(FlockFallback, local)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, flock(fd, LOCK_EX)) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * Even if the fuse file system supports POSIX locks, we must implement flock
+ * locks locally until protocol 7.17. Protocol 7.9 added partial buggy support
+ * but we won't implement that.
+ */
+TEST_F(Flock, local)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, flock(fd, LOCK_EX)) << strerror(errno);
+ leak(fd);
+}
+
+/* Set a new flock lock with FUSE_SETLK */
+/* TODO: enable after upgrading to protocol 7.17 */
+TEST_F(Flock, DISABLED_set)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_setlk(ino, F_WRLCK, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, flock(fd, LOCK_EX)) << strerror(errno);
+ leak(fd);
+}
+
+/* Fail to set a flock lock in non-blocking mode */
+/* TODO: enable after upgrading to protocol 7.17 */
+TEST_F(Flock, DISABLED_eagain)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_setlk(ino, F_WRLCK, EAGAIN);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_NE(0, flock(fd, LOCK_EX | LOCK_NB));
+ ASSERT_EQ(EAGAIN, errno);
+ leak(fd);
+}
+
+/*
+ * If the fuse filesystem does not support posix file locks, then the kernel
+ * should fall back to local locks.
+ */
+TEST_F(GetlkFallback, local)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ struct flock fl;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ fl.l_start = 10;
+ fl.l_len = 1000;
+ fl.l_pid = 0;
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_SET;
+ fl.l_sysid = 0;
+ ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * If the filesystem has no locks that fit the description, the filesystem
+ * should return F_UNLCK
+ */
+TEST_F(Getlk, no_locks)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ struct flock fl;
+ int fd;
+ pid_t pid = getpid();
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETLK &&
+ in.header.nodeid == ino &&
+ in.body.getlk.fh == FH &&
+ /*
+ * Though it seems useless, libfuse expects the
+ * owner and pid fields to be set during
+ * FUSE_GETLK.
+ */
+ in.body.getlk.owner == (uint32_t)pid &&
+ in.body.getlk.lk.pid == (uint64_t)pid &&
+ in.body.getlk.lk.start == 10 &&
+ in.body.getlk.lk.end == 1009 &&
+ in.body.getlk.lk.type == F_RDLCK);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) {
+ SET_OUT_HEADER_LEN(out, getlk);
+ out.body.getlk.lk = in.body.getlk.lk;
+ out.body.getlk.lk.type = F_UNLCK;
+ })));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ fl.l_start = 10;
+ fl.l_len = 1000;
+ fl.l_pid = 42;
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_SET;
+ fl.l_sysid = 42;
+ ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno);
+
+ /*
+ * If no lock is found that would prevent this lock from being created,
+ * the structure is left unchanged by this system call except for the
+ * lock type which is set to F_UNLCK.
+ */
+ ASSERT_EQ(F_UNLCK, fl.l_type);
+ ASSERT_EQ(fl.l_pid, 42);
+ ASSERT_EQ(fl.l_start, 10);
+ ASSERT_EQ(fl.l_len, 1000);
+ ASSERT_EQ(fl.l_whence, SEEK_SET);
+ ASSERT_EQ(fl.l_sysid, 42);
+
+ leak(fd);
+}
+
+/* A different pid does have a lock */
+TEST_F(Getlk, lock_exists)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ struct flock fl;
+ int fd;
+ pid_t pid = getpid();
+ pid_t pid2 = 1235;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETLK &&
+ in.header.nodeid == ino &&
+ in.body.getlk.fh == FH &&
+ /*
+ * Though it seems useless, libfuse expects the
+ * owner and pid fields to be set during
+ * FUSE_GETLK.
+ */
+ in.body.getlk.owner == (uint32_t)pid &&
+ in.body.getlk.lk.pid == (uint64_t)pid &&
+ in.body.getlk.lk.start == 10 &&
+ in.body.getlk.lk.end == 1009 &&
+ in.body.getlk.lk.type == F_RDLCK);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, getlk);
+ out.body.getlk.lk.start = 100;
+ out.body.getlk.lk.end = 199;
+ out.body.getlk.lk.type = F_WRLCK;
+ out.body.getlk.lk.pid = (uint32_t)pid2;;
+ })));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ fl.l_start = 10;
+ fl.l_len = 1000;
+ fl.l_pid = 0;
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_SET;
+ fl.l_sysid = 0;
+ ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno);
+ EXPECT_EQ(100, fl.l_start);
+ EXPECT_EQ(100, fl.l_len);
+ EXPECT_EQ(pid2, fl.l_pid);
+ EXPECT_EQ(F_WRLCK, fl.l_type);
+ EXPECT_EQ(SEEK_SET, fl.l_whence);
+ EXPECT_EQ(0, fl.l_sysid);
+ leak(fd);
+}
+
+/*
+ * F_GETLK with SEEK_CUR
+ */
+TEST_F(Getlk, seek_cur)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ struct flock fl;
+ int fd;
+ pid_t pid = getpid();
+
+ expect_lookup(RELPATH, ino, 1024);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETLK &&
+ in.header.nodeid == ino &&
+ in.body.getlk.fh == FH &&
+ /*
+ * Though it seems useless, libfuse expects the
+ * owner and pid fields to be set during
+ * FUSE_GETLK.
+ */
+ in.body.getlk.owner == (uint32_t)pid &&
+ in.body.getlk.lk.pid == (uint64_t)pid &&
+ in.body.getlk.lk.start == 500 &&
+ in.body.getlk.lk.end == 509 &&
+ in.body.getlk.lk.type == F_RDLCK);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, getlk);
+ out.body.getlk.lk.start = 400;
+ out.body.getlk.lk.end = 499;
+ out.body.getlk.lk.type = F_WRLCK;
+ out.body.getlk.lk.pid = (uint32_t)pid + 1;
+ })));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_NE(-1, lseek(fd, 500, SEEK_SET));
+
+ fl.l_start = 0;
+ fl.l_len = 10;
+ fl.l_pid = 42;
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_CUR;
+ fl.l_sysid = 0;
+ ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno);
+
+ /*
+ * After a successful F_GETLK request, the value of l_whence is
+ * SEEK_SET.
+ */
+ EXPECT_EQ(F_WRLCK, fl.l_type);
+ EXPECT_EQ(fl.l_pid, pid + 1);
+ EXPECT_EQ(fl.l_start, 400);
+ EXPECT_EQ(fl.l_len, 100);
+ EXPECT_EQ(fl.l_whence, SEEK_SET);
+ ASSERT_EQ(fl.l_sysid, 0);
+
+ leak(fd);
+}
+
+/*
+ * F_GETLK with SEEK_END
+ */
+TEST_F(Getlk, seek_end)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ struct flock fl;
+ int fd;
+ pid_t pid = getpid();
+
+ expect_lookup(RELPATH, ino, 1024);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETLK &&
+ in.header.nodeid == ino &&
+ in.body.getlk.fh == FH &&
+ /*
+ * Though it seems useless, libfuse expects the
+ * owner and pid fields to be set during
+ * FUSE_GETLK.
+ */
+ in.body.getlk.owner == (uint32_t)pid &&
+ in.body.getlk.lk.pid == (uint64_t)pid &&
+ in.body.getlk.lk.start == 512 &&
+ in.body.getlk.lk.end == 1023 &&
+ in.body.getlk.lk.type == F_RDLCK);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, getlk);
+ out.body.getlk.lk.start = 400;
+ out.body.getlk.lk.end = 499;
+ out.body.getlk.lk.type = F_WRLCK;
+ out.body.getlk.lk.pid = (uint32_t)pid + 1;
+ })));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_NE(-1, lseek(fd, 500, SEEK_SET));
+
+ fl.l_start = -512;
+ fl.l_len = 512;
+ fl.l_pid = 42;
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_END;
+ fl.l_sysid = 0;
+ ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno);
+
+ /*
+ * After a successful F_GETLK request, the value of l_whence is
+ * SEEK_SET.
+ */
+ EXPECT_EQ(F_WRLCK, fl.l_type);
+ EXPECT_EQ(fl.l_pid, pid + 1);
+ EXPECT_EQ(fl.l_start, 400);
+ EXPECT_EQ(fl.l_len, 100);
+ EXPECT_EQ(fl.l_whence, SEEK_SET);
+ ASSERT_EQ(fl.l_sysid, 0);
+
+ leak(fd);
+}
+
+/*
+ * If the fuse filesystem does not support posix file locks, then the kernel
+ * should fall back to local locks.
+ */
+TEST_F(SetlkFallback, local)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ struct flock fl;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ fl.l_start = 10;
+ fl.l_len = 1000;
+ fl.l_pid = getpid();
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_SET;
+ fl.l_sysid = 0;
+ ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno);
+ leak(fd);
+}
+
+/* Clear a lock with FUSE_SETLK */
+TEST_F(Setlk, clear)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ struct flock fl;
+ int fd;
+ pid_t pid = getpid();
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_setlk(ino, pid, 10, 1009, F_UNLCK, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ fl.l_start = 10;
+ fl.l_len = 1000;
+ fl.l_pid = 0;
+ fl.l_type = F_UNLCK;
+ fl.l_whence = SEEK_SET;
+ fl.l_sysid = 0;
+ ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno);
+ leak(fd);
+}
+
+/* Set a new lock with FUSE_SETLK */
+TEST_F(Setlk, set)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ struct flock fl;
+ int fd;
+ pid_t pid = getpid();
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_setlk(ino, pid, 10, 1009, F_RDLCK, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ fl.l_start = 10;
+ fl.l_len = 1000;
+ fl.l_pid = 0;
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_SET;
+ fl.l_sysid = 0;
+ ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno);
+ leak(fd);
+}
+
+/* l_len = 0 is a flag value that means to lock until EOF */
+TEST_F(Setlk, set_eof)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ struct flock fl;
+ int fd;
+ pid_t pid = getpid();
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_setlk(ino, pid, 10, OFFSET_MAX, F_RDLCK, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ fl.l_start = 10;
+ fl.l_len = 0;
+ fl.l_pid = 0;
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_SET;
+ fl.l_sysid = 0;
+ ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno);
+ leak(fd);
+}
+
+/* Set a new lock with FUSE_SETLK, using SEEK_CUR for l_whence */
+TEST_F(Setlk, set_seek_cur)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ struct flock fl;
+ int fd;
+ pid_t pid = getpid();
+
+ expect_lookup(RELPATH, ino, 1024);
+ expect_open(ino, 0, 1);
+ expect_setlk(ino, pid, 500, 509, F_RDLCK, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_NE(-1, lseek(fd, 500, SEEK_SET));
+
+ fl.l_start = 0;
+ fl.l_len = 10;
+ fl.l_pid = 0;
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_CUR;
+ fl.l_sysid = 0;
+ ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno);
+
+ leak(fd);
+}
+
+/* Set a new lock with FUSE_SETLK, using SEEK_END for l_whence */
+TEST_F(Setlk, set_seek_end)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ struct flock fl;
+ int fd;
+ pid_t pid = getpid();
+
+ expect_lookup(RELPATH, ino, 1024);
+ expect_open(ino, 0, 1);
+ expect_setlk(ino, pid, 1000, 1009, F_RDLCK, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ fl.l_start = -24;
+ fl.l_len = 10;
+ fl.l_pid = 0;
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_END;
+ fl.l_sysid = 0;
+ ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno);
+
+ leak(fd);
+}
+
+/* Fail to set a new lock with FUSE_SETLK due to a conflict */
+TEST_F(Setlk, eagain)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ struct flock fl;
+ int fd;
+ pid_t pid = getpid();
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_setlk(ino, pid, 10, 1009, F_RDLCK, EAGAIN);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ fl.l_start = 10;
+ fl.l_len = 1000;
+ fl.l_pid = 0;
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_SET;
+ fl.l_sysid = 0;
+ ASSERT_EQ(-1, fcntl(fd, F_SETLK, &fl));
+ ASSERT_EQ(EAGAIN, errno);
+ leak(fd);
+}
+
+/*
+ * If the fuse filesystem does not support posix file locks, then the kernel
+ * should fall back to local locks.
+ */
+TEST_F(SetlkwFallback, local)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ struct flock fl;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ fl.l_start = 10;
+ fl.l_len = 1000;
+ fl.l_pid = 0;
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_SET;
+ fl.l_sysid = 0;
+ ASSERT_NE(-1, fcntl(fd, F_SETLKW, &fl)) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * Set a new lock with FUSE_SETLK. If the lock is not available, then the
+ * command should block. But to the kernel, that's the same as just being
+ * slow, so we don't need a separate test method
+ */
+TEST_F(Setlkw, set)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ struct flock fl;
+ int fd;
+ pid_t pid = getpid();
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_setlkw(ino, pid, 10, 1009, F_RDLCK, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ fl.l_start = 10;
+ fl.l_len = 1000;
+ fl.l_pid = 0;
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_SET;
+ fl.l_sysid = 0;
+ ASSERT_NE(-1, fcntl(fd, F_SETLKW, &fl)) << strerror(errno);
+ leak(fd);
+}
diff --git a/tests/sys/fs/fusefs/lookup.cc b/tests/sys/fs/fusefs/lookup.cc
new file mode 100644
index 000000000000..2cfe888b6b08
--- /dev/null
+++ b/tests/sys/fs/fusefs/lookup.cc
@@ -0,0 +1,662 @@
+/*-
+ * 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/param.h>
+#include <sys/mount.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Lookup: public FuseTest {};
+
+class Lookup_7_8: public Lookup {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 8;
+ Lookup::SetUp();
+}
+};
+
+class LookupExportable: public Lookup {
+public:
+virtual void SetUp() {
+ m_init_flags = FUSE_EXPORT_SUPPORT;
+ Lookup::SetUp();
+}
+};
+
+/*
+ * If lookup returns a non-zero cache timeout, then subsequent VOP_GETATTRs
+ * should use the cached attributes, rather than query the daemon
+ */
+TEST_F(Lookup, attr_cache)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ const uint64_t generation = 13;
+ 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.nodeid = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.attr.ino = ino; // Must match nodeid
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ out.body.entry.attr.size = 1;
+ out.body.entry.attr.blocks = 2;
+ out.body.entry.attr.atime = 3;
+ out.body.entry.attr.mtime = 4;
+ out.body.entry.attr.ctime = 5;
+ out.body.entry.attr.atimensec = 6;
+ out.body.entry.attr.mtimensec = 7;
+ out.body.entry.attr.ctimensec = 8;
+ out.body.entry.attr.nlink = 9;
+ out.body.entry.attr.uid = 10;
+ out.body.entry.attr.gid = 11;
+ out.body.entry.attr.rdev = 12;
+ out.body.entry.generation = generation;
+ })));
+ /* stat(2) issues a VOP_LOOKUP followed by a VOP_GETATTR */
+ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
+ EXPECT_EQ(1, sb.st_size);
+ EXPECT_EQ(2, sb.st_blocks);
+ EXPECT_EQ(3, sb.st_atim.tv_sec);
+ EXPECT_EQ(6, sb.st_atim.tv_nsec);
+ EXPECT_EQ(4, sb.st_mtim.tv_sec);
+ EXPECT_EQ(7, sb.st_mtim.tv_nsec);
+ EXPECT_EQ(5, sb.st_ctim.tv_sec);
+ EXPECT_EQ(8, sb.st_ctim.tv_nsec);
+ EXPECT_EQ(9ull, sb.st_nlink);
+ EXPECT_EQ(10ul, sb.st_uid);
+ EXPECT_EQ(11ul, sb.st_gid);
+ EXPECT_EQ(12ul, sb.st_rdev);
+ EXPECT_EQ(ino, sb.st_ino);
+ EXPECT_EQ(S_IFREG | 0644, sb.st_mode);
+
+ // fuse(4) does not _yet_ support inode generations
+ //EXPECT_EQ(generation, sb.st_gen);
+
+ /*
+ * st_birthtim and st_flags are not supported by the fuse protocol.
+ * They're only supported as OS-specific extensions to OSX. For
+ * birthtime, the convention for "not supported" is "negative one
+ * second".
+ */
+ EXPECT_EQ(-1, sb.st_birthtim.tv_sec);
+ EXPECT_EQ(0, sb.st_birthtim.tv_nsec);
+ EXPECT_EQ(0u, sb.st_flags);
+}
+
+/*
+ * If lookup returns a finite but non-zero cache timeout, then we should discard
+ * the cached attributes and requery the daemon.
+ */
+TEST_F(Lookup, attr_cache_timeout)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ struct stat sb;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .Times(2)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr_valid_nsec = NAP_NS / 2;
+ out.body.entry.attr.ino = ino; // Must match nodeid
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ })));
+
+ /* access(2) will issue a VOP_LOOKUP and fill the attr cache */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+ /* Next access(2) will use the cached attributes */
+ nap();
+ /* The cache has timed out; VOP_GETATTR should query the daemon*/
+ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
+}
+
+TEST_F(Lookup, dot)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ /*
+ * access(2) is one of the few syscalls that will not (always) follow
+ * up a successful VOP_LOOKUP with another VOP.
+ */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+}
+
+TEST_F(Lookup, dotdot)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/..";
+ const char RELDIRPATH[] = "some_dir";
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = 14;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ /*
+ * access(2) is one of the few syscalls that will not (always) follow
+ * up a successful VOP_LOOKUP with another VOP.
+ */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+}
+
+/*
+ * Lookup ".." when that vnode's entry cache has timed out, but its child's
+ * hasn't. Since this file system doesn't set FUSE_EXPORT_SUPPORT, we have no
+ * choice but to use the cached entry, even though it expired.
+ */
+TEST_F(Lookup, dotdot_entry_cache_timeout)
+{
+ uint64_t foo_ino = 42;
+ uint64_t bar_ino = 43;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, "foo")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = foo_ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0; // immediate timeout
+ })));
+ EXPECT_LOOKUP(foo_ino, "bar")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = bar_ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ expect_opendir(bar_ino);
+
+ int fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno);
+}
+
+/*
+ * Lookup ".." for a vnode with no valid parent nid
+ * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259974
+ * Since the file system is not exportable, we have no choice but to return an
+ * error.
+ */
+TEST_F(Lookup, dotdot_no_parent_nid)
+{
+ uint64_t foo_ino = 42;
+ uint64_t bar_ino = 43;
+ int fd;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, "foo")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = foo_ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ EXPECT_LOOKUP(foo_ino, "bar")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = bar_ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPENDIR);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, open);
+ })));
+ expect_forget(foo_ino, 1, NULL);
+
+ fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ // Try (and fail) to unmount the file system, to reclaim the mountpoint
+ // and foo vnodes.
+ ASSERT_NE(0, unmount("mountpoint", 0));
+ EXPECT_EQ(EBUSY, errno);
+ nap(); // Because vnode reclamation is asynchronous
+ EXPECT_NE(0, faccessat(fd, "../..", F_OK, 0));
+ EXPECT_EQ(ESTALE, errno);
+}
+
+/*
+ * A daemon that returns an illegal error value should be handled gracefully.
+ * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263220
+ */
+TEST_F(Lookup, ejustreturn)
+{
+ const char FULLPATH[] = "mountpoint/does_not_exist";
+ const char RELPATH[] = "does_not_exist";
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ out.header.len = sizeof(out.header);
+ out.header.error = 2;
+ out.expected_errno = EINVAL;
+ })));
+
+ EXPECT_NE(0, access(FULLPATH, F_OK));
+
+ EXPECT_EQ(EIO, errno);
+}
+
+TEST_F(Lookup, enoent)
+{
+ const char FULLPATH[] = "mountpoint/does_not_exist";
+ const char RELPATH[] = "does_not_exist";
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ EXPECT_NE(0, access(FULLPATH, F_OK));
+ EXPECT_EQ(ENOENT, errno);
+}
+
+TEST_F(Lookup, enotdir)
+{
+ const char FULLPATH[] = "mountpoint/not_a_dir/some_file.txt";
+ const char RELPATH[] = "not_a_dir";
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ out.body.entry.nodeid = 42;
+ })));
+
+ ASSERT_EQ(-1, access(FULLPATH, F_OK));
+ ASSERT_EQ(ENOTDIR, errno);
+}
+
+/*
+ * If lookup returns a non-zero entry timeout, then subsequent VOP_LOOKUPs
+ * should use the cached inode rather than requery the daemon
+ */
+TEST_F(Lookup, entry_cache)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ out.body.entry.nodeid = 14;
+ })));
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+ /* The second access(2) should use the cache */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+}
+
+/*
+ * If the daemon returns an error of 0 and an inode of 0, that's a flag for
+ * "ENOENT and cache it" with the given entry_timeout
+ */
+TEST_F(Lookup, entry_cache_negative)
+{
+ struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0};
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, "does_not_exist")
+ .Times(1)
+ .WillOnce(Invoke(ReturnNegativeCache(&entry_valid)));
+
+ EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK));
+ EXPECT_EQ(ENOENT, errno);
+ EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK));
+ EXPECT_EQ(ENOENT, errno);
+}
+
+/* Negative entry caches should timeout, too */
+TEST_F(Lookup, entry_cache_negative_timeout)
+{
+ const char *RELPATH = "does_not_exist";
+ const char *FULLPATH = "mountpoint/does_not_exist";
+ struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = NAP_NS / 2};
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .Times(2)
+ .WillRepeatedly(Invoke(ReturnNegativeCache(&entry_valid)));
+
+ EXPECT_NE(0, access(FULLPATH, F_OK));
+ EXPECT_EQ(ENOENT, errno);
+
+ nap();
+
+ /* The cache has timed out; VOP_LOOKUP should requery the daemon*/
+ EXPECT_NE(0, access(FULLPATH, F_OK));
+ EXPECT_EQ(ENOENT, errno);
+}
+
+/*
+ * If lookup returns a finite but non-zero entry cache timeout, then we should
+ * discard the cached inode and requery the daemon
+ */
+TEST_F(Lookup, entry_cache_timeout)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .Times(2)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.entry_valid_nsec = NAP_NS / 2;
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ out.body.entry.nodeid = 14;
+ })));
+
+ /* access(2) will issue a VOP_LOOKUP and fill the entry cache */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+ /* Next access(2) will use the cached entry */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+ nap();
+ /* The cache has timed out; VOP_LOOKUP should requery the daemon*/
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+}
+
+TEST_F(Lookup, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+
+ 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 = 14;
+ })));
+ /*
+ * access(2) is one of the few syscalls that will not (always) follow
+ * up a successful VOP_LOOKUP with another VOP.
+ */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+}
+
+/*
+ * Lookup in a subdirectory of the fuse mount. The naughty server returns the
+ * same inode for the child as for the parent.
+ */
+TEST_F(Lookup, parent_inode)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/some_file.txt";
+ const char DIRPATH[] = "some_dir";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t dir_ino = 2;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, DIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = dir_ino;
+ })));
+ EXPECT_LOOKUP(dir_ino, 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 = dir_ino;
+ })));
+ /*
+ * access(2) is one of the few syscalls that will not (always) follow
+ * up a successful VOP_LOOKUP with another VOP.
+ */
+ ASSERT_EQ(-1, access(FULLPATH, F_OK));
+ ASSERT_EQ(EIO, errno);
+}
+
+// Lookup in a subdirectory of the fuse mount
+TEST_F(Lookup, subdir)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/some_file.txt";
+ const char DIRPATH[] = "some_dir";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t dir_ino = 2;
+ uint64_t file_ino = 3;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, DIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = dir_ino;
+ })));
+ EXPECT_LOOKUP(dir_ino, 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 = file_ino;
+ })));
+ /*
+ * access(2) is one of the few syscalls that will not (always) follow
+ * up a successful VOP_LOOKUP with another VOP.
+ */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+}
+
+/*
+ * The server returns two different vtypes for the same nodeid. This is
+ * technically allowed if the entry's cache has already expired.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=258022
+ */
+TEST_F(Lookup, vtype_conflict)
+{
+ const char FIRSTFULLPATH[] = "mountpoint/foo";
+ const char SECONDFULLPATH[] = "mountpoint/bar";
+ const char FIRSTRELPATH[] = "foo";
+ const char SECONDRELPATH[] = "bar";
+ uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, FIRSTRELPATH)
+ .WillOnce(Invoke(
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0644;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ })));
+ expect_lookup(SECONDRELPATH, ino, S_IFREG | 0755, 0, 1, UINT64_MAX);
+ // VOP_FORGET happens asynchronously, so it may or may not arrive
+ // before the test completes.
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_FORGET &&
+ in.header.nodeid == ino &&
+ in.body.forget.nlookup == 1);
+ }, Eq(true)),
+ _)
+ ).Times(AtMost(1))
+ .WillOnce(Invoke([=](auto in __unused, auto &out __unused) { }));
+
+ ASSERT_EQ(0, access(FIRSTFULLPATH, F_OK)) << strerror(errno);
+ EXPECT_EQ(0, access(SECONDFULLPATH, F_OK)) << strerror(errno);
+}
+
+TEST_F(Lookup_7_8, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+
+ 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 | 0644;
+ out.body.entry.nodeid = 14;
+ })));
+ /*
+ * access(2) is one of the few syscalls that will not (always) follow
+ * up a successful VOP_LOOKUP with another VOP.
+ */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+}
+
+/*
+ * Lookup ".." when that vnode's entry cache has timed out, but its child's
+ * hasn't.
+ */
+TEST_F(LookupExportable, dotdot_entry_cache_timeout)
+{
+ uint64_t foo_ino = 42;
+ uint64_t bar_ino = 43;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, "foo")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = foo_ino;
+ out.body.entry.attr.ino = foo_ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0; // immediate timeout
+ })));
+ EXPECT_LOOKUP(foo_ino, "bar")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = bar_ino;
+ out.body.entry.attr.ino = bar_ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ expect_opendir(bar_ino);
+ EXPECT_LOOKUP(foo_ino, "..")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = FUSE_ROOT_ID;
+ out.body.entry.attr.ino = FUSE_ROOT_ID;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ int fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ /* FreeBSD's fusefs driver always uses the same cache expiration time
+ * for ".." as for the directory itself. So we need to look up two
+ * levels to find an expired ".." cache entry.
+ */
+ EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno);
+}
+
+/*
+ * Lookup ".." for a vnode with no valid parent nid
+ * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259974
+ * Since the file system is exportable, we should resolve the problem by
+ * sending a FUSE_LOOKUP for "..".
+ */
+TEST_F(LookupExportable, dotdot_no_parent_nid)
+{
+ uint64_t foo_ino = 42;
+ uint64_t bar_ino = 43;
+ int fd;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, "foo")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = foo_ino;
+ out.body.entry.attr.ino = foo_ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ EXPECT_LOOKUP(foo_ino, "bar")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = bar_ino;
+ out.body.entry.attr.ino = bar_ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPENDIR);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, open);
+ })));
+ expect_forget(foo_ino, 1, NULL);
+ EXPECT_LOOKUP(bar_ino, "..")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = foo_ino;
+ out.body.entry.attr.ino = foo_ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ EXPECT_LOOKUP(foo_ino, "..")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = FUSE_ROOT_ID;
+ out.body.entry.attr.ino = FUSE_ROOT_ID;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ // Try (and fail) to unmount the file system, to reclaim the mountpoint
+ // and foo vnodes.
+ ASSERT_NE(0, unmount("mountpoint", 0));
+ EXPECT_EQ(EBUSY, errno);
+ nap(); // Because vnode reclamation is asynchronous
+ EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno);
+}
diff --git a/tests/sys/fs/fusefs/lseek.cc b/tests/sys/fs/fusefs/lseek.cc
new file mode 100644
index 000000000000..12d41f7af1b2
--- /dev/null
+++ b/tests/sys/fs/fusefs/lseek.cc
@@ -0,0 +1,518 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2020 Alan Somers
+ *
+ * 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/param.h>
+
+#include <fcntl.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Lseek: public FuseTest {};
+class LseekPathconf: public Lseek {};
+class LseekPathconf_7_23: public LseekPathconf {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 23;
+ FuseTest::SetUp();
+}
+};
+class LseekSeekHole: public Lseek {};
+class LseekSeekData: public Lseek {};
+
+/*
+ * If a previous lseek operation has already returned enosys, then pathconf can
+ * return EINVAL immediately.
+ */
+TEST_F(LseekPathconf, already_enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_in = 1 << 28;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENOSYS)));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+
+ EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA));
+ EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EINVAL, errno);
+
+ leak(fd);
+}
+
+/*
+ * If a previous lseek operation has already returned successfully, then
+ * pathconf can return 1 immediately. 1 means "holes are reported, but size is
+ * not specified".
+ */
+TEST_F(LseekPathconf, already_seeked)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset = 1 << 28;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i, auto& out) {
+ SET_OUT_HEADER_LEN(out, lseek);
+ out.body.lseek.offset = i.body.lseek.offset;
+ })));
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(offset, lseek(fd, offset, SEEK_DATA));
+
+ EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+
+ leak(fd);
+}
+
+/*
+ * Use pathconf on a file not already opened. The server returns EACCES when
+ * the kernel tries to open it. The kernel should return EACCES, and make no
+ * judgement about whether the server does or does not support FUSE_LSEEK.
+ */
+TEST_F(LseekPathconf, eacces)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.size = fsize;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPEN &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(2)
+ .WillRepeatedly(Invoke(ReturnErrno(EACCES)));
+
+ EXPECT_EQ(-1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EACCES, errno);
+ /* Check again, to ensure that the kernel didn't record the response */
+ EXPECT_EQ(-1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EACCES, errno);
+}
+
+/*
+ * If the server returns some weird error when we try FUSE_LSEEK, send that to
+ * the caller but don't record the answer.
+ */
+TEST_F(LseekPathconf, eio)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).Times(2)
+ .WillRepeatedly(Invoke(ReturnErrno(EIO)));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+
+ EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EIO, errno);
+ /* Check again, to ensure that the kernel didn't record the response */
+ EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EIO, errno);
+
+ leak(fd);
+}
+
+/*
+ * If no FUSE_LSEEK operation has been attempted since mount, try once as soon
+ * as a pathconf request comes in.
+ */
+TEST_F(LseekPathconf, enosys_now)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENOSYS)));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+
+ EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EINVAL, errno);
+
+ leak(fd);
+}
+
+/*
+ * Use pathconf, rather than fpathconf, on a file not already opened.
+ * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=278135
+ */
+TEST_F(LseekPathconf, pathconf)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_out = 1 << 29;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, lseek);
+ out.body.lseek.offset = offset_out;
+ })));
+ expect_release(ino, FuseTest::FH);
+
+ EXPECT_EQ(1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE)) << strerror(errno);
+}
+
+/*
+ * If no FUSE_LSEEK operation has been attempted since mount, try one as soon
+ * as a pathconf request comes in. This is the typical pattern of bsdtar. It
+ * will only try SEEK_HOLE/SEEK_DATA if fpathconf says they're supported.
+ */
+TEST_F(LseekPathconf, seek_now)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_initial = 1 << 27;
+ off_t offset_out = 1 << 29;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, lseek);
+ out.body.lseek.offset = offset_out;
+ })));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(offset_initial, lseek(fd, offset_initial, SEEK_SET));
+ EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ /* And check that the file pointer hasn't changed */
+ EXPECT_EQ(offset_initial, lseek(fd, 0, SEEK_CUR));
+
+ leak(fd);
+}
+
+/*
+ * If the user calls pathconf(_, _PC_MIN_HOLE_SIZE) on a fully sparse or
+ * zero-length file, then SEEK_DATA will return ENXIO. That should be
+ * interpreted as success.
+ */
+TEST_F(LseekPathconf, zerolength)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 0;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK &&
+ in.header.nodeid == ino &&
+ in.body.lseek.whence == SEEK_DATA);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENXIO)));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ /* Check again, to ensure that the kernel recorded the response */
+ EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+
+ leak(fd);
+}
+
+/*
+ * For servers using older protocol versions, no FUSE_LSEEK should be attempted
+ */
+TEST_F(LseekPathconf_7_23, already_enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EINVAL, errno);
+
+ leak(fd);
+}
+
+TEST_F(LseekSeekData, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_in = 1 << 28;
+ off_t offset_out = 1 << 29;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK &&
+ in.header.nodeid == ino &&
+ in.body.lseek.fh == FH &&
+ (off_t)in.body.lseek.offset == offset_in &&
+ in.body.lseek.whence == SEEK_DATA);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, lseek);
+ out.body.lseek.offset = offset_out;
+ })));
+ fd = open(FULLPATH, O_RDONLY);
+ EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_DATA));
+ EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR));
+
+ leak(fd);
+}
+
+/*
+ * If the server returns ENOSYS, fusefs should fall back to the default
+ * behavior, and never query the server again.
+ */
+TEST_F(LseekSeekData, enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_in = 1 << 28;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK &&
+ in.header.nodeid == ino &&
+ in.body.lseek.fh == FH &&
+ (off_t)in.body.lseek.offset == offset_in &&
+ in.body.lseek.whence == SEEK_DATA);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENOSYS)));
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+
+ /*
+ * Default behavior: ENXIO if offset is < 0 or >= fsize, offset
+ * otherwise.
+ */
+ EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA));
+ EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE));
+ EXPECT_EQ(ENXIO, errno);
+ EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE));
+ EXPECT_EQ(ENXIO, errno);
+
+ leak(fd);
+}
+
+TEST_F(LseekSeekHole, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_in = 1 << 28;
+ off_t offset_out = 1 << 29;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK &&
+ in.header.nodeid == ino &&
+ in.body.lseek.fh == FH &&
+ (off_t)in.body.lseek.offset == offset_in &&
+ in.body.lseek.whence == SEEK_HOLE);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, lseek);
+ out.body.lseek.offset = offset_out;
+ })));
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_HOLE));
+ EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR));
+
+ leak(fd);
+}
+
+/*
+ * If the server returns ENOSYS, fusefs should fall back to the default
+ * behavior, and never query the server again.
+ */
+TEST_F(LseekSeekHole, enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_in = 1 << 28;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK &&
+ in.header.nodeid == ino &&
+ in.body.lseek.fh == FH &&
+ (off_t)in.body.lseek.offset == offset_in &&
+ in.body.lseek.whence == SEEK_HOLE);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENOSYS)));
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+
+ /*
+ * Default behavior: ENXIO if offset is < 0 or >= fsize, fsize
+ * otherwise.
+ */
+ EXPECT_EQ(fsize, lseek(fd, offset_in, SEEK_HOLE));
+ EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE));
+ EXPECT_EQ(ENXIO, errno);
+ EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE));
+ EXPECT_EQ(ENXIO, errno);
+
+ leak(fd);
+}
+
+/* lseek should return ENXIO when offset points to EOF */
+TEST_F(LseekSeekHole, enxio)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_in = fsize;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK &&
+ in.header.nodeid == ino &&
+ in.body.lseek.fh == FH &&
+ (off_t)in.body.lseek.offset == offset_in &&
+ in.body.lseek.whence == SEEK_HOLE);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENXIO)));
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(-1, lseek(fd, offset_in, SEEK_HOLE));
+ EXPECT_EQ(ENXIO, errno);
+
+ leak(fd);
+}
diff --git a/tests/sys/fs/fusefs/mkdir.cc b/tests/sys/fs/fusefs/mkdir.cc
new file mode 100644
index 000000000000..f020feb94ed8
--- /dev/null
+++ b/tests/sys/fs/fusefs/mkdir.cc
@@ -0,0 +1,274 @@
+/*-
+ * 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 <fcntl.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Mkdir: public FuseTest {};
+class Mkdir_7_8: public FuseTest {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 8;
+ FuseTest::SetUp();
+}
+};
+
+/*
+ * EMLINK is possible on filesystems that limit the number of hard links to a
+ * single file, like early versions of BtrFS
+ */
+TEST_F(Mkdir, emlink)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ mode_t mode = 0755;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes +
+ sizeof(fuse_mkdir_in);
+ return (in.header.opcode == FUSE_MKDIR &&
+ in.body.mkdir.mode == (S_IFDIR | mode) &&
+ (0 == strcmp(RELPATH, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(EMLINK)));
+
+ ASSERT_NE(1, mkdir(FULLPATH, mode));
+ ASSERT_EQ(EMLINK, errno);
+}
+
+/*
+ * Creating a new directory after FUSE_LOOKUP returned a negative cache entry
+ */
+TEST_F(Mkdir, entry_cache_negative)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = 0755;
+ uint64_t ino = 42;
+ /*
+ * Set entry_valid = 0 because this test isn't concerned with whether
+ * or not we actually cache negative entries, only with whether we
+ * interpret negative cache responses correctly.
+ */
+ struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0};
+
+ /* mkdir will first do a LOOKUP, adding a negative cache entry */
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(ReturnNegativeCache(&entry_valid));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes +
+ sizeof(fuse_open_in);
+ return (in.header.opcode == FUSE_MKDIR &&
+ in.body.mkdir.mode == (S_IFDIR | mode) &&
+ (0 == strcmp(RELPATH, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.create.entry.attr.mode = S_IFDIR | mode;
+ out.body.create.entry.nodeid = ino;
+ out.body.create.entry.entry_valid = UINT64_MAX;
+ out.body.create.entry.attr_valid = UINT64_MAX;
+ })));
+
+ ASSERT_EQ(0, mkdir(FULLPATH, mode)) << strerror(errno);
+}
+
+/*
+ * Creating a new directory should purge any negative namecache entries
+ */
+TEST_F(Mkdir, entry_cache_negative_purge)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = 0755;
+ uint64_t ino = 42;
+ struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0};
+
+ /* mkdir will first do a LOOKUP, adding a negative cache entry */
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .Times(1)
+ .WillOnce(Invoke(ReturnNegativeCache(&entry_valid)))
+ .RetiresOnSaturation();
+
+ /* Then the MKDIR should purge the negative cache entry */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes +
+ sizeof(fuse_open_in);
+ return (in.header.opcode == FUSE_MKDIR &&
+ in.body.mkdir.mode == (S_IFDIR | mode) &&
+ (0 == strcmp(RELPATH, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ })));
+
+ ASSERT_EQ(0, mkdir(FULLPATH, mode)) << strerror(errno);
+
+ /* Finally, a subsequent lookup should query the daemon */
+ expect_lookup(RELPATH, ino, S_IFDIR | mode, 0, 1);
+
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+}
+
+TEST_F(Mkdir, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ mode_t mode = 0755;
+ uint64_t ino = 42;
+ mode_t mask;
+
+ mask = umask(0);
+ (void)umask(mask);
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes +
+ sizeof(fuse_mkdir_in);
+ return (in.header.opcode == FUSE_MKDIR &&
+ in.body.mkdir.mode == (S_IFDIR | mode) &&
+ in.body.mkdir.umask == mask &&
+ (0 == strcmp(RELPATH, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.create.entry.attr.mode = S_IFDIR | mode;
+ out.body.create.entry.nodeid = ino;
+ out.body.create.entry.entry_valid = UINT64_MAX;
+ out.body.create.entry.attr_valid = UINT64_MAX;
+ })));
+
+ ASSERT_EQ(0, mkdir(FULLPATH, mode)) << strerror(errno);
+}
+
+/*
+ * Nothing bad should happen if the server returns the parent's inode number
+ * for the newly created directory. Regression test for bug 263662.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263662
+ */
+TEST_F(Mkdir, parent_inode)
+{
+ const char FULLPATH[] = "mountpoint/parent/some_dir";
+ const char PPATH[] = "parent";
+ const char RELPATH[] = "some_dir";
+ mode_t mode = 0755;
+ uint64_t ino = 42;
+ mode_t mask;
+
+ mask = umask(0);
+ (void)umask(mask);
+
+ expect_lookup(PPATH, ino, S_IFDIR | 0755, 0, 1);
+ EXPECT_LOOKUP(ino, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes +
+ sizeof(fuse_mkdir_in);
+ return (in.header.opcode == FUSE_MKDIR &&
+ in.body.mkdir.mode == (S_IFDIR | mode) &&
+ in.body.mkdir.umask == mask &&
+ (0 == strcmp(RELPATH, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.create.entry.attr.mode = S_IFDIR | mode;
+ out.body.create.entry.nodeid = ino;
+ out.body.create.entry.entry_valid = UINT64_MAX;
+ out.body.create.entry.attr_valid = UINT64_MAX;
+ })));
+ // FUSE_FORGET happens asynchronously, so it may or may not arrive
+ // before the test completes.
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_FORGET);
+ }, Eq(true)),
+ _)
+ ).Times(AtMost(1))
+ .WillOnce(Invoke([=](auto in __unused, auto &out __unused) { }));
+
+ ASSERT_EQ(-1, mkdir(FULLPATH, mode));
+ ASSERT_EQ(EIO, errno);
+}
+
+TEST_F(Mkdir_7_8, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ mode_t mode = 0755;
+ uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes +
+ sizeof(fuse_mkdir_in);
+ return (in.header.opcode == FUSE_MKDIR &&
+ in.body.mkdir.mode == (S_IFDIR | mode) &&
+ (0 == strcmp(RELPATH, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry_7_8);
+ out.body.create.entry.attr.mode = S_IFDIR | mode;
+ out.body.create.entry.nodeid = ino;
+ out.body.create.entry.entry_valid = UINT64_MAX;
+ out.body.create.entry.attr_valid = UINT64_MAX;
+ })));
+
+ ASSERT_EQ(0, mkdir(FULLPATH, mode)) << strerror(errno);
+}
diff --git a/tests/sys/fs/fusefs/mknod.cc b/tests/sys/fs/fusefs/mknod.cc
new file mode 100644
index 000000000000..eb745f19acd5
--- /dev/null
+++ b/tests/sys/fs/fusefs/mknod.cc
@@ -0,0 +1,318 @@
+/*-
+ * 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 <fcntl.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <semaphore.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+#ifndef VNOVAL
+#define VNOVAL (-1) /* Defined in sys/vnode.h */
+#endif
+
+class Mknod: public FuseTest {
+
+mode_t m_oldmask;
+const static mode_t c_umask = 022;
+
+public:
+
+Mknod() {
+ m_oldmask = umask(c_umask);
+}
+
+virtual void SetUp() {
+ if (geteuid() != 0) {
+ GTEST_SKIP() << "Only root may use most mknod(2) variations";
+ }
+ FuseTest::SetUp();
+}
+
+virtual void TearDown() {
+ FuseTest::TearDown();
+ (void)umask(m_oldmask);
+}
+
+/* Test an OK creation of a file with the given mode and device number */
+void expect_mknod(uint64_t parent_ino, const char* relpath, uint64_t ino,
+ mode_t mode, dev_t dev)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes +
+ sizeof(fuse_mknod_in);
+ return (in.header.nodeid == parent_ino &&
+ in.header.opcode == FUSE_MKNOD &&
+ in.body.mknod.mode == mode &&
+ in.body.mknod.rdev == (uint32_t)dev &&
+ in.body.mknod.umask == c_umask &&
+ (0 == strcmp(relpath, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.attr.rdev = dev;
+ })));
+}
+
+};
+
+class Mknod_7_11: public FuseTest {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 11;
+ if (geteuid() != 0) {
+ GTEST_SKIP() << "Only root may use most mknod(2) variations";
+ }
+ FuseTest::SetUp();
+}
+
+void expect_lookup(const char *relpath, uint64_t ino, uint64_t size)
+{
+ FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, size, 1);
+}
+
+/* Test an OK creation of a file with the given mode and device number */
+void expect_mknod(uint64_t parent_ino, const char* relpath, uint64_t ino,
+ mode_t mode, dev_t dev)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes +
+ FUSE_COMPAT_MKNOD_IN_SIZE;
+ return (in.header.nodeid == parent_ino &&
+ in.header.opcode == FUSE_MKNOD &&
+ in.body.mknod.mode == mode &&
+ in.body.mknod.rdev == (uint32_t)dev &&
+ (0 == strcmp(relpath, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.attr.rdev = dev;
+ })));
+}
+
+};
+
+/*
+ * mknod(2) should be able to create block devices on a FUSE filesystem. Even
+ * though FreeBSD doesn't use block devices, this is useful when copying media
+ * from or preparing media for other operating systems.
+ */
+TEST_F(Mknod, blk)
+{
+ const char FULLPATH[] = "mountpoint/some_node";
+ const char RELPATH[] = "some_node";
+ mode_t mode = S_IFBLK | 0755;
+ dev_t rdev = 0xfe00; /* /dev/vda's device number on Linux */
+ uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev);
+
+ EXPECT_EQ(0, mknod(FULLPATH, mode, rdev)) << strerror(errno);
+}
+
+TEST_F(Mknod, chr)
+{
+ const char FULLPATH[] = "mountpoint/some_node";
+ const char RELPATH[] = "some_node";
+ mode_t mode = S_IFCHR | 0755;
+ dev_t rdev = 54; /* /dev/fuse's device number */
+ uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev);
+
+ EXPECT_EQ(0, mknod(FULLPATH, mode, rdev)) << strerror(errno);
+}
+
+/*
+ * The daemon is responsible for checking file permissions (unless the
+ * default_permissions mount option was used)
+ */
+TEST_F(Mknod, eperm)
+{
+ const char FULLPATH[] = "mountpoint/some_node";
+ const char RELPATH[] = "some_node";
+ mode_t mode = S_IFIFO | 0755;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes +
+ sizeof(fuse_mknod_in);
+ return (in.header.opcode == FUSE_MKNOD &&
+ in.body.mknod.mode == mode &&
+ (0 == strcmp(RELPATH, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(EPERM)));
+ EXPECT_NE(0, mkfifo(FULLPATH, mode));
+ EXPECT_EQ(EPERM, errno);
+}
+
+TEST_F(Mknod, fifo)
+{
+ const char FULLPATH[] = "mountpoint/some_node";
+ const char RELPATH[] = "some_node";
+ mode_t mode = S_IFIFO | 0755;
+ dev_t rdev = VNOVAL; /* Fifos don't have device numbers */
+ uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev);
+
+ EXPECT_EQ(0, mkfifo(FULLPATH, mode)) << strerror(errno);
+}
+
+/*
+ * Create a unix-domain socket.
+ *
+ * This test case doesn't actually need root privileges.
+ */
+TEST_F(Mknod, socket)
+{
+ const char FULLPATH[] = "mountpoint/some_node";
+ const char RELPATH[] = "some_node";
+ mode_t mode = S_IFSOCK | 0755;
+ struct sockaddr_un sa;
+ int fd;
+ dev_t rdev = -1; /* Really it's a don't care */
+ uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev);
+
+ fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ ASSERT_LE(0, fd) << strerror(errno);
+ sa.sun_family = AF_UNIX;
+ strlcpy(sa.sun_path, FULLPATH, sizeof(sa.sun_path));
+ sa.sun_len = sizeof(FULLPATH);
+ ASSERT_EQ(0, bind(fd, (struct sockaddr*)&sa, sizeof(sa)))
+ << strerror(errno);
+
+ leak(fd);
+}
+
+/*
+ * Nothing bad should happen if the server returns the parent's inode number
+ * for the newly created file. Regression test for bug 263662.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263662
+ */
+TEST_F(Mknod, parent_inode)
+{
+ const char FULLPATH[] = "mountpoint/parent/some_node";
+ const char PPATH[] = "parent";
+ const char RELPATH[] = "some_node";
+ mode_t mode = S_IFSOCK | 0755;
+ struct sockaddr_un sa;
+ sem_t sem;
+ int fd;
+ dev_t rdev = -1; /* Really it's a don't care */
+ uint64_t ino = 42;
+
+ ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
+
+ expect_lookup(PPATH, ino, S_IFDIR | 0755, 0, 1);
+ EXPECT_LOOKUP(ino, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_mknod(ino, RELPATH, ino, mode, rdev);
+ expect_forget(ino, 1, &sem);
+
+ fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ ASSERT_LE(0, fd) << strerror(errno);
+ sa.sun_family = AF_UNIX;
+ strlcpy(sa.sun_path, FULLPATH, sizeof(sa.sun_path));
+ sa.sun_len = sizeof(FULLPATH);
+ ASSERT_EQ(-1, bind(fd, (struct sockaddr*)&sa, sizeof(sa)));
+ ASSERT_EQ(EIO, errno);
+
+ leak(fd);
+ sem_wait(&sem);
+ sem_destroy(&sem);
+}
+
+/*
+ * fusefs(4) lacks VOP_WHITEOUT support. No bugzilla entry, because that's a
+ * feature, not a bug
+ */
+TEST_F(Mknod, DISABLED_whiteout)
+{
+ const char FULLPATH[] = "mountpoint/some_node";
+ const char RELPATH[] = "some_node";
+ mode_t mode = S_IFWHT | 0755;
+ dev_t rdev = VNOVAL; /* whiteouts don't have device numbers */
+ uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev);
+
+ EXPECT_EQ(0, mknod(FULLPATH, mode, 0)) << strerror(errno);
+}
+
+/* A server built at protocol version 7.11 or earlier can still use mknod */
+TEST_F(Mknod_7_11, fifo)
+{
+ const char FULLPATH[] = "mountpoint/some_node";
+ const char RELPATH[] = "some_node";
+ mode_t mode = S_IFIFO | 0755;
+ dev_t rdev = VNOVAL;
+ uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev);
+
+ EXPECT_EQ(0, mkfifo(FULLPATH, mode)) << strerror(errno);
+}
diff --git a/tests/sys/fs/fusefs/mockfs.cc b/tests/sys/fs/fusefs/mockfs.cc
new file mode 100644
index 000000000000..b6a32d9b60af
--- /dev/null
+++ b/tests/sys/fs/fusefs/mockfs.cc
@@ -0,0 +1,1063 @@
+/*-
+ * 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/param.h>
+
+#include <sys/mount.h>
+#include <sys/select.h>
+#include <sys/stat.h>
+#include <sys/uio.h>
+#include <sys/user.h>
+
+#include <fcntl.h>
+#include <libutil.h>
+#include <mntopts.h> // for build_iovec
+#include <poll.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <unistd.h>
+}
+
+#include <cinttypes>
+
+#include <gtest/gtest.h>
+
+#include "mockfs.hh"
+
+using namespace testing;
+
+int verbosity = 0;
+
+const char* opcode2opname(uint32_t opcode)
+{
+ const char* table[] = {
+ "Unknown (opcode 0)",
+ "LOOKUP",
+ "FORGET",
+ "GETATTR",
+ "SETATTR",
+ "READLINK",
+ "SYMLINK",
+ "Unknown (opcode 7)",
+ "MKNOD",
+ "MKDIR",
+ "UNLINK",
+ "RMDIR",
+ "RENAME",
+ "LINK",
+ "OPEN",
+ "READ",
+ "WRITE",
+ "STATFS",
+ "RELEASE",
+ "Unknown (opcode 19)",
+ "FSYNC",
+ "SETXATTR",
+ "GETXATTR",
+ "LISTXATTR",
+ "REMOVEXATTR",
+ "FLUSH",
+ "INIT",
+ "OPENDIR",
+ "READDIR",
+ "RELEASEDIR",
+ "FSYNCDIR",
+ "GETLK",
+ "SETLK",
+ "SETLKW",
+ "ACCESS",
+ "CREATE",
+ "INTERRUPT",
+ "BMAP",
+ "DESTROY",
+ "IOCTL",
+ "POLL",
+ "NOTIFY_REPLY",
+ "BATCH_FORGET",
+ "FALLOCATE",
+ "READDIRPLUS",
+ "RENAME2",
+ "LSEEK",
+ "COPY_FILE_RANGE",
+ };
+ if (opcode >= nitems(table))
+ return ("Unknown (opcode > max)");
+ else
+ return (table[opcode]);
+}
+
+ProcessMockerT
+ReturnErrno(int error)
+{
+ return([=](auto in, auto &out) {
+ std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
+ out0->header.unique = in.header.unique;
+ out0->header.error = -error;
+ out0->header.len = sizeof(out0->header);
+ out.push_back(std::move(out0));
+ });
+}
+
+/* Helper function used for returning negative cache entries for LOOKUP */
+ProcessMockerT
+ReturnNegativeCache(const struct timespec *entry_valid)
+{
+ return([=](auto in, auto &out) {
+ /* nodeid means ENOENT and cache it */
+ std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
+ out0->body.entry.nodeid = 0;
+ out0->header.unique = in.header.unique;
+ out0->header.error = 0;
+ out0->body.entry.entry_valid = entry_valid->tv_sec;
+ out0->body.entry.entry_valid_nsec = entry_valid->tv_nsec;
+ SET_OUT_HEADER_LEN(*out0, entry);
+ out.push_back(std::move(out0));
+ });
+}
+
+ProcessMockerT
+ReturnImmediate(std::function<void(const mockfs_buf_in& in,
+ struct mockfs_buf_out &out)> f)
+{
+ return([=](auto& in, auto &out) {
+ std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
+ out0->header.unique = in.header.unique;
+ f(in, *out0);
+ out.push_back(std::move(out0));
+ });
+}
+
+void sigint_handler(int __unused sig) {
+ // Don't do anything except interrupt the daemon's read(2) call
+}
+
+void MockFS::debug_request(const mockfs_buf_in &in, ssize_t buflen)
+{
+ printf("%-11s ino=%2" PRIu64, opcode2opname(in.header.opcode),
+ in.header.nodeid);
+ if (verbosity > 1) {
+ printf(" uid=%5u gid=%5u pid=%5u unique=%" PRIu64 " len=%u"
+ " buflen=%zd",
+ in.header.uid, in.header.gid, in.header.pid,
+ in.header.unique, in.header.len, buflen);
+ }
+ switch (in.header.opcode) {
+ const char *name, *value;
+
+ case FUSE_ACCESS:
+ printf(" mask=%#x", in.body.access.mask);
+ break;
+ case FUSE_BMAP:
+ printf(" block=%" PRIx64 " blocksize=%#x",
+ in.body.bmap.block, in.body.bmap.blocksize);
+ break;
+ case FUSE_COPY_FILE_RANGE:
+ printf(" off_in=%" PRIu64 " ino_out=%" PRIu64
+ " off_out=%" PRIu64 " size=%" PRIu64,
+ in.body.copy_file_range.off_in,
+ in.body.copy_file_range.nodeid_out,
+ in.body.copy_file_range.off_out,
+ in.body.copy_file_range.len);
+ if (verbosity > 1)
+ printf(" fh_in=%" PRIu64 " fh_out=%" PRIu64
+ " flags=%" PRIx64,
+ in.body.copy_file_range.fh_in,
+ in.body.copy_file_range.fh_out,
+ in.body.copy_file_range.flags);
+ break;
+ case FUSE_CREATE:
+ if (m_kernel_minor_version >= 12)
+ name = (const char*)in.body.bytes +
+ sizeof(fuse_create_in);
+ else
+ name = (const char*)in.body.bytes +
+ sizeof(fuse_open_in);
+ printf(" flags=%#x name=%s",
+ in.body.open.flags, name);
+ break;
+ case FUSE_FALLOCATE:
+ printf(" fh=%#" PRIx64 " offset=%" PRIu64
+ " length=%" PRIx64 " mode=%#x",
+ in.body.fallocate.fh,
+ in.body.fallocate.offset,
+ in.body.fallocate.length,
+ in.body.fallocate.mode);
+ break;
+ case FUSE_FLUSH:
+ printf(" fh=%#" PRIx64 " lock_owner=%" PRIu64,
+ in.body.flush.fh,
+ in.body.flush.lock_owner);
+ break;
+ case FUSE_FORGET:
+ printf(" nlookup=%" PRIu64, in.body.forget.nlookup);
+ break;
+ case FUSE_FSYNC:
+ printf(" flags=%#x", in.body.fsync.fsync_flags);
+ break;
+ case FUSE_FSYNCDIR:
+ printf(" flags=%#x", in.body.fsyncdir.fsync_flags);
+ break;
+ case FUSE_GETLK:
+ printf(" fh=%#" PRIx64
+ " type=%u pid=%u",
+ in.body.getlk.fh,
+ in.body.getlk.lk.type,
+ in.body.getlk.lk.pid);
+ if (verbosity >= 2) {
+ printf(" range=[%" PRIi64 ":%" PRIi64 "]",
+ in.body.getlk.lk.start,
+ in.body.getlk.lk.end);
+ }
+ break;
+ case FUSE_INTERRUPT:
+ printf(" unique=%" PRIu64, in.body.interrupt.unique);
+ break;
+ case FUSE_LINK:
+ printf(" oldnodeid=%" PRIu64, in.body.link.oldnodeid);
+ break;
+ case FUSE_LISTXATTR:
+ printf(" size=%" PRIu32, in.body.listxattr.size);
+ break;
+ case FUSE_LOOKUP:
+ printf(" %s", in.body.lookup);
+ break;
+ case FUSE_LSEEK:
+ switch (in.body.lseek.whence) {
+ case SEEK_HOLE:
+ printf(" SEEK_HOLE offset=%jd",
+ in.body.lseek.offset);
+ break;
+ case SEEK_DATA:
+ printf(" SEEK_DATA offset=%jd",
+ in.body.lseek.offset);
+ break;
+ default:
+ printf(" whence=%u offset=%jd",
+ in.body.lseek.whence, in.body.lseek.offset);
+ break;
+ }
+ break;
+ case FUSE_MKDIR:
+ name = (const char*)in.body.bytes +
+ sizeof(fuse_mkdir_in);
+ printf(" name=%s mode=%#o umask=%#o", name,
+ in.body.mkdir.mode, in.body.mkdir.umask);
+ break;
+ case FUSE_MKNOD:
+ if (m_kernel_minor_version >= 12)
+ name = (const char*)in.body.bytes +
+ sizeof(fuse_mknod_in);
+ else
+ name = (const char*)in.body.bytes +
+ FUSE_COMPAT_MKNOD_IN_SIZE;
+ printf(" mode=%#o rdev=%x umask=%#o name=%s",
+ in.body.mknod.mode, in.body.mknod.rdev,
+ in.body.mknod.umask, name);
+ break;
+ case FUSE_OPEN:
+ printf(" flags=%#x", in.body.open.flags);
+ break;
+ case FUSE_OPENDIR:
+ printf(" flags=%#x", in.body.opendir.flags);
+ break;
+ case FUSE_READ:
+ printf(" offset=%" PRIu64 " size=%u",
+ in.body.read.offset,
+ in.body.read.size);
+ if (verbosity > 1)
+ printf(" flags=%#x", in.body.read.flags);
+ break;
+ case FUSE_READDIR:
+ printf(" fh=%#" PRIx64 " offset=%" PRIu64 " size=%u",
+ in.body.readdir.fh, in.body.readdir.offset,
+ in.body.readdir.size);
+ break;
+ case FUSE_RELEASE:
+ printf(" fh=%#" PRIx64 " flags=%#x lock_owner=%" PRIu64,
+ in.body.release.fh,
+ in.body.release.flags,
+ in.body.release.lock_owner);
+ break;
+ case FUSE_RENAME:
+ {
+ const char *src = (const char*)in.body.bytes +
+ sizeof(fuse_rename_in);
+ const char *dst = src + strlen(src) + 1;
+ printf(" src=%s newdir=%" PRIu64 " dst=%s",
+ src, in.body.rename.newdir, dst);
+ }
+ break;
+ case FUSE_SETATTR:
+ if (verbosity <= 1) {
+ printf(" valid=%#x", in.body.setattr.valid);
+ break;
+ }
+ if (in.body.setattr.valid & FATTR_MODE)
+ printf(" mode=%#o", in.body.setattr.mode);
+ if (in.body.setattr.valid & FATTR_UID)
+ printf(" uid=%u", in.body.setattr.uid);
+ if (in.body.setattr.valid & FATTR_GID)
+ printf(" gid=%u", in.body.setattr.gid);
+ if (in.body.setattr.valid & FATTR_SIZE)
+ printf(" size=%" PRIu64, in.body.setattr.size);
+ if (in.body.setattr.valid & FATTR_ATIME)
+ printf(" atime=%" PRIu64 ".%u",
+ in.body.setattr.atime,
+ in.body.setattr.atimensec);
+ if (in.body.setattr.valid & FATTR_MTIME)
+ printf(" mtime=%" PRIu64 ".%u",
+ in.body.setattr.mtime,
+ in.body.setattr.mtimensec);
+ if (in.body.setattr.valid & FATTR_FH)
+ printf(" fh=%" PRIu64 "", in.body.setattr.fh);
+ break;
+ case FUSE_SETLK:
+ printf(" fh=%#" PRIx64 " owner=%" PRIu64
+ " type=%u pid=%u",
+ in.body.setlk.fh, in.body.setlk.owner,
+ in.body.setlk.lk.type,
+ in.body.setlk.lk.pid);
+ if (verbosity >= 2) {
+ printf(" range=[%" PRIi64 ":%" PRIi64 "]",
+ in.body.setlk.lk.start,
+ in.body.setlk.lk.end);
+ }
+ break;
+ case FUSE_SETXATTR:
+ /*
+ * In theory neither the xattr name and value need be
+ * ASCII, but in this test suite they always are.
+ */
+ name = (const char*)in.body.bytes +
+ sizeof(fuse_setxattr_in);
+ value = name + strlen(name) + 1;
+ printf(" %s=%s", name, value);
+ break;
+ case FUSE_WRITE:
+ printf(" fh=%#" PRIx64 " offset=%" PRIu64
+ " size=%u write_flags=%u",
+ in.body.write.fh,
+ in.body.write.offset, in.body.write.size,
+ in.body.write.write_flags);
+ if (verbosity > 1)
+ printf(" flags=%#x", in.body.write.flags);
+ break;
+ default:
+ break;
+ }
+ printf("\n");
+}
+
+/*
+ * Debug a FUSE response.
+ *
+ * This is mostly useful for asynchronous notifications, which don't correspond
+ * to any request
+ */
+void MockFS::debug_response(const mockfs_buf_out &out) {
+ const char *name;
+
+ if (verbosity == 0)
+ return;
+
+ switch (out.header.error) {
+ case FUSE_NOTIFY_INVAL_ENTRY:
+ name = (const char*)out.body.bytes +
+ sizeof(fuse_notify_inval_entry_out);
+ printf("<- INVAL_ENTRY parent=%" PRIu64 " %s\n",
+ out.body.inval_entry.parent, name);
+ break;
+ case FUSE_NOTIFY_INVAL_INODE:
+ printf("<- INVAL_INODE ino=%" PRIu64 " off=%" PRIi64
+ " len=%" PRIi64 "\n",
+ out.body.inval_inode.ino,
+ out.body.inval_inode.off,
+ out.body.inval_inode.len);
+ break;
+ case FUSE_NOTIFY_STORE:
+ printf("<- STORE ino=%" PRIu64 " off=%" PRIu64
+ " size=%" PRIu32 "\n",
+ out.body.store.nodeid,
+ out.body.store.offset,
+ out.body.store.size);
+ break;
+ default:
+ break;
+ }
+}
+
+MockFS::MockFS(int max_read, int max_readahead, bool allow_other,
+ bool default_permissions,
+ bool push_symlinks_in, bool ro, enum poll_method pm, uint32_t flags,
+ uint32_t kernel_minor_version, uint32_t max_write, bool async,
+ bool noclusterr, unsigned time_gran, bool nointr, bool noatime,
+ const char *fsname, const char *subtype, bool no_auto_init)
+ : m_daemon_id(NULL),
+ m_kernel_minor_version(kernel_minor_version),
+ m_kq(pm == KQ ? kqueue() : -1),
+ m_maxread(max_read),
+ m_maxreadahead(max_readahead),
+ m_pid(getpid()),
+ m_uniques(new std::unordered_set<uint64_t>),
+ m_pm(pm),
+ m_time_gran(time_gran),
+ m_child_pid(-1),
+ m_maxwrite(MIN(max_write, max_max_write)),
+ m_nready(-1),
+ m_quit(false),
+ m_expect_unmount(false)
+{
+ struct sigaction sa;
+ struct iovec *iov = NULL;
+ int iovlen = 0;
+ char fdstr[15];
+ const bool trueval = true;
+
+ /*
+ * Kyua sets pwd to a testcase-unique tempdir; no need to use
+ * mkdtemp
+ */
+ /*
+ * googletest doesn't allow ASSERT_ in constructors, so we must throw
+ * instead.
+ */
+ if (mkdir("mountpoint" , 0755) && errno != EEXIST)
+ throw(std::system_error(errno, std::system_category(),
+ "Couldn't make mountpoint directory"));
+
+ switch (m_pm) {
+ case BLOCKING:
+ m_fuse_fd = open("/dev/fuse", O_CLOEXEC | O_RDWR);
+ break;
+ default:
+ m_fuse_fd = open("/dev/fuse", O_CLOEXEC | O_RDWR | O_NONBLOCK);
+ break;
+ }
+ if (m_fuse_fd < 0)
+ throw(std::system_error(errno, std::system_category(),
+ "Couldn't open /dev/fuse"));
+
+ build_iovec(&iov, &iovlen, "fstype", __DECONST(void *, "fusefs"), -1);
+ build_iovec(&iov, &iovlen, "fspath",
+ __DECONST(void *, "mountpoint"), -1);
+ build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1);
+ sprintf(fdstr, "%d", m_fuse_fd);
+ build_iovec(&iov, &iovlen, "fd", fdstr, -1);
+ if (m_maxread > 0) {
+ char val[12];
+
+ snprintf(val, sizeof(val), "%d", m_maxread);
+ build_iovec(&iov, &iovlen, "max_read=", &val, -1);
+ }
+ if (allow_other) {
+ build_iovec(&iov, &iovlen, "allow_other",
+ __DECONST(void*, &trueval), sizeof(bool));
+ }
+ if (default_permissions) {
+ build_iovec(&iov, &iovlen, "default_permissions",
+ __DECONST(void*, &trueval), sizeof(bool));
+ }
+ if (push_symlinks_in) {
+ build_iovec(&iov, &iovlen, "push_symlinks_in",
+ __DECONST(void*, &trueval), sizeof(bool));
+ }
+ if (ro) {
+ build_iovec(&iov, &iovlen, "ro",
+ __DECONST(void*, &trueval), sizeof(bool));
+ }
+ if (async) {
+ build_iovec(&iov, &iovlen, "async", __DECONST(void*, &trueval),
+ sizeof(bool));
+ }
+ if (noatime) {
+ build_iovec(&iov, &iovlen, "noatime",
+ __DECONST(void*, &trueval), sizeof(bool));
+ }
+ if (noclusterr) {
+ build_iovec(&iov, &iovlen, "noclusterr",
+ __DECONST(void*, &trueval), sizeof(bool));
+ }
+ if (nointr) {
+ build_iovec(&iov, &iovlen, "nointr",
+ __DECONST(void*, &trueval), sizeof(bool));
+ } else {
+ build_iovec(&iov, &iovlen, "intr",
+ __DECONST(void*, &trueval), sizeof(bool));
+ }
+ if (*fsname) {
+ build_iovec(&iov, &iovlen, "fsname=",
+ __DECONST(void*, fsname), -1);
+ }
+ if (*subtype) {
+ build_iovec(&iov, &iovlen, "subtype=",
+ __DECONST(void*, subtype), -1);
+ }
+ if (nmount(iov, iovlen, 0))
+ throw(std::system_error(errno, std::system_category(),
+ "Couldn't mount filesystem"));
+ free_iovec(&iov, &iovlen);
+
+ // Setup default handler
+ ON_CALL(*this, process(_, _))
+ .WillByDefault(Invoke(this, &MockFS::process_default));
+
+ if (!no_auto_init)
+ init(flags);
+
+ bzero(&sa, sizeof(sa));
+ sa.sa_handler = sigint_handler;
+ sa.sa_flags = 0; /* Don't set SA_RESTART! */
+ if (0 != sigaction(SIGUSR1, &sa, NULL))
+ throw(std::system_error(errno, std::system_category(),
+ "Couldn't handle SIGUSR1"));
+ if (pthread_create(&m_daemon_id, NULL, service, (void*)this))
+ throw(std::system_error(errno, std::system_category(),
+ "Couldn't Couldn't start fuse thread"));
+}
+
+MockFS::~MockFS() {
+ kill_daemon();
+ join_daemon();
+ ::unmount("mountpoint", MNT_FORCE);
+ rmdir("mountpoint");
+ if (m_kq >= 0)
+ close(m_kq);
+}
+
+void MockFS::audit_request(const mockfs_buf_in &in, ssize_t buflen) {
+ uint32_t inlen = in.header.len;
+ size_t fih = sizeof(in.header);
+ switch (in.header.opcode) {
+ case FUSE_LOOKUP:
+ case FUSE_RMDIR:
+ case FUSE_SYMLINK:
+ case FUSE_UNLINK:
+ EXPECT_GT(inlen, fih) << "Missing request filename";
+ // No redundant information for checking buflen
+ break;
+ case FUSE_FORGET:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.forget));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_GETATTR:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.getattr));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_SETATTR:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.setattr));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_READLINK:
+ EXPECT_EQ(inlen, fih) << "Unexpected request body";
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_MKNOD:
+ {
+ size_t s;
+ if (m_kernel_minor_version >= 12)
+ s = sizeof(in.body.mknod);
+ else
+ s = FUSE_COMPAT_MKNOD_IN_SIZE;
+ EXPECT_GE(inlen, fih + s) << "Missing request body";
+ EXPECT_GT(inlen, fih + s) << "Missing request filename";
+ // No redundant information for checking buflen
+ break;
+ }
+ case FUSE_MKDIR:
+ EXPECT_GE(inlen, fih + sizeof(in.body.mkdir)) <<
+ "Missing request body";
+ EXPECT_GT(inlen, fih + sizeof(in.body.mkdir)) <<
+ "Missing request filename";
+ // No redundant information for checking buflen
+ break;
+ case FUSE_RENAME:
+ EXPECT_GE(inlen, fih + sizeof(in.body.rename)) <<
+ "Missing request body";
+ EXPECT_GT(inlen, fih + sizeof(in.body.rename)) <<
+ "Missing request filename";
+ // No redundant information for checking buflen
+ break;
+ case FUSE_LINK:
+ EXPECT_GE(inlen, fih + sizeof(in.body.link)) <<
+ "Missing request body";
+ EXPECT_GT(inlen, fih + sizeof(in.body.link)) <<
+ "Missing request filename";
+ // No redundant information for checking buflen
+ break;
+ case FUSE_OPEN:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.open));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_READ:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.read));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_WRITE:
+ {
+ size_t s;
+
+ if (m_kernel_minor_version >= 9)
+ s = sizeof(in.body.write);
+ else
+ s = FUSE_COMPAT_WRITE_IN_SIZE;
+ // I suppose a 0-byte write should be allowed
+ EXPECT_GE(inlen, fih + s) << "Missing request body";
+ EXPECT_EQ((size_t)buflen, fih + s + in.body.write.size);
+ break;
+ }
+ case FUSE_DESTROY:
+ case FUSE_STATFS:
+ EXPECT_EQ(inlen, fih);
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_RELEASE:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.release));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_FSYNC:
+ case FUSE_FSYNCDIR:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.fsync));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_SETXATTR:
+ EXPECT_GE(inlen, fih + sizeof(in.body.setxattr)) <<
+ "Missing request body";
+ EXPECT_GT(inlen, fih + sizeof(in.body.setxattr)) <<
+ "Missing request attribute name";
+ // No redundant information for checking buflen
+ break;
+ case FUSE_GETXATTR:
+ EXPECT_GE(inlen, fih + sizeof(in.body.getxattr)) <<
+ "Missing request body";
+ EXPECT_GT(inlen, fih + sizeof(in.body.getxattr)) <<
+ "Missing request attribute name";
+ // No redundant information for checking buflen
+ break;
+ case FUSE_LISTXATTR:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.listxattr));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_REMOVEXATTR:
+ EXPECT_GT(inlen, fih) << "Missing request attribute name";
+ // No redundant information for checking buflen
+ break;
+ case FUSE_FLUSH:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.flush));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_INIT:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.init));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_OPENDIR:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.opendir));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_READDIR:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.readdir));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_RELEASEDIR:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.releasedir));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_GETLK:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.getlk));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_SETLK:
+ case FUSE_SETLKW:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.setlk));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_ACCESS:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.access));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_CREATE:
+ EXPECT_GE(inlen, fih + sizeof(in.body.create)) <<
+ "Missing request body";
+ EXPECT_GT(inlen, fih + sizeof(in.body.create)) <<
+ "Missing request filename";
+ // No redundant information for checking buflen
+ break;
+ case FUSE_INTERRUPT:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.interrupt));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_FALLOCATE:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.fallocate));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_BMAP:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.bmap));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_LSEEK:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.lseek));
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_COPY_FILE_RANGE:
+ EXPECT_EQ(inlen, fih + sizeof(in.body.copy_file_range));
+ EXPECT_EQ(0ul, in.body.copy_file_range.flags);
+ EXPECT_EQ((size_t)buflen, inlen);
+ break;
+ case FUSE_NOTIFY_REPLY:
+ case FUSE_BATCH_FORGET:
+ case FUSE_IOCTL:
+ case FUSE_POLL:
+ case FUSE_READDIRPLUS:
+ FAIL() << "Unsupported opcode?";
+ default:
+ FAIL() << "Unknown opcode " << in.header.opcode;
+ }
+ /* Verify that the ticket's unique value is actually unique. */
+ if (m_uniques->find(in.header.unique) != m_uniques->end())
+ FAIL() << "Non-unique \"unique\" value";
+ m_uniques->insert(in.header.unique);
+}
+
+void MockFS::init(uint32_t flags) {
+ ssize_t buflen;
+
+ std::unique_ptr<mockfs_buf_in> in(new mockfs_buf_in);
+ std::unique_ptr<mockfs_buf_out> out(new mockfs_buf_out);
+
+ read_request(*in, buflen);
+ if (verbosity > 0)
+ debug_request(*in, buflen);
+ audit_request(*in, buflen);
+ ASSERT_EQ(FUSE_INIT, in->header.opcode);
+
+ out->header.unique = in->header.unique;
+ out->header.error = 0;
+ out->body.init.major = FUSE_KERNEL_VERSION;
+ out->body.init.minor = m_kernel_minor_version;;
+ out->body.init.flags = in->body.init.flags & flags;
+ out->body.init.max_write = m_maxwrite;
+ out->body.init.max_readahead = m_maxreadahead;
+
+ if (m_kernel_minor_version < 23) {
+ SET_OUT_HEADER_LEN(*out, init_7_22);
+ } else {
+ out->body.init.time_gran = m_time_gran;
+ SET_OUT_HEADER_LEN(*out, init);
+ }
+
+ write(m_fuse_fd, out.get(), out->header.len);
+}
+
+void MockFS::kill_daemon() {
+ m_quit = true;
+ if (m_daemon_id != NULL)
+ pthread_kill(m_daemon_id, SIGUSR1);
+ // Closing the /dev/fuse file descriptor first allows unmount to
+ // succeed even if the daemon doesn't correctly respond to commands
+ // during the unmount sequence.
+ close(m_fuse_fd);
+ m_fuse_fd = -1;
+}
+
+void MockFS::join_daemon() {
+ if (m_daemon_id != NULL) {
+ pthread_join(m_daemon_id, NULL);
+ m_daemon_id = NULL;
+ }
+}
+
+void MockFS::loop() {
+ std::vector<std::unique_ptr<mockfs_buf_out>> out;
+
+ std::unique_ptr<mockfs_buf_in> in(new mockfs_buf_in);
+ ASSERT_TRUE(in != NULL);
+ while (!m_quit) {
+ ssize_t buflen;
+
+ bzero(in.get(), sizeof(*in));
+ read_request(*in, buflen);
+ if (m_quit)
+ break;
+ if (verbosity > 0)
+ debug_request(*in, buflen);
+ audit_request(*in, buflen);
+ if (pid_ok((pid_t)in->header.pid)) {
+ process(*in, out);
+ } else {
+ /*
+ * Reject any requests from unknown processes. Because
+ * we actually do mount a filesystem, plenty of
+ * unrelated system daemons may try to access it.
+ */
+ if (verbosity > 1)
+ printf("\tREJECTED (wrong pid %d)\n",
+ in->header.pid);
+ process_default(*in, out);
+ }
+ for (auto &it: out)
+ write_response(*it);
+ out.clear();
+ }
+}
+
+int MockFS::notify_inval_entry(ino_t parent, const char *name, size_t namelen,
+ int expected_errno)
+{
+ std::unique_ptr<mockfs_buf_out> out(new mockfs_buf_out);
+
+ out->expected_errno = expected_errno;
+ out->header.unique = 0; /* 0 means asynchronous notification */
+ out->header.error = FUSE_NOTIFY_INVAL_ENTRY;
+ out->body.inval_entry.parent = parent;
+ out->body.inval_entry.namelen = namelen;
+ strlcpy((char*)&out->body.bytes + sizeof(out->body.inval_entry),
+ name, sizeof(out->body.bytes) - sizeof(out->body.inval_entry));
+ out->header.len = sizeof(out->header) + sizeof(out->body.inval_entry) +
+ namelen;
+ debug_response(*out);
+ write_response(*out);
+ return 0;
+}
+
+int MockFS::notify_inval_inode(ino_t ino, off_t off, ssize_t len)
+{
+ std::unique_ptr<mockfs_buf_out> out(new mockfs_buf_out);
+
+ out->header.unique = 0; /* 0 means asynchronous notification */
+ out->header.error = FUSE_NOTIFY_INVAL_INODE;
+ out->body.inval_inode.ino = ino;
+ out->body.inval_inode.off = off;
+ out->body.inval_inode.len = len;
+ out->header.len = sizeof(out->header) + sizeof(out->body.inval_inode);
+ debug_response(*out);
+ write_response(*out);
+ return 0;
+}
+
+int MockFS::notify_store(ino_t ino, off_t off, const void* data, ssize_t size)
+{
+ std::unique_ptr<mockfs_buf_out> out(new mockfs_buf_out);
+
+ out->header.unique = 0; /* 0 means asynchronous notification */
+ out->header.error = FUSE_NOTIFY_STORE;
+ out->body.store.nodeid = ino;
+ out->body.store.offset = off;
+ out->body.store.size = size;
+ bcopy(data, (char*)&out->body.bytes + sizeof(out->body.store), size);
+ out->header.len = sizeof(out->header) + sizeof(out->body.store) + size;
+ debug_response(*out);
+ write_response(*out);
+ return 0;
+}
+
+bool MockFS::pid_ok(pid_t pid) {
+ if (pid == m_pid) {
+ return (true);
+ } else if (pid == m_child_pid) {
+ return (true);
+ } else {
+ struct kinfo_proc *ki;
+ bool ok = false;
+
+ ki = kinfo_getproc(pid);
+ if (ki == NULL)
+ return (false);
+ /*
+ * Allow access by the aio daemon processes so that our tests
+ * can use aio functions
+ */
+ if (0 == strncmp("aiod", ki->ki_comm, 4))
+ ok = true;
+ free(ki);
+ return (ok);
+ }
+}
+
+void MockFS::process_default(const mockfs_buf_in& in,
+ std::vector<std::unique_ptr<mockfs_buf_out>> &out)
+{
+ std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
+ out0->header.unique = in.header.unique;
+ out0->header.error = -EOPNOTSUPP;
+ out0->header.len = sizeof(out0->header);
+ out.push_back(std::move(out0));
+}
+
+void MockFS::read_request(mockfs_buf_in &in, ssize_t &res) {
+ int nready = 0;
+ fd_set readfds;
+ pollfd fds[1];
+ struct kevent changes[1];
+ struct kevent events[1];
+ struct timespec timeout_ts;
+ struct timeval timeout_tv;
+ const int timeout_ms = 999;
+ int timeout_int, nfds;
+ int fuse_fd;
+
+ switch (m_pm) {
+ case BLOCKING:
+ break;
+ case KQ:
+ timeout_ts.tv_sec = 0;
+ timeout_ts.tv_nsec = timeout_ms * 1'000'000;
+ while (nready == 0) {
+ EV_SET(&changes[0], m_fuse_fd, EVFILT_READ,
+ EV_ADD | EV_ONESHOT, 0, 0, 0);
+ nready = kevent(m_kq, &changes[0], 1, &events[0], 1,
+ &timeout_ts);
+ if (m_quit)
+ return;
+ }
+ ASSERT_LE(0, nready) << strerror(errno);
+ ASSERT_EQ(events[0].ident, (uintptr_t)m_fuse_fd);
+ if (events[0].flags & EV_ERROR)
+ FAIL() << strerror(events[0].data);
+ else if (events[0].flags & EV_EOF)
+ FAIL() << strerror(events[0].fflags);
+ m_nready = events[0].data;
+ break;
+ case POLL:
+ timeout_int = timeout_ms;
+ fds[0].fd = m_fuse_fd;
+ fds[0].events = POLLIN;
+ while (nready == 0) {
+ nready = poll(fds, 1, timeout_int);
+ if (m_quit)
+ return;
+ }
+ ASSERT_LE(0, nready) << strerror(errno);
+ ASSERT_TRUE(fds[0].revents & POLLIN);
+ break;
+ case SELECT:
+ fuse_fd = m_fuse_fd;
+ if (fuse_fd < 0)
+ break;
+ timeout_tv.tv_sec = 0;
+ timeout_tv.tv_usec = timeout_ms * 1'000;
+ nfds = fuse_fd + 1;
+ while (nready == 0) {
+ FD_ZERO(&readfds);
+ FD_SET(fuse_fd, &readfds);
+ nready = select(nfds, &readfds, NULL, NULL,
+ &timeout_tv);
+ if (m_quit)
+ return;
+ }
+ ASSERT_LE(0, nready) << strerror(errno);
+ ASSERT_TRUE(FD_ISSET(fuse_fd, &readfds));
+ break;
+ default:
+ FAIL() << "not yet implemented";
+ }
+ res = read(m_fuse_fd, &in, sizeof(in));
+
+ if (res < 0 && errno != EBADF && !m_quit && !m_expect_unmount) {
+ m_quit = true;
+ FAIL() << "read: " << strerror(errno);
+ }
+ ASSERT_TRUE(res >= static_cast<ssize_t>(sizeof(in.header)) || m_quit);
+ /*
+ * Inconsistently, fuse_in_header.len is the size of the entire
+ * request,including header, even though fuse_out_header.len excludes
+ * the size of the header.
+ */
+ ASSERT_TRUE(res == static_cast<ssize_t>(in.header.len) || m_quit);
+}
+
+void MockFS::write_response(const mockfs_buf_out &out) {
+ fd_set writefds;
+ pollfd fds[1];
+ struct kevent changes[1];
+ struct kevent events[1];
+ int nready, nfds;
+ ssize_t r;
+
+ switch (m_pm) {
+ case BLOCKING:
+ break;
+ case KQ:
+ EV_SET(&changes[0], m_fuse_fd, EVFILT_WRITE,
+ EV_ADD | EV_ONESHOT, 0, 0, 0);
+ nready = kevent(m_kq, &changes[0], 1, &events[0], 1,
+ NULL);
+ ASSERT_LE(0, nready) << strerror(errno);
+ ASSERT_EQ(events[0].ident, (uintptr_t)m_fuse_fd);
+ if (events[0].flags & EV_ERROR)
+ FAIL() << strerror(events[0].data);
+ else if (events[0].flags & EV_EOF)
+ FAIL() << strerror(events[0].fflags);
+ m_nready = events[0].data;
+ break;
+ case POLL:
+ fds[0].fd = m_fuse_fd;
+ fds[0].events = POLLOUT;
+ nready = poll(fds, 1, INFTIM);
+ ASSERT_LE(0, nready) << strerror(errno);
+ ASSERT_EQ(1, nready) << "NULL timeout expired?";
+ ASSERT_TRUE(fds[0].revents & POLLOUT);
+ break;
+ case SELECT:
+ FD_ZERO(&writefds);
+ FD_SET(m_fuse_fd, &writefds);
+ nfds = m_fuse_fd + 1;
+ nready = select(nfds, NULL, &writefds, NULL, NULL);
+ ASSERT_LE(0, nready) << strerror(errno);
+ ASSERT_EQ(1, nready) << "NULL timeout expired?";
+ ASSERT_TRUE(FD_ISSET(m_fuse_fd, &writefds));
+ break;
+ default:
+ FAIL() << "not yet implemented";
+ }
+ r = write(m_fuse_fd, &out, out.header.len);
+ if (out.expected_errno) {
+ ASSERT_EQ(-1, r);
+ ASSERT_EQ(out.expected_errno, errno) << strerror(errno);
+ } else {
+ if (r <= 0 && errno == EINVAL) {
+ printf("Failed to write response. unique=%" PRIu64
+ ":\n", out.header.unique);
+ }
+ ASSERT_TRUE(r > 0 || errno == EAGAIN) << strerror(errno);
+ }
+}
+
+void* MockFS::service(void *pthr_data) {
+ MockFS *mock_fs = (MockFS*)pthr_data;
+
+ mock_fs->loop();
+
+ return (NULL);
+}
+
+void MockFS::unmount() {
+ ::unmount("mountpoint", 0);
+}
diff --git a/tests/sys/fs/fusefs/mockfs.hh b/tests/sys/fs/fusefs/mockfs.hh
new file mode 100644
index 000000000000..f98a5337c9d1
--- /dev/null
+++ b/tests/sys/fs/fusefs/mockfs.hh
@@ -0,0 +1,446 @@
+/*-
+ * 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 <pthread.h>
+
+#include "fuse_kernel.h"
+}
+
+#include <unordered_set>
+
+#include <gmock/gmock.h>
+
+#define TIME_T_MAX (std::numeric_limits<time_t>::max())
+
+/*
+ * A pseudo-fuse errno used indicate that a fuse operation should have no
+ * response, at least not immediately
+ */
+#define FUSE_NORESPONSE 9999
+
+#define SET_OUT_HEADER_LEN(out, variant) { \
+ (out).header.len = (sizeof((out).header) + \
+ sizeof((out).body.variant)); \
+}
+
+/*
+ * Create an expectation on FUSE_LOOKUP and return it so the caller can set
+ * actions.
+ *
+ * This must be a macro instead of a method because EXPECT_CALL returns a type
+ * with a deleted constructor.
+ */
+#define EXPECT_LOOKUP(parent, path) \
+ EXPECT_CALL(*m_mock, process( \
+ ResultOf([=](auto in) { \
+ return (in.header.opcode == FUSE_LOOKUP && \
+ in.header.nodeid == (parent) && \
+ strcmp(in.body.lookup, (path)) == 0); \
+ }, Eq(true)), \
+ _) \
+ )
+
+extern int verbosity;
+
+/*
+ * The maximum that a test case can set max_write, limited by the buffer
+ * supplied when reading from /dev/fuse. This limitation is imposed by
+ * fusefs-libs, but not by the FUSE protocol.
+ */
+const uint32_t max_max_write = 0x20000;
+
+
+/* This struct isn't defined by fuse_kernel.h or libfuse, but it should be */
+struct fuse_create_out {
+ struct fuse_entry_out entry;
+ struct fuse_open_out open;
+};
+
+/* Protocol 7.8 version of struct fuse_attr */
+struct fuse_attr_7_8
+{
+ uint64_t ino;
+ uint64_t size;
+ uint64_t blocks;
+ uint64_t atime;
+ uint64_t mtime;
+ uint64_t ctime;
+ uint32_t atimensec;
+ uint32_t mtimensec;
+ uint32_t ctimensec;
+ uint32_t mode;
+ uint32_t nlink;
+ uint32_t uid;
+ uint32_t gid;
+ uint32_t rdev;
+};
+
+/* Protocol 7.8 version of struct fuse_attr_out */
+struct fuse_attr_out_7_8
+{
+ uint64_t attr_valid;
+ uint32_t attr_valid_nsec;
+ uint32_t dummy;
+ struct fuse_attr_7_8 attr;
+};
+
+/* Protocol 7.8 version of struct fuse_entry_out */
+struct fuse_entry_out_7_8 {
+ uint64_t nodeid; /* Inode ID */
+ uint64_t generation; /* Inode generation: nodeid:gen must
+ be unique for the fs's lifetime */
+ uint64_t entry_valid; /* Cache timeout for the name */
+ uint64_t attr_valid; /* Cache timeout for the attributes */
+ uint32_t entry_valid_nsec;
+ uint32_t attr_valid_nsec;
+ struct fuse_attr_7_8 attr;
+};
+
+/* Output struct for FUSE_CREATE for protocol 7.8 servers */
+struct fuse_create_out_7_8 {
+ struct fuse_entry_out_7_8 entry;
+ struct fuse_open_out open;
+};
+
+/* Output struct for FUSE_INIT for protocol 7.22 and earlier servers */
+struct fuse_init_out_7_22 {
+ uint32_t major;
+ uint32_t minor;
+ uint32_t max_readahead;
+ uint32_t flags;
+ uint16_t max_background;
+ uint16_t congestion_threshold;
+ uint32_t max_write;
+};
+
+union fuse_payloads_in {
+ fuse_access_in access;
+ fuse_bmap_in bmap;
+ /*
+ * In fusefs-libs 3.4.2 and below the buffer size is fixed at 0x21000
+ * minus the header sizes. fusefs-libs 3.4.3 (and FUSE Protocol 7.29)
+ * add a FUSE_MAX_PAGES option that allows it to be greater.
+ *
+ * See fuse_kern_chan.c in fusefs-libs 2.9.9 and below, or
+ * FUSE_DEFAULT_MAX_PAGES_PER_REQ in fusefs-libs 3.4.3 and above.
+ */
+ uint8_t bytes[
+ max_max_write + 0x1000 - sizeof(struct fuse_in_header)
+ ];
+ fuse_copy_file_range_in copy_file_range;
+ fuse_create_in create;
+ fuse_fallocate_in fallocate;
+ fuse_flush_in flush;
+ fuse_fsync_in fsync;
+ fuse_fsync_in fsyncdir;
+ fuse_forget_in forget;
+ fuse_getattr_in getattr;
+ fuse_interrupt_in interrupt;
+ fuse_lk_in getlk;
+ fuse_getxattr_in getxattr;
+ fuse_init_in init;
+ fuse_link_in link;
+ fuse_listxattr_in listxattr;
+ char lookup[0];
+ fuse_lseek_in lseek;
+ fuse_mkdir_in mkdir;
+ fuse_mknod_in mknod;
+ fuse_open_in open;
+ fuse_open_in opendir;
+ fuse_read_in read;
+ fuse_read_in readdir;
+ fuse_release_in release;
+ fuse_release_in releasedir;
+ fuse_rename_in rename;
+ char rmdir[0];
+ fuse_setattr_in setattr;
+ fuse_setxattr_in setxattr;
+ fuse_lk_in setlk;
+ fuse_lk_in setlkw;
+ char unlink[0];
+ fuse_write_in write;
+};
+
+struct mockfs_buf_in {
+ fuse_in_header header;
+ union fuse_payloads_in body;
+};
+
+union fuse_payloads_out {
+ fuse_attr_out attr;
+ fuse_attr_out_7_8 attr_7_8;
+ fuse_bmap_out bmap;
+ fuse_create_out create;
+ fuse_create_out_7_8 create_7_8;
+ /*
+ * The protocol places no limits on the size of bytes. Choose
+ * a size big enough for anything we'll test.
+ */
+ uint8_t bytes[0x40000];
+ fuse_entry_out entry;
+ fuse_entry_out_7_8 entry_7_8;
+ fuse_lk_out getlk;
+ fuse_getxattr_out getxattr;
+ fuse_init_out init;
+ fuse_init_out_7_22 init_7_22;
+ fuse_lseek_out lseek;
+ /* The inval_entry structure should be followed by the entry's name */
+ fuse_notify_inval_entry_out inval_entry;
+ fuse_notify_inval_inode_out inval_inode;
+ /* The store structure should be followed by the data to store */
+ fuse_notify_store_out store;
+ fuse_listxattr_out listxattr;
+ fuse_open_out open;
+ fuse_statfs_out statfs;
+ /*
+ * The protocol places no limits on the length of the string. This is
+ * merely convenient for testing.
+ */
+ char str[80];
+ fuse_write_out write;
+};
+
+struct mockfs_buf_out {
+ fuse_out_header header;
+ union fuse_payloads_out body;
+ /* the expected errno of the write to /dev/fuse */
+ int expected_errno;
+
+ /* Default constructor: zero everything */
+ mockfs_buf_out() {
+ memset(this, 0, sizeof(*this));
+ }
+};
+
+/* A function that can be invoked in place of MockFS::process */
+typedef std::function<void (const mockfs_buf_in& in,
+ std::vector<std::unique_ptr<mockfs_buf_out>> &out)>
+ProcessMockerT;
+
+/*
+ * Helper function used for setting an error expectation for any fuse operation.
+ * The operation will return the supplied error
+ */
+ProcessMockerT ReturnErrno(int error);
+
+/* Helper function used for returning negative cache entries for LOOKUP */
+ProcessMockerT ReturnNegativeCache(const struct timespec *entry_valid);
+
+/* Helper function used for returning a single immediate response */
+ProcessMockerT ReturnImmediate(
+ std::function<void(const mockfs_buf_in& in,
+ struct mockfs_buf_out &out)> f);
+
+/* How the daemon should check /dev/fuse for readiness */
+enum poll_method {
+ BLOCKING,
+ SELECT,
+ POLL,
+ KQ
+};
+
+/*
+ * Fake FUSE filesystem
+ *
+ * "Mounts" a filesystem to a temporary directory and services requests
+ * according to the programmed expectations.
+ *
+ * Operates directly on the fusefs(4) kernel API, not the libfuse(3) user api.
+ */
+class MockFS {
+ /*
+ * thread id of the fuse daemon thread
+ *
+ * It must run in a separate thread so it doesn't deadlock with the
+ * client test code.
+ */
+ pthread_t m_daemon_id;
+
+ /* file descriptor of /dev/fuse control device */
+ volatile int m_fuse_fd;
+
+ /* The minor version of the kernel API that this mock daemon targets */
+ uint32_t m_kernel_minor_version;
+
+ int m_kq;
+
+ /*
+ * If nonzero, the maximum size in bytes of a read that the kernel will
+ * send to the server.
+ */
+ int m_maxread;
+
+ /* The max_readahead file system option */
+ uint32_t m_maxreadahead;
+
+ /* pid of the test process */
+ pid_t m_pid;
+
+ /* Every "unique" value of a fuse ticket seen so far */
+ std::unique_ptr<std::unordered_set<uint64_t>> m_uniques;
+
+ /* Method the daemon should use for I/O to and from /dev/fuse */
+ enum poll_method m_pm;
+
+ /* Timestamp granularity in nanoseconds */
+ unsigned m_time_gran;
+
+ void audit_request(const mockfs_buf_in &in, ssize_t buflen);
+ void debug_request(const mockfs_buf_in&, ssize_t buflen);
+ void debug_response(const mockfs_buf_out&);
+
+ /* Initialize a session after mounting */
+ void init(uint32_t flags);
+
+ /* Is pid from a process that might be involved in the test? */
+ bool pid_ok(pid_t pid);
+
+ /* Default request handler */
+ void process_default(const mockfs_buf_in&,
+ std::vector<std::unique_ptr<mockfs_buf_out>>&);
+
+ /* Entry point for the daemon thread */
+ static void* service(void*);
+
+ /*
+ * Read, but do not process, a single request from the kernel
+ *
+ * @param in Return storage for the FUSE request
+ * @param res Return value of read(2). If positive, the amount of
+ * data read from the fuse device.
+ */
+ void read_request(mockfs_buf_in& in, ssize_t& res);
+
+ public:
+ /* Write a single response back to the kernel */
+ void write_response(const mockfs_buf_out &out);
+
+ /* pid of child process, for two-process test cases */
+ pid_t m_child_pid;
+
+ /* Maximum size of a FUSE_WRITE write */
+ uint32_t m_maxwrite;
+
+ /*
+ * Number of events that were available from /dev/fuse after the last
+ * kevent call. Only valid when m_pm = KQ.
+ */
+ int m_nready;
+
+ /* Tell the daemon to shut down ASAP */
+ bool m_quit;
+
+ /* Tell the daemon that the server might forcibly unmount us */
+ bool m_expect_unmount;
+
+ /* Create a new mockfs and mount it to a tempdir */
+ MockFS(int max_read, int max_readahead, bool allow_other,
+ bool default_permissions, bool push_symlinks_in, bool ro,
+ enum poll_method pm, uint32_t flags,
+ uint32_t kernel_minor_version, uint32_t max_write, bool async,
+ bool no_clusterr, unsigned time_gran, bool nointr,
+ bool noatime, const char *fsname, const char *subtype,
+ bool no_auto_init);
+
+ virtual ~MockFS();
+
+ /* Kill the filesystem daemon without unmounting the filesystem */
+ void kill_daemon();
+
+ /* Wait until the daemon thread terminates */
+ void join_daemon();
+
+ /* Process FUSE requests endlessly */
+ void loop();
+
+ /*
+ * Send an asynchronous notification to invalidate a directory entry.
+ * Similar to libfuse's fuse_lowlevel_notify_inval_entry
+ *
+ * This method will block until the client has responded, so it should
+ * generally be run in a separate thread from request processing.
+ *
+ * @param parent Parent directory's inode number
+ * @param name name of dirent to invalidate
+ * @param namelen size of name, including the NUL
+ * @param expected_errno The error that write() should return
+ */
+ int notify_inval_entry(ino_t parent, const char *name, size_t namelen,
+ int expected_errno = 0);
+
+ /*
+ * Send an asynchronous notification to invalidate an inode's cached
+ * data and/or attributes. Similar to libfuse's
+ * fuse_lowlevel_notify_inval_inode.
+ *
+ * This method will block until the client has responded, so it should
+ * generally be run in a separate thread from request processing.
+ *
+ * @param ino File's inode number
+ * @param off offset at which to begin invalidation. A
+ * negative offset means to invalidate attributes
+ * only.
+ * @param len Size of region of data to invalidate. 0 means
+ * to invalidate all cached data.
+ */
+ int notify_inval_inode(ino_t ino, off_t off, ssize_t len);
+
+ /*
+ * Send an asynchronous notification to store data directly into an
+ * inode's cache. Similar to libfuse's fuse_lowlevel_notify_store.
+ *
+ * This method will block until the client has responded, so it should
+ * generally be run in a separate thread from request processing.
+ *
+ * @param ino File's inode number
+ * @param off Offset at which to store data
+ * @param data Pointer to the data to cache
+ * @param len Size of data
+ */
+ int notify_store(ino_t ino, off_t off, const void* data, ssize_t size);
+
+ /*
+ * Request handler
+ *
+ * This method is expected to provide the responses to each FUSE
+ * operation. For an immediate response, push one buffer into out.
+ * For a delayed response, push nothing. For an immediate response
+ * plus a delayed response to an earlier operation, push two bufs.
+ * Test cases must define each response using Googlemock expectations
+ */
+ MOCK_METHOD2(process, void(const mockfs_buf_in&,
+ std::vector<std::unique_ptr<mockfs_buf_out>>&));
+
+ /* Gracefully unmount */
+ void unmount();
+};
diff --git a/tests/sys/fs/fusefs/mount.cc b/tests/sys/fs/fusefs/mount.cc
new file mode 100644
index 000000000000..ece518b09f66
--- /dev/null
+++ b/tests/sys/fs/fusefs/mount.cc
@@ -0,0 +1,200 @@
+/*-
+ * 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/param.h>
+#include <sys/mount.h>
+#include <sys/uio.h>
+
+#include <mntopts.h> // for build_iovec
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Mount: public FuseTest {
+public:
+void expect_statfs() {
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_STATFS);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, statfs);
+ })));
+}
+};
+
+class Fsname: public Mount {
+ void SetUp() {
+ m_fsname = "http://something";
+ Mount::SetUp();
+ }
+};
+
+class Subtype: public Mount {
+ void SetUp() {
+ m_subtype = "myfs";
+ Mount::SetUp();
+ }
+};
+
+class UpdateOk: public Mount, public WithParamInterface<const char*> {};
+class UpdateErr: public Mount, public WithParamInterface<const char*> {};
+
+int mntflag_from_string(const char *s)
+{
+ if (0 == strcmp("MNT_RDONLY", s))
+ return MNT_RDONLY;
+ else if (0 == strcmp("MNT_NOEXEC", s))
+ return MNT_NOEXEC;
+ else if (0 == strcmp("MNT_NOSUID", s))
+ return MNT_NOSUID;
+ else if (0 == strcmp("MNT_NOATIME", s))
+ return MNT_NOATIME;
+ else if (0 == strcmp("MNT_SUIDDIR", s))
+ return MNT_SUIDDIR;
+ else if (0 == strcmp("MNT_USER", s))
+ return MNT_USER;
+ else
+ return 0;
+}
+
+TEST_F(Fsname, fsname)
+{
+ struct statfs statbuf;
+
+ expect_statfs();
+
+ ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
+ ASSERT_STREQ("http://something", statbuf.f_mntfromname);
+}
+
+TEST_F(Subtype, subtype)
+{
+ struct statfs statbuf;
+
+ expect_statfs();
+
+ ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
+ ASSERT_STREQ("fusefs.myfs", statbuf.f_fstypename);
+}
+
+/* Some mount options can be changed by mount -u */
+TEST_P(UpdateOk, update)
+{
+ struct statfs statbuf;
+ struct iovec *iov = NULL;
+ int iovlen = 0;
+ int flag;
+ int newflags = MNT_UPDATE | MNT_SYNCHRONOUS;
+
+ flag = mntflag_from_string(GetParam());
+ if (flag == MNT_NOSUID && 0 != geteuid())
+ GTEST_SKIP() << "Only root may clear MNT_NOSUID";
+ if (flag == MNT_SUIDDIR && 0 != geteuid())
+ GTEST_SKIP() << "Only root may set MNT_SUIDDIR";
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_STATFS);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ /*
+ * All of the fields except f_flags are don't care, and f_flags is set by
+ * the VFS
+ */
+ SET_OUT_HEADER_LEN(out, statfs);
+ })));
+
+ ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
+ newflags = (statbuf.f_flags | MNT_UPDATE) ^ flag;
+
+ build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1);
+ build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1);
+ build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1);
+ ASSERT_EQ(0, nmount(iov, iovlen, newflags)) << strerror(errno);
+
+ ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
+ EXPECT_FALSE((newflags ^ statbuf.f_flags) & flag);
+}
+
+/* Some mount options cannnot be changed by mount -u */
+TEST_P(UpdateErr, update)
+{
+ struct statfs statbuf;
+ struct iovec *iov = NULL;
+ int iovlen = 0;
+ int flag;
+ int newflags = MNT_UPDATE | MNT_SYNCHRONOUS;
+
+ flag = mntflag_from_string(GetParam());
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_STATFS);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ /*
+ * All of the fields except f_flags are don't care, and f_flags is set by
+ * the VFS
+ */
+ SET_OUT_HEADER_LEN(out, statfs);
+ })));
+
+ ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
+ newflags = (statbuf.f_flags | MNT_UPDATE) ^ flag;
+
+ build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1);
+ build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1);
+ build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1);
+ /*
+ * Don't check nmount's return value, because vfs_domount may "fix" the
+ * options for us. The important thing is to check the final value of
+ * statbuf.f_flags below.
+ */
+ (void)nmount(iov, iovlen, newflags);
+
+ ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
+ EXPECT_TRUE((newflags ^ statbuf.f_flags) & flag);
+}
+
+INSTANTIATE_TEST_SUITE_P(Mount, UpdateOk,
+ ::testing::Values("MNT_RDONLY", "MNT_NOEXEC", "MNT_NOSUID", "MNT_NOATIME",
+ "MNT_SUIDDIR")
+);
+
+INSTANTIATE_TEST_SUITE_P(Mount, UpdateErr,
+ ::testing::Values( "MNT_USER")
+);
diff --git a/tests/sys/fs/fusefs/nfs.cc b/tests/sys/fs/fusefs/nfs.cc
new file mode 100644
index 000000000000..2fa2b290f383
--- /dev/null
+++ b/tests/sys/fs/fusefs/nfs.cc
@@ -0,0 +1,480 @@
+/*-
+ * 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.
+ */
+
+/* This file tests functionality needed by NFS servers */
+extern "C" {
+#include <sys/param.h>
+#include <sys/mount.h>
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace std;
+using namespace testing;
+
+
+class Nfs: public FuseTest {
+public:
+virtual void SetUp() {
+ if (geteuid() != 0)
+ GTEST_SKIP() << "This test requires a privileged user";
+ FuseTest::SetUp();
+}
+};
+
+class Exportable: public Nfs {
+public:
+virtual void SetUp() {
+ m_init_flags = FUSE_EXPORT_SUPPORT;
+ Nfs::SetUp();
+}
+};
+
+class Fhstat: public Exportable {};
+class FhstatNotExportable: public Nfs {};
+class Getfh: public Exportable {};
+class Readdir: public Exportable {};
+
+/* If the server returns a different generation number, then file is stale */
+TEST_F(Fhstat, estale)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t ino = 42;
+ const mode_t mode = S_IFDIR | 0755;
+ Sequence seq;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.generation = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ EXPECT_LOOKUP(ino, ".")
+ .InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.generation = 2;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ ASSERT_EQ(-1, fhstat(&fhp, &sb));
+ EXPECT_EQ(ESTALE, errno);
+}
+
+/* If we must lookup an entry from the server, send a LOOKUP request for "." */
+TEST_F(Fhstat, lookup_dot)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t ino = 42;
+ const mode_t mode = S_IFDIR | 0755;
+ const uid_t uid = 12345;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ EXPECT_LOOKUP(ino, ".")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
+ EXPECT_EQ(uid, sb.st_uid);
+ EXPECT_EQ(mode, sb.st_mode);
+}
+
+/* Gracefully handle failures to lookup ".". */
+TEST_F(Fhstat, lookup_dot_error)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t ino = 42;
+ const mode_t mode = S_IFDIR | 0755;
+ const uid_t uid = 12345;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ EXPECT_LOOKUP(ino, ".")
+ .WillOnce(Invoke(ReturnErrno(EDOOFUS)));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ ASSERT_EQ(-1, fhstat(&fhp, &sb));
+ EXPECT_EQ(EDOOFUS, errno);
+}
+
+/* Use a file handle whose entry is still cached */
+TEST_F(Fhstat, cached)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t ino = 42;
+ const mode_t mode = S_IFDIR | 0755;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
+ EXPECT_EQ(ino, sb.st_ino);
+}
+
+/* File handle entries should expire from the cache, too */
+TEST_F(Fhstat, cache_expired)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t ino = 42;
+ const mode_t mode = S_IFDIR | 0755;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid_nsec = NAP_NS / 2;
+ })));
+
+ EXPECT_LOOKUP(ino, ".")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
+ EXPECT_EQ(ino, sb.st_ino);
+
+ nap();
+
+ /* Cache should be expired; fuse should issue a FUSE_LOOKUP */
+ ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
+ EXPECT_EQ(ino, sb.st_ino);
+}
+
+/*
+ * If the server returns a FUSE_LOOKUP response for a nodeid that we didn't
+ * lookup, it's a bug. But we should handle it gracefully.
+ */
+TEST_F(Fhstat, inconsistent_nodeid)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t ino_in = 42;
+ const uint64_t ino_out = 43;
+ const mode_t mode = S_IFDIR | 0755;
+ const uid_t uid = 12345;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = ino_in;
+ out.body.entry.attr.ino = ino_in;
+ out.body.entry.attr.mode = mode;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ EXPECT_LOOKUP(ino_in, ".")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = ino_out;
+ out.body.entry.attr.ino = ino_out;
+ out.body.entry.attr.mode = mode;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ EXPECT_NE(0, fhstat(&fhp, &sb)) << strerror(errno);
+ EXPECT_EQ(EIO, errno);
+}
+
+/*
+ * If the server returns a FUSE_LOOKUP response where the nodeid doesn't match
+ * the inode number, and the file system is exported, it's a bug. But we
+ * should handle it gracefully.
+ */
+TEST_F(Fhstat, inconsistent_ino)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t nodeid = 42;
+ const uint64_t ino = 711; // Could be anything that != nodeid
+ const mode_t mode = S_IFDIR | 0755;
+ const uid_t uid = 12345;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = nodeid;
+ out.body.entry.attr.ino = nodeid;
+ out.body.entry.attr.mode = mode;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ EXPECT_LOOKUP(nodeid, ".")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = nodeid;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.attr.mode = mode;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ /*
+ * The fhstat operation will actually succeed. But future operations
+ * will likely fail.
+ */
+ ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
+ EXPECT_EQ(ino, sb.st_ino);
+}
+
+/*
+ * If the server doesn't set FUSE_EXPORT_SUPPORT, then we can't do NFS-style
+ * lookups
+ */
+TEST_F(FhstatNotExportable, lookup_dot)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ const uint64_t ino = 42;
+ const mode_t mode = S_IFDIR | 0755;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.generation = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ ASSERT_EQ(-1, getfh(FULLPATH, &fhp));
+ ASSERT_EQ(EOPNOTSUPP, errno);
+}
+
+/* FreeBSD's fid struct doesn't have enough space for 64-bit generations */
+TEST_F(Getfh, eoverflow)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.generation = (uint64_t)UINT32_MAX + 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ ASSERT_NE(0, getfh(FULLPATH, &fhp));
+ EXPECT_EQ(EOVERFLOW, errno);
+}
+
+/* Get an NFS file handle */
+TEST_F(Getfh, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+}
+
+/*
+ * Call readdir via a file handle.
+ *
+ * This is how a userspace nfs server like nfs-ganesha or unfs3 would call
+ * readdir. The in-kernel NFS server never does any equivalent of open. I
+ * haven't discovered a way to mimic nfsd's behavior short of actually running
+ * nfsd.
+ */
+TEST_F(Readdir, getdirentries)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ mode_t mode = S_IFDIR | 0755;
+ fhandle_t fhp;
+ int fd;
+ char buf[8192];
+ ssize_t r;
+
+ 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 = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.generation = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ EXPECT_LOOKUP(ino, ".")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.generation = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ expect_opendir(ino);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READDIR &&
+ in.header.nodeid == ino &&
+ in.body.readdir.size == sizeof(buf));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ out.header.error = 0;
+ out.header.len = sizeof(out.header);
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ fd = fhopen(&fhp, O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ r = getdirentries(fd, buf, sizeof(buf), 0);
+ ASSERT_EQ(0, r) << strerror(errno);
+
+ leak(fd);
+}
diff --git a/tests/sys/fs/fusefs/notify.cc b/tests/sys/fs/fusefs/notify.cc
new file mode 100644
index 000000000000..d370a1e6e706
--- /dev/null
+++ b/tests/sys/fs/fusefs/notify.cc
@@ -0,0 +1,602 @@
+/*-
+ * 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 <fcntl.h>
+#include <pthread.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+/*
+ * FUSE asynchonous notification
+ *
+ * FUSE servers can send unprompted notification messages for things like cache
+ * invalidation. This file tests our client's handling of those messages.
+ */
+
+class Notify: public FuseTest {
+public:
+/* Ignore an optional FUSE_FSYNC */
+void maybe_expect_fsync(uint64_t ino)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_FSYNC &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(0)));
+}
+
+void expect_lookup(uint64_t parent, const char *relpath, uint64_t ino,
+ off_t size, Sequence &seq)
+{
+ EXPECT_LOOKUP(parent, relpath)
+ .InSequence(seq)
+ .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.ino = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr.size = size;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+}
+};
+
+class NotifyWriteback: public Notify {
+public:
+virtual void SetUp() {
+ m_init_flags |= FUSE_WRITEBACK_CACHE;
+ m_async = true;
+ Notify::SetUp();
+ if (IsSkipped())
+ return;
+}
+
+void expect_write(uint64_t ino, uint64_t offset, uint64_t size,
+ const void *contents)
+{
+ FuseTest::expect_write(ino, offset, size, size, 0, 0, contents);
+}
+
+};
+
+struct inval_entry_args {
+ MockFS *mock;
+ ino_t parent;
+ const char *name;
+ size_t namelen;
+};
+
+static void* inval_entry(void* arg) {
+ const struct inval_entry_args *iea = (struct inval_entry_args*)arg;
+ ssize_t r;
+
+ r = iea->mock->notify_inval_entry(iea->parent, iea->name, iea->namelen);
+ if (r >= 0)
+ return 0;
+ else
+ return (void*)(intptr_t)errno;
+}
+
+struct inval_inode_args {
+ MockFS *mock;
+ ino_t ino;
+ off_t off;
+ ssize_t len;
+};
+
+struct store_args {
+ MockFS *mock;
+ ino_t nodeid;
+ off_t offset;
+ ssize_t size;
+ const void* data;
+};
+
+static void* inval_inode(void* arg) {
+ const struct inval_inode_args *iia = (struct inval_inode_args*)arg;
+ ssize_t r;
+
+ r = iia->mock->notify_inval_inode(iia->ino, iia->off, iia->len);
+ if (r >= 0)
+ return 0;
+ else
+ return (void*)(intptr_t)errno;
+}
+
+static void* store(void* arg) {
+ const struct store_args *sa = (struct store_args*)arg;
+ ssize_t r;
+
+ r = sa->mock->notify_store(sa->nodeid, sa->offset, sa->data, sa->size);
+ if (r >= 0)
+ return 0;
+ else
+ return (void*)(intptr_t)errno;
+}
+
+/* Invalidate a nonexistent entry */
+TEST_F(Notify, inval_entry_nonexistent)
+{
+ const static char *name = "foo";
+ struct inval_entry_args iea;
+ void *thr0_value;
+ pthread_t th0;
+
+ iea.mock = m_mock;
+ iea.parent = FUSE_ROOT_ID;
+ iea.name = name;
+ iea.namelen = strlen(name);
+ ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
+ << strerror(errno);
+ pthread_join(th0, &thr0_value);
+ /* It's not an error for an entry to not be cached */
+ EXPECT_EQ(0, (intptr_t)thr0_value);
+}
+
+/* Invalidate a cached entry */
+TEST_F(Notify, inval_entry)
+{
+ const static char FULLPATH[] = "mountpoint/foo";
+ const static char RELPATH[] = "foo";
+ struct inval_entry_args iea;
+ struct stat sb;
+ void *thr0_value;
+ uint64_t ino0 = 42;
+ uint64_t ino1 = 43;
+ Sequence seq;
+ pthread_t th0;
+
+ expect_lookup(FUSE_ROOT_ID, RELPATH, ino0, 0, seq);
+ expect_lookup(FUSE_ROOT_ID, RELPATH, ino1, 0, seq);
+
+ /* Fill the entry cache */
+ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
+ EXPECT_EQ(ino0, sb.st_ino);
+
+ /* Now invalidate the entry */
+ iea.mock = m_mock;
+ iea.parent = FUSE_ROOT_ID;
+ iea.name = RELPATH;
+ iea.namelen = strlen(RELPATH);
+ ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
+ << strerror(errno);
+ pthread_join(th0, &thr0_value);
+ EXPECT_EQ(0, (intptr_t)thr0_value);
+
+ /* The second lookup should return the alternate ino */
+ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
+ EXPECT_EQ(ino1, sb.st_ino);
+}
+
+/*
+ * Invalidate a cached entry beneath the root, which uses a slightly different
+ * code path.
+ */
+TEST_F(Notify, inval_entry_below_root)
+{
+ const static char FULLPATH[] = "mountpoint/some_dir/foo";
+ const static char DNAME[] = "some_dir";
+ const static char FNAME[] = "foo";
+ struct inval_entry_args iea;
+ struct stat sb;
+ void *thr0_value;
+ uint64_t dir_ino = 41;
+ uint64_t ino0 = 42;
+ uint64_t ino1 = 43;
+ Sequence seq;
+ pthread_t th0;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, DNAME)
+ .WillOnce(Invoke(
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = dir_ino;
+ out.body.entry.attr.nlink = 2;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ expect_lookup(dir_ino, FNAME, ino0, 0, seq);
+ expect_lookup(dir_ino, FNAME, ino1, 0, seq);
+
+ /* Fill the entry cache */
+ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
+ EXPECT_EQ(ino0, sb.st_ino);
+
+ /* Now invalidate the entry */
+ iea.mock = m_mock;
+ iea.parent = dir_ino;
+ iea.name = FNAME;
+ iea.namelen = strlen(FNAME);
+ ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
+ << strerror(errno);
+ pthread_join(th0, &thr0_value);
+ EXPECT_EQ(0, (intptr_t)thr0_value);
+
+ /* The second lookup should return the alternate ino */
+ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
+ EXPECT_EQ(ino1, sb.st_ino);
+}
+
+/* Invalidating an entry invalidates the parent directory's attributes */
+TEST_F(Notify, inval_entry_invalidates_parent_attrs)
+{
+ const static char FULLPATH[] = "mountpoint/foo";
+ const static char RELPATH[] = "foo";
+ struct inval_entry_args iea;
+ struct stat sb;
+ void *thr0_value;
+ uint64_t ino = 42;
+ Sequence seq;
+ pthread_t th0;
+
+ expect_lookup(FUSE_ROOT_ID, RELPATH, ino, 0, seq);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == FUSE_ROOT_ID);
+ }, Eq(true)),
+ _)
+ ).Times(2)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.mode = S_IFDIR | 0755;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+
+ /* Fill the attr and entry cache */
+ ASSERT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
+ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
+
+ /* Now invalidate the entry */
+ iea.mock = m_mock;
+ iea.parent = FUSE_ROOT_ID;
+ iea.name = RELPATH;
+ iea.namelen = strlen(RELPATH);
+ ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
+ << strerror(errno);
+ pthread_join(th0, &thr0_value);
+ EXPECT_EQ(0, (intptr_t)thr0_value);
+
+ /* /'s attribute cache should be cleared */
+ ASSERT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
+}
+
+
+TEST_F(Notify, inval_inode_nonexistent)
+{
+ struct inval_inode_args iia;
+ ino_t ino = 42;
+ void *thr0_value;
+ pthread_t th0;
+
+ iia.mock = m_mock;
+ iia.ino = ino;
+ iia.off = 0;
+ iia.len = 0;
+ ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
+ << strerror(errno);
+ pthread_join(th0, &thr0_value);
+ /* It's not an error for an inode to not be cached */
+ EXPECT_EQ(0, (intptr_t)thr0_value);
+}
+
+TEST_F(Notify, inval_inode_with_clean_cache)
+{
+ const static char FULLPATH[] = "mountpoint/foo";
+ const static char RELPATH[] = "foo";
+ const char CONTENTS0[] = "abcdefgh";
+ const char CONTENTS1[] = "ijklmnopqrstuvwxyz";
+ struct inval_inode_args iia;
+ struct stat sb;
+ ino_t ino = 42;
+ void *thr0_value;
+ Sequence seq;
+ uid_t uid = 12345;
+ pthread_t th0;
+ ssize_t size0 = sizeof(CONTENTS0);
+ ssize_t size1 = sizeof(CONTENTS1);
+ char buf[80];
+ int fd;
+
+ expect_lookup(FUSE_ROOT_ID, RELPATH, ino, size0, seq);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.mode = S_IFREG | 0644;
+ out.body.attr.attr_valid = UINT64_MAX;
+ out.body.attr.attr.size = size1;
+ out.body.attr.attr.uid = uid;
+ })));
+ expect_read(ino, 0, size0, size0, CONTENTS0);
+ expect_read(ino, 0, size1, size1, CONTENTS1);
+
+ /* Fill the data cache */
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(size0, read(fd, buf, size0)) << strerror(errno);
+ EXPECT_EQ(0, memcmp(buf, CONTENTS0, size0));
+
+ /* Evict the data cache */
+ iia.mock = m_mock;
+ iia.ino = ino;
+ iia.off = 0;
+ iia.len = 0;
+ ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
+ << strerror(errno);
+ pthread_join(th0, &thr0_value);
+ EXPECT_EQ(0, (intptr_t)thr0_value);
+
+ /* cache attributes were purged; this will trigger a new GETATTR */
+ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
+ EXPECT_EQ(uid, sb.st_uid);
+ EXPECT_EQ(size1, sb.st_size);
+
+ /* This read should not be serviced by cache */
+ ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno);
+ ASSERT_EQ(size1, read(fd, buf, size1)) << strerror(errno);
+ EXPECT_EQ(0, memcmp(buf, CONTENTS1, size1));
+
+ leak(fd);
+}
+
+/*
+ * Attempting to invalidate an entry or inode after unmounting should fail, but
+ * nothing bad should happen.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=290519
+ */
+TEST_F(Notify, notify_after_unmount)
+{
+ const static char *name = "foo";
+ struct inval_entry_args iea;
+
+ expect_destroy(0);
+
+ m_mock->unmount();
+
+ iea.mock = m_mock;
+ iea.parent = FUSE_ROOT_ID;
+ iea.name = name;
+ iea.namelen = strlen(name);
+ iea.mock->notify_inval_entry(iea.parent, iea.name, iea.namelen, ENODEV);
+}
+
+/* FUSE_NOTIFY_STORE with a file that's not in the entry cache */
+/* disabled because FUSE_NOTIFY_STORE is not yet implemented */
+TEST_F(Notify, DISABLED_store_nonexistent)
+{
+ struct store_args sa;
+ ino_t ino = 42;
+ void *thr0_value;
+ pthread_t th0;
+
+ sa.mock = m_mock;
+ sa.nodeid = ino;
+ sa.offset = 0;
+ sa.size = 0;
+ ASSERT_EQ(0, pthread_create(&th0, NULL, store, &sa)) << strerror(errno);
+ pthread_join(th0, &thr0_value);
+ /* It's not an error for a file to be unknown to the kernel */
+ EXPECT_EQ(0, (intptr_t)thr0_value);
+}
+
+/* Store data into for a file that does not yet have anything cached */
+/* disabled because FUSE_NOTIFY_STORE is not yet implemented */
+TEST_F(Notify, DISABLED_store_with_blank_cache)
+{
+ const static char FULLPATH[] = "mountpoint/foo";
+ const static char RELPATH[] = "foo";
+ const char CONTENTS1[] = "ijklmnopqrstuvwxyz";
+ struct store_args sa;
+ ino_t ino = 42;
+ void *thr0_value;
+ Sequence seq;
+ pthread_t th0;
+ ssize_t size1 = sizeof(CONTENTS1);
+ char buf[80];
+ int fd;
+
+ expect_lookup(FUSE_ROOT_ID, RELPATH, ino, size1, seq);
+ expect_open(ino, 0, 1);
+
+ /* Fill the data cache */
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /* Evict the data cache */
+ sa.mock = m_mock;
+ sa.nodeid = ino;
+ sa.offset = 0;
+ sa.size = size1;
+ sa.data = (const void*)CONTENTS1;
+ ASSERT_EQ(0, pthread_create(&th0, NULL, store, &sa)) << strerror(errno);
+ pthread_join(th0, &thr0_value);
+ EXPECT_EQ(0, (intptr_t)thr0_value);
+
+ /* This read should be serviced by cache */
+ ASSERT_EQ(size1, read(fd, buf, size1)) << strerror(errno);
+ EXPECT_EQ(0, memcmp(buf, CONTENTS1, size1));
+
+ leak(fd);
+}
+
+TEST_F(NotifyWriteback, inval_inode_with_dirty_cache)
+{
+ const static char FULLPATH[] = "mountpoint/foo";
+ const static char RELPATH[] = "foo";
+ const char CONTENTS[] = "abcdefgh";
+ struct inval_inode_args iia;
+ ino_t ino = 42;
+ void *thr0_value;
+ Sequence seq;
+ pthread_t th0;
+ ssize_t bufsize = sizeof(CONTENTS);
+ int fd;
+
+ expect_lookup(FUSE_ROOT_ID, RELPATH, ino, 0, seq);
+ expect_open(ino, 0, 1);
+
+ /* Fill the data cache */
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+
+ expect_write(ino, 0, bufsize, CONTENTS);
+ /*
+ * The FUSE protocol does not require an fsync here, but FreeBSD's
+ * bufobj_invalbuf sends it anyway
+ */
+ maybe_expect_fsync(ino);
+
+ /* Evict the data cache */
+ iia.mock = m_mock;
+ iia.ino = ino;
+ iia.off = 0;
+ iia.len = 0;
+ ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
+ << strerror(errno);
+ pthread_join(th0, &thr0_value);
+ EXPECT_EQ(0, (intptr_t)thr0_value);
+
+ leak(fd);
+}
+
+TEST_F(NotifyWriteback, inval_inode_attrs_only)
+{
+ const static char FULLPATH[] = "mountpoint/foo";
+ const static char RELPATH[] = "foo";
+ const char CONTENTS[] = "abcdefgh";
+ struct inval_inode_args iia;
+ struct stat sb;
+ uid_t uid = 12345;
+ ino_t ino = 42;
+ void *thr0_value;
+ Sequence seq;
+ pthread_t th0;
+ ssize_t bufsize = sizeof(CONTENTS);
+ int fd;
+
+ expect_lookup(FUSE_ROOT_ID, RELPATH, ino, 0, seq);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_WRITE);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.mode = S_IFREG | 0644;
+ out.body.attr.attr_valid = UINT64_MAX;
+ out.body.attr.attr.size = bufsize;
+ out.body.attr.attr.uid = uid;
+ })));
+
+ /* Fill the data cache */
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+
+ /* Evict the attributes, but not data cache */
+ iia.mock = m_mock;
+ iia.ino = ino;
+ iia.off = -1;
+ iia.len = 0;
+ ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
+ << strerror(errno);
+ pthread_join(th0, &thr0_value);
+ EXPECT_EQ(0, (intptr_t)thr0_value);
+
+ /* cache attributes were been purged; this will trigger a new GETATTR */
+ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
+ EXPECT_EQ(uid, sb.st_uid);
+ EXPECT_EQ(bufsize, sb.st_size);
+
+ leak(fd);
+}
+
+/*
+ * Attempting asynchronous invalidation of an Entry before mounting the file
+ * system should fail, but nothing bad should happen.
+ *
+ * Note that invalidating an inode before mount goes through the same path, and
+ * is not separately tested.
+ *
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=290519
+ */
+TEST(PreMount, inval_entry_before_mount)
+{
+ const static char name[] = "foo";
+ size_t namelen = strlen(name);
+ struct mockfs_buf_out *out;
+ int r;
+ int fuse_fd;
+
+ fuse_fd = open("/dev/fuse", O_CLOEXEC | O_RDWR);
+ ASSERT_GE(fuse_fd, 0) << strerror(errno);
+
+ out = new mockfs_buf_out;
+ out->header.unique = 0; /* 0 means asynchronous notification */
+ out->header.error = FUSE_NOTIFY_INVAL_ENTRY;
+ out->body.inval_entry.parent = FUSE_ROOT_ID;
+ out->body.inval_entry.namelen = namelen;
+ strlcpy((char*)&out->body.bytes + sizeof(out->body.inval_entry),
+ name, sizeof(out->body.bytes) - sizeof(out->body.inval_entry));
+ out->header.len = sizeof(out->header) + sizeof(out->body.inval_entry) +
+ namelen;
+ r = write(fuse_fd, out, out->header.len);
+ EXPECT_EQ(-1, r);
+ EXPECT_EQ(ENODEV, errno);
+ delete out;
+}
diff --git a/tests/sys/fs/fusefs/open.cc b/tests/sys/fs/fusefs/open.cc
new file mode 100644
index 000000000000..1212a7047f26
--- /dev/null
+++ b/tests/sys/fs/fusefs/open.cc
@@ -0,0 +1,307 @@
+/*-
+ * 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/wait.h>
+
+#include <fcntl.h>
+#include <semaphore.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Open: public FuseTest {
+
+public:
+
+/* Test an OK open of a file with the given flags */
+void test_ok(int os_flags, int fuse_flags) {
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPEN &&
+ in.body.open.flags == (uint32_t)fuse_flags &&
+ 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);
+ })));
+
+ fd = open(FULLPATH, os_flags);
+ ASSERT_LE(0, fd) << strerror(errno);
+ leak(fd);
+}
+};
+
+/*
+ * fusefs(4) does not support I/O on device nodes (neither does UFS). But it
+ * shouldn't crash
+ */
+TEST_F(Open, chr)
+{
+ const char FULLPATH[] = "mountpoint/zero";
+ const char RELPATH[] = "zero";
+ uint64_t ino = 42;
+
+ 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_IFCHR | 0644;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.attr.rdev = 44; /* /dev/zero's rdev */
+ })));
+
+ ASSERT_EQ(-1, open(FULLPATH, O_RDONLY));
+ EXPECT_EQ(EOPNOTSUPP, errno);
+}
+
+/*
+ * The fuse daemon fails the request with enoent. This usually indicates a
+ * race condition: some other FUSE client removed the file in between when the
+ * kernel checked for it with lookup and tried to open it
+ */
+TEST_F(Open, enoent)
+{
+ 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_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPEN &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENOENT)));
+ // Since FUSE_OPEN returns ENOENT, the kernel will reclaim the vnode
+ // and send a FUSE_FORGET
+ expect_forget(ino, 1, &sem);
+
+ ASSERT_EQ(-1, open(FULLPATH, O_RDONLY));
+ EXPECT_EQ(ENOENT, errno);
+
+ sem_wait(&sem);
+ sem_destroy(&sem);
+}
+
+/*
+ * The daemon is responsible for checking file permissions (unless the
+ * default_permissions mount option was used)
+ */
+TEST_F(Open, eperm)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPEN &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(EPERM)));
+ ASSERT_EQ(-1, open(FULLPATH, O_RDONLY));
+ EXPECT_EQ(EPERM, errno);
+}
+
+/*
+ * fusefs must issue multiple FUSE_OPEN operations if clients with different
+ * credentials open the same file, even if they use the same mode. This is
+ * necessary so that the daemon can validate each set of credentials.
+ */
+TEST_F(Open, multiple_creds)
+{
+ const static char FULLPATH[] = "mountpoint/some_file.txt";
+ const static char RELPATH[] = "some_file.txt";
+ int fd1, status;
+ const static uint64_t ino = 42;
+ const static uint64_t fh0 = 100, fh1 = 200;
+
+ /* Fork a child to open the file with different credentials */
+ fork(false, &status, [&] {
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPEN &&
+ in.header.pid == (uint32_t)getpid() &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(
+ ReturnImmediate([](auto in __unused, auto& out) {
+ out.body.open.fh = fh0;
+ out.header.len = sizeof(out.header);
+ SET_OUT_HEADER_LEN(out, open);
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPEN &&
+ in.header.pid != (uint32_t)getpid() &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(
+ ReturnImmediate([](auto in __unused, auto& out) {
+ out.body.open.fh = fh1;
+ out.header.len = sizeof(out.header);
+ SET_OUT_HEADER_LEN(out, open);
+ })));
+ expect_flush(ino, 2, ReturnErrno(0));
+ expect_release(ino, fh0);
+ expect_release(ino, fh1);
+
+ fd1 = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd1) << strerror(errno);
+ }, [] {
+ int fd0;
+
+ fd0 = open(FULLPATH, O_RDONLY);
+ if (fd0 < 0) {
+ perror("open");
+ return(1);
+ }
+ leak(fd0);
+ return 0;
+ }
+ );
+ ASSERT_EQ(0, WEXITSTATUS(status));
+
+ close(fd1);
+}
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */
+TEST_F(Open, DISABLED_o_append)
+{
+ test_ok(O_WRONLY | O_APPEND, O_WRONLY | O_APPEND);
+}
+
+/* The kernel is supposed to filter out this flag */
+TEST_F(Open, o_creat)
+{
+ test_ok(O_WRONLY | O_CREAT, O_WRONLY);
+}
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */
+TEST_F(Open, DISABLED_o_direct)
+{
+ test_ok(O_WRONLY | O_DIRECT, O_WRONLY | O_DIRECT);
+}
+
+/* The kernel is supposed to filter out this flag */
+TEST_F(Open, o_excl)
+{
+ test_ok(O_WRONLY | O_EXCL, O_WRONLY);
+}
+
+TEST_F(Open, o_exec)
+{
+ test_ok(O_EXEC, O_EXEC);
+}
+
+/* The kernel is supposed to filter out this flag */
+TEST_F(Open, o_noctty)
+{
+ test_ok(O_WRONLY | O_NOCTTY, O_WRONLY);
+}
+
+TEST_F(Open, o_rdonly)
+{
+ test_ok(O_RDONLY, O_RDONLY);
+}
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */
+TEST_F(Open, DISABLED_o_trunc)
+{
+ test_ok(O_WRONLY | O_TRUNC, O_WRONLY | O_TRUNC);
+}
+
+TEST_F(Open, o_wronly)
+{
+ test_ok(O_WRONLY, O_WRONLY);
+}
+
+TEST_F(Open, o_rdwr)
+{
+ test_ok(O_RDWR, O_RDWR);
+}
+
+/*
+ * If a fuse server returns ENOSYS to a
+ * FUSE_OPEN, then it and subsequent FUSE_OPEN and FUSE_RELEASE operations will
+ * also succeed automatically without being sent to the server.
+ */
+TEST_F(Open, enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPEN &&
+ in.body.open.flags == (uint32_t)O_RDONLY &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(1)
+ .WillOnce(Invoke(ReturnErrno(ENOSYS)));
+ expect_flush(ino, 1, ReturnErrno(ENOSYS));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ close(fd);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ leak(fd);
+}
diff --git a/tests/sys/fs/fusefs/opendir.cc b/tests/sys/fs/fusefs/opendir.cc
new file mode 100644
index 000000000000..e1fed59635fc
--- /dev/null
+++ b/tests/sys/fs/fusefs/opendir.cc
@@ -0,0 +1,197 @@
+/*-
+ * 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 <dirent.h>
+
+#include <fcntl.h>
+#include <semaphore.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Opendir: public FuseTest {
+public:
+void expect_lookup(const char *relpath, uint64_t ino)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFDIR | 0755, 0, 1);
+}
+
+void expect_opendir(uint64_t ino, uint32_t flags, ProcessMockerT r)
+{
+ /* opendir(3) calls fstatfs */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_STATFS);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, statfs);
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPENDIR &&
+ in.header.nodeid == ino &&
+ in.body.opendir.flags == flags);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(r));
+}
+
+};
+
+
+/*
+ * The fuse daemon fails the request with enoent. This usually indicates a
+ * race condition: some other FUSE client removed the file in between when the
+ * kernel checked for it with lookup and tried to open it
+ */
+TEST_F(Opendir, enoent)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ sem_t sem;
+
+ ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino, O_RDONLY, ReturnErrno(ENOENT));
+ // Since FUSE_OPENDIR returns ENOENT, the kernel will reclaim the vnode
+ // and send a FUSE_FORGET
+ expect_forget(ino, 1, &sem);
+
+ ASSERT_EQ(-1, open(FULLPATH, O_DIRECTORY));
+ EXPECT_EQ(ENOENT, errno);
+
+ sem_wait(&sem);
+ sem_destroy(&sem);
+}
+
+/*
+ * The daemon is responsible for checking file permissions (unless the
+ * default_permissions mount option was used)
+ */
+TEST_F(Opendir, eperm)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino, O_RDONLY, ReturnErrno(EPERM));
+
+ EXPECT_EQ(-1, open(FULLPATH, O_DIRECTORY));
+ EXPECT_EQ(EPERM, errno);
+}
+
+TEST_F(Opendir, open)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino, O_RDONLY,
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, open);
+ }));
+
+ fd = open(FULLPATH, O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ leak(fd);
+}
+
+/* Directories can be opened O_EXEC for stuff like fchdir(2) */
+TEST_F(Opendir, open_exec)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino, O_EXEC,
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, open);
+ }));
+
+ fd = open(FULLPATH, O_EXEC | O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ leak(fd);
+}
+
+TEST_F(Opendir, opendir)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino, O_RDONLY,
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, open);
+ }));
+
+ errno = 0;
+ EXPECT_NE(nullptr, opendir(FULLPATH)) << strerror(errno);
+}
+
+/*
+ * If a fuse server returns ENOSYS to a
+ * FUSE_OPENDIR, then it and subsequent FUSE_OPENDIR and FUSE_RELEASEDIR
+ * operations will also succeed automatically without being sent to the server.
+ */
+TEST_F(Opendir, enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ FuseTest::expect_lookup(RELPATH, ino, S_IFDIR | 0755, 0, 2);
+ expect_opendir(ino, O_RDONLY, ReturnErrno(ENOSYS));
+
+ fd = open(FULLPATH, O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ close(fd);
+
+ fd = open(FULLPATH, O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ leak(fd);
+}
diff --git a/tests/sys/fs/fusefs/pre-init.cc b/tests/sys/fs/fusefs/pre-init.cc
new file mode 100644
index 000000000000..2d3257500304
--- /dev/null
+++ b/tests/sys/fs/fusefs/pre-init.cc
@@ -0,0 +1,226 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 ConnectWise
+ *
+ * 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/param.h>
+#include <sys/mount.h>
+#include <sys/signal.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <pthread.h>
+#include <semaphore.h>
+#include <signal.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+/* Tests for behavior that happens before the server responds to FUSE_INIT */
+class PreInit: public FuseTest {
+public:
+void SetUp() {
+ m_no_auto_init = true;
+ FuseTest::SetUp();
+}
+};
+
+/*
+ * Tests for behavior that happens before the server responds to FUSE_INIT,
+ * parameterized on default_permissions
+ */
+class PreInitP: public PreInit,
+ public WithParamInterface<bool>
+{
+void SetUp() {
+ m_default_permissions = GetParam();
+ PreInit::SetUp();
+}
+};
+
+static void* unmount1(void* arg __unused) {
+ ssize_t r;
+
+ r = unmount("mountpoint", 0);
+ if (r >= 0)
+ return 0;
+ else
+ return (void*)(intptr_t)errno;
+}
+
+/*
+ * Attempting to unmount the file system before it fully initializes should
+ * work fine. The unmount will complete after initialization does.
+ */
+TEST_F(PreInit, unmount_before_init)
+{
+ sem_t sem0;
+ pthread_t th1;
+
+ ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_INIT);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
+ SET_OUT_HEADER_LEN(out, init);
+ out.body.init.major = FUSE_KERNEL_VERSION;
+ out.body.init.minor = FUSE_KERNEL_MINOR_VERSION;
+ out.body.init.flags = in.body.init.flags & m_init_flags;
+ out.body.init.max_write = m_maxwrite;
+ out.body.init.max_readahead = m_maxreadahead;
+ out.body.init.time_gran = m_time_gran;
+ sem_wait(&sem0);
+ })));
+ expect_destroy(0);
+
+ ASSERT_EQ(0, pthread_create(&th1, NULL, unmount1, NULL));
+ nap(); /* Wait for th1 to block in unmount() */
+ sem_post(&sem0);
+ /* The daemon will quit after receiving FUSE_DESTROY */
+ m_mock->join_daemon();
+}
+
+/*
+ * Don't panic in this very specific scenario:
+ *
+ * The server does not respond to FUSE_INIT in timely fashion.
+ * Some other process tries to do unmount.
+ * That other process gets killed by a signal.
+ * The server finally responds to FUSE_INIT.
+ *
+ * Regression test for bug 287438
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=287438
+ */
+TEST_F(PreInit, signal_during_unmount_before_init)
+{
+ sem_t sem0;
+ pid_t child;
+
+ ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_INIT);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
+ SET_OUT_HEADER_LEN(out, init);
+ out.body.init.major = FUSE_KERNEL_VERSION;
+ /*
+ * Use protocol 7.19, like libfuse2 does. The server must use
+ * protocol 7.27 or older to trigger the bug.
+ */
+ out.body.init.minor = 19;
+ out.body.init.flags = in.body.init.flags & m_init_flags;
+ out.body.init.max_write = m_maxwrite;
+ out.body.init.max_readahead = m_maxreadahead;
+ out.body.init.time_gran = m_time_gran;
+ sem_wait(&sem0);
+ })));
+
+ if ((child = ::fork()) == 0) {
+ /*
+ * In child. This will block waiting for FUSE_INIT to complete
+ * or the receipt of an asynchronous signal.
+ */
+ (void) unmount("mountpoint", 0);
+ _exit(0); /* Unreachable, unless parent dies after fork */
+ } else if (child > 0) {
+ /* In parent. Wait for child process to start, then kill it */
+ nap();
+ kill(child, SIGINT);
+ waitpid(child, NULL, WEXITED);
+ } else {
+ FAIL() << strerror(errno);
+ }
+ m_mock->m_quit = true; /* Since we are by now unmounted. */
+ sem_post(&sem0);
+ m_mock->join_daemon();
+}
+
+/*
+ * If some process attempts VOP_GETATTR for the mountpoint before init is
+ * complete, fusefs should wait, just like it does for other VOPs.
+ *
+ * To verify that fuse_vnop_getattr does indeed wait for FUSE_INIT to complete,
+ * invoke the test like this:
+ *
+> sudo cpuset -c -l 0 dtrace -i 'fbt:fusefs:fuse_internal_init_callback:' -i 'fbt:fusefs:fuse_vnop_getattr:' -c "./pre-init --gtest_filter=PI/PreInitP.getattr_before_init/0"
+...
+dtrace: pid 4224 has exited
+CPU ID FUNCTION:NAME
+ 0 68670 fuse_vnop_getattr:entry
+ 0 68893 fuse_internal_init_callback:entry
+ 0 68894 fuse_internal_init_callback:return
+ 0 68671 fuse_vnop_getattr:return
+ *
+ * Note that fuse_vnop_getattr was entered first, but exitted last.
+ */
+TEST_P(PreInitP, getattr_before_init)
+{
+ struct stat sb;
+ nlink_t nlink = 12345;
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_INIT);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
+ SET_OUT_HEADER_LEN(out, init);
+ out.body.init.major = FUSE_KERNEL_VERSION;
+ out.body.init.minor = FUSE_KERNEL_MINOR_VERSION;
+ out.body.init.flags = in.body.init.flags & m_init_flags;
+ out.body.init.max_write = m_maxwrite;
+ out.body.init.max_readahead = m_maxreadahead;
+ out.body.init.time_gran = m_time_gran;
+ nap(); /* Allow stat() to run first */
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == FUSE_ROOT_ID);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto& in, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = in.header.nodeid;
+ out.body.attr.attr.mode = S_IFDIR | 0644;
+ out.body.attr.attr.nlink = nlink;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+
+ EXPECT_EQ(0, stat("mountpoint", &sb));
+ EXPECT_EQ(nlink, sb.st_nlink);
+}
+
+INSTANTIATE_TEST_SUITE_P(PI, PreInitP, Bool());
diff --git a/tests/sys/fs/fusefs/read.cc b/tests/sys/fs/fusefs/read.cc
new file mode 100644
index 000000000000..e9c79ba2ffda
--- /dev/null
+++ b/tests/sys/fs/fusefs/read.cc
@@ -0,0 +1,1497 @@
+/*-
+ * 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/param.h>
+#include <sys/mman.h>
+#include <sys/socket.h>
+#include <sys/sysctl.h>
+#include <sys/uio.h>
+
+#include <aio.h>
+#include <fcntl.h>
+#include <semaphore.h>
+#include <setjmp.h>
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Read: public FuseTest {
+
+public:
+void expect_lookup(const char *relpath, uint64_t ino, uint64_t size)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, size, 1);
+}
+};
+
+class RofsRead: public Read {
+public:
+virtual void SetUp() {
+ m_ro = true;
+ Read::SetUp();
+}
+};
+
+class Read_7_8: public FuseTest {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 8;
+ FuseTest::SetUp();
+}
+
+void expect_lookup(const char *relpath, uint64_t ino, uint64_t size)
+{
+ FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, size, 1);
+}
+};
+
+class AioRead: public Read {
+public:
+virtual void SetUp() {
+ if (!is_unsafe_aio_enabled())
+ GTEST_SKIP() <<
+ "vfs.aio.enable_unsafe must be set for this test";
+ FuseTest::SetUp();
+}
+};
+
+class AsyncRead: public AioRead {
+ virtual void SetUp() {
+ m_init_flags = FUSE_ASYNC_READ;
+ AioRead::SetUp();
+ }
+};
+
+class ReadAhead: public Read,
+ public WithParamInterface<tuple<bool, int>>
+{
+ virtual void SetUp() {
+ int val;
+ const char *node = "vfs.maxbcachebuf";
+ size_t size = sizeof(val);
+ ASSERT_EQ(0, sysctlbyname(node, &val, &size, NULL, 0))
+ << strerror(errno);
+
+ m_maxreadahead = val * get<1>(GetParam());
+ m_noclusterr = get<0>(GetParam());
+ Read::SetUp();
+ }
+};
+
+class ReadMaxRead: public Read {
+ virtual void SetUp() {
+ m_maxread = 16384;
+ Read::SetUp();
+ }
+};
+
+class ReadNoatime: public Read {
+ virtual void SetUp() {
+ m_noatime = true;
+ Read::SetUp();
+ }
+};
+
+class ReadSigbus: public Read
+{
+public:
+static jmp_buf s_jmpbuf;
+static void *s_si_addr;
+
+void TearDown() {
+ struct sigaction sa;
+
+ bzero(&sa, sizeof(sa));
+ sa.sa_handler = SIG_DFL;
+ sigaction(SIGBUS, &sa, NULL);
+
+ FuseTest::TearDown();
+}
+
+};
+
+static void
+handle_sigbus(int signo __unused, siginfo_t *info, void *uap __unused) {
+ ReadSigbus::s_si_addr = info->si_addr;
+ longjmp(ReadSigbus::s_jmpbuf, 1);
+}
+
+jmp_buf ReadSigbus::s_jmpbuf;
+void *ReadSigbus::s_si_addr;
+
+class TimeGran: public Read, public WithParamInterface<unsigned> {
+public:
+virtual void SetUp() {
+ m_time_gran = 1 << GetParam();
+ Read::SetUp();
+}
+};
+
+/* AIO reads need to set the header's pid field correctly */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */
+TEST_F(AioRead, aio_read)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+ struct aiocb iocb, *piocb;
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ iocb.aio_nbytes = bufsize;
+ iocb.aio_fildes = fd;
+ iocb.aio_buf = buf;
+ iocb.aio_offset = 0;
+ iocb.aio_sigevent.sigev_notify = SIGEV_NONE;
+ ASSERT_EQ(0, aio_read(&iocb)) << strerror(errno);
+ ASSERT_EQ(bufsize, aio_waitcomplete(&piocb, NULL)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
+
+ leak(fd);
+}
+
+/*
+ * Without the FUSE_ASYNC_READ mount option, fuse(4) should ensure that there
+ * is at most one outstanding read operation per file handle
+ */
+TEST_F(AioRead, async_read_disabled)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = 50;
+ char buf0[bufsize], buf1[bufsize];
+ off_t off0 = 0;
+ off_t off1 = m_maxbcachebuf;
+ struct aiocb iocb0, iocb1;
+ volatile sig_atomic_t read_count = 0;
+
+ expect_lookup(RELPATH, ino, 131072);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ &&
+ in.header.nodeid == ino &&
+ in.body.read.fh == FH &&
+ in.body.read.offset == (uint64_t)off0);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke([&](auto in __unused, auto &out __unused) {
+ read_count++;
+ /* Filesystem is slow to respond */
+ }));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ &&
+ in.header.nodeid == ino &&
+ in.body.read.fh == FH &&
+ in.body.read.offset == (uint64_t)off1);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke([&](auto in __unused, auto &out __unused) {
+ read_count++;
+ /* Filesystem is slow to respond */
+ }));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /*
+ * Submit two AIO read requests, and respond to neither. If the
+ * filesystem ever gets the second read request, then we failed to
+ * limit outstanding reads.
+ */
+ iocb0.aio_nbytes = bufsize;
+ iocb0.aio_fildes = fd;
+ iocb0.aio_buf = buf0;
+ iocb0.aio_offset = off0;
+ iocb0.aio_sigevent.sigev_notify = SIGEV_NONE;
+ ASSERT_EQ(0, aio_read(&iocb0)) << strerror(errno);
+
+ iocb1.aio_nbytes = bufsize;
+ iocb1.aio_fildes = fd;
+ iocb1.aio_buf = buf1;
+ iocb1.aio_offset = off1;
+ iocb1.aio_sigevent.sigev_notify = SIGEV_NONE;
+ ASSERT_EQ(0, aio_read(&iocb1)) << strerror(errno);
+
+ /*
+ * Sleep for awhile to make sure the kernel has had a chance to issue
+ * the second read, even though the first has not yet returned
+ */
+ nap();
+ EXPECT_EQ(read_count, 1);
+
+ m_mock->kill_daemon();
+ /* Wait for AIO activity to complete, but ignore errors */
+ (void)aio_waitcomplete(NULL, NULL);
+
+ leak(fd);
+}
+
+/*
+ * With the FUSE_ASYNC_READ mount option, fuse(4) may issue multiple
+ * simultaneous read requests on the same file handle.
+ */
+TEST_F(AsyncRead, async_read)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = 50;
+ char buf0[bufsize], buf1[bufsize];
+ off_t off0 = 0;
+ off_t off1 = m_maxbcachebuf;
+ off_t fsize = 2 * m_maxbcachebuf;
+ struct aiocb iocb0, iocb1;
+ sem_t sem;
+
+ ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
+
+ expect_lookup(RELPATH, ino, fsize);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ &&
+ in.header.nodeid == ino &&
+ in.body.read.fh == FH &&
+ in.body.read.offset == (uint64_t)off0);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([&](auto in __unused, auto &out __unused) {
+ sem_post(&sem);
+ /* Filesystem is slow to respond */
+ }));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ &&
+ in.header.nodeid == ino &&
+ in.body.read.fh == FH &&
+ in.body.read.offset == (uint64_t)off1);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([&](auto in __unused, auto &out __unused) {
+ sem_post(&sem);
+ /* Filesystem is slow to respond */
+ }));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /*
+ * Submit two AIO read requests, but respond to neither. Ensure that
+ * we received both.
+ */
+ iocb0.aio_nbytes = bufsize;
+ iocb0.aio_fildes = fd;
+ iocb0.aio_buf = buf0;
+ iocb0.aio_offset = off0;
+ iocb0.aio_sigevent.sigev_notify = SIGEV_NONE;
+ ASSERT_EQ(0, aio_read(&iocb0)) << strerror(errno);
+
+ iocb1.aio_nbytes = bufsize;
+ iocb1.aio_fildes = fd;
+ iocb1.aio_buf = buf1;
+ iocb1.aio_offset = off1;
+ iocb1.aio_sigevent.sigev_notify = SIGEV_NONE;
+ ASSERT_EQ(0, aio_read(&iocb1)) << strerror(errno);
+
+ /* Wait until both reads have reached the daemon */
+ ASSERT_EQ(0, sem_wait(&sem)) << strerror(errno);
+ ASSERT_EQ(0, sem_wait(&sem)) << strerror(errno);
+
+ m_mock->kill_daemon();
+ /* Wait for AIO activity to complete, but ignore errors */
+ (void)aio_waitcomplete(NULL, NULL);
+
+ leak(fd);
+}
+
+/* The kernel should update the cached atime attribute during a read */
+TEST_F(Read, atime)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ struct stat sb1, sb2;
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb1));
+
+ /* Ensure atime will be different than it was during lookup */
+ nap();
+
+ ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb2));
+
+ /* The kernel should automatically update atime during read */
+ EXPECT_TRUE(timespeccmp(&sb1.st_atim, &sb2.st_atim, <));
+ EXPECT_TRUE(timespeccmp(&sb1.st_ctim, &sb2.st_ctim, ==));
+ EXPECT_TRUE(timespeccmp(&sb1.st_mtim, &sb2.st_mtim, ==));
+
+ leak(fd);
+}
+
+/* The kernel should update the cached atime attribute during a cached read */
+TEST_F(Read, atime_cached)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ struct stat sb1, sb2;
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, pread(fd, buf, bufsize, 0)) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb1));
+
+ /* Ensure atime will be different than it was during the first read */
+ nap();
+
+ ASSERT_EQ(bufsize, pread(fd, buf, bufsize, 0)) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb2));
+
+ /* The kernel should automatically update atime during read */
+ EXPECT_TRUE(timespeccmp(&sb1.st_atim, &sb2.st_atim, <));
+ EXPECT_TRUE(timespeccmp(&sb1.st_ctim, &sb2.st_ctim, ==));
+ EXPECT_TRUE(timespeccmp(&sb1.st_mtim, &sb2.st_mtim, ==));
+
+ leak(fd);
+}
+
+/* dirty atime values should be flushed during close */
+TEST_F(Read, atime_during_close)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ struct stat sb;
+ uint64_t ino = 42;
+ const mode_t newmode = 0755;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ uint32_t valid = FATTR_ATIME;
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.valid == valid &&
+ (time_t)in.body.setattr.atime ==
+ sb.st_atim.tv_sec &&
+ (long)in.body.setattr.atimensec ==
+ sb.st_atim.tv_nsec);
+ }, 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;
+ })));
+ 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(bufsize, read(fd, buf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb));
+
+ close(fd);
+}
+
+/*
+ * When not using -o default_permissions, the daemon may make its own decisions
+ * regarding access permissions, and these may be unpredictable. If it rejects
+ * our attempt to set atime, that should not cause close(2) to fail.
+ */
+TEST_F(Read, atime_during_close_eacces)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ uint32_t valid = FATTR_ATIME;
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.valid == valid);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(EACCES)));
+ 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(bufsize, read(fd, buf, bufsize)) << strerror(errno);
+
+ ASSERT_EQ(0, close(fd));
+}
+
+/* A cached atime should be flushed during FUSE_SETATTR */
+TEST_F(Read, atime_during_setattr)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ struct stat sb;
+ uint64_t ino = 42;
+ const mode_t newmode = 0755;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ uint32_t valid = FATTR_MODE | FATTR_ATIME;
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.valid == valid &&
+ (time_t)in.body.setattr.atime ==
+ sb.st_atim.tv_sec &&
+ (long)in.body.setattr.atimensec ==
+ sb.st_atim.tv_nsec);
+ }, 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;
+ })));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /* Ensure atime will be different than during lookup */
+ nap();
+
+ ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb));
+ ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno);
+
+ leak(fd);
+}
+
+/* 0-length reads shouldn't cause any confusion */
+TEST_F(Read, direct_io_read_nothing)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+ uint64_t offset = 100;
+ char buf[80];
+
+ expect_lookup(RELPATH, ino, offset + 1000);
+ expect_open(ino, FOPEN_DIRECT_IO, 1);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(0, pread(fd, buf, 0, offset)) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * With direct_io, reads should not fill the cache. They should go straight to
+ * the daemon
+ */
+TEST_F(Read, direct_io_pread)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ uint64_t offset = 100;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ expect_lookup(RELPATH, ino, offset + bufsize);
+ expect_open(ino, FOPEN_DIRECT_IO, 1);
+ expect_read(ino, offset, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, pread(fd, buf, bufsize, offset)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
+
+ // With FOPEN_DIRECT_IO, the cache should be bypassed. The server will
+ // get a 2nd read request.
+ expect_read(ino, offset, bufsize, bufsize, CONTENTS);
+ ASSERT_EQ(bufsize, pread(fd, buf, bufsize, offset)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
+ leak(fd);
+}
+
+/*
+ * With direct_io, filesystems are allowed to return less data than is
+ * requested. fuse(4) should return a short read to userland.
+ */
+TEST_F(Read, direct_io_short_read)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefghijklmnop";
+ uint64_t ino = 42;
+ int fd;
+ uint64_t offset = 100;
+ ssize_t bufsize = strlen(CONTENTS);
+ ssize_t halfbufsize = bufsize / 2;
+ uint8_t buf[bufsize];
+
+ expect_lookup(RELPATH, ino, offset + bufsize);
+ expect_open(ino, FOPEN_DIRECT_IO, 1);
+ expect_read(ino, offset, bufsize, halfbufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(halfbufsize, pread(fd, buf, bufsize, offset))
+ << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, CONTENTS, halfbufsize));
+ leak(fd);
+}
+
+TEST_F(Read, eio)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(EIO)));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(-1, read(fd, buf, bufsize)) << strerror(errno);
+ ASSERT_EQ(EIO, errno);
+ leak(fd);
+}
+
+/*
+ * If the server returns a short read when direct io is not in use, that
+ * indicates EOF, because of a server-side truncation. We should invalidate
+ * all cached attributes. We may update the file size,
+ */
+TEST_F(Read, eof)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefghijklmnop";
+ uint64_t ino = 42;
+ int fd;
+ uint64_t offset = 100;
+ ssize_t bufsize = strlen(CONTENTS);
+ ssize_t partbufsize = 3 * bufsize / 4;
+ ssize_t r;
+ uint8_t buf[bufsize];
+ struct stat sb;
+
+ expect_lookup(RELPATH, ino, offset + bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, offset + bufsize, offset + partbufsize, CONTENTS);
+ expect_getattr(ino, offset + partbufsize);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ r = pread(fd, buf, bufsize, offset);
+ ASSERT_LE(0, r) << strerror(errno);
+ EXPECT_EQ(partbufsize, r) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb));
+ EXPECT_EQ((off_t)(offset + partbufsize), sb.st_size);
+ leak(fd);
+}
+
+/* Like Read.eof, but causes an entire buffer to be invalidated */
+TEST_F(Read, eof_of_whole_buffer)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefghijklmnop";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ off_t old_filesize = m_maxbcachebuf * 2 + bufsize;
+ uint8_t buf[bufsize];
+ struct stat sb;
+
+ expect_lookup(RELPATH, ino, old_filesize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 2 * m_maxbcachebuf, bufsize, bufsize, CONTENTS);
+ expect_read(ino, m_maxbcachebuf, m_maxbcachebuf, 0, CONTENTS);
+ expect_getattr(ino, m_maxbcachebuf);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /* Cache the third block */
+ ASSERT_EQ(bufsize, pread(fd, buf, bufsize, m_maxbcachebuf * 2))
+ << strerror(errno);
+ /* Try to read the 2nd block, but it's past EOF */
+ ASSERT_EQ(0, pread(fd, buf, bufsize, m_maxbcachebuf))
+ << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb));
+ EXPECT_EQ((off_t)(m_maxbcachebuf), sb.st_size);
+ leak(fd);
+}
+
+/*
+ * With the keep_cache option, the kernel may keep its read cache across
+ * multiple open(2)s.
+ */
+TEST_F(Read, keep_cache)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd0, fd1;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, bufsize, 2);
+ expect_open(ino, FOPEN_KEEP_CACHE, 2);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd0 = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd0) << strerror(errno);
+ ASSERT_EQ(bufsize, read(fd0, buf, bufsize)) << strerror(errno);
+
+ fd1 = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd1) << strerror(errno);
+
+ /*
+ * This read should be serviced by cache, even though it's on the other
+ * file descriptor
+ */
+ ASSERT_EQ(bufsize, read(fd1, buf, bufsize)) << strerror(errno);
+
+ leak(fd0);
+ leak(fd1);
+}
+
+/*
+ * Without the keep_cache option, the kernel should drop its read caches on
+ * every open
+ */
+TEST_F(Read, keep_cache_disabled)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd0, fd1;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, bufsize, 2);
+ expect_open(ino, 0, 2);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd0 = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd0) << strerror(errno);
+ ASSERT_EQ(bufsize, read(fd0, buf, bufsize)) << strerror(errno);
+
+ fd1 = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd1) << strerror(errno);
+
+ /*
+ * This read should not be serviced by cache, even though it's on the
+ * original file descriptor
+ */
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+ ASSERT_EQ(0, lseek(fd0, 0, SEEK_SET)) << strerror(errno);
+ ASSERT_EQ(bufsize, read(fd0, buf, bufsize)) << strerror(errno);
+
+ leak(fd0);
+ leak(fd1);
+}
+
+TEST_F(Read, mmap)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t len;
+ size_t bufsize = strlen(CONTENTS);
+ void *p;
+
+ len = getpagesize();
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ &&
+ in.header.nodeid == ino &&
+ in.body.read.fh == Read::FH &&
+ in.body.read.offset == 0 &&
+ in.body.read.size == bufsize);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ out.header.len = sizeof(struct fuse_out_header) + bufsize;
+ memmove(out.body.bytes, CONTENTS, bufsize);
+ })));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0);
+ ASSERT_NE(MAP_FAILED, p) << strerror(errno);
+
+ ASSERT_EQ(0, memcmp(p, CONTENTS, bufsize));
+
+ ASSERT_EQ(0, munmap(p, len)) << strerror(errno);
+ leak(fd);
+}
+
+
+/* When max_read is set, large reads will be split up as necessary */
+TEST_F(ReadMaxRead, split)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = 65536;
+ ssize_t fragsize = bufsize / 4;
+ char *rbuf, *frag0, *frag1, *frag2, *frag3;
+
+ rbuf = new char[bufsize]();
+ frag0 = new char[fragsize]();
+ frag1 = new char[fragsize]();
+ frag2 = new char[fragsize]();
+ frag3 = new char[fragsize]();
+ memset(frag0, '0', fragsize);
+ memset(frag1, '1', fragsize);
+ memset(frag2, '2', fragsize);
+ memset(frag3, '3', fragsize);
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, fragsize, fragsize, frag0);
+ expect_read(ino, fragsize, fragsize, fragsize, frag1);
+ expect_read(ino, 2 * fragsize, fragsize, fragsize, frag2);
+ expect_read(ino, 3 * fragsize, fragsize, fragsize, frag3);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, read(fd, rbuf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(rbuf, frag0, fragsize));
+ ASSERT_EQ(0, memcmp(rbuf + fragsize, frag1, fragsize));
+ ASSERT_EQ(0, memcmp(rbuf + 2 * fragsize, frag2, fragsize));
+ ASSERT_EQ(0, memcmp(rbuf + 3 * fragsize, frag3, fragsize));
+
+ delete[] frag3;
+ delete[] frag2;
+ delete[] frag1;
+ delete[] frag0;
+ delete[] rbuf;
+ leak(fd);
+}
+
+/*
+ * The kernel should not update the cached atime attribute during a read, if
+ * MNT_NOATIME is used.
+ */
+TEST_F(ReadNoatime, atime)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ struct stat sb1, sb2;
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb1));
+
+ nap();
+
+ ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb2));
+
+ /* The kernel should not update atime during read */
+ EXPECT_TRUE(timespeccmp(&sb1.st_atim, &sb2.st_atim, ==));
+ EXPECT_TRUE(timespeccmp(&sb1.st_ctim, &sb2.st_ctim, ==));
+ EXPECT_TRUE(timespeccmp(&sb1.st_mtim, &sb2.st_mtim, ==));
+
+ leak(fd);
+}
+
+/*
+ * The kernel should not update the cached atime attribute during a cached
+ * read, if MNT_NOATIME is used.
+ */
+TEST_F(ReadNoatime, atime_cached)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ struct stat sb1, sb2;
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, pread(fd, buf, bufsize, 0)) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb1));
+
+ nap();
+
+ ASSERT_EQ(bufsize, pread(fd, buf, bufsize, 0)) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb2));
+
+ /* The kernel should automatically update atime during read */
+ EXPECT_TRUE(timespeccmp(&sb1.st_atim, &sb2.st_atim, ==));
+ EXPECT_TRUE(timespeccmp(&sb1.st_ctim, &sb2.st_ctim, ==));
+ EXPECT_TRUE(timespeccmp(&sb1.st_mtim, &sb2.st_mtim, ==));
+
+ leak(fd);
+}
+
+/* Read of an mmap()ed file fails */
+TEST_F(ReadSigbus, mmap_eio)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ struct sigaction sa;
+ uint64_t ino = 42;
+ int fd;
+ ssize_t len;
+ size_t bufsize = strlen(CONTENTS);
+ void *p;
+
+ len = getpagesize();
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ &&
+ in.header.nodeid == ino &&
+ in.body.read.fh == Read::FH);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnErrno(EIO)));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0);
+ ASSERT_NE(MAP_FAILED, p) << strerror(errno);
+
+ /* Accessing the mapped page should return SIGBUS. */
+
+ bzero(&sa, sizeof(sa));
+ sa.sa_handler = SIG_DFL;
+ sa.sa_sigaction = handle_sigbus;
+ sa.sa_flags = SA_RESETHAND | SA_SIGINFO;
+ ASSERT_EQ(0, sigaction(SIGBUS, &sa, NULL)) << strerror(errno);
+ if (setjmp(ReadSigbus::s_jmpbuf) == 0) {
+ atomic_signal_fence(std::memory_order::memory_order_seq_cst);
+ volatile char x __unused = *(volatile char*)p;
+ FAIL() << "shouldn't get here";
+ }
+
+ ASSERT_EQ(p, ReadSigbus::s_si_addr);
+ ASSERT_EQ(0, munmap(p, len)) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * A read via mmap comes up short, indicating that the file was truncated
+ * server-side.
+ */
+TEST_F(Read, mmap_eof)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t len;
+ size_t bufsize = strlen(CONTENTS);
+ struct stat sb;
+ void *p;
+
+ len = getpagesize();
+
+ expect_lookup(RELPATH, ino, m_maxbcachebuf);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ &&
+ in.header.nodeid == ino &&
+ in.body.read.fh == Read::FH &&
+ in.body.read.offset == 0 &&
+ in.body.read.size == (uint32_t)m_maxbcachebuf);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ out.header.len = sizeof(struct fuse_out_header) + bufsize;
+ memmove(out.body.bytes, CONTENTS, bufsize);
+ })));
+ expect_getattr(ino, bufsize);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0);
+ ASSERT_NE(MAP_FAILED, p) << strerror(errno);
+
+ /* The file size should be automatically truncated */
+ ASSERT_EQ(0, memcmp(p, CONTENTS, bufsize));
+ ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
+ EXPECT_EQ((off_t)bufsize, sb.st_size);
+
+ ASSERT_EQ(0, munmap(p, len)) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * During VOP_GETPAGES, the FUSE server fails a FUSE_GETATTR operation. This
+ * almost certainly indicates a buggy FUSE server, and our goal should be not
+ * to panic. Instead, generate SIGBUS.
+ */
+TEST_F(ReadSigbus, mmap_getblksz_fail)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ struct sigaction sa;
+ Sequence seq;
+ uint64_t ino = 42;
+ int fd;
+ ssize_t len;
+ size_t bufsize = strlen(CONTENTS);
+ mode_t mode = S_IFREG | 0644;
+ void *p;
+
+ len = getpagesize();
+
+ FuseTest::expect_lookup(RELPATH, ino, mode, bufsize, 1, 0);
+ /* Expect two GETATTR calls that succeed, followed by one that fail. */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(2)
+ .InSequence(seq)
+ .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 = bufsize;
+ out.body.attr.attr_valid = 0;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillRepeatedly(Invoke(ReturnErrno(EIO)));
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0);
+ ASSERT_NE(MAP_FAILED, p) << strerror(errno);
+
+ /* Accessing the mapped page should return SIGBUS. */
+ bzero(&sa, sizeof(sa));
+ sa.sa_handler = SIG_DFL;
+ sa.sa_sigaction = handle_sigbus;
+ sa.sa_flags = SA_RESETHAND | SA_SIGINFO;
+ ASSERT_EQ(0, sigaction(SIGBUS, &sa, NULL)) << strerror(errno);
+ if (setjmp(ReadSigbus::s_jmpbuf) == 0) {
+ atomic_signal_fence(std::memory_order::memory_order_seq_cst);
+ volatile char x __unused = *(volatile char*)p;
+ FAIL() << "shouldn't get here";
+ }
+
+ ASSERT_EQ(p, ReadSigbus::s_si_addr);
+ ASSERT_EQ(0, munmap(p, len)) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * Just as when FOPEN_DIRECT_IO is used, reads with O_DIRECT should bypass
+ * cache and to straight to the daemon
+ */
+TEST_F(Read, o_direct)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ // Fill the cache
+ ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
+
+ // Reads with o_direct should bypass the cache
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+ ASSERT_EQ(0, fcntl(fd, F_SETFL, O_DIRECT)) << strerror(errno);
+ ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno);
+ ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
+
+ leak(fd);
+}
+
+TEST_F(Read, pread)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ /*
+ * Set offset to a maxbcachebuf boundary so we'll be sure what offset
+ * to read from. Without this, the read might start at a lower offset.
+ */
+ uint64_t offset = m_maxbcachebuf;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ expect_lookup(RELPATH, ino, offset + bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, offset, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, pread(fd, buf, bufsize, offset)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
+ leak(fd);
+}
+
+TEST_F(Read, read)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
+
+ leak(fd);
+}
+
+TEST_F(Read_7_8, read)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
+
+ leak(fd);
+}
+
+/*
+ * If cacheing is enabled, the kernel should try to read an entire cache block
+ * at a time.
+ */
+TEST_F(Read, cache_block)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS0 = "abcdefghijklmnop";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = 8;
+ ssize_t filesize = m_maxbcachebuf * 2;
+ char *contents;
+ char buf[bufsize];
+ const char *contents1 = CONTENTS0 + bufsize;
+
+ contents = new char[filesize]();
+ memmove(contents, CONTENTS0, strlen(CONTENTS0));
+
+ expect_lookup(RELPATH, ino, filesize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, m_maxbcachebuf, m_maxbcachebuf,
+ contents);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, CONTENTS0, bufsize));
+
+ /* A subsequent read should be serviced by cache */
+ ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, contents1, bufsize));
+ leak(fd);
+ delete[] contents;
+}
+
+/* Reading with sendfile should work (though it obviously won't be 0-copy) */
+TEST_F(Read, sendfile)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ size_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+ int sp[2];
+ off_t sbytes;
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ &&
+ in.header.nodeid == ino &&
+ in.body.read.fh == Read::FH &&
+ in.body.read.offset == 0 &&
+ in.body.read.size == bufsize);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ out.header.len = sizeof(struct fuse_out_header) + bufsize;
+ memmove(out.body.bytes, CONTENTS, bufsize);
+ })));
+
+ ASSERT_EQ(0, socketpair(PF_LOCAL, SOCK_STREAM, 0, sp))
+ << strerror(errno);
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(0, sendfile(fd, sp[1], 0, bufsize, NULL, &sbytes, 0))
+ << strerror(errno);
+ ASSERT_EQ(static_cast<ssize_t>(bufsize), read(sp[0], buf, bufsize))
+ << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
+
+ close(sp[1]);
+ close(sp[0]);
+ leak(fd);
+}
+
+/* sendfile should fail gracefully if fuse declines the read */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236466 */
+TEST_F(Read, sendfile_eio)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ int sp[2];
+ off_t sbytes;
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(EIO)));
+
+ ASSERT_EQ(0, socketpair(PF_LOCAL, SOCK_STREAM, 0, sp))
+ << strerror(errno);
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_NE(0, sendfile(fd, sp[1], 0, bufsize, NULL, &sbytes, 0));
+
+ close(sp[1]);
+ close(sp[0]);
+ leak(fd);
+}
+
+/*
+ * Sequential reads should use readahead. And if allowed, large reads should
+ * be clustered.
+ */
+TEST_P(ReadAhead, readahead) {
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd, maxcontig, clustersize;
+ ssize_t bufsize = 4 * m_maxbcachebuf;
+ ssize_t filesize = bufsize;
+ uint64_t len;
+ char *rbuf, *contents;
+ off_t offs;
+
+ contents = new char[filesize];
+ memset(contents, 'X', filesize);
+ rbuf = new char[bufsize]();
+
+ expect_lookup(RELPATH, ino, filesize);
+ expect_open(ino, 0, 1);
+ maxcontig = m_noclusterr ? m_maxbcachebuf :
+ m_maxbcachebuf + m_maxreadahead;
+ clustersize = MIN((unsigned long )maxcontig, m_maxphys);
+ for (offs = 0; offs < bufsize; offs += clustersize) {
+ len = std::min((size_t)clustersize, (size_t)(filesize - offs));
+ expect_read(ino, offs, len, len, contents + offs);
+ }
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /* Set the internal readahead counter to a "large" value */
+ ASSERT_EQ(0, fcntl(fd, F_READAHEAD, 1'000'000'000)) << strerror(errno);
+
+ ASSERT_EQ(bufsize, read(fd, rbuf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(rbuf, contents, bufsize));
+
+ leak(fd);
+ delete[] rbuf;
+ delete[] contents;
+}
+
+INSTANTIATE_TEST_SUITE_P(RA, ReadAhead,
+ Values(tuple<bool, int>(false, 0),
+ tuple<bool, int>(false, 1),
+ tuple<bool, int>(false, 2),
+ tuple<bool, int>(false, 3),
+ tuple<bool, int>(true, 0),
+ tuple<bool, int>(true, 1),
+ tuple<bool, int>(true, 2)));
+
+/* With read-only mounts, fuse should never update atime during close */
+TEST_F(RofsRead, atime_during_close)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, bufsize, bufsize, 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(bufsize, read(fd, buf, bufsize)) << strerror(errno);
+
+ close(fd);
+}
+
+/* fuse_init_out.time_gran controls the granularity of timestamps */
+TEST_P(TimeGran, atime_during_setattr)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t buf[bufsize];
+ uint64_t ino = 42;
+ const mode_t newmode = 0755;
+ int fd;
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ uint32_t valid = FATTR_MODE | FATTR_ATIME;
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.valid == valid &&
+ in.body.setattr.atimensec % m_time_gran == 0);
+ }, 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;
+ })));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno);
+
+ leak(fd);
+}
+
+INSTANTIATE_TEST_SUITE_P(TG, TimeGran, Range(0u, 10u));
diff --git a/tests/sys/fs/fusefs/readdir.cc b/tests/sys/fs/fusefs/readdir.cc
new file mode 100644
index 000000000000..6b78e3a70697
--- /dev/null
+++ b/tests/sys/fs/fusefs/readdir.cc
@@ -0,0 +1,516 @@
+/*-
+ * 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 <dirent.h>
+#include <fcntl.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+using namespace std;
+
+class Readdir: public FuseTest {
+public:
+void expect_lookup(const char *relpath, uint64_t ino)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFDIR | 0755, 0, 1);
+}
+};
+
+class Readdir_7_8: public Readdir {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 8;
+ Readdir::SetUp();
+}
+
+void expect_lookup(const char *relpath, uint64_t ino)
+{
+ FuseTest::expect_lookup_7_8(relpath, ino, S_IFDIR | 0755, 0, 1);
+}
+};
+
+const char dot[] = ".";
+const char dotdot[] = "..";
+
+/* FUSE_READDIR returns nothing but "." and ".." */
+TEST_F(Readdir, dots)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ DIR *dir;
+ struct dirent *de;
+ vector<struct dirent> ents(2);
+ vector<struct dirent> empty_ents(0);
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+ ents[0].d_fileno = 2;
+ ents[0].d_off = 2000;
+ ents[0].d_namlen = sizeof(dotdot);
+ ents[0].d_type = DT_DIR;
+ strncpy(ents[0].d_name, dotdot, ents[0].d_namlen);
+ ents[1].d_fileno = 3;
+ ents[1].d_off = 3000;
+ ents[1].d_namlen = sizeof(dot);
+ ents[1].d_type = DT_DIR;
+ strncpy(ents[1].d_name, dot, ents[1].d_namlen);
+ expect_readdir(ino, 0, ents);
+ expect_readdir(ino, 3000, empty_ents);
+
+ errno = 0;
+ dir = opendir(FULLPATH);
+ ASSERT_NE(nullptr, dir) << strerror(errno);
+
+ errno = 0;
+ de = readdir(dir);
+ ASSERT_NE(nullptr, de) << strerror(errno);
+ EXPECT_EQ(2ul, de->d_fileno);
+ EXPECT_EQ(DT_DIR, de->d_type);
+ EXPECT_EQ(sizeof(dotdot), de->d_namlen);
+ EXPECT_EQ(0, strcmp(dotdot, de->d_name));
+
+ errno = 0;
+ de = readdir(dir);
+ ASSERT_NE(nullptr, de) << strerror(errno);
+ EXPECT_EQ(3ul, de->d_fileno);
+ EXPECT_EQ(DT_DIR, de->d_type);
+ EXPECT_EQ(sizeof(dot), de->d_namlen);
+ EXPECT_EQ(0, strcmp(dot, de->d_name));
+
+ ASSERT_EQ(nullptr, readdir(dir));
+ ASSERT_EQ(0, errno);
+
+ leakdir(dir);
+}
+
+TEST_F(Readdir, eio)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ DIR *dir;
+ struct dirent *de;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READDIR &&
+ in.header.nodeid == ino &&
+ in.body.readdir.offset == 0);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(EIO)));
+
+ errno = 0;
+ dir = opendir(FULLPATH);
+ ASSERT_NE(nullptr, dir) << strerror(errno);
+
+ errno = 0;
+ de = readdir(dir);
+ ASSERT_EQ(nullptr, de);
+ ASSERT_EQ(EIO, errno);
+
+ leakdir(dir);
+}
+
+/*
+ * getdirentries(2) can use a larger buffer size than readdir(3). It also has
+ * some additional non-standardized fields in the returned dirent.
+ */
+TEST_F(Readdir, getdirentries_empty)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ int fd;
+ char buf[8192];
+ ssize_t r;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READDIR &&
+ in.header.nodeid == ino &&
+ in.body.readdir.size == 8192);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ out.header.error = 0;
+ out.header.len = sizeof(out.header);
+ })));
+
+ fd = open(FULLPATH, O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ r = getdirentries(fd, buf, sizeof(buf), 0);
+ ASSERT_EQ(0, r) << strerror(errno);
+
+ leak(fd);
+}
+
+/*
+ * The dirent.d_off field can be used with lseek to position the directory so
+ * that getdirentries will return the subsequent dirent.
+ */
+TEST_F(Readdir, getdirentries_seek)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ vector<struct dirent> ents0(2);
+ vector<struct dirent> ents1(1);
+ uint64_t ino = 42;
+ int fd;
+ const size_t bufsize = 8192;
+ char buf[bufsize];
+ struct dirent *de0, *de1;
+ ssize_t r;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+
+ ents0[0].d_fileno = 2;
+ ents0[0].d_off = 2000;
+ ents0[0].d_namlen = sizeof(dotdot);
+ ents0[0].d_type = DT_DIR;
+ strncpy(ents0[0].d_name, dotdot, ents0[0].d_namlen);
+ expect_readdir(ino, 0, ents0);
+ ents0[1].d_fileno = 3;
+ ents0[1].d_off = 3000;
+ ents0[1].d_namlen = sizeof(dot);
+ ents0[1].d_type = DT_DIR;
+ ents1[0].d_fileno = 3;
+ ents1[0].d_off = 3000;
+ ents1[0].d_namlen = sizeof(dot);
+ ents1[0].d_type = DT_DIR;
+ strncpy(ents1[0].d_name, dot, ents1[0].d_namlen);
+ expect_readdir(ino, 0, ents0);
+ expect_readdir(ino, 2000, ents1);
+
+ fd = open(FULLPATH, O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ r = getdirentries(fd, buf, sizeof(buf), 0);
+ ASSERT_LT(0, r) << strerror(errno);
+ de0 = (struct dirent*)&buf[0];
+ ASSERT_EQ(2000, de0->d_off);
+ ASSERT_LT(de0->d_reclen + offsetof(struct dirent, d_fileno), bufsize);
+ de1 = (struct dirent*)(&(buf[de0->d_reclen]));
+ ASSERT_EQ(3ul, de1->d_fileno);
+
+ r = lseek(fd, de0->d_off, SEEK_SET);
+ ASSERT_LE(0, r);
+ r = getdirentries(fd, buf, sizeof(buf), 0);
+ ASSERT_LT(0, r) << strerror(errno);
+ de0 = (struct dirent*)&buf[0];
+ ASSERT_EQ(3000, de0->d_off);
+}
+
+/*
+ * Nothing bad should happen if getdirentries is called on two file descriptors
+ * which were concurrently open, but one has already been closed.
+ * This is a regression test for a specific bug dating from r238402.
+ */
+TEST_F(Readdir, getdirentries_concurrent)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ int fd0, fd1;
+ char buf[8192];
+ ssize_t r;
+
+ FuseTest::expect_lookup(RELPATH, ino, S_IFDIR | 0755, 0, 2);
+ expect_opendir(ino);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READDIR &&
+ in.header.nodeid == ino &&
+ in.body.readdir.size == 8192);
+ }, Eq(true)),
+ _)
+ ).Times(2)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ out.header.error = 0;
+ out.header.len = sizeof(out.header);
+ })));
+
+ fd0 = open(FULLPATH, O_DIRECTORY);
+ ASSERT_LE(0, fd0) << strerror(errno);
+
+ fd1 = open(FULLPATH, O_DIRECTORY);
+ ASSERT_LE(0, fd1) << strerror(errno);
+
+ r = getdirentries(fd0, buf, sizeof(buf), 0);
+ ASSERT_EQ(0, r) << strerror(errno);
+
+ EXPECT_EQ(0, close(fd0)) << strerror(errno);
+
+ r = getdirentries(fd1, buf, sizeof(buf), 0);
+ ASSERT_EQ(0, r) << strerror(errno);
+
+ leak(fd0);
+ leak(fd1);
+}
+
+/*
+ * FUSE_READDIR returns nothing, not even "." and "..". This is legal, though
+ * the filesystem obviously won't be fully functional.
+ */
+TEST_F(Readdir, nodots)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ DIR *dir;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READDIR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ out.header.error = 0;
+ out.header.len = sizeof(out.header);
+ })));
+
+ errno = 0;
+ dir = opendir(FULLPATH);
+ ASSERT_NE(nullptr, dir) << strerror(errno);
+ errno = 0;
+ ASSERT_EQ(nullptr, readdir(dir));
+ ASSERT_EQ(0, errno);
+
+ leakdir(dir);
+}
+
+/*
+ * FUSE_READDIR returns a path with an embedded NUL. Obviously illegal, but
+ * nothing bad should happen.
+ */
+TEST_F(Readdir, nul)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ DIR *dir;
+ struct dirent *de;
+ vector<struct dirent> ents(1);
+ vector<struct dirent> empty_ents(0);
+ const char nul[] = "foo\0bar";
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+ ents[0].d_fileno = 4;
+ ents[0].d_off = 4000;
+ ents[0].d_namlen = sizeof(nul);
+ ents[0].d_type = DT_REG;
+ strncpy(ents[0].d_name, nul, ents[0].d_namlen);
+ expect_readdir(ino, 0, ents);
+ expect_readdir(ino, 4000, empty_ents);
+
+ errno = 0;
+ dir = opendir(FULLPATH);
+ ASSERT_NE(nullptr, dir) << strerror(errno);
+
+ errno = 0;
+ de = readdir(dir);
+ ASSERT_NE(nullptr, de) << strerror(errno);
+ EXPECT_EQ(4ul, de->d_fileno);
+ EXPECT_EQ(DT_REG, de->d_type);
+ EXPECT_EQ(sizeof(nul), de->d_namlen);
+ EXPECT_EQ(0, strcmp(nul, de->d_name));
+
+ ASSERT_EQ(nullptr, readdir(dir));
+ ASSERT_EQ(0, errno);
+
+ leakdir(dir);
+}
+
+
+/* telldir(3) and seekdir(3) should work with fuse */
+TEST_F(Readdir, seekdir)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ DIR *dir;
+ struct dirent *de;
+ /*
+ * use enough entries to be > 4096 bytes, so getdirentries must be
+ * called
+ * multiple times.
+ */
+ vector<struct dirent> ents0(122), ents1(102), ents2(30);
+ long bookmark;
+ int i = 0;
+
+ for (auto& it: ents0) {
+ snprintf(it.d_name, MAXNAMLEN, "file.%d", i);
+ it.d_fileno = 2 + i;
+ it.d_off = (2 + i) * 1000;
+ it.d_namlen = strlen(it.d_name);
+ it.d_type = DT_REG;
+ i++;
+ }
+ for (auto& it: ents1) {
+ snprintf(it.d_name, MAXNAMLEN, "file.%d", i);
+ it.d_fileno = 2 + i;
+ it.d_off = (2 + i) * 1000;
+ it.d_namlen = strlen(it.d_name);
+ it.d_type = DT_REG;
+ i++;
+ }
+ for (auto& it: ents2) {
+ snprintf(it.d_name, MAXNAMLEN, "file.%d", i);
+ it.d_fileno = 2 + i;
+ it.d_off = (2 + i) * 1000;
+ it.d_namlen = strlen(it.d_name);
+ it.d_type = DT_REG;
+ i++;
+ }
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+
+ expect_readdir(ino, 0, ents0);
+ expect_readdir(ino, 123000, ents1);
+ expect_readdir(ino, 225000, ents2);
+
+ errno = 0;
+ dir = opendir(FULLPATH);
+ ASSERT_NE(nullptr, dir) << strerror(errno);
+
+ for (i=0; i < 128; i++) {
+ errno = 0;
+ de = readdir(dir);
+ ASSERT_NE(nullptr, de) << strerror(errno);
+ EXPECT_EQ(2 + (ino_t)i, de->d_fileno);
+ }
+ bookmark = telldir(dir);
+
+ for (; i < 232; i++) {
+ errno = 0;
+ de = readdir(dir);
+ ASSERT_NE(nullptr, de) << strerror(errno);
+ EXPECT_EQ(2 + (ino_t)i, de->d_fileno);
+ }
+
+ seekdir(dir, bookmark);
+ de = readdir(dir);
+ ASSERT_NE(nullptr, de) << strerror(errno);
+ EXPECT_EQ(130ul, de->d_fileno);
+
+ leakdir(dir);
+}
+
+/*
+ * FUSE_READDIR returns a path with an embedded /. Obviously illegal, but
+ * nothing bad should happen.
+ */
+TEST_F(Readdir, slash)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ DIR *dir;
+ struct dirent *de;
+ vector<struct dirent> ents(1);
+ vector<struct dirent> empty_ents(0);
+ const char foobar[] = "foo/bar";
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+ ents[0].d_fileno = 4;
+ ents[0].d_off = 4000;
+ ents[0].d_namlen = sizeof(foobar);
+ ents[0].d_type = DT_REG;
+ strncpy(ents[0].d_name, foobar, ents[0].d_namlen);
+ expect_readdir(ino, 0, ents);
+ expect_readdir(ino, 4000, empty_ents);
+
+ errno = 0;
+ dir = opendir(FULLPATH);
+ ASSERT_NE(nullptr, dir) << strerror(errno);
+
+ errno = 0;
+ de = readdir(dir);
+ ASSERT_NE(nullptr, de) << strerror(errno);
+ EXPECT_EQ(4ul, de->d_fileno);
+ EXPECT_EQ(DT_REG, de->d_type);
+ EXPECT_EQ(sizeof(foobar), de->d_namlen);
+ EXPECT_EQ(0, strcmp(foobar, de->d_name));
+
+ ASSERT_EQ(nullptr, readdir(dir));
+ ASSERT_EQ(0, errno);
+
+ leakdir(dir);
+}
+
+TEST_F(Readdir_7_8, nodots)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ DIR *dir;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READDIR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ out.header.error = 0;
+ out.header.len = sizeof(out.header);
+ })));
+
+ errno = 0;
+ dir = opendir(FULLPATH);
+ ASSERT_NE(nullptr, dir) << strerror(errno);
+ errno = 0;
+ ASSERT_EQ(nullptr, readdir(dir));
+ ASSERT_EQ(0, errno);
+
+ leakdir(dir);
+}
diff --git a/tests/sys/fs/fusefs/readlink.cc b/tests/sys/fs/fusefs/readlink.cc
new file mode 100644
index 000000000000..30815f2cd4b6
--- /dev/null
+++ b/tests/sys/fs/fusefs/readlink.cc
@@ -0,0 +1,162 @@
+/*-
+ * 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/param.h>
+
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Readlink: public FuseTest {
+public:
+void expect_lookup(const char *relpath, uint64_t ino)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFLNK | 0777, 0, 1);
+}
+void expect_readlink(uint64_t ino, ProcessMockerT r)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READLINK &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(r));
+}
+
+};
+
+class PushSymlinksIn: public Readlink {
+ virtual void SetUp() {
+ m_push_symlinks_in = true;
+ Readlink::SetUp();
+ }
+};
+
+TEST_F(Readlink, eloop)
+{
+ const char FULLPATH[] = "mountpoint/src";
+ const char RELPATH[] = "src";
+ const uint64_t ino = 42;
+ char buf[80];
+
+ expect_lookup(RELPATH, ino);
+ expect_readlink(ino, ReturnErrno(ELOOP));
+
+ EXPECT_EQ(-1, readlink(FULLPATH, buf, sizeof(buf)));
+ EXPECT_EQ(ELOOP, errno);
+}
+
+/*
+ * If a malicious or buggy server returns a NUL in the FUSE_READLINK result, it
+ * should be handled gracefully.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=274268
+ */
+TEST_F(Readlink, embedded_nul)
+{
+ const char FULLPATH[] = "mountpoint/src";
+ const char RELPATH[] = "src";
+ const char dst[] = "dst\0stuff";
+ char buf[80];
+ const uint64_t ino = 42;
+
+ 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_IFLNK | 0777;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READLINK &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ memcpy(out.body.str, dst, sizeof(dst));
+ out.header.len = sizeof(out.header) + sizeof(dst) + 1;
+ })));
+
+ EXPECT_EQ(-1, readlink(FULLPATH, buf, sizeof(buf)));
+ EXPECT_EQ(EIO, errno);
+ EXPECT_EQ(-1, access(FULLPATH, R_OK));
+ EXPECT_EQ(EIO, errno);
+}
+
+TEST_F(Readlink, ok)
+{
+ const char FULLPATH[] = "mountpoint/src";
+ const char RELPATH[] = "src";
+ const char dst[] = "dst";
+ const uint64_t ino = 42;
+ char buf[80];
+
+ expect_lookup(RELPATH, ino);
+ expect_readlink(ino, ReturnImmediate([=](auto in __unused, auto& out) {
+ strlcpy(out.body.str, dst, sizeof(out.body.str));
+ out.header.len = sizeof(out.header) + strlen(dst) + 1;
+ }));
+
+ EXPECT_EQ(static_cast<ssize_t>(strlen(dst)) + 1,
+ readlink(FULLPATH, buf, sizeof(buf)));
+ EXPECT_STREQ(dst, buf);
+}
+
+TEST_F(PushSymlinksIn, readlink)
+{
+ const char FULLPATH[] = "mountpoint/src";
+ const char RELPATH[] = "src";
+ const char dst[] = "/dst";
+ const uint64_t ino = 42;
+ char buf[MAXPATHLEN], wd[MAXPATHLEN], want[MAXPATHLEN];
+ int len;
+
+ expect_lookup(RELPATH, ino);
+ expect_readlink(ino, ReturnImmediate([=](auto in __unused, auto& out) {
+ strlcpy(out.body.str, dst, sizeof(out.body.str));
+ out.header.len = sizeof(out.header) + strlen(dst) + 1;
+ }));
+
+ ASSERT_NE(nullptr, getcwd(wd, sizeof(wd))) << strerror(errno);
+ len = snprintf(want, sizeof(want), "%s/mountpoint%s", wd, dst);
+ ASSERT_LE(0, len) << strerror(errno);
+
+ EXPECT_EQ(static_cast<ssize_t>(len) + 1,
+ readlink(FULLPATH, buf, sizeof(buf)));
+ EXPECT_STREQ(want, buf);
+}
diff --git a/tests/sys/fs/fusefs/release.cc b/tests/sys/fs/fusefs/release.cc
new file mode 100644
index 000000000000..9df236bfbaf7
--- /dev/null
+++ b/tests/sys/fs/fusefs/release.cc
@@ -0,0 +1,289 @@
+/*-
+ * 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/socket.h>
+#include <sys/un.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Release: public FuseTest {
+
+public:
+void expect_lookup(const char *relpath, uint64_t ino, int times)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, times);
+}
+
+void expect_release(uint64_t ino, uint64_t lock_owner,
+ uint32_t flags, int error)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_RELEASE &&
+ in.header.nodeid == ino &&
+ in.body.release.lock_owner == lock_owner &&
+ in.body.release.fh == FH &&
+ in.body.release.flags == flags);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(error)))
+ .RetiresOnSaturation();
+}
+};
+
+class ReleaseWithLocks: public Release {
+ virtual void SetUp() {
+ m_init_flags = FUSE_POSIX_LOCKS;
+ Release::SetUp();
+ }
+};
+
+
+/* If a file descriptor is duplicated, only the last close causes RELEASE */
+TEST_F(Release, dup)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd, fd2;
+
+ expect_lookup(RELPATH, ino, 1);
+ expect_open(ino, 0, 1);
+ expect_flush(ino, 1, ReturnErrno(0));
+ expect_release(ino, getpid(), O_RDONLY, 0);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ fd2 = dup(fd);
+ ASSERT_LE(0, fd2) << strerror(errno);
+
+ ASSERT_EQ(0, close(fd2)) << strerror(errno);
+ ASSERT_EQ(0, close(fd)) << strerror(errno);
+}
+
+/*
+ * Some FUSE filesystem cache data internally and flush it on release. Such
+ * filesystems may generate errors during release. On Linux, these get
+ * returned by close(2). However, POSIX does not require close(2) to return
+ * this error. FreeBSD's fuse(4) should return EIO if it returns an error at
+ * all.
+ */
+/* http://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html */
+TEST_F(Release, eio)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino, 1);
+ expect_open(ino, 0, 1);
+ expect_flush(ino, 1, ReturnErrno(0));
+ expect_release(ino, getpid(), O_WRONLY, EIO);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_TRUE(0 == close(fd) || errno == EIO) << strerror(errno);
+}
+
+/*
+ * FUSE_RELEASE should contain the same flags used for FUSE_OPEN
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */
+TEST_F(Release, DISABLED_flags)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino, 1);
+ expect_open(ino, 0, 1);
+ expect_flush(ino, 1, ReturnErrno(0));
+ expect_release(ino, getpid(), O_RDWR | O_APPEND, 0);
+
+ fd = open(FULLPATH, O_RDWR | O_APPEND);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(0, close(fd)) << strerror(errno);
+}
+
+/*
+ * fuse(4) will issue multiple FUSE_OPEN operations for the same file if it's
+ * opened with different modes. Each FUSE_OPEN should get its own
+ * FUSE_RELEASE.
+ */
+TEST_F(Release, multiple_opens)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd, fd2;
+
+ expect_lookup(RELPATH, ino, 2);
+ expect_open(ino, 0, 2);
+ expect_flush(ino, 2, ReturnErrno(0));
+ expect_release(ino, getpid(), O_RDONLY, 0);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ expect_release(ino, getpid(), O_WRONLY, 0);
+ fd2 = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd2) << strerror(errno);
+
+ ASSERT_EQ(0, close(fd2)) << strerror(errno);
+ ASSERT_EQ(0, close(fd)) << strerror(errno);
+}
+
+TEST_F(Release, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino, 1);
+ expect_open(ino, 0, 1);
+ expect_flush(ino, 1, ReturnErrno(0));
+ expect_release(ino, getpid(), O_RDONLY, 0);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(0, close(fd)) << strerror(errno);
+}
+
+/*
+ * Nothing bad should happen when closing a Unix-domain named socket that
+ * contains a fusefs file descriptor within its receive buffer.
+ * Regression test for
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=289686
+ */
+TEST_F(Release, scm_rights)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct msghdr msg;
+ struct iovec iov;
+ char message[CMSG_SPACE(sizeof(int))];
+ uint64_t ino = 42;
+ int fd;
+ int s[2];
+ union {
+ char buf[CMSG_SPACE(sizeof(fd))];
+ struct cmsghdr align;
+ } u;
+
+ expect_lookup(RELPATH, ino, 1);
+ expect_open(ino, 0, 1);
+ expect_flush(ino, 1, ReturnErrno(0));
+ expect_release(ino, getpid(), O_RDONLY, 0);
+
+ ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, s)) << strerror(errno);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ memset(&message, 0, sizeof(message));
+ memset(&msg, 0, sizeof(msg));
+ iov.iov_base = NULL;
+ iov.iov_len = 0;
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+ msg.msg_control = u.buf,
+ msg.msg_controllen = sizeof(u.buf);
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(fd));
+ memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));
+ ASSERT_GE(sendmsg(s[0], &msg, 0), 0) << strerror(errno);
+
+ close(fd); // Close fd within our process
+ close(s[0]);
+ close(s[1]); // The last copy of fd is within this socket's rcvbuf
+}
+
+/* When closing a file with a POSIX file lock, release should release the lock*/
+TEST_F(ReleaseWithLocks, unlock_on_close)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+ struct flock fl;
+ pid_t pid = getpid();
+
+ expect_lookup(RELPATH, ino, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_SETLK &&
+ in.header.nodeid == ino &&
+ in.body.setlk.lk.type == F_RDLCK &&
+ in.body.setlk.fh == FH);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(0)));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_SETLK &&
+ in.header.nodeid == ino &&
+ in.body.setlk.lk.type == F_UNLCK &&
+ in.body.setlk.fh == FH);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(0)));
+ expect_flush(ino, 1, ReturnErrno(0));
+ expect_release(ino, static_cast<uint64_t>(pid), O_RDWR, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ fl.l_start = 0;
+ fl.l_len = 0;
+ fl.l_pid = pid;
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_SET;
+ fl.l_sysid = 0;
+ ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno);
+
+ ASSERT_EQ(0, close(fd)) << strerror(errno);
+}
diff --git a/tests/sys/fs/fusefs/releasedir.cc b/tests/sys/fs/fusefs/releasedir.cc
new file mode 100644
index 000000000000..0f9337a5b761
--- /dev/null
+++ b/tests/sys/fs/fusefs/releasedir.cc
@@ -0,0 +1,116 @@
+/*-
+ * 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 <dirent.h>
+#include <fcntl.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class ReleaseDir: public FuseTest {
+
+public:
+void expect_lookup(const char *relpath, uint64_t ino)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFDIR | 0755, 0, 1);
+}
+};
+
+/* If a file descriptor is duplicated, only the last close causes RELEASE */
+TEST_F(ReleaseDir, dup)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ DIR *dir, *dir2;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READDIR &&
+ in.header.nodeid == ino &&
+ in.body.readdir.offset == 0);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ out.header.error = 0;
+ out.header.len = sizeof(out.header);
+ })));
+ expect_releasedir(ino, ReturnErrno(0));
+
+ dir = opendir(FULLPATH);
+ ASSERT_NE(nullptr, dir) << strerror(errno);
+
+ dir2 = fdopendir(dup(dirfd(dir)));
+ ASSERT_NE(nullptr, dir2) << strerror(errno);
+
+ ASSERT_EQ(0, closedir(dir)) << strerror(errno);
+ ASSERT_EQ(0, closedir(dir2)) << strerror(errno);
+}
+
+TEST_F(ReleaseDir, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ DIR *dir;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+ expect_releasedir(ino, ReturnErrno(0));
+
+ dir = opendir(FULLPATH);
+ ASSERT_NE(nullptr, dir) << strerror(errno);
+
+ ASSERT_EQ(0, closedir(dir)) << strerror(errno);
+}
+
+/* Directories opened O_EXEC should be properly released, too */
+TEST_F(ReleaseDir, o_exec)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino);
+ expect_releasedir(ino, ReturnErrno(0));
+
+ fd = open(FULLPATH, O_EXEC | O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(0, close(fd)) << strerror(errno);
+}
diff --git a/tests/sys/fs/fusefs/rename.cc b/tests/sys/fs/fusefs/rename.cc
new file mode 100644
index 000000000000..6b5687e209c6
--- /dev/null
+++ b/tests/sys/fs/fusefs/rename.cc
@@ -0,0 +1,330 @@
+/*-
+ * 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 <stdlib.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Rename: public FuseTest {
+ public:
+ int tmpfd = -1;
+ char tmpfile[80] = "/tmp/fuse.rename.XXXXXX";
+
+ virtual void TearDown() {
+ if (tmpfd >= 0) {
+ close(tmpfd);
+ unlink(tmpfile);
+ }
+
+ FuseTest::TearDown();
+ }
+};
+
+// EINVAL, dst is subdir of src
+TEST_F(Rename, einval)
+{
+ const char FULLDST[] = "mountpoint/src/dst";
+ const char RELDST[] = "dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ uint64_t src_ino = 42;
+
+ expect_lookup(RELSRC, src_ino, S_IFDIR | 0755, 0, 2);
+ EXPECT_LOOKUP(src_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ ASSERT_NE(0, rename(FULLSRC, FULLDST));
+ ASSERT_EQ(EINVAL, errno);
+}
+
+// source does not exist
+TEST_F(Rename, enoent)
+{
+ const char FULLDST[] = "mountpoint/dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ // FUSE hardcodes the mountpoint to inode 1
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELSRC)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ ASSERT_NE(0, rename(FULLSRC, FULLDST));
+ ASSERT_EQ(ENOENT, errno);
+}
+
+/*
+ * Renaming a file after FUSE_LOOKUP returned a negative cache entry for dst
+ */
+TEST_F(Rename, entry_cache_negative)
+{
+ const char FULLDST[] = "mountpoint/dst";
+ const char RELDST[] = "dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ uint64_t dst_dir_ino = FUSE_ROOT_ID;
+ uint64_t ino = 42;
+ /*
+ * Set entry_valid = 0 because this test isn't concerned with whether
+ * or not we actually cache negative entries, only with whether we
+ * interpret negative cache responses correctly.
+ */
+ struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0};
+
+ expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
+ /* LOOKUP returns a negative cache entry for dst */
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
+ .WillOnce(ReturnNegativeCache(&entry_valid));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *src = (const char*)in.body.bytes +
+ sizeof(fuse_rename_in);
+ const char *dst = src + strlen(src) + 1;
+ return (in.header.opcode == FUSE_RENAME &&
+ in.body.rename.newdir == dst_dir_ino &&
+ (0 == strcmp(RELDST, dst)) &&
+ (0 == strcmp(RELSRC, src)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(0)));
+
+ ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
+}
+
+/*
+ * Renaming a file should purge any negative namecache entries for the dst
+ */
+TEST_F(Rename, entry_cache_negative_purge)
+{
+ const char FULLDST[] = "mountpoint/dst";
+ const char RELDST[] = "dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ uint64_t dst_dir_ino = FUSE_ROOT_ID;
+ uint64_t ino = 42;
+ struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0};
+
+ expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
+ /* LOOKUP returns a negative cache entry for dst */
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
+ .WillOnce(ReturnNegativeCache(&entry_valid))
+ .RetiresOnSaturation();
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *src = (const char*)in.body.bytes +
+ sizeof(fuse_rename_in);
+ const char *dst = src + strlen(src) + 1;
+ return (in.header.opcode == FUSE_RENAME &&
+ in.body.rename.newdir == dst_dir_ino &&
+ (0 == strcmp(RELDST, dst)) &&
+ (0 == strcmp(RELSRC, src)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(0)));
+
+ ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
+
+ /* Finally, a subsequent lookup should query the daemon */
+ expect_lookup(RELDST, ino, S_IFREG | 0644, 0, 1);
+
+ ASSERT_EQ(0, access(FULLDST, F_OK)) << strerror(errno);
+}
+
+TEST_F(Rename, exdev)
+{
+ const char FULLB[] = "mountpoint/src";
+ const char RELB[] = "src";
+ // FUSE hardcodes the mountpoint to inode 1
+ uint64_t b_ino = 42;
+
+ tmpfd = mkstemp(tmpfile);
+ ASSERT_LE(0, tmpfd) << strerror(errno);
+
+ expect_lookup(RELB, b_ino, S_IFREG | 0644, 0, 2);
+
+ ASSERT_NE(0, rename(tmpfile, FULLB));
+ ASSERT_EQ(EXDEV, errno);
+
+ ASSERT_NE(0, rename(FULLB, tmpfile));
+ ASSERT_EQ(EXDEV, errno);
+}
+
+TEST_F(Rename, ok)
+{
+ const char FULLDST[] = "mountpoint/dst";
+ const char RELDST[] = "dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ uint64_t dst_dir_ino = FUSE_ROOT_ID;
+ uint64_t ino = 42;
+
+ expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *src = (const char*)in.body.bytes +
+ sizeof(fuse_rename_in);
+ const char *dst = src + strlen(src) + 1;
+ return (in.header.opcode == FUSE_RENAME &&
+ in.body.rename.newdir == dst_dir_ino &&
+ (0 == strcmp(RELDST, dst)) &&
+ (0 == strcmp(RELSRC, src)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(0)));
+
+ ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
+}
+
+/* When moving a file to a new directory, update its parent */
+TEST_F(Rename, parent)
+{
+ const char FULLDST[] = "mountpoint/dstdir/dst";
+ const char RELDSTDIR[] = "dstdir";
+ const char RELDST[] = "dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ const char FULLDSTPARENT[] = "mountpoint/dstdir";
+ const char FULLDSTDOTDOT[] = "mountpoint/dstdir/dst/..";
+ Sequence seq;
+ uint64_t dst_dir_ino = 43;
+ uint64_t ino = 42;
+ struct stat sb;
+
+ expect_lookup(RELSRC, ino, S_IFDIR | 0755, 0, 1);
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDSTDIR)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = dst_dir_ino;
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.attr.ino = dst_dir_ino;
+ out.body.entry.attr.nlink = 2;
+ })));
+ EXPECT_LOOKUP(dst_dir_ino, RELDST)
+ .InSequence(seq)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *src = (const char*)in.body.bytes +
+ sizeof(fuse_rename_in);
+ const char *dst = src + strlen(src) + 1;
+ return (in.header.opcode == FUSE_RENAME &&
+ in.body.rename.newdir == dst_dir_ino &&
+ (0 == strcmp(RELDST, dst)) &&
+ (0 == strcmp(RELSRC, src)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(0)));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == 1);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr_valid = UINT64_MAX;
+ out.body.attr.attr.ino = 1;
+ out.body.attr.attr.mode = S_IFDIR | 0755;
+ out.body.attr.attr.nlink = 2;
+ })));
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDSTDIR)
+ .InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = dst_dir_ino;
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.attr.ino = dst_dir_ino;
+ out.body.entry.attr.nlink = 3;
+ })));
+ EXPECT_LOOKUP(dst_dir_ino, RELDST)
+ .InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = ino;
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr_valid = UINT64_MAX;
+ })));
+
+ ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
+
+ ASSERT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
+ EXPECT_EQ(2ul, sb.st_nlink);
+
+ ASSERT_EQ(0, stat(FULLDSTPARENT, &sb)) << strerror(errno);
+ EXPECT_EQ(3ul, sb.st_nlink);
+
+ ASSERT_EQ(0, stat(FULLDSTDOTDOT, &sb)) << strerror(errno);
+ ASSERT_EQ(dst_dir_ino, sb.st_ino);
+}
+
+// Rename overwrites an existing destination file
+TEST_F(Rename, overwrite)
+{
+ 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 dst_dir_ino = FUSE_ROOT_ID;
+ uint64_t ino = 42;
+
+ expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
+ expect_lookup(RELDST, dst_ino, S_IFREG | 0644, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *src = (const char*)in.body.bytes +
+ sizeof(fuse_rename_in);
+ const char *dst = src + strlen(src) + 1;
+ return (in.header.opcode == FUSE_RENAME &&
+ in.body.rename.newdir == dst_dir_ino &&
+ (0 == strcmp(RELDST, dst)) &&
+ (0 == strcmp(RELSRC, src)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(0)));
+
+ ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
+}
diff --git a/tests/sys/fs/fusefs/rmdir.cc b/tests/sys/fs/fusefs/rmdir.cc
new file mode 100644
index 000000000000..146b916f3d9f
--- /dev/null
+++ b/tests/sys/fs/fusefs/rmdir.cc
@@ -0,0 +1,162 @@
+/*-
+ * 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 <fcntl.h>
+#include <semaphore.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Rmdir: public FuseTest {
+public:
+void expect_lookup(const char *relpath, uint64_t ino, int times=1)
+{
+ EXPECT_LOOKUP(FUSE_ROOT_ID, relpath)
+ .Times(times)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 2;
+ })));
+}
+
+void expect_rmdir(uint64_t parent, const char *relpath, int error)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_RMDIR &&
+ 0 == strcmp(relpath, in.body.rmdir) &&
+ in.header.nodeid == parent);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(error)));
+}
+};
+
+/*
+ * A successful rmdir should clear the parent directory's attribute cache,
+ * because the fuse daemon should update its mtime and ctime
+ */
+TEST_F(Rmdir, parent_attr_cache)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ struct stat sb;
+ sem_t sem;
+ uint64_t ino = 42;
+ Sequence seq;
+
+ ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
+
+ expect_lookup(RELPATH, ino);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_RMDIR &&
+ 0 == strcmp(RELPATH, in.body.rmdir) &&
+ in.header.nodeid == FUSE_ROOT_ID);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(Invoke(ReturnErrno(0)));
+ expect_forget(ino, 1, &sem);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == FUSE_ROOT_ID);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = FUSE_ROOT_ID;
+ out.body.attr.attr.mode = S_IFDIR | 0755;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+
+ ASSERT_EQ(0, rmdir(FULLPATH)) << strerror(errno);
+ EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
+ sem_wait(&sem);
+ sem_destroy(&sem);
+}
+
+TEST_F(Rmdir, enotempty)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+
+ expect_lookup(RELPATH, ino);
+ expect_rmdir(FUSE_ROOT_ID, RELPATH, ENOTEMPTY);
+
+ ASSERT_NE(0, rmdir(FULLPATH));
+ ASSERT_EQ(ENOTEMPTY, errno);
+}
+
+/* Removing a directory should expire its entry cache */
+TEST_F(Rmdir, entry_cache)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ sem_t sem;
+ uint64_t ino = 42;
+
+ expect_lookup(RELPATH, ino, 2);
+ expect_rmdir(FUSE_ROOT_ID, RELPATH, 0);
+ expect_forget(ino, 1, &sem);
+
+ ASSERT_EQ(0, rmdir(FULLPATH)) << strerror(errno);
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+ sem_wait(&sem);
+ sem_destroy(&sem);
+}
+
+TEST_F(Rmdir, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ sem_t sem;
+ uint64_t ino = 42;
+
+ ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
+
+ expect_lookup(RELPATH, ino);
+ expect_rmdir(FUSE_ROOT_ID, RELPATH, 0);
+ expect_forget(ino, 1, &sem);
+
+ ASSERT_EQ(0, rmdir(FULLPATH)) << strerror(errno);
+ sem_wait(&sem);
+ sem_destroy(&sem);
+}
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);
+}
diff --git a/tests/sys/fs/fusefs/statfs.cc b/tests/sys/fs/fusefs/statfs.cc
new file mode 100644
index 000000000000..1dd96e8073d7
--- /dev/null
+++ b/tests/sys/fs/fusefs/statfs.cc
@@ -0,0 +1,171 @@
+/*-
+ * 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/param.h>
+#include <sys/mount.h>
+#include <semaphore.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Statfs: public FuseTest {};
+
+TEST_F(Statfs, eio)
+{
+ struct statfs statbuf;
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_STATFS);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(EIO)));
+
+ ASSERT_NE(0, statfs("mountpoint", &statbuf));
+ ASSERT_EQ(EIO, errno);
+}
+
+/*
+ * When the daemon is dead but the filesystem is still mounted, fuse(4) fakes
+ * the statfs(2) response, which is necessary for unmounting.
+ */
+TEST_F(Statfs, enotconn)
+{
+ struct statfs statbuf;
+ char mp[PATH_MAX];
+
+ m_mock->kill_daemon();
+
+ ASSERT_NE(nullptr, getcwd(mp, PATH_MAX)) << strerror(errno);
+ strlcat(mp, "/mountpoint", PATH_MAX);
+ ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
+
+ EXPECT_EQ(getuid(), statbuf.f_owner);
+ EXPECT_EQ(0, strcmp("fusefs", statbuf.f_fstypename));
+ EXPECT_EQ(0, strcmp("/dev/fuse", statbuf.f_mntfromname));
+ EXPECT_EQ(0, strcmp(mp, statbuf.f_mntonname));
+}
+
+static void* statfs_th(void* arg) {
+ ssize_t r;
+ struct statfs *sb = (struct statfs*)arg;
+
+ r = statfs("mountpoint", sb);
+ if (r >= 0)
+ return 0;
+ else
+ return (void*)(intptr_t)errno;
+}
+
+/*
+ * Like the enotconn test, but in this case the daemon dies after we send the
+ * FUSE_STATFS operation but before we get a response.
+ */
+TEST_F(Statfs, enotconn_while_blocked)
+{
+ struct statfs statbuf;
+ void *thr0_value;
+ pthread_t th0;
+ char mp[PATH_MAX];
+ sem_t sem;
+
+ ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_STATFS);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([&](auto in __unused, auto &out __unused) {
+ sem_post(&sem);
+ /* Just block until the daemon dies */
+ }));
+
+ ASSERT_NE(nullptr, getcwd(mp, PATH_MAX)) << strerror(errno);
+ strlcat(mp, "/mountpoint", PATH_MAX);
+ ASSERT_EQ(0, pthread_create(&th0, NULL, statfs_th, (void*)&statbuf))
+ << strerror(errno);
+
+ ASSERT_EQ(0, sem_wait(&sem)) << strerror(errno);
+ m_mock->kill_daemon();
+
+ pthread_join(th0, &thr0_value);
+ ASSERT_EQ(0, (intptr_t)thr0_value);
+
+ EXPECT_EQ(getuid(), statbuf.f_owner);
+ EXPECT_EQ(0, strcmp("fusefs", statbuf.f_fstypename));
+ EXPECT_EQ(0, strcmp("/dev/fuse", statbuf.f_mntfromname));
+ EXPECT_EQ(0, strcmp(mp, statbuf.f_mntonname));
+}
+
+TEST_F(Statfs, ok)
+{
+ struct statfs statbuf;
+ char mp[PATH_MAX];
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_STATFS);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, statfs);
+ out.body.statfs.st.blocks = 1000;
+ out.body.statfs.st.bfree = 100;
+ out.body.statfs.st.bavail = 200;
+ out.body.statfs.st.files = 5;
+ out.body.statfs.st.ffree = 6;
+ out.body.statfs.st.namelen = 128;
+ out.body.statfs.st.frsize = 1024;
+ })));
+
+ ASSERT_NE(nullptr, getcwd(mp, PATH_MAX)) << strerror(errno);
+ strlcat(mp, "/mountpoint", PATH_MAX);
+ ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
+ EXPECT_EQ(1024ul, statbuf.f_bsize);
+ /*
+ * fuse(4) ignores the filesystem's reported optimal transfer size, and
+ * chooses a size that works well with the rest of the system instead
+ */
+ EXPECT_EQ(1000ul, statbuf.f_blocks);
+ EXPECT_EQ(100ul, statbuf.f_bfree);
+ EXPECT_EQ(200l, statbuf.f_bavail);
+ EXPECT_EQ(5ul, statbuf.f_files);
+ EXPECT_EQ(6l, statbuf.f_ffree);
+ EXPECT_EQ(128u, statbuf.f_namemax);
+ EXPECT_EQ(getuid(), statbuf.f_owner);
+ EXPECT_EQ(0, strcmp("fusefs", statbuf.f_fstypename));
+ EXPECT_EQ(0, strcmp("/dev/fuse", statbuf.f_mntfromname));
+ EXPECT_EQ(0, strcmp(mp, statbuf.f_mntonname));
+}
diff --git a/tests/sys/fs/fusefs/symlink.cc b/tests/sys/fs/fusefs/symlink.cc
new file mode 100644
index 000000000000..bd355497a8bd
--- /dev/null
+++ b/tests/sys/fs/fusefs/symlink.cc
@@ -0,0 +1,208 @@
+/*-
+ * 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 <semaphore.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Symlink: public FuseTest {
+public:
+
+void expect_symlink(uint64_t ino, const char *target, const char *relpath)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes;
+ const char *linkname = name + strlen(name) + 1;
+ return (in.header.opcode == FUSE_SYMLINK &&
+ (0 == strcmp(linkname, target)) &&
+ (0 == strcmp(name, relpath)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFLNK | 0777;
+ out.body.entry.nodeid = ino;
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr_valid = UINT64_MAX;
+ })));
+}
+
+};
+
+class Symlink_7_8: public FuseTest {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 8;
+ FuseTest::SetUp();
+}
+
+void expect_symlink(uint64_t ino, const char *target, const char *relpath)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes;
+ const char *linkname = name + strlen(name) + 1;
+ return (in.header.opcode == FUSE_SYMLINK &&
+ (0 == strcmp(linkname, target)) &&
+ (0 == strcmp(name, relpath)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry_7_8);
+ out.body.entry.attr.mode = S_IFLNK | 0777;
+ out.body.entry.nodeid = ino;
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr_valid = UINT64_MAX;
+ })));
+}
+
+};
+
+/*
+ * A successful symlink should clear the parent directory's attribute cache,
+ * because the fuse daemon should update its mtime and ctime
+ */
+TEST_F(Symlink, clear_attr_cache)
+{
+ const char FULLPATH[] = "mountpoint/src";
+ const char RELPATH[] = "src";
+ const char dst[] = "dst";
+ const uint64_t ino = 42;
+ struct stat sb;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == FUSE_ROOT_ID);
+ }, Eq(true)),
+ _)
+ ).Times(2)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = FUSE_ROOT_ID;
+ out.body.attr.attr.mode = S_IFDIR | 0755;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+ expect_symlink(ino, dst, RELPATH);
+
+ EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
+ EXPECT_EQ(0, symlink(dst, FULLPATH)) << strerror(errno);
+ EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
+}
+
+TEST_F(Symlink, enospc)
+{
+ const char FULLPATH[] = "mountpoint/lnk";
+ const char RELPATH[] = "lnk";
+ const char dst[] = "dst";
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *name = (const char*)in.body.bytes;
+ const char *linkname = name + strlen(name) + 1;
+ return (in.header.opcode == FUSE_SYMLINK &&
+ (0 == strcmp(linkname, dst)) &&
+ (0 == strcmp(name, RELPATH)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENOSPC)));
+
+ EXPECT_EQ(-1, symlink(dst, FULLPATH));
+ EXPECT_EQ(ENOSPC, errno);
+}
+
+TEST_F(Symlink, ok)
+{
+ const char FULLPATH[] = "mountpoint/src";
+ const char RELPATH[] = "src";
+ const char dst[] = "dst";
+ const uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_symlink(ino, dst, RELPATH);
+
+ EXPECT_EQ(0, symlink(dst, FULLPATH)) << strerror(errno);
+}
+
+/*
+ * Nothing bad should happen if the server returns the parent's inode number
+ * for the newly created symlink. Regression test for bug 263662.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263662
+ */
+TEST_F(Symlink, parent_ino)
+{
+ const char FULLPATH[] = "mountpoint/parent/src";
+ const char PPATH[] = "parent";
+ const char RELPATH[] = "src";
+ const char dst[] = "dst";
+ sem_t sem;
+ const uint64_t ino = 42;
+
+ ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
+
+ expect_lookup(PPATH, ino, S_IFDIR | 0755, 0, 1);
+ EXPECT_LOOKUP(ino, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_symlink(ino, dst, RELPATH);
+ expect_forget(ino, 1, &sem);
+
+ EXPECT_EQ(-1, symlink(dst, FULLPATH));
+ EXPECT_EQ(EIO, errno);
+
+ sem_wait(&sem);
+ sem_destroy(&sem);
+}
+
+TEST_F(Symlink_7_8, ok)
+{
+ const char FULLPATH[] = "mountpoint/src";
+ const char RELPATH[] = "src";
+ const char dst[] = "dst";
+ const uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnErrno(ENOENT)));
+ expect_symlink(ino, dst, RELPATH);
+
+ EXPECT_EQ(0, symlink(dst, FULLPATH)) << strerror(errno);
+}
diff --git a/tests/sys/fs/fusefs/unlink.cc b/tests/sys/fs/fusefs/unlink.cc
new file mode 100644
index 000000000000..1d8a371649ee
--- /dev/null
+++ b/tests/sys/fs/fusefs/unlink.cc
@@ -0,0 +1,237 @@
+/*-
+ * 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 <fcntl.h>
+#include <semaphore.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Unlink: public FuseTest {
+public:
+void expect_lookup(const char *relpath, uint64_t ino, int times, int nlink=1)
+{
+ EXPECT_LOOKUP(FUSE_ROOT_ID, relpath)
+ .Times(times)
+ .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.attr.nlink = nlink;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.attr.size = 0;
+ })));
+}
+
+};
+
+/*
+ * Unlinking a multiply linked file should update its ctime and nlink. This
+ * could be handled simply by invalidating the attributes, necessitating a new
+ * GETATTR, but we implement it in-kernel for efficiency's sake.
+ */
+TEST_F(Unlink, attr_cache)
+{
+ 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";
+ uint64_t ino = 42;
+ struct stat sb_old, sb_new;
+ int fd1;
+
+ expect_lookup(RELPATH0, ino, 1, 2);
+ expect_lookup(RELPATH1, ino, 1, 2);
+ expect_open(ino, 0, 1);
+ expect_unlink(1, RELPATH0, 0);
+
+ fd1 = open(FULLPATH1, O_RDONLY);
+ ASSERT_LE(0, fd1) << strerror(errno);
+
+ ASSERT_EQ(0, fstat(fd1, &sb_old)) << strerror(errno);
+ ASSERT_EQ(0, unlink(FULLPATH0)) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd1, &sb_new)) << strerror(errno);
+ EXPECT_NE(sb_old.st_ctime, sb_new.st_ctime);
+ EXPECT_EQ(1u, sb_new.st_nlink);
+
+ leak(fd1);
+}
+
+/*
+ * A successful unlink should clear the parent directory's attribute cache,
+ * because the fuse daemon should update its mtime and ctime
+ */
+TEST_F(Unlink, parent_attr_cache)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ struct stat sb;
+ uint64_t ino = 42;
+ Sequence seq;
+
+ /* Use nlink=2 so we don't get a FUSE_FORGET */
+ expect_lookup(RELPATH, ino, 1, 2);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_UNLINK &&
+ 0 == strcmp(RELPATH, in.body.unlink) &&
+ in.header.nodeid == FUSE_ROOT_ID);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(Invoke(ReturnErrno(0)));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == FUSE_ROOT_ID);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = FUSE_ROOT_ID;
+ out.body.attr.attr.mode = S_IFDIR | 0755;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+
+ ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno);
+ EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
+}
+
+TEST_F(Unlink, eperm)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+
+ expect_lookup(RELPATH, ino, 1);
+ expect_unlink(1, RELPATH, EPERM);
+
+ ASSERT_NE(0, unlink(FULLPATH));
+ ASSERT_EQ(EPERM, errno);
+}
+
+/*
+ * Unlinking a file should expire its entry cache, even if it's multiply linked
+ */
+TEST_F(Unlink, entry_cache)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+
+ expect_lookup(RELPATH, ino, 2, 2);
+ expect_unlink(1, RELPATH, 0);
+
+ ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno);
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+}
+
+/*
+ * Unlink a multiply-linked file. There should be no FUSE_FORGET because the
+ * file is still linked.
+ */
+TEST_F(Unlink, 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";
+ uint64_t ino = 42;
+
+ expect_lookup(RELPATH0, ino, 1, 2);
+ expect_unlink(1, RELPATH0, 0);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_FORGET &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+ expect_lookup(RELPATH1, ino, 1, 1);
+
+ ASSERT_EQ(0, unlink(FULLPATH0)) << strerror(errno);
+
+ /*
+ * The final syscall simply ensures that no FUSE_FORGET was ever sent,
+ * by scheduling an arbitrary different operation after a FUSE_FORGET
+ * would've been sent.
+ */
+ ASSERT_EQ(0, access(FULLPATH1, F_OK)) << 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_lookup(RELPATH, ino, 1);
+ expect_unlink(1, RELPATH, 0);
+ expect_forget(ino, 1, &sem);
+
+ ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno);
+ sem_wait(&sem);
+ sem_destroy(&sem);
+}
+
+/* Unlink an open file */
+TEST_F(Unlink, open_but_deleted)
+{
+ 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";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH0, ino, 2);
+ expect_open(ino, 0, 1);
+ expect_unlink(1, RELPATH0, 0);
+ expect_lookup(RELPATH1, ino, 1, 1);
+
+ fd = open(FULLPATH0, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, unlink(FULLPATH0)) << strerror(errno);
+
+ /*
+ * The final syscall simply ensures that no FUSE_FORGET was ever sent,
+ * by scheduling an arbitrary different operation after a FUSE_FORGET
+ * would've been sent.
+ */
+ ASSERT_EQ(0, access(FULLPATH1, F_OK)) << strerror(errno);
+ leak(fd);
+}
diff --git a/tests/sys/fs/fusefs/utils.cc b/tests/sys/fs/fusefs/utils.cc
new file mode 100644
index 000000000000..125b7e2d6fc7
--- /dev/null
+++ b/tests/sys/fs/fusefs/utils.cc
@@ -0,0 +1,673 @@
+/*-
+ * 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/param.h>
+#include <sys/mman.h>
+#include <sys/module.h>
+#include <sys/sysctl.h>
+#include <sys/wait.h>
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <pwd.h>
+#include <semaphore.h>
+#include <unistd.h>
+}
+
+#include <gtest/gtest.h>
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+/*
+ * The default max_write is set to this formula in libfuse, though
+ * individual filesystems can lower it. The "- 4096" was added in
+ * commit 154ffe2, with the commit message "fix".
+ */
+const uint32_t libfuse_max_write = 32 * getpagesize() + 0x1000 - 4096;
+
+/* Check that fusefs(4) is accessible and the current user can mount(2) */
+void check_environment()
+{
+ const char *devnode = "/dev/fuse";
+ const char *bsdextended_node = "security.mac.bsdextended.enabled";
+ int bsdextended_val = 0;
+ size_t bsdextended_size = sizeof(bsdextended_val);
+ int bsdextended_found;
+ const char *usermount_node = "vfs.usermount";
+ int usermount_val = 0;
+ size_t usermount_size = sizeof(usermount_val);
+ if (eaccess(devnode, R_OK | W_OK)) {
+ if (errno == ENOENT) {
+ GTEST_SKIP() << devnode << " does not exist";
+ } else if (errno == EACCES) {
+ GTEST_SKIP() << devnode <<
+ " is not accessible by the current user";
+ } else {
+ GTEST_SKIP() << strerror(errno);
+ }
+ }
+ // mac_bsdextended(4), when enabled, generates many more GETATTR
+ // operations. The fusefs tests' expectations don't account for those,
+ // and adding extra code to handle them obfuscates the real purpose of
+ // the tests. Better just to skip the fusefs tests if mac_bsdextended
+ // is enabled.
+ bsdextended_found = sysctlbyname(bsdextended_node, &bsdextended_val,
+ &bsdextended_size, NULL, 0);
+ if (bsdextended_found == 0 && bsdextended_val != 0)
+ GTEST_SKIP() <<
+ "The fusefs tests are incompatible with mac_bsdextended.";
+ ASSERT_EQ(sysctlbyname(usermount_node, &usermount_val, &usermount_size,
+ NULL, 0),
+ 0);
+ if (geteuid() != 0 && !usermount_val)
+ GTEST_SKIP() << "current user is not allowed to mount";
+}
+
+const char *cache_mode_to_s(enum cache_mode cm) {
+ switch (cm) {
+ case Uncached:
+ return "Uncached";
+ case Writethrough:
+ return "Writethrough";
+ case Writeback:
+ return "Writeback";
+ case WritebackAsync:
+ return "WritebackAsync";
+ default:
+ return "Unknown";
+ }
+}
+
+bool is_unsafe_aio_enabled(void) {
+ const char *node = "vfs.aio.enable_unsafe";
+ int val = 0;
+ size_t size = sizeof(val);
+
+ if (sysctlbyname(node, &val, &size, NULL, 0)) {
+ perror("sysctlbyname");
+ return (false);
+ }
+ return (val != 0);
+}
+
+class FuseEnv: public Environment {
+ virtual void SetUp() {
+ check_environment();
+ }
+};
+
+void FuseTest::SetUp() {
+ const char *maxbcachebuf_node = "vfs.maxbcachebuf";
+ const char *maxphys_node = "kern.maxphys";
+ size_t size;
+
+ size = sizeof(m_maxbcachebuf);
+ ASSERT_EQ(0, sysctlbyname(maxbcachebuf_node, &m_maxbcachebuf, &size,
+ NULL, 0)) << strerror(errno);
+ size = sizeof(m_maxphys);
+ ASSERT_EQ(0, sysctlbyname(maxphys_node, &m_maxphys, &size, NULL, 0))
+ << strerror(errno);
+ /*
+ * Set the default max_write to a distinct value from MAXPHYS to catch
+ * bugs that confuse the two.
+ */
+ if (m_maxwrite == 0)
+ m_maxwrite = MIN(libfuse_max_write, (uint32_t)m_maxphys / 2);
+
+ try {
+ m_mock = new MockFS(m_maxread, m_maxreadahead, m_allow_other,
+ m_default_permissions, m_push_symlinks_in, m_ro,
+ m_pm, m_init_flags, m_kernel_minor_version,
+ m_maxwrite, m_async, m_noclusterr, m_time_gran,
+ m_nointr, m_noatime, m_fsname, m_subtype,
+ m_no_auto_init);
+ /*
+ * FUSE_ACCESS is called almost universally. Expecting it in
+ * each test case would be super-annoying. Instead, set a
+ * default expectation for FUSE_ACCESS and return ENOSYS.
+ *
+ * Individual test cases can override this expectation since
+ * googlemock evaluates expectations in LIFO order.
+ */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_ACCESS);
+ }, Eq(true)),
+ _)
+ ).Times(AnyNumber())
+ .WillRepeatedly(Invoke(ReturnErrno(ENOSYS)));
+ /*
+ * FUSE_BMAP is called for most test cases that read data. Set
+ * a default expectation and return ENOSYS.
+ *
+ * Individual test cases can override this expectation since
+ * googlemock evaluates expectations in LIFO order.
+ */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_BMAP);
+ }, Eq(true)),
+ _)
+ ).Times(AnyNumber())
+ .WillRepeatedly(Invoke(ReturnErrno(ENOSYS)));
+ } catch (std::system_error err) {
+ FAIL() << err.what();
+ }
+}
+
+void
+FuseTest::expect_access(uint64_t ino, mode_t access_mode, int error)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_ACCESS &&
+ in.header.nodeid == ino &&
+ in.body.access.mask == access_mode);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(error)));
+}
+
+void
+FuseTest::expect_destroy(int error)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_DESTROY);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) {
+ m_mock->m_quit = true;
+ out.header.len = sizeof(out.header);
+ out.header.unique = in.header.unique;
+ out.header.error = -error;
+ })));
+}
+
+void
+FuseTest::expect_fallocate(uint64_t ino, uint64_t offset, uint64_t length,
+ uint32_t mode, int error, int times)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_FALLOCATE &&
+ in.header.nodeid == ino &&
+ in.body.fallocate.offset == offset &&
+ in.body.fallocate.length == length &&
+ in.body.fallocate.mode == mode);
+ }, Eq(true)),
+ _)
+ ).Times(times)
+ .WillRepeatedly(Invoke(ReturnErrno(error)));
+}
+
+void
+FuseTest::expect_flush(uint64_t ino, int times, ProcessMockerT r)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_FLUSH &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(times)
+ .WillRepeatedly(Invoke(r));
+}
+
+void
+FuseTest::expect_forget(uint64_t ino, uint64_t nlookup, sem_t *sem)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_FORGET &&
+ in.header.nodeid == ino &&
+ in.body.forget.nlookup == nlookup);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([=](auto in __unused, auto &out __unused) {
+ if (sem != NULL)
+ sem_post(sem);
+ /* FUSE_FORGET has no response! */
+ }));
+}
+
+void FuseTest::expect_getattr(uint64_t ino, uint64_t size)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(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 = S_IFREG | 0644;
+ out.body.attr.attr.size = size;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+}
+
+void FuseTest::expect_getxattr(uint64_t ino, const char *attr, ProcessMockerT r)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *a = (const char*)in.body.bytes +
+ sizeof(fuse_getxattr_in);
+ return (in.header.opcode == FUSE_GETXATTR &&
+ in.header.nodeid == ino &&
+ 0 == strcmp(attr, a));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(r));
+}
+
+void FuseTest::expect_lookup(const char *relpath, uint64_t ino, mode_t mode,
+ uint64_t size, int times, uint64_t attr_valid, uid_t uid, gid_t gid)
+{
+ EXPECT_LOOKUP(FUSE_ROOT_ID, relpath)
+ .Times(times)
+ .WillRepeatedly(Invoke(
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = attr_valid;
+ out.body.entry.attr.size = size;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr.gid = gid;
+ })));
+}
+
+void FuseTest::expect_lookup_7_8(const char *relpath, uint64_t ino, mode_t mode,
+ uint64_t size, int times, uint64_t attr_valid, uid_t uid, gid_t gid)
+{
+ EXPECT_LOOKUP(FUSE_ROOT_ID, relpath)
+ .Times(times)
+ .WillRepeatedly(Invoke(
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry_7_8);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = attr_valid;
+ out.body.entry.attr.size = size;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr.gid = gid;
+ })));
+}
+
+void FuseTest::expect_open(uint64_t ino, uint32_t flags, int times)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPEN &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(times)
+ .WillRepeatedly(Invoke(
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ out.header.len = sizeof(out.header);
+ SET_OUT_HEADER_LEN(out, open);
+ out.body.open.fh = FH;
+ out.body.open.open_flags = flags;
+ })));
+}
+
+void FuseTest::expect_opendir(uint64_t ino)
+{
+ /* opendir(3) calls fstatfs */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_STATFS);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(
+ ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, statfs);
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPENDIR &&
+ 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;
+ })));
+}
+
+void FuseTest::expect_read(uint64_t ino, uint64_t offset, uint64_t isize,
+ uint64_t osize, const void *contents, int flags, uint64_t fh)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READ &&
+ in.header.nodeid == ino &&
+ in.body.read.fh == fh &&
+ in.body.read.offset == offset &&
+ in.body.read.size == isize &&
+ (flags == -1 ?
+ (in.body.read.flags == O_RDONLY ||
+ in.body.read.flags == O_RDWR)
+ : in.body.read.flags == (uint32_t)flags));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ assert(osize <= sizeof(out.body.bytes));
+ out.header.len = sizeof(struct fuse_out_header) + osize;
+ memmove(out.body.bytes, contents, osize);
+ }))).RetiresOnSaturation();
+}
+
+void FuseTest::expect_readdir(uint64_t ino, uint64_t off,
+ std::vector<struct dirent> &ents)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READDIR &&
+ in.header.nodeid == ino &&
+ in.body.readdir.fh == FH &&
+ in.body.readdir.offset == off);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) {
+ struct fuse_dirent *fde = (struct fuse_dirent*)&(out.body);
+ int i = 0;
+
+ out.header.error = 0;
+ out.header.len = 0;
+
+ for (const auto& it: ents) {
+ size_t entlen, entsize;
+
+ fde->ino = it.d_fileno;
+ fde->off = it.d_off;
+ fde->type = it.d_type;
+ fde->namelen = it.d_namlen;
+ strncpy(fde->name, it.d_name, it.d_namlen);
+ entlen = FUSE_NAME_OFFSET + fde->namelen;
+ entsize = FUSE_DIRENT_SIZE(fde);
+ /*
+ * The FUSE protocol does not require zeroing out the
+ * unused portion of the name. But it's a good
+ * practice to prevent information disclosure to the
+ * FUSE client, even though the client is usually the
+ * kernel
+ */
+ memset(fde->name + fde->namelen, 0, entsize - entlen);
+ if (out.header.len + entsize > in.body.read.size) {
+ printf("Overflow in readdir expectation: i=%d\n"
+ , i);
+ break;
+ }
+ out.header.len += entsize;
+ fde = (struct fuse_dirent*)
+ ((intmax_t*)fde + entsize / sizeof(intmax_t));
+ i++;
+ }
+ out.header.len += sizeof(out.header);
+ })));
+
+}
+void FuseTest::expect_release(uint64_t ino, uint64_t fh)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_RELEASE &&
+ in.header.nodeid == ino &&
+ in.body.release.fh == fh);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(0)));
+}
+
+void FuseTest::expect_releasedir(uint64_t ino, ProcessMockerT r)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_RELEASEDIR &&
+ in.header.nodeid == ino &&
+ in.body.release.fh == FH);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(r));
+}
+
+void FuseTest::expect_unlink(uint64_t parent, const char *path, int error)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_UNLINK &&
+ 0 == strcmp(path, in.body.unlink) &&
+ in.header.nodeid == parent);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(error)));
+}
+
+void FuseTest::expect_write(uint64_t ino, uint64_t offset, uint64_t isize,
+ uint64_t osize, uint32_t flags_set, uint32_t flags_unset,
+ const void *contents)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *buf = (const char*)in.body.bytes +
+ sizeof(struct fuse_write_in);
+ bool pid_ok;
+ uint32_t wf = in.body.write.write_flags;
+
+ assert(isize <= sizeof(in.body.bytes) -
+ sizeof(struct fuse_write_in));
+ if (wf & FUSE_WRITE_CACHE)
+ pid_ok = true;
+ else
+ pid_ok = (pid_t)in.header.pid == getpid();
+
+ return (in.header.opcode == FUSE_WRITE &&
+ in.header.nodeid == ino &&
+ in.body.write.fh == FH &&
+ in.body.write.offset == offset &&
+ in.body.write.size == isize &&
+ pid_ok &&
+ (wf & flags_set) == flags_set &&
+ (wf & flags_unset) == 0 &&
+ (in.body.write.flags == O_WRONLY ||
+ in.body.write.flags == O_RDWR) &&
+ 0 == bcmp(buf, contents, isize));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = osize;
+ })));
+}
+
+void FuseTest::expect_write_7_8(uint64_t ino, uint64_t offset, uint64_t isize,
+ uint64_t osize, const void *contents)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *buf = (const char*)in.body.bytes +
+ FUSE_COMPAT_WRITE_IN_SIZE;
+ bool pid_ok = (pid_t)in.header.pid == getpid();
+
+ assert(isize <= sizeof(in.body.bytes) -
+ FUSE_COMPAT_WRITE_IN_SIZE);
+ return (in.header.opcode == FUSE_WRITE &&
+ in.header.nodeid == ino &&
+ in.body.write.fh == FH &&
+ in.body.write.offset == offset &&
+ in.body.write.size == isize &&
+ pid_ok &&
+ 0 == bcmp(buf, contents, isize));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = osize;
+ })));
+}
+
+void
+get_unprivileged_id(uid_t *uid, gid_t *gid)
+{
+ struct passwd *pw;
+ struct group *gr;
+
+ /*
+ * First try "tests", Kyua's default unprivileged user. XXX after
+ * GoogleTest gains a proper Kyua wrapper, get this with the Kyua API
+ */
+ pw = getpwnam("tests");
+ if (pw == NULL) {
+ /* Fall back to "nobody" */
+ pw = getpwnam("nobody");
+ }
+ if (pw == NULL)
+ GTEST_SKIP() << "Test requires an unprivileged user";
+ /* Use group "nobody", which is Kyua's default unprivileged group */
+ gr = getgrnam("nobody");
+ if (gr == NULL)
+ GTEST_SKIP() << "Test requires an unprivileged group";
+ *uid = pw->pw_uid;
+ *gid = gr->gr_gid;
+}
+
+void
+FuseTest::fork(bool drop_privs, int *child_status,
+ std::function<void()> parent_func,
+ std::function<int()> child_func)
+{
+ sem_t *sem;
+ int mprot = PROT_READ | PROT_WRITE;
+ int mflags = MAP_ANON | MAP_SHARED;
+ pid_t child;
+ uid_t uid;
+ gid_t gid;
+
+ if (drop_privs) {
+ get_unprivileged_id(&uid, &gid);
+ if (IsSkipped())
+ return;
+ }
+
+ sem = (sem_t*)mmap(NULL, sizeof(*sem), mprot, mflags, -1, 0);
+ ASSERT_NE(MAP_FAILED, sem) << strerror(errno);
+ ASSERT_EQ(0, sem_init(sem, 1, 0)) << strerror(errno);
+
+ if ((child = ::fork()) == 0) {
+ /* In child */
+ int err = 0;
+
+ if (sem_wait(sem)) {
+ perror("sem_wait");
+ err = 1;
+ goto out;
+ }
+
+ if (drop_privs && 0 != setegid(gid)) {
+ perror("setegid");
+ err = 1;
+ goto out;
+ }
+ if (drop_privs && 0 != setreuid(-1, uid)) {
+ perror("setreuid");
+ err = 1;
+ goto out;
+ }
+ err = child_func();
+
+out:
+ sem_destroy(sem);
+ _exit(err);
+ } else if (child > 0) {
+ /*
+ * In parent. Cleanup must happen here, because it's still
+ * privileged.
+ */
+ m_mock->m_child_pid = child;
+ ASSERT_NO_FATAL_FAILURE(parent_func());
+
+ /* Signal the child process to go */
+ ASSERT_EQ(0, sem_post(sem)) << strerror(errno);
+
+ ASSERT_LE(0, wait(child_status)) << strerror(errno);
+ } else {
+ FAIL() << strerror(errno);
+ }
+ munmap(sem, sizeof(*sem));
+ return;
+}
+
+void
+FuseTest::reclaim_vnode(const char *path)
+{
+ int err;
+
+ err = sysctlbyname(reclaim_mib, NULL, 0, path, strlen(path) + 1);
+ ASSERT_EQ(0, err) << strerror(errno);
+}
+
+static void usage(char* progname) {
+ fprintf(stderr, "Usage: %s [-v]\n\t-v increase verbosity\n", progname);
+ exit(2);
+}
+
+int main(int argc, char **argv) {
+ int ch;
+ FuseEnv *fuse_env = new FuseEnv;
+
+ InitGoogleTest(&argc, argv);
+ AddGlobalTestEnvironment(fuse_env);
+
+ while ((ch = getopt(argc, argv, "v")) != -1) {
+ switch (ch) {
+ case 'v':
+ verbosity++;
+ break;
+ default:
+ usage(argv[0]);
+ break;
+ }
+ }
+
+ return (RUN_ALL_TESTS());
+}
diff --git a/tests/sys/fs/fusefs/utils.hh b/tests/sys/fs/fusefs/utils.hh
new file mode 100644
index 000000000000..91bbba909672
--- /dev/null
+++ b/tests/sys/fs/fusefs/utils.hh
@@ -0,0 +1,277 @@
+/*-
+ * 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.
+ */
+
+struct _sem;
+typedef struct _sem sem_t;
+struct _dirdesc;
+typedef struct _dirdesc DIR;
+
+/* Nanoseconds to sleep, for tests that must */
+#define NAP_NS (100'000'000)
+
+void get_unprivileged_id(uid_t *uid, gid_t *gid);
+inline void nap()
+{
+ usleep(NAP_NS / 1000);
+}
+
+enum cache_mode {
+ Uncached,
+ Writethrough,
+ Writeback,
+ WritebackAsync
+};
+
+const char *cache_mode_to_s(enum cache_mode cm);
+bool is_unsafe_aio_enabled(void);
+
+extern const uint32_t libfuse_max_write;
+class FuseTest : public ::testing::Test {
+ protected:
+ uint32_t m_maxread;
+ uint32_t m_maxreadahead;
+ uint32_t m_maxwrite;
+ uint32_t m_init_flags;
+ bool m_allow_other;
+ bool m_default_permissions;
+ uint32_t m_kernel_minor_version;
+ enum poll_method m_pm;
+ bool m_noatime;
+ bool m_push_symlinks_in;
+ bool m_ro;
+ bool m_async;
+ bool m_noclusterr;
+ bool m_nointr;
+ bool m_no_auto_init;
+ unsigned m_time_gran;
+ MockFS *m_mock = NULL;
+ const static uint64_t FH = 0xdeadbeef1a7ebabe;
+ const char *reclaim_mib = "debug.try_reclaim_vnode";
+ const char *m_fsname;
+ const char *m_subtype;
+
+ public:
+ int m_maxbcachebuf;
+ unsigned long m_maxphys;
+
+ FuseTest():
+ m_maxread(0),
+ m_maxreadahead(0),
+ m_maxwrite(0),
+ m_init_flags(0),
+ m_allow_other(false),
+ m_default_permissions(false),
+ m_kernel_minor_version(FUSE_KERNEL_MINOR_VERSION),
+ m_pm(BLOCKING),
+ m_noatime(false),
+ m_push_symlinks_in(false),
+ m_ro(false),
+ m_async(false),
+ m_noclusterr(false),
+ m_nointr(false),
+ m_no_auto_init(false),
+ m_time_gran(1),
+ m_fsname(""),
+ m_subtype(""),
+ m_maxbcachebuf(0),
+ m_maxphys(0)
+ {}
+
+ virtual void SetUp();
+
+ virtual void TearDown() {
+ if (m_mock)
+ delete m_mock;
+ }
+
+ /*
+ * Create an expectation that FUSE_ACCESS will be called once for the
+ * given inode with the given access_mode, returning the given errno
+ */
+ void expect_access(uint64_t ino, mode_t access_mode, int error);
+
+ /* Expect FUSE_DESTROY and shutdown the daemon */
+ void expect_destroy(int error);
+
+ /*
+ * Create an expectation that FUSE_FALLOCATE will be called with the
+ * given inode, offset, length, and mode, exactly times times and
+ * returning error
+ */
+ void expect_fallocate(uint64_t ino, uint64_t offset, uint64_t length,
+ uint32_t mode, int error, int times=1);
+
+ /*
+ * Create an expectation that FUSE_FLUSH will be called times times for
+ * the given inode
+ */
+ void expect_flush(uint64_t ino, int times, ProcessMockerT r);
+
+ /*
+ * Create an expectation that FUSE_FORGET will be called for the given
+ * inode. There will be no response. If sem is provided, it will be
+ * posted after the operation is received by the daemon.
+ */
+ void expect_forget(uint64_t ino, uint64_t nlookup, sem_t *sem = NULL);
+
+ /*
+ * Create an expectation that FUSE_GETATTR will be called for the given
+ * inode any number of times. It will respond with a few basic
+ * attributes, like the given size and the mode S_IFREG | 0644
+ */
+ void expect_getattr(uint64_t ino, uint64_t size);
+
+ /*
+ * Create an expectation that FUSE_GETXATTR will be called once for the
+ * given inode.
+ */
+ void expect_getxattr(uint64_t ino, const char *attr, ProcessMockerT r);
+
+ /*
+ * Create an expectation that FUSE_LOOKUP will be called for the given
+ * path exactly times times and cache validity period. It will respond
+ * with inode ino, mode mode, filesize size.
+ */
+ void expect_lookup(const char *relpath, uint64_t ino, mode_t mode,
+ uint64_t size, int times, uint64_t attr_valid = UINT64_MAX,
+ uid_t uid = 0, gid_t gid = 0);
+
+ /* The protocol 7.8 version of expect_lookup */
+ void expect_lookup_7_8(const char *relpath, uint64_t ino, mode_t mode,
+ uint64_t size, int times, uint64_t attr_valid = UINT64_MAX,
+ uid_t uid = 0, gid_t gid = 0);
+
+ /*
+ * Create an expectation that FUSE_OPEN will be called for the given
+ * inode exactly times times. It will return with open_flags flags and
+ * file handle FH.
+ */
+ void expect_open(uint64_t ino, uint32_t flags, int times);
+
+ /*
+ * Create an expectation that FUSE_OPENDIR will be called exactly once
+ * for inode ino.
+ */
+ void expect_opendir(uint64_t ino);
+
+ /*
+ * Create an expectation that FUSE_READ will be called exactly once for
+ * the given inode, at offset offset and with size isize. It will
+ * return the first osize bytes from contents
+ *
+ * Protocol 7.8 tests can use this same expectation method because
+ * nothing currently validates the size of the fuse_read_in struct.
+ */
+ void expect_read(uint64_t ino, uint64_t offset, uint64_t isize,
+ uint64_t osize, const void *contents, int flags = -1,
+ uint64_t fh = FH);
+
+ /*
+ * Create an expectation that FUSE_READIR will be called any number of
+ * times on the given ino with the given offset, returning (by copy)
+ * the provided entries
+ */
+ void expect_readdir(uint64_t ino, uint64_t off,
+ std::vector<struct dirent> &ents);
+
+ /*
+ * Create an expectation that FUSE_RELEASE will be called exactly once
+ * for the given inode and filehandle, returning success
+ */
+ void expect_release(uint64_t ino, uint64_t fh);
+
+ /*
+ * Create an expectation that FUSE_RELEASEDIR will be called exactly
+ * once for the given inode
+ */
+ void expect_releasedir(uint64_t ino, ProcessMockerT r);
+
+ /*
+ * Create an expectation that FUSE_UNLINK will be called exactly once
+ * for the given path, returning an errno
+ */
+ void expect_unlink(uint64_t parent, const char *path, int error);
+
+ /*
+ * Create an expectation that FUSE_WRITE will be called exactly once
+ * for the given inode, at offset offset, with size isize and buffer
+ * contents. Any flags present in flags_set must be set, and any
+ * present in flags_unset must not be set. Other flags are don't care.
+ * It will return osize.
+ */
+ void expect_write(uint64_t ino, uint64_t offset, uint64_t isize,
+ uint64_t osize, uint32_t flags_set, uint32_t flags_unset,
+ const void *contents);
+
+ /* Protocol 7.8 version of expect_write */
+ void expect_write_7_8(uint64_t ino, uint64_t offset, uint64_t isize,
+ uint64_t osize, const void *contents);
+
+ /*
+ * Helper that runs code in a child process.
+ *
+ * First, parent_func runs in the parent process.
+ * Then, child_func runs in the child process, dropping privileges if
+ * desired.
+ * Finally, fusetest_fork returns.
+ *
+ * # Returns
+ *
+ * fusetest_fork may SKIP the test, which the caller should detect with
+ * the IsSkipped() method. If not, then the child's exit status will
+ * be returned in status.
+ */
+ void fork(bool drop_privs, int *status,
+ std::function<void()> parent_func,
+ std::function<int()> child_func);
+
+ /*
+ * Deliberately leak a file descriptor.
+ *
+ * Closing a file descriptor on fusefs would cause the server to
+ * receive FUSE_CLOSE and possibly FUSE_INACTIVE. Handling those
+ * operations would needlessly complicate most tests. So most tests
+ * deliberately leak the file descriptors instead. This method serves
+ * to document the leakage, and provide a single point of suppression
+ * for static analyzers.
+ */
+ /* coverity[+close: arg-0] */
+ static void leak(int fd __unused) {}
+
+ /*
+ * Deliberately leak a DIR* pointer
+ *
+ * See comments for FuseTest::leak
+ */
+ static void leakdir(DIR* dirp __unused) {}
+
+ /* Manually reclaim a vnode. Requires root privileges. */
+ void reclaim_vnode(const char *fullpath);
+};
diff --git a/tests/sys/fs/fusefs/write.cc b/tests/sys/fs/fusefs/write.cc
new file mode 100644
index 000000000000..f5573a865a04
--- /dev/null
+++ b/tests/sys/fs/fusefs/write.cc
@@ -0,0 +1,1662 @@
+/*-
+ * 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/param.h>
+#include <sys/mman.h>
+#include <sys/resource.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/uio.h>
+#include <sys/un.h>
+
+#include <aio.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Write: public FuseTest {
+
+public:
+void SetUp() {
+ FuseTest::SetUp();
+}
+
+void TearDown() {
+ struct sigaction sa;
+
+ bzero(&sa, sizeof(sa));
+ sa.sa_handler = SIG_DFL;
+ sigaction(SIGXFSZ, &sa, NULL);
+
+ FuseTest::TearDown();
+}
+
+void expect_lookup(const char *relpath, uint64_t ino, uint64_t size)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, size, 1);
+}
+
+void expect_release(uint64_t ino, ProcessMockerT r)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_RELEASE &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(r));
+}
+
+void expect_write(uint64_t ino, uint64_t offset, uint64_t isize,
+ uint64_t osize, const void *contents)
+{
+ FuseTest::expect_write(ino, offset, isize, osize, 0, 0, contents);
+}
+
+/* Expect a write that may or may not come, depending on the cache mode */
+void maybe_expect_write(uint64_t ino, uint64_t offset, uint64_t size,
+ const void *contents)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *buf = (const char*)in.body.bytes +
+ sizeof(struct fuse_write_in);
+
+ assert(size <= sizeof(in.body.bytes) -
+ sizeof(struct fuse_write_in));
+ return (in.header.opcode == FUSE_WRITE &&
+ in.header.nodeid == ino &&
+ in.body.write.offset == offset &&
+ in.body.write.size == size &&
+ 0 == bcmp(buf, contents, size));
+ }, Eq(true)),
+ _)
+ ).Times(AtMost(1))
+ .WillRepeatedly(Invoke(
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, write);
+ out.body.write.size = size;
+ })
+ ));
+}
+
+};
+
+class Write_7_8: public FuseTest {
+
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 8;
+ FuseTest::SetUp();
+}
+
+void expect_lookup(const char *relpath, uint64_t ino, uint64_t size)
+{
+ FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, size, 1);
+}
+
+};
+
+class AioWrite: public Write {
+virtual void SetUp() {
+ if (!is_unsafe_aio_enabled())
+ GTEST_SKIP() <<
+ "vfs.aio.enable_unsafe must be set for this test";
+ FuseTest::SetUp();
+}
+};
+
+/* Tests for the writeback cache mode */
+class WriteBack: public Write {
+public:
+virtual void SetUp() {
+ m_init_flags |= FUSE_WRITEBACK_CACHE;
+ FuseTest::SetUp();
+ if (IsSkipped())
+ return;
+}
+
+void expect_write(uint64_t ino, uint64_t offset, uint64_t isize,
+ uint64_t osize, const void *contents)
+{
+ FuseTest::expect_write(ino, offset, isize, osize, FUSE_WRITE_CACHE, 0,
+ contents);
+}
+};
+
+class WriteBackAsync: public WriteBack {
+public:
+virtual void SetUp() {
+ m_async = true;
+ m_maxwrite = 65536;
+ WriteBack::SetUp();
+}
+};
+
+class TimeGran: public WriteBackAsync, public WithParamInterface<unsigned> {
+public:
+virtual void SetUp() {
+ m_time_gran = 1 << GetParam();
+ WriteBackAsync::SetUp();
+}
+};
+
+/* Tests for clustered writes with WriteBack cacheing */
+class WriteCluster: public WriteBack {
+public:
+virtual void SetUp() {
+ m_async = true;
+ m_maxwrite = UINT32_MAX; // Anything larger than MAXPHYS will suffice
+ WriteBack::SetUp();
+ if (m_maxphys < 2 * DFLTPHYS)
+ GTEST_SKIP() << "MAXPHYS must be at least twice DFLTPHYS"
+ << " for this test";
+ if (m_maxphys < 2 * (unsigned long )m_maxbcachebuf)
+ GTEST_SKIP() << "MAXPHYS must be at least twice maxbcachebuf"
+ << " for this test";
+}
+};
+
+/* Tests relating to the server's max_write property */
+class WriteMaxWrite: public Write {
+public:
+virtual void SetUp() {
+ /*
+ * For this test, m_maxwrite must be less than either m_maxbcachebuf or
+ * maxphys.
+ */
+ m_maxwrite = 32768;
+ Write::SetUp();
+}
+};
+
+class WriteEofDuringVnopStrategy: public Write, public WithParamInterface<int>
+{};
+
+class WriteRlimitFsize: public Write, public WithParamInterface<int> {
+public:
+static sig_atomic_t s_sigxfsz;
+struct rlimit m_initial_limit;
+
+void SetUp() {
+ s_sigxfsz = 0;
+ getrlimit(RLIMIT_FSIZE, &m_initial_limit);
+ FuseTest::SetUp();
+}
+
+void TearDown() {
+ setrlimit(RLIMIT_FSIZE, &m_initial_limit);
+
+ FuseTest::TearDown();
+}
+};
+
+sig_atomic_t WriteRlimitFsize::s_sigxfsz = 0;
+
+void sigxfsz_handler(int __unused sig) {
+ WriteRlimitFsize::s_sigxfsz = 1;
+}
+
+/* AIO writes need to set the header's pid field correctly */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */
+TEST_F(AioWrite, DISABLED_aio_write)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ uint64_t offset = 4096;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ struct aiocb iocb, *piocb;
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_write(ino, offset, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ iocb.aio_nbytes = bufsize;
+ iocb.aio_fildes = fd;
+ iocb.aio_buf = __DECONST(void *, CONTENTS);
+ iocb.aio_offset = offset;
+ iocb.aio_sigevent.sigev_notify = SIGEV_NONE;
+ ASSERT_EQ(0, aio_write(&iocb)) << strerror(errno);
+ ASSERT_EQ(bufsize, aio_waitcomplete(&piocb, NULL)) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * When a file is opened with O_APPEND, we should forward that flag to
+ * FUSE_OPEN (tested by Open.o_append) but still attempt to calculate the
+ * offset internally. That way we'll work both with filesystems that
+ * understand O_APPEND (and ignore the offset) and filesystems that don't (and
+ * simply use the offset).
+ *
+ * Note that verifying the O_APPEND flag in FUSE_OPEN is done in the
+ * Open.o_append test.
+ */
+TEST_F(Write, append)
+{
+ const ssize_t BUFSIZE = 9;
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char CONTENTS[BUFSIZE] = "abcdefgh";
+ uint64_t ino = 42;
+ /*
+ * Set offset to a maxbcachebuf boundary so we don't need to RMW when
+ * using writeback caching
+ */
+ uint64_t initial_offset = m_maxbcachebuf;
+ int fd;
+
+ expect_lookup(RELPATH, ino, initial_offset);
+ expect_open(ino, 0, 1);
+ expect_write(ino, initial_offset, BUFSIZE, BUFSIZE, CONTENTS);
+
+ /* Must open O_RDWR or fuse(4) implicitly sets direct_io */
+ fd = open(FULLPATH, O_RDWR | O_APPEND);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(BUFSIZE, write(fd, CONTENTS, BUFSIZE)) << strerror(errno);
+ leak(fd);
+}
+
+/* If a file is cached, then appending to the end should not cause a read */
+TEST_F(Write, append_to_cached)
+{
+ const ssize_t BUFSIZE = 9;
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ char *oldcontents, *oldbuf;
+ const char CONTENTS[BUFSIZE] = "abcdefgh";
+ uint64_t ino = 42;
+ /*
+ * Set offset in between maxbcachebuf boundary to test buffer handling
+ */
+ uint64_t oldsize = m_maxbcachebuf / 2;
+ int fd;
+
+ oldcontents = new char[oldsize]();
+ oldbuf = new char[oldsize];
+
+ expect_lookup(RELPATH, ino, oldsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, oldsize, oldsize, oldcontents);
+ maybe_expect_write(ino, oldsize, BUFSIZE, CONTENTS);
+
+ /* Must open O_RDWR or fuse(4) implicitly sets direct_io */
+ fd = open(FULLPATH, O_RDWR | O_APPEND);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /* Read the old data into the cache */
+ ASSERT_EQ((ssize_t)oldsize, read(fd, oldbuf, oldsize))
+ << strerror(errno);
+
+ /* Write the new data. There should be no more read operations */
+ ASSERT_EQ(BUFSIZE, write(fd, CONTENTS, BUFSIZE)) << strerror(errno);
+ leak(fd);
+ delete[] oldbuf;
+ delete[] oldcontents;
+}
+
+TEST_F(Write, append_direct_io)
+{
+ const ssize_t BUFSIZE = 9;
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char CONTENTS[BUFSIZE] = "abcdefgh";
+ uint64_t ino = 42;
+ uint64_t initial_offset = 4096;
+ int fd;
+
+ expect_lookup(RELPATH, ino, initial_offset);
+ expect_open(ino, FOPEN_DIRECT_IO, 1);
+ expect_write(ino, initial_offset, BUFSIZE, BUFSIZE, CONTENTS);
+
+ fd = open(FULLPATH, O_WRONLY | O_APPEND);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(BUFSIZE, write(fd, CONTENTS, BUFSIZE)) << strerror(errno);
+ leak(fd);
+}
+
+/* A direct write should evict any overlapping cached data */
+TEST_F(Write, direct_io_evicts_cache)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char CONTENTS0[] = "abcdefgh";
+ const char CONTENTS1[] = "ijklmnop";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS0) + 1;
+ char readbuf[bufsize];
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS0);
+ expect_write(ino, 0, bufsize, bufsize, CONTENTS1);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ // Prime cache
+ ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno);
+
+ // Write directly, evicting cache
+ ASSERT_EQ(0, fcntl(fd, F_SETFL, O_DIRECT)) << strerror(errno);
+ ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS1, bufsize)) << strerror(errno);
+
+ // Read again. Cache should be bypassed
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS1);
+ ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno);
+ ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno);
+ ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno);
+ ASSERT_STREQ(readbuf, CONTENTS1);
+
+ leak(fd);
+}
+
+/*
+ * If the server doesn't return FOPEN_DIRECT_IO during FUSE_OPEN, then it's not
+ * allowed to return a short write for that file handle. However, if it does
+ * then we should still do our darndest to handle it by resending the unwritten
+ * portion.
+ */
+TEST_F(Write, indirect_io_short_write)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefghijklmnop";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ ssize_t bufsize0 = 11;
+ ssize_t bufsize1 = strlen(CONTENTS) - bufsize0;
+ const char *contents1 = CONTENTS + bufsize0;
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_write(ino, 0, bufsize, bufsize0, CONTENTS);
+ expect_write(ino, bufsize0, bufsize1, bufsize1, contents1);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ leak(fd);
+}
+
+/* It is an error if the daemon claims to have written more data than we sent */
+TEST_F(Write, indirect_io_long_write)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefghijklmnop";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ ssize_t bufsize_out = 100;
+ off_t some_other_size = 25;
+ struct stat sb;
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_write(ino, 0, bufsize, bufsize_out, CONTENTS);
+ expect_getattr(ino, some_other_size);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(-1, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ ASSERT_EQ(EINVAL, errno);
+
+ /*
+ * Following such an error, we should requery the server for the file's
+ * size.
+ */
+ fstat(fd, &sb);
+ ASSERT_EQ(sb.st_size, some_other_size);
+
+ leak(fd);
+}
+
+/*
+ * Don't crash if the server returns a write that can't be represented as a
+ * signed 32 bit number. Regression test for
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263263
+ */
+TEST_F(Write, indirect_io_very_long_write)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefghijklmnop";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ ssize_t bufsize_out = 3 << 30;
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_write(ino, 0, bufsize, bufsize_out, CONTENTS);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(-1, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ ASSERT_EQ(EINVAL, errno);
+ leak(fd);
+}
+
+/*
+ * When the direct_io option is used, filesystems are allowed to write less
+ * data than requested. We should return the short write to userland.
+ */
+TEST_F(Write, direct_io_short_write)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefghijklmnop";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ ssize_t halfbufsize = bufsize / 2;
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, FOPEN_DIRECT_IO, 1);
+ expect_write(ino, 0, bufsize, halfbufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(halfbufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * An insidious edge case: the filesystem returns a short write, and the
+ * difference between what we requested and what it actually wrote crosses an
+ * iov element boundary
+ */
+TEST_F(Write, direct_io_short_write_iov)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS0 = "abcdefgh";
+ const char *CONTENTS1 = "ijklmnop";
+ const char *EXPECTED0 = "abcdefghijklmnop";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t size0 = strlen(CONTENTS0) - 1;
+ ssize_t size1 = strlen(CONTENTS1) + 1;
+ ssize_t totalsize = size0 + size1;
+ struct iovec iov[2];
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, FOPEN_DIRECT_IO, 1);
+ expect_write(ino, 0, totalsize, size0, EXPECTED0);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ iov[0].iov_base = __DECONST(void*, CONTENTS0);
+ iov[0].iov_len = strlen(CONTENTS0);
+ iov[1].iov_base = __DECONST(void*, CONTENTS1);
+ iov[1].iov_len = strlen(CONTENTS1);
+ ASSERT_EQ(size0, writev(fd, iov, 2)) << strerror(errno);
+ leak(fd);
+}
+
+/* fusefs should respect RLIMIT_FSIZE */
+TEST_P(WriteRlimitFsize, rlimit_fsize)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ struct rlimit rl;
+ ssize_t bufsize = strlen(CONTENTS);
+ off_t offset = 1'000'000'000;
+ uint64_t ino = 42;
+ int fd, oflag;
+
+ oflag = GetParam();
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+
+ rl.rlim_cur = offset;
+ rl.rlim_max = m_initial_limit.rlim_max;
+ ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno);
+ ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno);
+
+ fd = open(FULLPATH, O_WRONLY | oflag);
+
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(-1, pwrite(fd, CONTENTS, bufsize, offset));
+ EXPECT_EQ(EFBIG, errno);
+ EXPECT_EQ(1, s_sigxfsz);
+ leak(fd);
+}
+
+/*
+ * When crossing the RLIMIT_FSIZE boundary, writes should be truncated, not
+ * aborted.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=164793
+ */
+TEST_P(WriteRlimitFsize, rlimit_fsize_truncate)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefghijklmnopqrstuvwxyz";
+ struct rlimit rl;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint64_t ino = 42;
+ off_t offset = 1 << 30;
+ off_t limit = offset + strlen(CONTENTS) / 2;
+ int fd, oflag;
+
+ oflag = GetParam();
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_write(ino, offset, bufsize / 2, bufsize / 2, CONTENTS);
+
+ rl.rlim_cur = limit;
+ rl.rlim_max = m_initial_limit.rlim_max;
+ ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno);
+ ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno);
+
+ fd = open(FULLPATH, O_WRONLY | oflag);
+
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize / 2, pwrite(fd, CONTENTS, bufsize, offset))
+ << strerror(errno);
+ leak(fd);
+}
+
+INSTANTIATE_TEST_SUITE_P(W, WriteRlimitFsize,
+ Values(0, O_DIRECT)
+);
+
+/*
+ * A short read indicates EOF. Test that nothing bad happens if we get EOF
+ * during the R of a RMW operation.
+ */
+TEST_F(Write, eof_during_rmw)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ const char *INITIAL = "XXXXXXXXXX";
+ uint64_t ino = 42;
+ uint64_t offset = 1;
+ ssize_t bufsize = strlen(CONTENTS) + 1;
+ off_t orig_fsize = 10;
+ off_t truncated_fsize = 5;
+ int fd;
+
+ FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, orig_fsize, 1);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, orig_fsize, truncated_fsize, INITIAL, O_RDWR);
+ maybe_expect_write(ino, offset, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, pwrite(fd, CONTENTS, bufsize, offset))
+ << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * VOP_STRATEGY should not query the server for the file's size, even if its
+ * cached attributes have expired.
+ * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=256937
+ */
+TEST_P(WriteEofDuringVnopStrategy, eof_during_vop_strategy)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ Sequence seq;
+ const off_t filesize = 2 * m_maxbcachebuf;
+ char *contents;
+ uint64_t ino = 42;
+ uint64_t attr_valid = 0;
+ uint64_t attr_valid_nsec = 0;
+ mode_t mode = S_IFREG | 0644;
+ int fd;
+ int ngetattrs;
+
+ ngetattrs = GetParam();
+ contents = new char[filesize]();
+
+ 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 = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr.size = filesize;
+ out.body.entry.attr_valid = attr_valid;
+ out.body.entry.attr_valid_nsec = attr_valid_nsec;
+ })));
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(Between(ngetattrs - 1, ngetattrs))
+ .InSequence(seq)
+ .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_valid = attr_valid;
+ out.body.attr.attr_valid_nsec = attr_valid_nsec;
+ out.body.attr.attr.size = filesize;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .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_valid = attr_valid;
+ out.body.attr.attr_valid_nsec = attr_valid_nsec;
+ out.body.attr.attr.size = filesize / 2;
+ })));
+ expect_write(ino, 0, filesize / 2, filesize / 2, contents);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(filesize / 2, write(fd, contents, filesize / 2))
+ << strerror(errno);
+
+}
+
+INSTANTIATE_TEST_SUITE_P(W, WriteEofDuringVnopStrategy,
+ Values(1, 2, 3)
+);
+
+/*
+ * If the kernel cannot be sure which uid, gid, or pid was responsible for a
+ * write, then it must set the FUSE_WRITE_CACHE bit
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236378 */
+TEST_F(Write, mmap)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ void *p;
+ uint64_t offset = 10;
+ size_t len;
+ char *zeros, *expected;
+
+ len = getpagesize();
+
+ zeros = new char[len]();
+ expected = new char[len]();
+ memmove((uint8_t*)expected + offset, CONTENTS, bufsize);
+
+ expect_lookup(RELPATH, ino, len);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, len, len, zeros);
+ /*
+ * Writes from the pager may or may not be associated with the correct
+ * pid, so they must set FUSE_WRITE_CACHE.
+ */
+ FuseTest::expect_write(ino, 0, len, len, FUSE_WRITE_CACHE, 0, expected);
+ expect_flush(ino, 1, ReturnErrno(0));
+ expect_release(ino, ReturnErrno(0));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ p = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ ASSERT_NE(MAP_FAILED, p) << strerror(errno);
+
+ memmove((uint8_t*)p + offset, CONTENTS, bufsize);
+
+ ASSERT_EQ(0, munmap(p, len)) << strerror(errno);
+ close(fd); // Write mmap'd data on close
+
+ delete[] expected;
+ delete[] zeros;
+
+ leak(fd);
+}
+
+TEST_F(Write, pwrite)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ uint64_t offset = m_maxbcachebuf;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_write(ino, offset, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, pwrite(fd, CONTENTS, bufsize, offset))
+ << strerror(errno);
+ leak(fd);
+}
+
+/* Writing a file should update its cached mtime and ctime */
+TEST_F(Write, timestamps)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ ssize_t bufsize = strlen(CONTENTS);
+ uint64_t ino = 42;
+ struct stat sb0, sb1;
+ int fd;
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ maybe_expect_write(ino, 0, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, fstat(fd, &sb0)) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+
+ nap();
+
+ ASSERT_EQ(0, fstat(fd, &sb1)) << strerror(errno);
+
+ EXPECT_EQ(sb0.st_atime, sb1.st_atime);
+ EXPECT_NE(sb0.st_mtime, sb1.st_mtime);
+ EXPECT_NE(sb0.st_ctime, sb1.st_ctime);
+
+ leak(fd);
+}
+
+TEST_F(Write, write)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_write(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ leak(fd);
+}
+
+/* fuse(4) should not issue writes of greater size than the daemon requests */
+TEST_F(WriteMaxWrite, write)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ int *contents;
+ uint64_t ino = 42;
+ int fd;
+ ssize_t halfbufsize, bufsize;
+
+ halfbufsize = m_mock->m_maxwrite;
+ if (halfbufsize >= m_maxbcachebuf ||
+ (unsigned long )halfbufsize >= m_maxphys)
+ GTEST_SKIP() << "Must lower m_maxwrite for this test";
+ bufsize = halfbufsize * 2;
+ contents = new int[bufsize / sizeof(int)];
+ for (int i = 0; i < (int)bufsize / (int)sizeof(i); i++) {
+ contents[i] = i;
+ }
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ maybe_expect_write(ino, 0, halfbufsize, contents);
+ maybe_expect_write(ino, halfbufsize, halfbufsize,
+ &contents[halfbufsize / sizeof(int)]);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, contents, bufsize)) << strerror(errno);
+ leak(fd);
+
+ delete[] contents;
+}
+
+TEST_F(Write, write_nothing)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = 0;
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ leak(fd);
+}
+
+TEST_F(Write_7_8, write)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_write_7_8(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ leak(fd);
+}
+
+/* In writeback mode, dirty data should be written on close */
+TEST_F(WriteBackAsync, close)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_write(ino, 0, bufsize, bufsize, CONTENTS);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_SETATTR);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = ino; // Must match nodeid
+ })));
+ expect_flush(ino, 1, ReturnErrno(0));
+ expect_release(ino, ReturnErrno(0));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ close(fd);
+}
+
+/* In writeback mode, adjacent writes will be clustered together */
+TEST_F(WriteCluster, clustering)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int i, fd;
+ char *wbuf, *wbuf2x;
+ ssize_t bufsize = m_maxbcachebuf;
+ off_t filesize = 5 * bufsize;
+
+ wbuf = new char[bufsize];
+ memset(wbuf, 'X', bufsize);
+ wbuf2x = new char[2 * bufsize];
+ memset(wbuf2x, 'X', 2 * bufsize);
+
+ expect_lookup(RELPATH, ino, filesize);
+ expect_open(ino, 0, 1);
+ /*
+ * Writes of bufsize-bytes each should be clustered into greater sizes.
+ * The amount of clustering is adaptive, so the first write actually
+ * issued will be 2x bufsize and subsequent writes may be larger
+ */
+ expect_write(ino, 0, 2 * bufsize, 2 * bufsize, wbuf2x);
+ expect_write(ino, 2 * bufsize, 2 * bufsize, 2 * bufsize, wbuf2x);
+ expect_flush(ino, 1, ReturnErrno(0));
+ expect_release(ino, ReturnErrno(0));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ for (i = 0; i < 4; i++) {
+ ASSERT_EQ(bufsize, write(fd, wbuf, bufsize))
+ << strerror(errno);
+ }
+ close(fd);
+ delete[] wbuf2x;
+ delete[] wbuf;
+}
+
+/*
+ * When clustering writes, an I/O error to any of the cluster's children should
+ * not panic the system on unmount
+ */
+/*
+ * Regression test for bug 238585
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=238565
+ */
+TEST_F(WriteCluster, cluster_write_err)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int i, fd;
+ char *wbuf;
+ ssize_t bufsize = m_maxbcachebuf;
+ off_t filesize = 4 * bufsize;
+
+ wbuf = new char[bufsize];
+ memset(wbuf, 'X', bufsize);
+
+ expect_lookup(RELPATH, ino, filesize);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_WRITE);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnErrno(EIO)));
+ expect_flush(ino, 1, ReturnErrno(0));
+ expect_release(ino, ReturnErrno(0));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ for (i = 0; i < 3; i++) {
+ ASSERT_EQ(bufsize, write(fd, wbuf, bufsize))
+ << strerror(errno);
+ }
+ close(fd);
+ delete[] wbuf;
+}
+
+/*
+ * In writeback mode, writes to an O_WRONLY file could trigger reads from the
+ * server. The FUSE protocol explicitly allows that.
+ */
+TEST_F(WriteBack, rmw)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ const char *INITIAL = "XXXXXXXXXX";
+ uint64_t ino = 42;
+ uint64_t offset = 1;
+ off_t fsize = 10;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+
+ FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, fsize, fsize, INITIAL, O_WRONLY);
+ maybe_expect_write(ino, offset, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, pwrite(fd, CONTENTS, bufsize, offset))
+ << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * Without direct_io, writes should be committed to cache
+ */
+TEST_F(WriteBack, cache)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t readbuf[bufsize];
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_write(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ /*
+ * A subsequent read should be serviced by cache, without querying the
+ * filesystem daemon
+ */
+ ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno);
+ ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * With O_DIRECT, writes should be not committed to cache. Admittedly this is
+ * an odd test, because it would be unusual to use O_DIRECT for writes but not
+ * reads.
+ */
+TEST_F(WriteBack, o_direct)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t readbuf[bufsize];
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ FuseTest::expect_write(ino, 0, bufsize, bufsize, 0, FUSE_WRITE_CACHE,
+ CONTENTS);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDWR | O_DIRECT);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ /* A subsequent read must query the daemon because cache is empty */
+ ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno);
+ ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno);
+ ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno);
+ leak(fd);
+}
+
+TEST_F(WriteBack, direct_io)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t readbuf[bufsize];
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, FOPEN_DIRECT_IO, 1);
+ FuseTest::expect_write(ino, 0, bufsize, bufsize, 0, FUSE_WRITE_CACHE,
+ CONTENTS);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ /* A subsequent read must query the daemon because cache is empty */
+ ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno);
+ ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno);
+ ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * mmap should still be possible even if the server used direct_io. Mmap will
+ * still use the cache, though.
+ *
+ * Regression test for bug 247276
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=247276
+ */
+TEST_F(WriteBack, mmap_direct_io)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ size_t len;
+ ssize_t bufsize = strlen(CONTENTS);
+ char *zeros;
+ void *p;
+
+ len = getpagesize();
+ zeros = new char[len]();
+
+ expect_lookup(RELPATH, ino, len);
+ expect_open(ino, FOPEN_DIRECT_IO, 1);
+ expect_read(ino, 0, len, len, zeros);
+ expect_flush(ino, 1, ReturnErrno(0));
+ FuseTest::expect_write(ino, 0, len, len, FUSE_WRITE_CACHE, 0, zeros);
+ expect_release(ino, ReturnErrno(0));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ p = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ ASSERT_NE(MAP_FAILED, p) << strerror(errno);
+
+ memmove((uint8_t*)p, CONTENTS, bufsize);
+
+ ASSERT_EQ(0, munmap(p, len)) << strerror(errno);
+ close(fd); // Write mmap'd data on close
+
+ delete[] zeros;
+}
+
+/*
+ * When mounted with -o async, the writeback cache mode should delay writes
+ */
+TEST_F(WriteBackAsync, delay)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ /* Write should be cached, but FUSE_WRITE shouldn't be sent */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_WRITE);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+
+ /* Don't close the file because that would flush the cache */
+ leak(fd);
+}
+
+/*
+ * A direct write should not evict dirty cached data from outside of its own
+ * byte range.
+ */
+TEST_F(WriteBackAsync, direct_io_ignores_unrelated_cached)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char CONTENTS0[] = "abcdefgh";
+ const char CONTENTS1[] = "ijklmnop";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS0) + 1;
+ ssize_t fsize = 2 * m_maxbcachebuf;
+ char readbuf[bufsize];
+ char *zeros;
+
+ zeros = new char[m_maxbcachebuf]();
+
+ expect_lookup(RELPATH, ino, fsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, m_maxbcachebuf, m_maxbcachebuf, zeros);
+ FuseTest::expect_write(ino, m_maxbcachebuf, bufsize, bufsize, 0, 0,
+ CONTENTS1);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ // Cache first block with dirty data. This will entail first reading
+ // the existing data.
+ ASSERT_EQ(bufsize, pwrite(fd, CONTENTS0, bufsize, 0))
+ << strerror(errno);
+
+ // Write directly to second block
+ ASSERT_EQ(0, fcntl(fd, F_SETFL, O_DIRECT)) << strerror(errno);
+ ASSERT_EQ(bufsize, pwrite(fd, CONTENTS1, bufsize, m_maxbcachebuf))
+ << strerror(errno);
+
+ // Read from the first block again. Should be serviced by cache.
+ ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno);
+ ASSERT_EQ(bufsize, pread(fd, readbuf, bufsize, 0)) << strerror(errno);
+ ASSERT_STREQ(readbuf, CONTENTS0);
+
+ leak(fd);
+ delete[] zeros;
+}
+
+/*
+ * If a direct io write partially overlaps one or two blocks of dirty cached
+ * data, No dirty data should be lost. Admittedly this is a weird test,
+ * because it would be unusual to use O_DIRECT and the writeback cache.
+ */
+TEST_F(WriteBackAsync, direct_io_partially_overlaps_cached_block)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+ off_t bs = m_maxbcachebuf;
+ ssize_t fsize = 3 * bs;
+ char *readbuf, *zeros, *ones, *zeroones, *onezeros;
+
+ readbuf = new char[bs];
+ zeros = new char[3 * bs]();
+ ones = new char[2 * bs];
+ memset(ones, 1, 2 * bs);
+ zeroones = new char[bs]();
+ memset((uint8_t*)zeroones + bs / 2, 1, bs / 2);
+ onezeros = new char[bs]();
+ memset(onezeros, 1, bs / 2);
+
+ expect_lookup(RELPATH, ino, fsize);
+ expect_open(ino, 0, 1);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /* Cache first and third blocks with dirty data. */
+ ASSERT_EQ(3 * bs, pwrite(fd, zeros, 3 * bs, 0)) << strerror(errno);
+
+ /*
+ * Write directly to all three blocks. The partially written blocks
+ * will be flushed because they're dirty.
+ */
+ FuseTest::expect_write(ino, 0, bs, bs, 0, 0, zeros);
+ FuseTest::expect_write(ino, 2 * bs, bs, bs, 0, 0, zeros);
+ /* The direct write is split in two because of the m_maxwrite value */
+ FuseTest::expect_write(ino, bs / 2, bs, bs, 0, 0, ones);
+ FuseTest::expect_write(ino, 3 * bs / 2, bs, bs, 0, 0, ones);
+ ASSERT_EQ(0, fcntl(fd, F_SETFL, O_DIRECT)) << strerror(errno);
+ ASSERT_EQ(2 * bs, pwrite(fd, ones, 2 * bs, bs / 2)) << strerror(errno);
+
+ /*
+ * Read from both the valid and invalid portions of the first and third
+ * blocks again. This will entail FUSE_READ operations because these
+ * blocks were invalidated by the direct write.
+ */
+ expect_read(ino, 0, bs, bs, zeroones);
+ expect_read(ino, 2 * bs, bs, bs, onezeros);
+ ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno);
+ ASSERT_EQ(bs / 2, pread(fd, readbuf, bs / 2, 0)) << strerror(errno);
+ EXPECT_EQ(0, memcmp(zeros, readbuf, bs / 2));
+ ASSERT_EQ(bs / 2, pread(fd, readbuf, bs / 2, 5 * bs / 2))
+ << strerror(errno);
+ EXPECT_EQ(0, memcmp(zeros, readbuf, bs / 2));
+ ASSERT_EQ(bs / 2, pread(fd, readbuf, bs / 2, bs / 2))
+ << strerror(errno);
+ EXPECT_EQ(0, memcmp(ones, readbuf, bs / 2));
+ ASSERT_EQ(bs / 2, pread(fd, readbuf, bs / 2, 2 * bs))
+ << strerror(errno);
+ EXPECT_EQ(0, memcmp(ones, readbuf, bs / 2));
+
+ leak(fd);
+ delete[] zeroones;
+ delete[] onezeros;
+ delete[] ones;
+ delete[] zeros;
+ delete[] readbuf;
+}
+
+/*
+ * In WriteBack mode, writes may be cached beyond what the server thinks is the
+ * EOF. In this case, a short read at EOF should _not_ cause fusefs to update
+ * the file's size.
+ */
+TEST_F(WriteBackAsync, eof)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS0 = "abcdefgh";
+ const char *CONTENTS1 = "ijklmnop";
+ uint64_t ino = 42;
+ int fd;
+ off_t offset = m_maxbcachebuf;
+ ssize_t wbufsize = strlen(CONTENTS1);
+ off_t old_filesize = (off_t)strlen(CONTENTS0);
+ ssize_t rbufsize = 2 * old_filesize;
+ char readbuf[rbufsize];
+ size_t holesize = rbufsize - old_filesize;
+ char hole[holesize];
+ struct stat sb;
+ ssize_t r;
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, m_maxbcachebuf, old_filesize, CONTENTS0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /* Write and cache data beyond EOF */
+ ASSERT_EQ(wbufsize, pwrite(fd, CONTENTS1, wbufsize, offset))
+ << strerror(errno);
+
+ /* Read from the old EOF */
+ r = pread(fd, readbuf, rbufsize, 0);
+ ASSERT_LE(0, r) << strerror(errno);
+ EXPECT_EQ(rbufsize, r) << "read should've synthesized a hole";
+ EXPECT_EQ(0, memcmp(CONTENTS0, readbuf, old_filesize));
+ bzero(hole, holesize);
+ EXPECT_EQ(0, memcmp(hole, readbuf + old_filesize, holesize));
+
+ /* The file's size should still be what was established by pwrite */
+ ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
+ EXPECT_EQ(offset + wbufsize, sb.st_size);
+ leak(fd);
+}
+
+/*
+ * Nothing bad should happen if a file with a dirty writeback cache is closed
+ * while the last copy lies in some socket's socket buffer. Inspired by bug
+ * 289686 .
+ */
+TEST_F(WriteBackAsync, scm_rights)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ int s[2];
+ struct msghdr msg;
+ struct iovec iov;
+ char message[CMSG_SPACE(sizeof(int))];
+ union {
+ char buf[CMSG_SPACE(sizeof(fd))];
+ struct cmsghdr align;
+ } u;
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ /* VOP_SETATTR will try to set timestamps during flush */
+ 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_IFREG | 0644;
+ out.body.attr.attr.size = bufsize;
+ })));
+
+ expect_write(ino, 0, bufsize, bufsize, CONTENTS);
+ expect_flush(ino, 1, ReturnErrno(0));
+ expect_release(ino, ReturnErrno(0));
+
+ /* Open a file on the fusefs file system */
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ /* Write to the file to dirty its writeback cache */
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+
+ /* Send the file into a socket */
+ ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, s)) << strerror(errno);
+ memset(&message, 0, sizeof(message));
+ memset(&msg, 0, sizeof(msg));
+ iov.iov_base = NULL;
+ iov.iov_len = 0;
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+ msg.msg_control = u.buf,
+ msg.msg_controllen = sizeof(u.buf);
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(fd));
+ memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));
+ ASSERT_GE(sendmsg(s[0], &msg, 0), 0) << strerror(errno);
+
+ close(fd); // Close fd within our process
+ close(s[0]);
+ close(s[1]); // The last copy of fd is within this socket's rcvbuf
+}
+
+/*
+ * When a file has dirty writes that haven't been flushed, the server's notion
+ * of its mtime and ctime will be wrong. The kernel should ignore those if it
+ * gets them from a FUSE_GETATTR before flushing.
+ */
+TEST_F(WriteBackAsync, timestamps)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ ssize_t bufsize = strlen(CONTENTS);
+ uint64_t ino = 42;
+ uint64_t attr_valid = 0;
+ uint64_t attr_valid_nsec = 0;
+ uint64_t server_time = 12345;
+ mode_t mode = S_IFREG | 0644;
+ int fd;
+
+ struct stat sb;
+
+ 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 = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.nlink = 1;
+ out.body.entry.attr_valid = attr_valid;
+ out.body.entry.attr_valid_nsec = attr_valid_nsec;
+ })));
+ expect_open(ino, 0, 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_valid = attr_valid;
+ out.body.attr.attr_valid_nsec = attr_valid_nsec;
+ out.body.attr.attr.atime = server_time;
+ out.body.attr.attr.mtime = server_time;
+ out.body.attr.attr.ctime = server_time;
+ })));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+
+ ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
+ EXPECT_EQ((time_t)server_time, sb.st_atime);
+ EXPECT_NE((time_t)server_time, sb.st_mtime);
+ EXPECT_NE((time_t)server_time, sb.st_ctime);
+
+ leak(fd);
+}
+
+/* Any dirty timestamp fields should be flushed during a SETATTR */
+TEST_F(WriteBackAsync, timestamps_during_setattr)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ ssize_t bufsize = strlen(CONTENTS);
+ uint64_t ino = 42;
+ const mode_t newmode = 0755;
+ int fd;
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ uint32_t valid = FATTR_MODE | FATTR_MTIME | FATTR_CTIME;
+ 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;
+ out.body.attr.attr.mode = S_IFREG | newmode;
+ })));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno);
+
+ leak(fd);
+}
+
+/* fuse_init_out.time_gran controls the granularity of timestamps */
+TEST_P(TimeGran, timestamps_during_setattr)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ ssize_t bufsize = strlen(CONTENTS);
+ uint64_t ino = 42;
+ const mode_t newmode = 0755;
+ int fd;
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ uint32_t valid = FATTR_MODE | FATTR_MTIME | FATTR_CTIME;
+ return (in.header.opcode == FUSE_SETATTR &&
+ in.header.nodeid == ino &&
+ in.body.setattr.valid == valid &&
+ in.body.setattr.mtimensec % m_time_gran == 0 &&
+ in.body.setattr.ctimensec % m_time_gran == 0);
+ }, 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;
+ })));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno);
+
+ leak(fd);
+}
+
+INSTANTIATE_TEST_SUITE_P(RA, TimeGran, Range(0u, 10u));
+
+/*
+ * Without direct_io, writes should be committed to cache
+ */
+TEST_F(Write, writethrough)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+ uint8_t readbuf[bufsize];
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_write(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ /*
+ * A subsequent read should be serviced by cache, without querying the
+ * filesystem daemon
+ */
+ ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno);
+ ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno);
+ leak(fd);
+}
+
+/* Writes that extend a file should update the cached file size */
+TEST_F(Write, update_file_size)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const char *CONTENTS = "abcdefgh";
+ struct stat sb;
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_write(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ /* Get cached attributes */
+ ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
+ ASSERT_EQ(bufsize, sb.st_size);
+ leak(fd);
+}
diff --git a/tests/sys/fs/fusefs/xattr.cc b/tests/sys/fs/fusefs/xattr.cc
new file mode 100644
index 000000000000..0ab203c96254
--- /dev/null
+++ b/tests/sys/fs/fusefs/xattr.cc
@@ -0,0 +1,891 @@
+/*-
+ * 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 all things relating to extended attributes and FUSE */
+
+extern "C" {
+#include <sys/types.h>
+#include <sys/extattr.h>
+#include <sys/wait.h>
+#include <semaphore.h>
+#include <signal.h>
+#include <string.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+const char FULLPATH[] = "mountpoint/some_file.txt";
+const char RELPATH[] = "some_file.txt";
+static sem_t killer_semaphore;
+
+void* killer(void* target) {
+ pid_t pid = *(pid_t*)target;
+ sem_wait(&killer_semaphore);
+ if (verbosity > 1)
+ printf("Killing! pid %d\n", pid);
+ kill(pid, SIGINT);
+
+ return(NULL);
+}
+
+class Xattr: public FuseTest {
+public:
+void expect_listxattr(uint64_t ino, uint32_t size, ProcessMockerT r,
+ Sequence *seq = NULL)
+{
+ if (seq == NULL) {
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LISTXATTR &&
+ in.header.nodeid == ino &&
+ in.body.listxattr.size == size);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(r))
+ .RetiresOnSaturation();
+ } else {
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LISTXATTR &&
+ in.header.nodeid == ino &&
+ in.body.listxattr.size == size);
+ }, Eq(true)),
+ _)
+ ).InSequence(*seq)
+ .WillOnce(Invoke(r))
+ .RetiresOnSaturation();
+ }
+}
+
+void expect_removexattr(uint64_t ino, const char *attr, int error)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *a = (const char*)in.body.bytes;
+ return (in.header.opcode == FUSE_REMOVEXATTR &&
+ in.header.nodeid == ino &&
+ 0 == strcmp(attr, a));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(error)));
+}
+
+void expect_setxattr(uint64_t ino, const char *attr, const char *value,
+ ProcessMockerT r)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *a = (const char*)in.body.bytes +
+ sizeof(fuse_setxattr_in);
+ const char *v = a + strlen(a) + 1;
+ return (in.header.opcode == FUSE_SETXATTR &&
+ in.header.nodeid == ino &&
+ in.body.setxattr.size == (strlen(value) + 1) &&
+ in.body.setxattr.setxattr_flags == 0 &&
+ 0 == strcmp(attr, a) &&
+ 0 == strcmp(value, v));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(r));
+}
+
+};
+
+class Xattr_7_32:public FuseTest {
+public:
+virtual void SetUp()
+{
+ m_kernel_minor_version = 32;
+ FuseTest::SetUp();
+}
+
+void expect_setxattr_7_32(uint64_t ino, const char *attr, const char *value,
+ ProcessMockerT r)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *a = (const char *)in.body.bytes +
+ FUSE_COMPAT_SETXATTR_IN_SIZE;
+ const char *v = a + strlen(a) + 1;
+ return (in.header.opcode == FUSE_SETXATTR &&
+ in.header.nodeid == ino &&
+ in.body.setxattr.size == (strlen(value) + 1) &&
+ 0 == strcmp(attr, a) &&
+ 0 == strcmp(value, v));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(r));
+}
+};
+
+class Getxattr: public Xattr {};
+
+class Listxattr: public Xattr {};
+
+/* Listxattr tests that need to use a signal */
+class ListxattrSig: public Listxattr {
+public:
+pthread_t m_killer_th;
+pid_t m_child;
+
+void SetUp() {
+ /*
+ * Mount with -o nointr so the mount can't get interrupted while
+ * waiting for a response from the server
+ */
+ m_nointr = true;
+ FuseTest::SetUp();
+
+ ASSERT_EQ(0, sem_init(&killer_semaphore, 0, 0)) << strerror(errno);
+}
+
+void TearDown() {
+ if (m_killer_th != NULL) {
+ pthread_join(m_killer_th, NULL);
+ }
+
+ sem_destroy(&killer_semaphore);
+
+ FuseTest::TearDown();
+}
+};
+
+class Removexattr: public Xattr {};
+class Setxattr: public Xattr {};
+class Setxattr_7_32:public Xattr_7_32 {};
+class RofsXattr: public Xattr {
+public:
+virtual void SetUp() {
+ m_ro = true;
+ Xattr::SetUp();
+}
+};
+
+/*
+ * If the extended attribute does not exist on this file, the daemon should
+ * return ENOATTR (ENODATA on Linux, but it's up to the daemon to choose the
+ * correct errror code)
+ */
+TEST_F(Getxattr, enoattr)
+{
+ char data[80];
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+ ssize_t r;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_getxattr(ino, "user.foo", ReturnErrno(ENOATTR));
+
+ r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
+ ASSERT_EQ(-1, r);
+ ASSERT_EQ(ENOATTR, errno);
+}
+
+/*
+ * If the filesystem returns ENOSYS, then it will be treated as a permanent
+ * failure and all future VOP_GETEXTATTR calls will fail with EOPNOTSUPP
+ * without querying the filesystem daemon
+ */
+TEST_F(Getxattr, enosys)
+{
+ char data[80];
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+ ssize_t r;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
+ expect_getxattr(ino, "user.foo", ReturnErrno(ENOSYS));
+
+ r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
+ ASSERT_EQ(-1, r);
+ EXPECT_EQ(EOPNOTSUPP, errno);
+
+ /* Subsequent attempts should not query the filesystem at all */
+ r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
+ ASSERT_EQ(-1, r);
+ EXPECT_EQ(EOPNOTSUPP, errno);
+}
+
+/*
+ * On FreeBSD, if the user passes an insufficiently large buffer then the
+ * filesystem is supposed to copy as much of the attribute's value as will fit.
+ *
+ * On Linux, however, the filesystem is supposed to return ERANGE.
+ *
+ * libfuse specifies the Linux behavior. However, that's probably an error.
+ * It would probably be correct for the filesystem to use platform-dependent
+ * behavior.
+ *
+ * This test case covers a filesystem that uses the Linux behavior
+ * TODO: require FreeBSD Behavior.
+ */
+TEST_F(Getxattr, erange)
+{
+ char data[10];
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+ ssize_t r;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_getxattr(ino, "user.foo", ReturnErrno(ERANGE));
+
+ r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
+ ASSERT_EQ(-1, r);
+ ASSERT_EQ(ERANGE, errno);
+}
+
+/*
+ * If the user passes a 0-length buffer, then the daemon should just return the
+ * size of the attribute
+ */
+TEST_F(Getxattr, size_only)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_getxattr(ino, "user.foo",
+ ReturnImmediate([](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, getxattr);
+ out.body.getxattr.size = 99;
+ })
+ );
+
+ ASSERT_EQ(99, extattr_get_file(FULLPATH, ns, "foo", NULL, 0))
+ << strerror(errno);;
+}
+
+/*
+ * Successfully get an attribute from the system namespace
+ */
+TEST_F(Getxattr, system)
+{
+ uint64_t ino = 42;
+ char data[80];
+ const char value[] = "whatever";
+ ssize_t value_len = strlen(value) + 1;
+ int ns = EXTATTR_NAMESPACE_SYSTEM;
+ ssize_t r;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_getxattr(ino, "system.foo",
+ 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);
+}
+
+/*
+ * Successfully get an attribute from the user namespace
+ */
+TEST_F(Getxattr, user)
+{
+ 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_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_getxattr(ino, "user.foo",
+ 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);
+}
+
+/*
+ * If the filesystem returns ENOSYS, then it will be treated as a permanent
+ * failure and all future VOP_LISTEXTATTR calls will fail with EOPNOTSUPP
+ * without querying the filesystem daemon
+ */
+TEST_F(Listxattr, enosys)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
+ expect_listxattr(ino, 0, ReturnErrno(ENOSYS));
+
+ ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
+ EXPECT_EQ(EOPNOTSUPP, errno);
+
+ /* Subsequent attempts should not query the filesystem at all */
+ ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
+ EXPECT_EQ(EOPNOTSUPP, errno);
+}
+
+/*
+ * Listing extended attributes failed because they aren't configured on this
+ * filesystem
+ */
+TEST_F(Listxattr, enotsup)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_listxattr(ino, 0, ReturnErrno(ENOTSUP));
+
+ ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
+ ASSERT_EQ(ENOTSUP, errno);
+}
+
+/*
+ * On FreeBSD, if the user passes an insufficiently large buffer to
+ * extattr_list_file(2) or VOP_LISTEXTATTR(9), then the file system is supposed
+ * to copy as much of the attribute's value as will fit.
+ *
+ * On Linux, however, the file system is supposed to return ERANGE if an
+ * insufficiently large buffer is passed to listxattr(2).
+ *
+ * fusefs(4) must guarantee the usual FreeBSD behavior.
+ */
+TEST_F(Listxattr, erange)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+ char attrs[9] = "user.foo";
+ char expected[3] = {3, 'f', 'o'};
+ char buf[3];
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out)
+ {
+ out.body.listxattr.size = sizeof(attrs);
+ SET_OUT_HEADER_LEN(out, listxattr);
+ }));
+ expect_listxattr(ino, sizeof(attrs),
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ memcpy((void*)out.body.bytes, attrs, sizeof(attrs));
+ out.header.len = sizeof(fuse_out_header) + sizeof(attrs);
+ }));
+
+
+ ASSERT_EQ(static_cast<ssize_t>(sizeof(buf)),
+ extattr_list_file(FULLPATH, ns, buf, sizeof(buf)));
+ ASSERT_EQ(0, memcmp(expected, buf, sizeof(buf)));
+}
+
+/*
+ * A buggy or malicious file system always returns ERANGE, even if we pass an
+ * appropriately sized buffer. That will send the kernel into an infinite
+ * loop. This test will ensure that the loop is interruptible by killing the
+ * blocked process with SIGINT.
+ */
+TEST_F(ListxattrSig, erange_forever)
+{
+ uint64_t ino = 42;
+ uint32_t lie_size = 10;
+ int status;
+
+ fork(false, &status, [&] {
+ 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.attr.nlink = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LISTXATTR &&
+ in.header.nodeid == ino &&
+ in.body.listxattr.size == 0);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(ReturnImmediate([=](auto i __unused, auto& out)
+ {
+ /* The file system requests 10 bytes, but it's a lie */
+ out.body.listxattr.size = lie_size;
+ SET_OUT_HEADER_LEN(out, listxattr);
+ /*
+ * We can send the signal any time after fusefs enters
+ * VOP_LISTEXTATTR
+ */
+ sem_post(&killer_semaphore);
+ }));
+ /*
+ * Even though the kernel faithfully respects our size request,
+ * we'll return ERANGE anyway.
+ */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LISTXATTR &&
+ in.header.nodeid == ino &&
+ in.body.listxattr.size == lie_size);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(ReturnErrno(ERANGE));
+
+ ASSERT_EQ(0, pthread_create(&m_killer_th, NULL, killer,
+ &m_mock->m_child_pid))
+ << strerror(errno);
+
+ }, [] {
+ /* Child process will block until it gets signaled */
+ int ns = EXTATTR_NAMESPACE_USER;
+ char buf[3];
+ extattr_list_file(FULLPATH, ns, buf, sizeof(buf));
+ return 0;
+ }
+ );
+
+ ASSERT_TRUE(WIFSIGNALED(status));
+}
+
+/*
+ * Get the size of the list that it would take to list no extended attributes
+ */
+TEST_F(Listxattr, size_only_empty)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) {
+ out.body.listxattr.size = 0;
+ SET_OUT_HEADER_LEN(out, listxattr);
+ }));
+
+ ASSERT_EQ(0, extattr_list_file(FULLPATH, ns, NULL, 0))
+ << strerror(errno);
+}
+
+/*
+ * Get the size of the list that it would take to list some extended
+ * attributes. Due to the format differences between a FreeBSD and a
+ * Linux/FUSE extended attribute list, fuse(4) will actually allocate a buffer
+ * and get the whole list, then convert it, just to figure out its size.
+ */
+TEST_F(Listxattr, size_only_nonempty)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+ char attrs[9] = "user.foo";
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out)
+ {
+ out.body.listxattr.size = sizeof(attrs);
+ SET_OUT_HEADER_LEN(out, listxattr);
+ }));
+
+ expect_listxattr(ino, sizeof(attrs),
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ size_t l = sizeof(attrs);
+ strlcpy((char*)out.body.bytes, attrs, l);
+ out.header.len = sizeof(fuse_out_header) + l;
+ })
+ );
+
+ ASSERT_EQ(4, extattr_list_file(FULLPATH, ns, NULL, 0))
+ << strerror(errno);
+}
+
+/*
+ * The list of extended attributes grows in between the server's two calls to
+ * FUSE_LISTXATTR.
+ */
+TEST_F(Listxattr, size_only_race_bigger)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+ char attrs0[9] = "user.foo";
+ char attrs1[18] = "user.foo\0user.bar";
+ Sequence seq;
+
+ 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.attr.nlink = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+ expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out)
+ {
+ out.body.listxattr.size = sizeof(attrs0);
+ SET_OUT_HEADER_LEN(out, listxattr);
+ }), &seq);
+
+ /*
+ * After the first FUSE_LISTXATTR the list grew, so the second
+ * operation returns ERANGE.
+ */
+ expect_listxattr(ino, sizeof(attrs0), ReturnErrno(ERANGE), &seq);
+
+ /* And now the kernel retries */
+ expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out)
+ {
+ out.body.listxattr.size = sizeof(attrs1);
+ SET_OUT_HEADER_LEN(out, listxattr);
+ }), &seq);
+ expect_listxattr(ino, sizeof(attrs1),
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ memcpy((char*)out.body.bytes, attrs1, sizeof(attrs1));
+ out.header.len = sizeof(fuse_out_header) +
+ sizeof(attrs1);
+ }), &seq
+ );
+
+ /* Userspace should never know about the retry */
+ ASSERT_EQ(8, extattr_list_file(FULLPATH, ns, NULL, 0))
+ << strerror(errno);
+}
+
+/*
+ * The list of extended attributes shrinks in between the server's two calls to
+ * FUSE_LISTXATTR
+ */
+TEST_F(Listxattr, size_only_race_smaller)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+ char attrs0[18] = "user.foo\0user.bar";
+ char attrs1[9] = "user.foo";
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out)
+ {
+ out.body.listxattr.size = sizeof(attrs0);
+ SET_OUT_HEADER_LEN(out, listxattr);
+ }));
+ expect_listxattr(ino, sizeof(attrs0),
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ memcpy((char*)out.body.bytes, attrs1, sizeof(attrs1));
+ out.header.len = sizeof(fuse_out_header) +
+ sizeof(attrs1);
+ })
+ );
+
+ ASSERT_EQ(4, extattr_list_file(FULLPATH, ns, NULL, 0))
+ << strerror(errno);
+}
+
+TEST_F(Listxattr, size_only_really_big)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) {
+ out.body.listxattr.size = 16000;
+ SET_OUT_HEADER_LEN(out, listxattr);
+ }));
+
+ expect_listxattr(ino, 16000,
+ ReturnImmediate([](auto in __unused, auto& out) {
+ const char l[16] = "user.foobarbang";
+ for (int i=0; i < 1000; i++) {
+ memcpy(&out.body.bytes[16 * i], l, 16);
+ }
+ out.header.len = sizeof(fuse_out_header) + 16000;
+ })
+ );
+
+ ASSERT_EQ(11000, extattr_list_file(FULLPATH, ns, NULL, 0))
+ << strerror(errno);
+}
+
+/*
+ * List all of the user attributes of a file which has both user and system
+ * attributes
+ */
+TEST_F(Listxattr, user)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+ char data[80];
+ char expected[9] = {3, 'f', 'o', 'o', 4, 'b', 'a', 'n', 'g'};
+ char attrs[28] = "user.foo\0system.x\0user.bang";
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_listxattr(ino, 0,
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ out.body.listxattr.size = sizeof(attrs);
+ SET_OUT_HEADER_LEN(out, listxattr);
+ })
+ );
+
+ expect_listxattr(ino, sizeof(attrs),
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ memcpy((void*)out.body.bytes, attrs, sizeof(attrs));
+ out.header.len = sizeof(fuse_out_header) + sizeof(attrs);
+ }));
+
+ ASSERT_EQ(static_cast<ssize_t>(sizeof(expected)),
+ extattr_list_file(FULLPATH, ns, data, sizeof(data)))
+ << strerror(errno);
+ ASSERT_EQ(0, memcmp(expected, data, sizeof(expected)));
+}
+
+/*
+ * List all of the system attributes of a file which has both user and system
+ * attributes
+ */
+TEST_F(Listxattr, system)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_SYSTEM;
+ char data[80];
+ char expected[2] = {1, 'x'};
+ char attrs[28] = "user.foo\0system.x\0user.bang";
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_listxattr(ino, 0,
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ out.body.listxattr.size = sizeof(attrs);
+ SET_OUT_HEADER_LEN(out, listxattr);
+ })
+ );
+
+ expect_listxattr(ino, sizeof(attrs),
+ ReturnImmediate([&](auto in __unused, auto& out) {
+ memcpy((void*)out.body.bytes, attrs, sizeof(attrs));
+ out.header.len = sizeof(fuse_out_header) + sizeof(attrs);
+ }));
+
+ ASSERT_EQ(static_cast<ssize_t>(sizeof(expected)),
+ extattr_list_file(FULLPATH, ns, data, sizeof(data)))
+ << strerror(errno);
+ ASSERT_EQ(0, memcmp(expected, data, sizeof(expected)));
+}
+
+/* Fail to remove a nonexistent attribute */
+TEST_F(Removexattr, enoattr)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_removexattr(ino, "user.foo", ENOATTR);
+
+ ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
+ ASSERT_EQ(ENOATTR, errno);
+}
+
+/*
+ * If the filesystem returns ENOSYS, then it will be treated as a permanent
+ * failure and all future VOP_DELETEEXTATTR calls will fail with EOPNOTSUPP
+ * without querying the filesystem daemon
+ */
+TEST_F(Removexattr, enosys)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
+ expect_removexattr(ino, "user.foo", ENOSYS);
+
+ ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
+ EXPECT_EQ(EOPNOTSUPP, errno);
+
+ /* Subsequent attempts should not query the filesystem at all */
+ ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
+ EXPECT_EQ(EOPNOTSUPP, errno);
+}
+
+/* Successfully remove a user xattr */
+TEST_F(Removexattr, user)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_removexattr(ino, "user.foo", 0);
+
+ ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))
+ << strerror(errno);
+}
+
+/* Successfully remove a system xattr */
+TEST_F(Removexattr, system)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_SYSTEM;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_removexattr(ino, "system.foo", 0);
+
+ ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))
+ << strerror(errno);
+}
+
+
+/*
+ * If the filesystem returns ENOSYS, then it will be treated as a permanent
+ * failure and all future VOP_SETEXTATTR calls will fail with EOPNOTSUPP
+ * without querying the filesystem daemon
+ */
+TEST_F(Setxattr, enosys)
+{
+ uint64_t ino = 42;
+ const char value[] = "whatever";
+ ssize_t value_len = strlen(value) + 1;
+ int ns = EXTATTR_NAMESPACE_USER;
+ ssize_t r;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
+ expect_setxattr(ino, "user.foo", value, ReturnErrno(ENOSYS));
+
+ r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
+ value_len);
+ ASSERT_EQ(-1, r);
+ EXPECT_EQ(EOPNOTSUPP, errno);
+
+ /* Subsequent attempts should not query the filesystem at all */
+ r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
+ value_len);
+ ASSERT_EQ(-1, r);
+ EXPECT_EQ(EOPNOTSUPP, errno);
+}
+
+/*
+ * SETXATTR will return ENOTSUP if the namespace is invalid or the filesystem
+ * as currently configured doesn't support extended attributes.
+ */
+TEST_F(Setxattr, enotsup)
+{
+ uint64_t ino = 42;
+ const char value[] = "whatever";
+ ssize_t value_len = strlen(value) + 1;
+ int ns = EXTATTR_NAMESPACE_USER;
+ ssize_t r;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_setxattr(ino, "user.foo", value, ReturnErrno(ENOTSUP));
+
+ r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
+ value_len);
+ ASSERT_EQ(-1, r);
+ EXPECT_EQ(ENOTSUP, errno);
+}
+
+/*
+ * Successfully set a user attribute.
+ */
+TEST_F(Setxattr, user)
+{
+ uint64_t ino = 42;
+ const char value[] = "whatever";
+ ssize_t value_len = strlen(value) + 1;
+ int ns = EXTATTR_NAMESPACE_USER;
+ ssize_t r;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_setxattr(ino, "user.foo", value, ReturnErrno(0));
+
+ r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
+ value_len);
+ ASSERT_EQ(value_len, r) << strerror(errno);
+}
+
+/*
+ * Successfully set a system attribute.
+ */
+TEST_F(Setxattr, system)
+{
+ uint64_t ino = 42;
+ const char value[] = "whatever";
+ ssize_t value_len = strlen(value) + 1;
+ int ns = EXTATTR_NAMESPACE_SYSTEM;
+ ssize_t r;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_setxattr(ino, "system.foo", value, ReturnErrno(0));
+
+ r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
+ value_len);
+ ASSERT_EQ(value_len, r) << strerror(errno);
+}
+
+
+TEST_F(Setxattr_7_32, ok)
+{
+ uint64_t ino = 42;
+ const char value[] = "whatever";
+ ssize_t value_len = strlen(value) + 1;
+ int ns = EXTATTR_NAMESPACE_USER;
+ ssize_t r;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_setxattr_7_32(ino, "user.foo", value, ReturnErrno(0));
+
+ r = extattr_set_file(FULLPATH, ns, "foo", (const void *)value,
+ value_len);
+ ASSERT_EQ(value_len, r) << strerror(errno);
+}
+
+TEST_F(RofsXattr, deleteextattr_erofs)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+
+ ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
+ ASSERT_EQ(EROFS, errno);
+}
+
+TEST_F(RofsXattr, setextattr_erofs)
+{
+ uint64_t ino = 42;
+ const char value[] = "whatever";
+ ssize_t value_len = strlen(value) + 1;
+ int ns = EXTATTR_NAMESPACE_USER;
+ ssize_t r;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+
+ r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,
+ value_len);
+ ASSERT_EQ(-1, r);
+ EXPECT_EQ(EROFS, errno);
+}