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 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 +#include +} + +#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 +#include +#include +#include +#include +#include +} + +#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 +} + +#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 +#include +} + +#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 +#include +} + +#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 +#include +#include +} + +#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 +#include +#include +} + +#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 +#include +#include +} + +#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 +} + +#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 +} + +#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 +} + +#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 +} + +#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 +} + +#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 + +#include + +#include "fuse_kernel.h" +} + +#include + +#define TIME_T_MAX (std::numeric_limits::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 &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 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&); + + /* 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&)); + + /* 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 + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "mntopts.h" // for build_iovec +} + +#include + +#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 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 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 &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 +} + +#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 +#include +} + +#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 +#include +#include +#include +#include + +#include +#include +#include +} + +#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 { + 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 +#include +} + +#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 &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 ents(2); + vector 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 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 + +#include +} + +#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 +#include +} + +#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 +} + +#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 +#include +} + +#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 +} + +#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 + +#include +} + +#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 +#include +} + +#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 +} + +#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 +} + +#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 +#include +#include + +#include +#include + +#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 +#include +#include +#include +#include + +#include +#include +#include +} + +#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 +#include +#include +} + +#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); +}