Page MenuHomeFreeBSD

D19752.id.diff
No OneTemporary

D19752.id.diff

This file is larger than 256 KB, so syntax highlighting was skipped.
Index: etc/mtree/BSD.tests.dist
===================================================================
--- etc/mtree/BSD.tests.dist
+++ etc/mtree/BSD.tests.dist
@@ -713,6 +713,8 @@
file
..
fs
+ fusefs
+ ..
tmpfs
..
..
Index: sys/fs/fuse/fuse_kernel.h
===================================================================
--- sys/fs/fuse/fuse_kernel.h
+++ sys/fs/fuse/fuse_kernel.h
@@ -194,6 +194,11 @@
struct fuse_attr attr;
};
+struct fuse_mknod_in {
+ __u32 mode;
+ __u32 rdev;
+};
+
struct fuse_mkdir_in {
__u32 mode;
__u32 padding;
Index: tests/sys/fs/Makefile
===================================================================
--- tests/sys/fs/Makefile
+++ tests/sys/fs/Makefile
@@ -7,6 +7,7 @@
TESTSRC= ${SRCTOP}/contrib/netbsd-tests/fs
#TESTS_SUBDIRS+= nullfs # XXX: needs rump
+TESTS_SUBDIRS+= fusefs
TESTS_SUBDIRS+= tmpfs
${PACKAGE}FILES+= h_funcs.subr
Index: tests/sys/fs/fusefs/Makefile
===================================================================
--- tests/sys/fs/fusefs/Makefile
+++ tests/sys/fs/fusefs/Makefile
@@ -0,0 +1,210 @@
+# $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+= create
+GTESTS+= default_permissions
+GTESTS+= destroy
+GTESTS+= flush
+GTESTS+= fsync
+GTESTS+= fsyncdir
+GTESTS+= getattr
+GTESTS+= interrupt
+GTESTS+= link
+GTESTS+= locks
+GTESTS+= lookup
+GTESTS+= mkdir
+GTESTS+= mknod
+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
+
+SRCS.access+= access.cc
+SRCS.access+= getmntopts.c
+SRCS.access+= mockfs.cc
+SRCS.access+= utils.cc
+
+SRCS.allow_other+= allow_other.cc
+SRCS.allow_other+= getmntopts.c
+SRCS.allow_other+= mockfs.cc
+SRCS.allow_other+= utils.cc
+
+SRCS.create+= create.cc
+SRCS.create+= getmntopts.c
+SRCS.create+= mockfs.cc
+SRCS.create+= utils.cc
+
+SRCS.default_permissions+= default_permissions.cc
+SRCS.default_permissions+= getmntopts.c
+SRCS.default_permissions+= mockfs.cc
+SRCS.default_permissions+= utils.cc
+TEST_METADATA.default_permissions+= required_user="unprivileged"
+
+SRCS.destroy+= destroy.cc
+SRCS.destroy+= getmntopts.c
+SRCS.destroy+= mockfs.cc
+SRCS.destroy+= utils.cc
+
+SRCS.flush+= flush.cc
+SRCS.flush+= getmntopts.c
+SRCS.flush+= mockfs.cc
+SRCS.flush+= utils.cc
+
+SRCS.fsync+= fsync.cc
+SRCS.fsync+= getmntopts.c
+SRCS.fsync+= mockfs.cc
+SRCS.fsync+= utils.cc
+
+SRCS.fsyncdir+= fsyncdir.cc
+SRCS.fsyncdir+= getmntopts.c
+SRCS.fsyncdir+= mockfs.cc
+SRCS.fsyncdir+= utils.cc
+
+SRCS.getattr+= getattr.cc
+SRCS.getattr+= getmntopts.c
+SRCS.getattr+= mockfs.cc
+SRCS.getattr+= utils.cc
+
+SRCS.interrupt+= interrupt.cc
+SRCS.interrupt+= getmntopts.c
+SRCS.interrupt+= mockfs.cc
+SRCS.interrupt+= utils.cc
+
+SRCS.link+= getmntopts.c
+SRCS.link+= link.cc
+SRCS.link+= mockfs.cc
+SRCS.link+= utils.cc
+
+SRCS.locks+= locks.cc
+SRCS.locks+= getmntopts.c
+SRCS.locks+= mockfs.cc
+SRCS.locks+= utils.cc
+
+SRCS.lookup+= getmntopts.c
+SRCS.lookup+= lookup.cc
+SRCS.lookup+= mockfs.cc
+SRCS.lookup+= utils.cc
+
+SRCS.mkdir+= getmntopts.c
+SRCS.mkdir+= mockfs.cc
+SRCS.mkdir+= mkdir.cc
+SRCS.mkdir+= utils.cc
+
+SRCS.mknod+= getmntopts.c
+SRCS.mknod+= mockfs.cc
+SRCS.mknod+= mknod.cc
+SRCS.mknod+= utils.cc
+TEST_METADATA.mknod+= required_user="root"
+
+SRCS.open+= getmntopts.c
+SRCS.open+= mockfs.cc
+SRCS.open+= open.cc
+SRCS.open+= utils.cc
+
+SRCS.opendir+= getmntopts.c
+SRCS.opendir+= mockfs.cc
+SRCS.opendir+= opendir.cc
+SRCS.opendir+= utils.cc
+
+SRCS.read+= getmntopts.c
+SRCS.read+= mockfs.cc
+SRCS.read+= read.cc
+SRCS.read+= utils.cc
+
+SRCS.readdir+= getmntopts.c
+SRCS.readdir+= mockfs.cc
+SRCS.readdir+= readdir.cc
+SRCS.readdir+= utils.cc
+
+SRCS.readlink+= getmntopts.c
+SRCS.readlink+= mockfs.cc
+SRCS.readlink+= readlink.cc
+SRCS.readlink+= utils.cc
+
+SRCS.release+= getmntopts.c
+SRCS.release+= mockfs.cc
+SRCS.release+= release.cc
+SRCS.release+= utils.cc
+
+SRCS.releasedir+= getmntopts.c
+SRCS.releasedir+= mockfs.cc
+SRCS.releasedir+= releasedir.cc
+SRCS.releasedir+= utils.cc
+
+SRCS.rename+= getmntopts.c
+SRCS.rename+= mockfs.cc
+SRCS.rename+= rename.cc
+SRCS.rename+= utils.cc
+
+SRCS.rmdir+= getmntopts.c
+SRCS.rmdir+= mockfs.cc
+SRCS.rmdir+= rmdir.cc
+SRCS.rmdir+= utils.cc
+
+SRCS.setattr+= getmntopts.c
+SRCS.setattr+= mockfs.cc
+SRCS.setattr+= setattr.cc
+SRCS.setattr+= utils.cc
+
+SRCS.statfs+= getmntopts.c
+SRCS.statfs+= mockfs.cc
+SRCS.statfs+= statfs.cc
+SRCS.statfs+= utils.cc
+
+SRCS.symlink+= getmntopts.c
+SRCS.symlink+= mockfs.cc
+SRCS.symlink+= symlink.cc
+SRCS.symlink+= utils.cc
+
+SRCS.unlink+= getmntopts.c
+SRCS.unlink+= mockfs.cc
+SRCS.unlink+= unlink.cc
+SRCS.unlink+= utils.cc
+
+SRCS.write+= getmntopts.c
+SRCS.write+= mockfs.cc
+SRCS.write+= write.cc
+SRCS.write+= utils.cc
+
+SRCS.xattr+= getmntopts.c
+SRCS.xattr+= mockfs.cc
+SRCS.xattr+= xattr.cc
+SRCS.xattr+= utils.cc
+
+# TODO: drastically increase timeout after test development is mostly complete
+TEST_METADATA+= timeout=10
+
+FUSEFS= ${.CURDIR:H:H:H:H}/sys/fs/fuse
+MOUNT= ${.CURDIR:H:H:H:H}/sbin/mount
+CFLAGS+= -I${.CURDIR:H:H:H}
+CFLAGS+= -I${FUSEFS}
+CFLAGS+= -I${MOUNT}
+.PATH: ${MOUNT}
+CXXSTD= c++14
+
+LIBADD+= pthread
+LIBADD+= gmock gtest
+LIBADD+= util
+
+WARNS?= 6
+
+.include <bsd.test.mk>
Index: tests/sys/fs/fusefs/access.cc
===================================================================
--- tests/sys/fs/fusefs/access.cc
+++ tests/sys/fs/fusefs/access.cc
@@ -0,0 +1,99 @@
+/*-
+ * 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);
+}
+};
+
+/* The error case of FUSE_ACCESS. */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236291 */
+TEST_F(Access, DISABLED_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_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.
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236557 */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236291 */
+TEST_F(Access, DISABLED_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_lookup(RELPATH, ino);
+ expect_access(ino, access_mode, ENOSYS);
+
+ ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
+ ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
+}
+
+/* The successful case of FUSE_ACCESS. */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236291 */
+TEST_F(Access, DISABLED_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_lookup(RELPATH, ino);
+ expect_access(ino, access_mode, 0);
+
+ ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
+}
Index: tests/sys/fs/fusefs/allow_other.cc
===================================================================
--- tests/sys/fs/fusefs/allow_other.cc
+++ tests/sys/fs/fusefs/allow_other.cc
@@ -0,0 +1,215 @@
+/*-
+ * 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/mman.h>
+#include <sys/wait.h>
+#include <fcntl.h>
+#include <pwd.h>
+#include <semaphore.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+void sighandler(int __unused sig) {}
+
+static void
+get_unprivileged_uid(int *uid)
+{
+ struct passwd *pw;
+
+ /*
+ * 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";
+ *uid = pw->pw_uid;
+}
+
+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";
+ }
+ get_unprivileged_uid(&m_uid);
+ if (IsSkipped())
+ return;
+
+ FuseTest::SetUp();
+}
+};
+
+class AllowOther: public NoAllowOther {
+
+public:
+virtual void SetUp() {
+ m_allow_other = true;
+ NoAllowOther::SetUp();
+}
+};
+
+TEST_F(AllowOther, allowed)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+ pid_t child;
+ sem_t *sem;
+ int mprot = PROT_READ | PROT_WRITE;
+ int mflags = MAP_ANON | MAP_SHARED;
+
+ 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;
+
+ ASSERT_EQ(0, sem_wait(sem)) << strerror(errno);
+
+ /* Drop privileges before accessing */
+ if (0 != setreuid(-1, m_uid)) {
+ perror("setreuid");
+ err = 1;
+ goto out;
+ }
+ fd = open(FULLPATH, O_RDONLY);
+ if (fd < 0) {
+ perror("open");
+ err = 1;
+ }
+
+out:
+ sem_destroy(sem);
+ _exit(err);
+ /* Deliberately leak fd */
+ } else if (child > 0) {
+ int child_status;
+
+ /*
+ * In parent. Cleanup must happen here, because it's still
+ * privileged.
+ */
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_open(ino, 0, 1);
+ expect_release(ino, 1, 0, 0);
+ /* Until the attr cache is working, we may send an additional
+ * GETATTR */
+ expect_getattr(ino, 0);
+ m_mock->m_child_pid = child;
+ /* Signal the child process to go */
+ ASSERT_EQ(0, sem_post(sem)) << strerror(errno);
+
+ wait(&child_status);
+ ASSERT_EQ(0, WEXITSTATUS(child_status));
+ } else {
+ FAIL() << strerror(errno);
+ }
+ munmap(sem, sizeof(*sem));
+}
+
+TEST_F(NoAllowOther, disallowed)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ int fd;
+ pid_t child;
+ sem_t *sem;
+ int mprot = PROT_READ | PROT_WRITE;
+ int mflags = MAP_ANON | MAP_SHARED;
+
+ 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;
+
+ ASSERT_EQ(0, sem_wait(sem)) << strerror(errno);
+
+ /* Drop privileges before accessing */
+ if (0 != setreuid(-1, m_uid)) {
+ perror("setreuid");
+ err = 1;
+ goto out;
+ }
+ fd = open(FULLPATH, O_RDONLY);
+ if (fd >= 0) {
+ fprintf(stderr, "open should've failed\n");
+ err = 1;
+ } else if (errno != EPERM) {
+ fprintf(stderr,
+ "Unexpected error: %s\n", strerror(errno));
+ err = 1;
+ }
+
+out:
+ sem_destroy(sem);
+ _exit(0);
+ /* Deliberately leak fd */
+ } else if (child > 0) {
+ /*
+ * In parent. Cleanup must happen here, because it's still
+ * privileged.
+ */
+ m_mock->m_child_pid = child;
+ /* Signal the child process to go */
+ ASSERT_EQ(0, sem_post(sem)) << strerror(errno);
+ int child_status;
+
+ wait(&child_status);
+ ASSERT_EQ(0, WEXITSTATUS(child_status));
+ } else {
+ FAIL() << strerror(errno);
+ }
+ munmap(sem, sizeof(*sem));
+}
Index: tests/sys/fs/fusefs/create.cc
===================================================================
--- tests/sys/fs/fusefs/create.cc
+++ tests/sys/fs/fusefs/create.cc
@@ -0,0 +1,364 @@
+/*-
+ * 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 {};
+
+/*
+ * If FUSE_CREATE sets the attr_valid, then subsequent GETATTRs should use the
+ * attribute cache
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235775 */
+TEST_F(Create, DISABLED_attr_cache)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = 0755;
+ uint64_t ino = 42;
+ int fd;
+
+ EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ 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 &&
+ (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 | 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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 = 0755;
+
+ EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ 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 &&
+ (0 == strcmp(RELPATH, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236236 */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236557 */
+TEST_F(Create, DISABLED_Enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = 0755;
+ uint64_t ino = 42;
+ int fd;
+
+ EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ 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 &&
+ (0 == strcmp(RELPATH, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(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, create);
+ out->body.create.entry.attr.mode = S_IFREG | 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_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);
+ })));
+
+ /* Until the attr cache is working, we may send an additional GETATTR */
+ 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; // Must match nodeid
+ out->body.attr.attr.mode = S_IFREG | 0644;
+ })));
+
+ fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
+ EXPECT_LE(0, fd) << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * Creating a new file after FUSE_LOOKUP returned a negative cache entry
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236231 */
+TEST_F(Create, DISABLED_entry_cache_negative)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = 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(1, 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_CREATE &&
+ (0 == strcmp(RELPATH, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto out) {
+ SET_OUT_HEADER_LEN(out, create);
+ out->body.create.entry.attr.mode = S_IFREG | mode;
+ out->body.create.entry.nodeid = ino;
+ out->body.create.entry.entry_valid = UINT64_MAX;
+ out->body.create.entry.attr_valid = UINT64_MAX;
+ })));
+
+ /* Until the attr cache is working, we may send an additional GETATTR */
+ 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; // Must match nodeid
+ out->body.attr.attr.mode = S_IFREG | 0644;
+ })));
+
+ fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
+ ASSERT_LE(0, fd) << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * Creating a new file should purge any negative namecache entries
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236231 */
+TEST_F(Create, DISABLED_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;
+ 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(1, RELPATH).Times(1)
+ .WillOnce(Invoke(ReturnNegativeCache(&entry_valid)))
+ .RetiresOnSaturation();
+
+ /* Then the CREATE 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_CREATE &&
+ (0 == strcmp(RELPATH, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto out) {
+ SET_OUT_HEADER_LEN(out, create);
+ out->body.create.entry.attr.mode = S_IFREG | mode;
+ out->body.create.entry.nodeid = ino;
+ out->body.create.entry.attr_valid = UINT64_MAX;
+ })));
+
+ /* Until the attr cache is working, we may send an additional GETATTR */
+ 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; // Must match nodeid
+ out->body.attr.attr.mode = S_IFREG | 0644;
+ })));
+
+ 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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 = 0755;
+
+ EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ 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 &&
+ (0 == strcmp(RELPATH, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(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 = 0755;
+ uint64_t ino = 42;
+ int fd;
+
+ EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ 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 &&
+ (0 == strcmp(RELPATH, name)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto out) {
+ SET_OUT_HEADER_LEN(out, create);
+ out->body.create.entry.attr.mode = S_IFREG | mode;
+ out->body.create.entry.nodeid = ino;
+ out->body.create.entry.entry_valid = UINT64_MAX;
+ out->body.create.entry.attr_valid = UINT64_MAX;
+ })));
+
+ /* Until the attr cache is working, we may send an additional GETATTR */
+ 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; // Must match nodeid
+ out->body.attr.attr.mode = S_IFREG | 0644;
+ })));
+
+ fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
+ EXPECT_LE(0, fd) << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
Index: tests/sys/fs/fusefs/default_permissions.cc
===================================================================
--- tests/sys/fs/fusefs/default_permissions.cc
+++ tests/sys/fs/fusefs/default_permissions.cc
@@ -0,0 +1,138 @@
+/*-
+ * 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 <fcntl.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class DefaultPermissions: public FuseTest {
+
+virtual void SetUp() {
+ FuseTest::SetUp();
+
+ if (geteuid() == 0) {
+ GTEST_SKIP() << "This test requires an unprivileged user";
+ }
+ m_default_permissions = true;
+}
+
+public:
+void expect_lookup(const char *relpath, uint64_t ino, mode_t mode)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | mode, 0, 1);
+}
+
+};
+
+class Access: public DefaultPermissions {};
+class Open: public DefaultPermissions {};
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=216391 */
+TEST_F(Access, DISABLED_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_lookup(RELPATH, ino, S_IFREG | 0644);
+ /*
+ * 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);
+}
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236291 */
+TEST_F(Access, DISABLED_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_lookup(RELPATH, ino, S_IFREG | 0644);
+ expect_access(ino, access_mode, 0);
+ /*
+ * 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);
+}
+
+TEST_F(Open, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644);
+ expect_open(ino, 0, 1);
+ /* Until the attr cache is working, we may send an additional GETATTR */
+ expect_getattr(ino, 0);
+
+ fd = open(FULLPATH, O_RDONLY);
+ EXPECT_LE(0, fd) << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=216391 */
+TEST_F(Open, DISABLED_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);
+ /*
+ * Once default_permissions is properly implemented, there might be
+ * another FUSE_GETATTR or something in here. But there should not be
+ * a FUSE_ACCESS
+ */
+
+ EXPECT_NE(0, open(FULLPATH, O_RDWR));
+ EXPECT_EQ(EPERM, errno);
+}
Index: tests/sys/fs/fusefs/destroy.cc
===================================================================
--- tests/sys/fs/fusefs/destroy.cc
+++ tests/sys/fs/fusefs/destroy.cc
@@ -0,0 +1,92 @@
+/*-
+ * 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.
+ */
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Destroy: public FuseTest {
+public:
+void expect_destroy(int error)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in->header.opcode == FUSE_DESTROY);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(error)));
+}
+
+void expect_forget(uint64_t ino, uint64_t nlookup)
+{
+ 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) {
+ /* FUSE_FORGET has no response! */
+ }));
+}
+};
+
+/*
+ * On unmount the kernel should send a FUSE_DESTROY operation. It should also
+ * send FUSE_FORGET operations for all inodes with lookup_count > 0. It's hard
+ * to trigger FUSE_FORGET in way except by unmounting, so this is the only
+ * testing that FUSE_FORGET gets.
+ */
+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(1, 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();
+}
Index: tests/sys/fs/fusefs/flush.cc
===================================================================
--- tests/sys/fs/fusefs/flush.cc
+++ tests/sys/fs/fusefs/flush.cc
@@ -0,0 +1,231 @@
+/*-
+ * 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)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1);
+}
+
+/*
+ * 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 a file descriptor is duplicated, every close causes FLUSH */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236405 */
+TEST_F(Flush, DISABLED_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);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, 0);
+ expect_flush(ino, 2, 0, ReturnErrno(0));
+ expect_release();
+
+ fd = open(FULLPATH, O_WRONLY);
+ 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 */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236405 */
+TEST_F(Flush, DISABLED_eio)
+{
+ 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_getattr(ino, 0);
+ expect_flush(ino, 1, 0, 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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236405 */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236557 */
+TEST_F(Flush, DISABLED_enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd, fd2;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, 0);
+ /* On the 2nd close, FUSE_FLUSH won't be sent at all */
+ expect_flush(ino, 1, 0, ReturnErrno(ENOSYS));
+ expect_release();
+
+ fd = open(FULLPATH, O_WRONLY);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ fd2 = dup(fd);
+
+ EXPECT_EQ(0, close(fd2)) << strerror(errno);
+ EXPECT_EQ(0, close(fd)) << strerror(errno);
+}
+
+/* A FUSE_FLUSH should be sent on close(2) */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236405 */
+TEST_F(Flush, DISABLED_flush)
+{
+ 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_getattr(ino, 0);
+ expect_flush(ino, 1, 0, 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.
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236405 */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234581 */
+TEST_F(FlushWithLocks, DISABLED_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);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, 0);
+ 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(ReturnImmediate([=](auto in, auto out) {
+ SET_OUT_HEADER_LEN(out, setlk);
+ out->body.setlk.lk = in->body.setlk.lk;
+ })));
+ 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 = dup(fd);
+ ASSERT_EQ(0, close(fd2)) << strerror(errno);
+ /* Deliberately leak fd */
+}
Index: tests/sys/fs/fusefs/fsync.cc
===================================================================
--- tests/sys/fs/fusefs/fsync.cc
+++ tests/sys/fs/fusefs/fsync.cc
@@ -0,0 +1,313 @@
+/*-
+ * 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 &&
+ 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, contents);
+}
+
+};
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236473 */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236474 */
+TEST_F(Fsync, DISABLED_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_getattr(ino, 0);
+ 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);
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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_getattr(ino, 0);
+ 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_release(ino, 1, 0, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ close(fd);
+}
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236474 */
+TEST_F(Fsync, DISABLED_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_getattr(ino, 0);
+ 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);
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236474 */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236557 */
+TEST_F(Fsync, DISABLED_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_getattr(ino, 0);
+ 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));
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236474 */
+TEST_F(Fsync, DISABLED_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_getattr(ino, 0);
+ 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);
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236473 */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236474 */
+TEST_F(Fsync, DISABLED_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_getattr(ino, 0);
+ 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);
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* Fsync should sync a file with dirty metadata but clean data */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236473 */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236474 */
+TEST_F(Fsync, DISABLED_fsync_metadata_only)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+ mode_t mode = 0755;
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, 0);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in->header.opcode == FUSE_SETATTR);
+ }, 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 | mode;
+ })));
+
+ expect_fsync(ino, 0, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, fchmod(fd, mode)) << strerror(errno);
+ ASSERT_EQ(0, fsync(fd)) << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+// fsync()ing a file that isn't dirty should be a no-op
+TEST_F(Fsync, nop)
+{
+ 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_getattr(ino, 0);
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(0, fsync(fd)) << strerror(errno);
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+// TODO: ENOSYS test
Index: tests/sys/fs/fusefs/fsyncdir.cc
===================================================================
--- tests/sys/fs/fusefs/fsyncdir.cc
+++ tests/sys/fs/fusefs/fsyncdir.cc
@@ -0,0 +1,191 @@
+/*-
+ * 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 &&
+ //(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 */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236473 */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236474 */
+TEST_F(FsyncDir, DISABLED_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);
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236473 */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236474 */
+TEST_F(FsyncDir, DISABLED_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);
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236474 */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236557 */
+TEST_F(FsyncDir, DISABLED_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, FUSE_FSYNC_FDATASYNC, 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);
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236474 */
+TEST_F(FsyncDir, DISABLED_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);
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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)
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236473 */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236474 */
+TEST_F(FsyncDir, DISABLED_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);
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
Index: tests/sys/fs/fusefs/getattr.cc
===================================================================
--- tests/sys/fs/fusefs/getattr.cc
+++ tests/sys/fs/fusefs/getattr.cc
@@ -0,0 +1,185 @@
+/*-
+ * 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.
+ */
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Getattr : public FuseTest {};
+
+/*
+ * If getattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs
+ * should use the cached attributes, rather than query the daemon
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235775 */
+TEST_F(Getattr, DISABLED_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(1, 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;
+ })));
+ 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.
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235773 */
+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;
+ /*
+ * The timeout should be longer than the longest plausible time the
+ * daemon would take to complete a write(2) to /dev/fuse, but no longer.
+ */
+ long timeout_ns = 250'000'000;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
+ 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 = timeout_ns;
+ 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));
+ usleep(2 * timeout_ns / 1000);
+ /* Timeout has expire. stat(2) should requery the daemon */
+ EXPECT_EQ(0, stat(FULLPATH, &sb));
+}
+
+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);
+ 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);
+ 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;
+ })));
+
+ 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.
+ //EXPECT_EQ(, sb.st_birthtim);
+ //EXPECT_EQ(, sb.st_flags);
+
+ //FUSE can't set st_blksize until protocol 7.9
+}
Index: tests/sys/fs/fusefs/interrupt.cc
===================================================================
--- tests/sys/fs/fusefs/interrupt.cc
+++ tests/sys/fs/fusefs/interrupt.cc
@@ -0,0 +1,312 @@
+/*-
+ * 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 <signal.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+/* 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!\n");
+}
+
+void* killer(void* target) {
+ /*
+ * Sleep for awhile so we can be mostly confident that the main thread
+ * is already blocked in write(2)
+ */
+ usleep(250'000);
+ if (verbosity > 1)
+ printf("Signalling!\n");
+ pthread_kill(*(pthread_t*)target, SIGUSR2);
+
+ 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, 0, 1);
+}
+
+/*
+ * 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;
+ }));
+}
+
+void setup_interruptor(pthread_t self)
+{
+ ASSERT_EQ(0, signal(SIGUSR2, sigusr2_handler)) << strerror(errno);
+ ASSERT_EQ(0, pthread_create(&m_child, NULL, killer, (void*)self))
+ << strerror(errno);
+}
+
+void TearDown() {
+ if (m_child != NULL) {
+ pthread_join(m_child, NULL);
+ }
+
+ FuseTest::TearDown();
+}
+};
+
+/*
+ * 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(Interrupt, DISABLED_already_complete)
+{
+ 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);
+ pthread_t self;
+ uint64_t write_unique = 0;
+
+ self = pthread_self();
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, 0);
+ expect_write(ino, &write_unique);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([&](auto in) {
+ return (in->header.opcode == FUSE_INTERRUPT &&
+ in->body.interrupt.unique == write_unique);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([&](auto in, auto &out) {
+ // First complete the write request
+ auto out0 = new mockfs_buf_out;
+ out0->header.unique = write_unique;
+ SET_OUT_HEADER_LEN(out0, write);
+ out0->body.write.size = bufsize;
+ out.push_back(out0);
+
+ // Then, respond EAGAIN to the interrupt request
+ auto out1 = new mockfs_buf_out;
+ out1->header.unique = in->header.unique;
+ out1->header.error = -EAGAIN;
+ out1->header.len = sizeof(out1->header);
+ out.push_back(out1);
+ }));
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ setup_interruptor(self);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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(Interrupt, DISABLED_ignore)
+{
+ 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);
+ pthread_t self;
+ uint64_t write_unique;
+
+ self = pthread_self();
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, 0);
+ expect_write(ino, &write_unique);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in->header.opcode == FUSE_INTERRUPT &&
+ in->body.interrupt.unique == write_unique);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([&](auto in __unused, auto &out) {
+ // Ignore FUSE_INTERRUPT; respond to the FUSE_WRITE
+ auto out0 = new mockfs_buf_out;
+ out0->header.unique = write_unique;
+ SET_OUT_HEADER_LEN(out0, write);
+ out0->body.write.size = bufsize;
+ out.push_back(out0);
+ }));
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ setup_interruptor(self);
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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(Interrupt, DISABLED_in_progress)
+{
+ 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);
+ pthread_t self;
+ uint64_t write_unique;
+
+ self = pthread_self();
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, 0);
+ expect_write(ino, &write_unique);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in->header.opcode == FUSE_INTERRUPT &&
+ in->body.interrupt.unique == write_unique);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([&](auto in __unused, auto &out) {
+ auto out0 = new mockfs_buf_out;
+ out0->header.error = -EINTR;
+ out0->header.unique = write_unique;
+ out0->header.len = sizeof(out0->header);
+ out.push_back(out0);
+ }));
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ setup_interruptor(self);
+ ASSERT_EQ(-1, write(fd, CONTENTS, bufsize));
+ EXPECT_EQ(EINTR, errno);
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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(Interrupt, DISABLED_too_soon)
+{
+ 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);
+ pthread_t self;
+ uint64_t write_unique;
+
+ self = pthread_self();
+
+ expect_lookup(RELPATH, ino);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, 0);
+ expect_write(ino, &write_unique);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in->header.opcode == FUSE_INTERRUPT &&
+ in->body.interrupt.unique == write_unique);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(EAGAIN)))
+ .RetiresOnSaturation();
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in->header.opcode == FUSE_INTERRUPT &&
+ in->body.interrupt.unique == write_unique);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([&](auto in __unused, auto &out __unused) {
+ auto out0 = new mockfs_buf_out;
+ out0->header.error = -EINTR;
+ out0->header.unique = write_unique;
+ out0->header.len = sizeof(out0->header);
+ out.push_back(out0);
+ }));
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ setup_interruptor(self);
+ ASSERT_EQ(-1, write(fd, CONTENTS, bufsize));
+ EXPECT_EQ(EINTR, errno);
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
Index: tests/sys/fs/fusefs/link.cc
===================================================================
--- tests/sys/fs/fusefs/link.cc
+++ tests/sys/fs/fusefs/link.cc
@@ -0,0 +1,102 @@
+/*-
+ * 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_lookup(const char *relpath, uint64_t ino)
+{
+ FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1);
+}
+};
+
+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(1, 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";
+ uint64_t dst_ino = 42;
+ const uint64_t ino = 42;
+
+ EXPECT_LOOKUP(1, 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(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;
+ })));
+
+ EXPECT_EQ(0, link(FULLDST, FULLPATH)) << strerror(errno);
+}
Index: tests/sys/fs/fusefs/locks.cc
===================================================================
--- tests/sys/fs/fusefs/locks.cc
+++ tests/sys/fs/fusefs/locks.cc
@@ -0,0 +1,426 @@
+/*-
+ * 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"
+
+/* 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 GetlkFallback: public Fallback {};
+class Getlk: public Locks {};
+class SetlkFallback: public Fallback {};
+class Setlk: public Locks {};
+class SetlkwFallback: public Fallback {};
+class Setlkw: public Locks {};
+
+/*
+ * 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);
+ expect_getattr(ino, 0);
+
+ 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * If the filesystem has no locks that fit the description, the filesystem
+ * should return F_UNLCK
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234581 */
+TEST_F(Getlk, DISABLED_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_getattr(ino, 0);
+ 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* A different pid does have a lock */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234581 */
+TEST_F(Getlk, DISABLED_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_getattr(ino, 0);
+ 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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);
+ expect_getattr(ino, 0);
+
+ 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* Set a new lock with FUSE_SETLK */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234581 */
+TEST_F(Setlk, DISABLED_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_getattr(ino, 0);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in->header.opcode == FUSE_SETLK &&
+ in->header.nodeid == ino &&
+ in->body.setlk.fh == FH &&
+ in->body.setlk.owner == (uint32_t)pid &&
+ in->body.setlk.lk.start == 10 &&
+ in->body.setlk.lk.end == 1009 &&
+ in->body.setlk.lk.type == F_RDLCK &&
+ in->body.setlk.lk.pid == (uint64_t)pid);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto out) {
+ SET_OUT_HEADER_LEN(out, setlk);
+ out->body.setlk.lk = in->body.setlk.lk;
+ })));
+
+ 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* l_len = 0 is a flag value that means to lock until EOF */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234581 */
+TEST_F(Setlk, DISABLED_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_getattr(ino, 0);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in->header.opcode == FUSE_SETLK &&
+ in->header.nodeid == ino &&
+ in->body.setlk.fh == FH &&
+ in->body.setlk.owner == (uint32_t)pid &&
+ in->body.setlk.lk.start == 10 &&
+ in->body.setlk.lk.end == OFFSET_MAX &&
+ in->body.setlk.lk.type == F_RDLCK &&
+ in->body.setlk.lk.pid == (uint64_t)pid);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto out) {
+ SET_OUT_HEADER_LEN(out, setlk);
+ out->body.setlk.lk = in->body.setlk.lk;
+ })));
+
+ 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* Fail to set a new lock with FUSE_SETLK due to a conflict */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234581 */
+TEST_F(Setlk, DISABLED_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_getattr(ino, 0);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in->header.opcode == FUSE_SETLK &&
+ in->header.nodeid == ino &&
+ in->body.setlk.fh == FH &&
+ in->body.setlk.owner == (uint32_t)pid &&
+ in->body.setlk.lk.start == 10 &&
+ in->body.setlk.lk.end == 1009 &&
+ in->body.setlk.lk.type == F_RDLCK &&
+ in->body.setlk.lk.pid == (uint64_t)pid);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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);
+ expect_getattr(ino, 0);
+
+ 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234581 */
+TEST_F(Setlkw, DISABLED_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_getattr(ino, 0);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in->header.opcode == FUSE_SETLK &&
+ in->header.nodeid == ino &&
+ in->body.setlkw.fh == FH &&
+ in->body.setlkw.owner == (uint32_t)pid &&
+ in->body.setlkw.lk.start == 10 &&
+ in->body.setlkw.lk.end == 1009 &&
+ in->body.setlkw.lk.type == F_RDLCK &&
+ in->body.setlkw.lk.pid == (uint64_t)pid);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto out) {
+ SET_OUT_HEADER_LEN(out, setlkw);
+ out->body.setlkw.lk = in->body.setlkw.lk;
+ })));
+
+ 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
Index: tests/sys/fs/fusefs/lookup.cc
===================================================================
--- tests/sys/fs/fusefs/lookup.cc
+++ tests/sys/fs/fusefs/lookup.cc
@@ -0,0 +1,291 @@
+/*-
+ * 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 {};
+
+/*
+ * If lookup returns a non-zero cache timeout, then subsequent VOP_GETATTRs
+ * should use the cached attributes, rather than query the daemon
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235775 */
+TEST_F(Lookup, DISABLED_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(1, 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.
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235773 */
+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;
+ /*
+ * The timeout should be longer than the longest plausible time the
+ * daemon would take to complete a write(2) to /dev/fuse, but no longer.
+ */
+ long timeout_ns = 250'000'000;
+
+ EXPECT_LOOKUP(1, RELPATH)
+ .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 = timeout_ns;
+ out->body.entry.attr.ino = ino; // Must match nodeid
+ out->body.entry.attr.mode = S_IFREG | 0644;
+ })));
+ expect_getattr(ino, 0);
+
+ /* access(2) will issue a VOP_LOOKUP but not a VOP_GETATTR */
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+ usleep(2 * timeout_ns / 1000);
+ /* The cache has timed out; VOP_GETATTR should query the daemon*/
+ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
+}
+
+TEST_F(Lookup, enoent)
+{
+ const char FULLPATH[] = "mountpoint/does_not_exist";
+ const char RELPATH[] = "does_not_exist";
+
+ EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT)));
+ EXPECT_NE(0, access(FULLPATH, F_OK));
+ EXPECT_EQ(ENOENT, 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(1, 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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236226 */
+TEST_F(Lookup, DISABLED_entry_cache_negative)
+{
+ struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0};
+
+ EXPECT_LOOKUP(1, "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";
+ /*
+ * The timeout should be longer than the longest plausible time the
+ * daemon would take to complete a write(2) to /dev/fuse, but no longer.
+ */
+ struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 250'000'000};
+
+ EXPECT_LOOKUP(1, RELPATH).Times(2)
+ .WillRepeatedly(Invoke(ReturnNegativeCache(&entry_valid)));
+
+ EXPECT_NE(0, access(FULLPATH, F_OK));
+ EXPECT_EQ(ENOENT, errno);
+
+ usleep(2 * entry_valid.tv_nsec / 1000);
+
+ /* 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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235773 */
+TEST_F(Lookup, DISABLED_entry_cache_timeout)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ /*
+ * The timeout should be longer than the longest plausible time the
+ * daemon would take to complete a write(2) to /dev/fuse, but no longer.
+ */
+ long timeout_ns = 250'000'000;
+
+ EXPECT_LOOKUP(1, RELPATH).Times(2)
+ .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out->body.entry.entry_valid_nsec = timeout_ns;
+ out->body.entry.attr.mode = S_IFREG | 0644;
+ out->body.entry.nodeid = 14;
+ })));
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+ usleep(2 * timeout_ns / 1000);
+ /* The cache has timed out; VOP_LOOKUP should query the daemon*/
+ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
+}
+
+// TODO: export_support
+// After upgrading the protocol to 7.10, check that the kernel will only
+// attempt to lookup "." and ".." if the filesystem sets FUSE_EXPORT_SUPPORT in
+// the init flags. If not, then all lookups for those entries will return
+// ESTALE.
+
+TEST_F(Lookup, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+
+ EXPECT_LOOKUP(1, 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(1, 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);
+}
+
+
Index: tests/sys/fs/fusefs/mkdir.cc
===================================================================
--- tests/sys/fs/fusefs/mkdir.cc
+++ tests/sys/fs/fusefs/mkdir.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 <fcntl.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Mkdir: public FuseTest {};
+
+/*
+ * 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(1, 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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236231 */
+TEST_F(Mkdir, DISABLED_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(1, 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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236231 */
+TEST_F(Mkdir, DISABLED_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(1, 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;
+
+ EXPECT_LOOKUP(1, 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);
+ 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);
+}
Index: tests/sys/fs/fusefs/mknod.cc
===================================================================
--- tests/sys/fs/fusefs/mknod.cc
+++ tests/sys/fs/fusefs/mknod.cc
@@ -0,0 +1,138 @@
+/*-
+ * 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 Mknod: public FuseTest {
+
+public:
+
+virtual void SetUp() {
+ FuseTest::SetUp();
+
+ if (geteuid() != 0) {
+ GTEST_SKIP() << "Only root may use most mknod(2) variations";
+ }
+}
+
+/* Test an OK creation of a file with the given mode and device number */
+void test_ok(mode_t mode, dev_t dev) {
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+
+ EXPECT_LOOKUP(1, 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 == dev &&
+ (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 = mode;
+ out->body.create.entry.nodeid = ino;
+ out->body.create.entry.entry_valid = UINT64_MAX;
+ out->body.create.entry.attr_valid = UINT64_MAX;
+ out->body.create.entry.attr.rdev = dev;
+ })));
+ EXPECT_EQ(0, mknod(FULLPATH, mode, dev)) << strerror(errno);
+}
+
+};
+
+/*
+ * 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.
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236236 */
+TEST_F(Mknod, DISABLED_blk)
+{
+ test_ok(S_IFBLK | 0755, 0xfe00); /* /dev/vda's device number on Linux */
+}
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236236 */
+TEST_F(Mknod, DISABLED_chr)
+{
+ test_ok(S_IFCHR | 0755, 0x64); /* /dev/fuse's device number */
+}
+
+/*
+ * The daemon is responsible for checking file permissions (unless the
+ * default_permissions mount option was used)
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236236 */
+TEST_F(Mknod, DISABLED_eperm)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ mode_t mode = S_IFIFO | 0755;
+
+ EXPECT_LOOKUP(1, 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, mknod(FULLPATH, mode, 0));
+ EXPECT_EQ(EPERM, errno);
+}
+
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236236 */
+TEST_F(Mknod, DISABLED_fifo)
+{
+ test_ok(S_IFIFO | 0755, 0);
+}
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236236 */
+TEST_F(Mknod, DISABLED_whiteout)
+{
+ test_ok(S_IFWHT | 0755, 0);
+}
Index: tests/sys/fs/fusefs/mockfs.hh
===================================================================
--- tests/sys/fs/fusefs/mockfs.hh
+++ tests/sys/fs/fusefs/mockfs.hh
@@ -0,0 +1,243 @@
+/*-
+ * 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;
+};
+
+union fuse_payloads_in {
+ fuse_access_in access;
+ /* value is from fuse_kern_chan.c in fusefs-libs */
+ uint8_t bytes[0x21000 - sizeof(struct fuse_in_header)];
+ 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_create_out create;
+ /* The protocol places no limits on the size of bytes */
+ uint8_t bytes[0x20000];
+ fuse_entry_out entry;
+ fuse_lk_out getlk;
+ fuse_getxattr_out getxattr;
+ fuse_init_out init;
+ fuse_listxattr_out listxattr;
+ fuse_open_out open;
+ fuse_lk_out setlk;
+ fuse_lk_out setlkw;
+ 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 struct mockfs_buf_in *in,
+ std::vector<struct 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 struct mockfs_buf_in *in,
+ struct mockfs_buf_out *out)> f);
+
+/*
+ * 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 max_readahead filesystem option */
+ uint32_t m_maxreadahead;
+
+ /* pid of the test process */
+ pid_t m_pid;
+
+ /* 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<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*);
+
+ 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_max_write;
+
+ /* Create a new mockfs and mount it to a tempdir */
+ MockFS(int max_readahead, bool allow_other,
+ bool default_permissions, bool push_symlinks_in,
+ uint32_t flags);
+ virtual ~MockFS();
+
+ /* Kill the filesystem daemon without unmounting the filesystem */
+ void kill_daemon();
+
+ /* Process FUSE requests endlessly */
+ void loop();
+
+ /*
+ * 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<mockfs_buf_out*>&));
+
+ /* Gracefully unmount */
+ void unmount();
+};
Index: tests/sys/fs/fusefs/mockfs.cc
===================================================================
--- tests/sys/fs/fusefs/mockfs.cc
+++ tests/sys/fs/fusefs/mockfs.cc
@@ -0,0 +1,453 @@
+/*-
+ * 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/stat.h>
+#include <sys/uio.h>
+#include <sys/user.h>
+
+#include <fcntl.h>
+#include <libutil.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "mntopts.h" // for build_iovec
+}
+
+#include <gtest/gtest.h>
+
+#include "mockfs.hh"
+
+using namespace testing;
+
+int verbosity = 0;
+static sig_atomic_t quit = 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) {
+ auto out0 = new mockfs_buf_out;
+ out0->header.unique = in->header.unique;
+ out0->header.error = -error;
+ out0->header.len = sizeof(out0->header);
+ out.push_back(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 */
+ auto 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(out0);
+ });
+}
+
+ProcessMockerT
+ReturnImmediate(std::function<void(const struct mockfs_buf_in *in,
+ struct mockfs_buf_out *out)> f)
+{
+ return([=](auto in, auto &out) {
+ auto out0 = new mockfs_buf_out;
+ out0->header.unique = in->header.unique;
+ f(in, out0);
+ out.push_back(out0);
+ });
+}
+
+void sigint_handler(int __unused sig) {
+ quit = 1;
+}
+
+void debug_fuseop(const mockfs_buf_in *in)
+{
+ printf("%-11s ino=%2lu", opcode2opname(in->header.opcode),
+ in->header.nodeid);
+ if (verbosity > 1) {
+ printf(" uid=%5u gid=%5u pid=%5u unique=%lu len=%u",
+ in->header.uid, in->header.gid, in->header.pid,
+ in->header.unique, in->header.len);
+ }
+ switch (in->header.opcode) {
+ case FUSE_FLUSH:
+ printf(" lock_owner=%lu", in->body.flush.lock_owner);
+ break;
+ case FUSE_FORGET:
+ printf(" nlookup=%lu", 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_LOOKUP:
+ printf(" %s", in->body.lookup);
+ break;
+ case FUSE_OPEN:
+ printf(" flags=%#x mode=%#o",
+ in->body.open.flags, in->body.open.mode);
+ break;
+ case FUSE_OPENDIR:
+ printf(" flags=%#x mode=%#o",
+ in->body.opendir.flags, in->body.opendir.mode);
+ break;
+ case FUSE_READ:
+ printf(" offset=%lu size=%u", in->body.read.offset,
+ in->body.read.size);
+ break;
+ case FUSE_READDIR:
+ printf(" offset=%lu size=%u", in->body.readdir.offset,
+ in->body.readdir.size);
+ break;
+ case FUSE_RELEASE:
+ printf(" flags=%#x lock_owner=%lu",
+ 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=%zu", in->body.setattr.size);
+ if (in->body.setattr.valid & FATTR_ATIME)
+ printf(" atime=%zu.%u",
+ in->body.setattr.atime,
+ in->body.setattr.atimensec);
+ if (in->body.setattr.valid & FATTR_MTIME)
+ printf(" mtime=%zu.%u",
+ in->body.setattr.mtime,
+ in->body.setattr.mtimensec);
+ if (in->body.setattr.valid & FATTR_FH)
+ printf(" fh=%zu", in->body.setattr.fh);
+ break;
+ case FUSE_SETXATTR:
+ /*
+ * In theory neither the xattr name and value need be
+ * ASCII, but in this test suite they always are.
+ */
+ {
+ const char *attr = (const char*)in->body.bytes +
+ sizeof(fuse_setxattr_in);
+ const char *v = attr + strlen(attr) + 1;
+ printf(" %s=%s", attr, v);
+ }
+ break;
+ case FUSE_WRITE:
+ printf(" offset=%lu size=%u flags=%u",
+ in->body.write.offset, in->body.write.size,
+ in->body.write.write_flags);
+ break;
+ default:
+ break;
+ }
+ printf("\n");
+}
+
+MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions,
+ bool push_symlinks_in, uint32_t flags)
+{
+ struct iovec *iov = NULL;
+ int iovlen = 0;
+ char fdstr[15];
+ const bool trueval = true;
+
+ m_daemon_id = NULL;
+ m_maxreadahead = max_readahead;
+ quit = 0;
+
+ /*
+ * 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"));
+
+ m_fuse_fd = open("/dev/fuse", O_CLOEXEC | O_RDWR);
+ if (m_fuse_fd < 0)
+ throw(std::system_error(errno, std::system_category(),
+ "Couldn't open /dev/fuse"));
+ sprintf(fdstr, "%d", m_fuse_fd);
+
+ 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);
+ 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 (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);
+ signal(SIGUSR1, sigint_handler);
+ 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();
+ ::unmount("mountpoint", MNT_FORCE);
+ if (m_daemon_id != NULL) {
+ pthread_join(m_daemon_id, NULL);
+ m_daemon_id = NULL;
+ }
+ rmdir("mountpoint");
+}
+
+void MockFS::init(uint32_t flags) {
+ mockfs_buf_in *in;
+ mockfs_buf_out *out;
+
+ in = (mockfs_buf_in*) malloc(sizeof(*in));
+ ASSERT_TRUE(in != NULL);
+ out = (mockfs_buf_out*) malloc(sizeof(*out));
+ ASSERT_TRUE(out != NULL);
+
+ read_request(in);
+ ASSERT_EQ(FUSE_INIT, in->header.opcode);
+
+ memset(out, 0, sizeof(*out));
+ out->header.unique = in->header.unique;
+ out->header.error = 0;
+ out->body.init.major = FUSE_KERNEL_VERSION;
+ out->body.init.minor = FUSE_KERNEL_MINOR_VERSION;
+ out->body.init.flags = in->body.init.flags & flags;
+
+ /*
+ * 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".
+ */
+ uint32_t default_max_write = 32 * getpagesize() + 0x1000 - 4096;
+ /* For testing purposes, it should be distinct from MAXPHYS */
+ m_max_write = MIN(default_max_write, MAXPHYS / 2);
+ out->body.init.max_write = m_max_write;
+
+ out->body.init.max_readahead = m_maxreadahead;
+ SET_OUT_HEADER_LEN(out, init);
+ write(m_fuse_fd, out, out->header.len);
+
+ free(in);
+}
+
+void MockFS::kill_daemon() {
+ 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);
+ }
+}
+
+void MockFS::loop() {
+ mockfs_buf_in *in;
+ std::vector<mockfs_buf_out*> out;
+
+ in = (mockfs_buf_in*) malloc(sizeof(*in));
+ ASSERT_TRUE(in != NULL);
+ while (!quit) {
+ bzero(in, sizeof(*in));
+ read_request(in);
+ if (quit)
+ break;
+ if (verbosity > 0)
+ debug_fuseop(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.
+ */
+ process_default(in, out);
+ }
+ for (auto &it: out) {
+ ASSERT_TRUE(write(m_fuse_fd, it, it->header.len) > 0 ||
+ errno == EAGAIN)
+ << strerror(errno);
+ delete it;
+ }
+ out.clear();
+ }
+ free(in);
+}
+
+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<mockfs_buf_out*> &out)
+{
+ auto out0 = new mockfs_buf_out;
+ out0->header.unique = in->header.unique;
+ out0->header.error = -EOPNOTSUPP;
+ out0->header.len = sizeof(out0->header);
+ out.push_back(out0);
+}
+
+void MockFS::read_request(mockfs_buf_in *in) {
+ ssize_t res;
+
+ res = read(m_fuse_fd, in, sizeof(*in));
+ if (res < 0 && !quit)
+ perror("read");
+ ASSERT_TRUE(res >= (ssize_t)sizeof(in->header) || quit);
+}
+
+void* MockFS::service(void *pthr_data) {
+ MockFS *mock_fs = (MockFS*)pthr_data;
+
+ mock_fs->loop();
+
+ return (NULL);
+}
+
+void MockFS::unmount() {
+ ::unmount("mountpoint", 0);
+}
Index: tests/sys/fs/fusefs/open.cc
===================================================================
--- tests/sys/fs/fusefs/open.cc
+++ tests/sys/fs/fusefs/open.cc
@@ -0,0 +1,185 @@
+/*-
+ * 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 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);
+ })));
+
+ /* Until the attr cache is working, we may send an additional GETATTR */
+ 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; // Must match nodeid
+ out->body.attr.attr.mode = S_IFREG | 0644;
+ })));
+
+ fd = open(FULLPATH, os_flags);
+ EXPECT_LE(0, fd) << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+};
+
+
+/*
+ * 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);
+}
+
+/* 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);
+}
+
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236329 */
+TEST_F(Open, DISABLED_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);
+}
+
Index: tests/sys/fs/fusefs/opendir.cc
===================================================================
--- tests/sys/fs/fusefs/opendir.cc
+++ tests/sys/fs/fusefs/opendir.cc
@@ -0,0 +1,147 @@
+/*-
+ * 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);
+}
+};
+
+
+/*
+ * 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_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in->header.opcode == FUSE_OPENDIR &&
+ in->header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(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_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in->header.opcode == FUSE_OPENDIR &&
+ in->header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(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_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) {
+ SET_OUT_HEADER_LEN(out, open);
+ })));
+
+ EXPECT_LE(0, open(FULLPATH, O_DIRECTORY)) << 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_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);
+ })));
+
+ 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) {
+ SET_OUT_HEADER_LEN(out, open);
+ })));
+
+ errno = 0;
+ EXPECT_NE(NULL, opendir(FULLPATH)) << strerror(errno);
+}
Index: tests/sys/fs/fusefs/read.cc
===================================================================
--- tests/sys/fs/fusefs/read.cc
+++ tests/sys/fs/fusefs/read.cc
@@ -0,0 +1,739 @@
+/*-
+ * 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/socket.h>
+#include <sys/sysctl.h>
+#include <sys/uio.h>
+
+#include <aio.h>
+#include <fcntl.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 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<uint32_t> {
+ virtual void SetUp() {
+ m_maxreadahead = 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_getattr(ino, bufsize);
+ 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));
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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 = 4096;
+ struct aiocb iocb0, iocb1;
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, bufsize);
+ 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 &&
+ in->body.read.size == bufsize);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([](auto in __unused, auto &out __unused) {
+ /* 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 &&
+ in->body.read.size == bufsize);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+
+ 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
+ */
+ usleep(250'000);
+
+ /* Deliberately leak iocbs */
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * With the FUSE_ASYNC_READ mount option, fuse(4) may issue multiple
+ * simultaneous read requests on the same file handle.
+ */
+/*
+ * Disabled because we don't yet implement FUSE_ASYNC_READ. No bugzilla
+ * entry, because that's a feature request, not a bug.
+ */
+TEST_F(AsyncRead, DISABLED_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 = 4096;
+ struct aiocb iocb0, iocb1;
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, bufsize);
+ 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 &&
+ in->body.read.size == bufsize);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([](auto in __unused, auto &out __unused) {
+ /* 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 &&
+ in->body.read.size == bufsize);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke([](auto in __unused, auto &out __unused) {
+ /* 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);
+
+ /*
+ * Sleep for awhile to make sure the kernel has had a chance to issue
+ * both reads.
+ */
+ usleep(250'000);
+
+ /* Deliberately leak iocbs */
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* 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);
+ expect_getattr(ino, offset + 1000);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(0, pread(fd, buf, 0, offset)) << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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_getattr(ino, offset + bufsize);
+ 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));
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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_getattr(ino, offset + bufsize);
+ 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));
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+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_getattr(ino, bufsize);
+ 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * With the keep_cache option, the kernel may keep its read cache across
+ * multiple open(2)s.
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236560 */
+TEST_F(Read, DISABLED_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, 1);
+ expect_getattr(ino, bufsize);
+ 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_RDONLY);
+ 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);
+
+ /* Deliberately leak fd0 and 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, FOPEN_KEEP_CACHE, 1);
+ expect_getattr(ino, bufsize);
+ 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_RDONLY);
+ 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);
+
+ /* Deliberately leak fd0 and 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;
+ ssize_t bufsize = strlen(CONTENTS);
+ void *p;
+ //char buf[bufsize];
+
+ len = getpagesize();
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, bufsize);
+ /* mmap may legitimately try to read more data than is available */
+ 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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_getattr(ino, bufsize);
+ 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));
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+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_getattr(ino, offset + bufsize);
+ 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));
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+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_getattr(ino, bufsize);
+ 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));
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* If the filesystem allows it, the kernel should try to readahead */
+TEST_F(Read, default_readahead)
+{
+ 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;
+ /* hard-coded in fuse_internal.c */
+ size_t default_maxreadahead = 65536;
+ ssize_t filesize = default_maxreadahead * 2;
+ char *contents;
+ char buf[bufsize];
+ const char *contents1 = CONTENTS0 + bufsize;
+
+ contents = (char*)calloc(1, filesize);
+ ASSERT_NE(NULL, contents);
+ memmove(contents, CONTENTS0, strlen(CONTENTS0));
+
+ expect_lookup(RELPATH, ino, filesize);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, filesize);
+ expect_read(ino, 0, default_maxreadahead, default_maxreadahead,
+ 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));
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* 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;
+ ssize_t bufsize = strlen(CONTENTS);
+ char buf[bufsize];
+ int sp[2];
+ off_t sbytes;
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, bufsize);
+ /* Like mmap, sendfile may request more data than is available */
+ 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(bufsize, read(sp[0], buf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize));
+
+ close(sp[1]);
+ close(sp[0]);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* sendfile should fail gracefully if fuse declines the read */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236466 */
+TEST_F(Read, DISABLED_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_getattr(ino, bufsize);
+ 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]);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* fuse(4) should honor the filesystem's requested m_readahead parameter */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236472 */
+TEST_P(ReadAhead, DISABLED_readahead) {
+ 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];
+
+ ASSERT_TRUE(GetParam() < (uint32_t)m_maxbcachebuf)
+ << "Test assumes that max_readahead < maxbcachebuf";
+
+ contents = (char*)calloc(1, filesize);
+ ASSERT_NE(NULL, contents);
+ memmove(contents, CONTENTS0, strlen(CONTENTS0));
+
+ expect_lookup(RELPATH, ino, filesize);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, filesize);
+ /* fuse(4) should only read ahead the allowed amount */
+ expect_read(ino, 0, GetParam(), GetParam(), 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));
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+INSTANTIATE_TEST_CASE_P(RA, ReadAhead, ::testing::Values(0u, 2048u));
Index: tests/sys/fs/fusefs/readdir.cc
===================================================================
--- tests/sys/fs/fusefs/readdir.cc
+++ tests/sys/fs/fusefs/readdir.cc
@@ -0,0 +1,299 @@
+/*-
+ * 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);
+}
+
+void expect_readdir(uint64_t ino, uint64_t off, 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.offset == off);
+ }, Eq(true)),
+ _)
+ ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto out) {
+ struct fuse_dirent *fde = (struct fuse_dirent*)out->body.bytes;
+ 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*)
+ ((long*)fde + entsize / sizeof(long));
+ i++;
+ }
+ out->header.len += sizeof(out->header);
+ })));
+
+}
+};
+
+/* 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 = strlen(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 = strlen(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(NULL, dir) << strerror(errno);
+
+ errno = 0;
+ de = readdir(dir);
+ ASSERT_NE(NULL, 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(2, de->d_namlen);
+ EXPECT_EQ(0, strcmp("..", de->d_name));
+
+ errno = 0;
+ de = readdir(dir);
+ ASSERT_NE(NULL, de) << strerror(errno);
+ EXPECT_EQ(3ul, de->d_fileno);
+ //EXPECT_EQ(3000, de->d_off);
+ EXPECT_EQ(DT_DIR, de->d_type);
+ EXPECT_EQ(1, de->d_namlen);
+ EXPECT_EQ(0, strcmp(".", de->d_name));
+
+ ASSERT_EQ(NULL, readdir(dir));
+ ASSERT_EQ(0, errno);
+
+ /* Deliberately leak dir. RELEASEDIR will be tested separately */
+}
+
+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(NULL, dir) << strerror(errno);
+
+ errno = 0;
+ de = readdir(dir);
+ ASSERT_EQ(NULL, de);
+ ASSERT_EQ(EIO, errno);
+
+ /* Deliberately leak dir. RELEASEDIR will be tested separately */
+}
+
+/*
+ * 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(NULL, dir) << strerror(errno);
+ errno = 0;
+ ASSERT_EQ(NULL, readdir(dir));
+ ASSERT_EQ(0, errno);
+
+ /* Deliberately leak dir. RELEASEDIR will be tested separately */
+}
+
+/* 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(NULL, dir) << strerror(errno);
+
+ for (i=0; i < 128; i++) {
+ errno = 0;
+ de = readdir(dir);
+ ASSERT_NE(NULL, 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(NULL, de) << strerror(errno);
+ EXPECT_EQ(2 + (ino_t)i, de->d_fileno);
+ }
+
+ seekdir(dir, bookmark);
+ de = readdir(dir);
+ ASSERT_NE(NULL, de) << strerror(errno);
+ EXPECT_EQ(130ul, de->d_fileno);
+
+ /* Deliberately leak dir. RELEASEDIR will be tested separately */
+}
Index: tests/sys/fs/fusefs/readlink.cc
===================================================================
--- tests/sys/fs/fusefs/readlink.cc
+++ tests/sys/fs/fusefs/readlink.cc
@@ -0,0 +1,122 @@
+/*-
+ * 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((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(NULL, getcwd(wd, sizeof(wd))) << strerror(errno);
+ len = snprintf(want, sizeof(want), "%s/mountpoint%s", wd, dst);
+ ASSERT_LE(0, len) << strerror(errno);
+
+ EXPECT_EQ((ssize_t)len + 1, readlink(FULLPATH, buf, sizeof(buf)));
+ EXPECT_STREQ(want, buf);
+}
Index: tests/sys/fs/fusefs/release.cc
===================================================================
--- tests/sys/fs/fusefs/release.cc
+++ tests/sys/fs/fusefs/release.cc
@@ -0,0 +1,189 @@
+/*-
+ * 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);
+}
+};
+
+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_getattr(ino, 0);
+ expect_release(ino, 1, 0, 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_getattr(ino, 0);
+ expect_release(ino, 1, 0, EIO);
+
+ fd = open(FULLPATH, O_WRONLY);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ ASSERT_TRUE(0 == close(fd) || errno == EIO) << 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_getattr(ino, 0);
+ expect_release(ino, 2, 0, 0);
+
+ fd = open(FULLPATH, O_RDONLY);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ 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_getattr(ino, 0);
+ expect_release(ino, 1, 0, 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*/
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234581 */
+TEST_F(ReleaseWithLocks, DISABLED_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_getattr(ino, 0);
+ 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(ReturnImmediate([=](auto in, auto out) {
+ SET_OUT_HEADER_LEN(out, setlk);
+ out->body.setlk.lk = in->body.setlk.lk;
+ })));
+ expect_release(ino, 1, (uint64_t)pid, 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);
+}
Index: tests/sys/fs/fusefs/releasedir.cc
===================================================================
--- tests/sys/fs/fusefs/releasedir.cc
+++ tests/sys/fs/fusefs/releasedir.cc
@@ -0,0 +1,109 @@
+/*-
+ * 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 "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);
+}
+
+void 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));
+}
+};
+
+/* 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(NULL, dir) << strerror(errno);
+
+ dir2 = fdopendir(dup(dirfd(dir)));
+ ASSERT_NE(NULL, 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(NULL, dir) << strerror(errno);
+
+ ASSERT_EQ(0, closedir(dir)) << strerror(errno);
+}
Index: tests/sys/fs/fusefs/rename.cc
===================================================================
--- tests/sys/fs/fusefs/rename.cc
+++ tests/sys/fs/fusefs/rename.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 <stdlib.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Rename: public FuseTest {
+ public:
+ int tmpfd = -1;
+ char tmpfile[80] = "/tmp/fuse.rename.XXXXXX";
+
+ virtual void TearDown() {
+ if (tmpfd >= 0) {
+ close(tmpfd);
+ unlink(tmpfile);
+ }
+
+ FuseTest::TearDown();
+ }
+};
+
+// EINVAL, dst is subdir of src
+TEST_F(Rename, einval)
+{
+ const char FULLDST[] = "mountpoint/src/dst";
+ const char RELDST[] = "dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ uint64_t src_ino = 42;
+
+ expect_lookup(RELSRC, src_ino, S_IFDIR | 0755, 0, 2);
+ EXPECT_LOOKUP(src_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
+
+ ASSERT_NE(0, rename(FULLSRC, FULLDST));
+ ASSERT_EQ(EINVAL, errno);
+}
+
+// source does not exist
+TEST_F(Rename, enoent)
+{
+ const char FULLDST[] = "mountpoint/dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ // FUSE hardcodes the mountpoint to inode 1
+
+ EXPECT_LOOKUP(1, 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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236231 */
+TEST_F(Rename, DISABLED_entry_cache_negative)
+{
+ const char FULLDST[] = "mountpoint/dst";
+ const char RELDST[] = "dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ // FUSE hardcodes the mountpoint to inode 1
+ uint64_t dst_dir_ino = 1;
+ uint64_t ino = 42;
+ /*
+ * Set entry_valid = 0 because this test isn't concerned with whether
+ * or not we actually cache negative entries, only with whether we
+ * interpret negative cache responses correctly.
+ */
+ struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0};
+
+ expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
+ /* LOOKUP returns a negative cache entry for dst */
+ EXPECT_LOOKUP(1, 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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236231 */
+TEST_F(Rename, DISABLED_entry_cache_negative_purge)
+{
+ const char FULLDST[] = "mountpoint/dst";
+ const char RELDST[] = "dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ // FUSE hardcodes the mountpoint to inode 1
+ uint64_t dst_dir_ino = 1;
+ uint64_t ino = 42;
+ /*
+ * Set entry_valid = 0 because this test isn't concerned with whether
+ * or not we actually cache negative entries, only with whether we
+ * interpret negative cache responses correctly.
+ */
+ struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0};
+
+ expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
+ /* LOOKUP returns a negative cache entry for dst */
+ EXPECT_LOOKUP(1, 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(RELSRC, ino, S_IFREG | 0644, 0, 1);
+
+ ASSERT_EQ(0, access(FULLDST, F_OK)) << strerror(errno);
+}
+
+TEST_F(Rename, exdev)
+{
+ const char FULLB[] = "mountpoint/src";
+ const char RELB[] = "src";
+ // FUSE hardcodes the mountpoint to inode 1
+ uint64_t b_ino = 42;
+
+ tmpfd = mkstemp(tmpfile);
+ ASSERT_LE(0, tmpfd) << strerror(errno);
+
+ expect_lookup(RELB, b_ino, S_IFREG | 0644, 0, 2);
+
+ ASSERT_NE(0, rename(tmpfile, FULLB));
+ ASSERT_EQ(EXDEV, errno);
+
+ ASSERT_NE(0, rename(FULLB, tmpfile));
+ ASSERT_EQ(EXDEV, errno);
+}
+
+TEST_F(Rename, ok)
+{
+ const char FULLDST[] = "mountpoint/dst";
+ const char RELDST[] = "dst";
+ const char FULLSRC[] = "mountpoint/src";
+ const char RELSRC[] = "src";
+ // FUSE hardcodes the mountpoint to inode 1
+ uint64_t dst_dir_ino = 1;
+ uint64_t ino = 42;
+
+ expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
+ EXPECT_LOOKUP(1, 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);
+}
+
+// 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;
+ // FUSE hardcodes the mountpoint to inode 1
+ uint64_t dst_dir_ino = 1;
+ uint64_t ino = 42;
+
+ expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
+ expect_lookup(RELDST, dst_ino, S_IFREG | 0644, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *src = (const char*)in->body.bytes +
+ sizeof(fuse_rename_in);
+ const char *dst = src + strlen(src) + 1;
+ return (in->header.opcode == FUSE_RENAME &&
+ in->body.rename.newdir == dst_dir_ino &&
+ (0 == strcmp(RELDST, dst)) &&
+ (0 == strcmp(RELSRC, src)));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(0)));
+
+ ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
+}
Index: tests/sys/fs/fusefs/rmdir.cc
===================================================================
--- tests/sys/fs/fusefs/rmdir.cc
+++ tests/sys/fs/fusefs/rmdir.cc
@@ -0,0 +1,91 @@
+/*-
+ * 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 Rmdir: public FuseTest {
+public:
+void expect_lookup(const char *relpath, uint64_t ino)
+{
+ EXPECT_LOOKUP(1, relpath)
+ .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.nlink = 2;
+ })));
+}
+};
+
+TEST_F(Rmdir, enotempty)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+
+ expect_lookup(RELPATH, ino);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in->header.opcode == FUSE_RMDIR &&
+ 0 == strcmp(RELPATH, in->body.rmdir) &&
+ in->header.nodeid == 1);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENOTEMPTY)));
+
+ ASSERT_NE(0, rmdir(FULLPATH));
+ ASSERT_EQ(ENOTEMPTY, errno);
+}
+
+TEST_F(Rmdir, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+
+ expect_lookup(RELPATH, ino);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in->header.opcode == FUSE_RMDIR &&
+ 0 == strcmp(RELPATH, in->body.rmdir) &&
+ in->header.nodeid == 1);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(0)));
+
+ ASSERT_EQ(0, rmdir(FULLPATH)) << strerror(errno);
+}
Index: tests/sys/fs/fusefs/setattr.cc
===================================================================
--- tests/sys/fs/fusefs/setattr.cc
+++ tests/sys/fs/fusefs/setattr.cc
@@ -0,0 +1,512 @@
+/*-
+ * 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 {};
+
+
+/*
+ * If setattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs
+ * should use the cached attributes, rather than query the daemon
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235775 */
+TEST_F(Setattr, DISABLED_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(1, 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;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ /* In protocol 7.23, ctime will be changed too */
+ return (in->header.opcode == FUSE_SETATTR &&
+ in->header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out->body.attr.attr.ino = ino; // Must match nodeid
+ out->body.attr.attr.mode = S_IFREG | newmode;
+ })));
+ EXPECT_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(1, 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.mode = S_IFREG | oldmode;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ /* In protocol 7.23, ctime will be changed too */
+ 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);
+}
+
+/* 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(1, 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) {
+ /* In protocol 7.23, ctime will be changed too */
+ 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(1, 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(1, 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);
+ })));
+
+ /* Until the attr cache is working, we may send an additional GETATTR */
+ 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; // Must match nodeid
+ out->body.attr.attr.mode = S_IFREG | oldmode;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ /* In protocol 7.23, ctime will be changed too */
+ 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* 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(1, 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;
+ })));
+
+ /* Until the attr cache is working, we may send an additional GETATTR */
+ 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; // Must match nodeid
+ out->body.attr.attr.mode = S_IFREG | 0755;
+ out->body.attr.attr.size = oldsize;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ /* In protocol 7.23, ctime will be changed too */
+ 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* 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(1, 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) {
+ /* In protocol 7.23, ctime will be changed too */
+ 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);
+}
+
+/* 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(1, 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;
+ })));
+
+ /*
+ * Until bug 235775 is fixed, utimensat will make an extra FUSE_GETATTR
+ * call
+ */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in->header.opcode == FUSE_GETATTR &&
+ 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 | 0644;
+ out->body.attr.attr.atime = oldtimes[0].tv_sec;
+ out->body.attr.attr.atimensec = oldtimes[0].tv_nsec;
+ out->body.attr.attr.mtime = oldtimes[1].tv_sec;
+ out->body.attr.attr.mtimensec = oldtimes[1].tv_nsec;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ /* In protocol 7.23, ctime will be changed too */
+ uint32_t valid = FATTR_ATIME | FATTR_MTIME;
+ return (in->header.opcode == FUSE_SETATTR &&
+ in->header.nodeid == ino &&
+ in->body.setattr.valid == valid &&
+ in->body.setattr.atime == newtimes[0].tv_sec &&
+ in->body.setattr.atimensec ==
+ newtimes[0].tv_nsec &&
+ 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(1, 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;
+ })));
+
+ /*
+ * Until bug 235775 is fixed, utimensat will make an extra FUSE_GETATTR
+ * call
+ */
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in->header.opcode == FUSE_GETATTR &&
+ 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 | 0644;
+ out->body.attr.attr.atime = oldtimes[0].tv_sec;
+ out->body.attr.attr.atimensec = oldtimes[0].tv_nsec;
+ out->body.attr.attr.mtime = oldtimes[1].tv_sec;
+ out->body.attr.attr.mtimensec = oldtimes[1].tv_nsec;
+ })));
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ /* In protocol 7.23, ctime will be changed too */
+ uint32_t valid = FATTR_MTIME;
+ return (in->header.opcode == FUSE_SETATTR &&
+ in->header.nodeid == ino &&
+ in->body.setattr.valid == valid &&
+ 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);
+}
Index: tests/sys/fs/fusefs/statfs.cc
===================================================================
--- tests/sys/fs/fusefs/statfs.cc
+++ tests/sys/fs/fusefs/statfs.cc
@@ -0,0 +1,118 @@
+/*-
+ * 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 "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(NULL, 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));
+}
+
+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(NULL, 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));
+}
Index: tests/sys/fs/fusefs/symlink.cc
===================================================================
--- tests/sys/fs/fusefs/symlink.cc
+++ tests/sys/fs/fusefs/symlink.cc
@@ -0,0 +1,90 @@
+/*-
+ * 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 {};
+
+TEST_F(Symlink, enospc)
+{
+ const char FULLPATH[] = "mountpoint/lnk";
+ const char RELPATH[] = "lnk";
+ const char dst[] = "dst";
+
+ EXPECT_LOOKUP(1, 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(1, 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(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;
+ })));
+
+ EXPECT_EQ(0, symlink(dst, FULLPATH)) << strerror(errno);
+}
Index: tests/sys/fs/fusefs/unlink.cc
===================================================================
--- tests/sys/fs/fusefs/unlink.cc
+++ tests/sys/fs/fusefs/unlink.cc
@@ -0,0 +1,102 @@
+/*-
+ * 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 "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Unlink: 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_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)));
+}
+
+};
+
+TEST_F(Unlink, eperm)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+
+ expect_lookup(RELPATH, ino, 1);
+ expect_unlink(1, RELPATH, EPERM);
+
+ ASSERT_NE(0, unlink(FULLPATH));
+ ASSERT_EQ(EPERM, errno);
+}
+
+TEST_F(Unlink, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+
+ expect_lookup(RELPATH, ino, 1);
+ expect_unlink(1, RELPATH, 0);
+
+ ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno);
+}
+
+/* Unlink an open file */
+TEST_F(Unlink, open_but_deleted)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+
+ expect_lookup(RELPATH, ino, 2);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, 0);
+ expect_unlink(1, RELPATH, 0);
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
Index: tests/sys/fs/fusefs/utils.hh
===================================================================
--- tests/sys/fs/fusefs/utils.hh
+++ tests/sys/fs/fusefs/utils.hh
@@ -0,0 +1,128 @@
+/*-
+ * 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.
+ */
+
+/*
+ * TODO: remove FUSE_WRITE_CACHE definition when upgrading to protocol 7.9.
+ * This bit was actually part of kernel protocol version 7.2, but never
+ * documented until 7.9
+ */
+#ifndef FUSE_WRITE_CACHE
+#define FUSE_WRITE_CACHE 1
+#endif
+
+class FuseTest : public ::testing::Test {
+ protected:
+ uint32_t m_maxreadahead;
+ uint32_t m_init_flags;
+ bool m_allow_other;
+ bool m_default_permissions;
+ bool m_push_symlinks_in;
+ MockFS *m_mock = NULL;
+ const static uint64_t FH = 0xdeadbeef1a7ebabe;
+
+ public:
+ int m_maxbcachebuf;
+
+ FuseTest():
+ /*
+ * libfuse's default max_readahead is UINT_MAX, though it can
+ * be lowered
+ */
+ m_maxreadahead(UINT_MAX),
+ m_init_flags(0),
+ m_allow_other(false),
+ m_default_permissions(false),
+ m_push_symlinks_in(false)
+ {}
+
+ virtual void SetUp();
+
+ virtual void TearDown() {
+ if (m_mock)
+ delete m_mock;
+ }
+
+ /*
+ * Create an expectation that FUSE_ACCESS will be called oncde 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);
+
+ /*
+ * 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. It will respond with inode ino, mode
+ * mode, filesize size, and cache validity forever.
+ */
+ void expect_lookup(const char *relpath, uint64_t ino, mode_t mode,
+ uint64_t size, int times);
+
+ /*
+ * Create an expectation that FUSE_GETATTR 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
+ */
+ void expect_read(uint64_t ino, uint64_t offset, uint64_t isize,
+ uint64_t osize, const void *contents);
+
+ /*
+ * Create an expectation that FUSE_RELEASE will be called times times
+ * for the given inode, returning error error
+ */
+ void expect_release(uint64_t ino, int times, uint64_t lock_owner,
+ int error);
+
+ /*
+ * Create an expectation that FUSE_WRITE will be called exactly once
+ * for the given inode, at offset offset, with write_flags flags,
+ * size isize and buffer contents. It will return osize
+ */
+ void expect_write(uint64_t ino, uint64_t offset, uint64_t isize,
+ uint64_t osize, uint32_t flags, const void *contents);
+};
Index: tests/sys/fs/fusefs/utils.cc
===================================================================
--- tests/sys/fs/fusefs/utils.cc
+++ tests/sys/fs/fusefs/utils.cc
@@ -0,0 +1,270 @@
+/*-
+ * 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.
+ */
+
+#include <sys/param.h>
+#include <sys/module.h>
+#include <sys/sysctl.h>
+
+#include <gtest/gtest.h>
+#include <unistd.h>
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+/* 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 *node = "vfs.maxbcachebuf";
+ 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(node, &val, &size, NULL, 0))
+ << strerror(errno);
+ m_maxbcachebuf = val;
+
+ try {
+ m_mock = new MockFS(m_maxreadahead, m_allow_other,
+ m_default_permissions, m_push_symlinks_in,
+ m_init_flags);
+ } 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_getattr(uint64_t ino, uint64_t size)
+{
+ /* Until the attr cache is working, we may send an additional GETATTR */
+ 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; // 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)
+{
+ EXPECT_LOOKUP(1, 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 = UINT64_MAX;
+ out->body.entry.attr.size = size;
+ })));
+}
+
+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)
+{
+ 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)
+{
+ 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);
+ }, 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_release(uint64_t ino, int times, uint64_t lock_owner,
+ 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);
+ }, Eq(true)),
+ _)
+ ).Times(times)
+ .WillRepeatedly(Invoke(ReturnErrno(error)));
+}
+void FuseTest::expect_write(uint64_t ino, uint64_t offset, uint64_t isize,
+ uint64_t osize, uint32_t flags, 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;
+
+ if (in->body.write.write_flags & 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 &&
+ in->body.write.write_flags == flags &&
+ 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;
+ })));
+}
+
+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());
+}
Index: tests/sys/fs/fusefs/write.cc
===================================================================
--- tests/sys/fs/fusefs/write.cc
+++ tests/sys/fs/fusefs/write.cc
@@ -0,0 +1,659 @@
+/*-
+ * 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/stat.h>
+#include <sys/sysctl.h>
+#include <sys/uio.h>
+
+#include <aio.h>
+#include <fcntl.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Write: 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);
+}
+
+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 require_sync_resize_0() {
+ const char *sync_resize_node = "vfs.fusefs.sync_resize";
+ int val = 0;
+ size_t size = sizeof(val);
+
+ ASSERT_EQ(0, sysctlbyname(sync_resize_node, &val, &size, NULL, 0))
+ << strerror(errno);
+ if (val != 0)
+ GTEST_SKIP() <<
+ "vfs.fusefs.sync_resize must be set to 0 for this test."
+ " That sysctl will probably be removed soon.";
+}
+
+};
+
+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 write-through cache mode */
+class WriteThrough: public Write {
+
+virtual void SetUp() {
+ const char *cache_mode_node = "vfs.fusefs.data_cache_mode";
+ int val = 0;
+ size_t size = sizeof(val);
+
+ FuseTest::SetUp();
+ if (IsSkipped())
+ return;
+
+ ASSERT_EQ(0, sysctlbyname(cache_mode_node, &val, &size, NULL, 0))
+ << strerror(errno);
+ if (val != 1)
+ GTEST_SKIP() << "vfs.fusefs.data_cache_mode must be set to 1 "
+ "(writethrough) for this test";
+}
+
+};
+
+/* Tests for the writeback cache mode */
+class WriteBack: public Write {
+
+virtual void SetUp() {
+ const char *node = "vfs.fusefs.data_cache_mode";
+ int val = 0;
+ size_t size = sizeof(val);
+
+ FuseTest::SetUp();
+ if (IsSkipped())
+ return;
+
+ ASSERT_EQ(0, sysctlbyname(node, &val, &size, NULL, 0))
+ << strerror(errno);
+ if (val != 2)
+ GTEST_SKIP() << "vfs.fusefs.data_cache_mode must be set to 2 "
+ "(writeback) for this test";
+}
+
+};
+
+/* 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_getattr(ino, 0);
+ expect_write(ino, offset, bufsize, bufsize, 0, CONTENTS);
+
+ fd = open(FULLPATH, O_WRONLY);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ iocb.aio_nbytes = bufsize;
+ iocb.aio_fildes = fd;
+ iocb.aio_buf = (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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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;
+
+ require_sync_resize_0();
+
+ expect_lookup(RELPATH, ino, initial_offset);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, initial_offset);
+ expect_write(ino, initial_offset, BUFSIZE, BUFSIZE, 0, 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+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_getattr(ino, initial_offset);
+ expect_write(ino, initial_offset, BUFSIZE, BUFSIZE, 0, CONTENTS);
+
+ fd = open(FULLPATH, O_WRONLY | O_APPEND);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(BUFSIZE, write(fd, CONTENTS, BUFSIZE)) << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* A direct write should evict any overlapping cached data */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235774 */
+TEST_F(Write, DISABLED_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_getattr(ino, bufsize);
+ expect_read(ino, 0, bufsize, bufsize, CONTENTS0);
+ expect_write(ino, 0, bufsize, bufsize, 0, 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);
+
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * When the direct_io option is used, filesystems are allowed to write less
+ * data than requested
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236381 */
+TEST_F(Write, DISABLED_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;
+ const char *halfcontents = CONTENTS + halfbufsize;
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, FOPEN_DIRECT_IO, 1);
+ expect_getattr(ino, 0);
+ expect_write(ino, 0, bufsize, halfbufsize, 0, CONTENTS);
+ expect_write(ino, halfbufsize, halfbufsize, halfbufsize, 0,
+ halfcontents);
+
+ fd = open(FULLPATH, O_WRONLY);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236381 */
+TEST_F(Write, DISABLED_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";
+ const char *EXPECTED1 = "hijklmnop";
+ 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_getattr(ino, 0);
+ expect_write(ino, 0, totalsize, size0, 0, EXPECTED0);
+ expect_write(ino, size0, size1, size1, 0, EXPECTED1);
+
+ fd = open(FULLPATH, O_WRONLY);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ iov[0].iov_base = (void*)CONTENTS0;
+ iov[0].iov_len = strlen(CONTENTS0);
+ iov[1].iov_base = (void*)CONTENTS1;
+ iov[1].iov_len = strlen(CONTENTS1);
+ ASSERT_EQ(totalsize, writev(fd, iov, 2)) << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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 */
+// TODO: check vfs.fusefs.mmap_enable
+TEST_F(Write, DISABLED_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(NULL, zeros);
+ expected = calloc(1, len);
+ ASSERT_NE(NULL, expected);
+ memmove((uint8_t*)expected + offset, CONTENTS, bufsize);
+
+ expect_lookup(RELPATH, ino, len);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, len);
+ 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
+ */
+ expect_write(ino, 0, len, len, FUSE_WRITE_CACHE, expected);
+ 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 = 4096;
+ int fd;
+ ssize_t bufsize = strlen(CONTENTS);
+
+ expect_lookup(RELPATH, ino, 0);
+ expect_open(ino, 0, 1);
+ expect_getattr(ino, 0);
+ expect_write(ino, offset, bufsize, bufsize, 0, CONTENTS);
+
+ fd = open(FULLPATH, O_WRONLY);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, pwrite(fd, CONTENTS, bufsize, offset))
+ << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+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_getattr(ino, 0);
+ expect_write(ino, 0, bufsize, bufsize, 0, CONTENTS);
+
+ fd = open(FULLPATH, O_WRONLY);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* 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_max_write;
+ bufsize = halfbufsize * 2;
+ contents = (int*)malloc(bufsize);
+ ASSERT_NE(NULL, 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);
+ expect_getattr(ino, 0);
+ expect_write(ino, 0, halfbufsize, halfbufsize, 0, contents);
+ expect_write(ino, halfbufsize, halfbufsize, halfbufsize, 0,
+ &contents[halfbufsize / sizeof(int)]);
+
+ fd = open(FULLPATH, O_WRONLY);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, contents, bufsize)) << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+
+ 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);
+ expect_getattr(ino, 0);
+
+ fd = open(FULLPATH, O_WRONLY);
+ EXPECT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* In writeback mode, dirty data should be written on close */
+TEST_F(WriteBack, 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_getattr(ino, 0);
+ expect_write(ino, 0, bufsize, bufsize, 0, 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_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);
+}
+
+/*
+ * Without direct_io, writes should be committed to cache
+ */
+TEST_F(WriteBack, writeback)
+{
+ 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_getattr(ino, 0);
+ expect_write(ino, 0, bufsize, bufsize, 0, 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * 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);
+ expect_getattr(ino, 0);
+ expect_write(ino, 0, bufsize, bufsize, 0, 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/*
+ * Without direct_io, writes should be committed to cache
+ */
+/*
+ * Disabled because we don't yet implement write-through caching. No bugzilla
+ * entry, because that's a feature request, not a bug.
+ */
+TEST_F(WriteThrough, DISABLED_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_getattr(ino, 0);
+ expect_write(ino, 0, bufsize, bufsize, 0, 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(bufsize, read(fd, readbuf, bufsize)) << strerror(errno);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
+
+/* With writethrough caching, writes update the cached file size */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235775 */
+TEST_F(WriteThrough, DISABLED_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_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 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 = 0;
+ out->body.attr.attr_valid = UINT64_MAX;
+ })));
+ expect_write(ino, 0, bufsize, bufsize, 0, 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);
+ /* Deliberately leak fd. close(2) will be tested in release.cc */
+}
Index: tests/sys/fs/fusefs/xattr.cc
===================================================================
--- tests/sys/fs/fusefs/xattr.cc
+++ tests/sys/fs/fusefs/xattr.cc
@@ -0,0 +1,605 @@
+/*-
+ * 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 <string.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+const char FULLPATH[] = "mountpoint/some_file.txt";
+const char RELPATH[] = "some_file.txt";
+
+/* For testing filesystems without posix locking support */
+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 {};
+
+/*
+ * 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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236557 */
+TEST_F(Getxattr, DISABLED_enosys)
+{
+ 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(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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236557 */
+TEST_F(Listxattr, DISABLED_enosys)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ 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((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((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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236557 */
+TEST_F(Removexattr, DISABLED_enosys)
+{
+ uint64_t ino = 42;
+ int ns = EXTATTR_NAMESPACE_USER;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ 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
+ */
+/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236557 */
+TEST_F(Setxattr, DISABLED_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, 1);
+ expect_setxattr(ino, "user.foo", value, ReturnErrno(ENOSYS));
+
+ r = extattr_set_file(FULLPATH, ns, "foo", (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", (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", (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", (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", (void*)value, value_len);
+ ASSERT_EQ(value_len, r) << strerror(errno);
+}

File Metadata

Mime Type
text/plain
Expires
Mon, Apr 20, 5:30 AM (6 h, 37 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
31821313
Default Alt Text
D19752.id.diff (272 KB)

Event Timeline