diff options
Diffstat (limited to 'tests/sys/fs/fusefs')
52 files changed, 23472 insertions, 0 deletions
diff --git a/tests/sys/fs/fusefs/Makefile b/tests/sys/fs/fusefs/Makefile new file mode 100644 index 000000000000..a21512798597 --- /dev/null +++ b/tests/sys/fs/fusefs/Makefile @@ -0,0 +1,100 @@ +.include <bsd.compiler.mk> + +PACKAGE= tests + +TESTSDIR= ${TESTSBASE}/sys/fs/fusefs + +ATF_TESTS_SH+= ctl + +# We could simply link all of these files into a single executable. But since +# Kyua treats googletest programs as plain tests, it's better to separate them +# out, so we get more granular reporting. +GTESTS+= access +GTESTS+= allow_other +GTESTS+= bad_server +GTESTS+= bmap +GTESTS+= cache +GTESTS+= copy_file_range +GTESTS+= create +GTESTS+= default_permissions +GTESTS+= default_permissions_privileged +GTESTS+= destroy +GTESTS+= dev_fuse_poll +GTESTS+= fallocate +GTESTS+= fifo +GTESTS+= flush +GTESTS+= forget +GTESTS+= fsync +GTESTS+= fsyncdir +GTESTS+= getattr +GTESTS+= interrupt +GTESTS+= io +GTESTS+= last_local_modify +GTESTS+= link +GTESTS+= locks +GTESTS+= lookup +GTESTS+= lseek +GTESTS+= mkdir +GTESTS+= mknod +GTESTS+= mount +GTESTS+= nfs +GTESTS+= notify +GTESTS+= open +GTESTS+= opendir +GTESTS+= pre-init +GTESTS+= read +GTESTS+= readdir +GTESTS+= readlink +GTESTS+= release +GTESTS+= releasedir +GTESTS+= rename +GTESTS+= rmdir +GTESTS+= setattr +GTESTS+= statfs +GTESTS+= symlink +GTESTS+= unlink +GTESTS+= write +GTESTS+= xattr + +.for p in ${GTESTS} +SRCS.$p+= ${p}.cc +SRCS.$p+= mockfs.cc +SRCS.$p+= utils.cc +.endfor + +TEST_METADATA.default_permissions+= required_user="unprivileged" +TEST_METADATA.default_permissions_privileged+= required_user="root" +TEST_METADATA.mknod+= required_user="root" +TEST_METADATA.nfs+= required_user="root" +# ctl must be exclusive because it disables/enables camsim +TEST_METADATA.ctl+= is_exclusive="true" +TEST_METADATA.ctl+= required_user="root" + +TEST_METADATA+= timeout=10 +TEST_METADATA+= required_kmods="fusefs" + +FUSEFS= ${SRCTOP}/sys/fs/fuse +# Suppress warnings that GCC generates for the libc++ and gtest headers. +CXXWARNFLAGS.gcc+= -Wno-placement-new -Wno-attributes +# Suppress Wcast-align for readdir.cc, because it is unavoidable when using +# getdirentries. +CXXWARNFLAGS.readdir.cc+= -Wno-cast-align +.if ${COMPILER_TYPE} == "gcc" && ${COMPILER_VERSION} >= 80000 +CXXWARNFLAGS+= -Wno-class-memaccess +.endif +# Suppress false warning about set but unused DNAME in inval_entry_below_root +CXXWARNFLAGS.gcc+= -Wno-unused-but-set-variable +# Suppress warnings about deprecated implicit copy constructors in gtest. +CXXWARNFLAGS+= -Wno-deprecated-copy +.if ${COMPILER_TYPE} == "clang" && ${COMPILER_VERSION} >= 180000 +# clang 18.0.0 introduces a new warning about variable length arrays in C++. +CXXWARNFLAGS+= -Wno-vla-cxx-extension +.endif +CXXFLAGS+= -I${SRCTOP}/tests +CXXFLAGS+= -I${FUSEFS} + +LIBADD+= pthread +LIBADD+= gmock gtest +LIBADD+= util + +.include <bsd.test.mk> diff --git a/tests/sys/fs/fusefs/access.cc b/tests/sys/fs/fusefs/access.cc new file mode 100644 index 000000000000..5762269fac7b --- /dev/null +++ b/tests/sys/fs/fusefs/access.cc @@ -0,0 +1,301 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/types.h> +#include <sys/extattr.h> + +#include <fcntl.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Access: public FuseTest { +public: +virtual void SetUp() { + FuseTest::SetUp(); + // Clear the default FUSE_ACCESS expectation + Mock::VerifyAndClearExpectations(m_mock); +} + +void expect_lookup(const char *relpath, uint64_t ino) +{ + FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1); +} + +/* + * Expect that FUSE_ACCESS will never be called for the given inode, with any + * bits in the supplied access_mask set + */ +void expect_noaccess(uint64_t ino, mode_t access_mask) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_ACCESS && + in.header.nodeid == ino && + in.body.access.mask & access_mask); + }, Eq(true)), + _) + ).Times(0); +} + +}; + +class RofsAccess: public Access { +public: +virtual void SetUp() { + m_ro = true; + Access::SetUp(); +} +}; + +/* + * Change the mode of a file. + * + * There should never be a FUSE_ACCESS sent for this operation, except for + * search permissions on the parent directory. + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689 + */ +TEST_F(Access, chmod) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const mode_t newmode = 0644; + + expect_access(FUSE_ROOT_ID, X_OK, 0); + expect_lookup(RELPATH, ino); + expect_noaccess(ino, 0); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | newmode; + }))); + + EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); +} + +/* + * Create a new file + * + * There should never be a FUSE_ACCESS sent for this operation, except for + * search permissions on the parent directory. + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689 + */ +TEST_F(Access, create) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + mode_t mode = S_IFREG | 0755; + uint64_t ino = 42; + + expect_access(FUSE_ROOT_ID, X_OK, 0); + expect_noaccess(FUSE_ROOT_ID, R_OK | W_OK); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_noaccess(ino, 0); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_CREATE); + }, Eq(true)), + _) + ).WillOnce(ReturnErrno(EPERM)); + + EXPECT_EQ(-1, open(FULLPATH, O_CREAT | O_EXCL, mode)); + EXPECT_EQ(EPERM, errno); +} + +/* The error case of FUSE_ACCESS. */ +TEST_F(Access, eaccess) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + mode_t access_mode = X_OK; + + expect_access(FUSE_ROOT_ID, X_OK, 0); + expect_lookup(RELPATH, ino); + expect_access(ino, access_mode, EACCES); + + ASSERT_NE(0, access(FULLPATH, access_mode)); + ASSERT_EQ(EACCES, errno); +} + +/* + * If the filesystem returns ENOSYS, then it is treated as a permanent success, + * and subsequent VOP_ACCESS calls will succeed automatically without querying + * the daemon. + */ +TEST_F(Access, enosys) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + mode_t access_mode = R_OK; + + expect_access(FUSE_ROOT_ID, X_OK, ENOSYS); + FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2); + + ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno); + ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno); +} + +TEST_F(RofsAccess, erofs) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + mode_t access_mode = W_OK; + + expect_access(FUSE_ROOT_ID, X_OK, 0); + expect_lookup(RELPATH, ino); + + ASSERT_NE(0, access(FULLPATH, access_mode)); + ASSERT_EQ(EROFS, errno); +} + + +/* + * Lookup an extended attribute + * + * There should never be a FUSE_ACCESS sent for this operation, except for + * search permissions on the parent directory. + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689 + */ +TEST_F(Access, Getxattr) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + char data[80]; + int ns = EXTATTR_NAMESPACE_USER; + ssize_t r; + + expect_access(FUSE_ROOT_ID, X_OK, 0); + expect_lookup(RELPATH, ino); + expect_noaccess(ino, 0); + expect_getxattr(ino, "user.foo", ReturnErrno(ENOATTR)); + + r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)); + ASSERT_EQ(-1, r); + ASSERT_EQ(ENOATTR, errno); +} + +/* The successful case of FUSE_ACCESS. */ +TEST_F(Access, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + mode_t access_mode = R_OK; + + expect_access(FUSE_ROOT_ID, X_OK, 0); + expect_lookup(RELPATH, ino); + expect_access(ino, access_mode, 0); + + ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno); +} + +/* + * Unlink a file + * + * There should never be a FUSE_ACCESS sent for this operation, except for + * search permissions on the parent directory. + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689 + */ +TEST_F(Access, unlink) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + + expect_access(FUSE_ROOT_ID, X_OK, 0); + expect_noaccess(FUSE_ROOT_ID, W_OK | R_OK); + expect_noaccess(ino, 0); + expect_lookup(RELPATH, ino); + expect_unlink(1, RELPATH, EPERM); + + ASSERT_NE(0, unlink(FULLPATH)); + ASSERT_EQ(EPERM, errno); +} + +/* + * Unlink a file whose parent diretory's sticky bit is set + * + * There should never be a FUSE_ACCESS sent for this operation, except for + * search permissions on the parent directory. + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689 + */ +TEST_F(Access, unlink_sticky_directory) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + + expect_access(FUSE_ROOT_ID, X_OK, 0); + expect_noaccess(FUSE_ROOT_ID, W_OK | R_OK); + expect_noaccess(ino, 0); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == FUSE_ROOT_ID); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) + { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = FUSE_ROOT_ID; + out.body.attr.attr.mode = S_IFDIR | 01777; + out.body.attr.attr.uid = 0; + out.body.attr.attr_valid = UINT64_MAX; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_ACCESS && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(0); + expect_lookup(RELPATH, ino); + expect_unlink(FUSE_ROOT_ID, RELPATH, EPERM); + + ASSERT_EQ(-1, unlink(FULLPATH)); + ASSERT_EQ(EPERM, errno); +} diff --git a/tests/sys/fs/fusefs/allow_other.cc b/tests/sys/fs/fusefs/allow_other.cc new file mode 100644 index 000000000000..24a161166a90 --- /dev/null +++ b/tests/sys/fs/fusefs/allow_other.cc @@ -0,0 +1,306 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * Tests for the "allow_other" mount option. They must be in their own + * file so they can be run as root + */ + +extern "C" { +#include <sys/types.h> +#include <sys/extattr.h> +#include <sys/wait.h> +#include <fcntl.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +const static char FULLPATH[] = "mountpoint/some_file.txt"; +const static char RELPATH[] = "some_file.txt"; + +class NoAllowOther: public FuseTest { + +public: +virtual void SetUp() { + if (geteuid() != 0) { + GTEST_SKIP() << "This test must be run as root"; + } + + FuseTest::SetUp(); +} +}; + +class AllowOther: public NoAllowOther { + +public: +virtual void SetUp() { + m_allow_other = true; + NoAllowOther::SetUp(); +} +}; + +TEST_F(AllowOther, allowed) +{ + int status; + + fork(true, &status, [&] { + uint64_t ino = 42; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_open(ino, 0, 1); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, FH); + }, []() { + int fd; + + fd = open(FULLPATH, O_RDONLY); + if (fd < 0) { + perror("open"); + return(1); + } + + leak(fd); + return 0; + } + ); + ASSERT_EQ(0, WEXITSTATUS(status)); +} + +/* Check that fusefs uses the correct credentials for FUSE operations */ +TEST_F(AllowOther, creds) +{ + int status; + uid_t uid; + gid_t gid; + + get_unprivileged_id(&uid, &gid); + fork(true, &status, [=] { + EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LOOKUP && + in.header.uid == uid && + in.header.gid == gid); + }, Eq(true)), + _) + ).Times(1) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + }, []() { + eaccess(FULLPATH, F_OK); + return 0; + } + ); + ASSERT_EQ(0, WEXITSTATUS(status)); +} + +/* + * A variation of the Open.multiple_creds test showing how the bug can lead to a + * privilege elevation. The first process is privileged and opens a file only + * visible to root. The second process is unprivileged and shouldn't be able + * to open the file, but does thanks to the bug + */ +TEST_F(AllowOther, privilege_escalation) +{ + int fd1, status; + const static uint64_t ino = 42; + const static uint64_t fh = 100; + + /* Fork a child to open the file with different credentials */ + fork(true, &status, [&] { + + expect_lookup(RELPATH, ino, S_IFREG | 0600, 0, 2); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.header.pid == (uint32_t)getpid() && + in.header.uid == (uint32_t)geteuid() && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke( + ReturnImmediate([](auto in __unused, auto& out) { + out.body.open.fh = fh; + out.header.len = sizeof(out.header); + SET_OUT_HEADER_LEN(out, open); + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.header.pid != (uint32_t)getpid() && + in.header.uid != (uint32_t)geteuid() && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(AnyNumber()) + .WillRepeatedly(Invoke(ReturnErrno(EPERM))); + + fd1 = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd1) << strerror(errno); + }, [] { + int fd0; + + fd0 = open(FULLPATH, O_RDONLY); + if (fd0 >= 0) { + fprintf(stderr, "Privilege escalation!\n"); + return 1; + } + if (errno != EPERM) { + fprintf(stderr, "Unexpected error %s\n", + strerror(errno)); + return 1; + } + leak(fd0); + return 0; + } + ); + ASSERT_EQ(0, WEXITSTATUS(status)); + leak(fd1); +} + +TEST_F(NoAllowOther, disallowed) +{ + int status; + + fork(true, &status, [] { + }, []() { + int fd; + + fd = open(FULLPATH, O_RDONLY); + if (fd >= 0) { + fprintf(stderr, "open should've failed\n"); + leak(fd); + return(1); + } else if (errno != EPERM) { + fprintf(stderr, "Unexpected error: %s\n", + strerror(errno)); + return(1); + } + return 0; + } + ); + ASSERT_EQ(0, WEXITSTATUS(status)); +} + +/* + * When -o allow_other is not used, users other than the owner aren't allowed + * to open anything inside of the mount point, not just the mountpoint itself + * This is a regression test for bug 237052 + */ +TEST_F(NoAllowOther, disallowed_beneath_root) +{ + const static char RELPATH2[] = "other_dir"; + const static uint64_t ino = 42; + const static uint64_t ino2 = 43; + int dfd, status; + + expect_lookup(RELPATH, ino, S_IFDIR | 0755, 0, 1); + EXPECT_LOOKUP(ino, RELPATH2) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino2; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = UINT64_MAX; + }))); + expect_opendir(ino); + dfd = open(FULLPATH, O_DIRECTORY); + ASSERT_LE(0, dfd) << strerror(errno); + + fork(true, &status, [] { + }, [&]() { + int fd; + + fd = openat(dfd, RELPATH2, O_RDONLY); + if (fd >= 0) { + fprintf(stderr, "openat should've failed\n"); + leak(fd); + return(1); + } else if (errno != EPERM) { + fprintf(stderr, "Unexpected error: %s\n", + strerror(errno)); + return(1); + } + return 0; + } + ); + ASSERT_EQ(0, WEXITSTATUS(status)); + + leak(dfd); +} + +/* + * Provide coverage for the extattr methods, which have a slightly different + * code path + */ +TEST_F(NoAllowOther, setextattr) +{ + int ino = 42, status; + + fork(true, &status, [&] { + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + }))); + + /* + * lookup the file to get it into the cache. + * Otherwise, the unprivileged lookup will fail with + * EACCES + */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + }, [&]() { + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + int ns = EXTATTR_NAMESPACE_USER; + ssize_t r; + + r = extattr_set_file(FULLPATH, ns, "foo", + (const void*)value, value_len); + if (r >= 0) { + fprintf(stderr, "should've failed\n"); + return(1); + } else if (errno != EPERM) { + fprintf(stderr, "Unexpected error: %s\n", + strerror(errno)); + return(1); + } + return 0; + } + ); + ASSERT_EQ(0, WEXITSTATUS(status)); +} diff --git a/tests/sys/fs/fusefs/bad_server.cc b/tests/sys/fs/fusefs/bad_server.cc new file mode 100644 index 000000000000..825523cac2bb --- /dev/null +++ b/tests/sys/fs/fusefs/bad_server.cc @@ -0,0 +1,108 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2023 Axcient + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <fcntl.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class BadServer: public FuseTest {}; + +/* + * If the server sends a response for an unknown request, the kernel should + * gracefully return EINVAL. + */ +TEST_F(BadServer, UnknownUnique) +{ + mockfs_buf_out out; + + out.header.len = sizeof(out.header); + out.header.error = 0; + out.header.unique = 99999; // Invalid! + out.expected_errno = EINVAL; + m_mock->write_response(out); +} + +/* + * If the server sends less than a header's worth of data, the kernel should + * gracefully return EINVAL. + */ +TEST_F(BadServer, ShortWrite) +{ + mockfs_buf_out out; + + out.header.len = sizeof(out.header) - 1; + out.header.error = 0; + out.header.unique = 0; // Asynchronous notification + out.expected_errno = EINVAL; + /* + * Tell the event loop to quit. The kernel will disconnect us + * because of the short write. + */ + m_mock->m_expect_unmount = true; + m_mock->write_response(out); +} + +/* + * It is illegal to report an error, and also send back a payload. + */ +TEST_F(BadServer, ErrorWithPayload) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke([&](auto in, auto &out) { + // First send an invalid response + std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out); + out0->header.unique = in.header.unique; + out0->header.error = -ENOENT; + SET_OUT_HEADER_LEN(*out0, entry); // Invalid! + out0->expected_errno = EINVAL; + out.push_back(std::move(out0)); + + // Then, respond to the lookup so we can complete the test + std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out); + out1->header.unique = in.header.unique; + out1->header.error = -ENOENT; + out1->header.len = sizeof(out1->header); + out.push_back(std::move(out1)); + + // The kernel may disconnect us for bad behavior, so don't try + // to read or write any more. + m_mock->m_quit = true; + })); + + EXPECT_NE(0, access(FULLPATH, F_OK)); + + EXPECT_EQ(ENOENT, errno); +} diff --git a/tests/sys/fs/fusefs/bmap.cc b/tests/sys/fs/fusefs/bmap.cc new file mode 100644 index 000000000000..e61dadb6d79e --- /dev/null +++ b/tests/sys/fs/fusefs/bmap.cc @@ -0,0 +1,353 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> +#include <sys/ioctl.h> +#include <sys/filio.h> + +#include <fcntl.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +const static char FULLPATH[] = "mountpoint/foo"; +const static char RELPATH[] = "foo"; + +class Bmap: public FuseTest { +public: +virtual void SetUp() { + m_maxreadahead = UINT32_MAX; + FuseTest::SetUp(); +} +void expect_bmap(uint64_t ino, uint64_t lbn, uint32_t blocksize, uint64_t pbn) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_BMAP && + in.header.nodeid == ino && + in.body.bmap.block == lbn && + in.body.bmap.blocksize == blocksize); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, bmap); + out.body.bmap.block = pbn; + }))); +} + +void expect_lookup(const char *relpath, uint64_t ino, off_t size) +{ + FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, size, 1, + UINT64_MAX); +} +}; + +class BmapEof: public Bmap, public WithParamInterface<int> {}; + +/* + * Test FUSE_BMAP + */ +TEST_F(Bmap, bmap) +{ + struct fiobmap2_arg arg; + /* + * Pick fsize and lbn large enough that max length runs won't reach + * either beginning or end of file + */ + const off_t filesize = 1 << 30; + int64_t lbn = 100; + int64_t pbn = 12345; + const ino_t ino = 42; + int fd; + + expect_lookup(RELPATH, 42, filesize); + expect_open(ino, 0, 1); + expect_bmap(ino, lbn, m_maxbcachebuf, pbn); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + arg.bn = lbn; + arg.runp = -1; + arg.runb = -1; + ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); + EXPECT_EQ(arg.bn, pbn); + /* + * XXX The FUSE protocol does not include the runp and runb variables, + * so those must be guessed in-kernel. There's no "right" answer, so + * just check that they're within reasonable limits. + */ + EXPECT_LE(arg.runb, lbn); + EXPECT_LE((unsigned long)arg.runb, m_maxreadahead / m_maxbcachebuf); + EXPECT_LE((unsigned long)arg.runb, m_maxphys / m_maxbcachebuf); + EXPECT_GT(arg.runb, 0); + EXPECT_LE(arg.runp, filesize / m_maxbcachebuf - lbn); + EXPECT_LE((unsigned long)arg.runp, m_maxreadahead / m_maxbcachebuf); + EXPECT_LE((unsigned long)arg.runp, m_maxphys / m_maxbcachebuf); + EXPECT_GT(arg.runp, 0); + + leak(fd); +} + +/* + * If the daemon does not implement VOP_BMAP, fusefs should return sensible + * defaults. + */ +TEST_F(Bmap, default_) +{ + struct fiobmap2_arg arg; + const off_t filesize = 1 << 30; + const ino_t ino = 42; + int64_t lbn; + int fd; + + expect_lookup(RELPATH, 42, filesize); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_BMAP); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(ENOSYS))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + /* First block */ + lbn = 0; + arg.bn = lbn; + arg.runp = -1; + arg.runb = -1; + ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); + EXPECT_EQ(arg.bn, 0); + EXPECT_EQ((unsigned long )arg.runp, m_maxphys / m_maxbcachebuf - 1); + EXPECT_EQ(arg.runb, 0); + + /* In the middle */ + lbn = filesize / m_maxbcachebuf / 2; + arg.bn = lbn; + arg.runp = -1; + arg.runb = -1; + ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); + EXPECT_EQ(arg.bn, lbn * m_maxbcachebuf / DEV_BSIZE); + EXPECT_EQ((unsigned long )arg.runp, m_maxphys / m_maxbcachebuf - 1); + EXPECT_EQ((unsigned long )arg.runb, m_maxphys / m_maxbcachebuf - 1); + + /* Last block */ + lbn = filesize / m_maxbcachebuf - 1; + arg.bn = lbn; + arg.runp = -1; + arg.runb = -1; + ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); + EXPECT_EQ(arg.bn, lbn * m_maxbcachebuf / DEV_BSIZE); + EXPECT_EQ(arg.runp, 0); + EXPECT_EQ((unsigned long )arg.runb, m_maxphys / m_maxbcachebuf - 1); + + leak(fd); +} + +/* + * The server returns an error for some reason for FUSE_BMAP. fusefs should + * faithfully report that error up to the caller. + */ +TEST_F(Bmap, einval) +{ + struct fiobmap2_arg arg; + const off_t filesize = 1 << 30; + int64_t lbn = 100; + const ino_t ino = 42; + int fd; + + expect_lookup(RELPATH, 42, filesize); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_BMAP && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(EINVAL))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + arg.bn = lbn; + arg.runp = -1; + arg.runb = -1; + ASSERT_EQ(-1, ioctl(fd, FIOBMAP2, &arg)); + EXPECT_EQ(EINVAL, errno); + + leak(fd); +} + +/* + * Even if the server returns EINVAL during VOP_BMAP, we should still be able + * to successfully read a block. This is a regression test for + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=264196 . The bug did not + * lie in fusefs, but this is a convenient place for a regression test. + */ +TEST_F(Bmap, spurious_einval) +{ + const off_t filesize = 4ull << 30; + const ino_t ino = 42; + int fd, r; + char buf[1]; + + expect_lookup(RELPATH, 42, filesize); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_BMAP && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnErrno(EINVAL))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.offset == 0 && + in.body.read.size == (uint64_t)m_maxbcachebuf); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { + size_t osize = in.body.read.size; + + assert(osize < sizeof(out.body.bytes)); + out.header.len = sizeof(struct fuse_out_header) + osize; + bzero(out.body.bytes, osize); + }))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + /* + * Read the same block multiple times. On a system affected by PR + * 264196 , the second read will fail. + */ + r = read(fd, buf, sizeof(buf)); + EXPECT_EQ(r, 1) << strerror(errno); + r = read(fd, buf, sizeof(buf)); + EXPECT_EQ(r, 1) << strerror(errno); + r = read(fd, buf, sizeof(buf)); + EXPECT_EQ(r, 1) << strerror(errno); +} + +/* + * VOP_BMAP should not query the server for the file's size, even if its cached + * attributes have expired. + * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=256937 + */ +TEST_P(BmapEof, eof) +{ + /* + * Outline: + * 1) lookup the file, setting attr_valid=0 + * 2) Read more than one block, causing the kernel to issue VOP_BMAP to + * plan readahead. + * 3) Nothing should panic + * 4) Repeat the tests, truncating the file after different numbers of + * GETATTR operations. + */ + Sequence seq; + const off_t filesize = 2 * m_maxbcachebuf; + const ino_t ino = 42; + mode_t mode = S_IFREG | 0644; + char *buf; + int fd; + int ngetattrs; + + ngetattrs = GetParam(); + FuseTest::expect_lookup(RELPATH, ino, mode, filesize, 1, 0); + expect_open(ino, 0, 1); + // Depending on ngetattrs, FUSE_READ could be called with either + // filesize or filesize / 2 . + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.offset == 0 && + ( in.body.read.size == filesize || + in.body.read.size == filesize / 2)); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { + size_t osize = in.body.read.size; + + assert(osize < sizeof(out.body.bytes)); + out.header.len = sizeof(struct fuse_out_header) + osize; + bzero(out.body.bytes, osize); + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(Between(ngetattrs - 1, ngetattrs)) + .InSequence(seq) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr_valid = 0; + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr.size = filesize; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).InSequence(seq) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr_valid = 0; + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr.size = filesize / 2; + }))); + + buf = new char[filesize](); + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + read(fd, buf, filesize); + + delete[] buf; + leak(fd); +} + +INSTANTIATE_TEST_SUITE_P(BE, BmapEof, + Values(1, 2, 3) +); diff --git a/tests/sys/fs/fusefs/cache.cc b/tests/sys/fs/fusefs/cache.cc new file mode 100644 index 000000000000..ea6d87674da2 --- /dev/null +++ b/tests/sys/fs/fusefs/cache.cc @@ -0,0 +1,218 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2020 Alan Somers + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> +#include <fcntl.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +/* + * Tests for thorny cache problems not specific to any one opcode + */ + +using namespace testing; + +/* + * Parameters + * - reopen file - If true, close and reopen the file between reads + * - cache lookups - If true, allow lookups to be cached + * - cache attrs - If true, allow file attributes to be cached + * - cache_mode - uncached, writeback, or writethrough + * - initial size - File size before truncation + * - truncated size - File size after truncation + */ +typedef tuple<tuple<bool, bool, bool>, cache_mode, ssize_t, ssize_t> CacheParam; + +class Cache: public FuseTest, public WithParamInterface<CacheParam> { +public: +bool m_direct_io; + +Cache(): m_direct_io(false) {}; + +virtual void SetUp() { + int cache_mode = get<1>(GetParam()); + switch (cache_mode) { + case Uncached: + m_direct_io = true; + break; + case WritebackAsync: + m_async = true; + /* FALLTHROUGH */ + case Writeback: + m_init_flags |= FUSE_WRITEBACK_CACHE; + /* FALLTHROUGH */ + case Writethrough: + break; + default: + FAIL() << "Unknown cache mode"; + } + m_noatime = true; // To prevent SETATTR for atime on close + + FuseTest::SetUp(); + if (IsSkipped()) + return; +} + +void expect_getattr(uint64_t ino, int times, uint64_t size, uint64_t attr_valid) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(times) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr_valid = attr_valid; + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr.size = size; + }))); +} + +void expect_lookup(const char *relpath, uint64_t ino, + uint64_t size, uint64_t entry_valid, uint64_t attr_valid) +{ + EXPECT_LOOKUP(FUSE_ROOT_ID, relpath) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = attr_valid; + out.body.entry.attr.size = size; + out.body.entry.entry_valid = entry_valid; + }))); +} + +void expect_open(uint64_t ino, int times) +{ + FuseTest::expect_open(ino, m_direct_io ? FOPEN_DIRECT_IO: 0, times); +} + +void expect_release(uint64_t ino, ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_RELEASE && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(r)); +} + +}; + +// If the server truncates the file behind the kernel's back, the kernel should +// invalidate cached pages beyond the new EOF +TEST_P(Cache, truncate_by_surprise_invalidates_cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefghijklmnopqrstuvwxyz"; + uint64_t ino = 42; + uint64_t attr_valid, entry_valid; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + bool reopen = get<0>(get<0>(GetParam())); + bool cache_lookups = get<1>(get<0>(GetParam())); + bool cache_attrs = get<2>(get<0>(GetParam())); + ssize_t osize = get<2>(GetParam()); + ssize_t nsize = get<3>(GetParam()); + + ASSERT_LE(osize, bufsize); + ASSERT_LE(nsize, bufsize); + if (cache_attrs) + attr_valid = UINT64_MAX; + else + attr_valid = 0; + if (cache_lookups) + entry_valid = UINT64_MAX; + else + entry_valid = 0; + + expect_lookup(RELPATH, ino, osize, entry_valid, attr_valid); + expect_open(ino, 1); + if (!cache_attrs) + expect_getattr(ino, 2, osize, attr_valid); + expect_read(ino, 0, osize, osize, CONTENTS); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(osize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, osize)); + + // Now truncate the file behind the kernel's back. The next read + // should discard cache and fetch from disk again. + if (reopen) { + // Close and reopen the file + expect_flush(ino, 1, ReturnErrno(ENOSYS)); + expect_release(ino, ReturnErrno(0)); + ASSERT_EQ(0, close(fd)); + expect_lookup(RELPATH, ino, nsize, entry_valid, attr_valid); + expect_open(ino, 1); + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + } + + if (!cache_attrs) + expect_getattr(ino, 1, nsize, attr_valid); + expect_read(ino, 0, nsize, nsize, CONTENTS); + ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)); + ASSERT_EQ(nsize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, nsize)); + + leak(fd); +} + +INSTANTIATE_TEST_SUITE_P(Cache, Cache, + Combine( + /* Test every combination that: + * - does not cache at least one of entries and attrs + * - either doesn't cache attrs, or reopens the file + * In the other combinations, the kernel will never learn that + * the file's size has changed. + */ + Values( + std::make_tuple(false, false, false), + std::make_tuple(false, true, false), + std::make_tuple(true, false, false), + std::make_tuple(true, false, true), + std::make_tuple(true, true, false) + ), + Values(Writethrough, Writeback), + /* Test both reductions and extensions to file size */ + Values(20), + Values(10, 25) + ) +); diff --git a/tests/sys/fs/fusefs/copy_file_range.cc b/tests/sys/fs/fusefs/copy_file_range.cc new file mode 100644 index 000000000000..806ecf3c3653 --- /dev/null +++ b/tests/sys/fs/fusefs/copy_file_range.cc @@ -0,0 +1,802 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2020 Alan Somers + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> +#include <sys/mman.h> +#include <sys/time.h> +#include <sys/resource.h> + +#include <fcntl.h> +#include <signal.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class CopyFileRange: public FuseTest { +public: + +void expect_maybe_lseek(uint64_t ino) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(AtMost(1)) + .WillRepeatedly(Invoke(ReturnErrno(ENOSYS))); +} + +void expect_open(uint64_t ino, uint32_t flags, int times, uint64_t fh) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(times) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + out.header.len = sizeof(out.header); + SET_OUT_HEADER_LEN(out, open); + out.body.open.fh = fh; + out.body.open.open_flags = flags; + }))); +} + +void expect_write(uint64_t ino, uint64_t offset, uint64_t isize, + uint64_t osize, const void *contents) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *buf = (const char*)in.body.bytes + + sizeof(struct fuse_write_in); + + return (in.header.opcode == FUSE_WRITE && + in.header.nodeid == ino && + in.body.write.offset == offset && + in.body.write.size == isize && + 0 == bcmp(buf, contents, isize)); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = osize; + }))); +} + +}; + + +class CopyFileRange_7_27: public CopyFileRange { +public: +virtual void SetUp() { + m_kernel_minor_version = 27; + CopyFileRange::SetUp(); +} +}; + +class CopyFileRangeNoAtime: public CopyFileRange { +public: +virtual void SetUp() { + m_noatime = true; + CopyFileRange::SetUp(); +} +}; + +class CopyFileRangeRlimitFsize: public CopyFileRange { +public: +static sig_atomic_t s_sigxfsz; +struct rlimit m_initial_limit; + +virtual void SetUp() { + s_sigxfsz = 0; + getrlimit(RLIMIT_FSIZE, &m_initial_limit); + CopyFileRange::SetUp(); +} + +void TearDown() { + struct sigaction sa; + + setrlimit(RLIMIT_FSIZE, &m_initial_limit); + + bzero(&sa, sizeof(sa)); + sa.sa_handler = SIG_DFL; + sigaction(SIGXFSZ, &sa, NULL); + + FuseTest::TearDown(); +} + +}; + +sig_atomic_t CopyFileRangeRlimitFsize::s_sigxfsz = 0; + +void sigxfsz_handler(int __unused sig) { + CopyFileRangeRlimitFsize::s_sigxfsz = 1; +} + +TEST_F(CopyFileRange, eio) +{ + const char FULLPATH1[] = "mountpoint/src.txt"; + const char RELPATH1[] = "src.txt"; + const char FULLPATH2[] = "mountpoint/dst.txt"; + const char RELPATH2[] = "dst.txt"; + const uint64_t ino1 = 42; + const uint64_t ino2 = 43; + const uint64_t fh1 = 0xdeadbeef1a7ebabe; + const uint64_t fh2 = 0xdeadc0de88c0ffee; + off_t fsize1 = 1 << 20; /* 1 MiB */ + off_t fsize2 = 1 << 19; /* 512 KiB */ + off_t start1 = 1 << 18; + off_t start2 = 3 << 17; + ssize_t len = 65536; + int fd1, fd2; + + expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); + expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); + expect_open(ino1, 0, 1, fh1); + expect_open(ino2, 0, 1, fh2); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_COPY_FILE_RANGE && + in.header.nodeid == ino1 && + in.body.copy_file_range.fh_in == fh1 && + (off_t)in.body.copy_file_range.off_in == start1 && + in.body.copy_file_range.nodeid_out == ino2 && + in.body.copy_file_range.fh_out == fh2 && + (off_t)in.body.copy_file_range.off_out == start2 && + in.body.copy_file_range.len == (size_t)len && + in.body.copy_file_range.flags == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(EIO))); + + fd1 = open(FULLPATH1, O_RDONLY); + fd2 = open(FULLPATH2, O_WRONLY); + ASSERT_EQ(-1, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); + EXPECT_EQ(EIO, errno); +} + +/* + * copy_file_range should evict cached data for the modified region of the + * destination file. + */ +TEST_F(CopyFileRange, evicts_cache) +{ + const char FULLPATH1[] = "mountpoint/src.txt"; + const char RELPATH1[] = "src.txt"; + const char FULLPATH2[] = "mountpoint/dst.txt"; + const char RELPATH2[] = "dst.txt"; + char *buf0, *buf1, *buf; + const uint64_t ino1 = 42; + const uint64_t ino2 = 43; + const uint64_t fh1 = 0xdeadbeef1a7ebabe; + const uint64_t fh2 = 0xdeadc0de88c0ffee; + off_t fsize1 = 1 << 20; /* 1 MiB */ + off_t fsize2 = 1 << 19; /* 512 KiB */ + off_t start1 = 1 << 18; + off_t start2 = 3 << 17; + ssize_t len = m_maxbcachebuf; + int fd1, fd2; + + buf0 = new char[m_maxbcachebuf]; + memset(buf0, 42, m_maxbcachebuf); + + expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); + expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); + expect_open(ino1, 0, 1, fh1); + expect_open(ino2, 0, 1, fh2); + expect_read(ino2, start2, m_maxbcachebuf, m_maxbcachebuf, buf0, -1, + fh2); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_COPY_FILE_RANGE && + in.header.nodeid == ino1 && + in.body.copy_file_range.fh_in == fh1 && + (off_t)in.body.copy_file_range.off_in == start1 && + in.body.copy_file_range.nodeid_out == ino2 && + in.body.copy_file_range.fh_out == fh2 && + (off_t)in.body.copy_file_range.off_out == start2 && + in.body.copy_file_range.len == (size_t)len && + in.body.copy_file_range.flags == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = len; + }))); + + fd1 = open(FULLPATH1, O_RDONLY); + fd2 = open(FULLPATH2, O_RDWR); + + // Prime cache + buf = new char[m_maxbcachebuf]; + ASSERT_EQ(m_maxbcachebuf, pread(fd2, buf, m_maxbcachebuf, start2)) + << strerror(errno); + EXPECT_EQ(0, memcmp(buf0, buf, m_maxbcachebuf)); + + // Tell the FUSE server overwrite the region we just read + ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); + + // Read again. This should bypass the cache and read direct from server + buf1 = new char[m_maxbcachebuf]; + memset(buf1, 69, m_maxbcachebuf); + start2 -= len; + expect_read(ino2, start2, m_maxbcachebuf, m_maxbcachebuf, buf1, -1, + fh2); + ASSERT_EQ(m_maxbcachebuf, pread(fd2, buf, m_maxbcachebuf, start2)) + << strerror(errno); + EXPECT_EQ(0, memcmp(buf1, buf, m_maxbcachebuf)); + + delete[] buf1; + delete[] buf0; + delete[] buf; + leak(fd1); + leak(fd2); +} + +/* + * If the server doesn't support FUSE_COPY_FILE_RANGE, the kernel should + * fallback to a read/write based implementation. + */ +TEST_F(CopyFileRange, fallback) +{ + const char FULLPATH1[] = "mountpoint/src.txt"; + const char RELPATH1[] = "src.txt"; + const char FULLPATH2[] = "mountpoint/dst.txt"; + const char RELPATH2[] = "dst.txt"; + const uint64_t ino1 = 42; + const uint64_t ino2 = 43; + const uint64_t fh1 = 0xdeadbeef1a7ebabe; + const uint64_t fh2 = 0xdeadc0de88c0ffee; + off_t fsize2 = 0; + off_t start1 = 0; + off_t start2 = 0; + const char *contents = "Hello, world!"; + ssize_t len; + int fd1, fd2; + + len = strlen(contents); + + /* + * Ensure that we read to EOF, just so the buffer cache's read size is + * predictable. + */ + expect_lookup(RELPATH1, ino1, S_IFREG | 0644, start1 + len, 1); + expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); + expect_open(ino1, 0, 1, fh1); + expect_open(ino2, 0, 1, fh2); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_COPY_FILE_RANGE && + in.header.nodeid == ino1 && + in.body.copy_file_range.fh_in == fh1 && + (off_t)in.body.copy_file_range.off_in == start1 && + in.body.copy_file_range.nodeid_out == ino2 && + in.body.copy_file_range.fh_out == fh2 && + (off_t)in.body.copy_file_range.off_out == start2 && + in.body.copy_file_range.len == (size_t)len && + in.body.copy_file_range.flags == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(ENOSYS))); + expect_maybe_lseek(ino1); + expect_read(ino1, start1, len, len, contents, 0); + expect_write(ino2, start2, len, len, contents); + + fd1 = open(FULLPATH1, O_RDONLY); + ASSERT_GE(fd1, 0); + fd2 = open(FULLPATH2, O_WRONLY); + ASSERT_GE(fd2, 0); + ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); +} + +/* + * Writes via mmap should not conflict with using copy_file_range. Any dirty + * pages that overlap with copy_file_range's input should be flushed before + * FUSE_COPY_FILE_RANGE is sent. + */ +TEST_F(CopyFileRange, mmap_write) +{ + const char FULLPATH[] = "mountpoint/src.txt"; + const char RELPATH[] = "src.txt"; + uint8_t *wbuf, *fbuf; + void *p; + size_t fsize = 0x6000; + size_t wsize = 0x3000; + ssize_t r; + off_t offset2_in = 0; + off_t offset2_out = wsize; + size_t copysize = wsize; + const uint64_t ino = 42; + const uint64_t fh = 0xdeadbeef1a7ebabe; + int fd; + const mode_t mode = 0644; + + fbuf = new uint8_t[fsize](); + wbuf = new uint8_t[wsize]; + memset(wbuf, 1, wsize); + + expect_lookup(RELPATH, ino, S_IFREG | mode, fsize, 1); + expect_open(ino, 0, 1, fh); + /* This read is initiated by the mmap write */ + expect_read(ino, 0, fsize, fsize, fbuf, -1, fh); + /* This write flushes the buffer filled by the mmap write */ + expect_write(ino, 0, wsize, wsize, wbuf); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_COPY_FILE_RANGE && + (off_t)in.body.copy_file_range.off_in == offset2_in && + (off_t)in.body.copy_file_range.off_out == offset2_out && + in.body.copy_file_range.len == copysize + ); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([&](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = copysize; + }))); + + fd = open(FULLPATH, O_RDWR); + + /* First, write some data via mmap */ + p = mmap(NULL, wsize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + ASSERT_NE(MAP_FAILED, p) << strerror(errno); + memmove((uint8_t*)p, wbuf, wsize); + ASSERT_EQ(0, munmap(p, wsize)) << strerror(errno); + + /* + * Then copy it around the file via copy_file_range. This should + * trigger a FUSE_WRITE to flush the pages written by mmap. + */ + r = copy_file_range(fd, &offset2_in, fd, &offset2_out, copysize, 0); + ASSERT_EQ(copysize, (size_t)r) << strerror(errno); + + delete[] wbuf; + delete[] fbuf; +} + + +/* + * copy_file_range should send SIGXFSZ and return EFBIG when the operation + * would exceed the limit imposed by RLIMIT_FSIZE. + */ +TEST_F(CopyFileRangeRlimitFsize, signal) +{ + const char FULLPATH1[] = "mountpoint/src.txt"; + const char RELPATH1[] = "src.txt"; + const char FULLPATH2[] = "mountpoint/dst.txt"; + const char RELPATH2[] = "dst.txt"; + struct rlimit rl; + const uint64_t ino1 = 42; + const uint64_t ino2 = 43; + const uint64_t fh1 = 0xdeadbeef1a7ebabe; + const uint64_t fh2 = 0xdeadc0de88c0ffee; + off_t fsize1 = 1 << 20; /* 1 MiB */ + off_t fsize2 = 1 << 19; /* 512 KiB */ + off_t start1 = 1 << 18; + off_t start2 = fsize2; + ssize_t len = 65536; + int fd1, fd2; + + expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); + expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); + expect_open(ino1, 0, 1, fh1); + expect_open(ino2, 0, 1, fh2); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_COPY_FILE_RANGE); + }, Eq(true)), + _) + ).Times(0); + + rl.rlim_cur = fsize2; + rl.rlim_max = m_initial_limit.rlim_max; + ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); + ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); + + fd1 = open(FULLPATH1, O_RDONLY); + fd2 = open(FULLPATH2, O_WRONLY); + ASSERT_EQ(-1, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); + EXPECT_EQ(EFBIG, errno); + EXPECT_EQ(1, s_sigxfsz); +} + +/* + * When crossing the RLIMIT_FSIZE boundary, writes should be truncated, not + * aborted. + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=266611 + */ +TEST_F(CopyFileRangeRlimitFsize, truncate) +{ + const char FULLPATH1[] = "mountpoint/src.txt"; + const char RELPATH1[] = "src.txt"; + const char FULLPATH2[] = "mountpoint/dst.txt"; + const char RELPATH2[] = "dst.txt"; + struct rlimit rl; + const uint64_t ino1 = 42; + const uint64_t ino2 = 43; + const uint64_t fh1 = 0xdeadbeef1a7ebabe; + const uint64_t fh2 = 0xdeadc0de88c0ffee; + off_t fsize1 = 1 << 20; /* 1 MiB */ + off_t fsize2 = 1 << 19; /* 512 KiB */ + off_t start1 = 1 << 18; + off_t start2 = fsize2; + ssize_t len = 65536; + off_t limit = start2 + len / 2; + int fd1, fd2; + + expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); + expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); + expect_open(ino1, 0, 1, fh1); + expect_open(ino2, 0, 1, fh2); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_COPY_FILE_RANGE && + (off_t)in.body.copy_file_range.off_out == start2 && + in.body.copy_file_range.len == (size_t)len / 2 + ); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = len / 2; + }))); + + rl.rlim_cur = limit; + rl.rlim_max = m_initial_limit.rlim_max; + ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); + ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); + + fd1 = open(FULLPATH1, O_RDONLY); + fd2 = open(FULLPATH2, O_WRONLY); + ASSERT_EQ(len / 2, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); +} + +TEST_F(CopyFileRange, ok) +{ + const char FULLPATH1[] = "mountpoint/src.txt"; + const char RELPATH1[] = "src.txt"; + const char FULLPATH2[] = "mountpoint/dst.txt"; + const char RELPATH2[] = "dst.txt"; + const uint64_t ino1 = 42; + const uint64_t ino2 = 43; + const uint64_t fh1 = 0xdeadbeef1a7ebabe; + const uint64_t fh2 = 0xdeadc0de88c0ffee; + off_t fsize1 = 1 << 20; /* 1 MiB */ + off_t fsize2 = 1 << 19; /* 512 KiB */ + off_t start1 = 1 << 18; + off_t start2 = 3 << 17; + ssize_t len = 65536; + int fd1, fd2; + + expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); + expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); + expect_open(ino1, 0, 1, fh1); + expect_open(ino2, 0, 1, fh2); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_COPY_FILE_RANGE && + in.header.nodeid == ino1 && + in.body.copy_file_range.fh_in == fh1 && + (off_t)in.body.copy_file_range.off_in == start1 && + in.body.copy_file_range.nodeid_out == ino2 && + in.body.copy_file_range.fh_out == fh2 && + (off_t)in.body.copy_file_range.off_out == start2 && + in.body.copy_file_range.len == (size_t)len && + in.body.copy_file_range.flags == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = len; + }))); + + fd1 = open(FULLPATH1, O_RDONLY); + fd2 = open(FULLPATH2, O_WRONLY); + ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); +} + +/* + * copy_file_range can make copies within a single file, as long as the ranges + * don't overlap. + * */ +TEST_F(CopyFileRange, same_file) +{ + const char FULLPATH[] = "mountpoint/src.txt"; + const char RELPATH[] = "src.txt"; + const uint64_t ino = 4; + const uint64_t fh = 0xdeadbeefa7ebabe; + off_t fsize = 1 << 20; /* 1 MiB */ + off_t off_in = 1 << 18; + off_t off_out = 3 << 17; + ssize_t len = 65536; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1, fh); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_COPY_FILE_RANGE && + in.header.nodeid == ino && + in.body.copy_file_range.fh_in == fh && + (off_t)in.body.copy_file_range.off_in == off_in && + in.body.copy_file_range.nodeid_out == ino && + in.body.copy_file_range.fh_out == fh && + (off_t)in.body.copy_file_range.off_out == off_out && + in.body.copy_file_range.len == (size_t)len && + in.body.copy_file_range.flags == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = len; + }))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_EQ(len, copy_file_range(fd, &off_in, fd, &off_out, len, 0)); + + leak(fd); +} + +/* + * copy_file_range should update the destination's mtime and ctime, and + * the source's atime. + */ +TEST_F(CopyFileRange, timestamps) +{ + const char FULLPATH1[] = "mountpoint/src.txt"; + const char RELPATH1[] = "src.txt"; + const char FULLPATH2[] = "mountpoint/dst.txt"; + const char RELPATH2[] = "dst.txt"; + struct stat sb1a, sb1b, sb2a, sb2b; + const uint64_t ino1 = 42; + const uint64_t ino2 = 43; + const uint64_t fh1 = 0xdeadbeef1a7ebabe; + const uint64_t fh2 = 0xdeadc0de88c0ffee; + off_t fsize1 = 1 << 20; /* 1 MiB */ + off_t fsize2 = 1 << 19; /* 512 KiB */ + off_t start1 = 1 << 18; + off_t start2 = 3 << 17; + ssize_t len = 65536; + int fd1, fd2; + + expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); + expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); + expect_open(ino1, 0, 1, fh1); + expect_open(ino2, 0, 1, fh2); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_COPY_FILE_RANGE && + in.header.nodeid == ino1 && + in.body.copy_file_range.fh_in == fh1 && + (off_t)in.body.copy_file_range.off_in == start1 && + in.body.copy_file_range.nodeid_out == ino2 && + in.body.copy_file_range.fh_out == fh2 && + (off_t)in.body.copy_file_range.off_out == start2 && + in.body.copy_file_range.len == (size_t)len && + in.body.copy_file_range.flags == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = len; + }))); + + fd1 = open(FULLPATH1, O_RDONLY); + ASSERT_GE(fd1, 0); + fd2 = open(FULLPATH2, O_WRONLY); + ASSERT_GE(fd2, 0); + ASSERT_EQ(0, fstat(fd1, &sb1a)) << strerror(errno); + ASSERT_EQ(0, fstat(fd2, &sb2a)) << strerror(errno); + + nap(); + + ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); + ASSERT_EQ(0, fstat(fd1, &sb1b)) << strerror(errno); + ASSERT_EQ(0, fstat(fd2, &sb2b)) << strerror(errno); + + EXPECT_NE(sb1a.st_atime, sb1b.st_atime); + EXPECT_EQ(sb1a.st_mtime, sb1b.st_mtime); + EXPECT_EQ(sb1a.st_ctime, sb1b.st_ctime); + EXPECT_EQ(sb2a.st_atime, sb2b.st_atime); + EXPECT_NE(sb2a.st_mtime, sb2b.st_mtime); + EXPECT_NE(sb2a.st_ctime, sb2b.st_ctime); + + leak(fd1); + leak(fd2); +} + +/* + * copy_file_range can extend the size of a file + * */ +TEST_F(CopyFileRange, extend) +{ + const char FULLPATH[] = "mountpoint/src.txt"; + const char RELPATH[] = "src.txt"; + struct stat sb; + const uint64_t ino = 4; + const uint64_t fh = 0xdeadbeefa7ebabe; + off_t fsize = 65536; + off_t off_in = 0; + off_t off_out = 65536; + ssize_t len = 65536; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1, fh); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_COPY_FILE_RANGE && + in.header.nodeid == ino && + in.body.copy_file_range.fh_in == fh && + (off_t)in.body.copy_file_range.off_in == off_in && + in.body.copy_file_range.nodeid_out == ino && + in.body.copy_file_range.fh_out == fh && + (off_t)in.body.copy_file_range.off_out == off_out && + in.body.copy_file_range.len == (size_t)len && + in.body.copy_file_range.flags == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = len; + }))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_GE(fd, 0); + ASSERT_EQ(len, copy_file_range(fd, &off_in, fd, &off_out, len, 0)); + + /* Check that cached attributes were updated appropriately */ + ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); + EXPECT_EQ(fsize + len, sb.st_size); + + leak(fd); +} + +/* With older protocol versions, no FUSE_COPY_FILE_RANGE should be attempted */ +TEST_F(CopyFileRange_7_27, fallback) +{ + const char FULLPATH1[] = "mountpoint/src.txt"; + const char RELPATH1[] = "src.txt"; + const char FULLPATH2[] = "mountpoint/dst.txt"; + const char RELPATH2[] = "dst.txt"; + const uint64_t ino1 = 42; + const uint64_t ino2 = 43; + const uint64_t fh1 = 0xdeadbeef1a7ebabe; + const uint64_t fh2 = 0xdeadc0de88c0ffee; + off_t fsize2 = 0; + off_t start1 = 0; + off_t start2 = 0; + const char *contents = "Hello, world!"; + ssize_t len; + int fd1, fd2; + + len = strlen(contents); + + /* + * Ensure that we read to EOF, just so the buffer cache's read size is + * predictable. + */ + expect_lookup(RELPATH1, ino1, S_IFREG | 0644, start1 + len, 1); + expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); + expect_open(ino1, 0, 1, fh1); + expect_open(ino2, 0, 1, fh2); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_COPY_FILE_RANGE); + }, Eq(true)), + _) + ).Times(0); + expect_maybe_lseek(ino1); + expect_read(ino1, start1, len, len, contents, 0); + expect_write(ino2, start2, len, len, contents); + + fd1 = open(FULLPATH1, O_RDONLY); + ASSERT_GE(fd1, 0); + fd2 = open(FULLPATH2, O_WRONLY); + ASSERT_GE(fd2, 0); + ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); + + leak(fd1); + leak(fd2); +} + +/* + * With -o noatime, copy_file_range should update the destination's mtime and + * ctime, but not the source's atime. + */ +TEST_F(CopyFileRangeNoAtime, timestamps) +{ + const char FULLPATH1[] = "mountpoint/src.txt"; + const char RELPATH1[] = "src.txt"; + const char FULLPATH2[] = "mountpoint/dst.txt"; + const char RELPATH2[] = "dst.txt"; + struct stat sb1a, sb1b, sb2a, sb2b; + const uint64_t ino1 = 42; + const uint64_t ino2 = 43; + const uint64_t fh1 = 0xdeadbeef1a7ebabe; + const uint64_t fh2 = 0xdeadc0de88c0ffee; + off_t fsize1 = 1 << 20; /* 1 MiB */ + off_t fsize2 = 1 << 19; /* 512 KiB */ + off_t start1 = 1 << 18; + off_t start2 = 3 << 17; + ssize_t len = 65536; + int fd1, fd2; + + expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); + expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); + expect_open(ino1, 0, 1, fh1); + expect_open(ino2, 0, 1, fh2); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_COPY_FILE_RANGE && + in.header.nodeid == ino1 && + in.body.copy_file_range.fh_in == fh1 && + (off_t)in.body.copy_file_range.off_in == start1 && + in.body.copy_file_range.nodeid_out == ino2 && + in.body.copy_file_range.fh_out == fh2 && + (off_t)in.body.copy_file_range.off_out == start2 && + in.body.copy_file_range.len == (size_t)len && + in.body.copy_file_range.flags == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = len; + }))); + + fd1 = open(FULLPATH1, O_RDONLY); + ASSERT_GE(fd1, 0); + fd2 = open(FULLPATH2, O_WRONLY); + ASSERT_GE(fd2, 0); + ASSERT_EQ(0, fstat(fd1, &sb1a)) << strerror(errno); + ASSERT_EQ(0, fstat(fd2, &sb2a)) << strerror(errno); + + nap(); + + ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); + ASSERT_EQ(0, fstat(fd1, &sb1b)) << strerror(errno); + ASSERT_EQ(0, fstat(fd2, &sb2b)) << strerror(errno); + + EXPECT_EQ(sb1a.st_atime, sb1b.st_atime); + EXPECT_EQ(sb1a.st_mtime, sb1b.st_mtime); + EXPECT_EQ(sb1a.st_ctime, sb1b.st_ctime); + EXPECT_EQ(sb2a.st_atime, sb2b.st_atime); + EXPECT_NE(sb2a.st_mtime, sb2b.st_mtime); + EXPECT_NE(sb2a.st_ctime, sb2b.st_ctime); + + leak(fd1); + leak(fd2); +} diff --git a/tests/sys/fs/fusefs/create.cc b/tests/sys/fs/fusefs/create.cc new file mode 100644 index 000000000000..08126ffc9056 --- /dev/null +++ b/tests/sys/fs/fusefs/create.cc @@ -0,0 +1,493 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <fcntl.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Create: public FuseTest { +public: + +void expect_create(const char *relpath, mode_t mode, ProcessMockerT r) +{ + mode_t mask = umask(0); + (void)umask(mask); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(fuse_create_in); + return (in.header.opcode == FUSE_CREATE && + in.body.create.mode == mode && + in.body.create.umask == mask && + (0 == strcmp(relpath, name))); + }, Eq(true)), + _) + ).WillOnce(Invoke(r)); +} + +}; + +/* FUSE_CREATE operations for a protocol 7.8 server */ +class Create_7_8: public Create { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + Create::SetUp(); +} + +void expect_create(const char *relpath, mode_t mode, ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(fuse_open_in); + return (in.header.opcode == FUSE_CREATE && + in.body.create.mode == mode && + (0 == strcmp(relpath, name))); + }, Eq(true)), + _) + ).WillOnce(Invoke(r)); +} + +}; + +/* FUSE_CREATE operations for a server built at protocol <= 7.11 */ +class Create_7_11: public FuseTest { +public: +virtual void SetUp() { + m_kernel_minor_version = 11; + FuseTest::SetUp(); +} + +void expect_create(const char *relpath, mode_t mode, ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(fuse_open_in); + return (in.header.opcode == FUSE_CREATE && + in.body.create.mode == mode && + (0 == strcmp(relpath, name))); + }, Eq(true)), + _) + ).WillOnce(Invoke(r)); +} + +}; + + +/* + * If FUSE_CREATE sets attr_valid, then subsequent GETATTRs should use the + * attribute cache + */ +TEST_F(Create, attr_cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + mode_t mode = S_IFREG | 0755; + uint64_t ino = 42; + int fd; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_create(RELPATH, mode, + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, create); + out.body.create.entry.attr.mode = mode; + out.body.create.entry.nodeid = ino; + out.body.create.entry.entry_valid = UINT64_MAX; + out.body.create.entry.attr_valid = UINT64_MAX; + })); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(0); + + fd = open(FULLPATH, O_CREAT | O_EXCL, mode); + ASSERT_LE(0, fd) << strerror(errno); + leak(fd); +} + +/* A successful CREATE operation should purge the parent dir's attr cache */ +TEST_F(Create, clear_attr_cache) +{ + const char FULLPATH[] = "mountpoint/src"; + const char RELPATH[] = "src"; + mode_t mode = S_IFREG | 0755; + uint64_t ino = 42; + int fd; + struct stat sb; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == FUSE_ROOT_ID); + }, Eq(true)), + _) + ).Times(2) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = FUSE_ROOT_ID; + out.body.attr.attr.mode = S_IFDIR | 0755; + out.body.attr.attr_valid = UINT64_MAX; + }))); + + expect_create(RELPATH, mode, + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, create); + out.body.create.entry.attr.mode = mode; + out.body.create.entry.nodeid = ino; + out.body.create.entry.entry_valid = UINT64_MAX; + out.body.create.entry.attr_valid = UINT64_MAX; + })); + + EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); + fd = open(FULLPATH, O_CREAT | O_EXCL, mode); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); + + leak(fd); +} + +/* + * The fuse daemon fails the request with EEXIST. This usually indicates a + * race condition: some other FUSE client created the file in between when the + * kernel checked for it with lookup and tried to create it with create + */ +TEST_F(Create, eexist) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + mode_t mode = S_IFREG | 0755; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_create(RELPATH, mode, ReturnErrno(EEXIST)); + EXPECT_EQ(-1, open(FULLPATH, O_CREAT | O_EXCL, mode)); + EXPECT_EQ(EEXIST, errno); +} + +/* + * If the daemon doesn't implement FUSE_CREATE, then the kernel should fallback + * to FUSE_MKNOD/FUSE_OPEN + */ +TEST_F(Create, Enosys) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + mode_t mode = S_IFREG | 0755; + uint64_t ino = 42; + int fd; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_create(RELPATH, mode, ReturnErrno(ENOSYS)); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(fuse_mknod_in); + return (in.header.opcode == FUSE_MKNOD && + in.body.mknod.mode == (S_IFREG | mode) && + in.body.mknod.rdev == 0 && + (0 == strcmp(RELPATH, name))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr_valid = UINT64_MAX; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { + out.header.len = sizeof(out.header); + SET_OUT_HEADER_LEN(out, open); + }))); + + fd = open(FULLPATH, O_CREAT | O_EXCL, mode); + ASSERT_LE(0, fd) << strerror(errno); + leak(fd); +} + +/* + * Creating a new file after FUSE_LOOKUP returned a negative cache entry + */ +TEST_F(Create, entry_cache_negative) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + mode_t mode = S_IFREG | 0755; + uint64_t ino = 42; + int fd; + /* + * Set entry_valid = 0 because this test isn't concerned with whether + * or not we actually cache negative entries, only with whether we + * interpret negative cache responses correctly. + */ + struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0}; + + /* create will first do a LOOKUP, adding a negative cache entry */ + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(ReturnNegativeCache(&entry_valid)); + expect_create(RELPATH, mode, + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, create); + out.body.create.entry.attr.mode = mode; + out.body.create.entry.nodeid = ino; + out.body.create.entry.entry_valid = UINT64_MAX; + out.body.create.entry.attr_valid = UINT64_MAX; + })); + + fd = open(FULLPATH, O_CREAT | O_EXCL, mode); + ASSERT_LE(0, fd) << strerror(errno); + leak(fd); +} + +/* + * Creating a new file should purge any negative namecache entries + */ +TEST_F(Create, entry_cache_negative_purge) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + mode_t mode = S_IFREG | 0755; + uint64_t ino = 42; + int fd; + struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0}; + + /* create will first do a LOOKUP, adding a negative cache entry */ + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH).Times(1) + .WillOnce(Invoke(ReturnNegativeCache(&entry_valid))) + .RetiresOnSaturation(); + + /* Then the CREATE should purge the negative cache entry */ + expect_create(RELPATH, mode, + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, create); + out.body.create.entry.attr.mode = mode; + out.body.create.entry.nodeid = ino; + out.body.create.entry.attr_valid = UINT64_MAX; + })); + + fd = open(FULLPATH, O_CREAT | O_EXCL, mode); + ASSERT_LE(0, fd) << strerror(errno); + + /* Finally, a subsequent lookup should query the daemon */ + expect_lookup(RELPATH, ino, S_IFREG | mode, 0, 1); + + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + leak(fd); +} + +/* + * The daemon is responsible for checking file permissions (unless the + * default_permissions mount option was used) + */ +TEST_F(Create, eperm) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + mode_t mode = S_IFREG | 0755; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_create(RELPATH, mode, ReturnErrno(EPERM)); + + EXPECT_EQ(-1, open(FULLPATH, O_CREAT | O_EXCL, mode)); + EXPECT_EQ(EPERM, errno); +} + +TEST_F(Create, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + mode_t mode = S_IFREG | 0755; + uint64_t ino = 42; + int fd; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_create(RELPATH, mode, + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, create); + out.body.create.entry.attr.mode = mode; + out.body.create.entry.nodeid = ino; + out.body.create.entry.entry_valid = UINT64_MAX; + out.body.create.entry.attr_valid = UINT64_MAX; + })); + + fd = open(FULLPATH, O_CREAT | O_EXCL, mode); + ASSERT_LE(0, fd) << strerror(errno); + leak(fd); +} + +/* + * Nothing bad should happen if the server returns the parent's inode number + * for the newly created file. Regression test for bug 263662 + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263662 + */ +TEST_F(Create, parent_inode) +{ + const char FULLPATH[] = "mountpoint/some_dir/some_file.txt"; + const char RELDIRPATH[] = "some_dir"; + const char RELPATH[] = "some_file.txt"; + mode_t mode = 0755; + uint64_t ino = 42; + int fd; + + expect_lookup(RELDIRPATH, ino, S_IFDIR | mode, 0, 1); + EXPECT_LOOKUP(ino, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_create(RELPATH, S_IFREG | mode, + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, create); + out.body.create.entry.attr.mode = S_IFREG | mode; + /* Return the same inode as the parent dir */ + out.body.create.entry.nodeid = ino; + out.body.create.entry.entry_valid = UINT64_MAX; + out.body.create.entry.attr_valid = UINT64_MAX; + })); + // FUSE_RELEASE happens asynchronously, so it may or may not arrive + // before the test completes. + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_RELEASE); + }, Eq(true)), + _) + ).Times(AtMost(1)) + .WillOnce(Invoke([=](auto in __unused, auto &out __unused) { })); + + fd = open(FULLPATH, O_CREAT | O_EXCL, mode); + ASSERT_EQ(-1, fd); + EXPECT_EQ(EIO, errno); +} + +/* + * A regression test for a bug that affected old FUSE implementations: + * open(..., O_WRONLY | O_CREAT, 0444) should work despite the seeming + * contradiction between O_WRONLY and 0444 + * + * For example: + * https://bugs.launchpad.net/ubuntu/+source/sshfs-fuse/+bug/44886 + */ +TEST_F(Create, wronly_0444) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + mode_t mode = S_IFREG | 0444; + uint64_t ino = 42; + int fd; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_create(RELPATH, mode, + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, create); + out.body.create.entry.attr.mode = mode; + out.body.create.entry.nodeid = ino; + out.body.create.entry.entry_valid = UINT64_MAX; + out.body.create.entry.attr_valid = UINT64_MAX; + })); + + fd = open(FULLPATH, O_CREAT | O_WRONLY, mode); + ASSERT_LE(0, fd) << strerror(errno); + leak(fd); +} + +TEST_F(Create_7_8, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + mode_t mode = S_IFREG | 0755; + uint64_t ino = 42; + int fd; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_create(RELPATH, mode, + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, create_7_8); + out.body.create_7_8.entry.attr.mode = mode; + out.body.create_7_8.entry.nodeid = ino; + out.body.create_7_8.entry.entry_valid = UINT64_MAX; + out.body.create_7_8.entry.attr_valid = UINT64_MAX; + out.body.create_7_8.open.fh = FH; + })); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, FH); + + fd = open(FULLPATH, O_CREAT | O_EXCL, mode); + ASSERT_LE(0, fd) << strerror(errno); + close(fd); +} + +TEST_F(Create_7_11, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + mode_t mode = S_IFREG | 0755; + uint64_t ino = 42; + int fd; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_create(RELPATH, mode, + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, create); + out.body.create.entry.attr.mode = mode; + out.body.create.entry.nodeid = ino; + out.body.create.entry.entry_valid = UINT64_MAX; + out.body.create.entry.attr_valid = UINT64_MAX; + })); + + fd = open(FULLPATH, O_CREAT | O_EXCL, mode); + ASSERT_LE(0, fd) << strerror(errno); + leak(fd); +} diff --git a/tests/sys/fs/fusefs/ctl.sh b/tests/sys/fs/fusefs/ctl.sh new file mode 100644 index 000000000000..7d2e7593cbdc --- /dev/null +++ b/tests/sys/fs/fusefs/ctl.sh @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 ConnectWise +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +. $(atf_get_srcdir)/../../cam/ctl/ctl.subr + +# Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=283402 +# +# Almost any fuse file system would work, but this tests uses fusefs-ext2 +# because it's simple and its download is very small. +atf_test_case remove_lun_with_atime cleanup +remove_lun_with_atime_head() +{ + atf_set "descr" "Remove a fuse-backed CTL LUN when atime is enabled" + atf_set "require.user" "root" + atf_set "require.progs" "fuse-ext2 mkfs.ext2" +} +remove_lun_with_atime_body() +{ + MOUNTPOINT=$PWD/mnt + atf_check mkdir $MOUNTPOINT + atf_check truncate -s 1g ext2.img + atf_check mkfs.ext2 -q ext2.img + # Note: both default_permissions and atime must be enabled + atf_check fuse-ext2 -o default_permissions,allow_other,rw+ ext2.img \ + $MOUNTPOINT + + atf_check truncate -s 1m $MOUNTPOINT/file + create_block -o file=$MOUNTPOINT/file + + # Force fusefs to open the file, and dirty its atime + atf_check dd if=/dev/$dev of=/dev/null count=1 status=none + + # Finally, remove the LUN. Hopefully it won't panic. + atf_check -o ignore ctladm remove -b block -l $LUN + + rm lun-create.txt # So we don't try to remove the LUN twice +} +remove_lun_with_atime_cleanup() +{ + cleanup + umount $PWD/mnt +} + +atf_init_test_cases() +{ + atf_add_test_case remove_lun_with_atime +} diff --git a/tests/sys/fs/fusefs/default_permissions.cc b/tests/sys/fs/fusefs/default_permissions.cc new file mode 100644 index 000000000000..4b04297d97ac --- /dev/null +++ b/tests/sys/fs/fusefs/default_permissions.cc @@ -0,0 +1,1644 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * Tests for the "default_permissions" mount option. They must be in their own + * file so they can be run as an unprivileged user. + */ + +extern "C" { +#include <sys/types.h> +#include <sys/extattr.h> + +#include <fcntl.h> +#include <semaphore.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class DefaultPermissions: public FuseTest { + +virtual void SetUp() { + m_default_permissions = true; + FuseTest::SetUp(); + if (HasFatalFailure() || IsSkipped()) + return; + + if (geteuid() == 0) { + GTEST_SKIP() << "This test requires an unprivileged user"; + } + + /* With -o default_permissions, FUSE_ACCESS should never be called */ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_ACCESS); + }, Eq(true)), + _) + ).Times(0); +} + +public: +void expect_chmod(uint64_t ino, mode_t mode, uint64_t size = 0) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == FATTR_MODE && + in.body.setattr.mode == mode); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | mode; + out.body.attr.attr.size = size; + out.body.attr.attr_valid = UINT64_MAX; + }))); +} + +void expect_create(const char *relpath, uint64_t ino) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(fuse_create_in); + return (in.header.opcode == FUSE_CREATE && + (0 == strcmp(relpath, name))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, create); + out.body.create.entry.attr.mode = S_IFREG | 0644; + out.body.create.entry.nodeid = ino; + out.body.create.entry.entry_valid = UINT64_MAX; + out.body.create.entry.attr_valid = UINT64_MAX; + }))); +} + +void expect_copy_file_range(uint64_t ino_in, uint64_t off_in, uint64_t ino_out, + uint64_t off_out, uint64_t len) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_COPY_FILE_RANGE && + in.header.nodeid == ino_in && + in.body.copy_file_range.off_in == off_in && + in.body.copy_file_range.nodeid_out == ino_out && + in.body.copy_file_range.off_out == off_out && + in.body.copy_file_range.len == len); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = len; + }))); +} + +void expect_getattr(uint64_t ino, mode_t mode, uint64_t attr_valid, int times, + uid_t uid = 0, gid_t gid = 0) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(times) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = mode; + out.body.attr.attr.size = 0; + out.body.attr.attr.uid = uid; + out.body.attr.attr.gid = gid; + out.body.attr.attr_valid = attr_valid; + }))); +} + +void expect_lookup(const char *relpath, uint64_t ino, mode_t mode, + uint64_t attr_valid, uid_t uid = 0, gid_t gid = 0) +{ + FuseTest::expect_lookup(relpath, ino, mode, 0, 1, attr_valid, uid, gid); +} + +}; + +class Access: public DefaultPermissions {}; +class Chown: public DefaultPermissions {}; +class Chgrp: public DefaultPermissions {}; +class CopyFileRange: public DefaultPermissions {}; +class Fspacectl: public DefaultPermissions {}; +class Lookup: public DefaultPermissions {}; +class Open: public DefaultPermissions {}; +class PosixFallocate: public DefaultPermissions {}; +class Read: public DefaultPermissions {}; +class Setattr: public DefaultPermissions {}; +class Unlink: public DefaultPermissions {}; +class Utimensat: public DefaultPermissions {}; +class Write: public DefaultPermissions {}; + +/* + * Test permission handling during create, mkdir, mknod, link, symlink, and + * rename vops (they all share a common path for permission checks in + * VOP_LOOKUP) + */ +class Create: public DefaultPermissions {}; + +class Deleteextattr: public DefaultPermissions { +public: +void expect_removexattr() +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_REMOVEXATTR); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(0))); +} +}; + +class Getextattr: public DefaultPermissions { +public: +void expect_getxattr(ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETXATTR); + }, Eq(true)), + _) + ).WillOnce(Invoke(r)); +} +}; + +class Listextattr: public DefaultPermissions { +public: +void expect_listxattr() +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LISTXATTR); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) { + out.body.listxattr.size = 0; + SET_OUT_HEADER_LEN(out, listxattr); + }))); +} +}; + +class Rename: public DefaultPermissions { +public: + /* + * Expect a rename and respond with the given error. Don't both to + * validate arguments; the tests in rename.cc do that. + */ + void expect_rename(int error) + { + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_RENAME); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(error))); + } +}; + +class Setextattr: public DefaultPermissions { +public: +void expect_setxattr(int error) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETXATTR); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(error))); +} +}; + +/* Return a group to which this user does not belong */ +static gid_t excluded_group() +{ + int i, ngroups = 64; + gid_t newgid, groups[ngroups]; + + getgrouplist(getlogin(), getegid(), groups, &ngroups); + for (newgid = 0; ; newgid++) { + bool belongs = false; + + for (i = 0; i < ngroups; i++) { + if (groups[i] == newgid) + belongs = true; + } + if (!belongs) + break; + } + /* newgid is now a group to which the current user does not belong */ + return newgid; +} + +TEST_F(Access, eacces) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + mode_t access_mode = X_OK; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX); + + ASSERT_NE(0, access(FULLPATH, access_mode)); + ASSERT_EQ(EACCES, errno); +} + +TEST_F(Access, eacces_no_cached_attrs) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + mode_t access_mode = X_OK; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, 0, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0); + expect_getattr(ino, S_IFREG | 0644, 0, 1); + /* + * Once default_permissions is properly implemented, there might be + * another FUSE_GETATTR or something in here. But there should not be + * a FUSE_ACCESS + */ + + ASSERT_NE(0, access(FULLPATH, access_mode)); + ASSERT_EQ(EACCES, errno); +} + +TEST_F(Access, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + mode_t access_mode = R_OK; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX); + /* + * Once default_permissions is properly implemented, there might be + * another FUSE_GETATTR or something in here. + */ + + ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno); +} + +/* Unprivileged users may chown a file to their own uid */ +TEST_F(Chown, chown_to_self) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const mode_t mode = 0755; + uid_t uid; + + uid = geteuid(); + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid); + expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid); + /* The OS may optimize chown by omitting the redundant setattr */ + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_SETATTR); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out){ + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.mode = S_IFREG | mode; + out.body.attr.attr.uid = uid; + }))); + + EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno); +} + +/* + * A successful chown by a non-privileged non-owner should clear a file's SUID + * bit + */ +TEST_F(Chown, clear_suid) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + const mode_t oldmode = 06755; + const mode_t newmode = 0755; + uid_t uid = geteuid(); + uint32_t valid = FATTR_UID | FATTR_MODE; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid); + expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + in.body.setattr.mode == newmode); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | newmode; + out.body.attr.attr_valid = UINT64_MAX; + }))); + + EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno); +} + + +/* Only root may change a file's owner */ +TEST_F(Chown, eperm) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const mode_t mode = 0755; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, geteuid()); + expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, geteuid()); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_SETATTR); + }, Eq(true)), + _) + ).Times(0); + + EXPECT_NE(0, chown(FULLPATH, 0, -1)); + EXPECT_EQ(EPERM, errno); +} + +/* + * A successful chgrp by a non-privileged non-owner should clear a file's SUID + * bit + */ +TEST_F(Chgrp, clear_suid) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + const mode_t oldmode = 06755; + const mode_t newmode = 0755; + uid_t uid = geteuid(); + gid_t gid = getegid(); + uint32_t valid = FATTR_GID | FATTR_MODE; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid); + expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + in.body.setattr.mode == newmode); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | newmode; + out.body.attr.attr_valid = UINT64_MAX; + }))); + + EXPECT_EQ(0, chown(FULLPATH, -1, gid)) << strerror(errno); +} + +/* non-root users may only chgrp a file to a group they belong to */ +TEST_F(Chgrp, eperm) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const mode_t mode = 0755; + uid_t uid; + gid_t gid, newgid; + + uid = geteuid(); + gid = getegid(); + newgid = excluded_group(); + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid); + expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_SETATTR); + }, Eq(true)), + _) + ).Times(0); + + EXPECT_NE(0, chown(FULLPATH, -1, newgid)); + EXPECT_EQ(EPERM, errno); +} + +TEST_F(Chgrp, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const mode_t mode = 0755; + uid_t uid; + gid_t gid, newgid; + + uid = geteuid(); + gid = 0; + newgid = getegid(); + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid); + expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid); + /* The OS may optimize chgrp by omitting the redundant setattr */ + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out){ + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.mode = S_IFREG | mode; + out.body.attr.attr.uid = uid; + out.body.attr.attr.gid = newgid; + }))); + + EXPECT_EQ(0, chown(FULLPATH, -1, newgid)) << strerror(errno); +} + +/* A write by a non-owner should clear a file's SGID bit */ +TEST_F(CopyFileRange, clear_sgid) +{ + const char FULLPATH_IN[] = "mountpoint/in.txt"; + const char RELPATH_IN[] = "in.txt"; + const char FULLPATH_OUT[] = "mountpoint/out.txt"; + const char RELPATH_OUT[] = "out.txt"; + struct stat sb; + uint64_t ino_in = 42; + uint64_t ino_out = 43; + mode_t oldmode = 02777; + mode_t newmode = 0777; + off_t fsize = 16; + off_t off_in = 0; + off_t off_out = 8; + off_t len = 8; + int fd_in, fd_out; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + FuseTest::expect_lookup(RELPATH_IN, ino_in, S_IFREG | oldmode, fsize, 1, + UINT64_MAX, 0, 0); + expect_open(ino_in, 0, 1); + FuseTest::expect_lookup(RELPATH_OUT, ino_out, S_IFREG | oldmode, fsize, + 1, UINT64_MAX, 0, 0); + expect_open(ino_out, 0, 1); + expect_copy_file_range(ino_in, off_in, ino_out, off_out, len); + expect_chmod(ino_out, newmode, fsize); + + fd_in = open(FULLPATH_IN, O_RDONLY); + ASSERT_LE(0, fd_in) << strerror(errno); + fd_out = open(FULLPATH_OUT, O_WRONLY); + ASSERT_LE(0, fd_out) << strerror(errno); + ASSERT_EQ(len, + copy_file_range(fd_in, &off_in, fd_out, &off_out, len, 0)) + << strerror(errno); + ASSERT_EQ(0, fstat(fd_out, &sb)) << strerror(errno); + EXPECT_EQ(S_IFREG | newmode, sb.st_mode); + ASSERT_EQ(0, fstat(fd_in, &sb)) << strerror(errno); + EXPECT_EQ(S_IFREG | oldmode, sb.st_mode); + + leak(fd_in); + leak(fd_out); +} + +/* A write by a non-owner should clear a file's SUID bit */ +TEST_F(CopyFileRange, clear_suid) +{ + const char FULLPATH_IN[] = "mountpoint/in.txt"; + const char RELPATH_IN[] = "in.txt"; + const char FULLPATH_OUT[] = "mountpoint/out.txt"; + const char RELPATH_OUT[] = "out.txt"; + struct stat sb; + uint64_t ino_in = 42; + uint64_t ino_out = 43; + mode_t oldmode = 04777; + mode_t newmode = 0777; + off_t fsize = 16; + off_t off_in = 0; + off_t off_out = 8; + off_t len = 8; + int fd_in, fd_out; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + FuseTest::expect_lookup(RELPATH_IN, ino_in, S_IFREG | oldmode, fsize, 1, + UINT64_MAX, 0, 0); + expect_open(ino_in, 0, 1); + FuseTest::expect_lookup(RELPATH_OUT, ino_out, S_IFREG | oldmode, fsize, + 1, UINT64_MAX, 0, 0); + expect_open(ino_out, 0, 1); + expect_copy_file_range(ino_in, off_in, ino_out, off_out, len); + expect_chmod(ino_out, newmode, fsize); + + fd_in = open(FULLPATH_IN, O_RDONLY); + ASSERT_LE(0, fd_in) << strerror(errno); + fd_out = open(FULLPATH_OUT, O_WRONLY); + ASSERT_LE(0, fd_out) << strerror(errno); + ASSERT_EQ(len, + copy_file_range(fd_in, &off_in, fd_out, &off_out, len, 0)) + << strerror(errno); + ASSERT_EQ(0, fstat(fd_out, &sb)) << strerror(errno); + EXPECT_EQ(S_IFREG | newmode, sb.st_mode); + ASSERT_EQ(0, fstat(fd_in, &sb)) << strerror(errno); + EXPECT_EQ(S_IFREG | oldmode, sb.st_mode); + + leak(fd_in); + leak(fd_out); +} + +TEST_F(Create, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_create(RELPATH, ino); + + fd = open(FULLPATH, O_CREAT | O_EXCL, 0644); + ASSERT_LE(0, fd) << strerror(errno); + leak(fd); +} + +TEST_F(Create, eacces) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + + ASSERT_EQ(-1, open(FULLPATH, O_CREAT | O_EXCL, 0644)); + EXPECT_EQ(EACCES, errno); +} + +TEST_F(Deleteextattr, eacces) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0); + + ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo")); + ASSERT_EQ(EACCES, errno); +} + +TEST_F(Deleteextattr, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); + expect_removexattr(); + + ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo")) + << strerror(errno); +} + +/* Delete system attributes requires superuser privilege */ +TEST_F(Deleteextattr, system) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_SYSTEM; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid()); + + ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo")); + ASSERT_EQ(EPERM, errno); +} + +/* Anybody with write permission can set both timestamps to UTIME_NOW */ +TEST_F(Utimensat, utime_now) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + /* Write permissions for everybody */ + const mode_t mode = 0666; + uid_t owner = 0; + const timespec times[2] = { + {.tv_sec = 0, .tv_nsec = UTIME_NOW}, + {.tv_sec = 0, .tv_nsec = UTIME_NOW}, + }; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, owner); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid & FATTR_ATIME && + in.body.setattr.valid & FATTR_MTIME); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.mode = S_IFREG | mode; + }))); + + ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, ×[0], 0)) + << strerror(errno); +} + +/* Anybody can set both timestamps to UTIME_OMIT */ +TEST_F(Utimensat, utime_omit) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + /* Write permissions for no one */ + const mode_t mode = 0444; + uid_t owner = 0; + const timespec times[2] = { + {.tv_sec = 0, .tv_nsec = UTIME_OMIT}, + {.tv_sec = 0, .tv_nsec = UTIME_OMIT}, + }; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, owner); + + ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, ×[0], 0)) + << strerror(errno); +} + +/* Deleting user attributes merely requires WRITE privilege */ +TEST_F(Deleteextattr, user) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0); + expect_removexattr(); + + ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo")) + << strerror(errno); +} + +TEST_F(Getextattr, eacces) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + char data[80]; + int ns = EXTATTR_NAMESPACE_USER; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0600, UINT64_MAX, 0); + + ASSERT_EQ(-1, + extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data))); + ASSERT_EQ(EACCES, errno); +} + +TEST_F(Getextattr, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + char data[80]; + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + int ns = EXTATTR_NAMESPACE_USER; + ssize_t r; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + /* Getting user attributes only requires read access */ + expect_lookup(RELPATH, ino, S_IFREG | 0444, UINT64_MAX, 0); + expect_getxattr( + ReturnImmediate([&](auto in __unused, auto& out) { + memcpy((void*)out.body.bytes, value, value_len); + out.header.len = sizeof(out.header) + value_len; + }) + ); + + r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)); + ASSERT_EQ(value_len, r) << strerror(errno); + EXPECT_STREQ(value, data); +} + +/* Getting system attributes requires superuser privileges */ +TEST_F(Getextattr, system) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + char data[80]; + int ns = EXTATTR_NAMESPACE_SYSTEM; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid()); + + ASSERT_EQ(-1, + extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data))); + ASSERT_EQ(EPERM, errno); +} + +TEST_F(Listextattr, eacces) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0600, UINT64_MAX, 0); + + ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0)); + ASSERT_EQ(EACCES, errno); +} + +TEST_F(Listextattr, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); + /* Listing user extended attributes merely requires read access */ + expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0); + expect_listxattr(); + + ASSERT_EQ(0, extattr_list_file(FULLPATH, ns, NULL, 0)) + << strerror(errno); +} + +/* Listing system xattrs requires superuser privileges */ +TEST_F(Listextattr, system) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_SYSTEM; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); + /* Listing user extended attributes merely requires read access */ + expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); + + ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0)); + ASSERT_EQ(EPERM, errno); +} + +/* A write by a non-owner should clear a file's SGID bit */ +TEST_F(Fspacectl, clear_sgid) +{ + const char FULLPATH[] = "mountpoint/file.txt"; + const char RELPATH[] = "file.txt"; + struct stat sb; + struct spacectl_range rqsr; + uint64_t ino = 42; + mode_t oldmode = 02777; + mode_t newmode = 0777; + off_t fsize = 16; + off_t off = 8; + off_t len = 8; + int fd; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize, + 1, UINT64_MAX, 0, 0); + expect_open(ino, 0, 1); + expect_fallocate(ino, off, len, + FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); + expect_chmod(ino, newmode, fsize); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + rqsr.r_len = len; + rqsr.r_offset = off; + EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); + ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); + EXPECT_EQ(S_IFREG | newmode, sb.st_mode); + + leak(fd); +} + +/* A write by a non-owner should clear a file's SUID bit */ +TEST_F(Fspacectl, clear_suid) +{ + const char FULLPATH[] = "mountpoint/file.txt"; + const char RELPATH[] = "file.txt"; + struct stat sb; + struct spacectl_range rqsr; + uint64_t ino = 42; + mode_t oldmode = 04777; + mode_t newmode = 0777; + off_t fsize = 16; + off_t off = 8; + off_t len = 8; + int fd; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize, + 1, UINT64_MAX, 0, 0); + expect_open(ino, 0, 1); + expect_fallocate(ino, off, len, + FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); + expect_chmod(ino, newmode, fsize); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + rqsr.r_len = len; + rqsr.r_offset = off; + EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); + ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); + EXPECT_EQ(S_IFREG | newmode, sb.st_mode); + + leak(fd); +} + +/* + * fspacectl() of a file without writable permissions should succeed as + * long as the file descriptor is writable. This is important when combined + * with O_CREAT + */ +TEST_F(Fspacectl, posix_fallocate_of_newly_created_file) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct spacectl_range rqsr; + const uint64_t ino = 42; + off_t off = 8; + off_t len = 8; + int fd; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_create(RELPATH, ino); + expect_fallocate(ino, off, len, + FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); + + fd = open(FULLPATH, O_CREAT | O_RDWR, 0); + ASSERT_LE(0, fd) << strerror(errno); + rqsr.r_len = len; + rqsr.r_offset = off; + EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); + leak(fd); +} + +/* A component of the search path lacks execute permissions */ +TEST_F(Lookup, eacces) +{ + const char FULLPATH[] = "mountpoint/some_dir/some_file.txt"; + const char RELDIRPATH[] = "some_dir"; + uint64_t dir_ino = 42; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELDIRPATH, dir_ino, S_IFDIR | 0700, UINT64_MAX, 0); + + EXPECT_EQ(-1, access(FULLPATH, F_OK)); + EXPECT_EQ(EACCES, errno); +} + +TEST_F(Open, eacces) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX); + + EXPECT_EQ(-1, open(FULLPATH, O_RDWR)); + EXPECT_EQ(EACCES, errno); +} + +TEST_F(Open, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX); + expect_open(ino, 0, 1); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + leak(fd); +} + +/* A write by a non-owner should clear a file's SGID bit */ +TEST_F(PosixFallocate, clear_sgid) +{ + const char FULLPATH[] = "mountpoint/file.txt"; + const char RELPATH[] = "file.txt"; + struct stat sb; + uint64_t ino = 42; + mode_t oldmode = 02777; + mode_t newmode = 0777; + off_t fsize = 16; + off_t off = 8; + off_t len = 8; + int fd; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize, + 1, UINT64_MAX, 0, 0); + expect_open(ino, 0, 1); + expect_fallocate(ino, off, len, 0, 0); + expect_chmod(ino, newmode, fsize); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); + EXPECT_EQ(S_IFREG | newmode, sb.st_mode); + + leak(fd); +} + +/* A write by a non-owner should clear a file's SUID bit */ +TEST_F(PosixFallocate, clear_suid) +{ + const char FULLPATH[] = "mountpoint/file.txt"; + const char RELPATH[] = "file.txt"; + struct stat sb; + uint64_t ino = 42; + mode_t oldmode = 04777; + mode_t newmode = 0777; + off_t fsize = 16; + off_t off = 8; + off_t len = 8; + int fd; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize, + 1, UINT64_MAX, 0, 0); + expect_open(ino, 0, 1); + expect_fallocate(ino, off, len, 0, 0); + expect_chmod(ino, newmode, fsize); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); + EXPECT_EQ(S_IFREG | newmode, sb.st_mode); + + leak(fd); +} + +/* + * posix_fallocate() of a file without writable permissions should succeed as + * long as the file descriptor is writable. This is important when combined + * with O_CREAT + */ +TEST_F(PosixFallocate, posix_fallocate_of_newly_created_file) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t off = 8; + off_t len = 8; + int fd; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_create(RELPATH, ino); + expect_fallocate(ino, off, len, 0, 0); + + fd = open(FULLPATH, O_CREAT | O_RDWR, 0); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno); + leak(fd); +} + +TEST_F(Rename, eacces_on_srcdir) +{ + const char FULLDST[] = "mountpoint/d/dst"; + const char RELDST[] = "d/dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELSRC[] = "src"; + uint64_t ino = 42; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, 0); + expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) + .Times(AnyNumber()) + .WillRepeatedly(Invoke(ReturnErrno(ENOENT))); + + ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); + ASSERT_EQ(EACCES, errno); +} + +TEST_F(Rename, eacces_on_dstdir_for_creating) +{ + const char FULLDST[] = "mountpoint/d/dst"; + const char RELDSTDIR[] = "d"; + const char RELDST[] = "dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELSRC[] = "src"; + uint64_t src_ino = 42; + uint64_t dstdir_ino = 43; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); + expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX); + expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX); + EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT))); + + ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); + ASSERT_EQ(EACCES, errno); +} + +TEST_F(Rename, eacces_on_dstdir_for_removing) +{ + const char FULLDST[] = "mountpoint/d/dst"; + const char RELDSTDIR[] = "d"; + const char RELDST[] = "dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELSRC[] = "src"; + uint64_t src_ino = 42; + uint64_t dstdir_ino = 43; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); + expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX); + expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX); + EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT))); + + ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); + ASSERT_EQ(EACCES, errno); +} + +TEST_F(Rename, eperm_on_sticky_srcdir) +{ + const char FULLDST[] = "mountpoint/d/dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELSRC[] = "src"; + uint64_t ino = 42; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0); + expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX); + + ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); + ASSERT_EQ(EPERM, errno); +} + +/* + * A user cannot move out a subdirectory that he does not own, because that + * would require changing the subdirectory's ".." dirent + */ +TEST_F(Rename, eperm_for_subdirectory) +{ + const char FULLDST[] = "mountpoint/d/dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELDSTDIR[] = "d"; + const char RELDST[] = "dst"; + const char RELSRC[] = "src"; + uint64_t ino = 42; + uint64_t dstdir_ino = 43; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); + expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0); + expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0777, UINT64_MAX, 0); + EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT))); + + ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); + ASSERT_EQ(EACCES, errno); +} + +/* + * A user _can_ rename a subdirectory to which he lacks write permissions, if + * it will keep the same parent + */ +TEST_F(Rename, subdirectory_to_same_dir) +{ + const char FULLDST[] = "mountpoint/dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELDST[] = "dst"; + const char RELSRC[] = "src"; + uint64_t ino = 42; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); + expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_rename(0); + + ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); +} + +TEST_F(Rename, eperm_on_sticky_dstdir) +{ + const char FULLDST[] = "mountpoint/d/dst"; + const char RELDSTDIR[] = "d"; + const char RELDST[] = "dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELSRC[] = "src"; + uint64_t src_ino = 42; + uint64_t dstdir_ino = 43; + uint64_t dst_ino = 44; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); + expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX); + expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 01777, UINT64_MAX); + EXPECT_LOOKUP(dstdir_ino, RELDST) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = dst_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr.uid = 0; + }))); + + ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); + ASSERT_EQ(EPERM, errno); +} + +/* Successfully rename a file, overwriting the destination */ +TEST_F(Rename, ok) +{ + const char FULLDST[] = "mountpoint/dst"; + const char RELDST[] = "dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELSRC[] = "src"; + // The inode of the already-existing destination file + uint64_t dst_ino = 2; + uint64_t ino = 42; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, geteuid()); + expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX); + expect_lookup(RELDST, dst_ino, S_IFREG | 0644, UINT64_MAX); + expect_rename(0); + + ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); +} + +TEST_F(Rename, ok_to_remove_src_because_of_stickiness) +{ + const char FULLDST[] = "mountpoint/dst"; + const char RELDST[] = "dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELSRC[] = "src"; + uint64_t ino = 42; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0); + expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_rename(0); + + ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); +} + +// Don't update atime during close after read, if we lack permissions to write +// that file. +TEST_F(Read, atime_during_close) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = 100; + uint8_t buf[bufsize]; + const char *CONTENTS = "abcdefgh"; + ssize_t fsize = sizeof(CONTENTS); + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0755, fsize, + 1, UINT64_MAX, 0, 0); + expect_open(ino, 0, 1); + expect_read(ino, 0, fsize, fsize, CONTENTS); + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + return (in.header.opcode == FUSE_SETATTR); + }, Eq(true)), + _) + ).Times(0); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, FuseTest::FH); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + /* Ensure atime will be different than during lookup */ + nap(); + + ASSERT_EQ(fsize, read(fd, buf, bufsize)) << strerror(errno); + + close(fd); +} + +TEST_F(Setattr, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const mode_t oldmode = 0755; + const mode_t newmode = 0644; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid()); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.mode == newmode); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.mode = S_IFREG | newmode; + }))); + + EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); +} + +TEST_F(Setattr, eacces) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const mode_t oldmode = 0755; + const mode_t newmode = 0644; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, 0); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_SETATTR); + }, Eq(true)), + _) + ).Times(0); + + EXPECT_NE(0, chmod(FULLPATH, newmode)); + EXPECT_EQ(EPERM, errno); +} + +/* + * ftruncate() of a file without writable permissions should succeed as long as + * the file descriptor is writable. This is important when combined with + * O_CREAT + */ +TEST_F(Setattr, ftruncate_of_newly_created_file) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const mode_t mode = 0000; + int fd; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_create(RELPATH, ino); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + (in.body.setattr.valid & FATTR_SIZE)); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFREG | mode; + out.body.attr.attr_valid = UINT64_MAX; + }))); + + fd = open(FULLPATH, O_CREAT | O_RDWR, 0); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, ftruncate(fd, 100)) << strerror(errno); + leak(fd); +} + +/* + * Setting the sgid bit should fail for an unprivileged user who doesn't belong + * to the file's group + */ +TEST_F(Setattr, sgid_by_non_group_member) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const mode_t oldmode = 0755; + const mode_t newmode = 02755; + uid_t uid = geteuid(); + gid_t gid = excluded_group(); + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_SETATTR); + }, Eq(true)), + _) + ).Times(0); + + EXPECT_NE(0, chmod(FULLPATH, newmode)); + EXPECT_EQ(EPERM, errno); +} + +/* Only the superuser may set the sticky bit on a non-directory */ +TEST_F(Setattr, sticky_regular_file) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const mode_t oldmode = 0644; + const mode_t newmode = 01644; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid()); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_SETATTR); + }, Eq(true)), + _) + ).Times(0); + + EXPECT_NE(0, chmod(FULLPATH, newmode)); + EXPECT_EQ(EFTYPE, errno); +} + +TEST_F(Setextattr, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + int ns = EXTATTR_NAMESPACE_USER; + ssize_t r; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); + expect_setxattr(0); + + r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value, + value_len); + ASSERT_EQ(value_len, r) << strerror(errno); +} + +TEST_F(Setextattr, eacces) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + int ns = EXTATTR_NAMESPACE_USER; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0); + + ASSERT_EQ(-1, extattr_set_file(FULLPATH, ns, "foo", (const void*)value, + value_len)); + ASSERT_EQ(EACCES, errno); +} + +// Setting system attributes requires superuser privileges +TEST_F(Setextattr, system) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + int ns = EXTATTR_NAMESPACE_SYSTEM; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid()); + + ASSERT_EQ(-1, extattr_set_file(FULLPATH, ns, "foo", (const void*)value, + value_len)); + ASSERT_EQ(EPERM, errno); +} + +// Setting user attributes merely requires write privileges +TEST_F(Setextattr, user) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + int ns = EXTATTR_NAMESPACE_USER; + ssize_t r; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0); + expect_setxattr(0); + + r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value, + value_len); + ASSERT_EQ(value_len, r) << strerror(errno); +} + +TEST_F(Unlink, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + sem_t sem; + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); + expect_unlink(FUSE_ROOT_ID, RELPATH, 0); + expect_forget(ino, 1, &sem); + + ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno); + + sem_wait(&sem); + sem_destroy(&sem); +} + +/* + * Ensure that a cached name doesn't cause unlink to bypass permission checks + * in VOP_LOOKUP. + * + * This test should pass because lookup(9) purges the namecache entry by doing + * a vfs_cache_lookup with ~MAKEENTRY when nameiop == DELETE. + */ +TEST_F(Unlink, cached_unwritable_directory) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .Times(AnyNumber()) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.entry_valid = UINT64_MAX; + })) + ); + + /* Fill name cache */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + /* Despite cached name , unlink should fail */ + ASSERT_EQ(-1, unlink(FULLPATH)); + ASSERT_EQ(EACCES, errno); +} + +TEST_F(Unlink, unwritable_directory) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); + + ASSERT_EQ(-1, unlink(FULLPATH)); + ASSERT_EQ(EACCES, errno); +} + +TEST_F(Unlink, sticky_directory) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0); + + ASSERT_EQ(-1, unlink(FULLPATH)); + ASSERT_EQ(EPERM, errno); +} + +/* A write by a non-owner should clear a file's SUID bit */ +TEST_F(Write, clear_suid) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct stat sb; + uint64_t ino = 42; + mode_t oldmode = 04777; + mode_t newmode = 0777; + char wbuf[1] = {'x'}; + int fd; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX); + expect_open(ino, 0, 1); + expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf); + expect_chmod(ino, newmode, sizeof(wbuf)); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); + EXPECT_EQ(S_IFREG | newmode, sb.st_mode); + leak(fd); +} + +/* A write by a non-owner should clear a file's SGID bit */ +TEST_F(Write, clear_sgid) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct stat sb; + uint64_t ino = 42; + mode_t oldmode = 02777; + mode_t newmode = 0777; + char wbuf[1] = {'x'}; + int fd; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX); + expect_open(ino, 0, 1); + expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf); + expect_chmod(ino, newmode, sizeof(wbuf)); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); + EXPECT_EQ(S_IFREG | newmode, sb.st_mode); + leak(fd); +} + +/* Regression test for a specific recurse-of-nonrecursive-lock panic + * + * With writeback caching, we can't call vtruncbuf from fuse_io_strategy, or it + * may panic. That happens if the FUSE_SETATTR response indicates that the + * file's size has changed since the write. + */ +TEST_F(Write, recursion_panic_while_clearing_suid) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + mode_t oldmode = 04777; + mode_t newmode = 0777; + char wbuf[1] = {'x'}; + int fd; + + expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX); + expect_open(ino, 0, 1); + expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf); + /* XXX Return a smaller file size than what we just wrote! */ + expect_chmod(ino, newmode, 0); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno); + leak(fd); +} diff --git a/tests/sys/fs/fusefs/default_permissions_privileged.cc b/tests/sys/fs/fusefs/default_permissions_privileged.cc new file mode 100644 index 000000000000..43f2141c1984 --- /dev/null +++ b/tests/sys/fs/fusefs/default_permissions_privileged.cc @@ -0,0 +1,124 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * Tests for the "default_permissions" mount option that require a privileged + * user. + */ + +extern "C" { +#include <sys/types.h> +#include <sys/extattr.h> + +#include <fcntl.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class DefaultPermissionsPrivileged: public FuseTest { +virtual void SetUp() { + m_default_permissions = true; + FuseTest::SetUp(); + if (HasFatalFailure() || IsSkipped()) + return; + + if (geteuid() != 0) { + GTEST_SKIP() << "This test requires a privileged user"; + } + + /* With -o default_permissions, FUSE_ACCESS should never be called */ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_ACCESS); + }, Eq(true)), + _) + ).Times(0); +} + +public: +void expect_getattr(uint64_t ino, mode_t mode, uint64_t attr_valid, int times, + uid_t uid = 0, gid_t gid = 0) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(times) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = mode; + out.body.attr.attr.size = 0; + out.body.attr.attr.uid = uid; + out.body.attr.attr.gid = gid; + out.body.attr.attr_valid = attr_valid; + }))); +} + +void expect_lookup(const char *relpath, uint64_t ino, mode_t mode, + uint64_t attr_valid, uid_t uid = 0, gid_t gid = 0) +{ + FuseTest::expect_lookup(relpath, ino, mode, 0, 1, attr_valid, uid, gid); +} + +}; + +class Setattr: public DefaultPermissionsPrivileged {}; + +TEST_F(Setattr, sticky_regular_file) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const mode_t oldmode = 0644; + const mode_t newmode = 01644; + + expect_getattr(1, S_IFDIR | 0755, UINT64_MAX, 1); + expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid()); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_SETATTR); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.mode = S_IFREG | newmode; + }))); + + EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); +} + + diff --git a/tests/sys/fs/fusefs/destroy.cc b/tests/sys/fs/fusefs/destroy.cc new file mode 100644 index 000000000000..45acb1f99724 --- /dev/null +++ b/tests/sys/fs/fusefs/destroy.cc @@ -0,0 +1,157 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <fcntl.h> +#include <pthread.h> +#include <semaphore.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +/* Tests for orderly unmounts */ +class Destroy: public FuseTest {}; + +/* Tests for unexpected deaths of the server */ +class Death: public FuseTest{}; + +static void* open_th(void* arg) { + int fd; + const char *path = (const char*)arg; + + fd = open(path, O_RDONLY); + EXPECT_EQ(-1, fd); + EXPECT_EQ(ENOTCONN, errno); + return 0; +} + +/* + * The server dies with unsent operations still on the message queue. + * Check for any memory leaks like this: + * 1) kldunload fusefs, if necessary + * 2) kldload fusefs + * 3) ./destroy --gtest_filter=Death.unsent_operations + * 4) kldunload fusefs + * 5) check /var/log/messages for anything like this: +Freed UMA keg (fuse_ticket) was not empty (31 items). Lost 2 pages of memory. +Warning: memory type fuse_msgbuf leaked memory on destroy (68 allocations, 428800 bytes leaked). + */ +TEST_F(Death, unsent_operations) +{ + const char FULLPATH0[] = "mountpoint/some_file.txt"; + const char FULLPATH1[] = "mountpoint/other_file.txt"; + const char RELPATH0[] = "some_file.txt"; + const char RELPATH1[] = "other_file.txt"; + pthread_t th0, th1; + ino_t ino0 = 42, ino1 = 43; + sem_t sem; + mode_t mode = S_IFREG | 0644; + + sem_init(&sem, 0, 0); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH0) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino0; + out.body.entry.attr.nlink = 1; + }))); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH1) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino1; + out.body.entry.attr.nlink = 1; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + return (in.header.opcode == FUSE_OPEN); + }, Eq(true)), + _) + ).WillOnce(Invoke([&](auto in __unused, auto &out __unused) { + sem_post(&sem); + pause(); + })); + + /* + * One thread's operation will be sent to the daemon and block, and the + * other's will be stuck in the message queue. + */ + ASSERT_EQ(0, pthread_create(&th0, NULL, open_th, + __DECONST(void*, FULLPATH0))) << strerror(errno); + ASSERT_EQ(0, pthread_create(&th1, NULL, open_th, + __DECONST(void*, FULLPATH1))) << strerror(errno); + + /* Wait for the first thread to block */ + sem_wait(&sem); + /* Give the second thread time to block */ + nap(); + + m_mock->kill_daemon(); + + pthread_join(th0, NULL); + pthread_join(th1, NULL); + + sem_destroy(&sem); +} + +/* + * On unmount the kernel should send a FUSE_DESTROY operation. It should also + * send FUSE_FORGET operations for all inodes with lookup_count > 0. + */ +TEST_F(Destroy, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2); + expect_forget(ino, 2); + expect_destroy(0); + + /* + * access(2) the file to force a lookup. Access it twice to double its + * lookup count. + */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + + /* + * Unmount, triggering a FUSE_DESTROY and also causing a VOP_RECLAIM + * for every vnode on this mp, triggering FUSE_FORGET for each of them. + */ + m_mock->unmount(); +} diff --git a/tests/sys/fs/fusefs/dev_fuse_poll.cc b/tests/sys/fs/fusefs/dev_fuse_poll.cc new file mode 100644 index 000000000000..181cd69de665 --- /dev/null +++ b/tests/sys/fs/fusefs/dev_fuse_poll.cc @@ -0,0 +1,229 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * This file tests different polling methods for the /dev/fuse device + */ + +extern "C" { +#include <fcntl.h> +#include <semaphore.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +const char FULLPATH[] = "mountpoint/some_file.txt"; +const char RELPATH[] = "some_file.txt"; +const uint64_t ino = 42; +const mode_t access_mode = R_OK; + +/* + * Translate a poll method's string representation to the enum value. + * Using strings with ::testing::Values gives better output with + * --gtest_list_tests + */ +enum poll_method poll_method_from_string(const char *s) +{ + if (0 == strcmp("BLOCKING", s)) + return BLOCKING; + else if (0 == strcmp("KQ", s)) + return KQ; + else if (0 == strcmp("POLL", s)) + return POLL; + else + return SELECT; +} + +class DevFusePoll: public FuseTest, public WithParamInterface<const char *> { + virtual void SetUp() { + m_pm = poll_method_from_string(GetParam()); + FuseTest::SetUp(); + } +}; + +class Kqueue: public FuseTest { + virtual void SetUp() { + m_pm = KQ; + FuseTest::SetUp(); + } +}; + +TEST_P(DevFusePoll, access) +{ + expect_access(FUSE_ROOT_ID, X_OK, 0); + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_access(ino, access_mode, 0); + + ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno); +} + +/* Ensure that we wake up pollers during unmount */ +TEST_P(DevFusePoll, destroy) +{ + expect_destroy(0); + + m_mock->unmount(); +} + +INSTANTIATE_TEST_SUITE_P(PM, DevFusePoll, + ::testing::Values("BLOCKING", "KQ", "POLL", "SELECT")); + +static void* statter(void* arg) { + const char *name; + struct stat sb; + + name = (const char*)arg; + return ((void*)(intptr_t)stat(name, &sb)); +} + +/* + * A kevent's data field should contain the number of operations available to + * be immediately read. + */ +TEST_F(Kqueue, data) +{ + pthread_t th0, th1, th2; + sem_t sem0, sem1; + int nready0, nready1, nready2; + uint64_t foo_ino = 42; + uint64_t bar_ino = 43; + uint64_t baz_ino = 44; + Sequence seq; + void *th_ret; + + ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno); + ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno); + + EXPECT_LOOKUP(FUSE_ROOT_ID, "foo") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = foo_ino; + }))); + EXPECT_LOOKUP(FUSE_ROOT_ID, "bar") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = bar_ino; + }))); + EXPECT_LOOKUP(FUSE_ROOT_ID, "baz") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = baz_ino; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == foo_ino); + }, Eq(true)), + _) + ) + .WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) { + nready0 = m_mock->m_nready; + + sem_post(&sem0); + // Block the daemon so we can accumulate a few more ops + sem_wait(&sem1); + + out.header.unique = in.header.unique; + out.header.error = -EIO; + out.header.len = sizeof(out.header); + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + (in.header.nodeid == bar_ino || + in.header.nodeid == baz_ino)); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) { + nready1 = m_mock->m_nready; + out.header.unique = in.header.unique; + out.header.error = -EIO; + out.header.len = sizeof(out.header); + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + (in.header.nodeid == bar_ino || + in.header.nodeid == baz_ino)); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) { + nready2 = m_mock->m_nready; + out.header.unique = in.header.unique; + out.header.error = -EIO; + out.header.len = sizeof(out.header); + }))); + + /* + * Create cached lookup entries for these files. It seems that only + * one thread at a time can be in VOP_LOOKUP for a given directory + */ + access("mountpoint/foo", F_OK); + access("mountpoint/bar", F_OK); + access("mountpoint/baz", F_OK); + ASSERT_EQ(0, pthread_create(&th0, NULL, statter, + __DECONST(void*, "mountpoint/foo"))) << strerror(errno); + EXPECT_EQ(0, sem_wait(&sem0)) << strerror(errno); + ASSERT_EQ(0, pthread_create(&th1, NULL, statter, + __DECONST(void*, "mountpoint/bar"))) << strerror(errno); + ASSERT_EQ(0, pthread_create(&th2, NULL, statter, + __DECONST(void*, "mountpoint/baz"))) << strerror(errno); + + nap(); // Allow th1 and th2 to send their ops to the daemon + EXPECT_EQ(0, sem_post(&sem1)) << strerror(errno); + + pthread_join(th0, &th_ret); + ASSERT_EQ(-1, (intptr_t)th_ret); + pthread_join(th1, &th_ret); + ASSERT_EQ(-1, (intptr_t)th_ret); + pthread_join(th2, &th_ret); + ASSERT_EQ(-1, (intptr_t)th_ret); + + EXPECT_EQ(1, nready0); + EXPECT_EQ(2, nready1); + EXPECT_EQ(1, nready2); + + sem_destroy(&sem0); + sem_destroy(&sem1); +} diff --git a/tests/sys/fs/fusefs/fallocate.cc b/tests/sys/fs/fusefs/fallocate.cc new file mode 100644 index 000000000000..4e5b047b78b7 --- /dev/null +++ b/tests/sys/fs/fusefs/fallocate.cc @@ -0,0 +1,779 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2021 Alan Somers + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> +#include <sys/mount.h> +#include <sys/resource.h> +#include <sys/time.h> + +#include <fcntl.h> +#include <mntopts.h> // for build_iovec +#include <signal.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +/* Is buf all zero? */ +static bool +is_zero(const char *buf, uint64_t size) +{ + return buf[0] == 0 && !memcmp(buf, buf + 1, size - 1); +} + +class Fallocate: public FuseTest { +public: +/* + * expect VOP_DEALLOCATE to be implemented by vop_stddeallocate. + */ +void expect_vop_stddeallocate(uint64_t ino, uint64_t off, uint64_t length) +{ + /* XXX read offset and size may depend on cache mode */ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.offset <= off && + in.body.read.offset + in.body.read.size >= + off + length); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { + assert(in.body.read.size <= sizeof(out.body.bytes)); + out.header.len = sizeof(struct fuse_out_header) + + in.body.read.size; + memset(out.body.bytes, 'X', in.body.read.size); + }))).RetiresOnSaturation(); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *buf = (const char*)in.body.bytes + + sizeof(struct fuse_write_in); + + assert(length <= sizeof(in.body.bytes) - + sizeof(struct fuse_write_in)); + return (in.header.opcode == FUSE_WRITE && + in.header.nodeid == ino && + in.body.write.offset == off && + in.body.write.size == length && + is_zero(buf, length)); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = length; + }))); +} +}; + +class Fspacectl: public Fallocate {}; + +class Fspacectl_7_18: public Fspacectl { +public: +virtual void SetUp() { + m_kernel_minor_version = 18; + Fspacectl::SetUp(); +} +}; + +class FspacectlCache: public Fspacectl, public WithParamInterface<cache_mode> { +public: +bool m_direct_io; + +FspacectlCache(): m_direct_io(false) {}; + +virtual void SetUp() { + int cache_mode = GetParam(); + switch (cache_mode) { + case Uncached: + m_direct_io = true; + break; + case WritebackAsync: + m_async = true; + /* FALLTHROUGH */ + case Writeback: + m_init_flags |= FUSE_WRITEBACK_CACHE; + /* FALLTHROUGH */ + case Writethrough: + break; + default: + FAIL() << "Unknown cache mode"; + } + + FuseTest::SetUp(); + if (IsSkipped()) + return; +} +}; + +class PosixFallocate: public Fallocate { +public: +static sig_atomic_t s_sigxfsz; + +void SetUp() { + s_sigxfsz = 0; + FuseTest::SetUp(); +} + +void TearDown() { + struct sigaction sa; + + bzero(&sa, sizeof(sa)); + sa.sa_handler = SIG_DFL; + sigaction(SIGXFSZ, &sa, NULL); + + Fallocate::TearDown(); +} + +}; + +sig_atomic_t PosixFallocate::s_sigxfsz = 0; + +void sigxfsz_handler(int __unused sig) { + PosixFallocate::s_sigxfsz = 1; +} + +class PosixFallocate_7_18: public PosixFallocate { +public: +virtual void SetUp() { + m_kernel_minor_version = 18; + PosixFallocate::SetUp(); +} +}; + + +/* + * If the server returns ENOSYS, it indicates that the server does not support + * FUSE_FALLOCATE. This and future calls should fall back to vop_stddeallocate. + */ +TEST_F(Fspacectl, enosys) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + off_t fsize = 1 << 20; + off_t off0 = 100; + off_t len0 = 500; + struct spacectl_range rqsr = { .r_offset = off0, .r_len = len0 }; + uint64_t ino = 42; + uint64_t off1 = fsize; + uint64_t len1 = 1000; + off_t off2 = fsize / 2; + off_t len2 = 500; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + expect_fallocate(ino, off0, len0, + FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, ENOSYS); + expect_vop_stddeallocate(ino, off0, len0); + expect_vop_stddeallocate(ino, off2, len2); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); + + /* Subsequent calls shouldn't query the daemon either */ + rqsr.r_offset = off2; + rqsr.r_len = len2; + EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); + + /* Neither should posix_fallocate query the daemon */ + EXPECT_EQ(EINVAL, posix_fallocate(fd, off1, len1)); + + leak(fd); +} + +/* + * EOPNOTSUPP means "the file system does not support fallocate with the + * supplied mode on this particular file". So we should fallback, but not + * assume anything about whether the operation will fail on a different file or + * with a different mode. + */ +TEST_F(Fspacectl, eopnotsupp) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct spacectl_range rqsr; + uint64_t ino = 42; + uint64_t fsize = 1 << 20; + uint64_t off0 = 500; + uint64_t len = 1000; + uint64_t off1 = fsize / 2; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + expect_fallocate(ino, off0, len, + FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, + EOPNOTSUPP); + expect_vop_stddeallocate(ino, off0, len); + expect_fallocate(ino, off1, len, + FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, + EOPNOTSUPP); + expect_vop_stddeallocate(ino, off1, len); + expect_fallocate(ino, fsize, len, 0, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + /* + * Though the FUSE daemon will reject the call, the kernel should fall + * back to a read-modify-write approach. + */ + rqsr.r_offset = off0; + rqsr.r_len = len; + EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); + + /* Subsequent calls should still query the daemon */ + rqsr.r_offset = off1; + rqsr.r_len = len; + EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); + + /* But subsequent posix_fallocate calls _should_ query the daemon */ + EXPECT_EQ(0, posix_fallocate(fd, fsize, len)); + + leak(fd); +} + +TEST_F(Fspacectl, erofs) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct statfs statbuf; + uint64_t fsize = 2000; + struct spacectl_range rqsr = { .r_offset = 0, .r_len = 1 }; + struct iovec *iov = NULL; + int iovlen = 0; + uint64_t ino = 42; + int fd; + int newflags; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_STATFS); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) + { + /* + * All of the fields except f_flags are don't care, and f_flags + * is set by the VFS + */ + SET_OUT_HEADER_LEN(out, statfs); + }))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + /* Remount read-only */ + ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); + newflags = statbuf.f_flags | MNT_UPDATE | MNT_RDONLY; + build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1); + build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1); + build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1); + ASSERT_EQ(0, nmount(iov, iovlen, newflags)) << strerror(errno); + free_iovec(&iov, &iovlen); + + EXPECT_EQ(-1, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); + EXPECT_EQ(EROFS, errno); + + leak(fd); +} + +/* + * If FUSE_GETATTR fails when determining the size of the file, fspacectl + * should fail gracefully. This failure mode is easiest to trigger when + * attribute caching is disabled. + */ +TEST_F(Fspacectl, getattr_fails) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + Sequence seq; + struct spacectl_range rqsr; + const uint64_t ino = 42; + const uint64_t fsize = 2000; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1, 0); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(1) + .InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr.size = fsize; + out.body.attr.attr_valid = 0; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(ReturnErrno(EIO)); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + rqsr.r_offset = 500; + rqsr.r_len = 1000; + EXPECT_EQ(-1, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); + EXPECT_EQ(EIO, errno); + + leak(fd); +} + +TEST_F(Fspacectl, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct spacectl_range rqsr, rmsr; + struct stat sb0, sb1; + uint64_t ino = 42; + uint64_t fsize = 2000; + uint64_t offset = 500; + uint64_t length = 1000; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + expect_fallocate(ino, offset, length, + FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb0)) << strerror(errno); + rqsr.r_offset = offset; + rqsr.r_len = length; + EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr)); + EXPECT_EQ(0, rmsr.r_len); + EXPECT_EQ((off_t)(offset + length), rmsr.r_offset); + + /* + * The file's attributes should not have been invalidated, so this fstat + * will not requery the daemon. + */ + EXPECT_EQ(0, fstat(fd, &sb1)); + EXPECT_EQ(fsize, (uint64_t)sb1.st_size); + + /* mtime and ctime should be updated */ + EXPECT_EQ(sb0.st_atime, sb1.st_atime); + EXPECT_NE(sb0.st_mtime, sb1.st_mtime); + EXPECT_NE(sb0.st_ctime, sb1.st_ctime); + + leak(fd); +} + +/* The returned rqsr.r_off should be clipped at EoF */ +TEST_F(Fspacectl, past_eof) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct spacectl_range rqsr, rmsr; + uint64_t ino = 42; + uint64_t fsize = 1000; + uint64_t offset = 1500; + uint64_t length = 1000; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + expect_fallocate(ino, offset, length, + FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + rqsr.r_offset = offset; + rqsr.r_len = length; + EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr)); + EXPECT_EQ(0, rmsr.r_len); + EXPECT_EQ((off_t)fsize, rmsr.r_offset); + + leak(fd); +} + +/* The returned rqsr.r_off should be clipped at EoF */ +TEST_F(Fspacectl, spans_eof) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct spacectl_range rqsr, rmsr; + uint64_t ino = 42; + uint64_t fsize = 1000; + uint64_t offset = 500; + uint64_t length = 1000; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + expect_fallocate(ino, offset, length, + FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + rqsr.r_offset = offset; + rqsr.r_len = length; + EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr)); + EXPECT_EQ(0, rmsr.r_len); + EXPECT_EQ((off_t)fsize, rmsr.r_offset); + + leak(fd); +} + +/* + * With older servers, no FUSE_FALLOCATE should be attempted. The kernel + * should fall back to vop_stddeallocate. + */ +TEST_F(Fspacectl_7_18, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct spacectl_range rqsr, rmsr; + char *buf; + uint64_t ino = 42; + uint64_t fsize = 2000; + uint64_t offset = 500; + uint64_t length = 1000; + int fd; + + buf = new char[length]; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + expect_vop_stddeallocate(ino, offset, length); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + rqsr.r_offset = offset; + rqsr.r_len = length; + EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr)); + EXPECT_EQ(0, rmsr.r_len); + EXPECT_EQ((off_t)(offset + length), rmsr.r_offset); + + leak(fd); + delete[] buf; +} + +/* + * A successful fspacectl should clear the zeroed data from the kernel cache. + */ +TEST_P(FspacectlCache, clears_cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefghijklmnopqrstuvwxyz"; + struct spacectl_range rqsr, rmsr; + uint64_t ino = 42; + ssize_t bufsize = strlen(CONTENTS); + uint64_t fsize = bufsize; + uint8_t buf[bufsize]; + char zbuf[bufsize]; + uint64_t offset = 0; + uint64_t length = bufsize; + int fd; + + bzero(zbuf, bufsize); + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + /* NB: expectations are applied in LIFO order */ + expect_read(ino, 0, fsize, fsize, zbuf); + expect_read(ino, 0, fsize, fsize, CONTENTS); + expect_fallocate(ino, offset, length, + FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + /* Populate the cache */ + ASSERT_EQ(fsize, (uint64_t)pread(fd, buf, bufsize, 0)) + << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, fsize)); + + /* Zero the file */ + rqsr.r_offset = offset; + rqsr.r_len = length; + EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr)); + EXPECT_EQ(0, rmsr.r_len); + EXPECT_EQ((off_t)(offset + length), rmsr.r_offset); + + /* Read again. This should query the daemon */ + ASSERT_EQ(fsize, (uint64_t)pread(fd, buf, bufsize, 0)) + << strerror(errno); + ASSERT_EQ(0, memcmp(buf, zbuf, fsize)); + + leak(fd); +} + +INSTANTIATE_TEST_SUITE_P(FspacectlCache, FspacectlCache, + Values(Uncached, Writethrough, Writeback) +); + +/* + * If the server returns ENOSYS, it indicates that the server does not support + * FUSE_FALLOCATE. This and future calls should return EINVAL. + */ +TEST_F(PosixFallocate, enosys) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + uint64_t off0 = 0; + uint64_t len0 = 1000; + off_t off1 = 100; + off_t len1 = 200; + uint64_t fsize = 500; + struct spacectl_range rqsr = { .r_offset = off1, .r_len = len1 }; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + expect_fallocate(ino, off0, len0, 0, ENOSYS); + expect_vop_stddeallocate(ino, off1, len1); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(EINVAL, posix_fallocate(fd, off0, len0)); + + /* Subsequent calls shouldn't query the daemon*/ + EXPECT_EQ(EINVAL, posix_fallocate(fd, off0, len0)); + + /* Neither should VOP_DEALLOCATE query the daemon */ + EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); + + leak(fd); +} + +/* + * EOPNOTSUPP means "the file system does not support fallocate with the + * supplied mode on this particular file". So we should fallback, but not + * assume anything about whether the operation will fail on a different file or + * with a different mode. + */ +TEST_F(PosixFallocate, eopnotsupp) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct spacectl_range rqsr; + uint64_t ino = 42; + uint64_t fsize = 2000; + uint64_t offset = 0; + uint64_t length = 1000; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + expect_fallocate(ino, fsize, length, 0, EOPNOTSUPP); + expect_fallocate(ino, offset, length, 0, EOPNOTSUPP); + expect_fallocate(ino, offset, length, + FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(EINVAL, posix_fallocate(fd, fsize, length)); + + /* Subsequent calls should still query the daemon*/ + EXPECT_EQ(EINVAL, posix_fallocate(fd, offset, length)); + + /* And subsequent VOP_DEALLOCATE calls should also query the daemon */ + rqsr.r_len = length; + rqsr.r_offset = offset; + EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); + + leak(fd); +} + +/* EIO is not a permanent error, and may be retried */ +TEST_F(PosixFallocate, eio) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + uint64_t offset = 0; + uint64_t length = 1000; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_open(ino, 0, 1); + expect_fallocate(ino, offset, length, 0, EIO); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(EIO, posix_fallocate(fd, offset, length)); + + expect_fallocate(ino, offset, length, 0, 0); + + EXPECT_EQ(0, posix_fallocate(fd, offset, length)); + + leak(fd); +} + +TEST_F(PosixFallocate, erofs) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct statfs statbuf; + struct iovec *iov = NULL; + int iovlen = 0; + uint64_t ino = 42; + uint64_t offset = 0; + uint64_t length = 1000; + int fd; + int newflags; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_STATFS); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) + { + /* + * All of the fields except f_flags are don't care, and f_flags + * is set by the VFS + */ + SET_OUT_HEADER_LEN(out, statfs); + }))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + /* Remount read-only */ + ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); + newflags = statbuf.f_flags | MNT_UPDATE | MNT_RDONLY; + build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1); + build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1); + build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1); + ASSERT_EQ(0, nmount(iov, iovlen, newflags)) << strerror(errno); + free_iovec(&iov, &iovlen); + + EXPECT_EQ(EROFS, posix_fallocate(fd, offset, length)); + + leak(fd); +} + +TEST_F(PosixFallocate, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct stat sb0, sb1; + uint64_t ino = 42; + uint64_t offset = 0; + uint64_t length = 1000; + int fd; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr_valid = UINT64_MAX; + }))); + expect_open(ino, 0, 1); + expect_fallocate(ino, offset, length, 0, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb0)) << strerror(errno); + EXPECT_EQ(0, posix_fallocate(fd, offset, length)); + /* + * Despite the originally cached file size of zero, stat should now + * return either the new size or requery the daemon. + */ + EXPECT_EQ(0, stat(FULLPATH, &sb1)); + EXPECT_EQ(length, (uint64_t)sb1.st_size); + + /* mtime and ctime should be updated */ + EXPECT_EQ(sb0.st_atime, sb1.st_atime); + EXPECT_NE(sb0.st_mtime, sb1.st_mtime); + EXPECT_NE(sb0.st_ctime, sb1.st_ctime); + + leak(fd); +} + +/* fusefs should respect RLIMIT_FSIZE */ +TEST_F(PosixFallocate, rlimit_fsize) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct rlimit rl; + uint64_t ino = 42; + uint64_t offset = 0; + uint64_t length = 1'000'000; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_open(ino, 0, 1); + + rl.rlim_cur = length / 2; + rl.rlim_max = 10 * length; + ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); + ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(EFBIG, posix_fallocate(fd, offset, length)); + EXPECT_EQ(1, s_sigxfsz); + + leak(fd); +} + +/* With older servers, no FUSE_FALLOCATE should be attempted */ +TEST_F(PosixFallocate_7_18, einval) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + uint64_t offset = 0; + uint64_t length = 1000; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_open(ino, 0, 1); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(EINVAL, posix_fallocate(fd, offset, length)); + + leak(fd); +} diff --git a/tests/sys/fs/fusefs/fifo.cc b/tests/sys/fs/fusefs/fifo.cc new file mode 100644 index 000000000000..3ec21f3f779b --- /dev/null +++ b/tests/sys/fs/fusefs/fifo.cc @@ -0,0 +1,211 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <fcntl.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +const char FULLPATH[] = "mountpoint/some_fifo"; +const char RELPATH[] = "some_fifo"; +const char MESSAGE[] = "Hello, World!\n"; +const int msgsize = sizeof(MESSAGE); + +class Fifo: public FuseTest { +public: +pthread_t m_child; + +Fifo(): m_child(NULL) {}; + +void TearDown() { + if (m_child != NULL) { + pthread_join(m_child, NULL); + } + FuseTest::TearDown(); +} +}; + +class Socket: public Fifo {}; + +/* Writer thread */ +static void* writer(void* arg) { + ssize_t sent = 0; + int fd; + + fd = *(int*)arg; + while (sent < msgsize) { + ssize_t r; + + r = write(fd, MESSAGE + sent, msgsize - sent); + if (r < 0) + return (void*)(intptr_t)errno; + else + sent += r; + + } + return 0; +} + +/* + * Reading and writing FIFOs works. None of the I/O actually goes through FUSE + */ +TEST_F(Fifo, read_write) +{ + mode_t mode = S_IFIFO | 0755; + const int bufsize = 80; + char message[bufsize]; + ssize_t recvd = 0, r; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino, mode, 0, 1); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, pthread_create(&m_child, NULL, writer, &fd)) + << strerror(errno); + while (recvd < msgsize) { + r = read(fd, message + recvd, bufsize - recvd); + ASSERT_LE(0, r) << strerror(errno); + ASSERT_LT(0, r) << "unexpected EOF"; + recvd += r; + } + ASSERT_STREQ(message, MESSAGE); + + leak(fd); +} + +/* Writer thread */ +static void* socket_writer(void* arg __unused) { + ssize_t sent = 0; + int fd, err; + struct sockaddr_un sa; + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { + perror("socket"); + return (void*)(intptr_t)errno; + } + sa.sun_family = AF_UNIX; + strlcpy(sa.sun_path, FULLPATH, sizeof(sa.sun_path)); + sa.sun_len = sizeof(FULLPATH); + err = connect(fd, (struct sockaddr*)&sa, sizeof(sa)); + if (err < 0) { + perror("connect"); + return (void*)(intptr_t)errno; + } + + while (sent < msgsize) { + ssize_t r; + + r = write(fd, MESSAGE + sent, msgsize - sent); + if (r < 0) + return (void*)(intptr_t)errno; + else + sent += r; + + } + + FuseTest::leak(fd); + return 0; +} + +/* + * Reading and writing unix-domain sockets works. None of the I/O actually + * goes through FUSE. + */ +TEST_F(Socket, read_write) +{ + mode_t mode = S_IFSOCK | 0755; + const int bufsize = 80; + char message[bufsize]; + struct sockaddr_un sa; + ssize_t recvd = 0, r; + uint64_t ino = 42; + int fd, connected; + Sequence seq; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_MKNOD); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr_valid = UINT64_MAX; + }))); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, fd) << strerror(errno); + sa.sun_family = AF_UNIX; + strlcpy(sa.sun_path, FULLPATH, sizeof(sa.sun_path)); + sa.sun_len = sizeof(FULLPATH); + ASSERT_EQ(0, bind(fd, (struct sockaddr*)&sa, sizeof(sa))) + << strerror(errno); + listen(fd, 5); + ASSERT_EQ(0, pthread_create(&m_child, NULL, socket_writer, NULL)) + << strerror(errno); + connected = accept(fd, 0, 0); + ASSERT_LE(0, connected) << strerror(errno); + + while (recvd < msgsize) { + r = read(connected, message + recvd, bufsize - recvd); + ASSERT_LE(0, r) << strerror(errno); + ASSERT_LT(0, r) << "unexpected EOF"; + recvd += r; + } + ASSERT_STREQ(message, MESSAGE); + + leak(fd); +} diff --git a/tests/sys/fs/fusefs/flush.cc b/tests/sys/fs/fusefs/flush.cc new file mode 100644 index 000000000000..7ba1218b3287 --- /dev/null +++ b/tests/sys/fs/fusefs/flush.cc @@ -0,0 +1,272 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <fcntl.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Flush: public FuseTest { + +public: +void +expect_flush(uint64_t ino, int times, pid_t lo, ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FLUSH && + in.header.nodeid == ino && + in.body.flush.lock_owner == (uint64_t)lo && + in.body.flush.fh == FH); + }, Eq(true)), + _) + ).Times(times) + .WillRepeatedly(Invoke(r)); +} + +void expect_lookup(const char *relpath, uint64_t ino, int times) +{ + FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, times); +} + +/* + * When testing FUSE_FLUSH, the FUSE_RELEASE calls are uninteresting. This + * expectation will silence googlemock warnings + */ +void expect_release() +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_RELEASE); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnErrno(0))); +} +}; + +class FlushWithLocks: public Flush { + virtual void SetUp() { + m_init_flags = FUSE_POSIX_LOCKS; + Flush::SetUp(); + } +}; + +/* + * If multiple file descriptors refer to the same file handle, closing each + * should send FUSE_FLUSH + */ +TEST_F(Flush, open_twice) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd, fd2; + + expect_lookup(RELPATH, ino, 2); + expect_open(ino, 0, 1); + expect_flush(ino, 2, getpid(), ReturnErrno(0)); + expect_release(); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + fd2 = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd2) << strerror(errno); + + EXPECT_EQ(0, close(fd2)) << strerror(errno); + EXPECT_EQ(0, close(fd)) << strerror(errno); +} + +/** + * Test for FOPEN_NOFLUSH: we expect that zero flush calls will be performed. + */ +TEST_F(Flush, open_noflush) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + uint64_t pid = (uint64_t)getpid(); + int fd; + + expect_lookup(RELPATH, ino, 1); + expect_open(ino, FOPEN_NOFLUSH, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FLUSH && + in.header.nodeid == ino && + in.body.flush.lock_owner == pid && + in.body.flush.fh == FH); + }, Eq(true)), + _) + ).Times(0); + expect_release(); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + // close MUST not flush + EXPECT_EQ(0, close(fd)) << strerror(errno); +} + +/* + * Some FUSE filesystem cache data internally and flush it on release. Such + * filesystems may generate errors during release. On Linux, these get + * returned by close(2). However, POSIX does not require close(2) to return + * this error. FreeBSD's fuse(4) should return EIO if it returns an error at + * all. + */ +/* http://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html */ +TEST_F(Flush, eio) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino, 1); + expect_open(ino, 0, 1); + expect_flush(ino, 1, getpid(), ReturnErrno(EIO)); + expect_release(); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_TRUE(0 == close(fd) || errno == EIO) << strerror(errno); +} + +/* + * If the filesystem returns ENOSYS, it will be treated as success and + * no more FUSE_FLUSH operations will be sent to the daemon + */ +TEST_F(Flush, enosys) +{ + const char FULLPATH0[] = "mountpoint/some_file.txt"; + const char RELPATH0[] = "some_file.txt"; + const char FULLPATH1[] = "mountpoint/other_file.txt"; + const char RELPATH1[] = "other_file.txt"; + uint64_t ino0 = 42; + uint64_t ino1 = 43; + int fd0, fd1; + + expect_lookup(RELPATH0, ino0, 1); + expect_open(ino0, 0, 1); + /* On the 2nd close, FUSE_FLUSH won't be sent at all */ + expect_flush(ino0, 1, getpid(), ReturnErrno(ENOSYS)); + expect_release(); + + expect_lookup(RELPATH1, ino1, 1); + expect_open(ino1, 0, 1); + /* On the 2nd close, FUSE_FLUSH won't be sent at all */ + expect_release(); + + fd0 = open(FULLPATH0, O_WRONLY); + ASSERT_LE(0, fd0) << strerror(errno); + + fd1 = open(FULLPATH1, O_WRONLY); + ASSERT_LE(0, fd1) << strerror(errno); + + EXPECT_EQ(0, close(fd0)) << strerror(errno); + EXPECT_EQ(0, close(fd1)) << strerror(errno); +} + +/* A FUSE_FLUSH should be sent on close(2) */ +TEST_F(Flush, flush) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino, 1); + expect_open(ino, 0, 1); + expect_flush(ino, 1, getpid(), ReturnErrno(0)); + expect_release(); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_TRUE(0 == close(fd)) << strerror(errno); +} + +/* + * When closing a file with a POSIX file lock, flush should release the lock, + * _even_if_ it's not the process's last file descriptor for this file. + */ +TEST_F(FlushWithLocks, unlock_on_close) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd, fd2; + struct flock fl; + pid_t pid = getpid(); + + expect_lookup(RELPATH, ino, 2); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETLK && + in.header.nodeid == ino && + in.body.setlk.lk.type == F_RDLCK && + in.body.setlk.fh == FH); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(0))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETLK && + in.header.nodeid == ino && + in.body.setlk.lk.type == F_UNLCK && + in.body.setlk.fh == FH); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(0))); + expect_flush(ino, 1, pid, ReturnErrno(0)); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + fl.l_start = 0; + fl.l_len = 0; + fl.l_pid = pid; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_SET; + fl.l_sysid = 0; + ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno); + + fd2 = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd2) << strerror(errno); + ASSERT_EQ(0, close(fd2)) << strerror(errno); + leak(fd); + leak(fd2); +} diff --git a/tests/sys/fs/fusefs/forget.cc b/tests/sys/fs/fusefs/forget.cc new file mode 100644 index 000000000000..1e7764ac4782 --- /dev/null +++ b/tests/sys/fs/fusefs/forget.cc @@ -0,0 +1,177 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/types.h> +#include <sys/mount.h> + +#include <fcntl.h> +#include <semaphore.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Forget: public FuseTest { +public: +void SetUp() { + if (geteuid() != 0) + GTEST_SKIP() << "Only root may use " << reclaim_mib; + + FuseTest::SetUp(); +} + +}; + +/* + * When a fusefs vnode is reclaimed, it should send a FUSE_FORGET operation. + */ +TEST_F(Forget, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + mode_t mode = S_IFREG | 0755; + sem_t sem; + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .Times(3) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = UINT64_MAX; + }))); + expect_forget(ino, 3, &sem); + + /* + * access(2) the file to force a lookup. Access it twice to double its + * lookup count. + */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + + reclaim_vnode(FULLPATH); + + sem_wait(&sem); + sem_destroy(&sem); +} + +/* + * When a directory is reclaimed, the names of its entries vanish from the + * namecache + */ +TEST_F(Forget, invalidate_names) +{ + const char FULLFPATH[] = "mountpoint/some_dir/some_file.txt"; + const char FULLDPATH[] = "mountpoint/some_dir"; + const char DNAME[] = "some_dir"; + const char FNAME[] = "some_file.txt"; + uint64_t dir_ino = 42; + uint64_t file_ino = 43; + + EXPECT_LOOKUP(FUSE_ROOT_ID, DNAME) + .Times(2) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = dir_ino; + out.body.entry.attr.nlink = 2; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + /* + * Even though we don't reclaim FNAME and its entry is cacheable, we + * should get two lookups because the reclaim of DNAME will invalidate + * the cached FNAME entry. + */ + EXPECT_LOOKUP(dir_ino, FNAME) + .Times(2) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = file_ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + expect_forget(dir_ino, 1); + + /* Access the file to cache its name */ + ASSERT_EQ(0, access(FULLFPATH, F_OK)) << strerror(errno); + + /* Reclaim the directory, invalidating its children from namecache */ + reclaim_vnode(FULLDPATH); + + /* Access the file again, causing another lookup */ + ASSERT_EQ(0, access(FULLFPATH, F_OK)) << strerror(errno); +} + +/* + * Reclaiming the root inode should not send a FUSE_FORGET request, nor should + * it interfere with further lookup operations. + */ +TEST_F(Forget, root) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + mode_t mode = S_IFREG | 0755; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + /* access(2) the file to force a lookup. */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + + reclaim_vnode("mountpoint"); + nap(); + + /* Access it again, to make sure it's still possible. */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); +} diff --git a/tests/sys/fs/fusefs/fsync.cc b/tests/sys/fs/fusefs/fsync.cc new file mode 100644 index 000000000000..d6f4e6f70da0 --- /dev/null +++ b/tests/sys/fs/fusefs/fsync.cc @@ -0,0 +1,285 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <aio.h> +#include <fcntl.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +/* + * TODO: remove FUSE_FSYNC_FDATASYNC definition when upgrading to protocol 7.28. + * This bit was actually part of kernel protocol version 5.2, but never + * documented until after 7.28 + */ +#ifndef FUSE_FSYNC_FDATASYNC +#define FUSE_FSYNC_FDATASYNC 1 +#endif + +class Fsync: public FuseTest { +public: +void expect_fsync(uint64_t ino, uint32_t flags, int error, int times = 1) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FSYNC && + in.header.nodeid == ino && + /* + * TODO: reenable pid check after fixing + * bug 236379 + */ + //(pid_t)in.header.pid == getpid() && + in.body.fsync.fh == FH && + in.body.fsync.fsync_flags == flags); + }, Eq(true)), + _) + ).Times(times) + .WillRepeatedly(Invoke(ReturnErrno(error))); +} + +void expect_lookup(const char *relpath, uint64_t ino, int times = 1) +{ + FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, times); +} + +void expect_write(uint64_t ino, uint64_t size, const void *contents) +{ + FuseTest::expect_write(ino, 0, size, size, 0, 0, contents); +} + +}; + +class AioFsync: public Fsync { +virtual void SetUp() { + if (!is_unsafe_aio_enabled()) + GTEST_SKIP() << + "vfs.aio.enable_unsafe must be set for this test"; + FuseTest::SetUp(); +} +}; + +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */ +TEST_F(AioFsync, aio_fsync) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + ssize_t bufsize = strlen(CONTENTS); + uint64_t ino = 42; + struct aiocb iocb, *piocb; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_write(ino, bufsize, CONTENTS); + expect_fsync(ino, 0, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + + bzero(&iocb, sizeof(iocb)); + iocb.aio_fildes = fd; + + ASSERT_EQ(0, aio_fsync(O_SYNC, &iocb)) << strerror(errno); + ASSERT_EQ(0, aio_waitcomplete(&piocb, NULL)) << strerror(errno); + + leak(fd); +} + +/* + * fuse(4) should NOT fsync during VOP_RELEASE or VOP_INACTIVE + * + * This test only really make sense in writeback caching mode, but it should + * still pass in any cache mode. + */ +TEST_F(Fsync, close) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + ssize_t bufsize = strlen(CONTENTS); + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_write(ino, bufsize, CONTENTS); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETATTR); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FSYNC); + }, Eq(true)), + _) + ).Times(0); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, FH); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + close(fd); +} + +TEST_F(Fsync, eio) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + ssize_t bufsize = strlen(CONTENTS); + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_write(ino, bufsize, CONTENTS); + expect_fsync(ino, FUSE_FSYNC_FDATASYNC, EIO); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + ASSERT_NE(0, fdatasync(fd)); + ASSERT_EQ(EIO, errno); + + leak(fd); +} + +/* + * If the filesystem returns ENOSYS, it will be treated as success and + * subsequent calls to VOP_FSYNC will succeed automatically without being sent + * to the filesystem daemon + */ +TEST_F(Fsync, enosys) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + ssize_t bufsize = strlen(CONTENTS); + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_write(ino, bufsize, CONTENTS); + expect_fsync(ino, FUSE_FSYNC_FDATASYNC, ENOSYS); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + EXPECT_EQ(0, fdatasync(fd)); + + /* Subsequent calls shouldn't query the daemon*/ + EXPECT_EQ(0, fdatasync(fd)); + leak(fd); +} + + +TEST_F(Fsync, fdatasync) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + ssize_t bufsize = strlen(CONTENTS); + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_write(ino, bufsize, CONTENTS); + expect_fsync(ino, FUSE_FSYNC_FDATASYNC, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + ASSERT_EQ(0, fdatasync(fd)) << strerror(errno); + + leak(fd); +} + +TEST_F(Fsync, fsync) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + ssize_t bufsize = strlen(CONTENTS); + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_write(ino, bufsize, CONTENTS); + expect_fsync(ino, 0, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + ASSERT_EQ(0, fsync(fd)) << strerror(errno); + + leak(fd); +} + +/* If multiple FUSE file handles are active, we must fsync them all */ +TEST_F(Fsync, two_handles) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + ssize_t bufsize = strlen(CONTENTS); + uint64_t ino = 42; + int fd1, fd2; + + expect_lookup(RELPATH, ino, 2); + expect_open(ino, 0, 2); + expect_write(ino, bufsize, CONTENTS); + expect_fsync(ino, 0, 0, 2); + + fd1 = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd1) << strerror(errno); + fd2 = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd2) << strerror(errno); + ASSERT_EQ(bufsize, write(fd1, CONTENTS, bufsize)) << strerror(errno); + ASSERT_EQ(0, fsync(fd1)) << strerror(errno); + + leak(fd1); + leak(fd2); +} diff --git a/tests/sys/fs/fusefs/fsyncdir.cc b/tests/sys/fs/fusefs/fsyncdir.cc new file mode 100644 index 000000000000..aef93a0fb1d5 --- /dev/null +++ b/tests/sys/fs/fusefs/fsyncdir.cc @@ -0,0 +1,195 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <aio.h> +#include <fcntl.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +/* + * TODO: remove FUSE_FSYNC_FDATASYNC definition when upgrading to protocol 7.28. + * This bit was actually part of kernel protocol version 5.2, but never + * documented until after 7.28 + */ +#ifndef FUSE_FSYNC_FDATASYNC +#define FUSE_FSYNC_FDATASYNC 1 +#endif + +class FsyncDir: public FuseTest { +public: +void expect_fsyncdir(uint64_t ino, uint32_t flags, int error) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FSYNCDIR && + in.header.nodeid == ino && + /* + * TODO: reenable pid check after fixing + * bug 236379 + */ + //(pid_t)in.header.pid == getpid() && + in.body.fsyncdir.fh == FH && + in.body.fsyncdir.fsync_flags == flags); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(error))); +} + +void expect_lookup(const char *relpath, uint64_t ino) +{ + FuseTest::expect_lookup(relpath, ino, S_IFDIR | 0755, 0, 1); +} + +}; + +class AioFsyncDir: public FsyncDir { +virtual void SetUp() { + if (!is_unsafe_aio_enabled()) + GTEST_SKIP() << + "vfs.aio.enable_unsafe must be set for this test"; + FuseTest::SetUp(); +} +}; + +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */ +TEST_F(AioFsyncDir, aio_fsync) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + struct aiocb iocb, *piocb; + int fd; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + expect_fsyncdir(ino, 0, 0); + + fd = open(FULLPATH, O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + + bzero(&iocb, sizeof(iocb)); + iocb.aio_fildes = fd; + + ASSERT_EQ(0, aio_fsync(O_SYNC, &iocb)) << strerror(errno); + ASSERT_EQ(0, aio_waitcomplete(&piocb, NULL)) << strerror(errno); + + leak(fd); +} + +TEST_F(FsyncDir, eio) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + expect_fsyncdir(ino, 0, EIO); + + fd = open(FULLPATH, O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_NE(0, fsync(fd)); + ASSERT_EQ(EIO, errno); + + leak(fd); +} + +/* + * If the filesystem returns ENOSYS, it will be treated as success and + * subsequent calls to VOP_FSYNC will succeed automatically without being sent + * to the filesystem daemon + */ +TEST_F(FsyncDir, enosys) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + expect_fsyncdir(ino, 0, ENOSYS); + + fd = open(FULLPATH, O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(0, fsync(fd)) << strerror(errno); + + /* Subsequent calls shouldn't query the daemon*/ + EXPECT_EQ(0, fsync(fd)) << strerror(errno); + + leak(fd); +} + +TEST_F(FsyncDir, fsyncdata) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + expect_fsyncdir(ino, FUSE_FSYNC_FDATASYNC, 0); + + fd = open(FULLPATH, O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, fdatasync(fd)) << strerror(errno); + + leak(fd); +} + +/* + * Unlike regular files, the kernel doesn't know whether a directory is or + * isn't dirty, so fuse(4) should always send FUSE_FSYNCDIR on fsync(2) + */ +TEST_F(FsyncDir, fsync) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + expect_fsyncdir(ino, 0, 0); + + fd = open(FULLPATH, O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, fsync(fd)) << strerror(errno); + + leak(fd); +} diff --git a/tests/sys/fs/fusefs/getattr.cc b/tests/sys/fs/fusefs/getattr.cc new file mode 100644 index 000000000000..98a757fdff94 --- /dev/null +++ b/tests/sys/fs/fusefs/getattr.cc @@ -0,0 +1,367 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> + +#include <semaphore.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Getattr : public FuseTest { +public: +void expect_lookup(const char *relpath, uint64_t ino, mode_t mode, + uint64_t size, int times, uint64_t attr_valid, uint32_t attr_valid_nsec) +{ + EXPECT_LOOKUP(FUSE_ROOT_ID, relpath) + .Times(times) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = attr_valid; + out.body.entry.attr_valid_nsec = attr_valid_nsec; + out.body.entry.attr.size = size; + out.body.entry.entry_valid = UINT64_MAX; + }))); +} +}; + +class Getattr_7_8: public FuseTest { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + FuseTest::SetUp(); +} +}; + +/* + * If getattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs + * should use the cached attributes, rather than query the daemon + */ +TEST_F(Getattr, attr_cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + struct stat sb; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.entry_valid = UINT64_MAX; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr_valid = UINT64_MAX; + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | 0644; + }))); + EXPECT_EQ(0, stat(FULLPATH, &sb)); + /* The second stat(2) should use cached attributes */ + EXPECT_EQ(0, stat(FULLPATH, &sb)); +} + +/* + * If getattr returns a finite but non-zero cache timeout, then we should + * discard the cached attributes and requery the daemon after the timeout + * period passes. + */ +TEST_F(Getattr, attr_cache_timeout) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + struct stat sb; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1, 0, 0); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(2) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr_valid_nsec = NAP_NS / 2; + out.body.attr.attr_valid = 0; + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | 0644; + }))); + + EXPECT_EQ(0, stat(FULLPATH, &sb)); + nap(); + /* Timeout has expired. stat(2) should requery the daemon */ + EXPECT_EQ(0, stat(FULLPATH, &sb)); +} + +/* + * If attr.blksize is zero, then the kernel should use a default value for + * st_blksize + */ +TEST_F(Getattr, blksize_zero) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + struct stat sb; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 1, 1, 0, 0); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.blksize = 0; + out.body.attr.attr.size = 1; + }))); + + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + EXPECT_EQ((blksize_t)PAGE_SIZE, sb.st_blksize); +} + +TEST_F(Getattr, enoent) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct stat sb; + const uint64_t ino = 42; + sem_t sem; + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1, 0, 0); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(ENOENT))); + // Since FUSE_GETATTR returns ENOENT, the kernel will reclaim the vnode + // and send a FUSE_FORGET + expect_forget(ino, 1, &sem); + + EXPECT_NE(0, stat(FULLPATH, &sb)); + EXPECT_EQ(ENOENT, errno); + + sem_wait(&sem); + sem_destroy(&sem); +} + +TEST_F(Getattr, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + struct stat sb; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 1, 1, 0, 0); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.body.getattr.getattr_flags == 0 && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr.size = 1; + out.body.attr.attr.blocks = 2; + out.body.attr.attr.atime = 3; + out.body.attr.attr.mtime = 4; + out.body.attr.attr.ctime = 5; + out.body.attr.attr.atimensec = 6; + out.body.attr.attr.mtimensec = 7; + out.body.attr.attr.ctimensec = 8; + out.body.attr.attr.nlink = 9; + out.body.attr.attr.uid = 10; + out.body.attr.attr.gid = 11; + out.body.attr.attr.rdev = 12; + out.body.attr.attr.blksize = 12345; + }))); + + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + EXPECT_EQ(1, sb.st_size); + EXPECT_EQ(2, sb.st_blocks); + EXPECT_EQ(3, sb.st_atim.tv_sec); + EXPECT_EQ(6, sb.st_atim.tv_nsec); + EXPECT_EQ(4, sb.st_mtim.tv_sec); + EXPECT_EQ(7, sb.st_mtim.tv_nsec); + EXPECT_EQ(5, sb.st_ctim.tv_sec); + EXPECT_EQ(8, sb.st_ctim.tv_nsec); + EXPECT_EQ(9ull, sb.st_nlink); + EXPECT_EQ(10ul, sb.st_uid); + EXPECT_EQ(11ul, sb.st_gid); + EXPECT_EQ(12ul, sb.st_rdev); + EXPECT_EQ((blksize_t)12345, sb.st_blksize); + EXPECT_EQ(ino, sb.st_ino); + EXPECT_EQ(S_IFREG | 0644, sb.st_mode); + + /* + * st_birthtim and st_flags are not supported by the fuse protocol. + * They're only supported as OS-specific extensions to OSX. For + * birthtime, the convention for "not supported" is "negative one + * second". + */ + EXPECT_EQ(-1, sb.st_birthtim.tv_sec); + EXPECT_EQ(0, sb.st_birthtim.tv_nsec); + EXPECT_EQ(0u, sb.st_flags); +} + +/* + * FUSE_GETATTR returns a different file type, even though the entry cache + * hasn't expired. This is a server bug! It probably means that the server + * removed the file and recreated it with the same inode but a different vtyp. + * The best thing fusefs can do is return ENOENT to the caller. After all, the + * entry must not have existed recently. + */ +TEST_F(Getattr, vtyp_conflict) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + struct stat sb; + sem_t sem; + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = 0; + out.body.entry.entry_valid = UINT64_MAX; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.body.getattr.getattr_flags == 0 && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFDIR | 0755; // Changed! + out.body.attr.attr.nlink = 2; + }))); + // We should reclaim stale vnodes + expect_forget(ino, 1, &sem); + + ASSERT_NE(0, stat(FULLPATH, &sb)); + EXPECT_EQ(errno, ENOENT); + + sem_wait(&sem); + sem_destroy(&sem); +} + +TEST_F(Getattr_7_8, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + struct stat sb; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr.size = 1; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr_7_8); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr.size = 1; + out.body.attr.attr.blocks = 2; + out.body.attr.attr.atime = 3; + out.body.attr.attr.mtime = 4; + out.body.attr.attr.ctime = 5; + out.body.attr.attr.atimensec = 6; + out.body.attr.attr.mtimensec = 7; + out.body.attr.attr.ctimensec = 8; + out.body.attr.attr.nlink = 9; + out.body.attr.attr.uid = 10; + out.body.attr.attr.gid = 11; + out.body.attr.attr.rdev = 12; + }))); + + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + EXPECT_EQ(1, sb.st_size); + EXPECT_EQ(2, sb.st_blocks); + EXPECT_EQ(3, sb.st_atim.tv_sec); + EXPECT_EQ(6, sb.st_atim.tv_nsec); + EXPECT_EQ(4, sb.st_mtim.tv_sec); + EXPECT_EQ(7, sb.st_mtim.tv_nsec); + EXPECT_EQ(5, sb.st_ctim.tv_sec); + EXPECT_EQ(8, sb.st_ctim.tv_nsec); + EXPECT_EQ(9ull, sb.st_nlink); + EXPECT_EQ(10ul, sb.st_uid); + EXPECT_EQ(11ul, sb.st_gid); + EXPECT_EQ(12ul, sb.st_rdev); + EXPECT_EQ(ino, sb.st_ino); + EXPECT_EQ(S_IFREG | 0644, sb.st_mode); + + //st_birthtim and st_flags are not supported by protocol 7.8. They're + //only supported as OS-specific extensions to OSX. +} diff --git a/tests/sys/fs/fusefs/interrupt.cc b/tests/sys/fs/fusefs/interrupt.cc new file mode 100644 index 000000000000..3bfd4a834932 --- /dev/null +++ b/tests/sys/fs/fusefs/interrupt.cc @@ -0,0 +1,796 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/types.h> +#include <sys/extattr.h> +#include <sys/mman.h> +#include <sys/wait.h> +#include <fcntl.h> +#include <pthread.h> +#include <semaphore.h> +#include <signal.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +/* Initial size of files used by these tests */ +const off_t FILESIZE = 1000; +/* Access mode used by all directories in these tests */ +const mode_t MODE = 0755; +const char FULLDIRPATH0[] = "mountpoint/some_dir"; +const char RELDIRPATH0[] = "some_dir"; +const char FULLDIRPATH1[] = "mountpoint/other_dir"; +const char RELDIRPATH1[] = "other_dir"; + +static sem_t *blocked_semaphore; +static sem_t *signaled_semaphore; + +static bool killer_should_sleep = false; + +/* Don't do anything; all we care about is that the syscall gets interrupted */ +void sigusr2_handler(int __unused sig) { + if (verbosity > 1) { + printf("Signaled! thread %p\n", pthread_self()); + } + +} + +void* killer(void* target) { + /* Wait until the main thread is blocked in fdisp_wait_answ */ + if (killer_should_sleep) + nap(); + else + sem_wait(blocked_semaphore); + if (verbosity > 1) + printf("Signalling! thread %p\n", target); + pthread_kill((pthread_t)target, SIGUSR2); + if (signaled_semaphore != NULL) + sem_post(signaled_semaphore); + + return(NULL); +} + +class Interrupt: public FuseTest { +public: +pthread_t m_child; + +Interrupt(): m_child(NULL) {}; + +void expect_lookup(const char *relpath, uint64_t ino) +{ + FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, FILESIZE, 1); +} + +/* + * Expect a FUSE_MKDIR but don't reply. Instead, just record the unique value + * to the provided pointer + */ +void expect_mkdir(uint64_t *mkdir_unique) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_MKDIR); + }, Eq(true)), + _) + ).WillOnce(Invoke([=](auto in, auto &out __unused) { + *mkdir_unique = in.header.unique; + sem_post(blocked_semaphore); + })); +} + +/* + * Expect a FUSE_READ but don't reply. Instead, just record the unique value + * to the provided pointer + */ +void expect_read(uint64_t ino, uint64_t *read_unique) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke([=](auto in, auto &out __unused) { + *read_unique = in.header.unique; + sem_post(blocked_semaphore); + })); +} + +/* + * Expect a FUSE_WRITE but don't reply. Instead, just record the unique value + * to the provided pointer + */ +void expect_write(uint64_t ino, uint64_t *write_unique) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_WRITE && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke([=](auto in, auto &out __unused) { + *write_unique = in.header.unique; + sem_post(blocked_semaphore); + })); +} + +void setup_interruptor(pthread_t target, bool sleep = false) +{ + ASSERT_NE(SIG_ERR, signal(SIGUSR2, sigusr2_handler)) << strerror(errno); + killer_should_sleep = sleep; + ASSERT_EQ(0, pthread_create(&m_child, NULL, killer, (void*)target)) + << strerror(errno); +} + +void SetUp() { + const int mprot = PROT_READ | PROT_WRITE; + const int mflags = MAP_ANON | MAP_SHARED; + + signaled_semaphore = NULL; + + blocked_semaphore = (sem_t*)mmap(NULL, sizeof(*blocked_semaphore), + mprot, mflags, -1, 0); + ASSERT_NE(MAP_FAILED, blocked_semaphore) << strerror(errno); + ASSERT_EQ(0, sem_init(blocked_semaphore, 1, 0)) << strerror(errno); + ASSERT_EQ(0, siginterrupt(SIGUSR2, 1)); + + FuseTest::SetUp(); +} + +void TearDown() { + struct sigaction sa; + + if (m_child != NULL) { + pthread_join(m_child, NULL); + } + bzero(&sa, sizeof(sa)); + sa.sa_handler = SIG_DFL; + sigaction(SIGUSR2, &sa, NULL); + + sem_destroy(blocked_semaphore); + munmap(blocked_semaphore, sizeof(*blocked_semaphore)); + + FuseTest::TearDown(); +} +}; + +class Intr: public Interrupt {}; + +class Nointr: public Interrupt { + void SetUp() { + m_nointr = true; + Interrupt::SetUp(); + } +}; + +static void* mkdir0(void* arg __unused) { + ssize_t r; + + r = mkdir(FULLDIRPATH0, MODE); + if (r >= 0) + return 0; + else + return (void*)(intptr_t)errno; +} + +static void* read1(void* arg) { + const size_t bufsize = FILESIZE; + char buf[bufsize]; + int fd = (int)(intptr_t)arg; + ssize_t r; + + r = read(fd, buf, bufsize); + if (r >= 0) + return 0; + else + return (void*)(intptr_t)errno; +} + +/* + * An interrupt operation that gets received after the original command is + * complete should generate an EAGAIN response. + */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */ +TEST_F(Intr, already_complete) +{ + uint64_t ino = 42; + pthread_t self; + uint64_t mkdir_unique = 0; + Sequence seq; + + self = pthread_self(); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0) + .InSequence(seq) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_mkdir(&mkdir_unique); + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + return (in.header.opcode == FUSE_INTERRUPT && + in.body.interrupt.unique == mkdir_unique); + }, Eq(true)), + _) + ).WillOnce(Invoke([&](auto in, auto &out) { + // First complete the mkdir request + std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out); + out0->header.unique = mkdir_unique; + SET_OUT_HEADER_LEN(*out0, entry); + out0->body.create.entry.attr.mode = S_IFDIR | MODE; + out0->body.create.entry.nodeid = ino; + out.push_back(std::move(out0)); + + // Then, respond EAGAIN to the interrupt request + std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out); + out1->header.unique = in.header.unique; + out1->header.error = -EAGAIN; + out1->header.len = sizeof(out1->header); + out.push_back(std::move(out1)); + })); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0) + .InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | MODE; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 2; + }))); + + setup_interruptor(self); + EXPECT_EQ(0, mkdir(FULLDIRPATH0, MODE)) << strerror(errno); + /* + * The final syscall simply ensures that the test's main thread doesn't + * end before the daemon finishes responding to the FUSE_INTERRUPT. + */ + EXPECT_EQ(0, access(FULLDIRPATH0, F_OK)) << strerror(errno); +} + +/* + * If a FUSE file system returns ENOSYS for a FUSE_INTERRUPT operation, the + * kernel should not attempt to interrupt any other operations on that mount + * point. + */ +TEST_F(Intr, enosys) +{ + uint64_t ino0 = 42, ino1 = 43;; + uint64_t mkdir_unique; + pthread_t self, th0; + sem_t sem0, sem1; + void *thr0_value; + Sequence seq; + + self = pthread_self(); + ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno); + ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH1) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_mkdir(&mkdir_unique); + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + return (in.header.opcode == FUSE_INTERRUPT && + in.body.interrupt.unique == mkdir_unique); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke([&](auto in, auto &out) { + // reject FUSE_INTERRUPT and respond to the FUSE_MKDIR + std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out); + std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out); + + out0->header.unique = in.header.unique; + out0->header.error = -ENOSYS; + out0->header.len = sizeof(out0->header); + out.push_back(std::move(out0)); + + SET_OUT_HEADER_LEN(*out1, entry); + out1->body.create.entry.attr.mode = S_IFDIR | MODE; + out1->body.create.entry.nodeid = ino1; + out1->header.unique = mkdir_unique; + out.push_back(std::move(out1)); + })); + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + return (in.header.opcode == FUSE_MKDIR); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke([&](auto in, auto &out) { + std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out); + + sem_post(&sem0); + sem_wait(&sem1); + + SET_OUT_HEADER_LEN(*out0, entry); + out0->body.create.entry.attr.mode = S_IFDIR | MODE; + out0->body.create.entry.nodeid = ino0; + out0->header.unique = in.header.unique; + out.push_back(std::move(out0)); + })); + + setup_interruptor(self); + /* First mkdir operation should finish synchronously */ + ASSERT_EQ(0, mkdir(FULLDIRPATH1, MODE)) << strerror(errno); + + ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL)) + << strerror(errno); + + sem_wait(&sem0); + /* + * th0 should be blocked waiting for the fuse daemon thread. + * Signal it. No FUSE_INTERRUPT should result + */ + pthread_kill(th0, SIGUSR1); + /* Allow the daemon thread to proceed */ + sem_post(&sem1); + pthread_join(th0, &thr0_value); + /* Second mkdir should've finished without error */ + EXPECT_EQ(0, (intptr_t)thr0_value); +} + +/* + * A FUSE filesystem is legally allowed to ignore INTERRUPT operations, and + * complete the original operation whenever it damn well pleases. + */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */ +TEST_F(Intr, ignore) +{ + uint64_t ino = 42; + pthread_t self; + uint64_t mkdir_unique; + + self = pthread_self(); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_mkdir(&mkdir_unique); + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + return (in.header.opcode == FUSE_INTERRUPT && + in.body.interrupt.unique == mkdir_unique); + }, Eq(true)), + _) + ).WillOnce(Invoke([&](auto in __unused, auto &out) { + // Ignore FUSE_INTERRUPT; respond to the FUSE_MKDIR + std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out); + out0->header.unique = mkdir_unique; + SET_OUT_HEADER_LEN(*out0, entry); + out0->body.create.entry.attr.mode = S_IFDIR | MODE; + out0->body.create.entry.nodeid = ino; + out.push_back(std::move(out0)); + })); + + setup_interruptor(self); + ASSERT_EQ(0, mkdir(FULLDIRPATH0, MODE)) << strerror(errno); +} + +/* + * A restartable operation (basically, anything except write or setextattr) + * that hasn't yet been sent to userland can be interrupted without sending + * FUSE_INTERRUPT, and will be automatically restarted. + */ +TEST_F(Intr, in_kernel_restartable) +{ + const char FULLPATH1[] = "mountpoint/other_file.txt"; + const char RELPATH1[] = "other_file.txt"; + uint64_t ino0 = 42, ino1 = 43; + int fd1; + pthread_t self, th0, th1; + sem_t sem0, sem1; + void *thr0_value, *thr1_value; + + ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno); + ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno); + self = pthread_self(); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_lookup(RELPATH1, ino1); + expect_open(ino1, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_MKDIR); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([&](auto in __unused, auto& out) { + /* Let the next write proceed */ + sem_post(&sem1); + /* Pause the daemon thread so it won't read the next op */ + sem_wait(&sem0); + + SET_OUT_HEADER_LEN(out, entry); + out.body.create.entry.attr.mode = S_IFDIR | MODE; + out.body.create.entry.nodeid = ino0; + }))); + FuseTest::expect_read(ino1, 0, FILESIZE, 0, NULL); + + fd1 = open(FULLPATH1, O_RDONLY); + ASSERT_LE(0, fd1) << strerror(errno); + + /* Use a separate thread for each operation */ + ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL)) + << strerror(errno); + + sem_wait(&sem1); /* Sequence the two operations */ + + ASSERT_EQ(0, pthread_create(&th1, NULL, read1, (void*)(intptr_t)fd1)) + << strerror(errno); + + setup_interruptor(self, true); + + pause(); /* Wait for signal */ + + /* Unstick the daemon */ + ASSERT_EQ(0, sem_post(&sem0)) << strerror(errno); + + /* Wait awhile to make sure the signal generates no FUSE_INTERRUPT */ + nap(); + + pthread_join(th1, &thr1_value); + pthread_join(th0, &thr0_value); + EXPECT_EQ(0, (intptr_t)thr1_value); + EXPECT_EQ(0, (intptr_t)thr0_value); + sem_destroy(&sem1); + sem_destroy(&sem0); + + leak(fd1); +} + +/* + * An operation that hasn't yet been sent to userland can be interrupted + * without sending FUSE_INTERRUPT. If it's a non-restartable operation (write + * or setextattr) it will return EINTR. + */ +TEST_F(Intr, in_kernel_nonrestartable) +{ + const char FULLPATH1[] = "mountpoint/other_file.txt"; + const char RELPATH1[] = "other_file.txt"; + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + uint64_t ino0 = 42, ino1 = 43; + int ns = EXTATTR_NAMESPACE_USER; + int fd1; + pthread_t self, th0; + sem_t sem0, sem1; + void *thr0_value; + ssize_t r; + + ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno); + ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno); + self = pthread_self(); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_lookup(RELPATH1, ino1); + expect_open(ino1, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_MKDIR); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([&](auto in __unused, auto& out) { + /* Let the next write proceed */ + sem_post(&sem1); + /* Pause the daemon thread so it won't read the next op */ + sem_wait(&sem0); + + SET_OUT_HEADER_LEN(out, entry); + out.body.create.entry.attr.mode = S_IFDIR | MODE; + out.body.create.entry.nodeid = ino0; + }))); + + fd1 = open(FULLPATH1, O_WRONLY); + ASSERT_LE(0, fd1) << strerror(errno); + + /* Use a separate thread for the first write */ + ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL)) + << strerror(errno); + + sem_wait(&sem1); /* Sequence the two operations */ + + setup_interruptor(self, true); + + r = extattr_set_fd(fd1, ns, "foo", (const void*)value, value_len); + EXPECT_NE(0, r); + EXPECT_EQ(EINTR, errno); + + /* Unstick the daemon */ + ASSERT_EQ(0, sem_post(&sem0)) << strerror(errno); + + /* Wait awhile to make sure the signal generates no FUSE_INTERRUPT */ + nap(); + + pthread_join(th0, &thr0_value); + EXPECT_EQ(0, (intptr_t)thr0_value); + sem_destroy(&sem1); + sem_destroy(&sem0); + + leak(fd1); +} + +/* + * A syscall that gets interrupted while blocking on FUSE I/O should send a + * FUSE_INTERRUPT command to the fuse filesystem, which should then send EINTR + * in response to the _original_ operation. The kernel should ultimately + * return EINTR to userspace + */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */ +TEST_F(Intr, in_progress) +{ + pthread_t self; + uint64_t mkdir_unique; + + self = pthread_self(); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_mkdir(&mkdir_unique); + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + return (in.header.opcode == FUSE_INTERRUPT && + in.body.interrupt.unique == mkdir_unique); + }, Eq(true)), + _) + ).WillOnce(Invoke([&](auto in __unused, auto &out) { + std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out); + out0->header.error = -EINTR; + out0->header.unique = mkdir_unique; + out0->header.len = sizeof(out0->header); + out.push_back(std::move(out0)); + })); + + setup_interruptor(self); + ASSERT_EQ(-1, mkdir(FULLDIRPATH0, MODE)); + EXPECT_EQ(EINTR, errno); +} + +/* Reads should also be interruptible */ +TEST_F(Intr, in_progress_read) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const size_t bufsize = 80; + char buf[bufsize]; + uint64_t ino = 42; + int fd; + pthread_t self; + uint64_t read_unique; + + self = pthread_self(); + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_read(ino, &read_unique); + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + return (in.header.opcode == FUSE_INTERRUPT && + in.body.interrupt.unique == read_unique); + }, Eq(true)), + _) + ).WillOnce(Invoke([&](auto in __unused, auto &out) { + std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out); + out0->header.error = -EINTR; + out0->header.unique = read_unique; + out0->header.len = sizeof(out0->header); + out.push_back(std::move(out0)); + })); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + setup_interruptor(self); + ASSERT_EQ(-1, read(fd, buf, bufsize)); + EXPECT_EQ(EINTR, errno); + + leak(fd); +} + +/* + * When mounted with -o nointr, fusefs will block signals while waiting for the + * server. + */ +TEST_F(Nointr, block) +{ + uint64_t ino = 42; + pthread_t self; + sem_t sem0; + + ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno); + signaled_semaphore = &sem0; + self = pthread_self(); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_MKDIR); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([&](auto in __unused, auto& out) { + /* Let the killer proceed */ + sem_post(blocked_semaphore); + + /* Wait until after the signal has been sent */ + sem_wait(signaled_semaphore); + /* Allow time for the mkdir thread to receive the signal */ + nap(); + + /* Finally, complete the original op */ + SET_OUT_HEADER_LEN(out, entry); + out.body.create.entry.attr.mode = S_IFDIR | MODE; + out.body.create.entry.nodeid = ino; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + return (in.header.opcode == FUSE_INTERRUPT); + }, Eq(true)), + _) + ).Times(0); + + setup_interruptor(self); + ASSERT_EQ(0, mkdir(FULLDIRPATH0, MODE)) << strerror(errno); + + sem_destroy(&sem0); +} + +/* FUSE_INTERRUPT operations should take priority over other pending ops */ +TEST_F(Intr, priority) +{ + Sequence seq; + uint64_t ino1 = 43; + uint64_t mkdir_unique; + pthread_t th0; + sem_t sem0, sem1; + + ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno); + ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH1) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_MKDIR); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) { + mkdir_unique = in.header.unique; + + /* Let the next mkdir proceed */ + sem_post(&sem1); + + /* Pause the daemon thread so it won't read the next op */ + sem_wait(&sem0); + + /* Finally, interrupt the original op */ + out.header.error = -EINTR; + out.header.unique = mkdir_unique; + out.header.len = sizeof(out.header); + }))); + /* + * FUSE_INTERRUPT should be received before the second FUSE_MKDIR, + * even though it was generated later + */ + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + return (in.header.opcode == FUSE_INTERRUPT && + in.body.interrupt.unique == mkdir_unique); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke(ReturnErrno(EAGAIN))); + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + return (in.header.opcode == FUSE_MKDIR); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.create.entry.attr.mode = S_IFDIR | MODE; + out.body.create.entry.nodeid = ino1; + }))); + + /* Use a separate thread for the first mkdir */ + ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL)) + << strerror(errno); + + signaled_semaphore = &sem0; + + sem_wait(&sem1); /* Sequence the two mkdirs */ + setup_interruptor(th0, true); + ASSERT_EQ(0, mkdir(FULLDIRPATH1, MODE)) << strerror(errno); + + pthread_join(th0, NULL); + sem_destroy(&sem1); + sem_destroy(&sem0); +} + +/* + * If the FUSE filesystem receives the FUSE_INTERRUPT operation before + * processing the original, then it should wait for "some timeout" for the + * original operation to arrive. If not, it should send EAGAIN to the + * INTERRUPT operation, and the kernel should requeue the INTERRUPT. + * + * In this test, we'll pretend that the INTERRUPT arrives too soon, gets + * EAGAINed, then the kernel requeues it, and the second time around it + * successfully interrupts the original + */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */ +TEST_F(Intr, too_soon) +{ + Sequence seq; + pthread_t self; + uint64_t mkdir_unique; + + self = pthread_self(); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH0) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_mkdir(&mkdir_unique); + + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + return (in.header.opcode == FUSE_INTERRUPT && + in.body.interrupt.unique == mkdir_unique); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke(ReturnErrno(EAGAIN))); + + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + return (in.header.opcode == FUSE_INTERRUPT && + in.body.interrupt.unique == mkdir_unique); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke([&](auto in __unused, auto &out __unused) { + std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out); + out0->header.error = -EINTR; + out0->header.unique = mkdir_unique; + out0->header.len = sizeof(out0->header); + out.push_back(std::move(out0)); + })); + + setup_interruptor(self); + ASSERT_EQ(-1, mkdir(FULLDIRPATH0, MODE)); + EXPECT_EQ(EINTR, errno); +} + + +// TODO: add a test where write returns EWOULDBLOCK diff --git a/tests/sys/fs/fusefs/io.cc b/tests/sys/fs/fusefs/io.cc new file mode 100644 index 000000000000..ced291836da0 --- /dev/null +++ b/tests/sys/fs/fusefs/io.cc @@ -0,0 +1,609 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/types.h> +#include <sys/mman.h> + +#include <fcntl.h> +#include <stdlib.h> +#include <unistd.h> +} + +#include <iomanip> + +#include "mockfs.hh" +#include "utils.hh" + +/* + * For testing I/O like fsx does, but deterministically and without a real + * underlying file system + */ + +using namespace testing; + +const char FULLPATH[] = "mountpoint/some_file.txt"; +const char RELPATH[] = "some_file.txt"; +const uint64_t ino = 42; + +static void compare(const void *tbuf, const void *controlbuf, off_t baseofs, + ssize_t size) +{ + int i; + + for (i = 0; i < size; i++) { + if (((const char*)tbuf)[i] != ((const char*)controlbuf)[i]) { + off_t ofs = baseofs + i; + FAIL() << "miscompare at offset " + << std::hex + << std::showbase + << ofs + << ". expected = " + << std::setw(2) + << (unsigned)((const uint8_t*)controlbuf)[i] + << " got = " + << (unsigned)((const uint8_t*)tbuf)[i]; + } + } +} + +typedef tuple<bool, uint32_t, cache_mode, uint32_t> IoParam; + +class Io: public FuseTest, public WithParamInterface<IoParam> { +public: +int m_backing_fd, m_control_fd, m_test_fd; +off_t m_filesize; +bool m_direct_io; + +Io(): m_backing_fd(-1), m_control_fd(-1), m_test_fd(-1), m_filesize(0), + m_direct_io(false) {}; + +void SetUp() +{ + m_backing_fd = open("backing_file", O_RDWR | O_CREAT | O_TRUNC, 0644); + if (m_backing_fd < 0) + FAIL() << strerror(errno); + m_control_fd = open("control", O_RDWR | O_CREAT | O_TRUNC, 0644); + if (m_control_fd < 0) + FAIL() << strerror(errno); + srandom(22'9'1982); // Seed with my birthday + + if (get<0>(GetParam())) + m_init_flags |= FUSE_ASYNC_READ; + m_maxwrite = get<1>(GetParam()); + switch (get<2>(GetParam())) { + case Uncached: + m_direct_io = true; + break; + case WritebackAsync: + m_async = true; + /* FALLTHROUGH */ + case Writeback: + m_init_flags |= FUSE_WRITEBACK_CACHE; + /* FALLTHROUGH */ + case Writethrough: + break; + default: + FAIL() << "Unknown cache mode"; + } + m_kernel_minor_version = get<3>(GetParam()); + m_noatime = true; // To prevent SETATTR for atime on close + + FuseTest::SetUp(); + if (IsSkipped()) + return; + + if (verbosity > 0) { + printf("Test Parameters: init_flags=%#x maxwrite=%#x " + "%sasync cache=%s kernel_minor_version=%d\n", + m_init_flags, m_maxwrite, m_async? "" : "no", + cache_mode_to_s(get<2>(GetParam())), + m_kernel_minor_version); + } + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_open(ino, m_direct_io ? FOPEN_DIRECT_IO : 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_WRITE && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) { + const char *buf = (const char*)in.body.bytes + + sizeof(struct fuse_write_in); + ssize_t isize = in.body.write.size; + off_t iofs = in.body.write.offset; + + assert((size_t)isize <= sizeof(in.body.bytes) - + sizeof(struct fuse_write_in)); + ASSERT_EQ(isize, pwrite(m_backing_fd, buf, isize, iofs)) + << strerror(errno); + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = isize; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) { + ssize_t isize = in.body.write.size; + off_t iofs = in.body.write.offset; + void *buf = out.body.bytes; + ssize_t osize; + + assert((size_t)isize <= sizeof(out.body.bytes)); + osize = pread(m_backing_fd, buf, isize, iofs); + ASSERT_LE(0, osize) << strerror(errno); + out.header.len = sizeof(struct fuse_out_header) + osize; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + (in.body.setattr.valid & FATTR_SIZE)); + + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) { + ASSERT_EQ(0, ftruncate(m_backing_fd, in.body.setattr.size)) + << strerror(errno); + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFREG | 0755; + out.body.attr.attr.size = in.body.setattr.size; + out.body.attr.attr_valid = UINT64_MAX; + }))); + /* Any test that close()s will send FUSE_FLUSH and FUSE_RELEASE */ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FLUSH && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnErrno(0))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_RELEASE && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnErrno(0))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_COPY_FILE_RANGE && + in.header.nodeid == ino && + in.body.copy_file_range.nodeid_out == ino && + in.body.copy_file_range.flags == 0); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) { + off_t off_in = in.body.copy_file_range.off_in; + off_t off_out = in.body.copy_file_range.off_out; + ASSERT_EQ((ssize_t)in.body.copy_file_range.len, + copy_file_range(m_backing_fd, &off_in, m_backing_fd, + &off_out, in.body.copy_file_range.len, 0)); + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = in.body.copy_file_range.len; + }))); + /* Claim that we don't support FUSE_LSEEK */ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnErrno(ENOSYS))); + + m_test_fd = open(FULLPATH, O_RDWR ); + EXPECT_LE(0, m_test_fd) << strerror(errno); +} + +void TearDown() +{ + if (m_test_fd >= 0) + close(m_test_fd); + if (m_backing_fd >= 0) + close(m_backing_fd); + if (m_control_fd >= 0) + close(m_control_fd); + FuseTest::TearDown(); + leak(m_test_fd); +} + +void do_closeopen() +{ + ASSERT_EQ(0, close(m_test_fd)) << strerror(errno); + m_test_fd = open("backing_file", O_RDWR); + ASSERT_LE(0, m_test_fd) << strerror(errno); + + ASSERT_EQ(0, close(m_control_fd)) << strerror(errno); + m_control_fd = open("control", O_RDWR); + ASSERT_LE(0, m_control_fd) << strerror(errno); +} + +void do_copy_file_range(off_t off_in, off_t off_out, size_t size) +{ + ssize_t r; + off_t test_off_in = off_in; + off_t test_off_out = off_out; + off_t test_size = size; + off_t control_off_in = off_in; + off_t control_off_out = off_out; + off_t control_size = size; + + while (test_size > 0) { + r = copy_file_range(m_test_fd, &test_off_in, m_test_fd, + &test_off_out, test_size, 0); + ASSERT_GT(r, 0) << strerror(errno); + test_size -= r; + } + while (control_size > 0) { + r = copy_file_range(m_control_fd, &control_off_in, m_control_fd, + &control_off_out, control_size, 0); + ASSERT_GT(r, 0) << strerror(errno); + control_size -= r; + } + m_filesize = std::max(m_filesize, off_out + (off_t)size); +} + +void do_ftruncate(off_t offs) +{ + ASSERT_EQ(0, ftruncate(m_test_fd, offs)) << strerror(errno); + ASSERT_EQ(0, ftruncate(m_control_fd, offs)) << strerror(errno); + m_filesize = offs; +} + +void do_mapread(off_t offs, ssize_t size) +{ + char *control_buf; + void *p; + off_t pg_offset, page_mask; + size_t map_size; + + page_mask = getpagesize() - 1; + pg_offset = offs & page_mask; + map_size = pg_offset + size; + + p = mmap(NULL, map_size, PROT_READ, MAP_FILE | MAP_SHARED, m_test_fd, + offs - pg_offset); + ASSERT_NE(p, MAP_FAILED) << strerror(errno); + + control_buf = new char[size]; + + ASSERT_EQ(size, pread(m_control_fd, control_buf, size, offs)) + << strerror(errno); + + compare((void*)((char*)p + pg_offset), control_buf, offs, size); + + ASSERT_EQ(0, munmap(p, map_size)) << strerror(errno); + delete[] control_buf; +} + +void do_read(off_t offs, ssize_t size) +{ + char *test_buf, *control_buf; + ssize_t r; + + test_buf = new char[size]; + control_buf = new char[size]; + + errno = 0; + r = pread(m_test_fd, test_buf, size, offs); + ASSERT_NE(-1, r) << strerror(errno); + ASSERT_EQ(size, r) << "unexpected short read"; + r = pread(m_control_fd, control_buf, size, offs); + ASSERT_NE(-1, r) << strerror(errno); + ASSERT_EQ(size, r) << "unexpected short read"; + + compare(test_buf, control_buf, offs, size); + + delete[] control_buf; + delete[] test_buf; +} + +void do_mapwrite(off_t offs, ssize_t size) +{ + char *buf; + void *p; + off_t pg_offset, page_mask; + size_t map_size; + long i; + + page_mask = getpagesize() - 1; + pg_offset = offs & page_mask; + map_size = pg_offset + size; + + buf = new char[size]; + for (i=0; i < size; i++) + buf[i] = random(); + + if (offs + size > m_filesize) { + /* + * Must manually extend. vm_mmap_vnode will not implicitly + * extend a vnode + */ + do_ftruncate(offs + size); + } + + p = mmap(NULL, map_size, PROT_READ | PROT_WRITE, + MAP_FILE | MAP_SHARED, m_test_fd, offs - pg_offset); + ASSERT_NE(p, MAP_FAILED) << strerror(errno); + + bcopy(buf, (char*)p + pg_offset, size); + ASSERT_EQ(size, pwrite(m_control_fd, buf, size, offs)) + << strerror(errno); + + delete[] buf; + ASSERT_EQ(0, munmap(p, map_size)) << strerror(errno); +} + +void do_write(off_t offs, ssize_t size) +{ + char *buf; + long i; + + buf = new char[size]; + for (i=0; i < size; i++) + buf[i] = random(); + + ASSERT_EQ(size, pwrite(m_test_fd, buf, size, offs )) + << strerror(errno); + ASSERT_EQ(size, pwrite(m_control_fd, buf, size, offs)) + << strerror(errno); + m_filesize = std::max(m_filesize, offs + size); + + delete[] buf; +} + +}; + +class IoCacheable: public Io { +public: +virtual void SetUp() { + Io::SetUp(); +} +}; + +class IoCopyFileRange: public Io { +public: +virtual void SetUp() { + Io::SetUp(); +} +}; + +/* + * Extend a file with dirty data in the last page of the last block. + * + * fsx -WR -P /tmp -S8 -N3 fsx.bin + */ +TEST_P(Io, extend_from_dirty_page) +{ + off_t wofs = 0x21a0; + ssize_t wsize = 0xf0a8; + off_t rofs = 0xb284; + ssize_t rsize = 0x9b22; + off_t truncsize = 0x28702; + + do_write(wofs, wsize); + do_ftruncate(truncsize); + do_read(rofs, rsize); +} + +/* + * mapwrite into a newly extended part of a file. + * + * fsx -c 100 -i 100 -l 524288 -o 131072 -N5 -P /tmp -S19 fsx.bin + */ +TEST_P(IoCacheable, extend_by_mapwrite) +{ + do_mapwrite(0x29a3a, 0x849e); /* [0x29a3a, 0x31ed7] */ + do_mapwrite(0x3c7d8, 0x3994); /* [0x3c7d8, 0x4016b] */ + do_read(0x30c16, 0xf556); /* [0x30c16, 0x4016b] */ +} + +/* + * When writing the last page of a file, it must be written synchronously. + * Otherwise the cached page can become invalid by a subsequent extend + * operation. + * + * fsx -WR -P /tmp -S642 -N3 fsx.bin + */ +TEST_P(Io, last_page) +{ + do_write(0x1134f, 0xcc77); /* [0x1134f, 0x1dfc5] */ + do_write(0x2096a, 0xdfa7); /* [0x2096a, 0x2e910] */ + do_read(0x1a3aa, 0xb5b7); /* [0x1a3aa, 0x25960] */ +} + +/* + * Read a hole using mmap + * + * fsx -c 100 -i 100 -l 524288 -o 131072 -N11 -P /tmp -S14 fsx.bin + */ +TEST_P(IoCacheable, mapread_hole) +{ + do_write(0xf205, 0x123b7); /* [0xf205, 0x215bb] */ + do_mapread(0x2f4c, 0xeeea); /* [0x2f4c, 0x11e35] */ +} + +/* + * Read a hole from a block that contains some cached data. + * + * fsx -WR -P /tmp -S55 fsx.bin + */ +TEST_P(Io, read_hole_from_cached_block) +{ + off_t wofs = 0x160c5; + ssize_t wsize = 0xa996; + off_t rofs = 0x472e; + ssize_t rsize = 0xd8d5; + + do_write(wofs, wsize); + do_read(rofs, rsize); +} + +/* + * Truncating a file into a dirty buffer should not causing anything untoward + * to happen when that buffer is eventually flushed. + * + * fsx -WR -P /tmp -S839 -d -N6 fsx.bin + */ +TEST_P(Io, truncate_into_dirty_buffer) +{ + off_t wofs0 = 0x3bad7; + ssize_t wsize0 = 0x4529; + off_t wofs1 = 0xc30d; + ssize_t wsize1 = 0x5f77; + off_t truncsize0 = 0x10916; + off_t rofs = 0xdf17; + ssize_t rsize = 0x29ff; + off_t truncsize1 = 0x152b4; + + do_write(wofs0, wsize0); + do_write(wofs1, wsize1); + do_ftruncate(truncsize0); + do_read(rofs, rsize); + do_ftruncate(truncsize1); + close(m_test_fd); +} + +/* + * Truncating a file into a dirty buffer should not causing anything untoward + * to happen when that buffer is eventually flushed, even when the buffer's + * dirty_off is > 0. + * + * Based on this command with a few steps removed: + * fsx -WR -P /tmp -S677 -d -N8 fsx.bin + */ +TEST_P(Io, truncate_into_dirty_buffer2) +{ + off_t truncsize0 = 0x344f3; + off_t wofs = 0x2790c; + ssize_t wsize = 0xd86a; + off_t truncsize1 = 0x2de38; + off_t rofs2 = 0x1fd7a; + ssize_t rsize2 = 0xc594; + off_t truncsize2 = 0x31e71; + + /* Sets the file size to something larger than the next write */ + do_ftruncate(truncsize0); + /* + * Creates a dirty buffer. The part in lbn 2 doesn't flush + * synchronously. + */ + do_write(wofs, wsize); + /* Truncates part of the dirty buffer created in step 2 */ + do_ftruncate(truncsize1); + /* XXX ?I don't know why this is necessary? */ + do_read(rofs2, rsize2); + /* Truncates the dirty buffer */ + do_ftruncate(truncsize2); + close(m_test_fd); +} + +/* + * Regression test for a bug introduced in r348931 + * + * Sequence of operations: + * 1) The first write reads lbn so it can modify it + * 2) The first write flushes lbn 3 immediately because it's the end of file + * 3) The first write then flushes lbn 4 because it's the end of the file + * 4) The second write modifies the cached versions of lbn 3 and 4 + * 5) The third write's getblkx invalidates lbn 4's B_CACHE because it's + * extending the buffer. Then it flushes lbn 4 because B_DELWRI was set but + * B_CACHE was clear. + * 6) fuse_write_biobackend erroneously called vfs_bio_clrbuf, putting the + * buffer into a weird write-only state. All read operations would return + * 0. Writes were apparently still processed, because the buffer's contents + * were correct when examined in a core dump. + * 7) The third write reads lbn 4 because cache is clear + * 9) uiomove dutifully copies new data into the buffer + * 10) The buffer's dirty is flushed to lbn 4 + * 11) The read returns all zeros because of step 6. + * + * Based on: + * fsx -WR -l 524388 -o 131072 -P /tmp -S6456 -q fsx.bin + */ +TEST_P(Io, resize_a_valid_buffer_while_extending) +{ + do_write(0x36ee6, 0x14530); /* [0x36ee6, 0x4b415] */ + do_write(0x33256, 0x1507c); /* [0x33256, 0x482d1] */ + do_write(0x4c03d, 0x175c); /* [0x4c03d, 0x4d798] */ + do_read(0x3599c, 0xe277); /* [0x3599c, 0x43c12] */ + close(m_test_fd); +} + +/* + * mmap of a suitable region could trigger a panic. I'm not sure what + * combination of size and offset counts as "suitable". Regression test for + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=276191 + */ +TEST_P(IoCacheable, vnode_pager_generic_putpage_clean_block_at_eof) +{ + do_mapwrite(0x3b4e0, 0x1bbc3); +} + +/* + * A copy_file_range that follows an mmap write to the input area needs to + * flush the mmap buffer first. + */ +TEST_P(IoCopyFileRange, copy_file_range_from_mapped_write) +{ + do_mapwrite(0, 0x1000); + do_copy_file_range(0, 0x1000, 0x1000); + do_read(0x1000, 0x1000); +} + + +INSTANTIATE_TEST_SUITE_P(Io, Io, + Combine(Bool(), /* async read */ + Values(0x1000, 0x10000, 0x20000), /* m_maxwrite */ + Values(Uncached, Writethrough, Writeback, WritebackAsync), + Values(28) /* kernel_minor_vers */ + ) +); + +INSTANTIATE_TEST_SUITE_P(Io, IoCacheable, + Combine(Bool(), /* async read */ + Values(0x1000, 0x10000, 0x20000), /* m_maxwrite */ + Values(Writethrough, Writeback, WritebackAsync), + Values(28) /* kernel_minor_vers */ + ) +); + +INSTANTIATE_TEST_SUITE_P(Io, IoCopyFileRange, + Combine(Values(true), /* async read */ + Values(0x10000), /* m_maxwrite */ + Values(Writethrough, Writeback, WritebackAsync), + Values(27, 28) /* kernel_minor_vers */ + ) +); diff --git a/tests/sys/fs/fusefs/last_local_modify.cc b/tests/sys/fs/fusefs/last_local_modify.cc new file mode 100644 index 000000000000..6b8c19f1efc7 --- /dev/null +++ b/tests/sys/fs/fusefs/last_local_modify.cc @@ -0,0 +1,521 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2021 Alan Somers + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> +#include <sys/mount.h> +#include <sys/stat.h> + +#include <fcntl.h> +#include <pthread.h> +#include <semaphore.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +/* + * "Last Local Modify" bugs + * + * This file tests a class of race conditions caused by one thread fetching a + * file's size with FUSE_LOOKUP while another thread simultaneously modifies it + * with FUSE_SETATTR, FUSE_WRITE, FUSE_COPY_FILE_RANGE or similar. It's + * possible for the second thread to start later yet finish first. If that + * happens, the first thread must not override the size set by the second + * thread. + * + * FUSE_GETATTR is not vulnerable to the same race, because it is always called + * with the vnode lock held. + * + * A few other operations like FUSE_LINK can also trigger the same race but + * with the file's ctime instead of size. However, the consequences of an + * incorrect ctime are much less disastrous than an incorrect size, so fusefs + * does not attempt to prevent such races. + */ + +enum Mutator { + VOP_ALLOCATE, + VOP_COPY_FILE_RANGE, + VOP_SETATTR, + VOP_WRITE, +}; + +/* + * Translate a poll method's string representation to the enum value. + * Using strings with ::testing::Values gives better output with + * --gtest_list_tests + */ +enum Mutator writer_from_str(const char* s) { + if (0 == strcmp("VOP_ALLOCATE", s)) + return VOP_ALLOCATE; + else if (0 == strcmp("VOP_COPY_FILE_RANGE", s)) + return VOP_COPY_FILE_RANGE; + else if (0 == strcmp("VOP_SETATTR", s)) + return VOP_SETATTR; + else + return VOP_WRITE; +} + +uint32_t fuse_op_from_mutator(enum Mutator mutator) { + switch(mutator) { + case VOP_ALLOCATE: + return(FUSE_FALLOCATE); + case VOP_COPY_FILE_RANGE: + return(FUSE_COPY_FILE_RANGE); + case VOP_SETATTR: + return(FUSE_SETATTR); + case VOP_WRITE: + return(FUSE_WRITE); + } +} + +class LastLocalModify: public FuseTest, public WithParamInterface<const char*> { +public: +virtual void SetUp() { + m_init_flags = FUSE_EXPORT_SUPPORT; + + FuseTest::SetUp(); +} +}; + +static void* allocate_th(void* arg) { + int fd; + ssize_t r; + sem_t *sem = (sem_t*) arg; + + if (sem) + sem_wait(sem); + + fd = open("mountpoint/some_file.txt", O_RDWR); + if (fd < 0) + return (void*)(intptr_t)errno; + + r = posix_fallocate(fd, 0, 15); + LastLocalModify::leak(fd); + if (r >= 0) + return 0; + else + return (void*)(intptr_t)errno; +} + +static void* copy_file_range_th(void* arg) { + ssize_t r; + int fd; + sem_t *sem = (sem_t*) arg; + off_t off_in = 0; + off_t off_out = 10; + ssize_t len = 5; + + if (sem) + sem_wait(sem); + fd = open("mountpoint/some_file.txt", O_RDWR); + if (fd < 0) + return (void*)(intptr_t)errno; + + r = copy_file_range(fd, &off_in, fd, &off_out, len, 0); + if (r >= 0) { + LastLocalModify::leak(fd); + return 0; + } else + return (void*)(intptr_t)errno; +} + +static void* setattr_th(void* arg) { + int fd; + ssize_t r; + sem_t *sem = (sem_t*) arg; + + if (sem) + sem_wait(sem); + + fd = open("mountpoint/some_file.txt", O_RDWR); + if (fd < 0) + return (void*)(intptr_t)errno; + + r = ftruncate(fd, 15); + LastLocalModify::leak(fd); + if (r >= 0) + return 0; + else + return (void*)(intptr_t)errno; +} + +static void* write_th(void* arg) { + ssize_t r; + int fd; + sem_t *sem = (sem_t*) arg; + const char BUF[] = "abcdefghijklmn"; + + if (sem) + sem_wait(sem); + /* + * Open the file in direct mode. + * The race condition affects both direct and non-direct writes, and + * they have separate code paths. However, in the non-direct case, the + * kernel updates last_local_modify _before_ sending FUSE_WRITE to the + * server. So the technique that this test program uses to invoke the + * race cannot work. Therefore, test with O_DIRECT only. + */ + fd = open("mountpoint/some_file.txt", O_RDWR | O_DIRECT); + if (fd < 0) + return (void*)(intptr_t)errno; + + r = write(fd, BUF, sizeof(BUF)); + if (r >= 0) { + LastLocalModify::leak(fd); + return 0; + } else + return (void*)(intptr_t)errno; +} + +/* + * VOP_LOOKUP should discard attributes returned by the server if they were + * modified by another VOP while the VOP_LOOKUP was in progress. + * + * Sequence of operations: + * * Thread 1 calls a mutator like ftruncate, which acquires the vnode lock + * exclusively. + * * Thread 2 calls stat, which does VOP_LOOKUP, which sends FUSE_LOOKUP to the + * server. The server replies with the old file length. Thread 2 blocks + * waiting for the vnode lock. + * * Thread 1 sends the mutator operation like FUSE_SETATTR that changes the + * file's size and updates the attribute cache. Then it releases the vnode + * lock. + * * Thread 2 acquires the vnode lock. At this point it must not add the + * now-stale file size to the attribute cache. + * + * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071 + */ +TEST_P(LastLocalModify, lookup) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + Sequence seq; + uint64_t ino = 3; + uint64_t mutator_unique; + const uint64_t oldsize = 10; + const uint64_t newsize = 15; + pthread_t th0; + void *thr0_value; + struct stat sb; + static sem_t sem; + Mutator mutator; + uint32_t mutator_op; + size_t mutator_size; + + mutator = writer_from_str(GetParam()); + mutator_op = fuse_op_from_mutator(mutator); + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + /* Called by the mutator, caches attributes but not entries */ + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = ino; + out.body.entry.attr.size = oldsize; + out.body.entry.attr_valid_nsec = NAP_NS / 2; + out.body.entry.attr.ino = ino; + out.body.entry.attr.mode = S_IFREG | 0644; + }))); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == mutator_op && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke([&](auto in, auto &out __unused) { + /* + * The mutator changes the file size, but in order to simulate + * a race, don't reply. Instead, just save the unique for + * later. + */ + mutator_unique = in.header.unique; + switch(mutator) { + case VOP_WRITE: + mutator_size = in.body.write.size; + break; + case VOP_COPY_FILE_RANGE: + mutator_size = in.body.copy_file_range.len; + break; + default: + break; + } + /* Allow the lookup thread to proceed */ + sem_post(&sem); + })); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .InSequence(seq) + .WillOnce(Invoke([&](auto in __unused, auto& out) { + std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out); + std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out); + + /* First complete the lookup request, returning the old size */ + out0->header.unique = in.header.unique; + SET_OUT_HEADER_LEN(*out0, entry); + out0->body.entry.attr.mode = S_IFREG | 0644; + out0->body.entry.nodeid = ino; + out0->body.entry.attr.ino = ino; + out0->body.entry.entry_valid = UINT64_MAX; + out0->body.entry.attr_valid = UINT64_MAX; + out0->body.entry.attr.size = oldsize; + out.push_back(std::move(out0)); + + /* Then, respond to the mutator request */ + out1->header.unique = mutator_unique; + switch(mutator) { + case VOP_ALLOCATE: + out1->header.error = 0; + out1->header.len = sizeof(out1->header); + break; + case VOP_COPY_FILE_RANGE: + SET_OUT_HEADER_LEN(*out1, write); + out1->body.write.size = mutator_size; + break; + case VOP_SETATTR: + SET_OUT_HEADER_LEN(*out1, attr); + out1->body.attr.attr.ino = ino; + out1->body.attr.attr.mode = S_IFREG | 0644; + out1->body.attr.attr.size = newsize; // Changed size + out1->body.attr.attr_valid = UINT64_MAX; + break; + case VOP_WRITE: + SET_OUT_HEADER_LEN(*out1, write); + out1->body.write.size = mutator_size; + break; + } + out.push_back(std::move(out1)); + })); + + /* Start the mutator thread */ + switch(mutator) { + case VOP_ALLOCATE: + ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th, + NULL)) << strerror(errno); + break; + case VOP_COPY_FILE_RANGE: + ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th, + NULL)) << strerror(errno); + break; + case VOP_SETATTR: + ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th, NULL)) + << strerror(errno); + break; + case VOP_WRITE: + ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, NULL)) + << strerror(errno); + break; + } + + + /* Wait for FUSE_SETATTR to be sent */ + sem_wait(&sem); + + /* Lookup again, which will race with the mutator */ + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + ASSERT_EQ((off_t)newsize, sb.st_size); + + /* ftruncate should've completed without error */ + pthread_join(th0, &thr0_value); + EXPECT_EQ(0, (intptr_t)thr0_value); +} + +/* + * VFS_VGET should discard attributes returned by the server if they were + * modified by another VOP while the VFS_VGET was in progress. + * + * Sequence of operations: + * * Thread 1 calls fhstat, entering VFS_VGET, and issues FUSE_LOOKUP + * * Thread 2 calls a mutator like ftruncate, which acquires the vnode lock + * exclusively and issues a FUSE op like FUSE_SETATTR. + * * Thread 1's FUSE_LOOKUP returns with the old size, but the thread blocks + * waiting for the vnode lock. + * * Thread 2's FUSE op returns, and that thread sets the file's new size + * in the attribute cache. Finally it releases the vnode lock. + * * The vnode lock acquired, thread 1 must not overwrite the attr cache's size + * with the old value. + * + * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071 + */ +TEST_P(LastLocalModify, vfs_vget) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + Sequence seq; + uint64_t ino = 3; + uint64_t lookup_unique; + const uint64_t oldsize = 10; + const uint64_t newsize = 15; + pthread_t th0; + void *thr0_value; + struct stat sb; + static sem_t sem; + fhandle_t fhp; + Mutator mutator; + uint32_t mutator_op; + + if (geteuid() != 0) + GTEST_SKIP() << "This test requires a privileged user"; + + mutator = writer_from_str(GetParam()); + mutator_op = fuse_op_from_mutator(mutator); + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .Times(1) + .InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) + { + /* Called by getfh, caches attributes but not entries */ + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = ino; + out.body.entry.attr.size = oldsize; + out.body.entry.attr_valid_nsec = NAP_NS / 2; + out.body.entry.attr.ino = ino; + out.body.entry.attr.mode = S_IFREG | 0644; + }))); + EXPECT_LOOKUP(ino, ".") + .InSequence(seq) + .WillOnce(Invoke([&](auto in, auto &out __unused) { + /* Called by fhstat. Block to simulate a race */ + lookup_unique = in.header.unique; + sem_post(&sem); + })); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .Times(1) + .InSequence(seq) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) + { + /* Called by VOP_SETATTR, caches attributes but not entries */ + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = ino; + out.body.entry.attr.size = oldsize; + out.body.entry.attr_valid_nsec = NAP_NS / 2; + out.body.entry.attr.ino = ino; + out.body.entry.attr.mode = S_IFREG | 0644; + }))); + + /* Called by the mutator thread */ + expect_open(ino, 0, 1); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == mutator_op && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke([&](auto in __unused, auto& out) { + std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out); + std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out); + + /* First complete the lookup request, returning the old size */ + out0->header.unique = lookup_unique; + SET_OUT_HEADER_LEN(*out0, entry); + out0->body.entry.attr.mode = S_IFREG | 0644; + out0->body.entry.nodeid = ino; + out0->body.entry.attr.ino = ino; + out0->body.entry.entry_valid = UINT64_MAX; + out0->body.entry.attr_valid = UINT64_MAX; + out0->body.entry.attr.size = oldsize; + out.push_back(std::move(out0)); + + /* Then, respond to the mutator request */ + out1->header.unique = in.header.unique; + switch(mutator) { + case VOP_ALLOCATE: + out1->header.error = 0; + out1->header.len = sizeof(out1->header); + break; + case VOP_COPY_FILE_RANGE: + SET_OUT_HEADER_LEN(*out1, write); + out1->body.write.size = in.body.copy_file_range.len; + break; + case VOP_SETATTR: + SET_OUT_HEADER_LEN(*out1, attr); + out1->body.attr.attr.ino = ino; + out1->body.attr.attr.mode = S_IFREG | 0644; + out1->body.attr.attr.size = newsize; // Changed size + out1->body.attr.attr_valid = UINT64_MAX; + break; + case VOP_WRITE: + SET_OUT_HEADER_LEN(*out1, write); + out1->body.write.size = in.body.write.size; + break; + } + out.push_back(std::move(out1)); + })); + + /* First get a file handle */ + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + + /* Start the mutator thread */ + switch(mutator) { + case VOP_ALLOCATE: + ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th, + (void*)&sem)) << strerror(errno); + break; + case VOP_COPY_FILE_RANGE: + ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th, + (void*)&sem)) << strerror(errno); + break; + case VOP_SETATTR: + ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th, + (void*)&sem)) << strerror(errno); + break; + case VOP_WRITE: + ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, (void*)&sem)) + << strerror(errno); + break; + } + + /* Lookup again, which will race with setattr */ + ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); + + ASSERT_EQ((off_t)newsize, sb.st_size); + + /* mutator should've completed without error */ + pthread_join(th0, &thr0_value); + EXPECT_EQ(0, (intptr_t)thr0_value); +} + + +INSTANTIATE_TEST_SUITE_P(LLM, LastLocalModify, + Values( + "VOP_ALLOCATE", + "VOP_COPY_FILE_RANGE", + "VOP_SETATTR", + "VOP_WRITE" + ) +); diff --git a/tests/sys/fs/fusefs/link.cc b/tests/sys/fs/fusefs/link.cc new file mode 100644 index 000000000000..ac77347cd03b --- /dev/null +++ b/tests/sys/fs/fusefs/link.cc @@ -0,0 +1,280 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Link: public FuseTest { +public: +void expect_link(uint64_t ino, const char *relpath, mode_t mode, uint32_t nlink) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(struct fuse_link_in); + return (in.header.opcode == FUSE_LINK && + in.body.link.oldnodeid == ino && + (0 == strcmp(name, relpath))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = ino; + out.body.entry.attr.mode = mode; + out.body.entry.attr.nlink = nlink; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); +} + +void expect_lookup(const char *relpath, uint64_t ino) +{ + FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1); +} +}; + +class Link_7_8: public FuseTest { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + FuseTest::SetUp(); +} + +void expect_link(uint64_t ino, const char *relpath, mode_t mode, uint32_t nlink) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(struct fuse_link_in); + return (in.header.opcode == FUSE_LINK && + in.body.link.oldnodeid == ino && + (0 == strcmp(name, relpath))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out.body.entry.nodeid = ino; + out.body.entry.attr.mode = mode; + out.body.entry.attr.nlink = nlink; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); +} + +void expect_lookup(const char *relpath, uint64_t ino) +{ + FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, 0, 1); +} +}; + +/* + * A successful link should clear the parent directory's attribute cache, + * because the fuse daemon should update its mtime and ctime + */ +TEST_F(Link, clear_attr_cache) +{ + const char FULLPATH[] = "mountpoint/src"; + const char RELPATH[] = "src"; + const char FULLDST[] = "mountpoint/dst"; + const char RELDST[] = "dst"; + const uint64_t ino = 42; + mode_t mode = S_IFREG | 0644; + struct stat sb; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == FUSE_ROOT_ID); + }, Eq(true)), + _) + ).Times(2) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = FUSE_ROOT_ID; + out.body.attr.attr.mode = S_IFDIR | 0755; + out.body.attr.attr_valid = UINT64_MAX; + }))); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) + + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + expect_link(ino, RELPATH, mode, 2); + + EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); + EXPECT_EQ(0, link(FULLDST, FULLPATH)) << strerror(errno); + EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); +} + +TEST_F(Link, emlink) +{ + const char FULLPATH[] = "mountpoint/lnk"; + const char RELPATH[] = "lnk"; + const char FULLDST[] = "mountpoint/dst"; + const char RELDST[] = "dst"; + uint64_t dst_ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_lookup(RELDST, dst_ino); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(struct fuse_link_in); + return (in.header.opcode == FUSE_LINK && + in.body.link.oldnodeid == dst_ino && + (0 == strcmp(name, RELPATH))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(EMLINK))); + + EXPECT_EQ(-1, link(FULLDST, FULLPATH)); + EXPECT_EQ(EMLINK, errno); +} + +/* + * A hard link should always have the same inode as its source. If it doesn't, + * then it's not a hard link. + */ +TEST_F(Link, bad_inode) +{ + const char FULLPATH[] = "mountpoint/src"; + const char RELPATH[] = "src"; + const char FULLDST[] = "mountpoint/dst"; + const char RELDST[] = "dst"; + const uint64_t src_ino = 42; + const uint64_t dst_ino = 43; + mode_t mode = S_IFREG | 0644; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = dst_ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(struct fuse_link_in); + return (in.header.opcode == FUSE_LINK && + in.body.link.oldnodeid == dst_ino && + (0 == strcmp(name, RELPATH))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = src_ino; + out.body.entry.attr.mode = mode; + out.body.entry.attr.nlink = 2; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + ASSERT_EQ(-1, link(FULLDST, FULLPATH)); + ASSERT_EQ(EIO, errno); +} + +TEST_F(Link, ok) +{ + const char FULLPATH[] = "mountpoint/src"; + const char RELPATH[] = "src"; + const char FULLDST[] = "mountpoint/dst"; + const char RELDST[] = "dst"; + const uint64_t ino = 42; + mode_t mode = S_IFREG | 0644; + struct stat sb; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + expect_link(ino, RELPATH, mode, 2); + + ASSERT_EQ(0, link(FULLDST, FULLPATH)) << strerror(errno); + // Check that the original file's nlink count has increased. + ASSERT_EQ(0, stat(FULLDST, &sb)) << strerror(errno); + EXPECT_EQ(2ul, sb.st_nlink); +} + +TEST_F(Link_7_8, ok) +{ + const char FULLPATH[] = "mountpoint/src"; + const char RELPATH[] = "src"; + const char FULLDST[] = "mountpoint/dst"; + const char RELDST[] = "dst"; + const uint64_t ino = 42; + mode_t mode = S_IFREG | 0644; + struct stat sb; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + expect_link(ino, RELPATH, mode, 2); + + ASSERT_EQ(0, link(FULLDST, FULLPATH)) << strerror(errno); + // Check that the original file's nlink count has increased. + ASSERT_EQ(0, stat(FULLDST, &sb)) << strerror(errno); + EXPECT_EQ(2ul, sb.st_nlink); +} diff --git a/tests/sys/fs/fusefs/locks.cc b/tests/sys/fs/fusefs/locks.cc new file mode 100644 index 000000000000..222217c3cfbe --- /dev/null +++ b/tests/sys/fs/fusefs/locks.cc @@ -0,0 +1,730 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/file.h> +#include <fcntl.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +/* This flag value should probably be defined in fuse_kernel.h */ +#define OFFSET_MAX 0x7fffffffffffffffLL + +using namespace testing; + +/* For testing filesystems without posix locking support */ +class Fallback: public FuseTest { +public: + +void expect_lookup(const char *relpath, uint64_t ino, uint64_t size = 0) +{ + FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, size, 1); +} + +}; + +/* For testing filesystems with posix locking support */ +class Locks: public Fallback { + virtual void SetUp() { + m_init_flags = FUSE_POSIX_LOCKS; + Fallback::SetUp(); + } +}; + +class Fcntl: public Locks { +public: +void expect_setlk(uint64_t ino, pid_t pid, uint64_t start, uint64_t end, + uint32_t type, int err) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETLK && + in.header.nodeid == ino && + in.body.setlk.fh == FH && + in.body.setlk.owner == (uint32_t)pid && + in.body.setlk.lk.start == start && + in.body.setlk.lk.end == end && + in.body.setlk.lk.type == type && + in.body.setlk.lk.pid == (uint64_t)pid); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(err))); +} +void expect_setlkw(uint64_t ino, pid_t pid, uint64_t start, uint64_t end, + uint32_t type, int err) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETLKW && + in.header.nodeid == ino && + in.body.setlkw.fh == FH && + in.body.setlkw.owner == (uint32_t)pid && + in.body.setlkw.lk.start == start && + in.body.setlkw.lk.end == end && + in.body.setlkw.lk.type == type && + in.body.setlkw.lk.pid == (uint64_t)pid); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(err))); +} +}; + +class Flock: public Locks { +public: +void expect_setlk(uint64_t ino, uint32_t type, int err) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETLK && + in.header.nodeid == ino && + in.body.setlk.fh == FH && + /* + * The owner should be set to the address of + * the vnode. That's hard to verify. + */ + /* in.body.setlk.owner == ??? && */ + in.body.setlk.lk.type == type); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(err))); +} +}; + +class FlockFallback: public Fallback {}; +class GetlkFallback: public Fallback {}; +class Getlk: public Fcntl {}; +class SetlkFallback: public Fallback {}; +class Setlk: public Fcntl {}; +class SetlkwFallback: public Fallback {}; +class Setlkw: public Fcntl {}; + +/* + * If the fuse filesystem does not support flock locks, then the kernel should + * fall back to local locks. + */ +TEST_F(FlockFallback, local) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, flock(fd, LOCK_EX)) << strerror(errno); + leak(fd); +} + +/* + * Even if the fuse file system supports POSIX locks, we must implement flock + * locks locally until protocol 7.17. Protocol 7.9 added partial buggy support + * but we won't implement that. + */ +TEST_F(Flock, local) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, flock(fd, LOCK_EX)) << strerror(errno); + leak(fd); +} + +/* Set a new flock lock with FUSE_SETLK */ +/* TODO: enable after upgrading to protocol 7.17 */ +TEST_F(Flock, DISABLED_set) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_setlk(ino, F_WRLCK, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, flock(fd, LOCK_EX)) << strerror(errno); + leak(fd); +} + +/* Fail to set a flock lock in non-blocking mode */ +/* TODO: enable after upgrading to protocol 7.17 */ +TEST_F(Flock, DISABLED_eagain) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_setlk(ino, F_WRLCK, EAGAIN); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_NE(0, flock(fd, LOCK_EX | LOCK_NB)); + ASSERT_EQ(EAGAIN, errno); + leak(fd); +} + +/* + * If the fuse filesystem does not support posix file locks, then the kernel + * should fall back to local locks. + */ +TEST_F(GetlkFallback, local) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + struct flock fl; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + fl.l_start = 10; + fl.l_len = 1000; + fl.l_pid = 0; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_SET; + fl.l_sysid = 0; + ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno); + leak(fd); +} + +/* + * If the filesystem has no locks that fit the description, the filesystem + * should return F_UNLCK + */ +TEST_F(Getlk, no_locks) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + struct flock fl; + int fd; + pid_t pid = getpid(); + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETLK && + in.header.nodeid == ino && + in.body.getlk.fh == FH && + /* + * Though it seems useless, libfuse expects the + * owner and pid fields to be set during + * FUSE_GETLK. + */ + in.body.getlk.owner == (uint32_t)pid && + in.body.getlk.lk.pid == (uint64_t)pid && + in.body.getlk.lk.start == 10 && + in.body.getlk.lk.end == 1009 && + in.body.getlk.lk.type == F_RDLCK); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { + SET_OUT_HEADER_LEN(out, getlk); + out.body.getlk.lk = in.body.getlk.lk; + out.body.getlk.lk.type = F_UNLCK; + }))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + fl.l_start = 10; + fl.l_len = 1000; + fl.l_pid = 42; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_SET; + fl.l_sysid = 42; + ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno); + + /* + * If no lock is found that would prevent this lock from being created, + * the structure is left unchanged by this system call except for the + * lock type which is set to F_UNLCK. + */ + ASSERT_EQ(F_UNLCK, fl.l_type); + ASSERT_EQ(fl.l_pid, 42); + ASSERT_EQ(fl.l_start, 10); + ASSERT_EQ(fl.l_len, 1000); + ASSERT_EQ(fl.l_whence, SEEK_SET); + ASSERT_EQ(fl.l_sysid, 42); + + leak(fd); +} + +/* A different pid does have a lock */ +TEST_F(Getlk, lock_exists) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + struct flock fl; + int fd; + pid_t pid = getpid(); + pid_t pid2 = 1235; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETLK && + in.header.nodeid == ino && + in.body.getlk.fh == FH && + /* + * Though it seems useless, libfuse expects the + * owner and pid fields to be set during + * FUSE_GETLK. + */ + in.body.getlk.owner == (uint32_t)pid && + in.body.getlk.lk.pid == (uint64_t)pid && + in.body.getlk.lk.start == 10 && + in.body.getlk.lk.end == 1009 && + in.body.getlk.lk.type == F_RDLCK); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, getlk); + out.body.getlk.lk.start = 100; + out.body.getlk.lk.end = 199; + out.body.getlk.lk.type = F_WRLCK; + out.body.getlk.lk.pid = (uint32_t)pid2;; + }))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + fl.l_start = 10; + fl.l_len = 1000; + fl.l_pid = 0; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_SET; + fl.l_sysid = 0; + ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno); + EXPECT_EQ(100, fl.l_start); + EXPECT_EQ(100, fl.l_len); + EXPECT_EQ(pid2, fl.l_pid); + EXPECT_EQ(F_WRLCK, fl.l_type); + EXPECT_EQ(SEEK_SET, fl.l_whence); + EXPECT_EQ(0, fl.l_sysid); + leak(fd); +} + +/* + * F_GETLK with SEEK_CUR + */ +TEST_F(Getlk, seek_cur) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + struct flock fl; + int fd; + pid_t pid = getpid(); + + expect_lookup(RELPATH, ino, 1024); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETLK && + in.header.nodeid == ino && + in.body.getlk.fh == FH && + /* + * Though it seems useless, libfuse expects the + * owner and pid fields to be set during + * FUSE_GETLK. + */ + in.body.getlk.owner == (uint32_t)pid && + in.body.getlk.lk.pid == (uint64_t)pid && + in.body.getlk.lk.start == 500 && + in.body.getlk.lk.end == 509 && + in.body.getlk.lk.type == F_RDLCK); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, getlk); + out.body.getlk.lk.start = 400; + out.body.getlk.lk.end = 499; + out.body.getlk.lk.type = F_WRLCK; + out.body.getlk.lk.pid = (uint32_t)pid + 1; + }))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_NE(-1, lseek(fd, 500, SEEK_SET)); + + fl.l_start = 0; + fl.l_len = 10; + fl.l_pid = 42; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_CUR; + fl.l_sysid = 0; + ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno); + + /* + * After a successful F_GETLK request, the value of l_whence is + * SEEK_SET. + */ + EXPECT_EQ(F_WRLCK, fl.l_type); + EXPECT_EQ(fl.l_pid, pid + 1); + EXPECT_EQ(fl.l_start, 400); + EXPECT_EQ(fl.l_len, 100); + EXPECT_EQ(fl.l_whence, SEEK_SET); + ASSERT_EQ(fl.l_sysid, 0); + + leak(fd); +} + +/* + * F_GETLK with SEEK_END + */ +TEST_F(Getlk, seek_end) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + struct flock fl; + int fd; + pid_t pid = getpid(); + + expect_lookup(RELPATH, ino, 1024); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETLK && + in.header.nodeid == ino && + in.body.getlk.fh == FH && + /* + * Though it seems useless, libfuse expects the + * owner and pid fields to be set during + * FUSE_GETLK. + */ + in.body.getlk.owner == (uint32_t)pid && + in.body.getlk.lk.pid == (uint64_t)pid && + in.body.getlk.lk.start == 512 && + in.body.getlk.lk.end == 1023 && + in.body.getlk.lk.type == F_RDLCK); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, getlk); + out.body.getlk.lk.start = 400; + out.body.getlk.lk.end = 499; + out.body.getlk.lk.type = F_WRLCK; + out.body.getlk.lk.pid = (uint32_t)pid + 1; + }))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_NE(-1, lseek(fd, 500, SEEK_SET)); + + fl.l_start = -512; + fl.l_len = 512; + fl.l_pid = 42; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_END; + fl.l_sysid = 0; + ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno); + + /* + * After a successful F_GETLK request, the value of l_whence is + * SEEK_SET. + */ + EXPECT_EQ(F_WRLCK, fl.l_type); + EXPECT_EQ(fl.l_pid, pid + 1); + EXPECT_EQ(fl.l_start, 400); + EXPECT_EQ(fl.l_len, 100); + EXPECT_EQ(fl.l_whence, SEEK_SET); + ASSERT_EQ(fl.l_sysid, 0); + + leak(fd); +} + +/* + * If the fuse filesystem does not support posix file locks, then the kernel + * should fall back to local locks. + */ +TEST_F(SetlkFallback, local) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + struct flock fl; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + fl.l_start = 10; + fl.l_len = 1000; + fl.l_pid = getpid(); + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_SET; + fl.l_sysid = 0; + ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno); + leak(fd); +} + +/* Clear a lock with FUSE_SETLK */ +TEST_F(Setlk, clear) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + struct flock fl; + int fd; + pid_t pid = getpid(); + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_setlk(ino, pid, 10, 1009, F_UNLCK, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + fl.l_start = 10; + fl.l_len = 1000; + fl.l_pid = 0; + fl.l_type = F_UNLCK; + fl.l_whence = SEEK_SET; + fl.l_sysid = 0; + ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno); + leak(fd); +} + +/* Set a new lock with FUSE_SETLK */ +TEST_F(Setlk, set) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + struct flock fl; + int fd; + pid_t pid = getpid(); + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_setlk(ino, pid, 10, 1009, F_RDLCK, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + fl.l_start = 10; + fl.l_len = 1000; + fl.l_pid = 0; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_SET; + fl.l_sysid = 0; + ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno); + leak(fd); +} + +/* l_len = 0 is a flag value that means to lock until EOF */ +TEST_F(Setlk, set_eof) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + struct flock fl; + int fd; + pid_t pid = getpid(); + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_setlk(ino, pid, 10, OFFSET_MAX, F_RDLCK, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + fl.l_start = 10; + fl.l_len = 0; + fl.l_pid = 0; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_SET; + fl.l_sysid = 0; + ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno); + leak(fd); +} + +/* Set a new lock with FUSE_SETLK, using SEEK_CUR for l_whence */ +TEST_F(Setlk, set_seek_cur) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + struct flock fl; + int fd; + pid_t pid = getpid(); + + expect_lookup(RELPATH, ino, 1024); + expect_open(ino, 0, 1); + expect_setlk(ino, pid, 500, 509, F_RDLCK, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_NE(-1, lseek(fd, 500, SEEK_SET)); + + fl.l_start = 0; + fl.l_len = 10; + fl.l_pid = 0; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_CUR; + fl.l_sysid = 0; + ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno); + + leak(fd); +} + +/* Set a new lock with FUSE_SETLK, using SEEK_END for l_whence */ +TEST_F(Setlk, set_seek_end) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + struct flock fl; + int fd; + pid_t pid = getpid(); + + expect_lookup(RELPATH, ino, 1024); + expect_open(ino, 0, 1); + expect_setlk(ino, pid, 1000, 1009, F_RDLCK, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + fl.l_start = -24; + fl.l_len = 10; + fl.l_pid = 0; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_END; + fl.l_sysid = 0; + ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno); + + leak(fd); +} + +/* Fail to set a new lock with FUSE_SETLK due to a conflict */ +TEST_F(Setlk, eagain) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + struct flock fl; + int fd; + pid_t pid = getpid(); + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_setlk(ino, pid, 10, 1009, F_RDLCK, EAGAIN); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + fl.l_start = 10; + fl.l_len = 1000; + fl.l_pid = 0; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_SET; + fl.l_sysid = 0; + ASSERT_EQ(-1, fcntl(fd, F_SETLK, &fl)); + ASSERT_EQ(EAGAIN, errno); + leak(fd); +} + +/* + * If the fuse filesystem does not support posix file locks, then the kernel + * should fall back to local locks. + */ +TEST_F(SetlkwFallback, local) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + struct flock fl; + int fd; + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + fl.l_start = 10; + fl.l_len = 1000; + fl.l_pid = 0; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_SET; + fl.l_sysid = 0; + ASSERT_NE(-1, fcntl(fd, F_SETLKW, &fl)) << strerror(errno); + leak(fd); +} + +/* + * Set a new lock with FUSE_SETLK. If the lock is not available, then the + * command should block. But to the kernel, that's the same as just being + * slow, so we don't need a separate test method + */ +TEST_F(Setlkw, set) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + struct flock fl; + int fd; + pid_t pid = getpid(); + + expect_lookup(RELPATH, ino); + expect_open(ino, 0, 1); + expect_setlkw(ino, pid, 10, 1009, F_RDLCK, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + fl.l_start = 10; + fl.l_len = 1000; + fl.l_pid = 0; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_SET; + fl.l_sysid = 0; + ASSERT_NE(-1, fcntl(fd, F_SETLKW, &fl)) << strerror(errno); + leak(fd); +} diff --git a/tests/sys/fs/fusefs/lookup.cc b/tests/sys/fs/fusefs/lookup.cc new file mode 100644 index 000000000000..2cfe888b6b08 --- /dev/null +++ b/tests/sys/fs/fusefs/lookup.cc @@ -0,0 +1,662 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> +#include <sys/mount.h> + +#include <fcntl.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Lookup: public FuseTest {}; + +class Lookup_7_8: public Lookup { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + Lookup::SetUp(); +} +}; + +class LookupExportable: public Lookup { +public: +virtual void SetUp() { + m_init_flags = FUSE_EXPORT_SUPPORT; + Lookup::SetUp(); +} +}; + +/* + * If lookup returns a non-zero cache timeout, then subsequent VOP_GETATTRs + * should use the cached attributes, rather than query the daemon + */ +TEST_F(Lookup, attr_cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const uint64_t generation = 13; + struct stat sb; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.attr.ino = ino; // Must match nodeid + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.attr.size = 1; + out.body.entry.attr.blocks = 2; + out.body.entry.attr.atime = 3; + out.body.entry.attr.mtime = 4; + out.body.entry.attr.ctime = 5; + out.body.entry.attr.atimensec = 6; + out.body.entry.attr.mtimensec = 7; + out.body.entry.attr.ctimensec = 8; + out.body.entry.attr.nlink = 9; + out.body.entry.attr.uid = 10; + out.body.entry.attr.gid = 11; + out.body.entry.attr.rdev = 12; + out.body.entry.generation = generation; + }))); + /* stat(2) issues a VOP_LOOKUP followed by a VOP_GETATTR */ + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + EXPECT_EQ(1, sb.st_size); + EXPECT_EQ(2, sb.st_blocks); + EXPECT_EQ(3, sb.st_atim.tv_sec); + EXPECT_EQ(6, sb.st_atim.tv_nsec); + EXPECT_EQ(4, sb.st_mtim.tv_sec); + EXPECT_EQ(7, sb.st_mtim.tv_nsec); + EXPECT_EQ(5, sb.st_ctim.tv_sec); + EXPECT_EQ(8, sb.st_ctim.tv_nsec); + EXPECT_EQ(9ull, sb.st_nlink); + EXPECT_EQ(10ul, sb.st_uid); + EXPECT_EQ(11ul, sb.st_gid); + EXPECT_EQ(12ul, sb.st_rdev); + EXPECT_EQ(ino, sb.st_ino); + EXPECT_EQ(S_IFREG | 0644, sb.st_mode); + + // fuse(4) does not _yet_ support inode generations + //EXPECT_EQ(generation, sb.st_gen); + + /* + * st_birthtim and st_flags are not supported by the fuse protocol. + * They're only supported as OS-specific extensions to OSX. For + * birthtime, the convention for "not supported" is "negative one + * second". + */ + EXPECT_EQ(-1, sb.st_birthtim.tv_sec); + EXPECT_EQ(0, sb.st_birthtim.tv_nsec); + EXPECT_EQ(0u, sb.st_flags); +} + +/* + * If lookup returns a finite but non-zero cache timeout, then we should discard + * the cached attributes and requery the daemon. + */ +TEST_F(Lookup, attr_cache_timeout) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + struct stat sb; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .Times(2) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = ino; + out.body.entry.attr_valid_nsec = NAP_NS / 2; + out.body.entry.attr.ino = ino; // Must match nodeid + out.body.entry.attr.mode = S_IFREG | 0644; + }))); + + /* access(2) will issue a VOP_LOOKUP and fill the attr cache */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + /* Next access(2) will use the cached attributes */ + nap(); + /* The cache has timed out; VOP_GETATTR should query the daemon*/ + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); +} + +TEST_F(Lookup, dot) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + /* + * access(2) is one of the few syscalls that will not (always) follow + * up a successful VOP_LOOKUP with another VOP. + */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); +} + +TEST_F(Lookup, dotdot) +{ + const char FULLPATH[] = "mountpoint/some_dir/.."; + const char RELDIRPATH[] = "some_dir"; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = 14; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + /* + * access(2) is one of the few syscalls that will not (always) follow + * up a successful VOP_LOOKUP with another VOP. + */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); +} + +/* + * Lookup ".." when that vnode's entry cache has timed out, but its child's + * hasn't. Since this file system doesn't set FUSE_EXPORT_SUPPORT, we have no + * choice but to use the cached entry, even though it expired. + */ +TEST_F(Lookup, dotdot_entry_cache_timeout) +{ + uint64_t foo_ino = 42; + uint64_t bar_ino = 43; + + EXPECT_LOOKUP(FUSE_ROOT_ID, "foo") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = foo_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; // immediate timeout + }))); + EXPECT_LOOKUP(foo_ino, "bar") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = bar_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + expect_opendir(bar_ino); + + int fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno); +} + +/* + * Lookup ".." for a vnode with no valid parent nid + * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259974 + * Since the file system is not exportable, we have no choice but to return an + * error. + */ +TEST_F(Lookup, dotdot_no_parent_nid) +{ + uint64_t foo_ino = 42; + uint64_t bar_ino = 43; + int fd; + + EXPECT_LOOKUP(FUSE_ROOT_ID, "foo") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = foo_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + EXPECT_LOOKUP(foo_ino, "bar") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = bar_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPENDIR); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, open); + }))); + expect_forget(foo_ino, 1, NULL); + + fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + // Try (and fail) to unmount the file system, to reclaim the mountpoint + // and foo vnodes. + ASSERT_NE(0, unmount("mountpoint", 0)); + EXPECT_EQ(EBUSY, errno); + nap(); // Because vnode reclamation is asynchronous + EXPECT_NE(0, faccessat(fd, "../..", F_OK, 0)); + EXPECT_EQ(ESTALE, errno); +} + +/* + * A daemon that returns an illegal error value should be handled gracefully. + * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263220 + */ +TEST_F(Lookup, ejustreturn) +{ + const char FULLPATH[] = "mountpoint/does_not_exist"; + const char RELPATH[] = "does_not_exist"; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + out.header.len = sizeof(out.header); + out.header.error = 2; + out.expected_errno = EINVAL; + }))); + + EXPECT_NE(0, access(FULLPATH, F_OK)); + + EXPECT_EQ(EIO, errno); +} + +TEST_F(Lookup, enoent) +{ + const char FULLPATH[] = "mountpoint/does_not_exist"; + const char RELPATH[] = "does_not_exist"; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + EXPECT_NE(0, access(FULLPATH, F_OK)); + EXPECT_EQ(ENOENT, errno); +} + +TEST_F(Lookup, enotdir) +{ + const char FULLPATH[] = "mountpoint/not_a_dir/some_file.txt"; + const char RELPATH[] = "not_a_dir"; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = 42; + }))); + + ASSERT_EQ(-1, access(FULLPATH, F_OK)); + ASSERT_EQ(ENOTDIR, errno); +} + +/* + * If lookup returns a non-zero entry timeout, then subsequent VOP_LOOKUPs + * should use the cached inode rather than requery the daemon + */ +TEST_F(Lookup, entry_cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = 14; + }))); + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + /* The second access(2) should use the cache */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); +} + +/* + * If the daemon returns an error of 0 and an inode of 0, that's a flag for + * "ENOENT and cache it" with the given entry_timeout + */ +TEST_F(Lookup, entry_cache_negative) +{ + struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0}; + + EXPECT_LOOKUP(FUSE_ROOT_ID, "does_not_exist") + .Times(1) + .WillOnce(Invoke(ReturnNegativeCache(&entry_valid))); + + EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK)); + EXPECT_EQ(ENOENT, errno); + EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK)); + EXPECT_EQ(ENOENT, errno); +} + +/* Negative entry caches should timeout, too */ +TEST_F(Lookup, entry_cache_negative_timeout) +{ + const char *RELPATH = "does_not_exist"; + const char *FULLPATH = "mountpoint/does_not_exist"; + struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = NAP_NS / 2}; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .Times(2) + .WillRepeatedly(Invoke(ReturnNegativeCache(&entry_valid))); + + EXPECT_NE(0, access(FULLPATH, F_OK)); + EXPECT_EQ(ENOENT, errno); + + nap(); + + /* The cache has timed out; VOP_LOOKUP should requery the daemon*/ + EXPECT_NE(0, access(FULLPATH, F_OK)); + EXPECT_EQ(ENOENT, errno); +} + +/* + * If lookup returns a finite but non-zero entry cache timeout, then we should + * discard the cached inode and requery the daemon + */ +TEST_F(Lookup, entry_cache_timeout) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .Times(2) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.entry_valid_nsec = NAP_NS / 2; + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = 14; + }))); + + /* access(2) will issue a VOP_LOOKUP and fill the entry cache */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + /* Next access(2) will use the cached entry */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + nap(); + /* The cache has timed out; VOP_LOOKUP should requery the daemon*/ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); +} + +TEST_F(Lookup, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = 14; + }))); + /* + * access(2) is one of the few syscalls that will not (always) follow + * up a successful VOP_LOOKUP with another VOP. + */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); +} + +/* + * Lookup in a subdirectory of the fuse mount. The naughty server returns the + * same inode for the child as for the parent. + */ +TEST_F(Lookup, parent_inode) +{ + const char FULLPATH[] = "mountpoint/some_dir/some_file.txt"; + const char DIRPATH[] = "some_dir"; + const char RELPATH[] = "some_file.txt"; + uint64_t dir_ino = 2; + + EXPECT_LOOKUP(FUSE_ROOT_ID, DIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = dir_ino; + }))); + EXPECT_LOOKUP(dir_ino, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = dir_ino; + }))); + /* + * access(2) is one of the few syscalls that will not (always) follow + * up a successful VOP_LOOKUP with another VOP. + */ + ASSERT_EQ(-1, access(FULLPATH, F_OK)); + ASSERT_EQ(EIO, errno); +} + +// Lookup in a subdirectory of the fuse mount +TEST_F(Lookup, subdir) +{ + const char FULLPATH[] = "mountpoint/some_dir/some_file.txt"; + const char DIRPATH[] = "some_dir"; + const char RELPATH[] = "some_file.txt"; + uint64_t dir_ino = 2; + uint64_t file_ino = 3; + + EXPECT_LOOKUP(FUSE_ROOT_ID, DIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = dir_ino; + }))); + EXPECT_LOOKUP(dir_ino, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = file_ino; + }))); + /* + * access(2) is one of the few syscalls that will not (always) follow + * up a successful VOP_LOOKUP with another VOP. + */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); +} + +/* + * The server returns two different vtypes for the same nodeid. This is + * technically allowed if the entry's cache has already expired. + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=258022 + */ +TEST_F(Lookup, vtype_conflict) +{ + const char FIRSTFULLPATH[] = "mountpoint/foo"; + const char SECONDFULLPATH[] = "mountpoint/bar"; + const char FIRSTRELPATH[] = "foo"; + const char SECONDRELPATH[] = "bar"; + uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, FIRSTRELPATH) + .WillOnce(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + }))); + expect_lookup(SECONDRELPATH, ino, S_IFREG | 0755, 0, 1, UINT64_MAX); + // VOP_FORGET happens asynchronously, so it may or may not arrive + // before the test completes. + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FORGET && + in.header.nodeid == ino && + in.body.forget.nlookup == 1); + }, Eq(true)), + _) + ).Times(AtMost(1)) + .WillOnce(Invoke([=](auto in __unused, auto &out __unused) { })); + + ASSERT_EQ(0, access(FIRSTFULLPATH, F_OK)) << strerror(errno); + EXPECT_EQ(0, access(SECONDFULLPATH, F_OK)) << strerror(errno); +} + +TEST_F(Lookup_7_8, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = 14; + }))); + /* + * access(2) is one of the few syscalls that will not (always) follow + * up a successful VOP_LOOKUP with another VOP. + */ + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); +} + +/* + * Lookup ".." when that vnode's entry cache has timed out, but its child's + * hasn't. + */ +TEST_F(LookupExportable, dotdot_entry_cache_timeout) +{ + uint64_t foo_ino = 42; + uint64_t bar_ino = 43; + + EXPECT_LOOKUP(FUSE_ROOT_ID, "foo") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = foo_ino; + out.body.entry.attr.ino = foo_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; // immediate timeout + }))); + EXPECT_LOOKUP(foo_ino, "bar") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = bar_ino; + out.body.entry.attr.ino = bar_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + expect_opendir(bar_ino); + EXPECT_LOOKUP(foo_ino, "..") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = FUSE_ROOT_ID; + out.body.entry.attr.ino = FUSE_ROOT_ID; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + int fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + /* FreeBSD's fusefs driver always uses the same cache expiration time + * for ".." as for the directory itself. So we need to look up two + * levels to find an expired ".." cache entry. + */ + EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno); +} + +/* + * Lookup ".." for a vnode with no valid parent nid + * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259974 + * Since the file system is exportable, we should resolve the problem by + * sending a FUSE_LOOKUP for "..". + */ +TEST_F(LookupExportable, dotdot_no_parent_nid) +{ + uint64_t foo_ino = 42; + uint64_t bar_ino = 43; + int fd; + + EXPECT_LOOKUP(FUSE_ROOT_ID, "foo") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = foo_ino; + out.body.entry.attr.ino = foo_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + EXPECT_LOOKUP(foo_ino, "bar") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = bar_ino; + out.body.entry.attr.ino = bar_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPENDIR); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, open); + }))); + expect_forget(foo_ino, 1, NULL); + EXPECT_LOOKUP(bar_ino, "..") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = foo_ino; + out.body.entry.attr.ino = foo_ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + EXPECT_LOOKUP(foo_ino, "..") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = FUSE_ROOT_ID; + out.body.entry.attr.ino = FUSE_ROOT_ID; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + // Try (and fail) to unmount the file system, to reclaim the mountpoint + // and foo vnodes. + ASSERT_NE(0, unmount("mountpoint", 0)); + EXPECT_EQ(EBUSY, errno); + nap(); // Because vnode reclamation is asynchronous + EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno); +} diff --git a/tests/sys/fs/fusefs/lseek.cc b/tests/sys/fs/fusefs/lseek.cc new file mode 100644 index 000000000000..12d41f7af1b2 --- /dev/null +++ b/tests/sys/fs/fusefs/lseek.cc @@ -0,0 +1,518 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2020 Alan Somers + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> + +#include <fcntl.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Lseek: public FuseTest {}; +class LseekPathconf: public Lseek {}; +class LseekPathconf_7_23: public LseekPathconf { +public: +virtual void SetUp() { + m_kernel_minor_version = 23; + FuseTest::SetUp(); +} +}; +class LseekSeekHole: public Lseek {}; +class LseekSeekData: public Lseek {}; + +/* + * If a previous lseek operation has already returned enosys, then pathconf can + * return EINVAL immediately. + */ +TEST_F(LseekPathconf, already_enosys) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + off_t offset_in = 1 << 28; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(ENOSYS))); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); + + EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA)); + EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); + EXPECT_EQ(EINVAL, errno); + + leak(fd); +} + +/* + * If a previous lseek operation has already returned successfully, then + * pathconf can return 1 immediately. 1 means "holes are reported, but size is + * not specified". + */ +TEST_F(LseekPathconf, already_seeked) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + off_t offset = 1 << 28; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto i, auto& out) { + SET_OUT_HEADER_LEN(out, lseek); + out.body.lseek.offset = i.body.lseek.offset; + }))); + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); + EXPECT_EQ(offset, lseek(fd, offset, SEEK_DATA)); + + EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); + + leak(fd); +} + +/* + * Use pathconf on a file not already opened. The server returns EACCES when + * the kernel tries to open it. The kernel should return EACCES, and make no + * judgement about whether the server does or does not support FUSE_LSEEK. + */ +TEST_F(LseekPathconf, eacces) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr.size = fsize; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(2) + .WillRepeatedly(Invoke(ReturnErrno(EACCES))); + + EXPECT_EQ(-1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE)); + EXPECT_EQ(EACCES, errno); + /* Check again, to ensure that the kernel didn't record the response */ + EXPECT_EQ(-1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE)); + EXPECT_EQ(EACCES, errno); +} + +/* + * If the server returns some weird error when we try FUSE_LSEEK, send that to + * the caller but don't record the answer. + */ +TEST_F(LseekPathconf, eio) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK); + }, Eq(true)), + _) + ).Times(2) + .WillRepeatedly(Invoke(ReturnErrno(EIO))); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); + + EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); + EXPECT_EQ(EIO, errno); + /* Check again, to ensure that the kernel didn't record the response */ + EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); + EXPECT_EQ(EIO, errno); + + leak(fd); +} + +/* + * If no FUSE_LSEEK operation has been attempted since mount, try once as soon + * as a pathconf request comes in. + */ +TEST_F(LseekPathconf, enosys_now) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(ENOSYS))); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); + + EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); + EXPECT_EQ(EINVAL, errno); + + leak(fd); +} + +/* + * Use pathconf, rather than fpathconf, on a file not already opened. + * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=278135 + */ +TEST_F(LseekPathconf, pathconf) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + off_t offset_out = 1 << 29; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, lseek); + out.body.lseek.offset = offset_out; + }))); + expect_release(ino, FuseTest::FH); + + EXPECT_EQ(1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE)) << strerror(errno); +} + +/* + * If no FUSE_LSEEK operation has been attempted since mount, try one as soon + * as a pathconf request comes in. This is the typical pattern of bsdtar. It + * will only try SEEK_HOLE/SEEK_DATA if fpathconf says they're supported. + */ +TEST_F(LseekPathconf, seek_now) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + off_t offset_initial = 1 << 27; + off_t offset_out = 1 << 29; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, lseek); + out.body.lseek.offset = offset_out; + }))); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); + EXPECT_EQ(offset_initial, lseek(fd, offset_initial, SEEK_SET)); + EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); + /* And check that the file pointer hasn't changed */ + EXPECT_EQ(offset_initial, lseek(fd, 0, SEEK_CUR)); + + leak(fd); +} + +/* + * If the user calls pathconf(_, _PC_MIN_HOLE_SIZE) on a fully sparse or + * zero-length file, then SEEK_DATA will return ENXIO. That should be + * interpreted as success. + */ +TEST_F(LseekPathconf, zerolength) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 0; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK && + in.header.nodeid == ino && + in.body.lseek.whence == SEEK_DATA); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(ENXIO))); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); + EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); + /* Check again, to ensure that the kernel recorded the response */ + EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); + + leak(fd); +} + +/* + * For servers using older protocol versions, no FUSE_LSEEK should be attempted + */ +TEST_F(LseekPathconf_7_23, already_enosys) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK); + }, Eq(true)), + _) + ).Times(0); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); + EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); + EXPECT_EQ(EINVAL, errno); + + leak(fd); +} + +TEST_F(LseekSeekData, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + off_t offset_in = 1 << 28; + off_t offset_out = 1 << 29; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK && + in.header.nodeid == ino && + in.body.lseek.fh == FH && + (off_t)in.body.lseek.offset == offset_in && + in.body.lseek.whence == SEEK_DATA); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, lseek); + out.body.lseek.offset = offset_out; + }))); + fd = open(FULLPATH, O_RDONLY); + EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_DATA)); + EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR)); + + leak(fd); +} + +/* + * If the server returns ENOSYS, fusefs should fall back to the default + * behavior, and never query the server again. + */ +TEST_F(LseekSeekData, enosys) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + off_t offset_in = 1 << 28; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK && + in.header.nodeid == ino && + in.body.lseek.fh == FH && + (off_t)in.body.lseek.offset == offset_in && + in.body.lseek.whence == SEEK_DATA); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(ENOSYS))); + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); + + /* + * Default behavior: ENXIO if offset is < 0 or >= fsize, offset + * otherwise. + */ + EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA)); + EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE)); + EXPECT_EQ(ENXIO, errno); + EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE)); + EXPECT_EQ(ENXIO, errno); + + leak(fd); +} + +TEST_F(LseekSeekHole, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + off_t offset_in = 1 << 28; + off_t offset_out = 1 << 29; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK && + in.header.nodeid == ino && + in.body.lseek.fh == FH && + (off_t)in.body.lseek.offset == offset_in && + in.body.lseek.whence == SEEK_HOLE); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, lseek); + out.body.lseek.offset = offset_out; + }))); + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); + EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_HOLE)); + EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR)); + + leak(fd); +} + +/* + * If the server returns ENOSYS, fusefs should fall back to the default + * behavior, and never query the server again. + */ +TEST_F(LseekSeekHole, enosys) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + off_t offset_in = 1 << 28; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK && + in.header.nodeid == ino && + in.body.lseek.fh == FH && + (off_t)in.body.lseek.offset == offset_in && + in.body.lseek.whence == SEEK_HOLE); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(ENOSYS))); + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); + + /* + * Default behavior: ENXIO if offset is < 0 or >= fsize, fsize + * otherwise. + */ + EXPECT_EQ(fsize, lseek(fd, offset_in, SEEK_HOLE)); + EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE)); + EXPECT_EQ(ENXIO, errno); + EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE)); + EXPECT_EQ(ENXIO, errno); + + leak(fd); +} + +/* lseek should return ENXIO when offset points to EOF */ +TEST_F(LseekSeekHole, enxio) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + off_t offset_in = fsize; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK && + in.header.nodeid == ino && + in.body.lseek.fh == FH && + (off_t)in.body.lseek.offset == offset_in && + in.body.lseek.whence == SEEK_HOLE); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(ENXIO))); + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); + EXPECT_EQ(-1, lseek(fd, offset_in, SEEK_HOLE)); + EXPECT_EQ(ENXIO, errno); + + leak(fd); +} diff --git a/tests/sys/fs/fusefs/mkdir.cc b/tests/sys/fs/fusefs/mkdir.cc new file mode 100644 index 000000000000..f020feb94ed8 --- /dev/null +++ b/tests/sys/fs/fusefs/mkdir.cc @@ -0,0 +1,274 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <fcntl.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Mkdir: public FuseTest {}; +class Mkdir_7_8: public FuseTest { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + FuseTest::SetUp(); +} +}; + +/* + * EMLINK is possible on filesystems that limit the number of hard links to a + * single file, like early versions of BtrFS + */ +TEST_F(Mkdir, emlink) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + mode_t mode = 0755; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(fuse_mkdir_in); + return (in.header.opcode == FUSE_MKDIR && + in.body.mkdir.mode == (S_IFDIR | mode) && + (0 == strcmp(RELPATH, name))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(EMLINK))); + + ASSERT_NE(1, mkdir(FULLPATH, mode)); + ASSERT_EQ(EMLINK, errno); +} + +/* + * Creating a new directory after FUSE_LOOKUP returned a negative cache entry + */ +TEST_F(Mkdir, entry_cache_negative) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + mode_t mode = 0755; + uint64_t ino = 42; + /* + * Set entry_valid = 0 because this test isn't concerned with whether + * or not we actually cache negative entries, only with whether we + * interpret negative cache responses correctly. + */ + struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0}; + + /* mkdir will first do a LOOKUP, adding a negative cache entry */ + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(ReturnNegativeCache(&entry_valid)); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(fuse_open_in); + return (in.header.opcode == FUSE_MKDIR && + in.body.mkdir.mode == (S_IFDIR | mode) && + (0 == strcmp(RELPATH, name))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.create.entry.attr.mode = S_IFDIR | mode; + out.body.create.entry.nodeid = ino; + out.body.create.entry.entry_valid = UINT64_MAX; + out.body.create.entry.attr_valid = UINT64_MAX; + }))); + + ASSERT_EQ(0, mkdir(FULLPATH, mode)) << strerror(errno); +} + +/* + * Creating a new directory should purge any negative namecache entries + */ +TEST_F(Mkdir, entry_cache_negative_purge) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + mode_t mode = 0755; + uint64_t ino = 42; + struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0}; + + /* mkdir will first do a LOOKUP, adding a negative cache entry */ + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .Times(1) + .WillOnce(Invoke(ReturnNegativeCache(&entry_valid))) + .RetiresOnSaturation(); + + /* Then the MKDIR should purge the negative cache entry */ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(fuse_open_in); + return (in.header.opcode == FUSE_MKDIR && + in.body.mkdir.mode == (S_IFDIR | mode) && + (0 == strcmp(RELPATH, name))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | mode; + out.body.entry.nodeid = ino; + out.body.entry.attr_valid = UINT64_MAX; + }))); + + ASSERT_EQ(0, mkdir(FULLPATH, mode)) << strerror(errno); + + /* Finally, a subsequent lookup should query the daemon */ + expect_lookup(RELPATH, ino, S_IFDIR | mode, 0, 1); + + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); +} + +TEST_F(Mkdir, ok) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + mode_t mode = 0755; + uint64_t ino = 42; + mode_t mask; + + mask = umask(0); + (void)umask(mask); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(fuse_mkdir_in); + return (in.header.opcode == FUSE_MKDIR && + in.body.mkdir.mode == (S_IFDIR | mode) && + in.body.mkdir.umask == mask && + (0 == strcmp(RELPATH, name))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.create.entry.attr.mode = S_IFDIR | mode; + out.body.create.entry.nodeid = ino; + out.body.create.entry.entry_valid = UINT64_MAX; + out.body.create.entry.attr_valid = UINT64_MAX; + }))); + + ASSERT_EQ(0, mkdir(FULLPATH, mode)) << strerror(errno); +} + +/* + * Nothing bad should happen if the server returns the parent's inode number + * for the newly created directory. Regression test for bug 263662. + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263662 + */ +TEST_F(Mkdir, parent_inode) +{ + const char FULLPATH[] = "mountpoint/parent/some_dir"; + const char PPATH[] = "parent"; + const char RELPATH[] = "some_dir"; + mode_t mode = 0755; + uint64_t ino = 42; + mode_t mask; + + mask = umask(0); + (void)umask(mask); + + expect_lookup(PPATH, ino, S_IFDIR | 0755, 0, 1); + EXPECT_LOOKUP(ino, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(fuse_mkdir_in); + return (in.header.opcode == FUSE_MKDIR && + in.body.mkdir.mode == (S_IFDIR | mode) && + in.body.mkdir.umask == mask && + (0 == strcmp(RELPATH, name))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.create.entry.attr.mode = S_IFDIR | mode; + out.body.create.entry.nodeid = ino; + out.body.create.entry.entry_valid = UINT64_MAX; + out.body.create.entry.attr_valid = UINT64_MAX; + }))); + // FUSE_FORGET happens asynchronously, so it may or may not arrive + // before the test completes. + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FORGET); + }, Eq(true)), + _) + ).Times(AtMost(1)) + .WillOnce(Invoke([=](auto in __unused, auto &out __unused) { })); + + ASSERT_EQ(-1, mkdir(FULLPATH, mode)); + ASSERT_EQ(EIO, errno); +} + +TEST_F(Mkdir_7_8, ok) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + mode_t mode = 0755; + uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(fuse_mkdir_in); + return (in.header.opcode == FUSE_MKDIR && + in.body.mkdir.mode == (S_IFDIR | mode) && + (0 == strcmp(RELPATH, name))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out.body.create.entry.attr.mode = S_IFDIR | mode; + out.body.create.entry.nodeid = ino; + out.body.create.entry.entry_valid = UINT64_MAX; + out.body.create.entry.attr_valid = UINT64_MAX; + }))); + + ASSERT_EQ(0, mkdir(FULLPATH, mode)) << strerror(errno); +} diff --git a/tests/sys/fs/fusefs/mknod.cc b/tests/sys/fs/fusefs/mknod.cc new file mode 100644 index 000000000000..eb745f19acd5 --- /dev/null +++ b/tests/sys/fs/fusefs/mknod.cc @@ -0,0 +1,318 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <fcntl.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <semaphore.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +#ifndef VNOVAL +#define VNOVAL (-1) /* Defined in sys/vnode.h */ +#endif + +class Mknod: public FuseTest { + +mode_t m_oldmask; +const static mode_t c_umask = 022; + +public: + +Mknod() { + m_oldmask = umask(c_umask); +} + +virtual void SetUp() { + if (geteuid() != 0) { + GTEST_SKIP() << "Only root may use most mknod(2) variations"; + } + FuseTest::SetUp(); +} + +virtual void TearDown() { + FuseTest::TearDown(); + (void)umask(m_oldmask); +} + +/* Test an OK creation of a file with the given mode and device number */ +void expect_mknod(uint64_t parent_ino, const char* relpath, uint64_t ino, + mode_t mode, dev_t dev) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(fuse_mknod_in); + return (in.header.nodeid == parent_ino && + in.header.opcode == FUSE_MKNOD && + in.body.mknod.mode == mode && + in.body.mknod.rdev == (uint32_t)dev && + in.body.mknod.umask == c_umask && + (0 == strcmp(relpath, name))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.attr.rdev = dev; + }))); +} + +}; + +class Mknod_7_11: public FuseTest { +public: +virtual void SetUp() { + m_kernel_minor_version = 11; + if (geteuid() != 0) { + GTEST_SKIP() << "Only root may use most mknod(2) variations"; + } + FuseTest::SetUp(); +} + +void expect_lookup(const char *relpath, uint64_t ino, uint64_t size) +{ + FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, size, 1); +} + +/* Test an OK creation of a file with the given mode and device number */ +void expect_mknod(uint64_t parent_ino, const char* relpath, uint64_t ino, + mode_t mode, dev_t dev) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + FUSE_COMPAT_MKNOD_IN_SIZE; + return (in.header.nodeid == parent_ino && + in.header.opcode == FUSE_MKNOD && + in.body.mknod.mode == mode && + in.body.mknod.rdev == (uint32_t)dev && + (0 == strcmp(relpath, name))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.attr.rdev = dev; + }))); +} + +}; + +/* + * mknod(2) should be able to create block devices on a FUSE filesystem. Even + * though FreeBSD doesn't use block devices, this is useful when copying media + * from or preparing media for other operating systems. + */ +TEST_F(Mknod, blk) +{ + const char FULLPATH[] = "mountpoint/some_node"; + const char RELPATH[] = "some_node"; + mode_t mode = S_IFBLK | 0755; + dev_t rdev = 0xfe00; /* /dev/vda's device number on Linux */ + uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev); + + EXPECT_EQ(0, mknod(FULLPATH, mode, rdev)) << strerror(errno); +} + +TEST_F(Mknod, chr) +{ + const char FULLPATH[] = "mountpoint/some_node"; + const char RELPATH[] = "some_node"; + mode_t mode = S_IFCHR | 0755; + dev_t rdev = 54; /* /dev/fuse's device number */ + uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev); + + EXPECT_EQ(0, mknod(FULLPATH, mode, rdev)) << strerror(errno); +} + +/* + * The daemon is responsible for checking file permissions (unless the + * default_permissions mount option was used) + */ +TEST_F(Mknod, eperm) +{ + const char FULLPATH[] = "mountpoint/some_node"; + const char RELPATH[] = "some_node"; + mode_t mode = S_IFIFO | 0755; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes + + sizeof(fuse_mknod_in); + return (in.header.opcode == FUSE_MKNOD && + in.body.mknod.mode == mode && + (0 == strcmp(RELPATH, name))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(EPERM))); + EXPECT_NE(0, mkfifo(FULLPATH, mode)); + EXPECT_EQ(EPERM, errno); +} + +TEST_F(Mknod, fifo) +{ + const char FULLPATH[] = "mountpoint/some_node"; + const char RELPATH[] = "some_node"; + mode_t mode = S_IFIFO | 0755; + dev_t rdev = VNOVAL; /* Fifos don't have device numbers */ + uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev); + + EXPECT_EQ(0, mkfifo(FULLPATH, mode)) << strerror(errno); +} + +/* + * Create a unix-domain socket. + * + * This test case doesn't actually need root privileges. + */ +TEST_F(Mknod, socket) +{ + const char FULLPATH[] = "mountpoint/some_node"; + const char RELPATH[] = "some_node"; + mode_t mode = S_IFSOCK | 0755; + struct sockaddr_un sa; + int fd; + dev_t rdev = -1; /* Really it's a don't care */ + uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev); + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, fd) << strerror(errno); + sa.sun_family = AF_UNIX; + strlcpy(sa.sun_path, FULLPATH, sizeof(sa.sun_path)); + sa.sun_len = sizeof(FULLPATH); + ASSERT_EQ(0, bind(fd, (struct sockaddr*)&sa, sizeof(sa))) + << strerror(errno); + + leak(fd); +} + +/* + * Nothing bad should happen if the server returns the parent's inode number + * for the newly created file. Regression test for bug 263662. + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263662 + */ +TEST_F(Mknod, parent_inode) +{ + const char FULLPATH[] = "mountpoint/parent/some_node"; + const char PPATH[] = "parent"; + const char RELPATH[] = "some_node"; + mode_t mode = S_IFSOCK | 0755; + struct sockaddr_un sa; + sem_t sem; + int fd; + dev_t rdev = -1; /* Really it's a don't care */ + uint64_t ino = 42; + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + expect_lookup(PPATH, ino, S_IFDIR | 0755, 0, 1); + EXPECT_LOOKUP(ino, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_mknod(ino, RELPATH, ino, mode, rdev); + expect_forget(ino, 1, &sem); + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, fd) << strerror(errno); + sa.sun_family = AF_UNIX; + strlcpy(sa.sun_path, FULLPATH, sizeof(sa.sun_path)); + sa.sun_len = sizeof(FULLPATH); + ASSERT_EQ(-1, bind(fd, (struct sockaddr*)&sa, sizeof(sa))); + ASSERT_EQ(EIO, errno); + + leak(fd); + sem_wait(&sem); + sem_destroy(&sem); +} + +/* + * fusefs(4) lacks VOP_WHITEOUT support. No bugzilla entry, because that's a + * feature, not a bug + */ +TEST_F(Mknod, DISABLED_whiteout) +{ + const char FULLPATH[] = "mountpoint/some_node"; + const char RELPATH[] = "some_node"; + mode_t mode = S_IFWHT | 0755; + dev_t rdev = VNOVAL; /* whiteouts don't have device numbers */ + uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev); + + EXPECT_EQ(0, mknod(FULLPATH, mode, 0)) << strerror(errno); +} + +/* A server built at protocol version 7.11 or earlier can still use mknod */ +TEST_F(Mknod_7_11, fifo) +{ + const char FULLPATH[] = "mountpoint/some_node"; + const char RELPATH[] = "some_node"; + mode_t mode = S_IFIFO | 0755; + dev_t rdev = VNOVAL; + uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_mknod(FUSE_ROOT_ID, RELPATH, ino, mode, rdev); + + EXPECT_EQ(0, mkfifo(FULLPATH, mode)) << strerror(errno); +} diff --git a/tests/sys/fs/fusefs/mockfs.cc b/tests/sys/fs/fusefs/mockfs.cc new file mode 100644 index 000000000000..b6a32d9b60af --- /dev/null +++ b/tests/sys/fs/fusefs/mockfs.cc @@ -0,0 +1,1063 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> + +#include <sys/mount.h> +#include <sys/select.h> +#include <sys/stat.h> +#include <sys/uio.h> +#include <sys/user.h> + +#include <fcntl.h> +#include <libutil.h> +#include <mntopts.h> // for build_iovec +#include <poll.h> +#include <pthread.h> +#include <signal.h> +#include <stdlib.h> +#include <unistd.h> +} + +#include <cinttypes> + +#include <gtest/gtest.h> + +#include "mockfs.hh" + +using namespace testing; + +int verbosity = 0; + +const char* opcode2opname(uint32_t opcode) +{ + const char* table[] = { + "Unknown (opcode 0)", + "LOOKUP", + "FORGET", + "GETATTR", + "SETATTR", + "READLINK", + "SYMLINK", + "Unknown (opcode 7)", + "MKNOD", + "MKDIR", + "UNLINK", + "RMDIR", + "RENAME", + "LINK", + "OPEN", + "READ", + "WRITE", + "STATFS", + "RELEASE", + "Unknown (opcode 19)", + "FSYNC", + "SETXATTR", + "GETXATTR", + "LISTXATTR", + "REMOVEXATTR", + "FLUSH", + "INIT", + "OPENDIR", + "READDIR", + "RELEASEDIR", + "FSYNCDIR", + "GETLK", + "SETLK", + "SETLKW", + "ACCESS", + "CREATE", + "INTERRUPT", + "BMAP", + "DESTROY", + "IOCTL", + "POLL", + "NOTIFY_REPLY", + "BATCH_FORGET", + "FALLOCATE", + "READDIRPLUS", + "RENAME2", + "LSEEK", + "COPY_FILE_RANGE", + }; + if (opcode >= nitems(table)) + return ("Unknown (opcode > max)"); + else + return (table[opcode]); +} + +ProcessMockerT +ReturnErrno(int error) +{ + return([=](auto in, auto &out) { + std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out); + out0->header.unique = in.header.unique; + out0->header.error = -error; + out0->header.len = sizeof(out0->header); + out.push_back(std::move(out0)); + }); +} + +/* Helper function used for returning negative cache entries for LOOKUP */ +ProcessMockerT +ReturnNegativeCache(const struct timespec *entry_valid) +{ + return([=](auto in, auto &out) { + /* nodeid means ENOENT and cache it */ + std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out); + out0->body.entry.nodeid = 0; + out0->header.unique = in.header.unique; + out0->header.error = 0; + out0->body.entry.entry_valid = entry_valid->tv_sec; + out0->body.entry.entry_valid_nsec = entry_valid->tv_nsec; + SET_OUT_HEADER_LEN(*out0, entry); + out.push_back(std::move(out0)); + }); +} + +ProcessMockerT +ReturnImmediate(std::function<void(const mockfs_buf_in& in, + struct mockfs_buf_out &out)> f) +{ + return([=](auto& in, auto &out) { + std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out); + out0->header.unique = in.header.unique; + f(in, *out0); + out.push_back(std::move(out0)); + }); +} + +void sigint_handler(int __unused sig) { + // Don't do anything except interrupt the daemon's read(2) call +} + +void MockFS::debug_request(const mockfs_buf_in &in, ssize_t buflen) +{ + printf("%-11s ino=%2" PRIu64, opcode2opname(in.header.opcode), + in.header.nodeid); + if (verbosity > 1) { + printf(" uid=%5u gid=%5u pid=%5u unique=%" PRIu64 " len=%u" + " buflen=%zd", + in.header.uid, in.header.gid, in.header.pid, + in.header.unique, in.header.len, buflen); + } + switch (in.header.opcode) { + const char *name, *value; + + case FUSE_ACCESS: + printf(" mask=%#x", in.body.access.mask); + break; + case FUSE_BMAP: + printf(" block=%" PRIx64 " blocksize=%#x", + in.body.bmap.block, in.body.bmap.blocksize); + break; + case FUSE_COPY_FILE_RANGE: + printf(" off_in=%" PRIu64 " ino_out=%" PRIu64 + " off_out=%" PRIu64 " size=%" PRIu64, + in.body.copy_file_range.off_in, + in.body.copy_file_range.nodeid_out, + in.body.copy_file_range.off_out, + in.body.copy_file_range.len); + if (verbosity > 1) + printf(" fh_in=%" PRIu64 " fh_out=%" PRIu64 + " flags=%" PRIx64, + in.body.copy_file_range.fh_in, + in.body.copy_file_range.fh_out, + in.body.copy_file_range.flags); + break; + case FUSE_CREATE: + if (m_kernel_minor_version >= 12) + name = (const char*)in.body.bytes + + sizeof(fuse_create_in); + else + name = (const char*)in.body.bytes + + sizeof(fuse_open_in); + printf(" flags=%#x name=%s", + in.body.open.flags, name); + break; + case FUSE_FALLOCATE: + printf(" fh=%#" PRIx64 " offset=%" PRIu64 + " length=%" PRIx64 " mode=%#x", + in.body.fallocate.fh, + in.body.fallocate.offset, + in.body.fallocate.length, + in.body.fallocate.mode); + break; + case FUSE_FLUSH: + printf(" fh=%#" PRIx64 " lock_owner=%" PRIu64, + in.body.flush.fh, + in.body.flush.lock_owner); + break; + case FUSE_FORGET: + printf(" nlookup=%" PRIu64, in.body.forget.nlookup); + break; + case FUSE_FSYNC: + printf(" flags=%#x", in.body.fsync.fsync_flags); + break; + case FUSE_FSYNCDIR: + printf(" flags=%#x", in.body.fsyncdir.fsync_flags); + break; + case FUSE_GETLK: + printf(" fh=%#" PRIx64 + " type=%u pid=%u", + in.body.getlk.fh, + in.body.getlk.lk.type, + in.body.getlk.lk.pid); + if (verbosity >= 2) { + printf(" range=[%" PRIi64 ":%" PRIi64 "]", + in.body.getlk.lk.start, + in.body.getlk.lk.end); + } + break; + case FUSE_INTERRUPT: + printf(" unique=%" PRIu64, in.body.interrupt.unique); + break; + case FUSE_LINK: + printf(" oldnodeid=%" PRIu64, in.body.link.oldnodeid); + break; + case FUSE_LISTXATTR: + printf(" size=%" PRIu32, in.body.listxattr.size); + break; + case FUSE_LOOKUP: + printf(" %s", in.body.lookup); + break; + case FUSE_LSEEK: + switch (in.body.lseek.whence) { + case SEEK_HOLE: + printf(" SEEK_HOLE offset=%jd", + in.body.lseek.offset); + break; + case SEEK_DATA: + printf(" SEEK_DATA offset=%jd", + in.body.lseek.offset); + break; + default: + printf(" whence=%u offset=%jd", + in.body.lseek.whence, in.body.lseek.offset); + break; + } + break; + case FUSE_MKDIR: + name = (const char*)in.body.bytes + + sizeof(fuse_mkdir_in); + printf(" name=%s mode=%#o umask=%#o", name, + in.body.mkdir.mode, in.body.mkdir.umask); + break; + case FUSE_MKNOD: + if (m_kernel_minor_version >= 12) + name = (const char*)in.body.bytes + + sizeof(fuse_mknod_in); + else + name = (const char*)in.body.bytes + + FUSE_COMPAT_MKNOD_IN_SIZE; + printf(" mode=%#o rdev=%x umask=%#o name=%s", + in.body.mknod.mode, in.body.mknod.rdev, + in.body.mknod.umask, name); + break; + case FUSE_OPEN: + printf(" flags=%#x", in.body.open.flags); + break; + case FUSE_OPENDIR: + printf(" flags=%#x", in.body.opendir.flags); + break; + case FUSE_READ: + printf(" offset=%" PRIu64 " size=%u", + in.body.read.offset, + in.body.read.size); + if (verbosity > 1) + printf(" flags=%#x", in.body.read.flags); + break; + case FUSE_READDIR: + printf(" fh=%#" PRIx64 " offset=%" PRIu64 " size=%u", + in.body.readdir.fh, in.body.readdir.offset, + in.body.readdir.size); + break; + case FUSE_RELEASE: + printf(" fh=%#" PRIx64 " flags=%#x lock_owner=%" PRIu64, + in.body.release.fh, + in.body.release.flags, + in.body.release.lock_owner); + break; + case FUSE_RENAME: + { + const char *src = (const char*)in.body.bytes + + sizeof(fuse_rename_in); + const char *dst = src + strlen(src) + 1; + printf(" src=%s newdir=%" PRIu64 " dst=%s", + src, in.body.rename.newdir, dst); + } + break; + case FUSE_SETATTR: + if (verbosity <= 1) { + printf(" valid=%#x", in.body.setattr.valid); + break; + } + if (in.body.setattr.valid & FATTR_MODE) + printf(" mode=%#o", in.body.setattr.mode); + if (in.body.setattr.valid & FATTR_UID) + printf(" uid=%u", in.body.setattr.uid); + if (in.body.setattr.valid & FATTR_GID) + printf(" gid=%u", in.body.setattr.gid); + if (in.body.setattr.valid & FATTR_SIZE) + printf(" size=%" PRIu64, in.body.setattr.size); + if (in.body.setattr.valid & FATTR_ATIME) + printf(" atime=%" PRIu64 ".%u", + in.body.setattr.atime, + in.body.setattr.atimensec); + if (in.body.setattr.valid & FATTR_MTIME) + printf(" mtime=%" PRIu64 ".%u", + in.body.setattr.mtime, + in.body.setattr.mtimensec); + if (in.body.setattr.valid & FATTR_FH) + printf(" fh=%" PRIu64 "", in.body.setattr.fh); + break; + case FUSE_SETLK: + printf(" fh=%#" PRIx64 " owner=%" PRIu64 + " type=%u pid=%u", + in.body.setlk.fh, in.body.setlk.owner, + in.body.setlk.lk.type, + in.body.setlk.lk.pid); + if (verbosity >= 2) { + printf(" range=[%" PRIi64 ":%" PRIi64 "]", + in.body.setlk.lk.start, + in.body.setlk.lk.end); + } + break; + case FUSE_SETXATTR: + /* + * In theory neither the xattr name and value need be + * ASCII, but in this test suite they always are. + */ + name = (const char*)in.body.bytes + + sizeof(fuse_setxattr_in); + value = name + strlen(name) + 1; + printf(" %s=%s", name, value); + break; + case FUSE_WRITE: + printf(" fh=%#" PRIx64 " offset=%" PRIu64 + " size=%u write_flags=%u", + in.body.write.fh, + in.body.write.offset, in.body.write.size, + in.body.write.write_flags); + if (verbosity > 1) + printf(" flags=%#x", in.body.write.flags); + break; + default: + break; + } + printf("\n"); +} + +/* + * Debug a FUSE response. + * + * This is mostly useful for asynchronous notifications, which don't correspond + * to any request + */ +void MockFS::debug_response(const mockfs_buf_out &out) { + const char *name; + + if (verbosity == 0) + return; + + switch (out.header.error) { + case FUSE_NOTIFY_INVAL_ENTRY: + name = (const char*)out.body.bytes + + sizeof(fuse_notify_inval_entry_out); + printf("<- INVAL_ENTRY parent=%" PRIu64 " %s\n", + out.body.inval_entry.parent, name); + break; + case FUSE_NOTIFY_INVAL_INODE: + printf("<- INVAL_INODE ino=%" PRIu64 " off=%" PRIi64 + " len=%" PRIi64 "\n", + out.body.inval_inode.ino, + out.body.inval_inode.off, + out.body.inval_inode.len); + break; + case FUSE_NOTIFY_STORE: + printf("<- STORE ino=%" PRIu64 " off=%" PRIu64 + " size=%" PRIu32 "\n", + out.body.store.nodeid, + out.body.store.offset, + out.body.store.size); + break; + default: + break; + } +} + +MockFS::MockFS(int max_read, int max_readahead, bool allow_other, + bool default_permissions, + bool push_symlinks_in, bool ro, enum poll_method pm, uint32_t flags, + uint32_t kernel_minor_version, uint32_t max_write, bool async, + bool noclusterr, unsigned time_gran, bool nointr, bool noatime, + const char *fsname, const char *subtype, bool no_auto_init) + : m_daemon_id(NULL), + m_kernel_minor_version(kernel_minor_version), + m_kq(pm == KQ ? kqueue() : -1), + m_maxread(max_read), + m_maxreadahead(max_readahead), + m_pid(getpid()), + m_uniques(new std::unordered_set<uint64_t>), + m_pm(pm), + m_time_gran(time_gran), + m_child_pid(-1), + m_maxwrite(MIN(max_write, max_max_write)), + m_nready(-1), + m_quit(false), + m_expect_unmount(false) +{ + struct sigaction sa; + struct iovec *iov = NULL; + int iovlen = 0; + char fdstr[15]; + const bool trueval = true; + + /* + * Kyua sets pwd to a testcase-unique tempdir; no need to use + * mkdtemp + */ + /* + * googletest doesn't allow ASSERT_ in constructors, so we must throw + * instead. + */ + if (mkdir("mountpoint" , 0755) && errno != EEXIST) + throw(std::system_error(errno, std::system_category(), + "Couldn't make mountpoint directory")); + + switch (m_pm) { + case BLOCKING: + m_fuse_fd = open("/dev/fuse", O_CLOEXEC | O_RDWR); + break; + default: + m_fuse_fd = open("/dev/fuse", O_CLOEXEC | O_RDWR | O_NONBLOCK); + break; + } + if (m_fuse_fd < 0) + throw(std::system_error(errno, std::system_category(), + "Couldn't open /dev/fuse")); + + build_iovec(&iov, &iovlen, "fstype", __DECONST(void *, "fusefs"), -1); + build_iovec(&iov, &iovlen, "fspath", + __DECONST(void *, "mountpoint"), -1); + build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1); + sprintf(fdstr, "%d", m_fuse_fd); + build_iovec(&iov, &iovlen, "fd", fdstr, -1); + if (m_maxread > 0) { + char val[12]; + + snprintf(val, sizeof(val), "%d", m_maxread); + build_iovec(&iov, &iovlen, "max_read=", &val, -1); + } + if (allow_other) { + build_iovec(&iov, &iovlen, "allow_other", + __DECONST(void*, &trueval), sizeof(bool)); + } + if (default_permissions) { + build_iovec(&iov, &iovlen, "default_permissions", + __DECONST(void*, &trueval), sizeof(bool)); + } + if (push_symlinks_in) { + build_iovec(&iov, &iovlen, "push_symlinks_in", + __DECONST(void*, &trueval), sizeof(bool)); + } + if (ro) { + build_iovec(&iov, &iovlen, "ro", + __DECONST(void*, &trueval), sizeof(bool)); + } + if (async) { + build_iovec(&iov, &iovlen, "async", __DECONST(void*, &trueval), + sizeof(bool)); + } + if (noatime) { + build_iovec(&iov, &iovlen, "noatime", + __DECONST(void*, &trueval), sizeof(bool)); + } + if (noclusterr) { + build_iovec(&iov, &iovlen, "noclusterr", + __DECONST(void*, &trueval), sizeof(bool)); + } + if (nointr) { + build_iovec(&iov, &iovlen, "nointr", + __DECONST(void*, &trueval), sizeof(bool)); + } else { + build_iovec(&iov, &iovlen, "intr", + __DECONST(void*, &trueval), sizeof(bool)); + } + if (*fsname) { + build_iovec(&iov, &iovlen, "fsname=", + __DECONST(void*, fsname), -1); + } + if (*subtype) { + build_iovec(&iov, &iovlen, "subtype=", + __DECONST(void*, subtype), -1); + } + if (nmount(iov, iovlen, 0)) + throw(std::system_error(errno, std::system_category(), + "Couldn't mount filesystem")); + free_iovec(&iov, &iovlen); + + // Setup default handler + ON_CALL(*this, process(_, _)) + .WillByDefault(Invoke(this, &MockFS::process_default)); + + if (!no_auto_init) + init(flags); + + bzero(&sa, sizeof(sa)); + sa.sa_handler = sigint_handler; + sa.sa_flags = 0; /* Don't set SA_RESTART! */ + if (0 != sigaction(SIGUSR1, &sa, NULL)) + throw(std::system_error(errno, std::system_category(), + "Couldn't handle SIGUSR1")); + if (pthread_create(&m_daemon_id, NULL, service, (void*)this)) + throw(std::system_error(errno, std::system_category(), + "Couldn't Couldn't start fuse thread")); +} + +MockFS::~MockFS() { + kill_daemon(); + join_daemon(); + ::unmount("mountpoint", MNT_FORCE); + rmdir("mountpoint"); + if (m_kq >= 0) + close(m_kq); +} + +void MockFS::audit_request(const mockfs_buf_in &in, ssize_t buflen) { + uint32_t inlen = in.header.len; + size_t fih = sizeof(in.header); + switch (in.header.opcode) { + case FUSE_LOOKUP: + case FUSE_RMDIR: + case FUSE_SYMLINK: + case FUSE_UNLINK: + EXPECT_GT(inlen, fih) << "Missing request filename"; + // No redundant information for checking buflen + break; + case FUSE_FORGET: + EXPECT_EQ(inlen, fih + sizeof(in.body.forget)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_GETATTR: + EXPECT_EQ(inlen, fih + sizeof(in.body.getattr)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_SETATTR: + EXPECT_EQ(inlen, fih + sizeof(in.body.setattr)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_READLINK: + EXPECT_EQ(inlen, fih) << "Unexpected request body"; + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_MKNOD: + { + size_t s; + if (m_kernel_minor_version >= 12) + s = sizeof(in.body.mknod); + else + s = FUSE_COMPAT_MKNOD_IN_SIZE; + EXPECT_GE(inlen, fih + s) << "Missing request body"; + EXPECT_GT(inlen, fih + s) << "Missing request filename"; + // No redundant information for checking buflen + break; + } + case FUSE_MKDIR: + EXPECT_GE(inlen, fih + sizeof(in.body.mkdir)) << + "Missing request body"; + EXPECT_GT(inlen, fih + sizeof(in.body.mkdir)) << + "Missing request filename"; + // No redundant information for checking buflen + break; + case FUSE_RENAME: + EXPECT_GE(inlen, fih + sizeof(in.body.rename)) << + "Missing request body"; + EXPECT_GT(inlen, fih + sizeof(in.body.rename)) << + "Missing request filename"; + // No redundant information for checking buflen + break; + case FUSE_LINK: + EXPECT_GE(inlen, fih + sizeof(in.body.link)) << + "Missing request body"; + EXPECT_GT(inlen, fih + sizeof(in.body.link)) << + "Missing request filename"; + // No redundant information for checking buflen + break; + case FUSE_OPEN: + EXPECT_EQ(inlen, fih + sizeof(in.body.open)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_READ: + EXPECT_EQ(inlen, fih + sizeof(in.body.read)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_WRITE: + { + size_t s; + + if (m_kernel_minor_version >= 9) + s = sizeof(in.body.write); + else + s = FUSE_COMPAT_WRITE_IN_SIZE; + // I suppose a 0-byte write should be allowed + EXPECT_GE(inlen, fih + s) << "Missing request body"; + EXPECT_EQ((size_t)buflen, fih + s + in.body.write.size); + break; + } + case FUSE_DESTROY: + case FUSE_STATFS: + EXPECT_EQ(inlen, fih); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_RELEASE: + EXPECT_EQ(inlen, fih + sizeof(in.body.release)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_FSYNC: + case FUSE_FSYNCDIR: + EXPECT_EQ(inlen, fih + sizeof(in.body.fsync)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_SETXATTR: + EXPECT_GE(inlen, fih + sizeof(in.body.setxattr)) << + "Missing request body"; + EXPECT_GT(inlen, fih + sizeof(in.body.setxattr)) << + "Missing request attribute name"; + // No redundant information for checking buflen + break; + case FUSE_GETXATTR: + EXPECT_GE(inlen, fih + sizeof(in.body.getxattr)) << + "Missing request body"; + EXPECT_GT(inlen, fih + sizeof(in.body.getxattr)) << + "Missing request attribute name"; + // No redundant information for checking buflen + break; + case FUSE_LISTXATTR: + EXPECT_EQ(inlen, fih + sizeof(in.body.listxattr)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_REMOVEXATTR: + EXPECT_GT(inlen, fih) << "Missing request attribute name"; + // No redundant information for checking buflen + break; + case FUSE_FLUSH: + EXPECT_EQ(inlen, fih + sizeof(in.body.flush)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_INIT: + EXPECT_EQ(inlen, fih + sizeof(in.body.init)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_OPENDIR: + EXPECT_EQ(inlen, fih + sizeof(in.body.opendir)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_READDIR: + EXPECT_EQ(inlen, fih + sizeof(in.body.readdir)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_RELEASEDIR: + EXPECT_EQ(inlen, fih + sizeof(in.body.releasedir)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_GETLK: + EXPECT_EQ(inlen, fih + sizeof(in.body.getlk)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_SETLK: + case FUSE_SETLKW: + EXPECT_EQ(inlen, fih + sizeof(in.body.setlk)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_ACCESS: + EXPECT_EQ(inlen, fih + sizeof(in.body.access)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_CREATE: + EXPECT_GE(inlen, fih + sizeof(in.body.create)) << + "Missing request body"; + EXPECT_GT(inlen, fih + sizeof(in.body.create)) << + "Missing request filename"; + // No redundant information for checking buflen + break; + case FUSE_INTERRUPT: + EXPECT_EQ(inlen, fih + sizeof(in.body.interrupt)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_FALLOCATE: + EXPECT_EQ(inlen, fih + sizeof(in.body.fallocate)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_BMAP: + EXPECT_EQ(inlen, fih + sizeof(in.body.bmap)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_LSEEK: + EXPECT_EQ(inlen, fih + sizeof(in.body.lseek)); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_COPY_FILE_RANGE: + EXPECT_EQ(inlen, fih + sizeof(in.body.copy_file_range)); + EXPECT_EQ(0ul, in.body.copy_file_range.flags); + EXPECT_EQ((size_t)buflen, inlen); + break; + case FUSE_NOTIFY_REPLY: + case FUSE_BATCH_FORGET: + case FUSE_IOCTL: + case FUSE_POLL: + case FUSE_READDIRPLUS: + FAIL() << "Unsupported opcode?"; + default: + FAIL() << "Unknown opcode " << in.header.opcode; + } + /* Verify that the ticket's unique value is actually unique. */ + if (m_uniques->find(in.header.unique) != m_uniques->end()) + FAIL() << "Non-unique \"unique\" value"; + m_uniques->insert(in.header.unique); +} + +void MockFS::init(uint32_t flags) { + ssize_t buflen; + + std::unique_ptr<mockfs_buf_in> in(new mockfs_buf_in); + std::unique_ptr<mockfs_buf_out> out(new mockfs_buf_out); + + read_request(*in, buflen); + if (verbosity > 0) + debug_request(*in, buflen); + audit_request(*in, buflen); + ASSERT_EQ(FUSE_INIT, in->header.opcode); + + out->header.unique = in->header.unique; + out->header.error = 0; + out->body.init.major = FUSE_KERNEL_VERSION; + out->body.init.minor = m_kernel_minor_version;; + out->body.init.flags = in->body.init.flags & flags; + out->body.init.max_write = m_maxwrite; + out->body.init.max_readahead = m_maxreadahead; + + if (m_kernel_minor_version < 23) { + SET_OUT_HEADER_LEN(*out, init_7_22); + } else { + out->body.init.time_gran = m_time_gran; + SET_OUT_HEADER_LEN(*out, init); + } + + write(m_fuse_fd, out.get(), out->header.len); +} + +void MockFS::kill_daemon() { + m_quit = true; + if (m_daemon_id != NULL) + pthread_kill(m_daemon_id, SIGUSR1); + // Closing the /dev/fuse file descriptor first allows unmount to + // succeed even if the daemon doesn't correctly respond to commands + // during the unmount sequence. + close(m_fuse_fd); + m_fuse_fd = -1; +} + +void MockFS::join_daemon() { + if (m_daemon_id != NULL) { + pthread_join(m_daemon_id, NULL); + m_daemon_id = NULL; + } +} + +void MockFS::loop() { + std::vector<std::unique_ptr<mockfs_buf_out>> out; + + std::unique_ptr<mockfs_buf_in> in(new mockfs_buf_in); + ASSERT_TRUE(in != NULL); + while (!m_quit) { + ssize_t buflen; + + bzero(in.get(), sizeof(*in)); + read_request(*in, buflen); + if (m_quit) + break; + if (verbosity > 0) + debug_request(*in, buflen); + audit_request(*in, buflen); + if (pid_ok((pid_t)in->header.pid)) { + process(*in, out); + } else { + /* + * Reject any requests from unknown processes. Because + * we actually do mount a filesystem, plenty of + * unrelated system daemons may try to access it. + */ + if (verbosity > 1) + printf("\tREJECTED (wrong pid %d)\n", + in->header.pid); + process_default(*in, out); + } + for (auto &it: out) + write_response(*it); + out.clear(); + } +} + +int MockFS::notify_inval_entry(ino_t parent, const char *name, size_t namelen, + int expected_errno) +{ + std::unique_ptr<mockfs_buf_out> out(new mockfs_buf_out); + + out->expected_errno = expected_errno; + out->header.unique = 0; /* 0 means asynchronous notification */ + out->header.error = FUSE_NOTIFY_INVAL_ENTRY; + out->body.inval_entry.parent = parent; + out->body.inval_entry.namelen = namelen; + strlcpy((char*)&out->body.bytes + sizeof(out->body.inval_entry), + name, sizeof(out->body.bytes) - sizeof(out->body.inval_entry)); + out->header.len = sizeof(out->header) + sizeof(out->body.inval_entry) + + namelen; + debug_response(*out); + write_response(*out); + return 0; +} + +int MockFS::notify_inval_inode(ino_t ino, off_t off, ssize_t len) +{ + std::unique_ptr<mockfs_buf_out> out(new mockfs_buf_out); + + out->header.unique = 0; /* 0 means asynchronous notification */ + out->header.error = FUSE_NOTIFY_INVAL_INODE; + out->body.inval_inode.ino = ino; + out->body.inval_inode.off = off; + out->body.inval_inode.len = len; + out->header.len = sizeof(out->header) + sizeof(out->body.inval_inode); + debug_response(*out); + write_response(*out); + return 0; +} + +int MockFS::notify_store(ino_t ino, off_t off, const void* data, ssize_t size) +{ + std::unique_ptr<mockfs_buf_out> out(new mockfs_buf_out); + + out->header.unique = 0; /* 0 means asynchronous notification */ + out->header.error = FUSE_NOTIFY_STORE; + out->body.store.nodeid = ino; + out->body.store.offset = off; + out->body.store.size = size; + bcopy(data, (char*)&out->body.bytes + sizeof(out->body.store), size); + out->header.len = sizeof(out->header) + sizeof(out->body.store) + size; + debug_response(*out); + write_response(*out); + return 0; +} + +bool MockFS::pid_ok(pid_t pid) { + if (pid == m_pid) { + return (true); + } else if (pid == m_child_pid) { + return (true); + } else { + struct kinfo_proc *ki; + bool ok = false; + + ki = kinfo_getproc(pid); + if (ki == NULL) + return (false); + /* + * Allow access by the aio daemon processes so that our tests + * can use aio functions + */ + if (0 == strncmp("aiod", ki->ki_comm, 4)) + ok = true; + free(ki); + return (ok); + } +} + +void MockFS::process_default(const mockfs_buf_in& in, + std::vector<std::unique_ptr<mockfs_buf_out>> &out) +{ + std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out); + out0->header.unique = in.header.unique; + out0->header.error = -EOPNOTSUPP; + out0->header.len = sizeof(out0->header); + out.push_back(std::move(out0)); +} + +void MockFS::read_request(mockfs_buf_in &in, ssize_t &res) { + int nready = 0; + fd_set readfds; + pollfd fds[1]; + struct kevent changes[1]; + struct kevent events[1]; + struct timespec timeout_ts; + struct timeval timeout_tv; + const int timeout_ms = 999; + int timeout_int, nfds; + int fuse_fd; + + switch (m_pm) { + case BLOCKING: + break; + case KQ: + timeout_ts.tv_sec = 0; + timeout_ts.tv_nsec = timeout_ms * 1'000'000; + while (nready == 0) { + EV_SET(&changes[0], m_fuse_fd, EVFILT_READ, + EV_ADD | EV_ONESHOT, 0, 0, 0); + nready = kevent(m_kq, &changes[0], 1, &events[0], 1, + &timeout_ts); + if (m_quit) + return; + } + ASSERT_LE(0, nready) << strerror(errno); + ASSERT_EQ(events[0].ident, (uintptr_t)m_fuse_fd); + if (events[0].flags & EV_ERROR) + FAIL() << strerror(events[0].data); + else if (events[0].flags & EV_EOF) + FAIL() << strerror(events[0].fflags); + m_nready = events[0].data; + break; + case POLL: + timeout_int = timeout_ms; + fds[0].fd = m_fuse_fd; + fds[0].events = POLLIN; + while (nready == 0) { + nready = poll(fds, 1, timeout_int); + if (m_quit) + return; + } + ASSERT_LE(0, nready) << strerror(errno); + ASSERT_TRUE(fds[0].revents & POLLIN); + break; + case SELECT: + fuse_fd = m_fuse_fd; + if (fuse_fd < 0) + break; + timeout_tv.tv_sec = 0; + timeout_tv.tv_usec = timeout_ms * 1'000; + nfds = fuse_fd + 1; + while (nready == 0) { + FD_ZERO(&readfds); + FD_SET(fuse_fd, &readfds); + nready = select(nfds, &readfds, NULL, NULL, + &timeout_tv); + if (m_quit) + return; + } + ASSERT_LE(0, nready) << strerror(errno); + ASSERT_TRUE(FD_ISSET(fuse_fd, &readfds)); + break; + default: + FAIL() << "not yet implemented"; + } + res = read(m_fuse_fd, &in, sizeof(in)); + + if (res < 0 && errno != EBADF && !m_quit && !m_expect_unmount) { + m_quit = true; + FAIL() << "read: " << strerror(errno); + } + ASSERT_TRUE(res >= static_cast<ssize_t>(sizeof(in.header)) || m_quit); + /* + * Inconsistently, fuse_in_header.len is the size of the entire + * request,including header, even though fuse_out_header.len excludes + * the size of the header. + */ + ASSERT_TRUE(res == static_cast<ssize_t>(in.header.len) || m_quit); +} + +void MockFS::write_response(const mockfs_buf_out &out) { + fd_set writefds; + pollfd fds[1]; + struct kevent changes[1]; + struct kevent events[1]; + int nready, nfds; + ssize_t r; + + switch (m_pm) { + case BLOCKING: + break; + case KQ: + EV_SET(&changes[0], m_fuse_fd, EVFILT_WRITE, + EV_ADD | EV_ONESHOT, 0, 0, 0); + nready = kevent(m_kq, &changes[0], 1, &events[0], 1, + NULL); + ASSERT_LE(0, nready) << strerror(errno); + ASSERT_EQ(events[0].ident, (uintptr_t)m_fuse_fd); + if (events[0].flags & EV_ERROR) + FAIL() << strerror(events[0].data); + else if (events[0].flags & EV_EOF) + FAIL() << strerror(events[0].fflags); + m_nready = events[0].data; + break; + case POLL: + fds[0].fd = m_fuse_fd; + fds[0].events = POLLOUT; + nready = poll(fds, 1, INFTIM); + ASSERT_LE(0, nready) << strerror(errno); + ASSERT_EQ(1, nready) << "NULL timeout expired?"; + ASSERT_TRUE(fds[0].revents & POLLOUT); + break; + case SELECT: + FD_ZERO(&writefds); + FD_SET(m_fuse_fd, &writefds); + nfds = m_fuse_fd + 1; + nready = select(nfds, NULL, &writefds, NULL, NULL); + ASSERT_LE(0, nready) << strerror(errno); + ASSERT_EQ(1, nready) << "NULL timeout expired?"; + ASSERT_TRUE(FD_ISSET(m_fuse_fd, &writefds)); + break; + default: + FAIL() << "not yet implemented"; + } + r = write(m_fuse_fd, &out, out.header.len); + if (out.expected_errno) { + ASSERT_EQ(-1, r); + ASSERT_EQ(out.expected_errno, errno) << strerror(errno); + } else { + if (r <= 0 && errno == EINVAL) { + printf("Failed to write response. unique=%" PRIu64 + ":\n", out.header.unique); + } + ASSERT_TRUE(r > 0 || errno == EAGAIN) << strerror(errno); + } +} + +void* MockFS::service(void *pthr_data) { + MockFS *mock_fs = (MockFS*)pthr_data; + + mock_fs->loop(); + + return (NULL); +} + +void MockFS::unmount() { + ::unmount("mountpoint", 0); +} diff --git a/tests/sys/fs/fusefs/mockfs.hh b/tests/sys/fs/fusefs/mockfs.hh new file mode 100644 index 000000000000..f98a5337c9d1 --- /dev/null +++ b/tests/sys/fs/fusefs/mockfs.hh @@ -0,0 +1,446 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/types.h> + +#include <pthread.h> + +#include "fuse_kernel.h" +} + +#include <unordered_set> + +#include <gmock/gmock.h> + +#define TIME_T_MAX (std::numeric_limits<time_t>::max()) + +/* + * A pseudo-fuse errno used indicate that a fuse operation should have no + * response, at least not immediately + */ +#define FUSE_NORESPONSE 9999 + +#define SET_OUT_HEADER_LEN(out, variant) { \ + (out).header.len = (sizeof((out).header) + \ + sizeof((out).body.variant)); \ +} + +/* + * Create an expectation on FUSE_LOOKUP and return it so the caller can set + * actions. + * + * This must be a macro instead of a method because EXPECT_CALL returns a type + * with a deleted constructor. + */ +#define EXPECT_LOOKUP(parent, path) \ + EXPECT_CALL(*m_mock, process( \ + ResultOf([=](auto in) { \ + return (in.header.opcode == FUSE_LOOKUP && \ + in.header.nodeid == (parent) && \ + strcmp(in.body.lookup, (path)) == 0); \ + }, Eq(true)), \ + _) \ + ) + +extern int verbosity; + +/* + * The maximum that a test case can set max_write, limited by the buffer + * supplied when reading from /dev/fuse. This limitation is imposed by + * fusefs-libs, but not by the FUSE protocol. + */ +const uint32_t max_max_write = 0x20000; + + +/* This struct isn't defined by fuse_kernel.h or libfuse, but it should be */ +struct fuse_create_out { + struct fuse_entry_out entry; + struct fuse_open_out open; +}; + +/* Protocol 7.8 version of struct fuse_attr */ +struct fuse_attr_7_8 +{ + uint64_t ino; + uint64_t size; + uint64_t blocks; + uint64_t atime; + uint64_t mtime; + uint64_t ctime; + uint32_t atimensec; + uint32_t mtimensec; + uint32_t ctimensec; + uint32_t mode; + uint32_t nlink; + uint32_t uid; + uint32_t gid; + uint32_t rdev; +}; + +/* Protocol 7.8 version of struct fuse_attr_out */ +struct fuse_attr_out_7_8 +{ + uint64_t attr_valid; + uint32_t attr_valid_nsec; + uint32_t dummy; + struct fuse_attr_7_8 attr; +}; + +/* Protocol 7.8 version of struct fuse_entry_out */ +struct fuse_entry_out_7_8 { + uint64_t nodeid; /* Inode ID */ + uint64_t generation; /* Inode generation: nodeid:gen must + be unique for the fs's lifetime */ + uint64_t entry_valid; /* Cache timeout for the name */ + uint64_t attr_valid; /* Cache timeout for the attributes */ + uint32_t entry_valid_nsec; + uint32_t attr_valid_nsec; + struct fuse_attr_7_8 attr; +}; + +/* Output struct for FUSE_CREATE for protocol 7.8 servers */ +struct fuse_create_out_7_8 { + struct fuse_entry_out_7_8 entry; + struct fuse_open_out open; +}; + +/* Output struct for FUSE_INIT for protocol 7.22 and earlier servers */ +struct fuse_init_out_7_22 { + uint32_t major; + uint32_t minor; + uint32_t max_readahead; + uint32_t flags; + uint16_t max_background; + uint16_t congestion_threshold; + uint32_t max_write; +}; + +union fuse_payloads_in { + fuse_access_in access; + fuse_bmap_in bmap; + /* + * In fusefs-libs 3.4.2 and below the buffer size is fixed at 0x21000 + * minus the header sizes. fusefs-libs 3.4.3 (and FUSE Protocol 7.29) + * add a FUSE_MAX_PAGES option that allows it to be greater. + * + * See fuse_kern_chan.c in fusefs-libs 2.9.9 and below, or + * FUSE_DEFAULT_MAX_PAGES_PER_REQ in fusefs-libs 3.4.3 and above. + */ + uint8_t bytes[ + max_max_write + 0x1000 - sizeof(struct fuse_in_header) + ]; + fuse_copy_file_range_in copy_file_range; + fuse_create_in create; + fuse_fallocate_in fallocate; + fuse_flush_in flush; + fuse_fsync_in fsync; + fuse_fsync_in fsyncdir; + fuse_forget_in forget; + fuse_getattr_in getattr; + fuse_interrupt_in interrupt; + fuse_lk_in getlk; + fuse_getxattr_in getxattr; + fuse_init_in init; + fuse_link_in link; + fuse_listxattr_in listxattr; + char lookup[0]; + fuse_lseek_in lseek; + fuse_mkdir_in mkdir; + fuse_mknod_in mknod; + fuse_open_in open; + fuse_open_in opendir; + fuse_read_in read; + fuse_read_in readdir; + fuse_release_in release; + fuse_release_in releasedir; + fuse_rename_in rename; + char rmdir[0]; + fuse_setattr_in setattr; + fuse_setxattr_in setxattr; + fuse_lk_in setlk; + fuse_lk_in setlkw; + char unlink[0]; + fuse_write_in write; +}; + +struct mockfs_buf_in { + fuse_in_header header; + union fuse_payloads_in body; +}; + +union fuse_payloads_out { + fuse_attr_out attr; + fuse_attr_out_7_8 attr_7_8; + fuse_bmap_out bmap; + fuse_create_out create; + fuse_create_out_7_8 create_7_8; + /* + * The protocol places no limits on the size of bytes. Choose + * a size big enough for anything we'll test. + */ + uint8_t bytes[0x40000]; + fuse_entry_out entry; + fuse_entry_out_7_8 entry_7_8; + fuse_lk_out getlk; + fuse_getxattr_out getxattr; + fuse_init_out init; + fuse_init_out_7_22 init_7_22; + fuse_lseek_out lseek; + /* The inval_entry structure should be followed by the entry's name */ + fuse_notify_inval_entry_out inval_entry; + fuse_notify_inval_inode_out inval_inode; + /* The store structure should be followed by the data to store */ + fuse_notify_store_out store; + fuse_listxattr_out listxattr; + fuse_open_out open; + fuse_statfs_out statfs; + /* + * The protocol places no limits on the length of the string. This is + * merely convenient for testing. + */ + char str[80]; + fuse_write_out write; +}; + +struct mockfs_buf_out { + fuse_out_header header; + union fuse_payloads_out body; + /* the expected errno of the write to /dev/fuse */ + int expected_errno; + + /* Default constructor: zero everything */ + mockfs_buf_out() { + memset(this, 0, sizeof(*this)); + } +}; + +/* A function that can be invoked in place of MockFS::process */ +typedef std::function<void (const mockfs_buf_in& in, + std::vector<std::unique_ptr<mockfs_buf_out>> &out)> +ProcessMockerT; + +/* + * Helper function used for setting an error expectation for any fuse operation. + * The operation will return the supplied error + */ +ProcessMockerT ReturnErrno(int error); + +/* Helper function used for returning negative cache entries for LOOKUP */ +ProcessMockerT ReturnNegativeCache(const struct timespec *entry_valid); + +/* Helper function used for returning a single immediate response */ +ProcessMockerT ReturnImmediate( + std::function<void(const mockfs_buf_in& in, + struct mockfs_buf_out &out)> f); + +/* How the daemon should check /dev/fuse for readiness */ +enum poll_method { + BLOCKING, + SELECT, + POLL, + KQ +}; + +/* + * Fake FUSE filesystem + * + * "Mounts" a filesystem to a temporary directory and services requests + * according to the programmed expectations. + * + * Operates directly on the fusefs(4) kernel API, not the libfuse(3) user api. + */ +class MockFS { + /* + * thread id of the fuse daemon thread + * + * It must run in a separate thread so it doesn't deadlock with the + * client test code. + */ + pthread_t m_daemon_id; + + /* file descriptor of /dev/fuse control device */ + volatile int m_fuse_fd; + + /* The minor version of the kernel API that this mock daemon targets */ + uint32_t m_kernel_minor_version; + + int m_kq; + + /* + * If nonzero, the maximum size in bytes of a read that the kernel will + * send to the server. + */ + int m_maxread; + + /* The max_readahead file system option */ + uint32_t m_maxreadahead; + + /* pid of the test process */ + pid_t m_pid; + + /* Every "unique" value of a fuse ticket seen so far */ + std::unique_ptr<std::unordered_set<uint64_t>> m_uniques; + + /* Method the daemon should use for I/O to and from /dev/fuse */ + enum poll_method m_pm; + + /* Timestamp granularity in nanoseconds */ + unsigned m_time_gran; + + void audit_request(const mockfs_buf_in &in, ssize_t buflen); + void debug_request(const mockfs_buf_in&, ssize_t buflen); + void debug_response(const mockfs_buf_out&); + + /* Initialize a session after mounting */ + void init(uint32_t flags); + + /* Is pid from a process that might be involved in the test? */ + bool pid_ok(pid_t pid); + + /* Default request handler */ + void process_default(const mockfs_buf_in&, + std::vector<std::unique_ptr<mockfs_buf_out>>&); + + /* Entry point for the daemon thread */ + static void* service(void*); + + /* + * Read, but do not process, a single request from the kernel + * + * @param in Return storage for the FUSE request + * @param res Return value of read(2). If positive, the amount of + * data read from the fuse device. + */ + void read_request(mockfs_buf_in& in, ssize_t& res); + + public: + /* Write a single response back to the kernel */ + void write_response(const mockfs_buf_out &out); + + /* pid of child process, for two-process test cases */ + pid_t m_child_pid; + + /* Maximum size of a FUSE_WRITE write */ + uint32_t m_maxwrite; + + /* + * Number of events that were available from /dev/fuse after the last + * kevent call. Only valid when m_pm = KQ. + */ + int m_nready; + + /* Tell the daemon to shut down ASAP */ + bool m_quit; + + /* Tell the daemon that the server might forcibly unmount us */ + bool m_expect_unmount; + + /* Create a new mockfs and mount it to a tempdir */ + MockFS(int max_read, int max_readahead, bool allow_other, + bool default_permissions, bool push_symlinks_in, bool ro, + enum poll_method pm, uint32_t flags, + uint32_t kernel_minor_version, uint32_t max_write, bool async, + bool no_clusterr, unsigned time_gran, bool nointr, + bool noatime, const char *fsname, const char *subtype, + bool no_auto_init); + + virtual ~MockFS(); + + /* Kill the filesystem daemon without unmounting the filesystem */ + void kill_daemon(); + + /* Wait until the daemon thread terminates */ + void join_daemon(); + + /* Process FUSE requests endlessly */ + void loop(); + + /* + * Send an asynchronous notification to invalidate a directory entry. + * Similar to libfuse's fuse_lowlevel_notify_inval_entry + * + * This method will block until the client has responded, so it should + * generally be run in a separate thread from request processing. + * + * @param parent Parent directory's inode number + * @param name name of dirent to invalidate + * @param namelen size of name, including the NUL + * @param expected_errno The error that write() should return + */ + int notify_inval_entry(ino_t parent, const char *name, size_t namelen, + int expected_errno = 0); + + /* + * Send an asynchronous notification to invalidate an inode's cached + * data and/or attributes. Similar to libfuse's + * fuse_lowlevel_notify_inval_inode. + * + * This method will block until the client has responded, so it should + * generally be run in a separate thread from request processing. + * + * @param ino File's inode number + * @param off offset at which to begin invalidation. A + * negative offset means to invalidate attributes + * only. + * @param len Size of region of data to invalidate. 0 means + * to invalidate all cached data. + */ + int notify_inval_inode(ino_t ino, off_t off, ssize_t len); + + /* + * Send an asynchronous notification to store data directly into an + * inode's cache. Similar to libfuse's fuse_lowlevel_notify_store. + * + * This method will block until the client has responded, so it should + * generally be run in a separate thread from request processing. + * + * @param ino File's inode number + * @param off Offset at which to store data + * @param data Pointer to the data to cache + * @param len Size of data + */ + int notify_store(ino_t ino, off_t off, const void* data, ssize_t size); + + /* + * Request handler + * + * This method is expected to provide the responses to each FUSE + * operation. For an immediate response, push one buffer into out. + * For a delayed response, push nothing. For an immediate response + * plus a delayed response to an earlier operation, push two bufs. + * Test cases must define each response using Googlemock expectations + */ + MOCK_METHOD2(process, void(const mockfs_buf_in&, + std::vector<std::unique_ptr<mockfs_buf_out>>&)); + + /* Gracefully unmount */ + void unmount(); +}; diff --git a/tests/sys/fs/fusefs/mount.cc b/tests/sys/fs/fusefs/mount.cc new file mode 100644 index 000000000000..ece518b09f66 --- /dev/null +++ b/tests/sys/fs/fusefs/mount.cc @@ -0,0 +1,200 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> +#include <sys/mount.h> +#include <sys/uio.h> + +#include <mntopts.h> // for build_iovec +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Mount: public FuseTest { +public: +void expect_statfs() { + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_STATFS); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, statfs); + }))); +} +}; + +class Fsname: public Mount { + void SetUp() { + m_fsname = "http://something"; + Mount::SetUp(); + } +}; + +class Subtype: public Mount { + void SetUp() { + m_subtype = "myfs"; + Mount::SetUp(); + } +}; + +class UpdateOk: public Mount, public WithParamInterface<const char*> {}; +class UpdateErr: public Mount, public WithParamInterface<const char*> {}; + +int mntflag_from_string(const char *s) +{ + if (0 == strcmp("MNT_RDONLY", s)) + return MNT_RDONLY; + else if (0 == strcmp("MNT_NOEXEC", s)) + return MNT_NOEXEC; + else if (0 == strcmp("MNT_NOSUID", s)) + return MNT_NOSUID; + else if (0 == strcmp("MNT_NOATIME", s)) + return MNT_NOATIME; + else if (0 == strcmp("MNT_SUIDDIR", s)) + return MNT_SUIDDIR; + else if (0 == strcmp("MNT_USER", s)) + return MNT_USER; + else + return 0; +} + +TEST_F(Fsname, fsname) +{ + struct statfs statbuf; + + expect_statfs(); + + ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); + ASSERT_STREQ("http://something", statbuf.f_mntfromname); +} + +TEST_F(Subtype, subtype) +{ + struct statfs statbuf; + + expect_statfs(); + + ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); + ASSERT_STREQ("fusefs.myfs", statbuf.f_fstypename); +} + +/* Some mount options can be changed by mount -u */ +TEST_P(UpdateOk, update) +{ + struct statfs statbuf; + struct iovec *iov = NULL; + int iovlen = 0; + int flag; + int newflags = MNT_UPDATE | MNT_SYNCHRONOUS; + + flag = mntflag_from_string(GetParam()); + if (flag == MNT_NOSUID && 0 != geteuid()) + GTEST_SKIP() << "Only root may clear MNT_NOSUID"; + if (flag == MNT_SUIDDIR && 0 != geteuid()) + GTEST_SKIP() << "Only root may set MNT_SUIDDIR"; + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_STATFS); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + /* + * All of the fields except f_flags are don't care, and f_flags is set by + * the VFS + */ + SET_OUT_HEADER_LEN(out, statfs); + }))); + + ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); + newflags = (statbuf.f_flags | MNT_UPDATE) ^ flag; + + build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1); + build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1); + build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1); + ASSERT_EQ(0, nmount(iov, iovlen, newflags)) << strerror(errno); + + ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); + EXPECT_FALSE((newflags ^ statbuf.f_flags) & flag); +} + +/* Some mount options cannnot be changed by mount -u */ +TEST_P(UpdateErr, update) +{ + struct statfs statbuf; + struct iovec *iov = NULL; + int iovlen = 0; + int flag; + int newflags = MNT_UPDATE | MNT_SYNCHRONOUS; + + flag = mntflag_from_string(GetParam()); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_STATFS); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + /* + * All of the fields except f_flags are don't care, and f_flags is set by + * the VFS + */ + SET_OUT_HEADER_LEN(out, statfs); + }))); + + ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); + newflags = (statbuf.f_flags | MNT_UPDATE) ^ flag; + + build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1); + build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1); + build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1); + /* + * Don't check nmount's return value, because vfs_domount may "fix" the + * options for us. The important thing is to check the final value of + * statbuf.f_flags below. + */ + (void)nmount(iov, iovlen, newflags); + + ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); + EXPECT_TRUE((newflags ^ statbuf.f_flags) & flag); +} + +INSTANTIATE_TEST_SUITE_P(Mount, UpdateOk, + ::testing::Values("MNT_RDONLY", "MNT_NOEXEC", "MNT_NOSUID", "MNT_NOATIME", + "MNT_SUIDDIR") +); + +INSTANTIATE_TEST_SUITE_P(Mount, UpdateErr, + ::testing::Values( "MNT_USER") +); diff --git a/tests/sys/fs/fusefs/nfs.cc b/tests/sys/fs/fusefs/nfs.cc new file mode 100644 index 000000000000..2fa2b290f383 --- /dev/null +++ b/tests/sys/fs/fusefs/nfs.cc @@ -0,0 +1,480 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* This file tests functionality needed by NFS servers */ +extern "C" { +#include <sys/param.h> +#include <sys/mount.h> + +#include <dirent.h> +#include <fcntl.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace std; +using namespace testing; + + +class Nfs: public FuseTest { +public: +virtual void SetUp() { + if (geteuid() != 0) + GTEST_SKIP() << "This test requires a privileged user"; + FuseTest::SetUp(); +} +}; + +class Exportable: public Nfs { +public: +virtual void SetUp() { + m_init_flags = FUSE_EXPORT_SUPPORT; + Nfs::SetUp(); +} +}; + +class Fhstat: public Exportable {}; +class FhstatNotExportable: public Nfs {}; +class Getfh: public Exportable {}; +class Readdir: public Exportable {}; + +/* If the server returns a different generation number, then file is stale */ +TEST_F(Fhstat, estale) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + struct stat sb; + const uint64_t ino = 42; + const mode_t mode = S_IFDIR | 0755; + Sequence seq; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) + .InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.generation = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + EXPECT_LOOKUP(ino, ".") + .InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.generation = 2; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + ASSERT_EQ(-1, fhstat(&fhp, &sb)); + EXPECT_EQ(ESTALE, errno); +} + +/* If we must lookup an entry from the server, send a LOOKUP request for "." */ +TEST_F(Fhstat, lookup_dot) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + struct stat sb; + const uint64_t ino = 42; + const mode_t mode = S_IFDIR | 0755; + const uid_t uid = 12345; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.generation = 1; + out.body.entry.attr.uid = uid; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + EXPECT_LOOKUP(ino, ".") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.generation = 1; + out.body.entry.attr.uid = uid; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); + EXPECT_EQ(uid, sb.st_uid); + EXPECT_EQ(mode, sb.st_mode); +} + +/* Gracefully handle failures to lookup ".". */ +TEST_F(Fhstat, lookup_dot_error) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + struct stat sb; + const uint64_t ino = 42; + const mode_t mode = S_IFDIR | 0755; + const uid_t uid = 12345; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.generation = 1; + out.body.entry.attr.uid = uid; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + EXPECT_LOOKUP(ino, ".") + .WillOnce(Invoke(ReturnErrno(EDOOFUS))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + ASSERT_EQ(-1, fhstat(&fhp, &sb)); + EXPECT_EQ(EDOOFUS, errno); +} + +/* Use a file handle whose entry is still cached */ +TEST_F(Fhstat, cached) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + struct stat sb; + const uint64_t ino = 42; + const mode_t mode = S_IFDIR | 0755; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.generation = 1; + out.body.entry.attr.ino = ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); + EXPECT_EQ(ino, sb.st_ino); +} + +/* File handle entries should expire from the cache, too */ +TEST_F(Fhstat, cache_expired) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + struct stat sb; + const uint64_t ino = 42; + const mode_t mode = S_IFDIR | 0755; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.generation = 1; + out.body.entry.attr.ino = ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid_nsec = NAP_NS / 2; + }))); + + EXPECT_LOOKUP(ino, ".") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.generation = 1; + out.body.entry.attr.ino = ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); + EXPECT_EQ(ino, sb.st_ino); + + nap(); + + /* Cache should be expired; fuse should issue a FUSE_LOOKUP */ + ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); + EXPECT_EQ(ino, sb.st_ino); +} + +/* + * If the server returns a FUSE_LOOKUP response for a nodeid that we didn't + * lookup, it's a bug. But we should handle it gracefully. + */ +TEST_F(Fhstat, inconsistent_nodeid) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + struct stat sb; + const uint64_t ino_in = 42; + const uint64_t ino_out = 43; + const mode_t mode = S_IFDIR | 0755; + const uid_t uid = 12345; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = ino_in; + out.body.entry.attr.ino = ino_in; + out.body.entry.attr.mode = mode; + out.body.entry.generation = 1; + out.body.entry.attr.uid = uid; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + EXPECT_LOOKUP(ino_in, ".") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = ino_out; + out.body.entry.attr.ino = ino_out; + out.body.entry.attr.mode = mode; + out.body.entry.generation = 1; + out.body.entry.attr.uid = uid; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + EXPECT_NE(0, fhstat(&fhp, &sb)) << strerror(errno); + EXPECT_EQ(EIO, errno); +} + +/* + * If the server returns a FUSE_LOOKUP response where the nodeid doesn't match + * the inode number, and the file system is exported, it's a bug. But we + * should handle it gracefully. + */ +TEST_F(Fhstat, inconsistent_ino) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + struct stat sb; + const uint64_t nodeid = 42; + const uint64_t ino = 711; // Could be anything that != nodeid + const mode_t mode = S_IFDIR | 0755; + const uid_t uid = 12345; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = nodeid; + out.body.entry.attr.ino = nodeid; + out.body.entry.attr.mode = mode; + out.body.entry.generation = 1; + out.body.entry.attr.uid = uid; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + EXPECT_LOOKUP(nodeid, ".") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = nodeid; + out.body.entry.attr.ino = ino; + out.body.entry.attr.mode = mode; + out.body.entry.generation = 1; + out.body.entry.attr.uid = uid; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + /* + * The fhstat operation will actually succeed. But future operations + * will likely fail. + */ + ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); + EXPECT_EQ(ino, sb.st_ino); +} + +/* + * If the server doesn't set FUSE_EXPORT_SUPPORT, then we can't do NFS-style + * lookups + */ +TEST_F(FhstatNotExportable, lookup_dot) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + const uint64_t ino = 42; + const mode_t mode = S_IFDIR | 0755; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.generation = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + ASSERT_EQ(-1, getfh(FULLPATH, &fhp)); + ASSERT_EQ(EOPNOTSUPP, errno); +} + +/* FreeBSD's fid struct doesn't have enough space for 64-bit generations */ +TEST_F(Getfh, eoverflow) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.generation = (uint64_t)UINT32_MAX + 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + ASSERT_NE(0, getfh(FULLPATH, &fhp)); + EXPECT_EQ(EOVERFLOW, errno); +} + +/* Get an NFS file handle */ +TEST_F(Getfh, ok) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); +} + +/* + * Call readdir via a file handle. + * + * This is how a userspace nfs server like nfs-ganesha or unfs3 would call + * readdir. The in-kernel NFS server never does any equivalent of open. I + * haven't discovered a way to mimic nfsd's behavior short of actually running + * nfsd. + */ +TEST_F(Readdir, getdirentries) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + mode_t mode = S_IFDIR | 0755; + fhandle_t fhp; + int fd; + char buf[8192]; + ssize_t r; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.generation = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + EXPECT_LOOKUP(ino, ".") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.generation = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + expect_opendir(ino); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READDIR && + in.header.nodeid == ino && + in.body.readdir.size == sizeof(buf)); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + out.header.error = 0; + out.header.len = sizeof(out.header); + }))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + fd = fhopen(&fhp, O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + r = getdirentries(fd, buf, sizeof(buf), 0); + ASSERT_EQ(0, r) << strerror(errno); + + leak(fd); +} diff --git a/tests/sys/fs/fusefs/notify.cc b/tests/sys/fs/fusefs/notify.cc new file mode 100644 index 000000000000..d370a1e6e706 --- /dev/null +++ b/tests/sys/fs/fusefs/notify.cc @@ -0,0 +1,602 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/types.h> + +#include <fcntl.h> +#include <pthread.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +/* + * FUSE asynchonous notification + * + * FUSE servers can send unprompted notification messages for things like cache + * invalidation. This file tests our client's handling of those messages. + */ + +class Notify: public FuseTest { +public: +/* Ignore an optional FUSE_FSYNC */ +void maybe_expect_fsync(uint64_t ino) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FSYNC && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(0))); +} + +void expect_lookup(uint64_t parent, const char *relpath, uint64_t ino, + off_t size, Sequence &seq) +{ + EXPECT_LOOKUP(parent, relpath) + .InSequence(seq) + .WillOnce(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr.size = size; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); +} +}; + +class NotifyWriteback: public Notify { +public: +virtual void SetUp() { + m_init_flags |= FUSE_WRITEBACK_CACHE; + m_async = true; + Notify::SetUp(); + if (IsSkipped()) + return; +} + +void expect_write(uint64_t ino, uint64_t offset, uint64_t size, + const void *contents) +{ + FuseTest::expect_write(ino, offset, size, size, 0, 0, contents); +} + +}; + +struct inval_entry_args { + MockFS *mock; + ino_t parent; + const char *name; + size_t namelen; +}; + +static void* inval_entry(void* arg) { + const struct inval_entry_args *iea = (struct inval_entry_args*)arg; + ssize_t r; + + r = iea->mock->notify_inval_entry(iea->parent, iea->name, iea->namelen); + if (r >= 0) + return 0; + else + return (void*)(intptr_t)errno; +} + +struct inval_inode_args { + MockFS *mock; + ino_t ino; + off_t off; + ssize_t len; +}; + +struct store_args { + MockFS *mock; + ino_t nodeid; + off_t offset; + ssize_t size; + const void* data; +}; + +static void* inval_inode(void* arg) { + const struct inval_inode_args *iia = (struct inval_inode_args*)arg; + ssize_t r; + + r = iia->mock->notify_inval_inode(iia->ino, iia->off, iia->len); + if (r >= 0) + return 0; + else + return (void*)(intptr_t)errno; +} + +static void* store(void* arg) { + const struct store_args *sa = (struct store_args*)arg; + ssize_t r; + + r = sa->mock->notify_store(sa->nodeid, sa->offset, sa->data, sa->size); + if (r >= 0) + return 0; + else + return (void*)(intptr_t)errno; +} + +/* Invalidate a nonexistent entry */ +TEST_F(Notify, inval_entry_nonexistent) +{ + const static char *name = "foo"; + struct inval_entry_args iea; + void *thr0_value; + pthread_t th0; + + iea.mock = m_mock; + iea.parent = FUSE_ROOT_ID; + iea.name = name; + iea.namelen = strlen(name); + ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea)) + << strerror(errno); + pthread_join(th0, &thr0_value); + /* It's not an error for an entry to not be cached */ + EXPECT_EQ(0, (intptr_t)thr0_value); +} + +/* Invalidate a cached entry */ +TEST_F(Notify, inval_entry) +{ + const static char FULLPATH[] = "mountpoint/foo"; + const static char RELPATH[] = "foo"; + struct inval_entry_args iea; + struct stat sb; + void *thr0_value; + uint64_t ino0 = 42; + uint64_t ino1 = 43; + Sequence seq; + pthread_t th0; + + expect_lookup(FUSE_ROOT_ID, RELPATH, ino0, 0, seq); + expect_lookup(FUSE_ROOT_ID, RELPATH, ino1, 0, seq); + + /* Fill the entry cache */ + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + EXPECT_EQ(ino0, sb.st_ino); + + /* Now invalidate the entry */ + iea.mock = m_mock; + iea.parent = FUSE_ROOT_ID; + iea.name = RELPATH; + iea.namelen = strlen(RELPATH); + ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea)) + << strerror(errno); + pthread_join(th0, &thr0_value); + EXPECT_EQ(0, (intptr_t)thr0_value); + + /* The second lookup should return the alternate ino */ + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + EXPECT_EQ(ino1, sb.st_ino); +} + +/* + * Invalidate a cached entry beneath the root, which uses a slightly different + * code path. + */ +TEST_F(Notify, inval_entry_below_root) +{ + const static char FULLPATH[] = "mountpoint/some_dir/foo"; + const static char DNAME[] = "some_dir"; + const static char FNAME[] = "foo"; + struct inval_entry_args iea; + struct stat sb; + void *thr0_value; + uint64_t dir_ino = 41; + uint64_t ino0 = 42; + uint64_t ino1 = 43; + Sequence seq; + pthread_t th0; + + EXPECT_LOOKUP(FUSE_ROOT_ID, DNAME) + .WillOnce(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = dir_ino; + out.body.entry.attr.nlink = 2; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + expect_lookup(dir_ino, FNAME, ino0, 0, seq); + expect_lookup(dir_ino, FNAME, ino1, 0, seq); + + /* Fill the entry cache */ + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + EXPECT_EQ(ino0, sb.st_ino); + + /* Now invalidate the entry */ + iea.mock = m_mock; + iea.parent = dir_ino; + iea.name = FNAME; + iea.namelen = strlen(FNAME); + ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea)) + << strerror(errno); + pthread_join(th0, &thr0_value); + EXPECT_EQ(0, (intptr_t)thr0_value); + + /* The second lookup should return the alternate ino */ + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + EXPECT_EQ(ino1, sb.st_ino); +} + +/* Invalidating an entry invalidates the parent directory's attributes */ +TEST_F(Notify, inval_entry_invalidates_parent_attrs) +{ + const static char FULLPATH[] = "mountpoint/foo"; + const static char RELPATH[] = "foo"; + struct inval_entry_args iea; + struct stat sb; + void *thr0_value; + uint64_t ino = 42; + Sequence seq; + pthread_t th0; + + expect_lookup(FUSE_ROOT_ID, RELPATH, ino, 0, seq); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == FUSE_ROOT_ID); + }, Eq(true)), + _) + ).Times(2) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.mode = S_IFDIR | 0755; + out.body.attr.attr_valid = UINT64_MAX; + }))); + + /* Fill the attr and entry cache */ + ASSERT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + + /* Now invalidate the entry */ + iea.mock = m_mock; + iea.parent = FUSE_ROOT_ID; + iea.name = RELPATH; + iea.namelen = strlen(RELPATH); + ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea)) + << strerror(errno); + pthread_join(th0, &thr0_value); + EXPECT_EQ(0, (intptr_t)thr0_value); + + /* /'s attribute cache should be cleared */ + ASSERT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); +} + + +TEST_F(Notify, inval_inode_nonexistent) +{ + struct inval_inode_args iia; + ino_t ino = 42; + void *thr0_value; + pthread_t th0; + + iia.mock = m_mock; + iia.ino = ino; + iia.off = 0; + iia.len = 0; + ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia)) + << strerror(errno); + pthread_join(th0, &thr0_value); + /* It's not an error for an inode to not be cached */ + EXPECT_EQ(0, (intptr_t)thr0_value); +} + +TEST_F(Notify, inval_inode_with_clean_cache) +{ + const static char FULLPATH[] = "mountpoint/foo"; + const static char RELPATH[] = "foo"; + const char CONTENTS0[] = "abcdefgh"; + const char CONTENTS1[] = "ijklmnopqrstuvwxyz"; + struct inval_inode_args iia; + struct stat sb; + ino_t ino = 42; + void *thr0_value; + Sequence seq; + uid_t uid = 12345; + pthread_t th0; + ssize_t size0 = sizeof(CONTENTS0); + ssize_t size1 = sizeof(CONTENTS1); + char buf[80]; + int fd; + + expect_lookup(FUSE_ROOT_ID, RELPATH, ino, size0, seq); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr_valid = UINT64_MAX; + out.body.attr.attr.size = size1; + out.body.attr.attr.uid = uid; + }))); + expect_read(ino, 0, size0, size0, CONTENTS0); + expect_read(ino, 0, size1, size1, CONTENTS1); + + /* Fill the data cache */ + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(size0, read(fd, buf, size0)) << strerror(errno); + EXPECT_EQ(0, memcmp(buf, CONTENTS0, size0)); + + /* Evict the data cache */ + iia.mock = m_mock; + iia.ino = ino; + iia.off = 0; + iia.len = 0; + ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia)) + << strerror(errno); + pthread_join(th0, &thr0_value); + EXPECT_EQ(0, (intptr_t)thr0_value); + + /* cache attributes were purged; this will trigger a new GETATTR */ + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + EXPECT_EQ(uid, sb.st_uid); + EXPECT_EQ(size1, sb.st_size); + + /* This read should not be serviced by cache */ + ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); + ASSERT_EQ(size1, read(fd, buf, size1)) << strerror(errno); + EXPECT_EQ(0, memcmp(buf, CONTENTS1, size1)); + + leak(fd); +} + +/* + * Attempting to invalidate an entry or inode after unmounting should fail, but + * nothing bad should happen. + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=290519 + */ +TEST_F(Notify, notify_after_unmount) +{ + const static char *name = "foo"; + struct inval_entry_args iea; + + expect_destroy(0); + + m_mock->unmount(); + + iea.mock = m_mock; + iea.parent = FUSE_ROOT_ID; + iea.name = name; + iea.namelen = strlen(name); + iea.mock->notify_inval_entry(iea.parent, iea.name, iea.namelen, ENODEV); +} + +/* FUSE_NOTIFY_STORE with a file that's not in the entry cache */ +/* disabled because FUSE_NOTIFY_STORE is not yet implemented */ +TEST_F(Notify, DISABLED_store_nonexistent) +{ + struct store_args sa; + ino_t ino = 42; + void *thr0_value; + pthread_t th0; + + sa.mock = m_mock; + sa.nodeid = ino; + sa.offset = 0; + sa.size = 0; + ASSERT_EQ(0, pthread_create(&th0, NULL, store, &sa)) << strerror(errno); + pthread_join(th0, &thr0_value); + /* It's not an error for a file to be unknown to the kernel */ + EXPECT_EQ(0, (intptr_t)thr0_value); +} + +/* Store data into for a file that does not yet have anything cached */ +/* disabled because FUSE_NOTIFY_STORE is not yet implemented */ +TEST_F(Notify, DISABLED_store_with_blank_cache) +{ + const static char FULLPATH[] = "mountpoint/foo"; + const static char RELPATH[] = "foo"; + const char CONTENTS1[] = "ijklmnopqrstuvwxyz"; + struct store_args sa; + ino_t ino = 42; + void *thr0_value; + Sequence seq; + pthread_t th0; + ssize_t size1 = sizeof(CONTENTS1); + char buf[80]; + int fd; + + expect_lookup(FUSE_ROOT_ID, RELPATH, ino, size1, seq); + expect_open(ino, 0, 1); + + /* Fill the data cache */ + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + /* Evict the data cache */ + sa.mock = m_mock; + sa.nodeid = ino; + sa.offset = 0; + sa.size = size1; + sa.data = (const void*)CONTENTS1; + ASSERT_EQ(0, pthread_create(&th0, NULL, store, &sa)) << strerror(errno); + pthread_join(th0, &thr0_value); + EXPECT_EQ(0, (intptr_t)thr0_value); + + /* This read should be serviced by cache */ + ASSERT_EQ(size1, read(fd, buf, size1)) << strerror(errno); + EXPECT_EQ(0, memcmp(buf, CONTENTS1, size1)); + + leak(fd); +} + +TEST_F(NotifyWriteback, inval_inode_with_dirty_cache) +{ + const static char FULLPATH[] = "mountpoint/foo"; + const static char RELPATH[] = "foo"; + const char CONTENTS[] = "abcdefgh"; + struct inval_inode_args iia; + ino_t ino = 42; + void *thr0_value; + Sequence seq; + pthread_t th0; + ssize_t bufsize = sizeof(CONTENTS); + int fd; + + expect_lookup(FUSE_ROOT_ID, RELPATH, ino, 0, seq); + expect_open(ino, 0, 1); + + /* Fill the data cache */ + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd); + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + + expect_write(ino, 0, bufsize, CONTENTS); + /* + * The FUSE protocol does not require an fsync here, but FreeBSD's + * bufobj_invalbuf sends it anyway + */ + maybe_expect_fsync(ino); + + /* Evict the data cache */ + iia.mock = m_mock; + iia.ino = ino; + iia.off = 0; + iia.len = 0; + ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia)) + << strerror(errno); + pthread_join(th0, &thr0_value); + EXPECT_EQ(0, (intptr_t)thr0_value); + + leak(fd); +} + +TEST_F(NotifyWriteback, inval_inode_attrs_only) +{ + const static char FULLPATH[] = "mountpoint/foo"; + const static char RELPATH[] = "foo"; + const char CONTENTS[] = "abcdefgh"; + struct inval_inode_args iia; + struct stat sb; + uid_t uid = 12345; + ino_t ino = 42; + void *thr0_value; + Sequence seq; + pthread_t th0; + ssize_t bufsize = sizeof(CONTENTS); + int fd; + + expect_lookup(FUSE_ROOT_ID, RELPATH, ino, 0, seq); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_WRITE); + }, Eq(true)), + _) + ).Times(0); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr_valid = UINT64_MAX; + out.body.attr.attr.size = bufsize; + out.body.attr.attr.uid = uid; + }))); + + /* Fill the data cache */ + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + + /* Evict the attributes, but not data cache */ + iia.mock = m_mock; + iia.ino = ino; + iia.off = -1; + iia.len = 0; + ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia)) + << strerror(errno); + pthread_join(th0, &thr0_value); + EXPECT_EQ(0, (intptr_t)thr0_value); + + /* cache attributes were been purged; this will trigger a new GETATTR */ + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + EXPECT_EQ(uid, sb.st_uid); + EXPECT_EQ(bufsize, sb.st_size); + + leak(fd); +} + +/* + * Attempting asynchronous invalidation of an Entry before mounting the file + * system should fail, but nothing bad should happen. + * + * Note that invalidating an inode before mount goes through the same path, and + * is not separately tested. + * + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=290519 + */ +TEST(PreMount, inval_entry_before_mount) +{ + const static char name[] = "foo"; + size_t namelen = strlen(name); + struct mockfs_buf_out *out; + int r; + int fuse_fd; + + fuse_fd = open("/dev/fuse", O_CLOEXEC | O_RDWR); + ASSERT_GE(fuse_fd, 0) << strerror(errno); + + out = new mockfs_buf_out; + out->header.unique = 0; /* 0 means asynchronous notification */ + out->header.error = FUSE_NOTIFY_INVAL_ENTRY; + out->body.inval_entry.parent = FUSE_ROOT_ID; + out->body.inval_entry.namelen = namelen; + strlcpy((char*)&out->body.bytes + sizeof(out->body.inval_entry), + name, sizeof(out->body.bytes) - sizeof(out->body.inval_entry)); + out->header.len = sizeof(out->header) + sizeof(out->body.inval_entry) + + namelen; + r = write(fuse_fd, out, out->header.len); + EXPECT_EQ(-1, r); + EXPECT_EQ(ENODEV, errno); + delete out; +} diff --git a/tests/sys/fs/fusefs/open.cc b/tests/sys/fs/fusefs/open.cc new file mode 100644 index 000000000000..1212a7047f26 --- /dev/null +++ b/tests/sys/fs/fusefs/open.cc @@ -0,0 +1,307 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/wait.h> + +#include <fcntl.h> +#include <semaphore.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Open: public FuseTest { + +public: + +/* Test an OK open of a file with the given flags */ +void test_ok(int os_flags, int fuse_flags) { + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.body.open.flags == (uint32_t)fuse_flags && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { + out.header.len = sizeof(out.header); + SET_OUT_HEADER_LEN(out, open); + }))); + + fd = open(FULLPATH, os_flags); + ASSERT_LE(0, fd) << strerror(errno); + leak(fd); +} +}; + +/* + * fusefs(4) does not support I/O on device nodes (neither does UFS). But it + * shouldn't crash + */ +TEST_F(Open, chr) +{ + const char FULLPATH[] = "mountpoint/zero"; + const char RELPATH[] = "zero"; + uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFCHR | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.attr.rdev = 44; /* /dev/zero's rdev */ + }))); + + ASSERT_EQ(-1, open(FULLPATH, O_RDONLY)); + EXPECT_EQ(EOPNOTSUPP, errno); +} + +/* + * The fuse daemon fails the request with enoent. This usually indicates a + * race condition: some other FUSE client removed the file in between when the + * kernel checked for it with lookup and tried to open it + */ +TEST_F(Open, enoent) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + sem_t sem; + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(ENOENT))); + // Since FUSE_OPEN returns ENOENT, the kernel will reclaim the vnode + // and send a FUSE_FORGET + expect_forget(ino, 1, &sem); + + ASSERT_EQ(-1, open(FULLPATH, O_RDONLY)); + EXPECT_EQ(ENOENT, errno); + + sem_wait(&sem); + sem_destroy(&sem); +} + +/* + * The daemon is responsible for checking file permissions (unless the + * default_permissions mount option was used) + */ +TEST_F(Open, eperm) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(EPERM))); + ASSERT_EQ(-1, open(FULLPATH, O_RDONLY)); + EXPECT_EQ(EPERM, errno); +} + +/* + * fusefs must issue multiple FUSE_OPEN operations if clients with different + * credentials open the same file, even if they use the same mode. This is + * necessary so that the daemon can validate each set of credentials. + */ +TEST_F(Open, multiple_creds) +{ + const static char FULLPATH[] = "mountpoint/some_file.txt"; + const static char RELPATH[] = "some_file.txt"; + int fd1, status; + const static uint64_t ino = 42; + const static uint64_t fh0 = 100, fh1 = 200; + + /* Fork a child to open the file with different credentials */ + fork(false, &status, [&] { + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.header.pid == (uint32_t)getpid() && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke( + ReturnImmediate([](auto in __unused, auto& out) { + out.body.open.fh = fh0; + out.header.len = sizeof(out.header); + SET_OUT_HEADER_LEN(out, open); + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.header.pid != (uint32_t)getpid() && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke( + ReturnImmediate([](auto in __unused, auto& out) { + out.body.open.fh = fh1; + out.header.len = sizeof(out.header); + SET_OUT_HEADER_LEN(out, open); + }))); + expect_flush(ino, 2, ReturnErrno(0)); + expect_release(ino, fh0); + expect_release(ino, fh1); + + fd1 = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd1) << strerror(errno); + }, [] { + int fd0; + + fd0 = open(FULLPATH, O_RDONLY); + if (fd0 < 0) { + perror("open"); + return(1); + } + leak(fd0); + return 0; + } + ); + ASSERT_EQ(0, WEXITSTATUS(status)); + + close(fd1); +} + +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */ +TEST_F(Open, DISABLED_o_append) +{ + test_ok(O_WRONLY | O_APPEND, O_WRONLY | O_APPEND); +} + +/* The kernel is supposed to filter out this flag */ +TEST_F(Open, o_creat) +{ + test_ok(O_WRONLY | O_CREAT, O_WRONLY); +} + +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */ +TEST_F(Open, DISABLED_o_direct) +{ + test_ok(O_WRONLY | O_DIRECT, O_WRONLY | O_DIRECT); +} + +/* The kernel is supposed to filter out this flag */ +TEST_F(Open, o_excl) +{ + test_ok(O_WRONLY | O_EXCL, O_WRONLY); +} + +TEST_F(Open, o_exec) +{ + test_ok(O_EXEC, O_EXEC); +} + +/* The kernel is supposed to filter out this flag */ +TEST_F(Open, o_noctty) +{ + test_ok(O_WRONLY | O_NOCTTY, O_WRONLY); +} + +TEST_F(Open, o_rdonly) +{ + test_ok(O_RDONLY, O_RDONLY); +} + +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */ +TEST_F(Open, DISABLED_o_trunc) +{ + test_ok(O_WRONLY | O_TRUNC, O_WRONLY | O_TRUNC); +} + +TEST_F(Open, o_wronly) +{ + test_ok(O_WRONLY, O_WRONLY); +} + +TEST_F(Open, o_rdwr) +{ + test_ok(O_RDWR, O_RDWR); +} + +/* + * If a fuse server returns ENOSYS to a + * FUSE_OPEN, then it and subsequent FUSE_OPEN and FUSE_RELEASE operations will + * also succeed automatically without being sent to the server. + */ +TEST_F(Open, enosys) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.body.open.flags == (uint32_t)O_RDONLY && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(1) + .WillOnce(Invoke(ReturnErrno(ENOSYS))); + expect_flush(ino, 1, ReturnErrno(ENOSYS)); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + close(fd); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + leak(fd); +} diff --git a/tests/sys/fs/fusefs/opendir.cc b/tests/sys/fs/fusefs/opendir.cc new file mode 100644 index 000000000000..e1fed59635fc --- /dev/null +++ b/tests/sys/fs/fusefs/opendir.cc @@ -0,0 +1,197 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <dirent.h> + +#include <fcntl.h> +#include <semaphore.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Opendir: public FuseTest { +public: +void expect_lookup(const char *relpath, uint64_t ino) +{ + FuseTest::expect_lookup(relpath, ino, S_IFDIR | 0755, 0, 1); +} + +void expect_opendir(uint64_t ino, uint32_t flags, ProcessMockerT r) +{ + /* opendir(3) calls fstatfs */ + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_STATFS); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, statfs); + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPENDIR && + in.header.nodeid == ino && + in.body.opendir.flags == flags); + }, Eq(true)), + _) + ).WillOnce(Invoke(r)); +} + +}; + + +/* + * The fuse daemon fails the request with enoent. This usually indicates a + * race condition: some other FUSE client removed the file in between when the + * kernel checked for it with lookup and tried to open it + */ +TEST_F(Opendir, enoent) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + sem_t sem; + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + expect_lookup(RELPATH, ino); + expect_opendir(ino, O_RDONLY, ReturnErrno(ENOENT)); + // Since FUSE_OPENDIR returns ENOENT, the kernel will reclaim the vnode + // and send a FUSE_FORGET + expect_forget(ino, 1, &sem); + + ASSERT_EQ(-1, open(FULLPATH, O_DIRECTORY)); + EXPECT_EQ(ENOENT, errno); + + sem_wait(&sem); + sem_destroy(&sem); +} + +/* + * The daemon is responsible for checking file permissions (unless the + * default_permissions mount option was used) + */ +TEST_F(Opendir, eperm) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + + expect_lookup(RELPATH, ino); + expect_opendir(ino, O_RDONLY, ReturnErrno(EPERM)); + + EXPECT_EQ(-1, open(FULLPATH, O_DIRECTORY)); + EXPECT_EQ(EPERM, errno); +} + +TEST_F(Opendir, open) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_opendir(ino, O_RDONLY, + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, open); + })); + + fd = open(FULLPATH, O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + + leak(fd); +} + +/* Directories can be opened O_EXEC for stuff like fchdir(2) */ +TEST_F(Opendir, open_exec) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_opendir(ino, O_EXEC, + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, open); + })); + + fd = open(FULLPATH, O_EXEC | O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + + leak(fd); +} + +TEST_F(Opendir, opendir) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + + expect_lookup(RELPATH, ino); + expect_opendir(ino, O_RDONLY, + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, open); + })); + + errno = 0; + EXPECT_NE(nullptr, opendir(FULLPATH)) << strerror(errno); +} + +/* + * If a fuse server returns ENOSYS to a + * FUSE_OPENDIR, then it and subsequent FUSE_OPENDIR and FUSE_RELEASEDIR + * operations will also succeed automatically without being sent to the server. + */ +TEST_F(Opendir, enosys) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + FuseTest::expect_lookup(RELPATH, ino, S_IFDIR | 0755, 0, 2); + expect_opendir(ino, O_RDONLY, ReturnErrno(ENOSYS)); + + fd = open(FULLPATH, O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + close(fd); + + fd = open(FULLPATH, O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + + leak(fd); +} diff --git a/tests/sys/fs/fusefs/pre-init.cc b/tests/sys/fs/fusefs/pre-init.cc new file mode 100644 index 000000000000..2d3257500304 --- /dev/null +++ b/tests/sys/fs/fusefs/pre-init.cc @@ -0,0 +1,226 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 ConnectWise + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> +#include <sys/mount.h> +#include <sys/signal.h> +#include <sys/wait.h> + +#include <fcntl.h> +#include <pthread.h> +#include <semaphore.h> +#include <signal.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +/* Tests for behavior that happens before the server responds to FUSE_INIT */ +class PreInit: public FuseTest { +public: +void SetUp() { + m_no_auto_init = true; + FuseTest::SetUp(); +} +}; + +/* + * Tests for behavior that happens before the server responds to FUSE_INIT, + * parameterized on default_permissions + */ +class PreInitP: public PreInit, + public WithParamInterface<bool> +{ +void SetUp() { + m_default_permissions = GetParam(); + PreInit::SetUp(); +} +}; + +static void* unmount1(void* arg __unused) { + ssize_t r; + + r = unmount("mountpoint", 0); + if (r >= 0) + return 0; + else + return (void*)(intptr_t)errno; +} + +/* + * Attempting to unmount the file system before it fully initializes should + * work fine. The unmount will complete after initialization does. + */ +TEST_F(PreInit, unmount_before_init) +{ + sem_t sem0; + pthread_t th1; + + ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno); + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_INIT); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) { + SET_OUT_HEADER_LEN(out, init); + out.body.init.major = FUSE_KERNEL_VERSION; + out.body.init.minor = FUSE_KERNEL_MINOR_VERSION; + out.body.init.flags = in.body.init.flags & m_init_flags; + out.body.init.max_write = m_maxwrite; + out.body.init.max_readahead = m_maxreadahead; + out.body.init.time_gran = m_time_gran; + sem_wait(&sem0); + }))); + expect_destroy(0); + + ASSERT_EQ(0, pthread_create(&th1, NULL, unmount1, NULL)); + nap(); /* Wait for th1 to block in unmount() */ + sem_post(&sem0); + /* The daemon will quit after receiving FUSE_DESTROY */ + m_mock->join_daemon(); +} + +/* + * Don't panic in this very specific scenario: + * + * The server does not respond to FUSE_INIT in timely fashion. + * Some other process tries to do unmount. + * That other process gets killed by a signal. + * The server finally responds to FUSE_INIT. + * + * Regression test for bug 287438 + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=287438 + */ +TEST_F(PreInit, signal_during_unmount_before_init) +{ + sem_t sem0; + pid_t child; + + ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno); + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_INIT); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) { + SET_OUT_HEADER_LEN(out, init); + out.body.init.major = FUSE_KERNEL_VERSION; + /* + * Use protocol 7.19, like libfuse2 does. The server must use + * protocol 7.27 or older to trigger the bug. + */ + out.body.init.minor = 19; + out.body.init.flags = in.body.init.flags & m_init_flags; + out.body.init.max_write = m_maxwrite; + out.body.init.max_readahead = m_maxreadahead; + out.body.init.time_gran = m_time_gran; + sem_wait(&sem0); + }))); + + if ((child = ::fork()) == 0) { + /* + * In child. This will block waiting for FUSE_INIT to complete + * or the receipt of an asynchronous signal. + */ + (void) unmount("mountpoint", 0); + _exit(0); /* Unreachable, unless parent dies after fork */ + } else if (child > 0) { + /* In parent. Wait for child process to start, then kill it */ + nap(); + kill(child, SIGINT); + waitpid(child, NULL, WEXITED); + } else { + FAIL() << strerror(errno); + } + m_mock->m_quit = true; /* Since we are by now unmounted. */ + sem_post(&sem0); + m_mock->join_daemon(); +} + +/* + * If some process attempts VOP_GETATTR for the mountpoint before init is + * complete, fusefs should wait, just like it does for other VOPs. + * + * To verify that fuse_vnop_getattr does indeed wait for FUSE_INIT to complete, + * invoke the test like this: + * +> sudo cpuset -c -l 0 dtrace -i 'fbt:fusefs:fuse_internal_init_callback:' -i 'fbt:fusefs:fuse_vnop_getattr:' -c "./pre-init --gtest_filter=PI/PreInitP.getattr_before_init/0" +... +dtrace: pid 4224 has exited +CPU ID FUNCTION:NAME + 0 68670 fuse_vnop_getattr:entry + 0 68893 fuse_internal_init_callback:entry + 0 68894 fuse_internal_init_callback:return + 0 68671 fuse_vnop_getattr:return + * + * Note that fuse_vnop_getattr was entered first, but exitted last. + */ +TEST_P(PreInitP, getattr_before_init) +{ + struct stat sb; + nlink_t nlink = 12345; + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_INIT); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) { + SET_OUT_HEADER_LEN(out, init); + out.body.init.major = FUSE_KERNEL_VERSION; + out.body.init.minor = FUSE_KERNEL_MINOR_VERSION; + out.body.init.flags = in.body.init.flags & m_init_flags; + out.body.init.max_write = m_maxwrite; + out.body.init.max_readahead = m_maxreadahead; + out.body.init.time_gran = m_time_gran; + nap(); /* Allow stat() to run first */ + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == FUSE_ROOT_ID); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto& in, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = in.header.nodeid; + out.body.attr.attr.mode = S_IFDIR | 0644; + out.body.attr.attr.nlink = nlink; + out.body.attr.attr_valid = UINT64_MAX; + }))); + + EXPECT_EQ(0, stat("mountpoint", &sb)); + EXPECT_EQ(nlink, sb.st_nlink); +} + +INSTANTIATE_TEST_SUITE_P(PI, PreInitP, Bool()); diff --git a/tests/sys/fs/fusefs/read.cc b/tests/sys/fs/fusefs/read.cc new file mode 100644 index 000000000000..e9c79ba2ffda --- /dev/null +++ b/tests/sys/fs/fusefs/read.cc @@ -0,0 +1,1497 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> +#include <sys/mman.h> +#include <sys/socket.h> +#include <sys/sysctl.h> +#include <sys/uio.h> + +#include <aio.h> +#include <fcntl.h> +#include <semaphore.h> +#include <setjmp.h> +#include <signal.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Read: public FuseTest { + +public: +void expect_lookup(const char *relpath, uint64_t ino, uint64_t size) +{ + FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, size, 1); +} +}; + +class RofsRead: public Read { +public: +virtual void SetUp() { + m_ro = true; + Read::SetUp(); +} +}; + +class Read_7_8: public FuseTest { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + FuseTest::SetUp(); +} + +void expect_lookup(const char *relpath, uint64_t ino, uint64_t size) +{ + FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, size, 1); +} +}; + +class AioRead: public Read { +public: +virtual void SetUp() { + if (!is_unsafe_aio_enabled()) + GTEST_SKIP() << + "vfs.aio.enable_unsafe must be set for this test"; + FuseTest::SetUp(); +} +}; + +class AsyncRead: public AioRead { + virtual void SetUp() { + m_init_flags = FUSE_ASYNC_READ; + AioRead::SetUp(); + } +}; + +class ReadAhead: public Read, + public WithParamInterface<tuple<bool, int>> +{ + virtual void SetUp() { + int val; + const char *node = "vfs.maxbcachebuf"; + size_t size = sizeof(val); + ASSERT_EQ(0, sysctlbyname(node, &val, &size, NULL, 0)) + << strerror(errno); + + m_maxreadahead = val * get<1>(GetParam()); + m_noclusterr = get<0>(GetParam()); + Read::SetUp(); + } +}; + +class ReadMaxRead: public Read { + virtual void SetUp() { + m_maxread = 16384; + Read::SetUp(); + } +}; + +class ReadNoatime: public Read { + virtual void SetUp() { + m_noatime = true; + Read::SetUp(); + } +}; + +class ReadSigbus: public Read +{ +public: +static jmp_buf s_jmpbuf; +static void *s_si_addr; + +void TearDown() { + struct sigaction sa; + + bzero(&sa, sizeof(sa)); + sa.sa_handler = SIG_DFL; + sigaction(SIGBUS, &sa, NULL); + + FuseTest::TearDown(); +} + +}; + +static void +handle_sigbus(int signo __unused, siginfo_t *info, void *uap __unused) { + ReadSigbus::s_si_addr = info->si_addr; + longjmp(ReadSigbus::s_jmpbuf, 1); +} + +jmp_buf ReadSigbus::s_jmpbuf; +void *ReadSigbus::s_si_addr; + +class TimeGran: public Read, public WithParamInterface<unsigned> { +public: +virtual void SetUp() { + m_time_gran = 1 << GetParam(); + Read::SetUp(); +} +}; + +/* AIO reads need to set the header's pid field correctly */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */ +TEST_F(AioRead, aio_read) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + struct aiocb iocb, *piocb; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + iocb.aio_nbytes = bufsize; + iocb.aio_fildes = fd; + iocb.aio_buf = buf; + iocb.aio_offset = 0; + iocb.aio_sigevent.sigev_notify = SIGEV_NONE; + ASSERT_EQ(0, aio_read(&iocb)) << strerror(errno); + ASSERT_EQ(bufsize, aio_waitcomplete(&piocb, NULL)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); + + leak(fd); +} + +/* + * Without the FUSE_ASYNC_READ mount option, fuse(4) should ensure that there + * is at most one outstanding read operation per file handle + */ +TEST_F(AioRead, async_read_disabled) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = 50; + char buf0[bufsize], buf1[bufsize]; + off_t off0 = 0; + off_t off1 = m_maxbcachebuf; + struct aiocb iocb0, iocb1; + volatile sig_atomic_t read_count = 0; + + expect_lookup(RELPATH, ino, 131072); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.fh == FH && + in.body.read.offset == (uint64_t)off0); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke([&](auto in __unused, auto &out __unused) { + read_count++; + /* Filesystem is slow to respond */ + })); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.fh == FH && + in.body.read.offset == (uint64_t)off1); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke([&](auto in __unused, auto &out __unused) { + read_count++; + /* Filesystem is slow to respond */ + })); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + /* + * Submit two AIO read requests, and respond to neither. If the + * filesystem ever gets the second read request, then we failed to + * limit outstanding reads. + */ + iocb0.aio_nbytes = bufsize; + iocb0.aio_fildes = fd; + iocb0.aio_buf = buf0; + iocb0.aio_offset = off0; + iocb0.aio_sigevent.sigev_notify = SIGEV_NONE; + ASSERT_EQ(0, aio_read(&iocb0)) << strerror(errno); + + iocb1.aio_nbytes = bufsize; + iocb1.aio_fildes = fd; + iocb1.aio_buf = buf1; + iocb1.aio_offset = off1; + iocb1.aio_sigevent.sigev_notify = SIGEV_NONE; + ASSERT_EQ(0, aio_read(&iocb1)) << strerror(errno); + + /* + * Sleep for awhile to make sure the kernel has had a chance to issue + * the second read, even though the first has not yet returned + */ + nap(); + EXPECT_EQ(read_count, 1); + + m_mock->kill_daemon(); + /* Wait for AIO activity to complete, but ignore errors */ + (void)aio_waitcomplete(NULL, NULL); + + leak(fd); +} + +/* + * With the FUSE_ASYNC_READ mount option, fuse(4) may issue multiple + * simultaneous read requests on the same file handle. + */ +TEST_F(AsyncRead, async_read) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = 50; + char buf0[bufsize], buf1[bufsize]; + off_t off0 = 0; + off_t off1 = m_maxbcachebuf; + off_t fsize = 2 * m_maxbcachebuf; + struct aiocb iocb0, iocb1; + sem_t sem; + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + expect_lookup(RELPATH, ino, fsize); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.fh == FH && + in.body.read.offset == (uint64_t)off0); + }, Eq(true)), + _) + ).WillOnce(Invoke([&](auto in __unused, auto &out __unused) { + sem_post(&sem); + /* Filesystem is slow to respond */ + })); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.fh == FH && + in.body.read.offset == (uint64_t)off1); + }, Eq(true)), + _) + ).WillOnce(Invoke([&](auto in __unused, auto &out __unused) { + sem_post(&sem); + /* Filesystem is slow to respond */ + })); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + /* + * Submit two AIO read requests, but respond to neither. Ensure that + * we received both. + */ + iocb0.aio_nbytes = bufsize; + iocb0.aio_fildes = fd; + iocb0.aio_buf = buf0; + iocb0.aio_offset = off0; + iocb0.aio_sigevent.sigev_notify = SIGEV_NONE; + ASSERT_EQ(0, aio_read(&iocb0)) << strerror(errno); + + iocb1.aio_nbytes = bufsize; + iocb1.aio_fildes = fd; + iocb1.aio_buf = buf1; + iocb1.aio_offset = off1; + iocb1.aio_sigevent.sigev_notify = SIGEV_NONE; + ASSERT_EQ(0, aio_read(&iocb1)) << strerror(errno); + + /* Wait until both reads have reached the daemon */ + ASSERT_EQ(0, sem_wait(&sem)) << strerror(errno); + ASSERT_EQ(0, sem_wait(&sem)) << strerror(errno); + + m_mock->kill_daemon(); + /* Wait for AIO activity to complete, but ignore errors */ + (void)aio_waitcomplete(NULL, NULL); + + leak(fd); +} + +/* The kernel should update the cached atime attribute during a read */ +TEST_F(Read, atime) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + struct stat sb1, sb2; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb1)); + + /* Ensure atime will be different than it was during lookup */ + nap(); + + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb2)); + + /* The kernel should automatically update atime during read */ + EXPECT_TRUE(timespeccmp(&sb1.st_atim, &sb2.st_atim, <)); + EXPECT_TRUE(timespeccmp(&sb1.st_ctim, &sb2.st_ctim, ==)); + EXPECT_TRUE(timespeccmp(&sb1.st_mtim, &sb2.st_mtim, ==)); + + leak(fd); +} + +/* The kernel should update the cached atime attribute during a cached read */ +TEST_F(Read, atime_cached) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + struct stat sb1, sb2; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, pread(fd, buf, bufsize, 0)) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb1)); + + /* Ensure atime will be different than it was during the first read */ + nap(); + + ASSERT_EQ(bufsize, pread(fd, buf, bufsize, 0)) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb2)); + + /* The kernel should automatically update atime during read */ + EXPECT_TRUE(timespeccmp(&sb1.st_atim, &sb2.st_atim, <)); + EXPECT_TRUE(timespeccmp(&sb1.st_ctim, &sb2.st_ctim, ==)); + EXPECT_TRUE(timespeccmp(&sb1.st_mtim, &sb2.st_mtim, ==)); + + leak(fd); +} + +/* dirty atime values should be flushed during close */ +TEST_F(Read, atime_during_close) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + struct stat sb; + uint64_t ino = 42; + const mode_t newmode = 0755; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + uint32_t valid = FATTR_ATIME; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + (time_t)in.body.setattr.atime == + sb.st_atim.tv_sec && + (long)in.body.setattr.atimensec == + sb.st_atim.tv_nsec); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFREG | newmode; + }))); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, FuseTest::FH); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + /* Ensure atime will be different than during lookup */ + nap(); + + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb)); + + close(fd); +} + +/* + * When not using -o default_permissions, the daemon may make its own decisions + * regarding access permissions, and these may be unpredictable. If it rejects + * our attempt to set atime, that should not cause close(2) to fail. + */ +TEST_F(Read, atime_during_close_eacces) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + uint32_t valid = FATTR_ATIME; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(EACCES))); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, FuseTest::FH); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + /* Ensure atime will be different than during lookup */ + nap(); + + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + + ASSERT_EQ(0, close(fd)); +} + +/* A cached atime should be flushed during FUSE_SETATTR */ +TEST_F(Read, atime_during_setattr) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + struct stat sb; + uint64_t ino = 42; + const mode_t newmode = 0755; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + uint32_t valid = FATTR_MODE | FATTR_ATIME; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + (time_t)in.body.setattr.atime == + sb.st_atim.tv_sec && + (long)in.body.setattr.atimensec == + sb.st_atim.tv_nsec); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFREG | newmode; + }))); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + /* Ensure atime will be different than during lookup */ + nap(); + + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb)); + ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno); + + leak(fd); +} + +/* 0-length reads shouldn't cause any confusion */ +TEST_F(Read, direct_io_read_nothing) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + uint64_t offset = 100; + char buf[80]; + + expect_lookup(RELPATH, ino, offset + 1000); + expect_open(ino, FOPEN_DIRECT_IO, 1); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(0, pread(fd, buf, 0, offset)) << strerror(errno); + leak(fd); +} + +/* + * With direct_io, reads should not fill the cache. They should go straight to + * the daemon + */ +TEST_F(Read, direct_io_pread) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + uint64_t offset = 100; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino, offset + bufsize); + expect_open(ino, FOPEN_DIRECT_IO, 1); + expect_read(ino, offset, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, pread(fd, buf, bufsize, offset)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); + + // With FOPEN_DIRECT_IO, the cache should be bypassed. The server will + // get a 2nd read request. + expect_read(ino, offset, bufsize, bufsize, CONTENTS); + ASSERT_EQ(bufsize, pread(fd, buf, bufsize, offset)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); + leak(fd); +} + +/* + * With direct_io, filesystems are allowed to return less data than is + * requested. fuse(4) should return a short read to userland. + */ +TEST_F(Read, direct_io_short_read) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefghijklmnop"; + uint64_t ino = 42; + int fd; + uint64_t offset = 100; + ssize_t bufsize = strlen(CONTENTS); + ssize_t halfbufsize = bufsize / 2; + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino, offset + bufsize); + expect_open(ino, FOPEN_DIRECT_IO, 1); + expect_read(ino, offset, bufsize, halfbufsize, CONTENTS); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(halfbufsize, pread(fd, buf, bufsize, offset)) + << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, halfbufsize)); + leak(fd); +} + +TEST_F(Read, eio) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(EIO))); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(-1, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(EIO, errno); + leak(fd); +} + +/* + * If the server returns a short read when direct io is not in use, that + * indicates EOF, because of a server-side truncation. We should invalidate + * all cached attributes. We may update the file size, + */ +TEST_F(Read, eof) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefghijklmnop"; + uint64_t ino = 42; + int fd; + uint64_t offset = 100; + ssize_t bufsize = strlen(CONTENTS); + ssize_t partbufsize = 3 * bufsize / 4; + ssize_t r; + uint8_t buf[bufsize]; + struct stat sb; + + expect_lookup(RELPATH, ino, offset + bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, offset + bufsize, offset + partbufsize, CONTENTS); + expect_getattr(ino, offset + partbufsize); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + r = pread(fd, buf, bufsize, offset); + ASSERT_LE(0, r) << strerror(errno); + EXPECT_EQ(partbufsize, r) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb)); + EXPECT_EQ((off_t)(offset + partbufsize), sb.st_size); + leak(fd); +} + +/* Like Read.eof, but causes an entire buffer to be invalidated */ +TEST_F(Read, eof_of_whole_buffer) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefghijklmnop"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + off_t old_filesize = m_maxbcachebuf * 2 + bufsize; + uint8_t buf[bufsize]; + struct stat sb; + + expect_lookup(RELPATH, ino, old_filesize); + expect_open(ino, 0, 1); + expect_read(ino, 2 * m_maxbcachebuf, bufsize, bufsize, CONTENTS); + expect_read(ino, m_maxbcachebuf, m_maxbcachebuf, 0, CONTENTS); + expect_getattr(ino, m_maxbcachebuf); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + /* Cache the third block */ + ASSERT_EQ(bufsize, pread(fd, buf, bufsize, m_maxbcachebuf * 2)) + << strerror(errno); + /* Try to read the 2nd block, but it's past EOF */ + ASSERT_EQ(0, pread(fd, buf, bufsize, m_maxbcachebuf)) + << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb)); + EXPECT_EQ((off_t)(m_maxbcachebuf), sb.st_size); + leak(fd); +} + +/* + * With the keep_cache option, the kernel may keep its read cache across + * multiple open(2)s. + */ +TEST_F(Read, keep_cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd0, fd1; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, bufsize, 2); + expect_open(ino, FOPEN_KEEP_CACHE, 2); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + + fd0 = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd0) << strerror(errno); + ASSERT_EQ(bufsize, read(fd0, buf, bufsize)) << strerror(errno); + + fd1 = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd1) << strerror(errno); + + /* + * This read should be serviced by cache, even though it's on the other + * file descriptor + */ + ASSERT_EQ(bufsize, read(fd1, buf, bufsize)) << strerror(errno); + + leak(fd0); + leak(fd1); +} + +/* + * Without the keep_cache option, the kernel should drop its read caches on + * every open + */ +TEST_F(Read, keep_cache_disabled) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd0, fd1; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, bufsize, 2); + expect_open(ino, 0, 2); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + + fd0 = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd0) << strerror(errno); + ASSERT_EQ(bufsize, read(fd0, buf, bufsize)) << strerror(errno); + + fd1 = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd1) << strerror(errno); + + /* + * This read should not be serviced by cache, even though it's on the + * original file descriptor + */ + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + ASSERT_EQ(0, lseek(fd0, 0, SEEK_SET)) << strerror(errno); + ASSERT_EQ(bufsize, read(fd0, buf, bufsize)) << strerror(errno); + + leak(fd0); + leak(fd1); +} + +TEST_F(Read, mmap) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t len; + size_t bufsize = strlen(CONTENTS); + void *p; + + len = getpagesize(); + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.fh == Read::FH && + in.body.read.offset == 0 && + in.body.read.size == bufsize); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + out.header.len = sizeof(struct fuse_out_header) + bufsize; + memmove(out.body.bytes, CONTENTS, bufsize); + }))); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0); + ASSERT_NE(MAP_FAILED, p) << strerror(errno); + + ASSERT_EQ(0, memcmp(p, CONTENTS, bufsize)); + + ASSERT_EQ(0, munmap(p, len)) << strerror(errno); + leak(fd); +} + + +/* When max_read is set, large reads will be split up as necessary */ +TEST_F(ReadMaxRead, split) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = 65536; + ssize_t fragsize = bufsize / 4; + char *rbuf, *frag0, *frag1, *frag2, *frag3; + + rbuf = new char[bufsize](); + frag0 = new char[fragsize](); + frag1 = new char[fragsize](); + frag2 = new char[fragsize](); + frag3 = new char[fragsize](); + memset(frag0, '0', fragsize); + memset(frag1, '1', fragsize); + memset(frag2, '2', fragsize); + memset(frag3, '3', fragsize); + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, fragsize, fragsize, frag0); + expect_read(ino, fragsize, fragsize, fragsize, frag1); + expect_read(ino, 2 * fragsize, fragsize, fragsize, frag2); + expect_read(ino, 3 * fragsize, fragsize, fragsize, frag3); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, read(fd, rbuf, bufsize)) << strerror(errno); + ASSERT_EQ(0, memcmp(rbuf, frag0, fragsize)); + ASSERT_EQ(0, memcmp(rbuf + fragsize, frag1, fragsize)); + ASSERT_EQ(0, memcmp(rbuf + 2 * fragsize, frag2, fragsize)); + ASSERT_EQ(0, memcmp(rbuf + 3 * fragsize, frag3, fragsize)); + + delete[] frag3; + delete[] frag2; + delete[] frag1; + delete[] frag0; + delete[] rbuf; + leak(fd); +} + +/* + * The kernel should not update the cached atime attribute during a read, if + * MNT_NOATIME is used. + */ +TEST_F(ReadNoatime, atime) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + struct stat sb1, sb2; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb1)); + + nap(); + + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb2)); + + /* The kernel should not update atime during read */ + EXPECT_TRUE(timespeccmp(&sb1.st_atim, &sb2.st_atim, ==)); + EXPECT_TRUE(timespeccmp(&sb1.st_ctim, &sb2.st_ctim, ==)); + EXPECT_TRUE(timespeccmp(&sb1.st_mtim, &sb2.st_mtim, ==)); + + leak(fd); +} + +/* + * The kernel should not update the cached atime attribute during a cached + * read, if MNT_NOATIME is used. + */ +TEST_F(ReadNoatime, atime_cached) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + struct stat sb1, sb2; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, pread(fd, buf, bufsize, 0)) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb1)); + + nap(); + + ASSERT_EQ(bufsize, pread(fd, buf, bufsize, 0)) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb2)); + + /* The kernel should automatically update atime during read */ + EXPECT_TRUE(timespeccmp(&sb1.st_atim, &sb2.st_atim, ==)); + EXPECT_TRUE(timespeccmp(&sb1.st_ctim, &sb2.st_ctim, ==)); + EXPECT_TRUE(timespeccmp(&sb1.st_mtim, &sb2.st_mtim, ==)); + + leak(fd); +} + +/* Read of an mmap()ed file fails */ +TEST_F(ReadSigbus, mmap_eio) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + struct sigaction sa; + uint64_t ino = 42; + int fd; + ssize_t len; + size_t bufsize = strlen(CONTENTS); + void *p; + + len = getpagesize(); + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.fh == Read::FH); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnErrno(EIO))); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0); + ASSERT_NE(MAP_FAILED, p) << strerror(errno); + + /* Accessing the mapped page should return SIGBUS. */ + + bzero(&sa, sizeof(sa)); + sa.sa_handler = SIG_DFL; + sa.sa_sigaction = handle_sigbus; + sa.sa_flags = SA_RESETHAND | SA_SIGINFO; + ASSERT_EQ(0, sigaction(SIGBUS, &sa, NULL)) << strerror(errno); + if (setjmp(ReadSigbus::s_jmpbuf) == 0) { + atomic_signal_fence(std::memory_order::memory_order_seq_cst); + volatile char x __unused = *(volatile char*)p; + FAIL() << "shouldn't get here"; + } + + ASSERT_EQ(p, ReadSigbus::s_si_addr); + ASSERT_EQ(0, munmap(p, len)) << strerror(errno); + leak(fd); +} + +/* + * A read via mmap comes up short, indicating that the file was truncated + * server-side. + */ +TEST_F(Read, mmap_eof) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t len; + size_t bufsize = strlen(CONTENTS); + struct stat sb; + void *p; + + len = getpagesize(); + + expect_lookup(RELPATH, ino, m_maxbcachebuf); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.fh == Read::FH && + in.body.read.offset == 0 && + in.body.read.size == (uint32_t)m_maxbcachebuf); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + out.header.len = sizeof(struct fuse_out_header) + bufsize; + memmove(out.body.bytes, CONTENTS, bufsize); + }))); + expect_getattr(ino, bufsize); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0); + ASSERT_NE(MAP_FAILED, p) << strerror(errno); + + /* The file size should be automatically truncated */ + ASSERT_EQ(0, memcmp(p, CONTENTS, bufsize)); + ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); + EXPECT_EQ((off_t)bufsize, sb.st_size); + + ASSERT_EQ(0, munmap(p, len)) << strerror(errno); + leak(fd); +} + +/* + * During VOP_GETPAGES, the FUSE server fails a FUSE_GETATTR operation. This + * almost certainly indicates a buggy FUSE server, and our goal should be not + * to panic. Instead, generate SIGBUS. + */ +TEST_F(ReadSigbus, mmap_getblksz_fail) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + struct sigaction sa; + Sequence seq; + uint64_t ino = 42; + int fd; + ssize_t len; + size_t bufsize = strlen(CONTENTS); + mode_t mode = S_IFREG | 0644; + void *p; + + len = getpagesize(); + + FuseTest::expect_lookup(RELPATH, ino, mode, bufsize, 1, 0); + /* Expect two GETATTR calls that succeed, followed by one that fail. */ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(2) + .InSequence(seq) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = mode; + out.body.attr.attr.size = bufsize; + out.body.attr.attr_valid = 0; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).InSequence(seq) + .WillRepeatedly(Invoke(ReturnErrno(EIO))); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ); + }, Eq(true)), + _) + ).Times(0); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0); + ASSERT_NE(MAP_FAILED, p) << strerror(errno); + + /* Accessing the mapped page should return SIGBUS. */ + bzero(&sa, sizeof(sa)); + sa.sa_handler = SIG_DFL; + sa.sa_sigaction = handle_sigbus; + sa.sa_flags = SA_RESETHAND | SA_SIGINFO; + ASSERT_EQ(0, sigaction(SIGBUS, &sa, NULL)) << strerror(errno); + if (setjmp(ReadSigbus::s_jmpbuf) == 0) { + atomic_signal_fence(std::memory_order::memory_order_seq_cst); + volatile char x __unused = *(volatile char*)p; + FAIL() << "shouldn't get here"; + } + + ASSERT_EQ(p, ReadSigbus::s_si_addr); + ASSERT_EQ(0, munmap(p, len)) << strerror(errno); + leak(fd); +} + +/* + * Just as when FOPEN_DIRECT_IO is used, reads with O_DIRECT should bypass + * cache and to straight to the daemon + */ +TEST_F(Read, o_direct) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + // Fill the cache + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); + + // Reads with o_direct should bypass the cache + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + ASSERT_EQ(0, fcntl(fd, F_SETFL, O_DIRECT)) << strerror(errno); + ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); + + leak(fd); +} + +TEST_F(Read, pread) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + /* + * Set offset to a maxbcachebuf boundary so we'll be sure what offset + * to read from. Without this, the read might start at a lower offset. + */ + uint64_t offset = m_maxbcachebuf; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino, offset + bufsize); + expect_open(ino, 0, 1); + expect_read(ino, offset, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, pread(fd, buf, bufsize, offset)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); + leak(fd); +} + +TEST_F(Read, read) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); + + leak(fd); +} + +TEST_F(Read_7_8, read) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); + + leak(fd); +} + +/* + * If cacheing is enabled, the kernel should try to read an entire cache block + * at a time. + */ +TEST_F(Read, cache_block) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS0 = "abcdefghijklmnop"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = 8; + ssize_t filesize = m_maxbcachebuf * 2; + char *contents; + char buf[bufsize]; + const char *contents1 = CONTENTS0 + bufsize; + + contents = new char[filesize](); + memmove(contents, CONTENTS0, strlen(CONTENTS0)); + + expect_lookup(RELPATH, ino, filesize); + expect_open(ino, 0, 1); + expect_read(ino, 0, m_maxbcachebuf, m_maxbcachebuf, + contents); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS0, bufsize)); + + /* A subsequent read should be serviced by cache */ + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, contents1, bufsize)); + leak(fd); + delete[] contents; +} + +/* Reading with sendfile should work (though it obviously won't be 0-copy) */ +TEST_F(Read, sendfile) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + size_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + int sp[2]; + off_t sbytes; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.fh == Read::FH && + in.body.read.offset == 0 && + in.body.read.size == bufsize); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + out.header.len = sizeof(struct fuse_out_header) + bufsize; + memmove(out.body.bytes, CONTENTS, bufsize); + }))); + + ASSERT_EQ(0, socketpair(PF_LOCAL, SOCK_STREAM, 0, sp)) + << strerror(errno); + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(0, sendfile(fd, sp[1], 0, bufsize, NULL, &sbytes, 0)) + << strerror(errno); + ASSERT_EQ(static_cast<ssize_t>(bufsize), read(sp[0], buf, bufsize)) + << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); + + close(sp[1]); + close(sp[0]); + leak(fd); +} + +/* sendfile should fail gracefully if fuse declines the read */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236466 */ +TEST_F(Read, sendfile_eio) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + int sp[2]; + off_t sbytes; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(EIO))); + + ASSERT_EQ(0, socketpair(PF_LOCAL, SOCK_STREAM, 0, sp)) + << strerror(errno); + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_NE(0, sendfile(fd, sp[1], 0, bufsize, NULL, &sbytes, 0)); + + close(sp[1]); + close(sp[0]); + leak(fd); +} + +/* + * Sequential reads should use readahead. And if allowed, large reads should + * be clustered. + */ +TEST_P(ReadAhead, readahead) { + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd, maxcontig, clustersize; + ssize_t bufsize = 4 * m_maxbcachebuf; + ssize_t filesize = bufsize; + uint64_t len; + char *rbuf, *contents; + off_t offs; + + contents = new char[filesize]; + memset(contents, 'X', filesize); + rbuf = new char[bufsize](); + + expect_lookup(RELPATH, ino, filesize); + expect_open(ino, 0, 1); + maxcontig = m_noclusterr ? m_maxbcachebuf : + m_maxbcachebuf + m_maxreadahead; + clustersize = MIN((unsigned long )maxcontig, m_maxphys); + for (offs = 0; offs < bufsize; offs += clustersize) { + len = std::min((size_t)clustersize, (size_t)(filesize - offs)); + expect_read(ino, offs, len, len, contents + offs); + } + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + /* Set the internal readahead counter to a "large" value */ + ASSERT_EQ(0, fcntl(fd, F_READAHEAD, 1'000'000'000)) << strerror(errno); + + ASSERT_EQ(bufsize, read(fd, rbuf, bufsize)) << strerror(errno); + ASSERT_EQ(0, memcmp(rbuf, contents, bufsize)); + + leak(fd); + delete[] rbuf; + delete[] contents; +} + +INSTANTIATE_TEST_SUITE_P(RA, ReadAhead, + Values(tuple<bool, int>(false, 0), + tuple<bool, int>(false, 1), + tuple<bool, int>(false, 2), + tuple<bool, int>(false, 3), + tuple<bool, int>(true, 0), + tuple<bool, int>(true, 1), + tuple<bool, int>(true, 2))); + +/* With read-only mounts, fuse should never update atime during close */ +TEST_F(RofsRead, atime_during_close) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + EXPECT_CALL(*m_mock, process( + ResultOf([&](auto in) { + return (in.header.opcode == FUSE_SETATTR); + }, Eq(true)), + _) + ).Times(0); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, FuseTest::FH); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + /* Ensure atime will be different than during lookup */ + nap(); + + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + + close(fd); +} + +/* fuse_init_out.time_gran controls the granularity of timestamps */ +TEST_P(TimeGran, atime_during_setattr) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + uint64_t ino = 42; + const mode_t newmode = 0755; + int fd; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + uint32_t valid = FATTR_MODE | FATTR_ATIME; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + in.body.setattr.atimensec % m_time_gran == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFREG | newmode; + }))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno); + + leak(fd); +} + +INSTANTIATE_TEST_SUITE_P(TG, TimeGran, Range(0u, 10u)); diff --git a/tests/sys/fs/fusefs/readdir.cc b/tests/sys/fs/fusefs/readdir.cc new file mode 100644 index 000000000000..6b78e3a70697 --- /dev/null +++ b/tests/sys/fs/fusefs/readdir.cc @@ -0,0 +1,516 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <dirent.h> +#include <fcntl.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; +using namespace std; + +class Readdir: public FuseTest { +public: +void expect_lookup(const char *relpath, uint64_t ino) +{ + FuseTest::expect_lookup(relpath, ino, S_IFDIR | 0755, 0, 1); +} +}; + +class Readdir_7_8: public Readdir { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + Readdir::SetUp(); +} + +void expect_lookup(const char *relpath, uint64_t ino) +{ + FuseTest::expect_lookup_7_8(relpath, ino, S_IFDIR | 0755, 0, 1); +} +}; + +const char dot[] = "."; +const char dotdot[] = ".."; + +/* FUSE_READDIR returns nothing but "." and ".." */ +TEST_F(Readdir, dots) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + DIR *dir; + struct dirent *de; + vector<struct dirent> ents(2); + vector<struct dirent> empty_ents(0); + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + ents[0].d_fileno = 2; + ents[0].d_off = 2000; + ents[0].d_namlen = sizeof(dotdot); + ents[0].d_type = DT_DIR; + strncpy(ents[0].d_name, dotdot, ents[0].d_namlen); + ents[1].d_fileno = 3; + ents[1].d_off = 3000; + ents[1].d_namlen = sizeof(dot); + ents[1].d_type = DT_DIR; + strncpy(ents[1].d_name, dot, ents[1].d_namlen); + expect_readdir(ino, 0, ents); + expect_readdir(ino, 3000, empty_ents); + + errno = 0; + dir = opendir(FULLPATH); + ASSERT_NE(nullptr, dir) << strerror(errno); + + errno = 0; + de = readdir(dir); + ASSERT_NE(nullptr, de) << strerror(errno); + EXPECT_EQ(2ul, de->d_fileno); + EXPECT_EQ(DT_DIR, de->d_type); + EXPECT_EQ(sizeof(dotdot), de->d_namlen); + EXPECT_EQ(0, strcmp(dotdot, de->d_name)); + + errno = 0; + de = readdir(dir); + ASSERT_NE(nullptr, de) << strerror(errno); + EXPECT_EQ(3ul, de->d_fileno); + EXPECT_EQ(DT_DIR, de->d_type); + EXPECT_EQ(sizeof(dot), de->d_namlen); + EXPECT_EQ(0, strcmp(dot, de->d_name)); + + ASSERT_EQ(nullptr, readdir(dir)); + ASSERT_EQ(0, errno); + + leakdir(dir); +} + +TEST_F(Readdir, eio) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + DIR *dir; + struct dirent *de; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READDIR && + in.header.nodeid == ino && + in.body.readdir.offset == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(EIO))); + + errno = 0; + dir = opendir(FULLPATH); + ASSERT_NE(nullptr, dir) << strerror(errno); + + errno = 0; + de = readdir(dir); + ASSERT_EQ(nullptr, de); + ASSERT_EQ(EIO, errno); + + leakdir(dir); +} + +/* + * getdirentries(2) can use a larger buffer size than readdir(3). It also has + * some additional non-standardized fields in the returned dirent. + */ +TEST_F(Readdir, getdirentries_empty) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + int fd; + char buf[8192]; + ssize_t r; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READDIR && + in.header.nodeid == ino && + in.body.readdir.size == 8192); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + out.header.error = 0; + out.header.len = sizeof(out.header); + }))); + + fd = open(FULLPATH, O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + r = getdirentries(fd, buf, sizeof(buf), 0); + ASSERT_EQ(0, r) << strerror(errno); + + leak(fd); +} + +/* + * The dirent.d_off field can be used with lseek to position the directory so + * that getdirentries will return the subsequent dirent. + */ +TEST_F(Readdir, getdirentries_seek) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + vector<struct dirent> ents0(2); + vector<struct dirent> ents1(1); + uint64_t ino = 42; + int fd; + const size_t bufsize = 8192; + char buf[bufsize]; + struct dirent *de0, *de1; + ssize_t r; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + + ents0[0].d_fileno = 2; + ents0[0].d_off = 2000; + ents0[0].d_namlen = sizeof(dotdot); + ents0[0].d_type = DT_DIR; + strncpy(ents0[0].d_name, dotdot, ents0[0].d_namlen); + expect_readdir(ino, 0, ents0); + ents0[1].d_fileno = 3; + ents0[1].d_off = 3000; + ents0[1].d_namlen = sizeof(dot); + ents0[1].d_type = DT_DIR; + ents1[0].d_fileno = 3; + ents1[0].d_off = 3000; + ents1[0].d_namlen = sizeof(dot); + ents1[0].d_type = DT_DIR; + strncpy(ents1[0].d_name, dot, ents1[0].d_namlen); + expect_readdir(ino, 0, ents0); + expect_readdir(ino, 2000, ents1); + + fd = open(FULLPATH, O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + r = getdirentries(fd, buf, sizeof(buf), 0); + ASSERT_LT(0, r) << strerror(errno); + de0 = (struct dirent*)&buf[0]; + ASSERT_EQ(2000, de0->d_off); + ASSERT_LT(de0->d_reclen + offsetof(struct dirent, d_fileno), bufsize); + de1 = (struct dirent*)(&(buf[de0->d_reclen])); + ASSERT_EQ(3ul, de1->d_fileno); + + r = lseek(fd, de0->d_off, SEEK_SET); + ASSERT_LE(0, r); + r = getdirentries(fd, buf, sizeof(buf), 0); + ASSERT_LT(0, r) << strerror(errno); + de0 = (struct dirent*)&buf[0]; + ASSERT_EQ(3000, de0->d_off); +} + +/* + * Nothing bad should happen if getdirentries is called on two file descriptors + * which were concurrently open, but one has already been closed. + * This is a regression test for a specific bug dating from r238402. + */ +TEST_F(Readdir, getdirentries_concurrent) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + int fd0, fd1; + char buf[8192]; + ssize_t r; + + FuseTest::expect_lookup(RELPATH, ino, S_IFDIR | 0755, 0, 2); + expect_opendir(ino); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READDIR && + in.header.nodeid == ino && + in.body.readdir.size == 8192); + }, Eq(true)), + _) + ).Times(2) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + out.header.error = 0; + out.header.len = sizeof(out.header); + }))); + + fd0 = open(FULLPATH, O_DIRECTORY); + ASSERT_LE(0, fd0) << strerror(errno); + + fd1 = open(FULLPATH, O_DIRECTORY); + ASSERT_LE(0, fd1) << strerror(errno); + + r = getdirentries(fd0, buf, sizeof(buf), 0); + ASSERT_EQ(0, r) << strerror(errno); + + EXPECT_EQ(0, close(fd0)) << strerror(errno); + + r = getdirentries(fd1, buf, sizeof(buf), 0); + ASSERT_EQ(0, r) << strerror(errno); + + leak(fd0); + leak(fd1); +} + +/* + * FUSE_READDIR returns nothing, not even "." and "..". This is legal, though + * the filesystem obviously won't be fully functional. + */ +TEST_F(Readdir, nodots) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + DIR *dir; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READDIR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + out.header.error = 0; + out.header.len = sizeof(out.header); + }))); + + errno = 0; + dir = opendir(FULLPATH); + ASSERT_NE(nullptr, dir) << strerror(errno); + errno = 0; + ASSERT_EQ(nullptr, readdir(dir)); + ASSERT_EQ(0, errno); + + leakdir(dir); +} + +/* + * FUSE_READDIR returns a path with an embedded NUL. Obviously illegal, but + * nothing bad should happen. + */ +TEST_F(Readdir, nul) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + DIR *dir; + struct dirent *de; + vector<struct dirent> ents(1); + vector<struct dirent> empty_ents(0); + const char nul[] = "foo\0bar"; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + ents[0].d_fileno = 4; + ents[0].d_off = 4000; + ents[0].d_namlen = sizeof(nul); + ents[0].d_type = DT_REG; + strncpy(ents[0].d_name, nul, ents[0].d_namlen); + expect_readdir(ino, 0, ents); + expect_readdir(ino, 4000, empty_ents); + + errno = 0; + dir = opendir(FULLPATH); + ASSERT_NE(nullptr, dir) << strerror(errno); + + errno = 0; + de = readdir(dir); + ASSERT_NE(nullptr, de) << strerror(errno); + EXPECT_EQ(4ul, de->d_fileno); + EXPECT_EQ(DT_REG, de->d_type); + EXPECT_EQ(sizeof(nul), de->d_namlen); + EXPECT_EQ(0, strcmp(nul, de->d_name)); + + ASSERT_EQ(nullptr, readdir(dir)); + ASSERT_EQ(0, errno); + + leakdir(dir); +} + + +/* telldir(3) and seekdir(3) should work with fuse */ +TEST_F(Readdir, seekdir) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + DIR *dir; + struct dirent *de; + /* + * use enough entries to be > 4096 bytes, so getdirentries must be + * called + * multiple times. + */ + vector<struct dirent> ents0(122), ents1(102), ents2(30); + long bookmark; + int i = 0; + + for (auto& it: ents0) { + snprintf(it.d_name, MAXNAMLEN, "file.%d", i); + it.d_fileno = 2 + i; + it.d_off = (2 + i) * 1000; + it.d_namlen = strlen(it.d_name); + it.d_type = DT_REG; + i++; + } + for (auto& it: ents1) { + snprintf(it.d_name, MAXNAMLEN, "file.%d", i); + it.d_fileno = 2 + i; + it.d_off = (2 + i) * 1000; + it.d_namlen = strlen(it.d_name); + it.d_type = DT_REG; + i++; + } + for (auto& it: ents2) { + snprintf(it.d_name, MAXNAMLEN, "file.%d", i); + it.d_fileno = 2 + i; + it.d_off = (2 + i) * 1000; + it.d_namlen = strlen(it.d_name); + it.d_type = DT_REG; + i++; + } + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + + expect_readdir(ino, 0, ents0); + expect_readdir(ino, 123000, ents1); + expect_readdir(ino, 225000, ents2); + + errno = 0; + dir = opendir(FULLPATH); + ASSERT_NE(nullptr, dir) << strerror(errno); + + for (i=0; i < 128; i++) { + errno = 0; + de = readdir(dir); + ASSERT_NE(nullptr, de) << strerror(errno); + EXPECT_EQ(2 + (ino_t)i, de->d_fileno); + } + bookmark = telldir(dir); + + for (; i < 232; i++) { + errno = 0; + de = readdir(dir); + ASSERT_NE(nullptr, de) << strerror(errno); + EXPECT_EQ(2 + (ino_t)i, de->d_fileno); + } + + seekdir(dir, bookmark); + de = readdir(dir); + ASSERT_NE(nullptr, de) << strerror(errno); + EXPECT_EQ(130ul, de->d_fileno); + + leakdir(dir); +} + +/* + * FUSE_READDIR returns a path with an embedded /. Obviously illegal, but + * nothing bad should happen. + */ +TEST_F(Readdir, slash) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + DIR *dir; + struct dirent *de; + vector<struct dirent> ents(1); + vector<struct dirent> empty_ents(0); + const char foobar[] = "foo/bar"; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + ents[0].d_fileno = 4; + ents[0].d_off = 4000; + ents[0].d_namlen = sizeof(foobar); + ents[0].d_type = DT_REG; + strncpy(ents[0].d_name, foobar, ents[0].d_namlen); + expect_readdir(ino, 0, ents); + expect_readdir(ino, 4000, empty_ents); + + errno = 0; + dir = opendir(FULLPATH); + ASSERT_NE(nullptr, dir) << strerror(errno); + + errno = 0; + de = readdir(dir); + ASSERT_NE(nullptr, de) << strerror(errno); + EXPECT_EQ(4ul, de->d_fileno); + EXPECT_EQ(DT_REG, de->d_type); + EXPECT_EQ(sizeof(foobar), de->d_namlen); + EXPECT_EQ(0, strcmp(foobar, de->d_name)); + + ASSERT_EQ(nullptr, readdir(dir)); + ASSERT_EQ(0, errno); + + leakdir(dir); +} + +TEST_F(Readdir_7_8, nodots) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + DIR *dir; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READDIR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + out.header.error = 0; + out.header.len = sizeof(out.header); + }))); + + errno = 0; + dir = opendir(FULLPATH); + ASSERT_NE(nullptr, dir) << strerror(errno); + errno = 0; + ASSERT_EQ(nullptr, readdir(dir)); + ASSERT_EQ(0, errno); + + leakdir(dir); +} diff --git a/tests/sys/fs/fusefs/readlink.cc b/tests/sys/fs/fusefs/readlink.cc new file mode 100644 index 000000000000..30815f2cd4b6 --- /dev/null +++ b/tests/sys/fs/fusefs/readlink.cc @@ -0,0 +1,162 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> + +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Readlink: public FuseTest { +public: +void expect_lookup(const char *relpath, uint64_t ino) +{ + FuseTest::expect_lookup(relpath, ino, S_IFLNK | 0777, 0, 1); +} +void expect_readlink(uint64_t ino, ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READLINK && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(r)); +} + +}; + +class PushSymlinksIn: public Readlink { + virtual void SetUp() { + m_push_symlinks_in = true; + Readlink::SetUp(); + } +}; + +TEST_F(Readlink, eloop) +{ + const char FULLPATH[] = "mountpoint/src"; + const char RELPATH[] = "src"; + const uint64_t ino = 42; + char buf[80]; + + expect_lookup(RELPATH, ino); + expect_readlink(ino, ReturnErrno(ELOOP)); + + EXPECT_EQ(-1, readlink(FULLPATH, buf, sizeof(buf))); + EXPECT_EQ(ELOOP, errno); +} + +/* + * If a malicious or buggy server returns a NUL in the FUSE_READLINK result, it + * should be handled gracefully. + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=274268 + */ +TEST_F(Readlink, embedded_nul) +{ + const char FULLPATH[] = "mountpoint/src"; + const char RELPATH[] = "src"; + const char dst[] = "dst\0stuff"; + char buf[80]; + const uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFLNK | 0777; + out.body.entry.nodeid = ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READLINK && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + memcpy(out.body.str, dst, sizeof(dst)); + out.header.len = sizeof(out.header) + sizeof(dst) + 1; + }))); + + EXPECT_EQ(-1, readlink(FULLPATH, buf, sizeof(buf))); + EXPECT_EQ(EIO, errno); + EXPECT_EQ(-1, access(FULLPATH, R_OK)); + EXPECT_EQ(EIO, errno); +} + +TEST_F(Readlink, ok) +{ + const char FULLPATH[] = "mountpoint/src"; + const char RELPATH[] = "src"; + const char dst[] = "dst"; + const uint64_t ino = 42; + char buf[80]; + + expect_lookup(RELPATH, ino); + expect_readlink(ino, ReturnImmediate([=](auto in __unused, auto& out) { + strlcpy(out.body.str, dst, sizeof(out.body.str)); + out.header.len = sizeof(out.header) + strlen(dst) + 1; + })); + + EXPECT_EQ(static_cast<ssize_t>(strlen(dst)) + 1, + readlink(FULLPATH, buf, sizeof(buf))); + EXPECT_STREQ(dst, buf); +} + +TEST_F(PushSymlinksIn, readlink) +{ + const char FULLPATH[] = "mountpoint/src"; + const char RELPATH[] = "src"; + const char dst[] = "/dst"; + const uint64_t ino = 42; + char buf[MAXPATHLEN], wd[MAXPATHLEN], want[MAXPATHLEN]; + int len; + + expect_lookup(RELPATH, ino); + expect_readlink(ino, ReturnImmediate([=](auto in __unused, auto& out) { + strlcpy(out.body.str, dst, sizeof(out.body.str)); + out.header.len = sizeof(out.header) + strlen(dst) + 1; + })); + + ASSERT_NE(nullptr, getcwd(wd, sizeof(wd))) << strerror(errno); + len = snprintf(want, sizeof(want), "%s/mountpoint%s", wd, dst); + ASSERT_LE(0, len) << strerror(errno); + + EXPECT_EQ(static_cast<ssize_t>(len) + 1, + readlink(FULLPATH, buf, sizeof(buf))); + EXPECT_STREQ(want, buf); +} diff --git a/tests/sys/fs/fusefs/release.cc b/tests/sys/fs/fusefs/release.cc new file mode 100644 index 000000000000..9df236bfbaf7 --- /dev/null +++ b/tests/sys/fs/fusefs/release.cc @@ -0,0 +1,289 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/socket.h> +#include <sys/un.h> + +#include <fcntl.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Release: public FuseTest { + +public: +void expect_lookup(const char *relpath, uint64_t ino, int times) +{ + FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, times); +} + +void expect_release(uint64_t ino, uint64_t lock_owner, + uint32_t flags, int error) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_RELEASE && + in.header.nodeid == ino && + in.body.release.lock_owner == lock_owner && + in.body.release.fh == FH && + in.body.release.flags == flags); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(error))) + .RetiresOnSaturation(); +} +}; + +class ReleaseWithLocks: public Release { + virtual void SetUp() { + m_init_flags = FUSE_POSIX_LOCKS; + Release::SetUp(); + } +}; + + +/* If a file descriptor is duplicated, only the last close causes RELEASE */ +TEST_F(Release, dup) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd, fd2; + + expect_lookup(RELPATH, ino, 1); + expect_open(ino, 0, 1); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, getpid(), O_RDONLY, 0); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + fd2 = dup(fd); + ASSERT_LE(0, fd2) << strerror(errno); + + ASSERT_EQ(0, close(fd2)) << strerror(errno); + ASSERT_EQ(0, close(fd)) << strerror(errno); +} + +/* + * Some FUSE filesystem cache data internally and flush it on release. Such + * filesystems may generate errors during release. On Linux, these get + * returned by close(2). However, POSIX does not require close(2) to return + * this error. FreeBSD's fuse(4) should return EIO if it returns an error at + * all. + */ +/* http://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html */ +TEST_F(Release, eio) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino, 1); + expect_open(ino, 0, 1); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, getpid(), O_WRONLY, EIO); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_TRUE(0 == close(fd) || errno == EIO) << strerror(errno); +} + +/* + * FUSE_RELEASE should contain the same flags used for FUSE_OPEN + */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */ +TEST_F(Release, DISABLED_flags) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino, 1); + expect_open(ino, 0, 1); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, getpid(), O_RDWR | O_APPEND, 0); + + fd = open(FULLPATH, O_RDWR | O_APPEND); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(0, close(fd)) << strerror(errno); +} + +/* + * fuse(4) will issue multiple FUSE_OPEN operations for the same file if it's + * opened with different modes. Each FUSE_OPEN should get its own + * FUSE_RELEASE. + */ +TEST_F(Release, multiple_opens) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd, fd2; + + expect_lookup(RELPATH, ino, 2); + expect_open(ino, 0, 2); + expect_flush(ino, 2, ReturnErrno(0)); + expect_release(ino, getpid(), O_RDONLY, 0); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + expect_release(ino, getpid(), O_WRONLY, 0); + fd2 = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd2) << strerror(errno); + + ASSERT_EQ(0, close(fd2)) << strerror(errno); + ASSERT_EQ(0, close(fd)) << strerror(errno); +} + +TEST_F(Release, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino, 1); + expect_open(ino, 0, 1); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, getpid(), O_RDONLY, 0); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(0, close(fd)) << strerror(errno); +} + +/* + * Nothing bad should happen when closing a Unix-domain named socket that + * contains a fusefs file descriptor within its receive buffer. + * Regression test for + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=289686 + */ +TEST_F(Release, scm_rights) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct msghdr msg; + struct iovec iov; + char message[CMSG_SPACE(sizeof(int))]; + uint64_t ino = 42; + int fd; + int s[2]; + union { + char buf[CMSG_SPACE(sizeof(fd))]; + struct cmsghdr align; + } u; + + expect_lookup(RELPATH, ino, 1); + expect_open(ino, 0, 1); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, getpid(), O_RDONLY, 0); + + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, s)) << strerror(errno); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + memset(&message, 0, sizeof(message)); + memset(&msg, 0, sizeof(msg)); + iov.iov_base = NULL; + iov.iov_len = 0; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = u.buf, + msg.msg_controllen = sizeof(u.buf); + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(fd)); + memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd)); + ASSERT_GE(sendmsg(s[0], &msg, 0), 0) << strerror(errno); + + close(fd); // Close fd within our process + close(s[0]); + close(s[1]); // The last copy of fd is within this socket's rcvbuf +} + +/* When closing a file with a POSIX file lock, release should release the lock*/ +TEST_F(ReleaseWithLocks, unlock_on_close) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + struct flock fl; + pid_t pid = getpid(); + + expect_lookup(RELPATH, ino, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETLK && + in.header.nodeid == ino && + in.body.setlk.lk.type == F_RDLCK && + in.body.setlk.fh == FH); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(0))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETLK && + in.header.nodeid == ino && + in.body.setlk.lk.type == F_UNLCK && + in.body.setlk.fh == FH); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(0))); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, static_cast<uint64_t>(pid), O_RDWR, 0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + fl.l_start = 0; + fl.l_len = 0; + fl.l_pid = pid; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_SET; + fl.l_sysid = 0; + ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno); + + ASSERT_EQ(0, close(fd)) << strerror(errno); +} diff --git a/tests/sys/fs/fusefs/releasedir.cc b/tests/sys/fs/fusefs/releasedir.cc new file mode 100644 index 000000000000..0f9337a5b761 --- /dev/null +++ b/tests/sys/fs/fusefs/releasedir.cc @@ -0,0 +1,116 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <dirent.h> +#include <fcntl.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class ReleaseDir: public FuseTest { + +public: +void expect_lookup(const char *relpath, uint64_t ino) +{ + FuseTest::expect_lookup(relpath, ino, S_IFDIR | 0755, 0, 1); +} +}; + +/* If a file descriptor is duplicated, only the last close causes RELEASE */ +TEST_F(ReleaseDir, dup) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + DIR *dir, *dir2; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READDIR && + in.header.nodeid == ino && + in.body.readdir.offset == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + out.header.error = 0; + out.header.len = sizeof(out.header); + }))); + expect_releasedir(ino, ReturnErrno(0)); + + dir = opendir(FULLPATH); + ASSERT_NE(nullptr, dir) << strerror(errno); + + dir2 = fdopendir(dup(dirfd(dir))); + ASSERT_NE(nullptr, dir2) << strerror(errno); + + ASSERT_EQ(0, closedir(dir)) << strerror(errno); + ASSERT_EQ(0, closedir(dir2)) << strerror(errno); +} + +TEST_F(ReleaseDir, ok) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + DIR *dir; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + expect_releasedir(ino, ReturnErrno(0)); + + dir = opendir(FULLPATH); + ASSERT_NE(nullptr, dir) << strerror(errno); + + ASSERT_EQ(0, closedir(dir)) << strerror(errno); +} + +/* Directories opened O_EXEC should be properly released, too */ +TEST_F(ReleaseDir, o_exec) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH, ino); + expect_opendir(ino); + expect_releasedir(ino, ReturnErrno(0)); + + fd = open(FULLPATH, O_EXEC | O_DIRECTORY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(0, close(fd)) << strerror(errno); +} diff --git a/tests/sys/fs/fusefs/rename.cc b/tests/sys/fs/fusefs/rename.cc new file mode 100644 index 000000000000..6b5687e209c6 --- /dev/null +++ b/tests/sys/fs/fusefs/rename.cc @@ -0,0 +1,330 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <stdlib.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Rename: public FuseTest { + public: + int tmpfd = -1; + char tmpfile[80] = "/tmp/fuse.rename.XXXXXX"; + + virtual void TearDown() { + if (tmpfd >= 0) { + close(tmpfd); + unlink(tmpfile); + } + + FuseTest::TearDown(); + } +}; + +// EINVAL, dst is subdir of src +TEST_F(Rename, einval) +{ + const char FULLDST[] = "mountpoint/src/dst"; + const char RELDST[] = "dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELSRC[] = "src"; + uint64_t src_ino = 42; + + expect_lookup(RELSRC, src_ino, S_IFDIR | 0755, 0, 2); + EXPECT_LOOKUP(src_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT))); + + ASSERT_NE(0, rename(FULLSRC, FULLDST)); + ASSERT_EQ(EINVAL, errno); +} + +// source does not exist +TEST_F(Rename, enoent) +{ + const char FULLDST[] = "mountpoint/dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELSRC[] = "src"; + // FUSE hardcodes the mountpoint to inode 1 + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELSRC) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + + ASSERT_NE(0, rename(FULLSRC, FULLDST)); + ASSERT_EQ(ENOENT, errno); +} + +/* + * Renaming a file after FUSE_LOOKUP returned a negative cache entry for dst + */ +TEST_F(Rename, entry_cache_negative) +{ + const char FULLDST[] = "mountpoint/dst"; + const char RELDST[] = "dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELSRC[] = "src"; + uint64_t dst_dir_ino = FUSE_ROOT_ID; + uint64_t ino = 42; + /* + * Set entry_valid = 0 because this test isn't concerned with whether + * or not we actually cache negative entries, only with whether we + * interpret negative cache responses correctly. + */ + struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0}; + + expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1); + /* LOOKUP returns a negative cache entry for dst */ + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) + .WillOnce(ReturnNegativeCache(&entry_valid)); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *src = (const char*)in.body.bytes + + sizeof(fuse_rename_in); + const char *dst = src + strlen(src) + 1; + return (in.header.opcode == FUSE_RENAME && + in.body.rename.newdir == dst_dir_ino && + (0 == strcmp(RELDST, dst)) && + (0 == strcmp(RELSRC, src))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(0))); + + ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); +} + +/* + * Renaming a file should purge any negative namecache entries for the dst + */ +TEST_F(Rename, entry_cache_negative_purge) +{ + const char FULLDST[] = "mountpoint/dst"; + const char RELDST[] = "dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELSRC[] = "src"; + uint64_t dst_dir_ino = FUSE_ROOT_ID; + uint64_t ino = 42; + struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0}; + + expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1); + /* LOOKUP returns a negative cache entry for dst */ + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) + .WillOnce(ReturnNegativeCache(&entry_valid)) + .RetiresOnSaturation(); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *src = (const char*)in.body.bytes + + sizeof(fuse_rename_in); + const char *dst = src + strlen(src) + 1; + return (in.header.opcode == FUSE_RENAME && + in.body.rename.newdir == dst_dir_ino && + (0 == strcmp(RELDST, dst)) && + (0 == strcmp(RELSRC, src))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(0))); + + ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); + + /* Finally, a subsequent lookup should query the daemon */ + expect_lookup(RELDST, ino, S_IFREG | 0644, 0, 1); + + ASSERT_EQ(0, access(FULLDST, F_OK)) << strerror(errno); +} + +TEST_F(Rename, exdev) +{ + const char FULLB[] = "mountpoint/src"; + const char RELB[] = "src"; + // FUSE hardcodes the mountpoint to inode 1 + uint64_t b_ino = 42; + + tmpfd = mkstemp(tmpfile); + ASSERT_LE(0, tmpfd) << strerror(errno); + + expect_lookup(RELB, b_ino, S_IFREG | 0644, 0, 2); + + ASSERT_NE(0, rename(tmpfile, FULLB)); + ASSERT_EQ(EXDEV, errno); + + ASSERT_NE(0, rename(FULLB, tmpfile)); + ASSERT_EQ(EXDEV, errno); +} + +TEST_F(Rename, ok) +{ + const char FULLDST[] = "mountpoint/dst"; + const char RELDST[] = "dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELSRC[] = "src"; + uint64_t dst_dir_ino = FUSE_ROOT_ID; + uint64_t ino = 42; + + expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *src = (const char*)in.body.bytes + + sizeof(fuse_rename_in); + const char *dst = src + strlen(src) + 1; + return (in.header.opcode == FUSE_RENAME && + in.body.rename.newdir == dst_dir_ino && + (0 == strcmp(RELDST, dst)) && + (0 == strcmp(RELSRC, src))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(0))); + + ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); +} + +/* When moving a file to a new directory, update its parent */ +TEST_F(Rename, parent) +{ + const char FULLDST[] = "mountpoint/dstdir/dst"; + const char RELDSTDIR[] = "dstdir"; + const char RELDST[] = "dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELSRC[] = "src"; + const char FULLDSTPARENT[] = "mountpoint/dstdir"; + const char FULLDSTDOTDOT[] = "mountpoint/dstdir/dst/.."; + Sequence seq; + uint64_t dst_dir_ino = 43; + uint64_t ino = 42; + struct stat sb; + + expect_lookup(RELSRC, ino, S_IFDIR | 0755, 0, 1); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDSTDIR) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = dst_dir_ino; + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.attr.ino = dst_dir_ino; + out.body.entry.attr.nlink = 2; + }))); + EXPECT_LOOKUP(dst_dir_ino, RELDST) + .InSequence(seq) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *src = (const char*)in.body.bytes + + sizeof(fuse_rename_in); + const char *dst = src + strlen(src) + 1; + return (in.header.opcode == FUSE_RENAME && + in.body.rename.newdir == dst_dir_ino && + (0 == strcmp(RELDST, dst)) && + (0 == strcmp(RELSRC, src))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(0))); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == 1); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr_valid = UINT64_MAX; + out.body.attr.attr.ino = 1; + out.body.attr.attr.mode = S_IFDIR | 0755; + out.body.attr.attr.nlink = 2; + }))); + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDSTDIR) + .InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = dst_dir_ino; + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.attr.ino = dst_dir_ino; + out.body.entry.attr.nlink = 3; + }))); + EXPECT_LOOKUP(dst_dir_ino, RELDST) + .InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = ino; + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr_valid = UINT64_MAX; + }))); + + ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); + + ASSERT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); + EXPECT_EQ(2ul, sb.st_nlink); + + ASSERT_EQ(0, stat(FULLDSTPARENT, &sb)) << strerror(errno); + EXPECT_EQ(3ul, sb.st_nlink); + + ASSERT_EQ(0, stat(FULLDSTDOTDOT, &sb)) << strerror(errno); + ASSERT_EQ(dst_dir_ino, sb.st_ino); +} + +// Rename overwrites an existing destination file +TEST_F(Rename, overwrite) +{ + const char FULLDST[] = "mountpoint/dst"; + const char RELDST[] = "dst"; + const char FULLSRC[] = "mountpoint/src"; + const char RELSRC[] = "src"; + // The inode of the already-existing destination file + uint64_t dst_ino = 2; + uint64_t dst_dir_ino = FUSE_ROOT_ID; + uint64_t ino = 42; + + expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1); + expect_lookup(RELDST, dst_ino, S_IFREG | 0644, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *src = (const char*)in.body.bytes + + sizeof(fuse_rename_in); + const char *dst = src + strlen(src) + 1; + return (in.header.opcode == FUSE_RENAME && + in.body.rename.newdir == dst_dir_ino && + (0 == strcmp(RELDST, dst)) && + (0 == strcmp(RELSRC, src))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(0))); + + ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); +} diff --git a/tests/sys/fs/fusefs/rmdir.cc b/tests/sys/fs/fusefs/rmdir.cc new file mode 100644 index 000000000000..146b916f3d9f --- /dev/null +++ b/tests/sys/fs/fusefs/rmdir.cc @@ -0,0 +1,162 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <fcntl.h> +#include <semaphore.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Rmdir: public FuseTest { +public: +void expect_lookup(const char *relpath, uint64_t ino, int times=1) +{ + EXPECT_LOOKUP(FUSE_ROOT_ID, relpath) + .Times(times) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.attr.mode = S_IFDIR | 0755; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 2; + }))); +} + +void expect_rmdir(uint64_t parent, const char *relpath, int error) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_RMDIR && + 0 == strcmp(relpath, in.body.rmdir) && + in.header.nodeid == parent); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(error))); +} +}; + +/* + * A successful rmdir should clear the parent directory's attribute cache, + * because the fuse daemon should update its mtime and ctime + */ +TEST_F(Rmdir, parent_attr_cache) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + struct stat sb; + sem_t sem; + uint64_t ino = 42; + Sequence seq; + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + expect_lookup(RELPATH, ino); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_RMDIR && + 0 == strcmp(RELPATH, in.body.rmdir) && + in.header.nodeid == FUSE_ROOT_ID); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke(ReturnErrno(0))); + expect_forget(ino, 1, &sem); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == FUSE_ROOT_ID); + }, Eq(true)), + _) + ).InSequence(seq) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = FUSE_ROOT_ID; + out.body.attr.attr.mode = S_IFDIR | 0755; + out.body.attr.attr_valid = UINT64_MAX; + }))); + + ASSERT_EQ(0, rmdir(FULLPATH)) << strerror(errno); + EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); + sem_wait(&sem); + sem_destroy(&sem); +} + +TEST_F(Rmdir, enotempty) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + uint64_t ino = 42; + + expect_lookup(RELPATH, ino); + expect_rmdir(FUSE_ROOT_ID, RELPATH, ENOTEMPTY); + + ASSERT_NE(0, rmdir(FULLPATH)); + ASSERT_EQ(ENOTEMPTY, errno); +} + +/* Removing a directory should expire its entry cache */ +TEST_F(Rmdir, entry_cache) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + sem_t sem; + uint64_t ino = 42; + + expect_lookup(RELPATH, ino, 2); + expect_rmdir(FUSE_ROOT_ID, RELPATH, 0); + expect_forget(ino, 1, &sem); + + ASSERT_EQ(0, rmdir(FULLPATH)) << strerror(errno); + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); + sem_wait(&sem); + sem_destroy(&sem); +} + +TEST_F(Rmdir, ok) +{ + const char FULLPATH[] = "mountpoint/some_dir"; + const char RELPATH[] = "some_dir"; + sem_t sem; + uint64_t ino = 42; + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + expect_lookup(RELPATH, ino); + expect_rmdir(FUSE_ROOT_ID, RELPATH, 0); + expect_forget(ino, 1, &sem); + + ASSERT_EQ(0, rmdir(FULLPATH)) << strerror(errno); + sem_wait(&sem); + sem_destroy(&sem); +} diff --git a/tests/sys/fs/fusefs/setattr.cc b/tests/sys/fs/fusefs/setattr.cc new file mode 100644 index 000000000000..79559db33b12 --- /dev/null +++ b/tests/sys/fs/fusefs/setattr.cc @@ -0,0 +1,862 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/types.h> +#include <sys/resource.h> +#include <sys/stat.h> +#include <sys/time.h> + +#include <fcntl.h> +#include <semaphore.h> +#include <signal.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Setattr : public FuseTest { +public: +static sig_atomic_t s_sigxfsz; +}; + +class RofsSetattr: public Setattr { +public: +virtual void SetUp() { + s_sigxfsz = 0; + m_ro = true; + Setattr::SetUp(); +} +}; + +class Setattr_7_8: public Setattr { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + Setattr::SetUp(); +} +}; + + +sig_atomic_t Setattr::s_sigxfsz = 0; + +void sigxfsz_handler(int __unused sig) { + Setattr::s_sigxfsz = 1; +} + +/* + * If setattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs + * should use the cached attributes, rather than query the daemon + */ +TEST_F(Setattr, attr_cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + struct stat sb; + const mode_t newmode = 0644; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | newmode; + out.body.attr.attr_valid = UINT64_MAX; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_GETATTR); + }, Eq(true)), + _) + ).Times(0); + + /* Set an attribute with SETATTR */ + ASSERT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); + + /* The stat(2) should use cached attributes */ + ASSERT_EQ(0, stat(FULLPATH, &sb)); + EXPECT_EQ(S_IFREG | newmode, sb.st_mode); +} + +/* Change the mode of a file */ +TEST_F(Setattr, chmod) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const mode_t oldmode = 0755; + const mode_t newmode = 0644; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | oldmode; + out.body.entry.nodeid = ino; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + uint32_t valid = FATTR_MODE; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + in.body.setattr.mode == newmode); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | newmode; + }))); + EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); +} + +/* + * Chmod a multiply-linked file with cached attributes. Check that both files' + * attributes have changed. + */ +TEST_F(Setattr, chmod_multiply_linked) +{ + const char FULLPATH0[] = "mountpoint/some_file.txt"; + const char RELPATH0[] = "some_file.txt"; + const char FULLPATH1[] = "mountpoint/other_file.txt"; + const char RELPATH1[] = "other_file.txt"; + struct stat sb; + const uint64_t ino = 42; + const mode_t oldmode = 0777; + const mode_t newmode = 0666; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH0) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | oldmode; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 2; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH1) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | oldmode; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 2; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + uint32_t valid = FATTR_MODE; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + in.body.setattr.mode == newmode); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFREG | newmode; + out.body.attr.attr.nlink = 2; + out.body.attr.attr_valid = UINT64_MAX; + }))); + + /* For a lookup of the 2nd file to get it into the cache*/ + ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno); + EXPECT_EQ(S_IFREG | oldmode, sb.st_mode); + + ASSERT_EQ(0, chmod(FULLPATH0, newmode)) << strerror(errno); + ASSERT_EQ(0, stat(FULLPATH0, &sb)) << strerror(errno); + EXPECT_EQ(S_IFREG | newmode, sb.st_mode); + ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno); + EXPECT_EQ(S_IFREG | newmode, sb.st_mode); +} + + +/* Change the owner and group of a file */ +TEST_F(Setattr, chown) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const gid_t oldgroup = 66; + const gid_t newgroup = 99; + const uid_t olduser = 33; + const uid_t newuser = 44; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr.gid = oldgroup; + out.body.entry.attr.uid = olduser; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + uint32_t valid = FATTR_GID | FATTR_UID; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + in.body.setattr.uid == newuser && + in.body.setattr.gid == newgroup); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr.uid = newuser; + out.body.attr.attr.gid = newgroup; + }))); + EXPECT_EQ(0, chown(FULLPATH, newuser, newgroup)) << strerror(errno); +} + + + +/* + * FUSE daemons are allowed to check permissions however they like. If the + * daemon returns EPERM, even if the file permissions "should" grant access, + * then fuse(4) should return EPERM too. + */ +TEST_F(Setattr, eperm) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0777; + out.body.entry.nodeid = ino; + out.body.entry.attr.uid = in.header.uid; + out.body.entry.attr.gid = in.header.gid; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(EPERM))); + EXPECT_NE(0, truncate(FULLPATH, 10)); + EXPECT_EQ(EPERM, errno); +} + +/* Change the mode of an open file, by its file descriptor */ +TEST_F(Setattr, fchmod) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + const mode_t oldmode = 0755; + const mode_t newmode = 0644; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | oldmode; + out.body.entry.nodeid = ino; + out.body.entry.attr_valid = UINT64_MAX; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + out.header.len = sizeof(out.header); + SET_OUT_HEADER_LEN(out, open); + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + uint32_t valid = FATTR_MODE; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + in.body.setattr.mode == newmode); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | newmode; + }))); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno); + leak(fd); +} + +/* Change the size of an open file, by its file descriptor */ +TEST_F(Setattr, ftruncate) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + uint64_t fh = 0xdeadbeef1a7ebabe; + const off_t oldsize = 99; + const off_t newsize = 12345; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0755; + out.body.entry.nodeid = ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.attr.size = oldsize; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + out.header.len = sizeof(out.header); + SET_OUT_HEADER_LEN(out, open); + out.body.open.fh = fh; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + uint32_t valid = FATTR_SIZE | FATTR_FH; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + in.body.setattr.fh == fh); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | 0755; + out.body.attr.attr.size = newsize; + }))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, ftruncate(fd, newsize)) << strerror(errno); + leak(fd); +} + +/* Change the size of the file */ +TEST_F(Setattr, truncate) { + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const uint64_t oldsize = 100'000'000; + const uint64_t newsize = 20'000'000; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr.size = oldsize; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + uint32_t valid = FATTR_SIZE; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + in.body.setattr.size == newsize); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr.size = newsize; + }))); + EXPECT_EQ(0, truncate(FULLPATH, newsize)) << strerror(errno); +} + +/* + * Truncating a file should discard cached data past the truncation point. + * This is a regression test for bug 233783. + * + * There are two distinct failure modes. The first one is a failure to zero + * the portion of the file's final buffer past EOF. It can be reproduced by + * fsx -WR -P /tmp -S10 fsx.bin + * + * The second is a failure to drop buffers beyond that. It can be reproduced by + * fsx -WR -P /tmp -S18 -n fsx.bin + * Also reproducible in sh with: + * $> /path/to/libfuse/build/example/passthrough -d /tmp/mnt + * $> cd /tmp/mnt/tmp + * $> dd if=/dev/random of=randfile bs=1k count=192 + * $> truncate -s 1k randfile && truncate -s 192k randfile + * $> xxd randfile | less # xxd will wrongly show random data at offset 0x8000 + */ +TEST_F(Setattr, truncate_discards_cached_data) { + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + char *w0buf, *r0buf, *r1buf, *expected; + off_t w0_offset = 0; + size_t w0_size = 0x30000; + off_t r0_offset = 0; + off_t r0_size = w0_size; + size_t trunc0_size = 0x400; + size_t trunc1_size = w0_size; + off_t r1_offset = trunc0_size; + off_t r1_size = w0_size - trunc0_size; + size_t cur_size = 0; + const uint64_t ino = 42; + mode_t mode = S_IFREG | 0644; + int fd, r; + bool should_have_data = false; + + w0buf = new char[w0_size]; + memset(w0buf, 'X', w0_size); + + r0buf = new char[r0_size]; + r1buf = new char[r1_size]; + + expected = new char[r1_size](); + + expect_lookup(RELPATH, ino, mode, 0, 1); + expect_open(ino, O_RDWR, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([&](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = mode; + out.body.attr.attr.size = cur_size; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_WRITE); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.attr.attr.ino = ino; + out.body.write.size = in.body.write.size; + cur_size = std::max(static_cast<uint64_t>(cur_size), + in.body.write.size + in.body.write.offset); + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + (in.body.setattr.valid & FATTR_SIZE)); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) { + auto trunc_size = in.body.setattr.size; + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = mode; + out.body.attr.attr.size = trunc_size; + cur_size = trunc_size; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) { + auto osize = std::min( + static_cast<uint64_t>(cur_size) - in.body.read.offset, + static_cast<uint64_t>(in.body.read.size)); + assert(osize <= sizeof(out.body.bytes)); + out.header.len = sizeof(struct fuse_out_header) + osize; + if (should_have_data) + memset(out.body.bytes, 'X', osize); + else + bzero(out.body.bytes, osize); + }))); + + fd = open(FULLPATH, O_RDWR, 0644); + ASSERT_LE(0, fd) << strerror(errno); + + /* Fill the file with Xs */ + ASSERT_EQ(static_cast<ssize_t>(w0_size), + pwrite(fd, w0buf, w0_size, w0_offset)); + should_have_data = true; + /* Fill the cache */ + ASSERT_EQ(static_cast<ssize_t>(r0_size), + pread(fd, r0buf, r0_size, r0_offset)); + /* 1st truncate should discard cached data */ + EXPECT_EQ(0, ftruncate(fd, trunc0_size)) << strerror(errno); + should_have_data = false; + /* 2nd truncate extends file into previously cached data */ + EXPECT_EQ(0, ftruncate(fd, trunc1_size)) << strerror(errno); + /* Read should return all zeros */ + ASSERT_EQ(static_cast<ssize_t>(r1_size), + pread(fd, r1buf, r1_size, r1_offset)); + + r = memcmp(expected, r1buf, r1_size); + ASSERT_EQ(0, r); + + delete[] expected; + delete[] r1buf; + delete[] r0buf; + delete[] w0buf; + + leak(fd); +} + +/* truncate should fail if it would cause the file to exceed RLIMIT_FSIZE */ +TEST_F(Setattr, truncate_rlimit_rsize) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct rlimit rl; + const uint64_t ino = 42; + const uint64_t oldsize = 0; + const uint64_t newsize = 100'000'000; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr.size = oldsize; + }))); + + rl.rlim_cur = newsize / 2; + rl.rlim_max = 10 * newsize; + ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); + ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); + + EXPECT_EQ(-1, truncate(FULLPATH, newsize)); + EXPECT_EQ(EFBIG, errno); + EXPECT_EQ(1, s_sigxfsz); +} + +/* Change a file's timestamps */ +TEST_F(Setattr, utimensat) { + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const timespec oldtimes[2] = { + {.tv_sec = 1, .tv_nsec = 2}, + {.tv_sec = 3, .tv_nsec = 4}, + }; + const timespec newtimes[2] = { + {.tv_sec = 5, .tv_nsec = 6}, + {.tv_sec = 7, .tv_nsec = 8}, + }; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.attr.atime = oldtimes[0].tv_sec; + out.body.entry.attr.atimensec = oldtimes[0].tv_nsec; + out.body.entry.attr.mtime = oldtimes[1].tv_sec; + out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + uint32_t valid = FATTR_ATIME | FATTR_MTIME; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + (time_t)in.body.setattr.atime == + newtimes[0].tv_sec && + (long)in.body.setattr.atimensec == + newtimes[0].tv_nsec && + (time_t)in.body.setattr.mtime == + newtimes[1].tv_sec && + (long)in.body.setattr.mtimensec == + newtimes[1].tv_nsec); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr.atime = newtimes[0].tv_sec; + out.body.attr.attr.atimensec = newtimes[0].tv_nsec; + out.body.attr.attr.mtime = newtimes[1].tv_sec; + out.body.attr.attr.mtimensec = newtimes[1].tv_nsec; + }))); + EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0)) + << strerror(errno); +} + +/* Change a file mtime but not its atime */ +TEST_F(Setattr, utimensat_mtime_only) { + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const timespec oldtimes[2] = { + {.tv_sec = 1, .tv_nsec = 2}, + {.tv_sec = 3, .tv_nsec = 4}, + }; + const timespec newtimes[2] = { + {.tv_sec = 5, .tv_nsec = UTIME_OMIT}, + {.tv_sec = 7, .tv_nsec = 8}, + }; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.attr.atime = oldtimes[0].tv_sec; + out.body.entry.attr.atimensec = oldtimes[0].tv_nsec; + out.body.entry.attr.mtime = oldtimes[1].tv_sec; + out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + uint32_t valid = FATTR_MTIME; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + (time_t)in.body.setattr.mtime == + newtimes[1].tv_sec && + (long)in.body.setattr.mtimensec == + newtimes[1].tv_nsec); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr.atime = oldtimes[0].tv_sec; + out.body.attr.attr.atimensec = oldtimes[0].tv_nsec; + out.body.attr.attr.mtime = newtimes[1].tv_sec; + out.body.attr.attr.mtimensec = newtimes[1].tv_nsec; + }))); + EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0)) + << strerror(errno); +} + +/* + * Set a file's mtime and atime to now + * + * The design of FreeBSD's VFS does not allow fusefs to set just one of atime + * or mtime to UTIME_NOW; it's both or neither. + */ +TEST_F(Setattr, utimensat_utime_now) { + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const timespec oldtimes[2] = { + {.tv_sec = 1, .tv_nsec = 2}, + {.tv_sec = 3, .tv_nsec = 4}, + }; + const timespec newtimes[2] = { + {.tv_sec = 0, .tv_nsec = UTIME_NOW}, + {.tv_sec = 0, .tv_nsec = UTIME_NOW}, + }; + /* "now" is whatever the server says it is */ + const timespec now[2] = { + {.tv_sec = 5, .tv_nsec = 7}, + {.tv_sec = 6, .tv_nsec = 8}, + }; + struct stat sb; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr.atime = oldtimes[0].tv_sec; + out.body.entry.attr.atimensec = oldtimes[0].tv_nsec; + out.body.entry.attr.mtime = oldtimes[1].tv_sec; + out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + uint32_t valid = FATTR_ATIME | FATTR_ATIME_NOW | + FATTR_MTIME | FATTR_MTIME_NOW; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr.atime = now[0].tv_sec; + out.body.attr.attr.atimensec = now[0].tv_nsec; + out.body.attr.attr.mtime = now[1].tv_sec; + out.body.attr.attr.mtimensec = now[1].tv_nsec; + out.body.attr.attr_valid = UINT64_MAX; + }))); + ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0)) + << strerror(errno); + ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); + EXPECT_EQ(now[0].tv_sec, sb.st_atim.tv_sec); + EXPECT_EQ(now[0].tv_nsec, sb.st_atim.tv_nsec); + EXPECT_EQ(now[1].tv_sec, sb.st_mtim.tv_sec); + EXPECT_EQ(now[1].tv_nsec, sb.st_mtim.tv_nsec); +} + +/* + * FUSE_SETATTR returns a different file type, even though the entry cache + * hasn't expired. This is a server bug! It probably means that the server + * removed the file and recreated it with the same inode but a different vtyp. + * The best thing fusefs can do is return ENOENT to the caller. After all, the + * entry must not have existed recently. + */ +TEST_F(Setattr, vtyp_conflict) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + uid_t newuser = 12345; + sem_t sem; + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0777; + out.body.entry.nodeid = ino; + out.body.entry.entry_valid = UINT64_MAX; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFDIR | 0777; // Changed! + out.body.attr.attr.uid = newuser; + }))); + // We should reclaim stale vnodes + expect_forget(ino, 1, &sem); + + EXPECT_NE(0, chown(FULLPATH, newuser, -1)); + EXPECT_EQ(ENOENT, errno); + + sem_wait(&sem); + sem_destroy(&sem); +} + +/* On a read-only mount, no attributes may be changed */ +TEST_F(RofsSetattr, erofs) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const mode_t oldmode = 0755; + const mode_t newmode = 0644; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | oldmode; + out.body.entry.nodeid = ino; + }))); + + ASSERT_EQ(-1, chmod(FULLPATH, newmode)); + ASSERT_EQ(EROFS, errno); +} + +/* Change the mode of a file */ +TEST_F(Setattr_7_8, chmod) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + const mode_t oldmode = 0755; + const mode_t newmode = 0644; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out.body.entry.attr.mode = S_IFREG | oldmode; + out.body.entry.nodeid = ino; + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + uint32_t valid = FATTR_MODE; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + in.body.setattr.mode == newmode); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr_7_8); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | newmode; + }))); + EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); +} diff --git a/tests/sys/fs/fusefs/statfs.cc b/tests/sys/fs/fusefs/statfs.cc new file mode 100644 index 000000000000..1dd96e8073d7 --- /dev/null +++ b/tests/sys/fs/fusefs/statfs.cc @@ -0,0 +1,171 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> +#include <sys/mount.h> +#include <semaphore.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Statfs: public FuseTest {}; + +TEST_F(Statfs, eio) +{ + struct statfs statbuf; + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_STATFS); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(EIO))); + + ASSERT_NE(0, statfs("mountpoint", &statbuf)); + ASSERT_EQ(EIO, errno); +} + +/* + * When the daemon is dead but the filesystem is still mounted, fuse(4) fakes + * the statfs(2) response, which is necessary for unmounting. + */ +TEST_F(Statfs, enotconn) +{ + struct statfs statbuf; + char mp[PATH_MAX]; + + m_mock->kill_daemon(); + + ASSERT_NE(nullptr, getcwd(mp, PATH_MAX)) << strerror(errno); + strlcat(mp, "/mountpoint", PATH_MAX); + ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); + + EXPECT_EQ(getuid(), statbuf.f_owner); + EXPECT_EQ(0, strcmp("fusefs", statbuf.f_fstypename)); + EXPECT_EQ(0, strcmp("/dev/fuse", statbuf.f_mntfromname)); + EXPECT_EQ(0, strcmp(mp, statbuf.f_mntonname)); +} + +static void* statfs_th(void* arg) { + ssize_t r; + struct statfs *sb = (struct statfs*)arg; + + r = statfs("mountpoint", sb); + if (r >= 0) + return 0; + else + return (void*)(intptr_t)errno; +} + +/* + * Like the enotconn test, but in this case the daemon dies after we send the + * FUSE_STATFS operation but before we get a response. + */ +TEST_F(Statfs, enotconn_while_blocked) +{ + struct statfs statbuf; + void *thr0_value; + pthread_t th0; + char mp[PATH_MAX]; + sem_t sem; + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_STATFS); + }, Eq(true)), + _) + ).WillOnce(Invoke([&](auto in __unused, auto &out __unused) { + sem_post(&sem); + /* Just block until the daemon dies */ + })); + + ASSERT_NE(nullptr, getcwd(mp, PATH_MAX)) << strerror(errno); + strlcat(mp, "/mountpoint", PATH_MAX); + ASSERT_EQ(0, pthread_create(&th0, NULL, statfs_th, (void*)&statbuf)) + << strerror(errno); + + ASSERT_EQ(0, sem_wait(&sem)) << strerror(errno); + m_mock->kill_daemon(); + + pthread_join(th0, &thr0_value); + ASSERT_EQ(0, (intptr_t)thr0_value); + + EXPECT_EQ(getuid(), statbuf.f_owner); + EXPECT_EQ(0, strcmp("fusefs", statbuf.f_fstypename)); + EXPECT_EQ(0, strcmp("/dev/fuse", statbuf.f_mntfromname)); + EXPECT_EQ(0, strcmp(mp, statbuf.f_mntonname)); +} + +TEST_F(Statfs, ok) +{ + struct statfs statbuf; + char mp[PATH_MAX]; + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_STATFS); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, statfs); + out.body.statfs.st.blocks = 1000; + out.body.statfs.st.bfree = 100; + out.body.statfs.st.bavail = 200; + out.body.statfs.st.files = 5; + out.body.statfs.st.ffree = 6; + out.body.statfs.st.namelen = 128; + out.body.statfs.st.frsize = 1024; + }))); + + ASSERT_NE(nullptr, getcwd(mp, PATH_MAX)) << strerror(errno); + strlcat(mp, "/mountpoint", PATH_MAX); + ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); + EXPECT_EQ(1024ul, statbuf.f_bsize); + /* + * fuse(4) ignores the filesystem's reported optimal transfer size, and + * chooses a size that works well with the rest of the system instead + */ + EXPECT_EQ(1000ul, statbuf.f_blocks); + EXPECT_EQ(100ul, statbuf.f_bfree); + EXPECT_EQ(200l, statbuf.f_bavail); + EXPECT_EQ(5ul, statbuf.f_files); + EXPECT_EQ(6l, statbuf.f_ffree); + EXPECT_EQ(128u, statbuf.f_namemax); + EXPECT_EQ(getuid(), statbuf.f_owner); + EXPECT_EQ(0, strcmp("fusefs", statbuf.f_fstypename)); + EXPECT_EQ(0, strcmp("/dev/fuse", statbuf.f_mntfromname)); + EXPECT_EQ(0, strcmp(mp, statbuf.f_mntonname)); +} diff --git a/tests/sys/fs/fusefs/symlink.cc b/tests/sys/fs/fusefs/symlink.cc new file mode 100644 index 000000000000..bd355497a8bd --- /dev/null +++ b/tests/sys/fs/fusefs/symlink.cc @@ -0,0 +1,208 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <semaphore.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Symlink: public FuseTest { +public: + +void expect_symlink(uint64_t ino, const char *target, const char *relpath) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes; + const char *linkname = name + strlen(name) + 1; + return (in.header.opcode == FUSE_SYMLINK && + (0 == strcmp(linkname, target)) && + (0 == strcmp(name, relpath))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFLNK | 0777; + out.body.entry.nodeid = ino; + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr_valid = UINT64_MAX; + }))); +} + +}; + +class Symlink_7_8: public FuseTest { +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + FuseTest::SetUp(); +} + +void expect_symlink(uint64_t ino, const char *target, const char *relpath) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes; + const char *linkname = name + strlen(name) + 1; + return (in.header.opcode == FUSE_SYMLINK && + (0 == strcmp(linkname, target)) && + (0 == strcmp(name, relpath))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out.body.entry.attr.mode = S_IFLNK | 0777; + out.body.entry.nodeid = ino; + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr_valid = UINT64_MAX; + }))); +} + +}; + +/* + * A successful symlink should clear the parent directory's attribute cache, + * because the fuse daemon should update its mtime and ctime + */ +TEST_F(Symlink, clear_attr_cache) +{ + const char FULLPATH[] = "mountpoint/src"; + const char RELPATH[] = "src"; + const char dst[] = "dst"; + const uint64_t ino = 42; + struct stat sb; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == FUSE_ROOT_ID); + }, Eq(true)), + _) + ).Times(2) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = FUSE_ROOT_ID; + out.body.attr.attr.mode = S_IFDIR | 0755; + out.body.attr.attr_valid = UINT64_MAX; + }))); + expect_symlink(ino, dst, RELPATH); + + EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); + EXPECT_EQ(0, symlink(dst, FULLPATH)) << strerror(errno); + EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); +} + +TEST_F(Symlink, enospc) +{ + const char FULLPATH[] = "mountpoint/lnk"; + const char RELPATH[] = "lnk"; + const char dst[] = "dst"; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *name = (const char*)in.body.bytes; + const char *linkname = name + strlen(name) + 1; + return (in.header.opcode == FUSE_SYMLINK && + (0 == strcmp(linkname, dst)) && + (0 == strcmp(name, RELPATH))); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(ENOSPC))); + + EXPECT_EQ(-1, symlink(dst, FULLPATH)); + EXPECT_EQ(ENOSPC, errno); +} + +TEST_F(Symlink, ok) +{ + const char FULLPATH[] = "mountpoint/src"; + const char RELPATH[] = "src"; + const char dst[] = "dst"; + const uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_symlink(ino, dst, RELPATH); + + EXPECT_EQ(0, symlink(dst, FULLPATH)) << strerror(errno); +} + +/* + * Nothing bad should happen if the server returns the parent's inode number + * for the newly created symlink. Regression test for bug 263662. + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263662 + */ +TEST_F(Symlink, parent_ino) +{ + const char FULLPATH[] = "mountpoint/parent/src"; + const char PPATH[] = "parent"; + const char RELPATH[] = "src"; + const char dst[] = "dst"; + sem_t sem; + const uint64_t ino = 42; + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + expect_lookup(PPATH, ino, S_IFDIR | 0755, 0, 1); + EXPECT_LOOKUP(ino, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_symlink(ino, dst, RELPATH); + expect_forget(ino, 1, &sem); + + EXPECT_EQ(-1, symlink(dst, FULLPATH)); + EXPECT_EQ(EIO, errno); + + sem_wait(&sem); + sem_destroy(&sem); +} + +TEST_F(Symlink_7_8, ok) +{ + const char FULLPATH[] = "mountpoint/src"; + const char RELPATH[] = "src"; + const char dst[] = "dst"; + const uint64_t ino = 42; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnErrno(ENOENT))); + expect_symlink(ino, dst, RELPATH); + + EXPECT_EQ(0, symlink(dst, FULLPATH)) << strerror(errno); +} diff --git a/tests/sys/fs/fusefs/unlink.cc b/tests/sys/fs/fusefs/unlink.cc new file mode 100644 index 000000000000..1d8a371649ee --- /dev/null +++ b/tests/sys/fs/fusefs/unlink.cc @@ -0,0 +1,237 @@ +/*- + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <fcntl.h> +#include <semaphore.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Unlink: public FuseTest { +public: +void expect_lookup(const char *relpath, uint64_t ino, int times, int nlink=1) +{ + EXPECT_LOOKUP(FUSE_ROOT_ID, relpath) + .Times(times) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = nlink; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.attr.size = 0; + }))); +} + +}; + +/* + * Unlinking a multiply linked file should update its ctime and nlink. This + * could be handled simply by invalidating the attributes, necessitating a new + * GETATTR, but we implement it in-kernel for efficiency's sake. + */ +TEST_F(Unlink, attr_cache) +{ + const char FULLPATH0[] = "mountpoint/some_file.txt"; + const char RELPATH0[] = "some_file.txt"; + const char FULLPATH1[] = "mountpoint/other_file.txt"; + const char RELPATH1[] = "other_file.txt"; + uint64_t ino = 42; + struct stat sb_old, sb_new; + int fd1; + + expect_lookup(RELPATH0, ino, 1, 2); + expect_lookup(RELPATH1, ino, 1, 2); + expect_open(ino, 0, 1); + expect_unlink(1, RELPATH0, 0); + + fd1 = open(FULLPATH1, O_RDONLY); + ASSERT_LE(0, fd1) << strerror(errno); + + ASSERT_EQ(0, fstat(fd1, &sb_old)) << strerror(errno); + ASSERT_EQ(0, unlink(FULLPATH0)) << strerror(errno); + ASSERT_EQ(0, fstat(fd1, &sb_new)) << strerror(errno); + EXPECT_NE(sb_old.st_ctime, sb_new.st_ctime); + EXPECT_EQ(1u, sb_new.st_nlink); + + leak(fd1); +} + +/* + * A successful unlink should clear the parent directory's attribute cache, + * because the fuse daemon should update its mtime and ctime + */ +TEST_F(Unlink, parent_attr_cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct stat sb; + uint64_t ino = 42; + Sequence seq; + + /* Use nlink=2 so we don't get a FUSE_FORGET */ + expect_lookup(RELPATH, ino, 1, 2); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_UNLINK && + 0 == strcmp(RELPATH, in.body.unlink) && + in.header.nodeid == FUSE_ROOT_ID); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(Invoke(ReturnErrno(0))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == FUSE_ROOT_ID); + }, Eq(true)), + _) + ).InSequence(seq) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = FUSE_ROOT_ID; + out.body.attr.attr.mode = S_IFDIR | 0755; + out.body.attr.attr_valid = UINT64_MAX; + }))); + + ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno); + EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); +} + +TEST_F(Unlink, eperm) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + + expect_lookup(RELPATH, ino, 1); + expect_unlink(1, RELPATH, EPERM); + + ASSERT_NE(0, unlink(FULLPATH)); + ASSERT_EQ(EPERM, errno); +} + +/* + * Unlinking a file should expire its entry cache, even if it's multiply linked + */ +TEST_F(Unlink, entry_cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + + expect_lookup(RELPATH, ino, 2, 2); + expect_unlink(1, RELPATH, 0); + + ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno); + ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); +} + +/* + * Unlink a multiply-linked file. There should be no FUSE_FORGET because the + * file is still linked. + */ +TEST_F(Unlink, multiply_linked) +{ + const char FULLPATH0[] = "mountpoint/some_file.txt"; + const char RELPATH0[] = "some_file.txt"; + const char FULLPATH1[] = "mountpoint/other_file.txt"; + const char RELPATH1[] = "other_file.txt"; + uint64_t ino = 42; + + expect_lookup(RELPATH0, ino, 1, 2); + expect_unlink(1, RELPATH0, 0); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FORGET && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(0); + expect_lookup(RELPATH1, ino, 1, 1); + + ASSERT_EQ(0, unlink(FULLPATH0)) << strerror(errno); + + /* + * The final syscall simply ensures that no FUSE_FORGET was ever sent, + * by scheduling an arbitrary different operation after a FUSE_FORGET + * would've been sent. + */ + ASSERT_EQ(0, access(FULLPATH1, F_OK)) << strerror(errno); +} + +TEST_F(Unlink, ok) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + sem_t sem; + + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + expect_lookup(RELPATH, ino, 1); + expect_unlink(1, RELPATH, 0); + expect_forget(ino, 1, &sem); + + ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno); + sem_wait(&sem); + sem_destroy(&sem); +} + +/* Unlink an open file */ +TEST_F(Unlink, open_but_deleted) +{ + const char FULLPATH0[] = "mountpoint/some_file.txt"; + const char RELPATH0[] = "some_file.txt"; + const char FULLPATH1[] = "mountpoint/other_file.txt"; + const char RELPATH1[] = "other_file.txt"; + uint64_t ino = 42; + int fd; + + expect_lookup(RELPATH0, ino, 2); + expect_open(ino, 0, 1); + expect_unlink(1, RELPATH0, 0); + expect_lookup(RELPATH1, ino, 1, 1); + + fd = open(FULLPATH0, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, unlink(FULLPATH0)) << strerror(errno); + + /* + * The final syscall simply ensures that no FUSE_FORGET was ever sent, + * by scheduling an arbitrary different operation after a FUSE_FORGET + * would've been sent. + */ + ASSERT_EQ(0, access(FULLPATH1, F_OK)) << strerror(errno); + leak(fd); +} diff --git a/tests/sys/fs/fusefs/utils.cc b/tests/sys/fs/fusefs/utils.cc new file mode 100644 index 000000000000..125b7e2d6fc7 --- /dev/null +++ b/tests/sys/fs/fusefs/utils.cc @@ -0,0 +1,673 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> +#include <sys/mman.h> +#include <sys/module.h> +#include <sys/sysctl.h> +#include <sys/wait.h> + +#include <dirent.h> +#include <fcntl.h> +#include <grp.h> +#include <pwd.h> +#include <semaphore.h> +#include <unistd.h> +} + +#include <gtest/gtest.h> + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +/* + * The default max_write is set to this formula in libfuse, though + * individual filesystems can lower it. The "- 4096" was added in + * commit 154ffe2, with the commit message "fix". + */ +const uint32_t libfuse_max_write = 32 * getpagesize() + 0x1000 - 4096; + +/* Check that fusefs(4) is accessible and the current user can mount(2) */ +void check_environment() +{ + const char *devnode = "/dev/fuse"; + const char *bsdextended_node = "security.mac.bsdextended.enabled"; + int bsdextended_val = 0; + size_t bsdextended_size = sizeof(bsdextended_val); + int bsdextended_found; + const char *usermount_node = "vfs.usermount"; + int usermount_val = 0; + size_t usermount_size = sizeof(usermount_val); + if (eaccess(devnode, R_OK | W_OK)) { + if (errno == ENOENT) { + GTEST_SKIP() << devnode << " does not exist"; + } else if (errno == EACCES) { + GTEST_SKIP() << devnode << + " is not accessible by the current user"; + } else { + GTEST_SKIP() << strerror(errno); + } + } + // mac_bsdextended(4), when enabled, generates many more GETATTR + // operations. The fusefs tests' expectations don't account for those, + // and adding extra code to handle them obfuscates the real purpose of + // the tests. Better just to skip the fusefs tests if mac_bsdextended + // is enabled. + bsdextended_found = sysctlbyname(bsdextended_node, &bsdextended_val, + &bsdextended_size, NULL, 0); + if (bsdextended_found == 0 && bsdextended_val != 0) + GTEST_SKIP() << + "The fusefs tests are incompatible with mac_bsdextended."; + ASSERT_EQ(sysctlbyname(usermount_node, &usermount_val, &usermount_size, + NULL, 0), + 0); + if (geteuid() != 0 && !usermount_val) + GTEST_SKIP() << "current user is not allowed to mount"; +} + +const char *cache_mode_to_s(enum cache_mode cm) { + switch (cm) { + case Uncached: + return "Uncached"; + case Writethrough: + return "Writethrough"; + case Writeback: + return "Writeback"; + case WritebackAsync: + return "WritebackAsync"; + default: + return "Unknown"; + } +} + +bool is_unsafe_aio_enabled(void) { + const char *node = "vfs.aio.enable_unsafe"; + int val = 0; + size_t size = sizeof(val); + + if (sysctlbyname(node, &val, &size, NULL, 0)) { + perror("sysctlbyname"); + return (false); + } + return (val != 0); +} + +class FuseEnv: public Environment { + virtual void SetUp() { + check_environment(); + } +}; + +void FuseTest::SetUp() { + const char *maxbcachebuf_node = "vfs.maxbcachebuf"; + const char *maxphys_node = "kern.maxphys"; + size_t size; + + size = sizeof(m_maxbcachebuf); + ASSERT_EQ(0, sysctlbyname(maxbcachebuf_node, &m_maxbcachebuf, &size, + NULL, 0)) << strerror(errno); + size = sizeof(m_maxphys); + ASSERT_EQ(0, sysctlbyname(maxphys_node, &m_maxphys, &size, NULL, 0)) + << strerror(errno); + /* + * Set the default max_write to a distinct value from MAXPHYS to catch + * bugs that confuse the two. + */ + if (m_maxwrite == 0) + m_maxwrite = MIN(libfuse_max_write, (uint32_t)m_maxphys / 2); + + try { + m_mock = new MockFS(m_maxread, m_maxreadahead, m_allow_other, + m_default_permissions, m_push_symlinks_in, m_ro, + m_pm, m_init_flags, m_kernel_minor_version, + m_maxwrite, m_async, m_noclusterr, m_time_gran, + m_nointr, m_noatime, m_fsname, m_subtype, + m_no_auto_init); + /* + * FUSE_ACCESS is called almost universally. Expecting it in + * each test case would be super-annoying. Instead, set a + * default expectation for FUSE_ACCESS and return ENOSYS. + * + * Individual test cases can override this expectation since + * googlemock evaluates expectations in LIFO order. + */ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_ACCESS); + }, Eq(true)), + _) + ).Times(AnyNumber()) + .WillRepeatedly(Invoke(ReturnErrno(ENOSYS))); + /* + * FUSE_BMAP is called for most test cases that read data. Set + * a default expectation and return ENOSYS. + * + * Individual test cases can override this expectation since + * googlemock evaluates expectations in LIFO order. + */ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_BMAP); + }, Eq(true)), + _) + ).Times(AnyNumber()) + .WillRepeatedly(Invoke(ReturnErrno(ENOSYS))); + } catch (std::system_error err) { + FAIL() << err.what(); + } +} + +void +FuseTest::expect_access(uint64_t ino, mode_t access_mode, int error) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_ACCESS && + in.header.nodeid == ino && + in.body.access.mask == access_mode); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(error))); +} + +void +FuseTest::expect_destroy(int error) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_DESTROY); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { + m_mock->m_quit = true; + out.header.len = sizeof(out.header); + out.header.unique = in.header.unique; + out.header.error = -error; + }))); +} + +void +FuseTest::expect_fallocate(uint64_t ino, uint64_t offset, uint64_t length, + uint32_t mode, int error, int times) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FALLOCATE && + in.header.nodeid == ino && + in.body.fallocate.offset == offset && + in.body.fallocate.length == length && + in.body.fallocate.mode == mode); + }, Eq(true)), + _) + ).Times(times) + .WillRepeatedly(Invoke(ReturnErrno(error))); +} + +void +FuseTest::expect_flush(uint64_t ino, int times, ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FLUSH && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(times) + .WillRepeatedly(Invoke(r)); +} + +void +FuseTest::expect_forget(uint64_t ino, uint64_t nlookup, sem_t *sem) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FORGET && + in.header.nodeid == ino && + in.body.forget.nlookup == nlookup); + }, Eq(true)), + _) + ).WillOnce(Invoke([=](auto in __unused, auto &out __unused) { + if (sem != NULL) + sem_post(sem); + /* FUSE_FORGET has no response! */ + })); +} + +void FuseTest::expect_getattr(uint64_t ino, uint64_t size) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr.size = size; + out.body.attr.attr_valid = UINT64_MAX; + }))); +} + +void FuseTest::expect_getxattr(uint64_t ino, const char *attr, ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *a = (const char*)in.body.bytes + + sizeof(fuse_getxattr_in); + return (in.header.opcode == FUSE_GETXATTR && + in.header.nodeid == ino && + 0 == strcmp(attr, a)); + }, Eq(true)), + _) + ).WillOnce(Invoke(r)); +} + +void FuseTest::expect_lookup(const char *relpath, uint64_t ino, mode_t mode, + uint64_t size, int times, uint64_t attr_valid, uid_t uid, gid_t gid) +{ + EXPECT_LOOKUP(FUSE_ROOT_ID, relpath) + .Times(times) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = attr_valid; + out.body.entry.attr.size = size; + out.body.entry.attr.uid = uid; + out.body.entry.attr.gid = gid; + }))); +} + +void FuseTest::expect_lookup_7_8(const char *relpath, uint64_t ino, mode_t mode, + uint64_t size, int times, uint64_t attr_valid, uid_t uid, gid_t gid) +{ + EXPECT_LOOKUP(FUSE_ROOT_ID, relpath) + .Times(times) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry_7_8); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = attr_valid; + out.body.entry.attr.size = size; + out.body.entry.attr.uid = uid; + out.body.entry.attr.gid = gid; + }))); +} + +void FuseTest::expect_open(uint64_t ino, uint32_t flags, int times) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(times) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + out.header.len = sizeof(out.header); + SET_OUT_HEADER_LEN(out, open); + out.body.open.fh = FH; + out.body.open.open_flags = flags; + }))); +} + +void FuseTest::expect_opendir(uint64_t ino) +{ + /* opendir(3) calls fstatfs */ + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_STATFS); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke( + ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, statfs); + }))); + + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPENDIR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + out.header.len = sizeof(out.header); + SET_OUT_HEADER_LEN(out, open); + out.body.open.fh = FH; + }))); +} + +void FuseTest::expect_read(uint64_t ino, uint64_t offset, uint64_t isize, + uint64_t osize, const void *contents, int flags, uint64_t fh) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.fh == fh && + in.body.read.offset == offset && + in.body.read.size == isize && + (flags == -1 ? + (in.body.read.flags == O_RDONLY || + in.body.read.flags == O_RDWR) + : in.body.read.flags == (uint32_t)flags)); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + assert(osize <= sizeof(out.body.bytes)); + out.header.len = sizeof(struct fuse_out_header) + osize; + memmove(out.body.bytes, contents, osize); + }))).RetiresOnSaturation(); +} + +void FuseTest::expect_readdir(uint64_t ino, uint64_t off, + std::vector<struct dirent> &ents) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READDIR && + in.header.nodeid == ino && + in.body.readdir.fh == FH && + in.body.readdir.offset == off); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) { + struct fuse_dirent *fde = (struct fuse_dirent*)&(out.body); + int i = 0; + + out.header.error = 0; + out.header.len = 0; + + for (const auto& it: ents) { + size_t entlen, entsize; + + fde->ino = it.d_fileno; + fde->off = it.d_off; + fde->type = it.d_type; + fde->namelen = it.d_namlen; + strncpy(fde->name, it.d_name, it.d_namlen); + entlen = FUSE_NAME_OFFSET + fde->namelen; + entsize = FUSE_DIRENT_SIZE(fde); + /* + * The FUSE protocol does not require zeroing out the + * unused portion of the name. But it's a good + * practice to prevent information disclosure to the + * FUSE client, even though the client is usually the + * kernel + */ + memset(fde->name + fde->namelen, 0, entsize - entlen); + if (out.header.len + entsize > in.body.read.size) { + printf("Overflow in readdir expectation: i=%d\n" + , i); + break; + } + out.header.len += entsize; + fde = (struct fuse_dirent*) + ((intmax_t*)fde + entsize / sizeof(intmax_t)); + i++; + } + out.header.len += sizeof(out.header); + }))); + +} +void FuseTest::expect_release(uint64_t ino, uint64_t fh) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_RELEASE && + in.header.nodeid == ino && + in.body.release.fh == fh); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(0))); +} + +void FuseTest::expect_releasedir(uint64_t ino, ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_RELEASEDIR && + in.header.nodeid == ino && + in.body.release.fh == FH); + }, Eq(true)), + _) + ).WillOnce(Invoke(r)); +} + +void FuseTest::expect_unlink(uint64_t parent, const char *path, int error) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_UNLINK && + 0 == strcmp(path, in.body.unlink) && + in.header.nodeid == parent); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(error))); +} + +void FuseTest::expect_write(uint64_t ino, uint64_t offset, uint64_t isize, + uint64_t osize, uint32_t flags_set, uint32_t flags_unset, + const void *contents) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *buf = (const char*)in.body.bytes + + sizeof(struct fuse_write_in); + bool pid_ok; + uint32_t wf = in.body.write.write_flags; + + assert(isize <= sizeof(in.body.bytes) - + sizeof(struct fuse_write_in)); + if (wf & FUSE_WRITE_CACHE) + pid_ok = true; + else + pid_ok = (pid_t)in.header.pid == getpid(); + + return (in.header.opcode == FUSE_WRITE && + in.header.nodeid == ino && + in.body.write.fh == FH && + in.body.write.offset == offset && + in.body.write.size == isize && + pid_ok && + (wf & flags_set) == flags_set && + (wf & flags_unset) == 0 && + (in.body.write.flags == O_WRONLY || + in.body.write.flags == O_RDWR) && + 0 == bcmp(buf, contents, isize)); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = osize; + }))); +} + +void FuseTest::expect_write_7_8(uint64_t ino, uint64_t offset, uint64_t isize, + uint64_t osize, const void *contents) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *buf = (const char*)in.body.bytes + + FUSE_COMPAT_WRITE_IN_SIZE; + bool pid_ok = (pid_t)in.header.pid == getpid(); + + assert(isize <= sizeof(in.body.bytes) - + FUSE_COMPAT_WRITE_IN_SIZE); + return (in.header.opcode == FUSE_WRITE && + in.header.nodeid == ino && + in.body.write.fh == FH && + in.body.write.offset == offset && + in.body.write.size == isize && + pid_ok && + 0 == bcmp(buf, contents, isize)); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = osize; + }))); +} + +void +get_unprivileged_id(uid_t *uid, gid_t *gid) +{ + struct passwd *pw; + struct group *gr; + + /* + * First try "tests", Kyua's default unprivileged user. XXX after + * GoogleTest gains a proper Kyua wrapper, get this with the Kyua API + */ + pw = getpwnam("tests"); + if (pw == NULL) { + /* Fall back to "nobody" */ + pw = getpwnam("nobody"); + } + if (pw == NULL) + GTEST_SKIP() << "Test requires an unprivileged user"; + /* Use group "nobody", which is Kyua's default unprivileged group */ + gr = getgrnam("nobody"); + if (gr == NULL) + GTEST_SKIP() << "Test requires an unprivileged group"; + *uid = pw->pw_uid; + *gid = gr->gr_gid; +} + +void +FuseTest::fork(bool drop_privs, int *child_status, + std::function<void()> parent_func, + std::function<int()> child_func) +{ + sem_t *sem; + int mprot = PROT_READ | PROT_WRITE; + int mflags = MAP_ANON | MAP_SHARED; + pid_t child; + uid_t uid; + gid_t gid; + + if (drop_privs) { + get_unprivileged_id(&uid, &gid); + if (IsSkipped()) + return; + } + + sem = (sem_t*)mmap(NULL, sizeof(*sem), mprot, mflags, -1, 0); + ASSERT_NE(MAP_FAILED, sem) << strerror(errno); + ASSERT_EQ(0, sem_init(sem, 1, 0)) << strerror(errno); + + if ((child = ::fork()) == 0) { + /* In child */ + int err = 0; + + if (sem_wait(sem)) { + perror("sem_wait"); + err = 1; + goto out; + } + + if (drop_privs && 0 != setegid(gid)) { + perror("setegid"); + err = 1; + goto out; + } + if (drop_privs && 0 != setreuid(-1, uid)) { + perror("setreuid"); + err = 1; + goto out; + } + err = child_func(); + +out: + sem_destroy(sem); + _exit(err); + } else if (child > 0) { + /* + * In parent. Cleanup must happen here, because it's still + * privileged. + */ + m_mock->m_child_pid = child; + ASSERT_NO_FATAL_FAILURE(parent_func()); + + /* Signal the child process to go */ + ASSERT_EQ(0, sem_post(sem)) << strerror(errno); + + ASSERT_LE(0, wait(child_status)) << strerror(errno); + } else { + FAIL() << strerror(errno); + } + munmap(sem, sizeof(*sem)); + return; +} + +void +FuseTest::reclaim_vnode(const char *path) +{ + int err; + + err = sysctlbyname(reclaim_mib, NULL, 0, path, strlen(path) + 1); + ASSERT_EQ(0, err) << strerror(errno); +} + +static void usage(char* progname) { + fprintf(stderr, "Usage: %s [-v]\n\t-v increase verbosity\n", progname); + exit(2); +} + +int main(int argc, char **argv) { + int ch; + FuseEnv *fuse_env = new FuseEnv; + + InitGoogleTest(&argc, argv); + AddGlobalTestEnvironment(fuse_env); + + while ((ch = getopt(argc, argv, "v")) != -1) { + switch (ch) { + case 'v': + verbosity++; + break; + default: + usage(argv[0]); + break; + } + } + + return (RUN_ALL_TESTS()); +} diff --git a/tests/sys/fs/fusefs/utils.hh b/tests/sys/fs/fusefs/utils.hh new file mode 100644 index 000000000000..91bbba909672 --- /dev/null +++ b/tests/sys/fs/fusefs/utils.hh @@ -0,0 +1,277 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +struct _sem; +typedef struct _sem sem_t; +struct _dirdesc; +typedef struct _dirdesc DIR; + +/* Nanoseconds to sleep, for tests that must */ +#define NAP_NS (100'000'000) + +void get_unprivileged_id(uid_t *uid, gid_t *gid); +inline void nap() +{ + usleep(NAP_NS / 1000); +} + +enum cache_mode { + Uncached, + Writethrough, + Writeback, + WritebackAsync +}; + +const char *cache_mode_to_s(enum cache_mode cm); +bool is_unsafe_aio_enabled(void); + +extern const uint32_t libfuse_max_write; +class FuseTest : public ::testing::Test { + protected: + uint32_t m_maxread; + uint32_t m_maxreadahead; + uint32_t m_maxwrite; + uint32_t m_init_flags; + bool m_allow_other; + bool m_default_permissions; + uint32_t m_kernel_minor_version; + enum poll_method m_pm; + bool m_noatime; + bool m_push_symlinks_in; + bool m_ro; + bool m_async; + bool m_noclusterr; + bool m_nointr; + bool m_no_auto_init; + unsigned m_time_gran; + MockFS *m_mock = NULL; + const static uint64_t FH = 0xdeadbeef1a7ebabe; + const char *reclaim_mib = "debug.try_reclaim_vnode"; + const char *m_fsname; + const char *m_subtype; + + public: + int m_maxbcachebuf; + unsigned long m_maxphys; + + FuseTest(): + m_maxread(0), + m_maxreadahead(0), + m_maxwrite(0), + m_init_flags(0), + m_allow_other(false), + m_default_permissions(false), + m_kernel_minor_version(FUSE_KERNEL_MINOR_VERSION), + m_pm(BLOCKING), + m_noatime(false), + m_push_symlinks_in(false), + m_ro(false), + m_async(false), + m_noclusterr(false), + m_nointr(false), + m_no_auto_init(false), + m_time_gran(1), + m_fsname(""), + m_subtype(""), + m_maxbcachebuf(0), + m_maxphys(0) + {} + + virtual void SetUp(); + + virtual void TearDown() { + if (m_mock) + delete m_mock; + } + + /* + * Create an expectation that FUSE_ACCESS will be called once for the + * given inode with the given access_mode, returning the given errno + */ + void expect_access(uint64_t ino, mode_t access_mode, int error); + + /* Expect FUSE_DESTROY and shutdown the daemon */ + void expect_destroy(int error); + + /* + * Create an expectation that FUSE_FALLOCATE will be called with the + * given inode, offset, length, and mode, exactly times times and + * returning error + */ + void expect_fallocate(uint64_t ino, uint64_t offset, uint64_t length, + uint32_t mode, int error, int times=1); + + /* + * Create an expectation that FUSE_FLUSH will be called times times for + * the given inode + */ + void expect_flush(uint64_t ino, int times, ProcessMockerT r); + + /* + * Create an expectation that FUSE_FORGET will be called for the given + * inode. There will be no response. If sem is provided, it will be + * posted after the operation is received by the daemon. + */ + void expect_forget(uint64_t ino, uint64_t nlookup, sem_t *sem = NULL); + + /* + * Create an expectation that FUSE_GETATTR will be called for the given + * inode any number of times. It will respond with a few basic + * attributes, like the given size and the mode S_IFREG | 0644 + */ + void expect_getattr(uint64_t ino, uint64_t size); + + /* + * Create an expectation that FUSE_GETXATTR will be called once for the + * given inode. + */ + void expect_getxattr(uint64_t ino, const char *attr, ProcessMockerT r); + + /* + * Create an expectation that FUSE_LOOKUP will be called for the given + * path exactly times times and cache validity period. It will respond + * with inode ino, mode mode, filesize size. + */ + void expect_lookup(const char *relpath, uint64_t ino, mode_t mode, + uint64_t size, int times, uint64_t attr_valid = UINT64_MAX, + uid_t uid = 0, gid_t gid = 0); + + /* The protocol 7.8 version of expect_lookup */ + void expect_lookup_7_8(const char *relpath, uint64_t ino, mode_t mode, + uint64_t size, int times, uint64_t attr_valid = UINT64_MAX, + uid_t uid = 0, gid_t gid = 0); + + /* + * Create an expectation that FUSE_OPEN will be called for the given + * inode exactly times times. It will return with open_flags flags and + * file handle FH. + */ + void expect_open(uint64_t ino, uint32_t flags, int times); + + /* + * Create an expectation that FUSE_OPENDIR will be called exactly once + * for inode ino. + */ + void expect_opendir(uint64_t ino); + + /* + * Create an expectation that FUSE_READ will be called exactly once for + * the given inode, at offset offset and with size isize. It will + * return the first osize bytes from contents + * + * Protocol 7.8 tests can use this same expectation method because + * nothing currently validates the size of the fuse_read_in struct. + */ + void expect_read(uint64_t ino, uint64_t offset, uint64_t isize, + uint64_t osize, const void *contents, int flags = -1, + uint64_t fh = FH); + + /* + * Create an expectation that FUSE_READIR will be called any number of + * times on the given ino with the given offset, returning (by copy) + * the provided entries + */ + void expect_readdir(uint64_t ino, uint64_t off, + std::vector<struct dirent> &ents); + + /* + * Create an expectation that FUSE_RELEASE will be called exactly once + * for the given inode and filehandle, returning success + */ + void expect_release(uint64_t ino, uint64_t fh); + + /* + * Create an expectation that FUSE_RELEASEDIR will be called exactly + * once for the given inode + */ + void expect_releasedir(uint64_t ino, ProcessMockerT r); + + /* + * Create an expectation that FUSE_UNLINK will be called exactly once + * for the given path, returning an errno + */ + void expect_unlink(uint64_t parent, const char *path, int error); + + /* + * Create an expectation that FUSE_WRITE will be called exactly once + * for the given inode, at offset offset, with size isize and buffer + * contents. Any flags present in flags_set must be set, and any + * present in flags_unset must not be set. Other flags are don't care. + * It will return osize. + */ + void expect_write(uint64_t ino, uint64_t offset, uint64_t isize, + uint64_t osize, uint32_t flags_set, uint32_t flags_unset, + const void *contents); + + /* Protocol 7.8 version of expect_write */ + void expect_write_7_8(uint64_t ino, uint64_t offset, uint64_t isize, + uint64_t osize, const void *contents); + + /* + * Helper that runs code in a child process. + * + * First, parent_func runs in the parent process. + * Then, child_func runs in the child process, dropping privileges if + * desired. + * Finally, fusetest_fork returns. + * + * # Returns + * + * fusetest_fork may SKIP the test, which the caller should detect with + * the IsSkipped() method. If not, then the child's exit status will + * be returned in status. + */ + void fork(bool drop_privs, int *status, + std::function<void()> parent_func, + std::function<int()> child_func); + + /* + * Deliberately leak a file descriptor. + * + * Closing a file descriptor on fusefs would cause the server to + * receive FUSE_CLOSE and possibly FUSE_INACTIVE. Handling those + * operations would needlessly complicate most tests. So most tests + * deliberately leak the file descriptors instead. This method serves + * to document the leakage, and provide a single point of suppression + * for static analyzers. + */ + /* coverity[+close: arg-0] */ + static void leak(int fd __unused) {} + + /* + * Deliberately leak a DIR* pointer + * + * See comments for FuseTest::leak + */ + static void leakdir(DIR* dirp __unused) {} + + /* Manually reclaim a vnode. Requires root privileges. */ + void reclaim_vnode(const char *fullpath); +}; diff --git a/tests/sys/fs/fusefs/write.cc b/tests/sys/fs/fusefs/write.cc new file mode 100644 index 000000000000..f5573a865a04 --- /dev/null +++ b/tests/sys/fs/fusefs/write.cc @@ -0,0 +1,1662 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/param.h> +#include <sys/mman.h> +#include <sys/resource.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/uio.h> +#include <sys/un.h> + +#include <aio.h> +#include <fcntl.h> +#include <signal.h> +#include <unistd.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Write: public FuseTest { + +public: +void SetUp() { + FuseTest::SetUp(); +} + +void TearDown() { + struct sigaction sa; + + bzero(&sa, sizeof(sa)); + sa.sa_handler = SIG_DFL; + sigaction(SIGXFSZ, &sa, NULL); + + FuseTest::TearDown(); +} + +void expect_lookup(const char *relpath, uint64_t ino, uint64_t size) +{ + FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, size, 1); +} + +void expect_release(uint64_t ino, ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_RELEASE && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(r)); +} + +void expect_write(uint64_t ino, uint64_t offset, uint64_t isize, + uint64_t osize, const void *contents) +{ + FuseTest::expect_write(ino, offset, isize, osize, 0, 0, contents); +} + +/* Expect a write that may or may not come, depending on the cache mode */ +void maybe_expect_write(uint64_t ino, uint64_t offset, uint64_t size, + const void *contents) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *buf = (const char*)in.body.bytes + + sizeof(struct fuse_write_in); + + assert(size <= sizeof(in.body.bytes) - + sizeof(struct fuse_write_in)); + return (in.header.opcode == FUSE_WRITE && + in.header.nodeid == ino && + in.body.write.offset == offset && + in.body.write.size == size && + 0 == bcmp(buf, contents, size)); + }, Eq(true)), + _) + ).Times(AtMost(1)) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = size; + }) + )); +} + +}; + +class Write_7_8: public FuseTest { + +public: +virtual void SetUp() { + m_kernel_minor_version = 8; + FuseTest::SetUp(); +} + +void expect_lookup(const char *relpath, uint64_t ino, uint64_t size) +{ + FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, size, 1); +} + +}; + +class AioWrite: public Write { +virtual void SetUp() { + if (!is_unsafe_aio_enabled()) + GTEST_SKIP() << + "vfs.aio.enable_unsafe must be set for this test"; + FuseTest::SetUp(); +} +}; + +/* Tests for the writeback cache mode */ +class WriteBack: public Write { +public: +virtual void SetUp() { + m_init_flags |= FUSE_WRITEBACK_CACHE; + FuseTest::SetUp(); + if (IsSkipped()) + return; +} + +void expect_write(uint64_t ino, uint64_t offset, uint64_t isize, + uint64_t osize, const void *contents) +{ + FuseTest::expect_write(ino, offset, isize, osize, FUSE_WRITE_CACHE, 0, + contents); +} +}; + +class WriteBackAsync: public WriteBack { +public: +virtual void SetUp() { + m_async = true; + m_maxwrite = 65536; + WriteBack::SetUp(); +} +}; + +class TimeGran: public WriteBackAsync, public WithParamInterface<unsigned> { +public: +virtual void SetUp() { + m_time_gran = 1 << GetParam(); + WriteBackAsync::SetUp(); +} +}; + +/* Tests for clustered writes with WriteBack cacheing */ +class WriteCluster: public WriteBack { +public: +virtual void SetUp() { + m_async = true; + m_maxwrite = UINT32_MAX; // Anything larger than MAXPHYS will suffice + WriteBack::SetUp(); + if (m_maxphys < 2 * DFLTPHYS) + GTEST_SKIP() << "MAXPHYS must be at least twice DFLTPHYS" + << " for this test"; + if (m_maxphys < 2 * (unsigned long )m_maxbcachebuf) + GTEST_SKIP() << "MAXPHYS must be at least twice maxbcachebuf" + << " for this test"; +} +}; + +/* Tests relating to the server's max_write property */ +class WriteMaxWrite: public Write { +public: +virtual void SetUp() { + /* + * For this test, m_maxwrite must be less than either m_maxbcachebuf or + * maxphys. + */ + m_maxwrite = 32768; + Write::SetUp(); +} +}; + +class WriteEofDuringVnopStrategy: public Write, public WithParamInterface<int> +{}; + +class WriteRlimitFsize: public Write, public WithParamInterface<int> { +public: +static sig_atomic_t s_sigxfsz; +struct rlimit m_initial_limit; + +void SetUp() { + s_sigxfsz = 0; + getrlimit(RLIMIT_FSIZE, &m_initial_limit); + FuseTest::SetUp(); +} + +void TearDown() { + setrlimit(RLIMIT_FSIZE, &m_initial_limit); + + FuseTest::TearDown(); +} +}; + +sig_atomic_t WriteRlimitFsize::s_sigxfsz = 0; + +void sigxfsz_handler(int __unused sig) { + WriteRlimitFsize::s_sigxfsz = 1; +} + +/* AIO writes need to set the header's pid field correctly */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */ +TEST_F(AioWrite, DISABLED_aio_write) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + uint64_t offset = 4096; + int fd; + ssize_t bufsize = strlen(CONTENTS); + struct aiocb iocb, *piocb; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + expect_write(ino, offset, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + iocb.aio_nbytes = bufsize; + iocb.aio_fildes = fd; + iocb.aio_buf = __DECONST(void *, CONTENTS); + iocb.aio_offset = offset; + iocb.aio_sigevent.sigev_notify = SIGEV_NONE; + ASSERT_EQ(0, aio_write(&iocb)) << strerror(errno); + ASSERT_EQ(bufsize, aio_waitcomplete(&piocb, NULL)) << strerror(errno); + leak(fd); +} + +/* + * When a file is opened with O_APPEND, we should forward that flag to + * FUSE_OPEN (tested by Open.o_append) but still attempt to calculate the + * offset internally. That way we'll work both with filesystems that + * understand O_APPEND (and ignore the offset) and filesystems that don't (and + * simply use the offset). + * + * Note that verifying the O_APPEND flag in FUSE_OPEN is done in the + * Open.o_append test. + */ +TEST_F(Write, append) +{ + const ssize_t BUFSIZE = 9; + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char CONTENTS[BUFSIZE] = "abcdefgh"; + uint64_t ino = 42; + /* + * Set offset to a maxbcachebuf boundary so we don't need to RMW when + * using writeback caching + */ + uint64_t initial_offset = m_maxbcachebuf; + int fd; + + expect_lookup(RELPATH, ino, initial_offset); + expect_open(ino, 0, 1); + expect_write(ino, initial_offset, BUFSIZE, BUFSIZE, CONTENTS); + + /* Must open O_RDWR or fuse(4) implicitly sets direct_io */ + fd = open(FULLPATH, O_RDWR | O_APPEND); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(BUFSIZE, write(fd, CONTENTS, BUFSIZE)) << strerror(errno); + leak(fd); +} + +/* If a file is cached, then appending to the end should not cause a read */ +TEST_F(Write, append_to_cached) +{ + const ssize_t BUFSIZE = 9; + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + char *oldcontents, *oldbuf; + const char CONTENTS[BUFSIZE] = "abcdefgh"; + uint64_t ino = 42; + /* + * Set offset in between maxbcachebuf boundary to test buffer handling + */ + uint64_t oldsize = m_maxbcachebuf / 2; + int fd; + + oldcontents = new char[oldsize](); + oldbuf = new char[oldsize]; + + expect_lookup(RELPATH, ino, oldsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, oldsize, oldsize, oldcontents); + maybe_expect_write(ino, oldsize, BUFSIZE, CONTENTS); + + /* Must open O_RDWR or fuse(4) implicitly sets direct_io */ + fd = open(FULLPATH, O_RDWR | O_APPEND); + ASSERT_LE(0, fd) << strerror(errno); + + /* Read the old data into the cache */ + ASSERT_EQ((ssize_t)oldsize, read(fd, oldbuf, oldsize)) + << strerror(errno); + + /* Write the new data. There should be no more read operations */ + ASSERT_EQ(BUFSIZE, write(fd, CONTENTS, BUFSIZE)) << strerror(errno); + leak(fd); + delete[] oldbuf; + delete[] oldcontents; +} + +TEST_F(Write, append_direct_io) +{ + const ssize_t BUFSIZE = 9; + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char CONTENTS[BUFSIZE] = "abcdefgh"; + uint64_t ino = 42; + uint64_t initial_offset = 4096; + int fd; + + expect_lookup(RELPATH, ino, initial_offset); + expect_open(ino, FOPEN_DIRECT_IO, 1); + expect_write(ino, initial_offset, BUFSIZE, BUFSIZE, CONTENTS); + + fd = open(FULLPATH, O_WRONLY | O_APPEND); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(BUFSIZE, write(fd, CONTENTS, BUFSIZE)) << strerror(errno); + leak(fd); +} + +/* A direct write should evict any overlapping cached data */ +TEST_F(Write, direct_io_evicts_cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char CONTENTS0[] = "abcdefgh"; + const char CONTENTS1[] = "ijklmnop"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS0) + 1; + char readbuf[bufsize]; + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, bufsize, bufsize, CONTENTS0); + expect_write(ino, 0, bufsize, bufsize, CONTENTS1); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + // Prime cache + ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno); + + // Write directly, evicting cache + ASSERT_EQ(0, fcntl(fd, F_SETFL, O_DIRECT)) << strerror(errno); + ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); + ASSERT_EQ(bufsize, write(fd, CONTENTS1, bufsize)) << strerror(errno); + + // Read again. Cache should be bypassed + expect_read(ino, 0, bufsize, bufsize, CONTENTS1); + ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno); + ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); + ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno); + ASSERT_STREQ(readbuf, CONTENTS1); + + leak(fd); +} + +/* + * If the server doesn't return FOPEN_DIRECT_IO during FUSE_OPEN, then it's not + * allowed to return a short write for that file handle. However, if it does + * then we should still do our darndest to handle it by resending the unwritten + * portion. + */ +TEST_F(Write, indirect_io_short_write) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefghijklmnop"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + ssize_t bufsize0 = 11; + ssize_t bufsize1 = strlen(CONTENTS) - bufsize0; + const char *contents1 = CONTENTS + bufsize0; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + expect_write(ino, 0, bufsize, bufsize0, CONTENTS); + expect_write(ino, bufsize0, bufsize1, bufsize1, contents1); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + leak(fd); +} + +/* It is an error if the daemon claims to have written more data than we sent */ +TEST_F(Write, indirect_io_long_write) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefghijklmnop"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + ssize_t bufsize_out = 100; + off_t some_other_size = 25; + struct stat sb; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + expect_write(ino, 0, bufsize, bufsize_out, CONTENTS); + expect_getattr(ino, some_other_size); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(-1, write(fd, CONTENTS, bufsize)) << strerror(errno); + ASSERT_EQ(EINVAL, errno); + + /* + * Following such an error, we should requery the server for the file's + * size. + */ + fstat(fd, &sb); + ASSERT_EQ(sb.st_size, some_other_size); + + leak(fd); +} + +/* + * Don't crash if the server returns a write that can't be represented as a + * signed 32 bit number. Regression test for + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263263 + */ +TEST_F(Write, indirect_io_very_long_write) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefghijklmnop"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + ssize_t bufsize_out = 3 << 30; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + expect_write(ino, 0, bufsize, bufsize_out, CONTENTS); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(-1, write(fd, CONTENTS, bufsize)) << strerror(errno); + ASSERT_EQ(EINVAL, errno); + leak(fd); +} + +/* + * When the direct_io option is used, filesystems are allowed to write less + * data than requested. We should return the short write to userland. + */ +TEST_F(Write, direct_io_short_write) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefghijklmnop"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + ssize_t halfbufsize = bufsize / 2; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, FOPEN_DIRECT_IO, 1); + expect_write(ino, 0, bufsize, halfbufsize, CONTENTS); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(halfbufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + leak(fd); +} + +/* + * An insidious edge case: the filesystem returns a short write, and the + * difference between what we requested and what it actually wrote crosses an + * iov element boundary + */ +TEST_F(Write, direct_io_short_write_iov) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS0 = "abcdefgh"; + const char *CONTENTS1 = "ijklmnop"; + const char *EXPECTED0 = "abcdefghijklmnop"; + uint64_t ino = 42; + int fd; + ssize_t size0 = strlen(CONTENTS0) - 1; + ssize_t size1 = strlen(CONTENTS1) + 1; + ssize_t totalsize = size0 + size1; + struct iovec iov[2]; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, FOPEN_DIRECT_IO, 1); + expect_write(ino, 0, totalsize, size0, EXPECTED0); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + iov[0].iov_base = __DECONST(void*, CONTENTS0); + iov[0].iov_len = strlen(CONTENTS0); + iov[1].iov_base = __DECONST(void*, CONTENTS1); + iov[1].iov_len = strlen(CONTENTS1); + ASSERT_EQ(size0, writev(fd, iov, 2)) << strerror(errno); + leak(fd); +} + +/* fusefs should respect RLIMIT_FSIZE */ +TEST_P(WriteRlimitFsize, rlimit_fsize) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + struct rlimit rl; + ssize_t bufsize = strlen(CONTENTS); + off_t offset = 1'000'000'000; + uint64_t ino = 42; + int fd, oflag; + + oflag = GetParam(); + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + + rl.rlim_cur = offset; + rl.rlim_max = m_initial_limit.rlim_max; + ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); + ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); + + fd = open(FULLPATH, O_WRONLY | oflag); + + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(-1, pwrite(fd, CONTENTS, bufsize, offset)); + EXPECT_EQ(EFBIG, errno); + EXPECT_EQ(1, s_sigxfsz); + leak(fd); +} + +/* + * When crossing the RLIMIT_FSIZE boundary, writes should be truncated, not + * aborted. + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=164793 + */ +TEST_P(WriteRlimitFsize, rlimit_fsize_truncate) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefghijklmnopqrstuvwxyz"; + struct rlimit rl; + ssize_t bufsize = strlen(CONTENTS); + uint64_t ino = 42; + off_t offset = 1 << 30; + off_t limit = offset + strlen(CONTENTS) / 2; + int fd, oflag; + + oflag = GetParam(); + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + expect_write(ino, offset, bufsize / 2, bufsize / 2, CONTENTS); + + rl.rlim_cur = limit; + rl.rlim_max = m_initial_limit.rlim_max; + ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); + ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); + + fd = open(FULLPATH, O_WRONLY | oflag); + + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize / 2, pwrite(fd, CONTENTS, bufsize, offset)) + << strerror(errno); + leak(fd); +} + +INSTANTIATE_TEST_SUITE_P(W, WriteRlimitFsize, + Values(0, O_DIRECT) +); + +/* + * A short read indicates EOF. Test that nothing bad happens if we get EOF + * during the R of a RMW operation. + */ +TEST_F(Write, eof_during_rmw) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + const char *INITIAL = "XXXXXXXXXX"; + uint64_t ino = 42; + uint64_t offset = 1; + ssize_t bufsize = strlen(CONTENTS) + 1; + off_t orig_fsize = 10; + off_t truncated_fsize = 5; + int fd; + + FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, orig_fsize, 1); + expect_open(ino, 0, 1); + expect_read(ino, 0, orig_fsize, truncated_fsize, INITIAL, O_RDWR); + maybe_expect_write(ino, offset, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, pwrite(fd, CONTENTS, bufsize, offset)) + << strerror(errno); + leak(fd); +} + +/* + * VOP_STRATEGY should not query the server for the file's size, even if its + * cached attributes have expired. + * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=256937 + */ +TEST_P(WriteEofDuringVnopStrategy, eof_during_vop_strategy) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + Sequence seq; + const off_t filesize = 2 * m_maxbcachebuf; + char *contents; + uint64_t ino = 42; + uint64_t attr_valid = 0; + uint64_t attr_valid_nsec = 0; + mode_t mode = S_IFREG | 0644; + int fd; + int ngetattrs; + + ngetattrs = GetParam(); + contents = new char[filesize](); + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr.size = filesize; + out.body.entry.attr_valid = attr_valid; + out.body.entry.attr_valid_nsec = attr_valid_nsec; + }))); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(Between(ngetattrs - 1, ngetattrs)) + .InSequence(seq) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = mode; + out.body.attr.attr_valid = attr_valid; + out.body.attr.attr_valid_nsec = attr_valid_nsec; + out.body.attr.attr.size = filesize; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).InSequence(seq) + .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = mode; + out.body.attr.attr_valid = attr_valid; + out.body.attr.attr_valid_nsec = attr_valid_nsec; + out.body.attr.attr.size = filesize / 2; + }))); + expect_write(ino, 0, filesize / 2, filesize / 2, contents); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(filesize / 2, write(fd, contents, filesize / 2)) + << strerror(errno); + +} + +INSTANTIATE_TEST_SUITE_P(W, WriteEofDuringVnopStrategy, + Values(1, 2, 3) +); + +/* + * If the kernel cannot be sure which uid, gid, or pid was responsible for a + * write, then it must set the FUSE_WRITE_CACHE bit + */ +/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236378 */ +TEST_F(Write, mmap) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + void *p; + uint64_t offset = 10; + size_t len; + char *zeros, *expected; + + len = getpagesize(); + + zeros = new char[len](); + expected = new char[len](); + memmove((uint8_t*)expected + offset, CONTENTS, bufsize); + + expect_lookup(RELPATH, ino, len); + expect_open(ino, 0, 1); + expect_read(ino, 0, len, len, zeros); + /* + * Writes from the pager may or may not be associated with the correct + * pid, so they must set FUSE_WRITE_CACHE. + */ + FuseTest::expect_write(ino, 0, len, len, FUSE_WRITE_CACHE, 0, expected); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, ReturnErrno(0)); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + p = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + ASSERT_NE(MAP_FAILED, p) << strerror(errno); + + memmove((uint8_t*)p + offset, CONTENTS, bufsize); + + ASSERT_EQ(0, munmap(p, len)) << strerror(errno); + close(fd); // Write mmap'd data on close + + delete[] expected; + delete[] zeros; + + leak(fd); +} + +TEST_F(Write, pwrite) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + uint64_t offset = m_maxbcachebuf; + int fd; + ssize_t bufsize = strlen(CONTENTS); + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + expect_write(ino, offset, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, pwrite(fd, CONTENTS, bufsize, offset)) + << strerror(errno); + leak(fd); +} + +/* Writing a file should update its cached mtime and ctime */ +TEST_F(Write, timestamps) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + ssize_t bufsize = strlen(CONTENTS); + uint64_t ino = 42; + struct stat sb0, sb1; + int fd; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + maybe_expect_write(ino, 0, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &sb0)) << strerror(errno); + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + + nap(); + + ASSERT_EQ(0, fstat(fd, &sb1)) << strerror(errno); + + EXPECT_EQ(sb0.st_atime, sb1.st_atime); + EXPECT_NE(sb0.st_mtime, sb1.st_mtime); + EXPECT_NE(sb0.st_ctime, sb1.st_ctime); + + leak(fd); +} + +TEST_F(Write, write) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + expect_write(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + leak(fd); +} + +/* fuse(4) should not issue writes of greater size than the daemon requests */ +TEST_F(WriteMaxWrite, write) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + int *contents; + uint64_t ino = 42; + int fd; + ssize_t halfbufsize, bufsize; + + halfbufsize = m_mock->m_maxwrite; + if (halfbufsize >= m_maxbcachebuf || + (unsigned long )halfbufsize >= m_maxphys) + GTEST_SKIP() << "Must lower m_maxwrite for this test"; + bufsize = halfbufsize * 2; + contents = new int[bufsize / sizeof(int)]; + for (int i = 0; i < (int)bufsize / (int)sizeof(i); i++) { + contents[i] = i; + } + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + maybe_expect_write(ino, 0, halfbufsize, contents); + maybe_expect_write(ino, halfbufsize, halfbufsize, + &contents[halfbufsize / sizeof(int)]); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, write(fd, contents, bufsize)) << strerror(errno); + leak(fd); + + delete[] contents; +} + +TEST_F(Write, write_nothing) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = ""; + uint64_t ino = 42; + int fd; + ssize_t bufsize = 0; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + leak(fd); +} + +TEST_F(Write_7_8, write) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + expect_write_7_8(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + leak(fd); +} + +/* In writeback mode, dirty data should be written on close */ +TEST_F(WriteBackAsync, close) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + expect_write(ino, 0, bufsize, bufsize, CONTENTS); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETATTR); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; // Must match nodeid + }))); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, ReturnErrno(0)); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + close(fd); +} + +/* In writeback mode, adjacent writes will be clustered together */ +TEST_F(WriteCluster, clustering) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int i, fd; + char *wbuf, *wbuf2x; + ssize_t bufsize = m_maxbcachebuf; + off_t filesize = 5 * bufsize; + + wbuf = new char[bufsize]; + memset(wbuf, 'X', bufsize); + wbuf2x = new char[2 * bufsize]; + memset(wbuf2x, 'X', 2 * bufsize); + + expect_lookup(RELPATH, ino, filesize); + expect_open(ino, 0, 1); + /* + * Writes of bufsize-bytes each should be clustered into greater sizes. + * The amount of clustering is adaptive, so the first write actually + * issued will be 2x bufsize and subsequent writes may be larger + */ + expect_write(ino, 0, 2 * bufsize, 2 * bufsize, wbuf2x); + expect_write(ino, 2 * bufsize, 2 * bufsize, 2 * bufsize, wbuf2x); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, ReturnErrno(0)); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + for (i = 0; i < 4; i++) { + ASSERT_EQ(bufsize, write(fd, wbuf, bufsize)) + << strerror(errno); + } + close(fd); + delete[] wbuf2x; + delete[] wbuf; +} + +/* + * When clustering writes, an I/O error to any of the cluster's children should + * not panic the system on unmount + */ +/* + * Regression test for bug 238585 + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=238565 + */ +TEST_F(WriteCluster, cluster_write_err) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int i, fd; + char *wbuf; + ssize_t bufsize = m_maxbcachebuf; + off_t filesize = 4 * bufsize; + + wbuf = new char[bufsize]; + memset(wbuf, 'X', bufsize); + + expect_lookup(RELPATH, ino, filesize); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_WRITE); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnErrno(EIO))); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, ReturnErrno(0)); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + for (i = 0; i < 3; i++) { + ASSERT_EQ(bufsize, write(fd, wbuf, bufsize)) + << strerror(errno); + } + close(fd); + delete[] wbuf; +} + +/* + * In writeback mode, writes to an O_WRONLY file could trigger reads from the + * server. The FUSE protocol explicitly allows that. + */ +TEST_F(WriteBack, rmw) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + const char *INITIAL = "XXXXXXXXXX"; + uint64_t ino = 42; + uint64_t offset = 1; + off_t fsize = 10; + int fd; + ssize_t bufsize = strlen(CONTENTS); + + FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + expect_read(ino, 0, fsize, fsize, INITIAL, O_WRONLY); + maybe_expect_write(ino, offset, bufsize, CONTENTS); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, pwrite(fd, CONTENTS, bufsize, offset)) + << strerror(errno); + leak(fd); +} + +/* + * Without direct_io, writes should be committed to cache + */ +TEST_F(WriteBack, cache) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t readbuf[bufsize]; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + expect_write(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + /* + * A subsequent read should be serviced by cache, without querying the + * filesystem daemon + */ + ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); + ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno); + leak(fd); +} + +/* + * With O_DIRECT, writes should be not committed to cache. Admittedly this is + * an odd test, because it would be unusual to use O_DIRECT for writes but not + * reads. + */ +TEST_F(WriteBack, o_direct) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t readbuf[bufsize]; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + FuseTest::expect_write(ino, 0, bufsize, bufsize, 0, FUSE_WRITE_CACHE, + CONTENTS); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDWR | O_DIRECT); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + /* A subsequent read must query the daemon because cache is empty */ + ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); + ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno); + ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno); + leak(fd); +} + +TEST_F(WriteBack, direct_io) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t readbuf[bufsize]; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, FOPEN_DIRECT_IO, 1); + FuseTest::expect_write(ino, 0, bufsize, bufsize, 0, FUSE_WRITE_CACHE, + CONTENTS); + expect_read(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + /* A subsequent read must query the daemon because cache is empty */ + ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); + ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno); + ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno); + leak(fd); +} + +/* + * mmap should still be possible even if the server used direct_io. Mmap will + * still use the cache, though. + * + * Regression test for bug 247276 + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=247276 + */ +TEST_F(WriteBack, mmap_direct_io) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + size_t len; + ssize_t bufsize = strlen(CONTENTS); + char *zeros; + void *p; + + len = getpagesize(); + zeros = new char[len](); + + expect_lookup(RELPATH, ino, len); + expect_open(ino, FOPEN_DIRECT_IO, 1); + expect_read(ino, 0, len, len, zeros); + expect_flush(ino, 1, ReturnErrno(0)); + FuseTest::expect_write(ino, 0, len, len, FUSE_WRITE_CACHE, 0, zeros); + expect_release(ino, ReturnErrno(0)); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + p = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + ASSERT_NE(MAP_FAILED, p) << strerror(errno); + + memmove((uint8_t*)p, CONTENTS, bufsize); + + ASSERT_EQ(0, munmap(p, len)) << strerror(errno); + close(fd); // Write mmap'd data on close + + delete[] zeros; +} + +/* + * When mounted with -o async, the writeback cache mode should delay writes + */ +TEST_F(WriteBackAsync, delay) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + /* Write should be cached, but FUSE_WRITE shouldn't be sent */ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_WRITE); + }, Eq(true)), + _) + ).Times(0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + + /* Don't close the file because that would flush the cache */ + leak(fd); +} + +/* + * A direct write should not evict dirty cached data from outside of its own + * byte range. + */ +TEST_F(WriteBackAsync, direct_io_ignores_unrelated_cached) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char CONTENTS0[] = "abcdefgh"; + const char CONTENTS1[] = "ijklmnop"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS0) + 1; + ssize_t fsize = 2 * m_maxbcachebuf; + char readbuf[bufsize]; + char *zeros; + + zeros = new char[m_maxbcachebuf](); + + expect_lookup(RELPATH, ino, fsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, m_maxbcachebuf, m_maxbcachebuf, zeros); + FuseTest::expect_write(ino, m_maxbcachebuf, bufsize, bufsize, 0, 0, + CONTENTS1); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + // Cache first block with dirty data. This will entail first reading + // the existing data. + ASSERT_EQ(bufsize, pwrite(fd, CONTENTS0, bufsize, 0)) + << strerror(errno); + + // Write directly to second block + ASSERT_EQ(0, fcntl(fd, F_SETFL, O_DIRECT)) << strerror(errno); + ASSERT_EQ(bufsize, pwrite(fd, CONTENTS1, bufsize, m_maxbcachebuf)) + << strerror(errno); + + // Read from the first block again. Should be serviced by cache. + ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno); + ASSERT_EQ(bufsize, pread(fd, readbuf, bufsize, 0)) << strerror(errno); + ASSERT_STREQ(readbuf, CONTENTS0); + + leak(fd); + delete[] zeros; +} + +/* + * If a direct io write partially overlaps one or two blocks of dirty cached + * data, No dirty data should be lost. Admittedly this is a weird test, + * because it would be unusual to use O_DIRECT and the writeback cache. + */ +TEST_F(WriteBackAsync, direct_io_partially_overlaps_cached_block) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + off_t bs = m_maxbcachebuf; + ssize_t fsize = 3 * bs; + char *readbuf, *zeros, *ones, *zeroones, *onezeros; + + readbuf = new char[bs]; + zeros = new char[3 * bs](); + ones = new char[2 * bs]; + memset(ones, 1, 2 * bs); + zeroones = new char[bs](); + memset((uint8_t*)zeroones + bs / 2, 1, bs / 2); + onezeros = new char[bs](); + memset(onezeros, 1, bs / 2); + + expect_lookup(RELPATH, ino, fsize); + expect_open(ino, 0, 1); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + /* Cache first and third blocks with dirty data. */ + ASSERT_EQ(3 * bs, pwrite(fd, zeros, 3 * bs, 0)) << strerror(errno); + + /* + * Write directly to all three blocks. The partially written blocks + * will be flushed because they're dirty. + */ + FuseTest::expect_write(ino, 0, bs, bs, 0, 0, zeros); + FuseTest::expect_write(ino, 2 * bs, bs, bs, 0, 0, zeros); + /* The direct write is split in two because of the m_maxwrite value */ + FuseTest::expect_write(ino, bs / 2, bs, bs, 0, 0, ones); + FuseTest::expect_write(ino, 3 * bs / 2, bs, bs, 0, 0, ones); + ASSERT_EQ(0, fcntl(fd, F_SETFL, O_DIRECT)) << strerror(errno); + ASSERT_EQ(2 * bs, pwrite(fd, ones, 2 * bs, bs / 2)) << strerror(errno); + + /* + * Read from both the valid and invalid portions of the first and third + * blocks again. This will entail FUSE_READ operations because these + * blocks were invalidated by the direct write. + */ + expect_read(ino, 0, bs, bs, zeroones); + expect_read(ino, 2 * bs, bs, bs, onezeros); + ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno); + ASSERT_EQ(bs / 2, pread(fd, readbuf, bs / 2, 0)) << strerror(errno); + EXPECT_EQ(0, memcmp(zeros, readbuf, bs / 2)); + ASSERT_EQ(bs / 2, pread(fd, readbuf, bs / 2, 5 * bs / 2)) + << strerror(errno); + EXPECT_EQ(0, memcmp(zeros, readbuf, bs / 2)); + ASSERT_EQ(bs / 2, pread(fd, readbuf, bs / 2, bs / 2)) + << strerror(errno); + EXPECT_EQ(0, memcmp(ones, readbuf, bs / 2)); + ASSERT_EQ(bs / 2, pread(fd, readbuf, bs / 2, 2 * bs)) + << strerror(errno); + EXPECT_EQ(0, memcmp(ones, readbuf, bs / 2)); + + leak(fd); + delete[] zeroones; + delete[] onezeros; + delete[] ones; + delete[] zeros; + delete[] readbuf; +} + +/* + * In WriteBack mode, writes may be cached beyond what the server thinks is the + * EOF. In this case, a short read at EOF should _not_ cause fusefs to update + * the file's size. + */ +TEST_F(WriteBackAsync, eof) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS0 = "abcdefgh"; + const char *CONTENTS1 = "ijklmnop"; + uint64_t ino = 42; + int fd; + off_t offset = m_maxbcachebuf; + ssize_t wbufsize = strlen(CONTENTS1); + off_t old_filesize = (off_t)strlen(CONTENTS0); + ssize_t rbufsize = 2 * old_filesize; + char readbuf[rbufsize]; + size_t holesize = rbufsize - old_filesize; + char hole[holesize]; + struct stat sb; + ssize_t r; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + expect_read(ino, 0, m_maxbcachebuf, old_filesize, CONTENTS0); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + /* Write and cache data beyond EOF */ + ASSERT_EQ(wbufsize, pwrite(fd, CONTENTS1, wbufsize, offset)) + << strerror(errno); + + /* Read from the old EOF */ + r = pread(fd, readbuf, rbufsize, 0); + ASSERT_LE(0, r) << strerror(errno); + EXPECT_EQ(rbufsize, r) << "read should've synthesized a hole"; + EXPECT_EQ(0, memcmp(CONTENTS0, readbuf, old_filesize)); + bzero(hole, holesize); + EXPECT_EQ(0, memcmp(hole, readbuf + old_filesize, holesize)); + + /* The file's size should still be what was established by pwrite */ + ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); + EXPECT_EQ(offset + wbufsize, sb.st_size); + leak(fd); +} + +/* + * Nothing bad should happen if a file with a dirty writeback cache is closed + * while the last copy lies in some socket's socket buffer. Inspired by bug + * 289686 . + */ +TEST_F(WriteBackAsync, scm_rights) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + int s[2]; + struct msghdr msg; + struct iovec iov; + char message[CMSG_SPACE(sizeof(int))]; + union { + char buf[CMSG_SPACE(sizeof(fd))]; + struct cmsghdr align; + } u; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + /* VOP_SETATTR will try to set timestamps during flush */ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr.size = bufsize; + }))); + + expect_write(ino, 0, bufsize, bufsize, CONTENTS); + expect_flush(ino, 1, ReturnErrno(0)); + expect_release(ino, ReturnErrno(0)); + + /* Open a file on the fusefs file system */ + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + /* Write to the file to dirty its writeback cache */ + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + + /* Send the file into a socket */ + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, s)) << strerror(errno); + memset(&message, 0, sizeof(message)); + memset(&msg, 0, sizeof(msg)); + iov.iov_base = NULL; + iov.iov_len = 0; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = u.buf, + msg.msg_controllen = sizeof(u.buf); + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(fd)); + memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd)); + ASSERT_GE(sendmsg(s[0], &msg, 0), 0) << strerror(errno); + + close(fd); // Close fd within our process + close(s[0]); + close(s[1]); // The last copy of fd is within this socket's rcvbuf +} + +/* + * When a file has dirty writes that haven't been flushed, the server's notion + * of its mtime and ctime will be wrong. The kernel should ignore those if it + * gets them from a FUSE_GETATTR before flushing. + */ +TEST_F(WriteBackAsync, timestamps) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + ssize_t bufsize = strlen(CONTENTS); + uint64_t ino = 42; + uint64_t attr_valid = 0; + uint64_t attr_valid_nsec = 0; + uint64_t server_time = 12345; + mode_t mode = S_IFREG | 0644; + int fd; + + struct stat sb; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = attr_valid; + out.body.entry.attr_valid_nsec = attr_valid_nsec; + }))); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke( + ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = mode; + out.body.attr.attr_valid = attr_valid; + out.body.attr.attr_valid_nsec = attr_valid_nsec; + out.body.attr.attr.atime = server_time; + out.body.attr.attr.mtime = server_time; + out.body.attr.attr.ctime = server_time; + }))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + + ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); + EXPECT_EQ((time_t)server_time, sb.st_atime); + EXPECT_NE((time_t)server_time, sb.st_mtime); + EXPECT_NE((time_t)server_time, sb.st_ctime); + + leak(fd); +} + +/* Any dirty timestamp fields should be flushed during a SETATTR */ +TEST_F(WriteBackAsync, timestamps_during_setattr) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + ssize_t bufsize = strlen(CONTENTS); + uint64_t ino = 42; + const mode_t newmode = 0755; + int fd; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + uint32_t valid = FATTR_MODE | FATTR_MTIME | FATTR_CTIME; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFREG | newmode; + }))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno); + + leak(fd); +} + +/* fuse_init_out.time_gran controls the granularity of timestamps */ +TEST_P(TimeGran, timestamps_during_setattr) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + ssize_t bufsize = strlen(CONTENTS); + uint64_t ino = 42; + const mode_t newmode = 0755; + int fd; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + uint32_t valid = FATTR_MODE | FATTR_MTIME | FATTR_CTIME; + return (in.header.opcode == FUSE_SETATTR && + in.header.nodeid == ino && + in.body.setattr.valid == valid && + in.body.setattr.mtimensec % m_time_gran == 0 && + in.body.setattr.ctimensec % m_time_gran == 0); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFREG | newmode; + }))); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno); + + leak(fd); +} + +INSTANTIATE_TEST_SUITE_P(RA, TimeGran, Range(0u, 10u)); + +/* + * Without direct_io, writes should be committed to cache + */ +TEST_F(Write, writethrough) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t readbuf[bufsize]; + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + expect_write(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + /* + * A subsequent read should be serviced by cache, without querying the + * filesystem daemon + */ + ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); + ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno); + leak(fd); +} + +/* Writes that extend a file should update the cached file size */ +TEST_F(Write, update_file_size) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + struct stat sb; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + + expect_lookup(RELPATH, ino, 0); + expect_open(ino, 0, 1); + expect_write(ino, 0, bufsize, bufsize, CONTENTS); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + /* Get cached attributes */ + ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); + ASSERT_EQ(bufsize, sb.st_size); + leak(fd); +} diff --git a/tests/sys/fs/fusefs/xattr.cc b/tests/sys/fs/fusefs/xattr.cc new file mode 100644 index 000000000000..0ab203c96254 --- /dev/null +++ b/tests/sys/fs/fusefs/xattr.cc @@ -0,0 +1,891 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* Tests for all things relating to extended attributes and FUSE */ + +extern "C" { +#include <sys/types.h> +#include <sys/extattr.h> +#include <sys/wait.h> +#include <semaphore.h> +#include <signal.h> +#include <string.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +const char FULLPATH[] = "mountpoint/some_file.txt"; +const char RELPATH[] = "some_file.txt"; +static sem_t killer_semaphore; + +void* killer(void* target) { + pid_t pid = *(pid_t*)target; + sem_wait(&killer_semaphore); + if (verbosity > 1) + printf("Killing! pid %d\n", pid); + kill(pid, SIGINT); + + return(NULL); +} + +class Xattr: public FuseTest { +public: +void expect_listxattr(uint64_t ino, uint32_t size, ProcessMockerT r, + Sequence *seq = NULL) +{ + if (seq == NULL) { + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LISTXATTR && + in.header.nodeid == ino && + in.body.listxattr.size == size); + }, Eq(true)), + _) + ).WillOnce(Invoke(r)) + .RetiresOnSaturation(); + } else { + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LISTXATTR && + in.header.nodeid == ino && + in.body.listxattr.size == size); + }, Eq(true)), + _) + ).InSequence(*seq) + .WillOnce(Invoke(r)) + .RetiresOnSaturation(); + } +} + +void expect_removexattr(uint64_t ino, const char *attr, int error) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *a = (const char*)in.body.bytes; + return (in.header.opcode == FUSE_REMOVEXATTR && + in.header.nodeid == ino && + 0 == strcmp(attr, a)); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(error))); +} + +void expect_setxattr(uint64_t ino, const char *attr, const char *value, + ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *a = (const char*)in.body.bytes + + sizeof(fuse_setxattr_in); + const char *v = a + strlen(a) + 1; + return (in.header.opcode == FUSE_SETXATTR && + in.header.nodeid == ino && + in.body.setxattr.size == (strlen(value) + 1) && + in.body.setxattr.setxattr_flags == 0 && + 0 == strcmp(attr, a) && + 0 == strcmp(value, v)); + }, Eq(true)), + _) + ).WillOnce(Invoke(r)); +} + +}; + +class Xattr_7_32:public FuseTest { +public: +virtual void SetUp() +{ + m_kernel_minor_version = 32; + FuseTest::SetUp(); +} + +void expect_setxattr_7_32(uint64_t ino, const char *attr, const char *value, + ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *a = (const char *)in.body.bytes + + FUSE_COMPAT_SETXATTR_IN_SIZE; + const char *v = a + strlen(a) + 1; + return (in.header.opcode == FUSE_SETXATTR && + in.header.nodeid == ino && + in.body.setxattr.size == (strlen(value) + 1) && + 0 == strcmp(attr, a) && + 0 == strcmp(value, v)); + }, Eq(true)), + _) + ).WillOnce(Invoke(r)); +} +}; + +class Getxattr: public Xattr {}; + +class Listxattr: public Xattr {}; + +/* Listxattr tests that need to use a signal */ +class ListxattrSig: public Listxattr { +public: +pthread_t m_killer_th; +pid_t m_child; + +void SetUp() { + /* + * Mount with -o nointr so the mount can't get interrupted while + * waiting for a response from the server + */ + m_nointr = true; + FuseTest::SetUp(); + + ASSERT_EQ(0, sem_init(&killer_semaphore, 0, 0)) << strerror(errno); +} + +void TearDown() { + if (m_killer_th != NULL) { + pthread_join(m_killer_th, NULL); + } + + sem_destroy(&killer_semaphore); + + FuseTest::TearDown(); +} +}; + +class Removexattr: public Xattr {}; +class Setxattr: public Xattr {}; +class Setxattr_7_32:public Xattr_7_32 {}; +class RofsXattr: public Xattr { +public: +virtual void SetUp() { + m_ro = true; + Xattr::SetUp(); +} +}; + +/* + * If the extended attribute does not exist on this file, the daemon should + * return ENOATTR (ENODATA on Linux, but it's up to the daemon to choose the + * correct errror code) + */ +TEST_F(Getxattr, enoattr) +{ + char data[80]; + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + ssize_t r; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_getxattr(ino, "user.foo", ReturnErrno(ENOATTR)); + + r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)); + ASSERT_EQ(-1, r); + ASSERT_EQ(ENOATTR, errno); +} + +/* + * If the filesystem returns ENOSYS, then it will be treated as a permanent + * failure and all future VOP_GETEXTATTR calls will fail with EOPNOTSUPP + * without querying the filesystem daemon + */ +TEST_F(Getxattr, enosys) +{ + char data[80]; + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + ssize_t r; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2); + expect_getxattr(ino, "user.foo", ReturnErrno(ENOSYS)); + + r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)); + ASSERT_EQ(-1, r); + EXPECT_EQ(EOPNOTSUPP, errno); + + /* Subsequent attempts should not query the filesystem at all */ + r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)); + ASSERT_EQ(-1, r); + EXPECT_EQ(EOPNOTSUPP, errno); +} + +/* + * On FreeBSD, if the user passes an insufficiently large buffer then the + * filesystem is supposed to copy as much of the attribute's value as will fit. + * + * On Linux, however, the filesystem is supposed to return ERANGE. + * + * libfuse specifies the Linux behavior. However, that's probably an error. + * It would probably be correct for the filesystem to use platform-dependent + * behavior. + * + * This test case covers a filesystem that uses the Linux behavior + * TODO: require FreeBSD Behavior. + */ +TEST_F(Getxattr, erange) +{ + char data[10]; + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + ssize_t r; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_getxattr(ino, "user.foo", ReturnErrno(ERANGE)); + + r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)); + ASSERT_EQ(-1, r); + ASSERT_EQ(ERANGE, errno); +} + +/* + * If the user passes a 0-length buffer, then the daemon should just return the + * size of the attribute + */ +TEST_F(Getxattr, size_only) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_getxattr(ino, "user.foo", + ReturnImmediate([](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, getxattr); + out.body.getxattr.size = 99; + }) + ); + + ASSERT_EQ(99, extattr_get_file(FULLPATH, ns, "foo", NULL, 0)) + << strerror(errno);; +} + +/* + * Successfully get an attribute from the system namespace + */ +TEST_F(Getxattr, system) +{ + uint64_t ino = 42; + char data[80]; + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + int ns = EXTATTR_NAMESPACE_SYSTEM; + ssize_t r; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_getxattr(ino, "system.foo", + ReturnImmediate([&](auto in __unused, auto& out) { + memcpy((void*)out.body.bytes, value, value_len); + out.header.len = sizeof(out.header) + value_len; + }) + ); + + r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)); + ASSERT_EQ(value_len, r) << strerror(errno); + EXPECT_STREQ(value, data); +} + +/* + * Successfully get an attribute from the user namespace + */ +TEST_F(Getxattr, user) +{ + uint64_t ino = 42; + char data[80]; + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + int ns = EXTATTR_NAMESPACE_USER; + ssize_t r; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_getxattr(ino, "user.foo", + ReturnImmediate([&](auto in __unused, auto& out) { + memcpy((void*)out.body.bytes, value, value_len); + out.header.len = sizeof(out.header) + value_len; + }) + ); + + r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)); + ASSERT_EQ(value_len, r) << strerror(errno); + EXPECT_STREQ(value, data); +} + +/* + * If the filesystem returns ENOSYS, then it will be treated as a permanent + * failure and all future VOP_LISTEXTATTR calls will fail with EOPNOTSUPP + * without querying the filesystem daemon + */ +TEST_F(Listxattr, enosys) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2); + expect_listxattr(ino, 0, ReturnErrno(ENOSYS)); + + ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0)); + EXPECT_EQ(EOPNOTSUPP, errno); + + /* Subsequent attempts should not query the filesystem at all */ + ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0)); + EXPECT_EQ(EOPNOTSUPP, errno); +} + +/* + * Listing extended attributes failed because they aren't configured on this + * filesystem + */ +TEST_F(Listxattr, enotsup) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_listxattr(ino, 0, ReturnErrno(ENOTSUP)); + + ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0)); + ASSERT_EQ(ENOTSUP, errno); +} + +/* + * On FreeBSD, if the user passes an insufficiently large buffer to + * extattr_list_file(2) or VOP_LISTEXTATTR(9), then the file system is supposed + * to copy as much of the attribute's value as will fit. + * + * On Linux, however, the file system is supposed to return ERANGE if an + * insufficiently large buffer is passed to listxattr(2). + * + * fusefs(4) must guarantee the usual FreeBSD behavior. + */ +TEST_F(Listxattr, erange) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + char attrs[9] = "user.foo"; + char expected[3] = {3, 'f', 'o'}; + char buf[3]; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) + { + out.body.listxattr.size = sizeof(attrs); + SET_OUT_HEADER_LEN(out, listxattr); + })); + expect_listxattr(ino, sizeof(attrs), + ReturnImmediate([&](auto in __unused, auto& out) { + memcpy((void*)out.body.bytes, attrs, sizeof(attrs)); + out.header.len = sizeof(fuse_out_header) + sizeof(attrs); + })); + + + ASSERT_EQ(static_cast<ssize_t>(sizeof(buf)), + extattr_list_file(FULLPATH, ns, buf, sizeof(buf))); + ASSERT_EQ(0, memcmp(expected, buf, sizeof(buf))); +} + +/* + * A buggy or malicious file system always returns ERANGE, even if we pass an + * appropriately sized buffer. That will send the kernel into an infinite + * loop. This test will ensure that the loop is interruptible by killing the + * blocked process with SIGINT. + */ +TEST_F(ListxattrSig, erange_forever) +{ + uint64_t ino = 42; + uint32_t lie_size = 10; + int status; + + fork(false, &status, [&] { + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LISTXATTR && + in.header.nodeid == ino && + in.body.listxattr.size == 0); + }, Eq(true)), + _) + ).WillRepeatedly(ReturnImmediate([=](auto i __unused, auto& out) + { + /* The file system requests 10 bytes, but it's a lie */ + out.body.listxattr.size = lie_size; + SET_OUT_HEADER_LEN(out, listxattr); + /* + * We can send the signal any time after fusefs enters + * VOP_LISTEXTATTR + */ + sem_post(&killer_semaphore); + })); + /* + * Even though the kernel faithfully respects our size request, + * we'll return ERANGE anyway. + */ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LISTXATTR && + in.header.nodeid == ino && + in.body.listxattr.size == lie_size); + }, Eq(true)), + _) + ).WillRepeatedly(ReturnErrno(ERANGE)); + + ASSERT_EQ(0, pthread_create(&m_killer_th, NULL, killer, + &m_mock->m_child_pid)) + << strerror(errno); + + }, [] { + /* Child process will block until it gets signaled */ + int ns = EXTATTR_NAMESPACE_USER; + char buf[3]; + extattr_list_file(FULLPATH, ns, buf, sizeof(buf)); + return 0; + } + ); + + ASSERT_TRUE(WIFSIGNALED(status)); +} + +/* + * Get the size of the list that it would take to list no extended attributes + */ +TEST_F(Listxattr, size_only_empty) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) { + out.body.listxattr.size = 0; + SET_OUT_HEADER_LEN(out, listxattr); + })); + + ASSERT_EQ(0, extattr_list_file(FULLPATH, ns, NULL, 0)) + << strerror(errno); +} + +/* + * Get the size of the list that it would take to list some extended + * attributes. Due to the format differences between a FreeBSD and a + * Linux/FUSE extended attribute list, fuse(4) will actually allocate a buffer + * and get the whole list, then convert it, just to figure out its size. + */ +TEST_F(Listxattr, size_only_nonempty) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + char attrs[9] = "user.foo"; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) + { + out.body.listxattr.size = sizeof(attrs); + SET_OUT_HEADER_LEN(out, listxattr); + })); + + expect_listxattr(ino, sizeof(attrs), + ReturnImmediate([=](auto in __unused, auto& out) { + size_t l = sizeof(attrs); + strlcpy((char*)out.body.bytes, attrs, l); + out.header.len = sizeof(fuse_out_header) + l; + }) + ); + + ASSERT_EQ(4, extattr_list_file(FULLPATH, ns, NULL, 0)) + << strerror(errno); +} + +/* + * The list of extended attributes grows in between the server's two calls to + * FUSE_LISTXATTR. + */ +TEST_F(Listxattr, size_only_race_bigger) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + char attrs0[9] = "user.foo"; + char attrs1[18] = "user.foo\0user.bar"; + Sequence seq; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + }))); + expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) + { + out.body.listxattr.size = sizeof(attrs0); + SET_OUT_HEADER_LEN(out, listxattr); + }), &seq); + + /* + * After the first FUSE_LISTXATTR the list grew, so the second + * operation returns ERANGE. + */ + expect_listxattr(ino, sizeof(attrs0), ReturnErrno(ERANGE), &seq); + + /* And now the kernel retries */ + expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) + { + out.body.listxattr.size = sizeof(attrs1); + SET_OUT_HEADER_LEN(out, listxattr); + }), &seq); + expect_listxattr(ino, sizeof(attrs1), + ReturnImmediate([&](auto in __unused, auto& out) { + memcpy((char*)out.body.bytes, attrs1, sizeof(attrs1)); + out.header.len = sizeof(fuse_out_header) + + sizeof(attrs1); + }), &seq + ); + + /* Userspace should never know about the retry */ + ASSERT_EQ(8, extattr_list_file(FULLPATH, ns, NULL, 0)) + << strerror(errno); +} + +/* + * The list of extended attributes shrinks in between the server's two calls to + * FUSE_LISTXATTR + */ +TEST_F(Listxattr, size_only_race_smaller) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + char attrs0[18] = "user.foo\0user.bar"; + char attrs1[9] = "user.foo"; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) + { + out.body.listxattr.size = sizeof(attrs0); + SET_OUT_HEADER_LEN(out, listxattr); + })); + expect_listxattr(ino, sizeof(attrs0), + ReturnImmediate([&](auto in __unused, auto& out) { + memcpy((char*)out.body.bytes, attrs1, sizeof(attrs1)); + out.header.len = sizeof(fuse_out_header) + + sizeof(attrs1); + }) + ); + + ASSERT_EQ(4, extattr_list_file(FULLPATH, ns, NULL, 0)) + << strerror(errno); +} + +TEST_F(Listxattr, size_only_really_big) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_listxattr(ino, 0, ReturnImmediate([](auto i __unused, auto& out) { + out.body.listxattr.size = 16000; + SET_OUT_HEADER_LEN(out, listxattr); + })); + + expect_listxattr(ino, 16000, + ReturnImmediate([](auto in __unused, auto& out) { + const char l[16] = "user.foobarbang"; + for (int i=0; i < 1000; i++) { + memcpy(&out.body.bytes[16 * i], l, 16); + } + out.header.len = sizeof(fuse_out_header) + 16000; + }) + ); + + ASSERT_EQ(11000, extattr_list_file(FULLPATH, ns, NULL, 0)) + << strerror(errno); +} + +/* + * List all of the user attributes of a file which has both user and system + * attributes + */ +TEST_F(Listxattr, user) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + char data[80]; + char expected[9] = {3, 'f', 'o', 'o', 4, 'b', 'a', 'n', 'g'}; + char attrs[28] = "user.foo\0system.x\0user.bang"; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_listxattr(ino, 0, + ReturnImmediate([&](auto in __unused, auto& out) { + out.body.listxattr.size = sizeof(attrs); + SET_OUT_HEADER_LEN(out, listxattr); + }) + ); + + expect_listxattr(ino, sizeof(attrs), + ReturnImmediate([&](auto in __unused, auto& out) { + memcpy((void*)out.body.bytes, attrs, sizeof(attrs)); + out.header.len = sizeof(fuse_out_header) + sizeof(attrs); + })); + + ASSERT_EQ(static_cast<ssize_t>(sizeof(expected)), + extattr_list_file(FULLPATH, ns, data, sizeof(data))) + << strerror(errno); + ASSERT_EQ(0, memcmp(expected, data, sizeof(expected))); +} + +/* + * List all of the system attributes of a file which has both user and system + * attributes + */ +TEST_F(Listxattr, system) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_SYSTEM; + char data[80]; + char expected[2] = {1, 'x'}; + char attrs[28] = "user.foo\0system.x\0user.bang"; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_listxattr(ino, 0, + ReturnImmediate([&](auto in __unused, auto& out) { + out.body.listxattr.size = sizeof(attrs); + SET_OUT_HEADER_LEN(out, listxattr); + }) + ); + + expect_listxattr(ino, sizeof(attrs), + ReturnImmediate([&](auto in __unused, auto& out) { + memcpy((void*)out.body.bytes, attrs, sizeof(attrs)); + out.header.len = sizeof(fuse_out_header) + sizeof(attrs); + })); + + ASSERT_EQ(static_cast<ssize_t>(sizeof(expected)), + extattr_list_file(FULLPATH, ns, data, sizeof(data))) + << strerror(errno); + ASSERT_EQ(0, memcmp(expected, data, sizeof(expected))); +} + +/* Fail to remove a nonexistent attribute */ +TEST_F(Removexattr, enoattr) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_removexattr(ino, "user.foo", ENOATTR); + + ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo")); + ASSERT_EQ(ENOATTR, errno); +} + +/* + * If the filesystem returns ENOSYS, then it will be treated as a permanent + * failure and all future VOP_DELETEEXTATTR calls will fail with EOPNOTSUPP + * without querying the filesystem daemon + */ +TEST_F(Removexattr, enosys) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2); + expect_removexattr(ino, "user.foo", ENOSYS); + + ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo")); + EXPECT_EQ(EOPNOTSUPP, errno); + + /* Subsequent attempts should not query the filesystem at all */ + ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo")); + EXPECT_EQ(EOPNOTSUPP, errno); +} + +/* Successfully remove a user xattr */ +TEST_F(Removexattr, user) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_removexattr(ino, "user.foo", 0); + + ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo")) + << strerror(errno); +} + +/* Successfully remove a system xattr */ +TEST_F(Removexattr, system) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_SYSTEM; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_removexattr(ino, "system.foo", 0); + + ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo")) + << strerror(errno); +} + + +/* + * If the filesystem returns ENOSYS, then it will be treated as a permanent + * failure and all future VOP_SETEXTATTR calls will fail with EOPNOTSUPP + * without querying the filesystem daemon + */ +TEST_F(Setxattr, enosys) +{ + uint64_t ino = 42; + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + int ns = EXTATTR_NAMESPACE_USER; + ssize_t r; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2); + expect_setxattr(ino, "user.foo", value, ReturnErrno(ENOSYS)); + + r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value, + value_len); + ASSERT_EQ(-1, r); + EXPECT_EQ(EOPNOTSUPP, errno); + + /* Subsequent attempts should not query the filesystem at all */ + r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value, + value_len); + ASSERT_EQ(-1, r); + EXPECT_EQ(EOPNOTSUPP, errno); +} + +/* + * SETXATTR will return ENOTSUP if the namespace is invalid or the filesystem + * as currently configured doesn't support extended attributes. + */ +TEST_F(Setxattr, enotsup) +{ + uint64_t ino = 42; + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + int ns = EXTATTR_NAMESPACE_USER; + ssize_t r; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_setxattr(ino, "user.foo", value, ReturnErrno(ENOTSUP)); + + r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value, + value_len); + ASSERT_EQ(-1, r); + EXPECT_EQ(ENOTSUP, errno); +} + +/* + * Successfully set a user attribute. + */ +TEST_F(Setxattr, user) +{ + uint64_t ino = 42; + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + int ns = EXTATTR_NAMESPACE_USER; + ssize_t r; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_setxattr(ino, "user.foo", value, ReturnErrno(0)); + + r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value, + value_len); + ASSERT_EQ(value_len, r) << strerror(errno); +} + +/* + * Successfully set a system attribute. + */ +TEST_F(Setxattr, system) +{ + uint64_t ino = 42; + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + int ns = EXTATTR_NAMESPACE_SYSTEM; + ssize_t r; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_setxattr(ino, "system.foo", value, ReturnErrno(0)); + + r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value, + value_len); + ASSERT_EQ(value_len, r) << strerror(errno); +} + + +TEST_F(Setxattr_7_32, ok) +{ + uint64_t ino = 42; + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + int ns = EXTATTR_NAMESPACE_USER; + ssize_t r; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_setxattr_7_32(ino, "user.foo", value, ReturnErrno(0)); + + r = extattr_set_file(FULLPATH, ns, "foo", (const void *)value, + value_len); + ASSERT_EQ(value_len, r) << strerror(errno); +} + +TEST_F(RofsXattr, deleteextattr_erofs) +{ + uint64_t ino = 42; + int ns = EXTATTR_NAMESPACE_USER; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + + ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo")); + ASSERT_EQ(EROFS, errno); +} + +TEST_F(RofsXattr, setextattr_erofs) +{ + uint64_t ino = 42; + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + int ns = EXTATTR_NAMESPACE_USER; + ssize_t r; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + + r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value, + value_len); + ASSERT_EQ(-1, r); + EXPECT_EQ(EROFS, errno); +} |
