aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/sys/fs/Makefile5
-rw-r--r--tests/sys/fs/fusefs/Makefile81
-rw-r--r--tests/sys/fs/fusefs/access.cc119
-rw-r--r--tests/sys/fs/fusefs/allow_other.cc303
-rw-r--r--tests/sys/fs/fusefs/bmap.cc159
-rw-r--r--tests/sys/fs/fusefs/create.cc449
-rw-r--r--tests/sys/fs/fusefs/default_permissions.cc1305
-rw-r--r--tests/sys/fs/fusefs/default_permissions_privileged.cc124
-rw-r--r--tests/sys/fs/fusefs/destroy.cc158
-rw-r--r--tests/sys/fs/fusefs/dev_fuse_poll.cc227
-rw-r--r--tests/sys/fs/fusefs/fifo.cc207
-rw-r--r--tests/sys/fs/fusefs/flush.cc232
-rw-r--r--tests/sys/fs/fusefs/forget.cc154
-rw-r--r--tests/sys/fs/fusefs/fsync.cc249
-rw-r--r--tests/sys/fs/fusefs/fsyncdir.cc186
-rw-r--r--tests/sys/fs/fusefs/getattr.cc300
-rw-r--r--tests/sys/fs/fusefs/interrupt.cc790
-rw-r--r--tests/sys/fs/fusefs/io.cc543
-rw-r--r--tests/sys/fs/fusefs/link.cc233
-rw-r--r--tests/sys/fs/fusefs/locks.cc478
-rw-r--r--tests/sys/fs/fusefs/lookup.cc381
-rw-r--r--tests/sys/fs/fusefs/mkdir.cc222
-rw-r--r--tests/sys/fs/fusefs/mknod.cc238
-rw-r--r--tests/sys/fs/fusefs/mockfs.cc733
-rw-r--r--tests/sys/fs/fusefs/mockfs.hh394
-rw-r--r--tests/sys/fs/fusefs/mount.cc152
-rw-r--r--tests/sys/fs/fusefs/nfs.cc344
-rw-r--r--tests/sys/fs/fusefs/notify.cc545
-rw-r--r--tests/sys/fs/fusefs/open.cc262
-rw-r--r--tests/sys/fs/fusefs/opendir.cc155
-rw-r--r--tests/sys/fs/fusefs/read.cc916
-rw-r--r--tests/sys/fs/fusefs/readdir.cc375
-rw-r--r--tests/sys/fs/fusefs/readlink.cc123
-rw-r--r--tests/sys/fs/fusefs/release.cc224
-rw-r--r--tests/sys/fs/fusefs/releasedir.cc116
-rw-r--r--tests/sys/fs/fusefs/rename.cc321
-rw-r--r--tests/sys/fs/fusefs/rmdir.cc172
-rw-r--r--tests/sys/fs/fusefs/setattr.cc774
-rw-r--r--tests/sys/fs/fusefs/statfs.cc171
-rw-r--r--tests/sys/fs/fusefs/symlink.cc178
-rw-r--r--tests/sys/fs/fusefs/unlink.cc251
-rw-r--r--tests/sys/fs/fusefs/utils.cc593
-rw-r--r--tests/sys/fs/fusefs/utils.hh236
-rw-r--r--tests/sys/fs/fusefs/write.cc1309
-rw-r--r--tests/sys/fs/fusefs/xattr.cc641
45 files changed, 16128 insertions, 0 deletions
diff --git a/tests/sys/fs/Makefile b/tests/sys/fs/Makefile
index c82ee143a785c..99d909b140de6 100644
--- a/tests/sys/fs/Makefile
+++ b/tests/sys/fs/Makefile
@@ -1,5 +1,7 @@
# $FreeBSD$
+.include <bsd.compiler.mk>
+
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/fs
@@ -7,6 +9,9 @@ TESTSDIR= ${TESTSBASE}/sys/fs
TESTSRC= ${SRCTOP}/contrib/netbsd-tests/fs
#TESTS_SUBDIRS+= nullfs # XXX: needs rump
+.if ${COMPILER_FEATURES:Mc++14}
+TESTS_SUBDIRS+= fusefs
+.endif
TESTS_SUBDIRS+= tmpfs
${PACKAGE}FILES+= h_funcs.subr
diff --git a/tests/sys/fs/fusefs/Makefile b/tests/sys/fs/fusefs/Makefile
new file mode 100644
index 0000000000000..957608dde7475
--- /dev/null
+++ b/tests/sys/fs/fusefs/Makefile
@@ -0,0 +1,81 @@
+# $FreeBSD$
+
+PACKAGE= tests
+
+TESTSDIR= ${TESTSBASE}/sys/fs/fusefs
+
+# 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+= bmap
+GTESTS+= create
+GTESTS+= default_permissions
+GTESTS+= default_permissions_privileged
+GTESTS+= destroy
+GTESTS+= dev_fuse_poll
+GTESTS+= fifo
+GTESTS+= flush
+GTESTS+= forget
+GTESTS+= fsync
+GTESTS+= fsyncdir
+GTESTS+= getattr
+GTESTS+= interrupt
+GTESTS+= io
+GTESTS+= link
+GTESTS+= locks
+GTESTS+= lookup
+GTESTS+= mkdir
+GTESTS+= mknod
+GTESTS+= mount
+GTESTS+= nfs
+GTESTS+= notify
+GTESTS+= open
+GTESTS+= opendir
+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+= getmntopts.c
+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"
+
+# TODO: drastically increase timeout after test development is mostly complete
+TEST_METADATA+= timeout=10
+
+FUSEFS= ${SRCTOP}/sys/fs/fuse
+MOUNT= ${SRCTOP}/sbin/mount
+# Suppress warnings that GCC generates for the libc++ and gtest headers.
+CXXWARNFLAGS.gcc+= -Wno-placement-new -Wno-attributes -Wno-class-memaccess
+CXXFLAGS+= -I${SRCTOP}/tests
+CXXFLAGS+= -I${FUSEFS}
+CXXFLAGS+= -I${MOUNT}
+.PATH: ${MOUNT}
+CXXSTD= c++14
+
+LIBADD+= pthread
+LIBADD+= gmock gtest
+LIBADD+= util
+
+WARNS?= 6
+
+.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 0000000000000..eaaa3ff7b293a
--- /dev/null
+++ b/tests/sys/fs/fusefs/access.cc
@@ -0,0 +1,119 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 Access: public FuseTest {
+public:
+void expect_lookup(const char *relpath, uint64_t ino)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1);
+}
+};
+
+class RofsAccess: public Access {
+public:
+virtual void SetUp() {
+ m_ro = true;
+ Access::SetUp();
+}
+};
+
+/* 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);
+}
+
+/* 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);
+}
diff --git a/tests/sys/fs/fusefs/allow_other.cc b/tests/sys/fs/fusefs/allow_other.cc
new file mode 100644
index 0000000000000..82b7b0907f946
--- /dev/null
+++ b/tests/sys/fs/fusefs/allow_other.cc
@@ -0,0 +1,303 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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:
+/* Unprivileged user id */
+int m_uid;
+
+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);
+ }
+ 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);
+ EXPECT_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");
+ 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");
+ return(1);
+ } else if (errno != EPERM) {
+ fprintf(stderr, "Unexpected error: %s\n",
+ strerror(errno));
+ return(1);
+ }
+ return 0;
+ }
+ );
+ ASSERT_EQ(0, WEXITSTATUS(status));
+}
+
+/*
+ * 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/bmap.cc b/tests/sys/fs/fusefs/bmap.cc
new file mode 100644
index 0000000000000..d5330d90183ce
--- /dev/null
+++ b/tests/sys/fs/fusefs/bmap.cc
@@ -0,0 +1,159 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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);
+}
+};
+
+/*
+ * Test FUSE_BMAP
+ * XXX The FUSE protocol does not include the runp and runb variables, so those
+ * must be guessed in-kernel.
+ */
+TEST_F(Bmap, bmap)
+{
+ struct fiobmap2_arg arg;
+ const off_t filesize = 1 << 20;
+ const ino_t ino = 42;
+ int64_t lbn = 10;
+ int64_t pbn = 12345;
+ 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);
+ EXPECT_EQ(arg.runp, m_maxphys / m_maxbcachebuf - 1);
+ EXPECT_EQ(arg.runb, m_maxphys / m_maxbcachebuf - 1);
+}
+
+/*
+ * 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 << 20;
+ 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(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(arg.runp, m_maxphys / m_maxbcachebuf - 1);
+ EXPECT_EQ(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(arg.runb, m_maxphys / m_maxbcachebuf - 1);
+}
diff --git a/tests/sys/fs/fusefs/create.cc b/tests/sys/fs/fusefs/create.cc
new file mode 100644
index 0000000000000..1030a1e5633c3
--- /dev/null
+++ b/tests/sys/fs/fusefs/create.cc
@@ -0,0 +1,449 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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);
+ EXPECT_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);
+ EXPECT_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_NE(0, 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);
+ EXPECT_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_NE(0, 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);
+ EXPECT_LE(0, fd) << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * 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);
+ EXPECT_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.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);
+ EXPECT_LE(0, fd) << strerror(errno);
+ leak(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);
+ EXPECT_LE(0, fd) << strerror(errno);
+ leak(fd);
+}
diff --git a/tests/sys/fs/fusefs/default_permissions.cc b/tests/sys/fs/fusefs/default_permissions.cc
new file mode 100644
index 0000000000000..ee2a9644cf8ab
--- /dev/null
+++ b/tests/sys/fs/fusefs/default_permissions.cc
@@ -0,0 +1,1305 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 <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_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.uid = 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 Lookup: public DefaultPermissions {};
+class Open: 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);
+}
+
+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);
+ EXPECT_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)));
+
+ EXPECT_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 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_NE(0, 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);
+ EXPECT_LE(0, fd) << 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);
+}
+
+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;
+
+ 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);
+
+ ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno);
+}
+
+/*
+ * 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 0000000000000..621411b09588b
--- /dev/null
+++ b/tests/sys/fs/fusefs/default_permissions_privileged.cc
@@ -0,0 +1,124 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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.uid = 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 0000000000000..11df47418f6f5
--- /dev/null
+++ b/tests/sys/fs/fusefs/destroy.cc
@@ -0,0 +1,158 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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=Destroy.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(FUSE_ROOT_ID, 1);
+ 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 0000000000000..4ced6428d7c6a
--- /dev/null
+++ b/tests/sys/fs/fusefs/dev_fuse_poll.cc
@@ -0,0 +1,227 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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_forget(FUSE_ROOT_ID, 1);
+ expect_destroy(0);
+
+ m_mock->unmount();
+}
+
+INSTANTIATE_TEST_CASE_P(PM, DevFusePoll,
+ ::testing::Values("BLOCKING", "KQ", "POLL", "SELECT"));
+
+static void* statter(void* arg) {
+ const char *name;
+ struct stat sb;
+
+ name = (const char*)arg;
+ stat(name, &sb);
+ return 0;
+}
+
+/*
+ * A kevent's data field should contain the number of operations available to
+ * be immediately rea.
+ */
+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;
+
+ 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, NULL);
+ pthread_join(th1, NULL);
+ pthread_join(th2, NULL);
+
+ 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/fifo.cc b/tests/sys/fs/fusefs/fifo.cc
new file mode 100644
index 0000000000000..be443a1ef0ac7
--- /dev/null
+++ b/tests/sys/fs/fusefs/fifo.cc
@@ -0,0 +1,207 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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));
+ 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;
+
+ }
+ 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));
+ 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 0000000000000..01aca311ffca7
--- /dev/null
+++ b/tests/sys/fs/fusefs/flush.cc
@@ -0,0 +1,232 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ fd2 = open(FULLPATH, O_WRONLY);
+ EXPECT_LE(0, fd2) << strerror(errno);
+
+ EXPECT_EQ(0, close(fd2)) << strerror(errno);
+ 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);
+ EXPECT_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);
+ EXPECT_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.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_SETLKW, &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 0000000000000..887d474028287
--- /dev/null
+++ b/tests/sys/fs/fusefs/forget.cc
@@ -0,0 +1,154 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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/sysctl.h>
+
+#include <fcntl.h>
+#include <semaphore.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+const char reclaim_mib[] = "debug.try_reclaim_vnode";
+
+class Forget: public FuseTest {
+public:
+void SetUp() {
+ if (geteuid() != 0)
+ GTEST_SKIP() << "Only root may use " << reclaim_mib;
+
+ if (-1 == sysctlbyname(reclaim_mib, NULL, 0, NULL, 0) &&
+ errno == ENOENT)
+ GTEST_SKIP() << reclaim_mib << " is not available";
+
+ 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;
+ int err;
+
+ 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);
+
+ err = sysctlbyname(reclaim_mib, NULL, 0, FULLPATH, sizeof(FULLPATH));
+ ASSERT_EQ(0, err) << strerror(errno);
+
+ 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;
+ int err;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, DNAME)
+ .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, 2);
+
+ /* Access the file to cache its name */
+ ASSERT_EQ(0, access(FULLFPATH, F_OK)) << strerror(errno);
+
+ /* Reclaim the directory, invalidating its children from namecache */
+ err = sysctlbyname(reclaim_mib, NULL, 0, FULLDPATH, sizeof(FULLDPATH));
+ ASSERT_EQ(0, err) << strerror(errno);
+
+ /* Access the file again, causing another lookup */
+ ASSERT_EQ(0, access(FULLFPATH, 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 0000000000000..b283f065c1ff5
--- /dev/null
+++ b/tests/sys/fs/fusefs/fsync.cc
@@ -0,0 +1,249 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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)
+{
+ 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)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(error)));
+}
+
+void expect_lookup(const char *relpath, uint64_t ino)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1);
+}
+
+void expect_write(uint64_t ino, uint64_t size, const void *contents)
+{
+ FuseTest::expect_write(ino, 0, size, size, 0, 0, contents);
+}
+
+};
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */
+TEST_F(Fsync, 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);
+}
diff --git a/tests/sys/fs/fusefs/fsyncdir.cc b/tests/sys/fs/fusefs/fsyncdir.cc
new file mode 100644
index 0000000000000..b87f8c47a2e5c
--- /dev/null
+++ b/tests/sys/fs/fusefs/fsyncdir.cc
@@ -0,0 +1,186 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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);
+}
+
+};
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */
+TEST_F(FsyncDir, 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 0000000000000..ccbc38843e153
--- /dev/null
+++ b/tests/sys/fs/fusefs/getattr.cc
@@ -0,0 +1,300 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 "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;
+ })));
+
+ 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;
+
+ 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)));
+ EXPECT_NE(0, stat(FULLPATH, &sb));
+ EXPECT_EQ(ENOENT, errno);
+}
+
+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.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 protocol 7.8. They're
+ //only supported as OS-specific extensions to OSX.
+ //EXPECT_EQ(, sb.st_birthtim);
+ //EXPECT_EQ(, sb.st_flags);
+
+ //FUSE can't set st_blksize until protocol 7.9
+}
+
+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 0000000000000..a62ca61f20cc2
--- /dev/null
+++ b/tests/sys/fs/fusefs/interrupt.cc
@@ -0,0 +1,790 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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);
+}
+
+/*
+ * 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);
+}
+
+/*
+ * 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);
+}
+
+/*
+ * 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 0000000000000..0daffb8b7ad5c
--- /dev/null
+++ b/tests/sys/fs/fusefs/io.cc
@@ -0,0 +1,543 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 <sys/sysctl.h>
+
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+/*
+ * For testing I/O like fsx does, but deterministically and without a real
+ * underlying file system
+ *
+ * TODO: after fusefs gains the options to select cache mode for each mount
+ * point, run each of these tests for all cache modes.
+ */
+
+using namespace testing;
+
+enum cache_mode {
+ Uncached,
+ Writethrough,
+ Writeback,
+ WritebackAsync
+};
+
+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";
+ }
+}
+
+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> 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_direct_io(false) {};
+
+void SetUp()
+{
+ m_filesize = 0;
+ 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";
+ }
+
+ FuseTest::SetUp();
+ if (IsSkipped())
+ return;
+
+ if (verbosity > 0) {
+ printf("Test Parameters: init_flags=%#x maxwrite=%#x "
+ "%sasync cache=%s\n",
+ m_init_flags, m_maxwrite, m_async? "" : "no",
+ cache_mode_to_s(get<2>(GetParam())));
+ }
+
+ 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_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;
+
+ 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)));
+
+ 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_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(ssize_t size, off_t offs)
+{
+ void *control_buf, *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 = malloc(size);
+ ASSERT_NE(nullptr, control_buf) << strerror(errno);
+
+ 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);
+ free(control_buf);
+}
+
+void do_read(ssize_t size, off_t offs)
+{
+ void *test_buf, *control_buf;
+ ssize_t r;
+
+ test_buf = malloc(size);
+ ASSERT_NE(nullptr, test_buf) << strerror(errno);
+ control_buf = malloc(size);
+ ASSERT_NE(nullptr, control_buf) << strerror(errno);
+
+ 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);
+
+ free(control_buf);
+ free(test_buf);
+}
+
+void do_mapwrite(ssize_t size, off_t offs)
+{
+ 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 = (char*)malloc(size);
+ ASSERT_NE(nullptr, buf) << strerror(errno);
+ 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);
+
+ free(buf);
+ ASSERT_EQ(0, munmap(p, map_size)) << strerror(errno);
+}
+
+void do_write(ssize_t size, off_t offs)
+{
+ char *buf;
+ long i;
+
+ buf = (char*)malloc(size);
+ ASSERT_NE(nullptr, buf) << strerror(errno);
+ 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);
+
+ free(buf);
+}
+
+};
+
+class IoCacheable: 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(wsize, wofs);
+ do_ftruncate(truncsize);
+ do_read(rsize, rofs);
+}
+
+/*
+ * 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(0x849e, 0x29a3a); /* [0x29a3a, 0x31ed7] */
+ do_mapwrite(0x3994, 0x3c7d8); /* [0x3c7d8, 0x4016b] */
+ do_read(0xf556, 0x30c16); /* [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(0xcc77, 0x1134f); /* [0x1134f, 0x1dfc5] */
+ do_write(0xdfa7, 0x2096a); /* [0x2096a, 0x2e910] */
+ do_read(0xb5b7, 0x1a3aa); /* [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(0x123b7, 0xf205); /* [0xf205, 0x215bb] */
+ do_mapread(0xeeea, 0x2f4c); /* [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(wsize, wofs);
+ do_read(rsize, rofs);
+}
+
+/*
+ * 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(wsize0, wofs0);
+ do_write(wsize1, wofs1);
+ do_ftruncate(truncsize0);
+ do_read(rsize, rofs);
+ 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(wsize, wofs);
+ /* Truncates part of the dirty buffer created in step 2 */
+ do_ftruncate(truncsize1);
+ /* XXX ?I don't know why this is necessary? */
+ do_read(rsize2, rofs2);
+ /* 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(0x14530, 0x36ee6); /* [0x36ee6, 0x4b415] */
+ do_write(0x1507c, 0x33256); /* [0x33256, 0x482d1] */
+ do_write(0x175c, 0x4c03d); /* [0x4c03d, 0x4d798] */
+ do_read(0xe277, 0x3599c); /* [0x3599c, 0x43c12] */
+ close(m_test_fd);
+}
+
+INSTANTIATE_TEST_CASE_P(Io, Io,
+ Combine(Bool(), /* async read */
+ Values(0x1000, 0x10000, 0x20000), /* m_maxwrite */
+ Values(Uncached, Writethrough, Writeback, WritebackAsync)
+ )
+);
+
+INSTANTIATE_TEST_CASE_P(Io, IoCacheable,
+ Combine(Bool(), /* async read */
+ Values(0x1000, 0x10000, 0x20000), /* m_maxwrite */
+ Values(Writethrough, Writeback, WritebackAsync)
+ )
+);
diff --git a/tests/sys/fs/fusefs/link.cc b/tests/sys/fs/fusefs/link.cc
new file mode 100644
index 0000000000000..d31e67575476c
--- /dev/null
+++ b/tests/sys/fs/fusefs/link.cc
@@ -0,0 +1,233 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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);
+}
+
+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 0000000000000..1d328d9f92db9
--- /dev/null
+++ b/tests/sys/fs/fusefs/locks.cc
@@ -0,0 +1,478 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 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.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 = getpid();
+ 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 = 1234;
+
+ 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 &&
+ in.body.getlk.owner == (uint32_t)pid &&
+ in.body.getlk.lk.start == 10 &&
+ in.body.getlk.lk.end == 1009 &&
+ in.body.getlk.lk.type == F_RDLCK &&
+ in.body.getlk.lk.pid == (uint64_t)pid);
+ }, 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 = pid;
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_SET;
+ fl.l_sysid = 0;
+ ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno);
+ ASSERT_EQ(F_UNLCK, fl.l_type);
+ 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 = 1234;
+ 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 &&
+ in.body.getlk.owner == (uint32_t)pid &&
+ in.body.getlk.lk.start == 10 &&
+ in.body.getlk.lk.end == 1009 &&
+ in.body.getlk.lk.type == F_RDLCK &&
+ in.body.getlk.lk.pid == (uint64_t)pid);
+ }, 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 = pid;
+ 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);
+}
+
+/*
+ * 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);
+}
+
+/* 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 = 1234;
+
+ 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 = 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);
+ 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 = 1234;
+
+ 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 = 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);
+ 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 = 1234;
+
+ 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 = pid;
+ 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 = getpid();
+ 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 = 1234;
+
+ 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 = pid;
+ 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 0000000000000..5f3f34d834b8b
--- /dev/null
+++ b/tests/sys/fs/fusefs/lookup.cc
@@ -0,0 +1,381 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 Lookup: public FuseTest {};
+class Lookup_7_8: public Lookup {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 8;
+ 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 protocol 7.8. They're
+ //only supported as OS-specific extensions to OSX.
+ //EXPECT_EQ(, sb.st_birthtim);
+ //EXPECT_EQ(, sb.st_flags);
+
+ //FUSE can't set st_blksize until protocol 7.9
+}
+
+/*
+ * 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);
+}
+
+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
+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 a bad
+ * server! But we shouldn't crash.
+ */
+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(FIRSTRELPATH, ino, S_IFREG | 0644, 0, 1, UINT64_MAX);
+ expect_lookup(SECONDRELPATH, ino, S_IFDIR | 0755, 0, 1, UINT64_MAX);
+
+ ASSERT_EQ(0, access(FIRSTFULLPATH, F_OK)) << strerror(errno);
+ ASSERT_EQ(-1, access(SECONDFULLPATH, F_OK));
+ ASSERT_EQ(EAGAIN, 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);
+}
+
+
diff --git a/tests/sys/fs/fusefs/mkdir.cc b/tests/sys/fs/fusefs/mkdir.cc
new file mode 100644
index 0000000000000..6268c7fd510d0
--- /dev/null
+++ b/tests/sys/fs/fusefs/mkdir.cc
@@ -0,0 +1,222 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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);
+}
+
+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 0000000000000..4f4def17e76eb
--- /dev/null
+++ b/tests/sys/fs/fusefs/mknod.cc
@@ -0,0 +1,238 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+#ifndef VNOVAL
+#define VNOVAL (-1) /* Defined in sys/vnode.h */
+#endif
+
+const char FULLPATH[] = "mountpoint/some_file.txt";
+const char RELPATH[] = "some_file.txt";
+
+class Mknod: public FuseTest {
+
+mode_t m_oldmask;
+const static mode_t c_umask = 022;
+
+public:
+
+virtual void SetUp() {
+ m_oldmask = umask(c_umask);
+ 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(mode_t mode, dev_t dev) {
+ 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_mknod_in);
+ return (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(mode_t mode, dev_t dev) {
+ 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 +
+ FUSE_COMPAT_MKNOD_IN_SIZE;
+ return (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)
+{
+ mode_t mode = S_IFBLK | 0755;
+ dev_t rdev = 0xfe00; /* /dev/vda's device number on Linux */
+ expect_mknod(mode, rdev);
+ EXPECT_EQ(0, mknod(FULLPATH, mode, rdev)) << strerror(errno);
+}
+
+TEST_F(Mknod, chr)
+{
+ mode_t mode = S_IFCHR | 0755;
+ dev_t rdev = 54; /* /dev/fuse's device number */
+ expect_mknod(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)
+{
+ 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)
+{
+ mode_t mode = S_IFIFO | 0755;
+ dev_t rdev = VNOVAL; /* Fifos don't have device numbers */
+ expect_mknod(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)
+{
+ mode_t mode = S_IFSOCK | 0755;
+ struct sockaddr_un sa;
+ int fd;
+ dev_t rdev = -1; /* Really it's a don't care */
+
+ expect_mknod(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));
+ ASSERT_EQ(0, bind(fd, (struct sockaddr*)&sa, sizeof(sa)))
+ << strerror(errno);
+}
+
+/*
+ * fusefs(5) lacks VOP_WHITEOUT support. No bugzilla entry, because that's a
+ * feature, not a bug
+ */
+TEST_F(Mknod, DISABLED_whiteout)
+{
+ mode_t mode = S_IFWHT | 0755;
+ dev_t rdev = VNOVAL; /* whiteouts don't have device numbers */
+ expect_mknod(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)
+{
+ mode_t mode = S_IFIFO | 0755;
+ dev_t rdev = VNOVAL;
+ expect_mknod(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 0000000000000..fddfbd6d710b7
--- /dev/null
+++ b/tests/sys/fs/fusefs/mockfs.cc
@@ -0,0 +1,733 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 <poll.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "mntopts.h" // for build_iovec
+}
+
+#include <cinttypes>
+
+#include <gtest/gtest.h>
+
+#include "mockfs.hh"
+
+using namespace testing;
+
+int verbosity = 0;
+
+const char* opcode2opname(uint32_t opcode)
+{
+ const int NUM_OPS = 39;
+ const char* table[NUM_OPS] = {
+ "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"
+ };
+ if (opcode >= NUM_OPS)
+ 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)
+{
+ 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",
+ in.header.uid, in.header.gid, in.header.pid,
+ in.header.unique, in.header.len);
+ }
+ 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_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_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_INTERRUPT:
+ printf(" unique=%" PRIu64, in.body.interrupt.unique);
+ break;
+ case FUSE_LINK:
+ printf(" oldnodeid=%" PRIu64, in.body.link.oldnodeid);
+ break;
+ case FUSE_LOOKUP:
+ printf(" %s", in.body.lookup);
+ 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_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=[%" PRIu64 "-%" PRIu64 "]",
+ 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_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)
+{
+ struct sigaction sa;
+ struct iovec *iov = NULL;
+ int iovlen = 0;
+ char fdstr[15];
+ const bool trueval = true;
+
+ m_daemon_id = NULL;
+ m_kernel_minor_version = kernel_minor_version;
+ m_maxreadahead = max_readahead;
+ m_maxwrite = max_write;
+ m_nready = -1;
+ m_pm = pm;
+ m_time_gran = time_gran;
+ m_quit = false;
+ if (m_pm == KQ)
+ m_kq = kqueue();
+ else
+ m_kq = -1;
+
+ /*
+ * 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"));
+
+ m_pid = getpid();
+ m_child_pid = -1;
+
+ 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 (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 (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 (nmount(iov, iovlen, 0))
+ throw(std::system_error(errno, std::system_category(),
+ "Couldn't mount filesystem"));
+
+ // Setup default handler
+ ON_CALL(*this, process(_, _))
+ .WillByDefault(Invoke(this, &MockFS::process_default));
+
+ 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();
+ if (m_daemon_id != NULL) {
+ pthread_join(m_daemon_id, NULL);
+ m_daemon_id = NULL;
+ }
+ ::unmount("mountpoint", MNT_FORCE);
+ rmdir("mountpoint");
+ if (m_kq >= 0)
+ close(m_kq);
+}
+
+void MockFS::init(uint32_t flags) {
+ 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);
+ 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::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) {
+ bzero(in.get(), sizeof(*in));
+ read_request(*in);
+ if (m_quit)
+ break;
+ if (verbosity > 0)
+ debug_request(*in);
+ 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)
+{
+ 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_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;
+
+ 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, 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:
+ timeout_tv.tv_sec = 0;
+ timeout_tv.tv_usec = timeout_ms * 1'000;
+ nfds = m_fuse_fd + 1;
+ while (nready == 0) {
+ FD_ZERO(&readfds);
+ FD_SET(m_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(m_fuse_fd, &readfds));
+ break;
+ default:
+ FAIL() << "not yet implemented";
+ }
+ res = read(m_fuse_fd, &in, sizeof(in));
+
+ if (res < 0 && !m_quit) {
+ FAIL() << "read: " << strerror(errno);
+ m_quit = true;
+ }
+ ASSERT_TRUE(res >= static_cast<ssize_t>(sizeof(in.header)) || m_quit);
+}
+
+void MockFS::write_response(const mockfs_buf_out &out) {
+ fd_set writefds;
+ pollfd fds[1];
+ int nready, nfds;
+ ssize_t r;
+
+ switch (m_pm) {
+ case BLOCKING:
+ case KQ: /* EVFILT_WRITE is not supported */
+ 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);
+ 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 0000000000000..977d7780ccb44
--- /dev/null
+++ b/tests/sys/fs/fusefs/mockfs.hh
@@ -0,0 +1,394 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 <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;
+
+/* 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;
+ /* value is from fuse_kern_chan.c in fusefs-libs */
+ uint8_t bytes[0x21000 - sizeof(struct fuse_in_header)];
+ fuse_create_in create;
+ fuse_flush_in flush;
+ fuse_fsync_in fsync;
+ fuse_fsync_in fsyncdir;
+ fuse_forget_in forget;
+ 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_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[0x20000];
+ 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;
+ /* 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;
+
+ /* 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 */
+ 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;
+
+ /* The max_readahead file system option */
+ uint32_t m_maxreadahead;
+
+ /* pid of the test process */
+ pid_t m_pid;
+
+ /* 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 debug_request(const mockfs_buf_in&);
+ 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 */
+ void read_request(mockfs_buf_in& in);
+
+ /* Write a single response back to the kernel */
+ void write_response(const mockfs_buf_out &out);
+
+ public:
+ /* 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;
+
+ /* Create a new mockfs and mount it to a tempdir */
+ MockFS(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);
+
+ virtual ~MockFS();
+
+ /* Kill the filesystem daemon without unmounting the filesystem */
+ void kill_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
+ */
+ int notify_inval_entry(ino_t parent, const char *name, size_t namelen);
+
+ /*
+ * 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 0000000000000..8c0e297199482
--- /dev/null
+++ b/tests/sys/fs/fusefs/mount.cc
@@ -0,0 +1,152 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 UpdateOk: public FuseTest, public WithParamInterface<const char*> {};
+class UpdateErr: public FuseTest, 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;
+}
+
+/* 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_CASE_P(Mount, UpdateOk,
+ ::testing::Values("MNT_RDONLY", "MNT_NOEXEC", "MNT_NOSUID", "MNT_NOATIME",
+ "MNT_SUIDDIR")
+);
+
+INSTANTIATE_TEST_CASE_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 0000000000000..b33df0af50d60
--- /dev/null
+++ b/tests/sys/fs/fusefs/nfs.cc
@@ -0,0 +1,344 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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.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.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.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.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);
+}
+
+/* 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.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.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.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 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.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.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_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.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.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 0000000000000..30b59f21b4e1a
--- /dev/null
+++ b/tests/sys/fs/fusefs/notify.cc
@@ -0,0 +1,545 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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/sysctl.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 been 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);
+}
+
+/* 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_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_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);
+}
diff --git a/tests/sys/fs/fusefs/open.cc b/tests/sys/fs/fusefs/open.cc
new file mode 100644
index 0000000000000..20d1421ce7fc1
--- /dev/null
+++ b/tests/sys/fs/fusefs/open.cc
@@ -0,0 +1,262 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 "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);
+ EXPECT_LE(0, fd) << strerror(errno);
+ leak(fd);
+}
+};
+
+
+/*
+ * fusefs(5) 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;
+
+ 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)));
+ EXPECT_NE(0, open(FULLPATH, O_RDONLY));
+ EXPECT_EQ(ENOENT, errno);
+}
+
+/*
+ * 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)));
+ EXPECT_NE(0, 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);
+ EXPECT_LE(0, fd1) << strerror(errno);
+ }, [] {
+ int fd0;
+
+ fd0 = open(FULLPATH, O_RDONLY);
+ if (fd0 < 0) {
+ perror("open");
+ return(1);
+ }
+ 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);
+}
+
diff --git a/tests/sys/fs/fusefs/opendir.cc b/tests/sys/fs/fusefs/opendir.cc
new file mode 100644
index 0000000000000..f970ab134e1e6
--- /dev/null
+++ b/tests/sys/fs/fusefs/opendir.cc
@@ -0,0 +1,155 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 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;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino, O_RDONLY, ReturnErrno(ENOENT));
+
+ EXPECT_NE(0, open(FULLPATH, O_DIRECTORY));
+ EXPECT_EQ(ENOENT, errno);
+}
+
+/*
+ * 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_NE(0, 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;
+
+ expect_lookup(RELPATH, ino);
+ expect_opendir(ino, O_RDONLY,
+ ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, open);
+ }));
+
+ EXPECT_LE(0, open(FULLPATH, O_DIRECTORY)) << strerror(errno);
+}
+
+/* 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);
+}
+
+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);
+}
diff --git a/tests/sys/fs/fusefs/read.cc b/tests/sys/fs/fusefs/read.cc
new file mode 100644
index 0000000000000..bf0641738fca3
--- /dev/null
+++ b/tests/sys/fs/fusefs/read.cc
@@ -0,0 +1,916 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 <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 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() {
+ const char *node = "vfs.aio.enable_unsafe";
+ int val = 0;
+ size_t size = sizeof(val);
+
+ FuseTest::SetUp();
+
+ ASSERT_EQ(0, sysctlbyname(node, &val, &size, NULL, 0))
+ << strerror(errno);
+ if (!val)
+ GTEST_SKIP() <<
+ "vfs.aio.enable_unsafe must be set for this test";
+}
+};
+
+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();
+ }
+};
+
+/* 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);
+ char 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);
+}
+
+/* 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);
+ char 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;
+ char 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);
+ char 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;
+ char 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;
+ char 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);
+ char 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);
+ char 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);
+}
+
+/*
+ * 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);
+}
+
+/*
+ * 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);
+ char 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);
+ char 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);
+ char 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);
+ char 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 = (char*)calloc(1, filesize);
+ ASSERT_NE(nullptr, contents);
+ 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);
+}
+
+/* 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);
+ char 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 = (char*)malloc(filesize);
+ ASSERT_NE(nullptr, contents);
+ memset(contents, 'X', filesize);
+ rbuf = (char*)calloc(1, bufsize);
+
+ expect_lookup(RELPATH, ino, filesize);
+ expect_open(ino, 0, 1);
+ maxcontig = m_noclusterr ? m_maxbcachebuf :
+ m_maxbcachebuf + m_maxreadahead;
+ clustersize = MIN(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);
+}
+
+INSTANTIATE_TEST_CASE_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)));
diff --git a/tests/sys/fs/fusefs/readdir.cc b/tests/sys/fs/fusefs/readdir.cc
new file mode 100644
index 0000000000000..24373bebf1bb1
--- /dev/null
+++ b/tests/sys/fs/fusefs/readdir.cc
@@ -0,0 +1,375 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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);
+}
+};
+
+/* 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);
+ const char dot[] = ".";
+ const char dotdot[] = "..";
+
+ 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);
+ /*
+ * fuse(4) doesn't actually set d_off, which is ok for now because
+ * nothing uses it.
+ */
+ //EXPECT_EQ(2000, de->d_off);
+ 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(3000, de->d_off);
+ 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) */
+TEST_F(Readdir, getdirentries)
+{
+ 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);
+}
+
+/*
+ * 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);
+}
+
+/* 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);
+}
+
+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 0000000000000..121c61b26ab6e
--- /dev/null
+++ b/tests/sys/fs/fusefs/readlink.cc
@@ -0,0 +1,123 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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);
+}
+
+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 0000000000000..d22aaf9b046c5
--- /dev/null
+++ b/tests/sys/fs/fusefs/release.cc
@@ -0,0 +1,224 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 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);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ fd2 = dup(fd);
+
+ 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);
+ EXPECT_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);
+ EXPECT_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);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ expect_release(ino, getpid(), O_WRONLY, 0);
+ fd2 = open(FULLPATH, O_WRONLY);
+ EXPECT_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);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(0, close(fd)) << strerror(errno);
+}
+
+/* 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.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_SETLKW, &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 0000000000000..7c9ee6e88d3ad
--- /dev/null
+++ b/tests/sys/fs/fusefs/releasedir.cc
@@ -0,0 +1,116 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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);
+ EXPECT_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 0000000000000..d3199fd726bb6
--- /dev/null
+++ b/tests/sys/fs/fusefs/rename.cc
@@ -0,0 +1,321 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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();
+ }
+
+ void expect_getattr(uint64_t ino, mode_t mode)
+ {
+ 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 = mode;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+ }
+
+};
+
+// 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_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
+ 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_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
+ 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_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
+ 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_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
+ 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_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
+ 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/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_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDSTDIR)
+ .WillRepeatedly(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;
+ })));
+ 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_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(FULLDSTPARENT, &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_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
+ 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 0000000000000..3c3e3ea4d155c
--- /dev/null
+++ b/tests/sys/fs/fusefs/rmdir.cc
@@ -0,0 +1,172 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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_getattr(uint64_t ino, mode_t mode)
+{
+ 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 = mode;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+}
+
+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;
+
+ ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
+
+ 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 = ino; // Must match nodeid
+ out.body.attr.attr.mode = S_IFDIR | 0755;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+ expect_lookup(RELPATH, ino);
+ expect_rmdir(FUSE_ROOT_ID, RELPATH, 0);
+ expect_forget(ino, 1, &sem);
+
+ 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_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
+ 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_getattr(1, S_IFDIR | 0755);
+ 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_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
+ 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 0000000000000..197f7ff33999d
--- /dev/null
+++ b/tests/sys/fs/fusefs/setattr.cc
@@ -0,0 +1,774 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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/stat.h>
+
+#include <fcntl.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Setattr : public FuseTest {};
+
+class RofsSetattr: public Setattr {
+public:
+virtual void SetUp() {
+ m_ro = true;
+ Setattr::SetUp();
+}
+};
+
+class Setattr_7_8: public Setattr {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 8;
+ Setattr::SetUp();
+}
+};
+
+
+/*
+ * 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";
+ void *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 = malloc(w0_size);
+ ASSERT_NE(nullptr, w0buf) << strerror(errno);
+ memset(w0buf, 'X', w0_size);
+
+ r0buf = malloc(r0_size);
+ ASSERT_NE(nullptr, r0buf) << strerror(errno);
+ r1buf = malloc(r1_size);
+ ASSERT_NE(nullptr, r1buf) << strerror(errno);
+
+ expected = malloc(r1_size);
+ ASSERT_NE(nullptr, expected) << strerror(errno);
+ memset(expected, 0, 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));
+ 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);
+
+ free(expected);
+ free(r1buf);
+ free(r0buf);
+ free(w0buf);
+}
+
+/* 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 &&
+ in.body.setattr.atimensec ==
+ newtimes[0].tv_nsec &&
+ (time_t)in.body.setattr.mtime ==
+ newtimes[1].tv_sec &&
+ 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 &&
+ 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);
+}
+
+/* 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 0000000000000..7a96355653554
--- /dev/null
+++ b/tests/sys/fs/fusefs/statfs.cc
@@ -0,0 +1,171 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 0000000000000..3141ace32d688
--- /dev/null
+++ b/tests/sys/fs/fusefs/symlink.cc
@@ -0,0 +1,178 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 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);
+}
+
+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 0000000000000..3471a67fec93b
--- /dev/null
+++ b/tests/sys/fs/fusefs/unlink.cc
@@ -0,0 +1,251 @@
+/*-
+ * Copyright (c) 2019 The FreeBSD Foundation
+ * All rights reserved.
+ *
+ * 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_getattr(uint64_t ino, mode_t mode)
+{
+ 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 = mode;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+}
+
+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_getattr(1, S_IFDIR | 0755);
+ 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;
+
+ 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 = ino; // Must match nodeid
+ out.body.attr.attr.mode = S_IFDIR | 0755;
+ out.body.attr.attr_valid = UINT64_MAX;
+ })));
+ /* Use nlink=2 so we don't get a FUSE_FORGET */
+ expect_lookup(RELPATH, ino, 1, 2);
+ expect_unlink(1, RELPATH, 0);
+
+ 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_getattr(1, S_IFDIR | 0755);
+ 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_getattr(1, S_IFDIR | 0755);
+ 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_getattr(1, S_IFDIR | 0755);
+ 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_getattr(1, S_IFDIR | 0755);
+ 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_getattr(1, S_IFDIR | 0755);
+ 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 0000000000000..c2451fc8bdc85
--- /dev/null
+++ b/tests/sys/fs/fusefs/utils.cc
@@ -0,0 +1,593 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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;
+
+/*
+ * Set the default max_write to a distinct value from MAXPHYS to catch bugs
+ * that confuse the two.
+ */
+const uint32_t default_max_write = MIN(libfuse_max_write, MAXPHYS / 2);
+
+
+/* Check that fusefs(4) is accessible and the current user can mount(2) */
+void check_environment()
+{
+ const char *devnode = "/dev/fuse";
+ 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);
+ }
+ }
+ sysctlbyname(usermount_node, &usermount_val, &usermount_size,
+ NULL, 0);
+ if (geteuid() != 0 && !usermount_val)
+ GTEST_SKIP() << "current user is not allowed to mount";
+}
+
+class FuseEnv: public Environment {
+ virtual void SetUp() {
+ }
+};
+
+void FuseTest::SetUp() {
+ const char *maxbcachebuf_node = "vfs.maxbcachebuf";
+ const char *maxphys_node = "kern.maxphys";
+ int val = 0;
+ size_t size = sizeof(val);
+
+ /*
+ * XXX check_environment should be called from FuseEnv::SetUp, but
+ * can't due to https://github.com/google/googletest/issues/2189
+ */
+ check_environment();
+ if (IsSkipped())
+ return;
+
+ ASSERT_EQ(0, sysctlbyname(maxbcachebuf_node, &val, &size, NULL, 0))
+ << strerror(errno);
+ m_maxbcachebuf = val;
+ ASSERT_EQ(0, sysctlbyname(maxphys_node, &val, &size, NULL, 0))
+ << strerror(errno);
+ m_maxphys = val;
+
+ try {
+ m_mock = new MockFS(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);
+ /*
+ * 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_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_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)
+{
+ 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) {
+ 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;
+
+ 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();
+ 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;
+}
+
+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 0000000000000..6fe870c091f68
--- /dev/null
+++ b/tests/sys/fs/fusefs/utils.hh
@@ -0,0 +1,236 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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);
+}
+
+extern const uint32_t libfuse_max_write;
+extern const uint32_t default_max_write;
+class FuseTest : public ::testing::Test {
+ protected:
+ 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_push_symlinks_in;
+ bool m_ro;
+ bool m_async;
+ bool m_noclusterr;
+ bool m_nointr;
+ unsigned m_time_gran;
+ MockFS *m_mock = NULL;
+ const static uint64_t FH = 0xdeadbeef1a7ebabe;
+
+ public:
+ int m_maxbcachebuf;
+ int m_maxphys;
+
+ FuseTest():
+ m_maxreadahead(0),
+ m_maxwrite(default_max_write),
+ m_init_flags(0),
+ m_allow_other(false),
+ m_default_permissions(false),
+ m_kernel_minor_version(FUSE_KERNEL_MINOR_VERSION),
+ m_pm(BLOCKING),
+ m_push_symlinks_in(false),
+ m_ro(false),
+ m_async(false),
+ m_noclusterr(false),
+ m_nointr(false),
+ m_time_gran(1)
+ {}
+
+ 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_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_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);
+
+ /*
+ * 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.
+ */
+ static void leak(int fd __unused) {}
+
+ /*
+ * Deliberately leak a DIR* pointer
+ *
+ * See comments for FuseTest::leak
+ */
+ static void leakdir(DIR* dirp __unused) {}
+};
diff --git a/tests/sys/fs/fusefs/write.cc b/tests/sys/fs/fusefs/write.cc
new file mode 100644
index 0000000000000..0f010d5648241
--- /dev/null
+++ b/tests/sys/fs/fusefs/write.cc
@@ -0,0 +1,1309 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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/stat.h>
+#include <sys/sysctl.h>
+#include <sys/time.h>
+#include <sys/uio.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:
+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);
+
+ 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);
+
+ 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;
+ })
+ ));
+}
+
+};
+
+sig_atomic_t Write::s_sigxfsz = 0;
+
+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() {
+ const char *node = "vfs.aio.enable_unsafe";
+ int val = 0;
+ size_t size = sizeof(val);
+
+ FuseTest::SetUp();
+
+ ASSERT_EQ(0, sysctlbyname(node, &val, &size, NULL, 0))
+ << strerror(errno);
+ if (!val)
+ GTEST_SKIP() <<
+ "vfs.aio.enable_unsafe must be set for this test";
+}
+};
+
+/* 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;
+ 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 = m_maxphys;
+ WriteBack::SetUp();
+ if (m_maxphys < 2 * DFLTPHYS)
+ GTEST_SKIP() << "MAXPHYS must be at least twice DFLTPHYS"
+ << " for this test";
+ if (m_maxphys < 2 * m_maxbcachebuf)
+ GTEST_SKIP() << "MAXPHYS must be at least twice maxbcachebuf"
+ << " for this test";
+}
+};
+
+void sigxfsz_handler(int __unused sig) {
+ Write::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);
+ EXPECT_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);
+ EXPECT_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 = (char*)calloc(1, oldsize);
+ ASSERT_NE(nullptr, oldcontents) << strerror(errno);
+ oldbuf = (char*)malloc(oldsize);
+ ASSERT_NE(nullptr, oldbuf) << strerror(errno);
+
+ 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);
+ EXPECT_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);
+}
+
+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);
+ EXPECT_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);
+ EXPECT_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);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(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);
+ EXPECT_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);
+ EXPECT_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_F(Write, 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;
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+
+ rl.rlim_cur = offset;
+ rl.rlim_max = 10 * offset;
+ ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno);
+ ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno);
+
+ fd = open(FULLPATH, O_WRONLY);
+
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(-1, pwrite(fd, CONTENTS, bufsize, offset));
+ EXPECT_EQ(EFBIG, errno);
+ EXPECT_EQ(1, s_sigxfsz);
+ leak(fd);
+}
+
+/*
+ * 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);
+ off_t orig_fsize = 10;
+ off_t truncated_fsize = 5;
+ off_t final_fsize = bufsize;
+ 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);
+ expect_getattr(ino, truncated_fsize);
+ expect_read(ino, 0, final_fsize, final_fsize, INITIAL, O_RDWR);
+ maybe_expect_write(ino, offset, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDWR);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, pwrite(fd, CONTENTS, bufsize, offset))
+ << strerror(errno);
+ leak(fd);
+}
+
+/*
+ * 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;
+ void *zeros, *expected;
+
+ len = getpagesize();
+
+ zeros = calloc(1, len);
+ ASSERT_NE(nullptr, zeros);
+ expected = calloc(1, len);
+ ASSERT_NE(nullptr, expected);
+ 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);
+ EXPECT_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
+
+ free(expected);
+ free(zeros);
+}
+
+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);
+ EXPECT_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);
+ EXPECT_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);
+}
+
+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);
+ EXPECT_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(Write, write_large)
+{
+ 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;
+ bufsize = halfbufsize * 2;
+ contents = (int*)malloc(bufsize);
+ ASSERT_NE(nullptr, contents);
+ 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);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, contents, bufsize)) << strerror(errno);
+ leak(fd);
+
+ free(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);
+ EXPECT_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);
+ EXPECT_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;
+ void *wbuf, *wbuf2x;
+ ssize_t bufsize = m_maxbcachebuf;
+ off_t filesize = 5 * bufsize;
+
+ wbuf = malloc(bufsize);
+ ASSERT_NE(nullptr, wbuf) << strerror(errno);
+ memset(wbuf, 'X', bufsize);
+ wbuf2x = malloc(2 * bufsize);
+ ASSERT_NE(nullptr, wbuf2x) << strerror(errno);
+ 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);
+}
+
+/*
+ * When clustering writes, an I/O error to any of the cluster's children should
+ * not panic the system on unmount
+ */
+/*
+ * Disabled because it panics.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=238565
+ */
+TEST_F(WriteCluster, DISABLED_cluster_write_err)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int i, fd;
+ void *wbuf;
+ ssize_t bufsize = m_maxbcachebuf;
+ off_t filesize = 4 * bufsize;
+
+ wbuf = malloc(bufsize);
+ ASSERT_NE(nullptr, wbuf) << strerror(errno);
+ 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);
+}
+
+/*
+ * 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);
+ EXPECT_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);
+ char readbuf[bufsize];
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_write(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDWR);
+ EXPECT_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);
+ char 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);
+ EXPECT_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);
+}
+
+/*
+ * 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);
+ EXPECT_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 */
+}
+
+/*
+ * 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];
+ void *zeros;
+
+ zeros = calloc(1, m_maxbcachebuf);
+ ASSERT_NE(nullptr, zeros);
+
+ 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);
+ EXPECT_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);
+ free(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;
+ void *readbuf, *zeros, *ones, *zeroones, *onezeros;
+
+ readbuf = malloc(bs);
+ ASSERT_NE(nullptr, readbuf) << strerror(errno);
+ zeros = calloc(1, 3 * bs);
+ ASSERT_NE(nullptr, zeros);
+ ones = calloc(1, 2 * bs);
+ ASSERT_NE(nullptr, ones);
+ memset(ones, 1, 2 * bs);
+ zeroones = calloc(1, bs);
+ ASSERT_NE(nullptr, zeroones);
+ memset((uint8_t*)zeroones + bs / 2, 1, bs / 2);
+ onezeros = calloc(1, bs);
+ ASSERT_NE(nullptr, onezeros);
+ memset(onezeros, 1, bs / 2);
+
+ expect_lookup(RELPATH, ino, fsize);
+ expect_open(ino, 0, 1);
+
+ fd = open(FULLPATH, O_RDWR);
+ EXPECT_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);
+ free(zeroones);
+ free(onezeros);
+ free(ones);
+ free(zeros);
+ free(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);
+ EXPECT_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);
+}
+
+/*
+ * 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);
+ EXPECT_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);
+}
+
+/* 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);
+ EXPECT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno);
+}
+
+/* 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);
+ EXPECT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno);
+}
+
+INSTANTIATE_TEST_CASE_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);
+ char readbuf[bufsize];
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_write(ino, 0, bufsize, bufsize, CONTENTS);
+
+ fd = open(FULLPATH, O_RDWR);
+ EXPECT_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);
+ EXPECT_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 0000000000000..a7f24bc33a77e
--- /dev/null
+++ b/tests/sys/fs/fusefs/xattr.cc
@@ -0,0 +1,641 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * 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 <string.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+const char FULLPATH[] = "mountpoint/some_file.txt";
+const char RELPATH[] = "some_file.txt";
+
+class Xattr: public FuseTest {
+public:
+void 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 expect_listxattr(uint64_t ino, uint32_t size, ProcessMockerT r)
+{
+ 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();
+}
+
+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 &&
+ 0 == strcmp(attr, a) &&
+ 0 == strcmp(value, v));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(r));
+}
+
+};
+
+class Getxattr: public Xattr {};
+class Listxattr: public Xattr {};
+class Removexattr: public Xattr {};
+class Setxattr: public Xattr {};
+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
+ */
+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 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
+ */
+TEST_F(Listxattr, erange)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_listxattr(ino, 0, ReturnErrno(ERANGE));
+
+ ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
+ ASSERT_EQ(ERANGE, errno);
+}
+
+/*
+ * 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;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) {
+ out.body.listxattr.size = 45;
+ SET_OUT_HEADER_LEN(out, listxattr);
+ }));
+
+ // TODO: fix the expected size after fixing the size calculation bug in
+ // fuse_vnop_listextattr. It should be exactly 45.
+ expect_listxattr(ino, 53,
+ ReturnImmediate([](auto in __unused, auto& out) {
+ const char l[] = "user.foo";
+ strlcpy((char*)out.body.bytes, l,
+ sizeof(out.body.bytes));
+ out.header.len = sizeof(fuse_out_header) + sizeof(l);
+ })
+ );
+
+ 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);
+ }));
+
+ // TODO: fix the expected size after fixing the size calculation bug in
+ // fuse_vnop_listextattr. It should be exactly 16000.
+ expect_listxattr(ino, 16008,
+ 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);
+ })
+ );
+
+ // TODO: fix the expected size after fixing the size calculation bug in
+ // fuse_vnop_listextattr.
+ expect_listxattr(ino, sizeof(attrs) + 8,
+ 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);
+ })
+ );
+
+ // TODO: fix the expected size after fixing the size calculation bug in
+ // fuse_vnop_listextattr.
+ expect_listxattr(ino, sizeof(attrs) + 8,
+ 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(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);
+}