Index: stable/12/tests/sys/fs/fusefs/allow_other.cc =================================================================== --- stable/12/tests/sys/fs/fusefs/allow_other.cc (revision 368595) +++ stable/12/tests/sys/fs/fusefs/allow_other.cc (revision 368596) @@ -1,307 +1,307 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ /* * Tests for the "allow_other" mount option. They must be in their own * file so they can be run as root */ extern "C" { #include #include #include #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; const static char FULLPATH[] = "mountpoint/some_file.txt"; const static char RELPATH[] = "some_file.txt"; class NoAllowOther: public FuseTest { public: /* Unprivileged user id */ int m_uid; virtual void SetUp() { if (geteuid() != 0) { GTEST_SKIP() << "This test must be run as root"; } FuseTest::SetUp(); } }; class AllowOther: public NoAllowOther { public: virtual void SetUp() { m_allow_other = true; NoAllowOther::SetUp(); } }; TEST_F(AllowOther, allowed) { int status; fork(true, &status, [&] { uint64_t ino = 42; expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); expect_open(ino, 0, 1); expect_flush(ino, 1, ReturnErrno(0)); expect_release(ino, FH); }, []() { int fd; fd = open(FULLPATH, O_RDONLY); if (fd < 0) { perror("open"); return(1); } return 0; } ); ASSERT_EQ(0, WEXITSTATUS(status)); } /* Check that fusefs uses the correct credentials for FUSE operations */ TEST_F(AllowOther, creds) { int status; uid_t uid; gid_t gid; get_unprivileged_id(&uid, &gid); fork(true, &status, [=] { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LOOKUP && in.header.uid == uid && in.header.gid == gid); }, Eq(true)), _) ).Times(1) .WillOnce(Invoke(ReturnErrno(ENOENT))); }, []() { eaccess(FULLPATH, F_OK); return 0; } ); ASSERT_EQ(0, WEXITSTATUS(status)); } /* * A variation of the Open.multiple_creds test showing how the bug can lead to a * privilege elevation. The first process is privileged and opens a file only * visible to root. The second process is unprivileged and shouldn't be able * to open the file, but does thanks to the bug */ TEST_F(AllowOther, privilege_escalation) { int fd1, status; const static uint64_t ino = 42; const static uint64_t fh = 100; /* Fork a child to open the file with different credentials */ fork(true, &status, [&] { expect_lookup(RELPATH, ino, S_IFREG | 0600, 0, 2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_OPEN && in.header.pid == (uint32_t)getpid() && in.header.uid == (uint32_t)geteuid() && in.header.nodeid == ino); }, Eq(true)), _) ).WillOnce(Invoke( ReturnImmediate([](auto in __unused, auto& out) { out.body.open.fh = fh; out.header.len = sizeof(out.header); SET_OUT_HEADER_LEN(out, open); }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_OPEN && in.header.pid != (uint32_t)getpid() && in.header.uid != (uint32_t)geteuid() && in.header.nodeid == ino); }, Eq(true)), _) ).Times(AnyNumber()) .WillRepeatedly(Invoke(ReturnErrno(EPERM))); fd1 = open(FULLPATH, O_RDONLY); - EXPECT_LE(0, fd1) << strerror(errno); + 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"); return(1); } else if (errno != EPERM) { fprintf(stderr, "Unexpected error: %s\n", strerror(errno)); return(1); } return 0; } ); ASSERT_EQ(0, WEXITSTATUS(status)); } /* * When -o allow_other is not used, users other than the owner aren't allowed * to open anything inside of the mount point, not just the mountpoint itself * This is a regression test for bug 237052 */ TEST_F(NoAllowOther, disallowed_beneath_root) { const static char RELPATH2[] = "other_dir"; const static uint64_t ino = 42; const static uint64_t ino2 = 43; int dfd, status; expect_lookup(RELPATH, ino, S_IFDIR | 0755, 0, 1); EXPECT_LOOKUP(ino, RELPATH2) .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | 0644; out.body.entry.nodeid = ino2; out.body.entry.attr.nlink = 1; out.body.entry.attr_valid = UINT64_MAX; }))); expect_opendir(ino); dfd = open(FULLPATH, O_DIRECTORY); ASSERT_LE(0, dfd) << strerror(errno); fork(true, &status, [] { }, [&]() { int fd; fd = openat(dfd, RELPATH2, O_RDONLY); if (fd >= 0) { fprintf(stderr, "openat should've failed\n"); return(1); } else if (errno != EPERM) { fprintf(stderr, "Unexpected error: %s\n", strerror(errno)); return(1); } return 0; } ); ASSERT_EQ(0, WEXITSTATUS(status)); 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)); } Index: stable/12/tests/sys/fs/fusefs/create.cc =================================================================== --- stable/12/tests/sys/fs/fusefs/create.cc (revision 368595) +++ stable/12/tests/sys/fs/fusefs/create.cc (revision 368596) @@ -1,451 +1,451 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ extern "C" { #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; class Create: public FuseTest { public: void expect_create(const char *relpath, mode_t mode, ProcessMockerT r) { mode_t mask = umask(0); (void)umask(mask); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { const char *name = (const char*)in.body.bytes + sizeof(fuse_create_in); return (in.header.opcode == FUSE_CREATE && in.body.create.mode == mode && in.body.create.umask == mask && (0 == strcmp(relpath, name))); }, Eq(true)), _) ).WillOnce(Invoke(r)); } }; /* FUSE_CREATE operations for a protocol 7.8 server */ class Create_7_8: public Create { public: virtual void SetUp() { m_kernel_minor_version = 8; Create::SetUp(); } void expect_create(const char *relpath, mode_t mode, ProcessMockerT r) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { const char *name = (const char*)in.body.bytes + sizeof(fuse_open_in); return (in.header.opcode == FUSE_CREATE && in.body.create.mode == mode && (0 == strcmp(relpath, name))); }, Eq(true)), _) ).WillOnce(Invoke(r)); } }; /* FUSE_CREATE operations for a server built at protocol <= 7.11 */ class Create_7_11: public FuseTest { public: virtual void SetUp() { m_kernel_minor_version = 11; FuseTest::SetUp(); } void expect_create(const char *relpath, mode_t mode, ProcessMockerT r) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { const char *name = (const char*)in.body.bytes + sizeof(fuse_open_in); return (in.header.opcode == FUSE_CREATE && in.body.create.mode == mode && (0 == strcmp(relpath, name))); }, Eq(true)), _) ).WillOnce(Invoke(r)); } }; /* * If FUSE_CREATE sets attr_valid, then subsequent GETATTRs should use the * attribute cache */ TEST_F(Create, attr_cache) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; mode_t mode = S_IFREG | 0755; uint64_t ino = 42; int fd; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnErrno(ENOENT))); expect_create(RELPATH, mode, ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, create); out.body.create.entry.attr.mode = mode; out.body.create.entry.nodeid = ino; out.body.create.entry.entry_valid = UINT64_MAX; out.body.create.entry.attr_valid = UINT64_MAX; })); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_GETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).Times(0); fd = open(FULLPATH, O_CREAT | O_EXCL, mode); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); leak(fd); } /* * A regression test for a bug that affected old FUSE implementations: * open(..., O_WRONLY | O_CREAT, 0444) should work despite the seeming * contradiction between O_WRONLY and 0444 * * For example: * https://bugs.launchpad.net/ubuntu/+source/sshfs-fuse/+bug/44886 */ TEST_F(Create, wronly_0444) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; mode_t mode = S_IFREG | 0444; uint64_t ino = 42; int fd; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnErrno(ENOENT))); expect_create(RELPATH, mode, ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, create); out.body.create.entry.attr.mode = mode; out.body.create.entry.nodeid = ino; out.body.create.entry.entry_valid = UINT64_MAX; out.body.create.entry.attr_valid = UINT64_MAX; })); fd = open(FULLPATH, O_CREAT | O_WRONLY, mode); - EXPECT_LE(0, fd) << strerror(errno); + 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.entry.attr.mode = mode; out.body.create.entry.nodeid = ino; out.body.create.entry.entry_valid = UINT64_MAX; out.body.create.entry.attr_valid = UINT64_MAX; })); fd = open(FULLPATH, O_CREAT | O_EXCL, mode); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); leak(fd); } TEST_F(Create_7_11, ok) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; mode_t mode = S_IFREG | 0755; uint64_t ino = 42; int fd; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnErrno(ENOENT))); expect_create(RELPATH, mode, ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, create); out.body.create.entry.attr.mode = mode; out.body.create.entry.nodeid = ino; out.body.create.entry.entry_valid = UINT64_MAX; out.body.create.entry.attr_valid = UINT64_MAX; })); fd = open(FULLPATH, O_CREAT | O_EXCL, mode); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); leak(fd); } Index: stable/12/tests/sys/fs/fusefs/default_permissions.cc =================================================================== --- stable/12/tests/sys/fs/fusefs/default_permissions.cc (revision 368595) +++ stable/12/tests/sys/fs/fusefs/default_permissions.cc (revision 368596) @@ -1,1315 +1,1315 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ /* * Tests for the "default_permissions" mount option. They must be in their own * file so they can be run as an unprivileged user */ extern "C" { #include #include #include #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; class DefaultPermissions: public FuseTest { virtual void SetUp() { m_default_permissions = true; FuseTest::SetUp(); if (HasFatalFailure() || IsSkipped()) return; if (geteuid() == 0) { GTEST_SKIP() << "This test requires an unprivileged user"; } /* With -o default_permissions, FUSE_ACCESS should never be called */ EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_ACCESS); }, Eq(true)), _) ).Times(0); } public: void expect_chmod(uint64_t ino, mode_t mode, uint64_t size = 0) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == FATTR_MODE && in.body.setattr.mode == mode); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid out.body.attr.attr.mode = S_IFREG | mode; out.body.attr.attr.size = size; out.body.attr.attr_valid = UINT64_MAX; }))); } void expect_create(const char *relpath, uint64_t ino) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { const char *name = (const char*)in.body.bytes + sizeof(fuse_create_in); return (in.header.opcode == FUSE_CREATE && (0 == strcmp(relpath, name))); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, create); out.body.create.entry.attr.mode = S_IFREG | 0644; out.body.create.entry.nodeid = ino; out.body.create.entry.entry_valid = UINT64_MAX; out.body.create.entry.attr_valid = UINT64_MAX; }))); } void expect_getattr(uint64_t ino, mode_t mode, uint64_t attr_valid, int times, uid_t uid = 0, gid_t gid = 0) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_GETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).Times(times) .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid out.body.attr.attr.mode = mode; out.body.attr.attr.size = 0; out.body.attr.attr.uid = uid; out.body.attr.attr.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 Lookup: public DefaultPermissions {}; class Open: public DefaultPermissions {}; class Setattr: public DefaultPermissions {}; class Unlink: public DefaultPermissions {}; class Utimensat: public DefaultPermissions {}; class Write: public DefaultPermissions {}; /* * Test permission handling during create, mkdir, mknod, link, symlink, and * rename vops (they all share a common path for permission checks in * VOP_LOOKUP) */ class Create: public DefaultPermissions {}; class Deleteextattr: public DefaultPermissions { public: void expect_removexattr() { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_REMOVEXATTR); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(0))); } }; class Getextattr: public DefaultPermissions { public: void expect_getxattr(ProcessMockerT r) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_GETXATTR); }, Eq(true)), _) ).WillOnce(Invoke(r)); } }; class Listextattr: public DefaultPermissions { public: void expect_listxattr() { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LISTXATTR); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) { out.body.listxattr.size = 0; SET_OUT_HEADER_LEN(out, listxattr); }))); } }; class Rename: public DefaultPermissions { public: /* * Expect a rename and respond with the given error. Don't both to * validate arguments; the tests in rename.cc do that. */ void expect_rename(int error) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_RENAME); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(error))); } }; class Setextattr: public DefaultPermissions { public: void expect_setxattr(int error) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_SETXATTR); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(error))); } }; /* Return a group to which this user does not belong */ static gid_t excluded_group() { int i, ngroups = 64; gid_t newgid, groups[ngroups]; getgrouplist(getlogin(), getegid(), groups, &ngroups); for (newgid = 0; ; newgid++) { bool belongs = false; for (i = 0; i < ngroups; i++) { if (groups[i] == newgid) belongs = true; } if (!belongs) break; } /* newgid is now a group to which the current user does not belong */ return newgid; } TEST_F(Access, eacces) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; mode_t access_mode = X_OK; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX); ASSERT_NE(0, access(FULLPATH, access_mode)); ASSERT_EQ(EACCES, errno); } TEST_F(Access, eacces_no_cached_attrs) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; mode_t access_mode = X_OK; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, 0, 1); expect_lookup(RELPATH, ino, S_IFREG | 0644, 0); expect_getattr(ino, S_IFREG | 0644, 0, 1); /* * Once default_permissions is properly implemented, there might be * another FUSE_GETATTR or something in here. But there should not be * a FUSE_ACCESS */ ASSERT_NE(0, access(FULLPATH, access_mode)); ASSERT_EQ(EACCES, errno); } TEST_F(Access, ok) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; mode_t access_mode = R_OK; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX); /* * Once default_permissions is properly implemented, there might be * another FUSE_GETATTR or something in here. */ ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno); } /* Unprivileged users may chown a file to their own uid */ TEST_F(Chown, chown_to_self) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const mode_t mode = 0755; uid_t uid; uid = geteuid(); expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid); expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid); /* The OS may optimize chown by omitting the redundant setattr */ EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_SETATTR); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out){ SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.mode = S_IFREG | mode; out.body.attr.attr.uid = uid; }))); EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno); } /* * A successful chown by a non-privileged non-owner should clear a file's SUID * bit */ TEST_F(Chown, clear_suid) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; const mode_t oldmode = 06755; const mode_t newmode = 0755; uid_t uid = geteuid(); uint32_t valid = FATTR_UID | FATTR_MODE; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid); expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid && in.body.setattr.mode == newmode); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid out.body.attr.attr.mode = S_IFREG | newmode; out.body.attr.attr_valid = UINT64_MAX; }))); EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno); } /* Only root may change a file's owner */ TEST_F(Chown, eperm) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const mode_t mode = 0755; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, geteuid()); expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, geteuid()); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_SETATTR); }, Eq(true)), _) ).Times(0); EXPECT_NE(0, chown(FULLPATH, 0, -1)); EXPECT_EQ(EPERM, errno); } /* * A successful chgrp by a non-privileged non-owner should clear a file's SUID * bit */ TEST_F(Chgrp, clear_suid) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; const mode_t oldmode = 06755; const mode_t newmode = 0755; uid_t uid = geteuid(); gid_t gid = getegid(); uint32_t valid = FATTR_GID | FATTR_MODE; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid); expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid && in.body.setattr.mode == newmode); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid out.body.attr.attr.mode = S_IFREG | newmode; out.body.attr.attr_valid = UINT64_MAX; }))); EXPECT_EQ(0, chown(FULLPATH, -1, gid)) << strerror(errno); } /* non-root users may only chgrp a file to a group they belong to */ TEST_F(Chgrp, eperm) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const mode_t mode = 0755; uid_t uid; gid_t gid, newgid; uid = geteuid(); gid = getegid(); newgid = excluded_group(); expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid); expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_SETATTR); }, Eq(true)), _) ).Times(0); EXPECT_NE(0, chown(FULLPATH, -1, newgid)); EXPECT_EQ(EPERM, errno); } TEST_F(Chgrp, ok) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const mode_t mode = 0755; uid_t uid; gid_t gid, newgid; uid = geteuid(); gid = 0; newgid = getegid(); expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid); expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid); /* The OS may optimize chgrp by omitting the redundant setattr */ EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out){ SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.mode = S_IFREG | mode; out.body.attr.attr.uid = uid; out.body.attr.attr.gid = newgid; }))); EXPECT_EQ(0, chown(FULLPATH, -1, newgid)) << strerror(errno); } TEST_F(Create, ok) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnErrno(ENOENT))); expect_create(RELPATH, ino); fd = open(FULLPATH, O_CREAT | O_EXCL, 0644); - EXPECT_LE(0, fd) << strerror(errno); + 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 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); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); leak(fd); } TEST_F(Rename, eacces_on_srcdir) { const char FULLDST[] = "mountpoint/d/dst"; const char RELDST[] = "d/dst"; const char FULLSRC[] = "mountpoint/src"; const char RELSRC[] = "src"; uint64_t ino = 42; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, 0); expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX); EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) .Times(AnyNumber()) .WillRepeatedly(Invoke(ReturnErrno(ENOENT))); ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); ASSERT_EQ(EACCES, errno); } TEST_F(Rename, eacces_on_dstdir_for_creating) { const char FULLDST[] = "mountpoint/d/dst"; const char RELDSTDIR[] = "d"; const char RELDST[] = "dst"; const char FULLSRC[] = "mountpoint/src"; const char RELSRC[] = "src"; uint64_t src_ino = 42; uint64_t dstdir_ino = 43; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX); expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX); EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT))); ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); ASSERT_EQ(EACCES, errno); } TEST_F(Rename, eacces_on_dstdir_for_removing) { const char FULLDST[] = "mountpoint/d/dst"; const char RELDSTDIR[] = "d"; const char RELDST[] = "dst"; const char FULLSRC[] = "mountpoint/src"; const char RELSRC[] = "src"; uint64_t src_ino = 42; uint64_t dstdir_ino = 43; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX); expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX); EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT))); ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); ASSERT_EQ(EACCES, errno); } TEST_F(Rename, eperm_on_sticky_srcdir) { const char FULLDST[] = "mountpoint/d/dst"; const char FULLSRC[] = "mountpoint/src"; const char RELSRC[] = "src"; uint64_t ino = 42; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0); expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX); ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); ASSERT_EQ(EPERM, errno); } /* * A user cannot move out a subdirectory that he does not own, because that * would require changing the subdirectory's ".." dirent */ TEST_F(Rename, eperm_for_subdirectory) { const char FULLDST[] = "mountpoint/d/dst"; const char FULLSRC[] = "mountpoint/src"; const char RELDSTDIR[] = "d"; const char RELDST[] = "dst"; const char RELSRC[] = "src"; uint64_t ino = 42; uint64_t dstdir_ino = 43; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0); expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0777, UINT64_MAX, 0); EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT))); ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); ASSERT_EQ(EACCES, errno); } /* * A user _can_ rename a subdirectory to which he lacks write permissions, if * it will keep the same parent */ TEST_F(Rename, subdirectory_to_same_dir) { const char FULLDST[] = "mountpoint/dst"; const char FULLSRC[] = "mountpoint/src"; const char RELDST[] = "dst"; const char RELSRC[] = "src"; uint64_t ino = 42; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0); EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) .WillOnce(Invoke(ReturnErrno(ENOENT))); expect_rename(0); ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); } TEST_F(Rename, eperm_on_sticky_dstdir) { const char FULLDST[] = "mountpoint/d/dst"; const char RELDSTDIR[] = "d"; const char RELDST[] = "dst"; const char FULLSRC[] = "mountpoint/src"; const char RELSRC[] = "src"; uint64_t src_ino = 42; uint64_t dstdir_ino = 43; uint64_t dst_ino = 44; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX); expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 01777, UINT64_MAX); EXPECT_LOOKUP(dstdir_ino, RELDST) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | 0644; out.body.entry.nodeid = dst_ino; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = UINT64_MAX; out.body.entry.attr.uid = 0; }))); ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); ASSERT_EQ(EPERM, errno); } /* Successfully rename a file, overwriting the destination */ TEST_F(Rename, ok) { const char FULLDST[] = "mountpoint/dst"; const char RELDST[] = "dst"; const char FULLSRC[] = "mountpoint/src"; const char RELSRC[] = "src"; // The inode of the already-existing destination file uint64_t dst_ino = 2; uint64_t ino = 42; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, geteuid()); expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX); expect_lookup(RELDST, dst_ino, S_IFREG | 0644, UINT64_MAX); expect_rename(0); ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); } TEST_F(Rename, ok_to_remove_src_because_of_stickiness) { const char FULLDST[] = "mountpoint/dst"; const char RELDST[] = "dst"; const char FULLSRC[] = "mountpoint/src"; const char RELSRC[] = "src"; uint64_t ino = 42; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0); expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) .WillOnce(Invoke(ReturnErrno(ENOENT))); expect_rename(0); ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); } TEST_F(Setattr, ok) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const mode_t oldmode = 0755; const mode_t newmode = 0644; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid()); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.mode == newmode); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.mode = S_IFREG | newmode; }))); EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); } TEST_F(Setattr, eacces) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const mode_t oldmode = 0755; const mode_t newmode = 0644; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, 0); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_SETATTR); }, Eq(true)), _) ).Times(0); EXPECT_NE(0, chmod(FULLPATH, newmode)); EXPECT_EQ(EPERM, errno); } /* * ftruncate() of a file without writable permissions should succeed as long as * the file descriptor is writable. This is important when combined with * O_CREAT */ TEST_F(Setattr, ftruncate_of_newly_created_file) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const mode_t mode = 0000; int fd; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnErrno(ENOENT))); expect_create(RELPATH, ino); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && (in.body.setattr.valid & FATTR_SIZE)); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = S_IFREG | mode; out.body.attr.attr_valid = UINT64_MAX; }))); fd = open(FULLPATH, O_CREAT | O_RDWR, 0); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, ftruncate(fd, 100)) << strerror(errno); leak(fd); } /* * Setting the sgid bit should fail for an unprivileged user who doesn't belong * to the file's group */ TEST_F(Setattr, sgid_by_non_group_member) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const mode_t oldmode = 0755; const mode_t newmode = 02755; uid_t uid = geteuid(); gid_t gid = excluded_group(); expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_SETATTR); }, Eq(true)), _) ).Times(0); EXPECT_NE(0, chmod(FULLPATH, newmode)); EXPECT_EQ(EPERM, errno); } /* Only the superuser may set the sticky bit on a non-directory */ TEST_F(Setattr, sticky_regular_file) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const mode_t oldmode = 0644; const mode_t newmode = 01644; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid()); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_SETATTR); }, Eq(true)), _) ).Times(0); EXPECT_NE(0, chmod(FULLPATH, newmode)); EXPECT_EQ(EFTYPE, errno); } TEST_F(Setextattr, ok) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; const char value[] = "whatever"; ssize_t value_len = strlen(value) + 1; int ns = EXTATTR_NAMESPACE_USER; ssize_t r; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); expect_setxattr(0); r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value, value_len); ASSERT_EQ(value_len, r) << strerror(errno); } TEST_F(Setextattr, eacces) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; const char value[] = "whatever"; ssize_t value_len = strlen(value) + 1; int ns = EXTATTR_NAMESPACE_USER; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0); ASSERT_EQ(-1, extattr_set_file(FULLPATH, ns, "foo", (const void*)value, value_len)); ASSERT_EQ(EACCES, errno); } // Setting system attributes requires superuser privileges TEST_F(Setextattr, system) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; const char value[] = "whatever"; ssize_t value_len = strlen(value) + 1; int ns = EXTATTR_NAMESPACE_SYSTEM; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid()); ASSERT_EQ(-1, extattr_set_file(FULLPATH, ns, "foo", (const void*)value, value_len)); ASSERT_EQ(EPERM, errno); } // Setting user attributes merely requires write privileges TEST_F(Setextattr, user) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; const char value[] = "whatever"; ssize_t value_len = strlen(value) + 1; int ns = EXTATTR_NAMESPACE_USER; ssize_t r; expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0); expect_setxattr(0); r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value, value_len); ASSERT_EQ(value_len, r) << strerror(errno); } TEST_F(Unlink, ok) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; 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); } Index: stable/12/tests/sys/fs/fusefs/flush.cc =================================================================== --- stable/12/tests/sys/fs/fusefs/flush.cc (revision 368595) +++ stable/12/tests/sys/fs/fusefs/flush.cc (revision 368596) @@ -1,234 +1,234 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ extern "C" { #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; class Flush: public FuseTest { public: void expect_flush(uint64_t ino, int times, pid_t lo, ProcessMockerT r) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_FLUSH && in.header.nodeid == ino && in.body.flush.lock_owner == (uint64_t)lo && in.body.flush.fh == FH); }, Eq(true)), _) ).Times(times) .WillRepeatedly(Invoke(r)); } void expect_lookup(const char *relpath, uint64_t ino, int times) { FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, times); } /* * When testing FUSE_FLUSH, the FUSE_RELEASE calls are uninteresting. This * expectation will silence googlemock warnings */ void expect_release() { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_RELEASE); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnErrno(0))); } }; class FlushWithLocks: public Flush { virtual void SetUp() { m_init_flags = FUSE_POSIX_LOCKS; Flush::SetUp(); } }; /* * If multiple file descriptors refer to the same file handle, closing each * should send FUSE_FLUSH */ TEST_F(Flush, open_twice) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd, fd2; expect_lookup(RELPATH, ino, 2); expect_open(ino, 0, 1); expect_flush(ino, 2, getpid(), ReturnErrno(0)); expect_release(); fd = open(FULLPATH, O_WRONLY); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); fd2 = open(FULLPATH, O_WRONLY); - EXPECT_LE(0, fd2) << strerror(errno); + ASSERT_LE(0, fd2) << strerror(errno); EXPECT_EQ(0, close(fd2)) << strerror(errno); EXPECT_EQ(0, close(fd)) << strerror(errno); } /* * Some FUSE filesystem cache data internally and flush it on release. Such * filesystems may generate errors during release. On Linux, these get * returned by close(2). However, POSIX does not require close(2) to return * this error. FreeBSD's fuse(4) should return EIO if it returns an error at * all. */ /* http://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html */ TEST_F(Flush, eio) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; expect_lookup(RELPATH, ino, 1); expect_open(ino, 0, 1); expect_flush(ino, 1, getpid(), ReturnErrno(EIO)); expect_release(); fd = open(FULLPATH, O_WRONLY); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); ASSERT_TRUE(0 == close(fd) || errno == EIO) << strerror(errno); } /* * If the filesystem returns ENOSYS, it will be treated as success and * no more FUSE_FLUSH operations will be sent to the daemon */ TEST_F(Flush, enosys) { const char FULLPATH0[] = "mountpoint/some_file.txt"; const char RELPATH0[] = "some_file.txt"; const char FULLPATH1[] = "mountpoint/other_file.txt"; const char RELPATH1[] = "other_file.txt"; uint64_t ino0 = 42; uint64_t ino1 = 43; int fd0, fd1; expect_lookup(RELPATH0, ino0, 1); expect_open(ino0, 0, 1); /* On the 2nd close, FUSE_FLUSH won't be sent at all */ expect_flush(ino0, 1, getpid(), ReturnErrno(ENOSYS)); expect_release(); expect_lookup(RELPATH1, ino1, 1); expect_open(ino1, 0, 1); /* On the 2nd close, FUSE_FLUSH won't be sent at all */ expect_release(); fd0 = open(FULLPATH0, O_WRONLY); ASSERT_LE(0, fd0) << strerror(errno); fd1 = open(FULLPATH1, O_WRONLY); ASSERT_LE(0, fd1) << strerror(errno); EXPECT_EQ(0, close(fd0)) << strerror(errno); EXPECT_EQ(0, close(fd1)) << strerror(errno); } /* A FUSE_FLUSH should be sent on close(2) */ TEST_F(Flush, flush) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; expect_lookup(RELPATH, ino, 1); expect_open(ino, 0, 1); expect_flush(ino, 1, getpid(), ReturnErrno(0)); expect_release(); fd = open(FULLPATH, O_WRONLY); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); ASSERT_TRUE(0 == close(fd)) << strerror(errno); } /* * When closing a file with a POSIX file lock, flush should release the lock, * _even_if_ it's not the process's last file descriptor for this file. */ TEST_F(FlushWithLocks, unlock_on_close) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd, fd2; struct flock fl; pid_t pid = getpid(); expect_lookup(RELPATH, ino, 2); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_SETLK && in.header.nodeid == ino && in.body.setlk.fh == FH); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(0))); expect_flush(ino, 1, pid, ReturnErrno(0)); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); fl.l_start = 0; fl.l_len = 0; fl.l_pid = pid; fl.l_type = F_RDLCK; fl.l_whence = SEEK_SET; fl.l_sysid = 0; ASSERT_NE(-1, fcntl(fd, F_SETLKW, &fl)) << strerror(errno); fd2 = open(FULLPATH, O_WRONLY); ASSERT_LE(0, fd2) << strerror(errno); ASSERT_EQ(0, close(fd2)) << strerror(errno); leak(fd); leak(fd2); } Index: stable/12/tests/sys/fs/fusefs/open.cc =================================================================== --- stable/12/tests/sys/fs/fusefs/open.cc (revision 368595) +++ stable/12/tests/sys/fs/fusefs/open.cc (revision 368596) @@ -1,276 +1,276 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ extern "C" { #include #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; class Open: public FuseTest { public: /* Test an OK open of a file with the given flags */ void test_ok(int os_flags, int fuse_flags) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_OPEN && in.body.open.flags == (uint32_t)fuse_flags && in.header.nodeid == ino); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { out.header.len = sizeof(out.header); SET_OUT_HEADER_LEN(out, open); }))); fd = open(FULLPATH, os_flags); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); leak(fd); } }; /* * fusefs(5) does not support I/O on device nodes (neither does UFS). But it * shouldn't crash */ TEST_F(Open, chr) { const char FULLPATH[] = "mountpoint/zero"; const char RELPATH[] = "zero"; uint64_t ino = 42; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFCHR | 0644; out.body.entry.nodeid = ino; out.body.entry.attr.nlink = 1; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.attr.rdev = 44; /* /dev/zero's rdev */ }))); ASSERT_EQ(-1, open(FULLPATH, O_RDONLY)); EXPECT_EQ(EOPNOTSUPP, errno); } /* * The fuse daemon fails the request with enoent. This usually indicates a * race condition: some other FUSE client removed the file in between when the * kernel checked for it with lookup and tried to open it */ TEST_F(Open, enoent) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; 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); - EXPECT_LE(0, fd1) << strerror(errno); + ASSERT_LE(0, fd1) << strerror(errno); }, [] { int fd0; fd0 = open(FULLPATH, O_RDONLY); if (fd0 < 0) { perror("open"); return(1); } return 0; } ); ASSERT_EQ(0, WEXITSTATUS(status)); close(fd1); } /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */ TEST_F(Open, DISABLED_o_append) { test_ok(O_WRONLY | O_APPEND, O_WRONLY | O_APPEND); } /* The kernel is supposed to filter out this flag */ TEST_F(Open, o_creat) { test_ok(O_WRONLY | O_CREAT, O_WRONLY); } /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */ TEST_F(Open, DISABLED_o_direct) { test_ok(O_WRONLY | O_DIRECT, O_WRONLY | O_DIRECT); } /* The kernel is supposed to filter out this flag */ TEST_F(Open, o_excl) { test_ok(O_WRONLY | O_EXCL, O_WRONLY); } TEST_F(Open, o_exec) { test_ok(O_EXEC, O_EXEC); } /* The kernel is supposed to filter out this flag */ TEST_F(Open, o_noctty) { test_ok(O_WRONLY | O_NOCTTY, O_WRONLY); } TEST_F(Open, o_rdonly) { test_ok(O_RDONLY, O_RDONLY); } /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */ TEST_F(Open, DISABLED_o_trunc) { test_ok(O_WRONLY | O_TRUNC, O_WRONLY | O_TRUNC); } TEST_F(Open, o_wronly) { test_ok(O_WRONLY, O_WRONLY); } TEST_F(Open, o_rdwr) { test_ok(O_RDWR, O_RDWR); } Index: stable/12/tests/sys/fs/fusefs/opendir.cc =================================================================== --- stable/12/tests/sys/fs/fusefs/opendir.cc (revision 368595) +++ stable/12/tests/sys/fs/fusefs/opendir.cc (revision 368596) @@ -1,174 +1,174 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ extern "C" { #include #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; class Opendir: public FuseTest { public: void expect_lookup(const char *relpath, uint64_t ino) { FuseTest::expect_lookup(relpath, ino, S_IFDIR | 0755, 0, 1); } 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); - EXPECT_LE(0, fd) << strerror(errno); + 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); } Index: stable/12/tests/sys/fs/fusefs/release.cc =================================================================== --- stable/12/tests/sys/fs/fusefs/release.cc (revision 368595) +++ stable/12/tests/sys/fs/fusefs/release.cc (revision 368596) @@ -1,227 +1,227 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ extern "C" { #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; class Release: public FuseTest { public: void expect_lookup(const char *relpath, uint64_t ino, int times) { FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, times); } void expect_release(uint64_t ino, uint64_t lock_owner, uint32_t flags, int error) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_RELEASE && in.header.nodeid == ino && in.body.release.lock_owner == lock_owner && in.body.release.fh == FH && in.body.release.flags == flags); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(error))) .RetiresOnSaturation(); } }; class ReleaseWithLocks: public Release { virtual void SetUp() { m_init_flags = FUSE_POSIX_LOCKS; Release::SetUp(); } }; /* If a file descriptor is duplicated, only the last close causes RELEASE */ TEST_F(Release, dup) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd, fd2; expect_lookup(RELPATH, ino, 1); expect_open(ino, 0, 1); expect_flush(ino, 1, ReturnErrno(0)); expect_release(ino, getpid(), O_RDONLY, 0); fd = open(FULLPATH, O_RDONLY); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); expect_release(ino, getpid(), O_WRONLY, 0); fd2 = open(FULLPATH, O_WRONLY); - EXPECT_LE(0, fd2) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, close(fd)) << strerror(errno); } /* When closing a file with a POSIX file lock, release should release the lock*/ TEST_F(ReleaseWithLocks, unlock_on_close) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; struct flock fl; pid_t pid = getpid(); expect_lookup(RELPATH, ino, 1); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_SETLK && in.header.nodeid == ino && in.body.setlk.fh == FH); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(0))); expect_flush(ino, 1, ReturnErrno(0)); expect_release(ino, static_cast(pid), O_RDWR, 0); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); fl.l_start = 0; fl.l_len = 0; fl.l_pid = pid; fl.l_type = F_RDLCK; fl.l_whence = SEEK_SET; fl.l_sysid = 0; ASSERT_NE(-1, fcntl(fd, F_SETLKW, &fl)) << strerror(errno); ASSERT_EQ(0, close(fd)) << strerror(errno); } Index: stable/12/tests/sys/fs/fusefs/releasedir.cc =================================================================== --- stable/12/tests/sys/fs/fusefs/releasedir.cc (revision 368595) +++ stable/12/tests/sys/fs/fusefs/releasedir.cc (revision 368596) @@ -1,118 +1,118 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ extern "C" { #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; class ReleaseDir: public FuseTest { public: void expect_lookup(const char *relpath, uint64_t ino) { FuseTest::expect_lookup(relpath, ino, S_IFDIR | 0755, 0, 1); } }; /* If a file descriptor is duplicated, only the last close causes RELEASE */ TEST_F(ReleaseDir, dup) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; DIR *dir, *dir2; expect_lookup(RELPATH, ino); expect_opendir(ino); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READDIR && in.header.nodeid == ino && in.body.readdir.offset == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { out.header.error = 0; out.header.len = sizeof(out.header); }))); expect_releasedir(ino, ReturnErrno(0)); dir = opendir(FULLPATH); ASSERT_NE(nullptr, dir) << strerror(errno); dir2 = fdopendir(dup(dirfd(dir))); ASSERT_NE(nullptr, dir2) << strerror(errno); ASSERT_EQ(0, closedir(dir)) << strerror(errno); ASSERT_EQ(0, closedir(dir2)) << strerror(errno); } TEST_F(ReleaseDir, ok) { const char FULLPATH[] = "mountpoint/some_dir"; const char RELPATH[] = "some_dir"; uint64_t ino = 42; DIR *dir; expect_lookup(RELPATH, ino); expect_opendir(ino); expect_releasedir(ino, ReturnErrno(0)); dir = opendir(FULLPATH); ASSERT_NE(nullptr, dir) << strerror(errno); ASSERT_EQ(0, closedir(dir)) << strerror(errno); } /* Directories opened O_EXEC should be properly released, too */ TEST_F(ReleaseDir, o_exec) { const char FULLPATH[] = "mountpoint/some_dir"; const char RELPATH[] = "some_dir"; uint64_t ino = 42; int fd; expect_lookup(RELPATH, ino); expect_opendir(ino); expect_releasedir(ino, ReturnErrno(0)); fd = open(FULLPATH, O_EXEC | O_DIRECTORY); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, close(fd)) << strerror(errno); } Index: stable/12/tests/sys/fs/fusefs/write.cc =================================================================== --- stable/12/tests/sys/fs/fusefs/write.cc (revision 368595) +++ stable/12/tests/sys/fs/fusefs/write.cc (revision 368596) @@ -1,1389 +1,1389 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ extern "C" { #include #include #include #include #include #include #include #include #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; class Write: public FuseTest { public: static sig_atomic_t s_sigxfsz; void SetUp() { s_sigxfsz = 0; FuseTest::SetUp(); } void TearDown() { struct sigaction sa; bzero(&sa, sizeof(sa)); sa.sa_handler = SIG_DFL; sigaction(SIGXFSZ, &sa, NULL); FuseTest::TearDown(); } void expect_lookup(const char *relpath, uint64_t ino, uint64_t size) { FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, size, 1); } void expect_release(uint64_t ino, ProcessMockerT r) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_RELEASE && in.header.nodeid == ino); }, Eq(true)), _) ).WillRepeatedly(Invoke(r)); } void expect_write(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, const void *contents) { FuseTest::expect_write(ino, offset, isize, osize, 0, 0, contents); } /* Expect a write that may or may not come, depending on the cache mode */ void maybe_expect_write(uint64_t ino, uint64_t offset, uint64_t size, const void *contents) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { const char *buf = (const char*)in.body.bytes + sizeof(struct fuse_write_in); return (in.header.opcode == FUSE_WRITE && in.header.nodeid == ino && in.body.write.offset == offset && in.body.write.size == size && 0 == bcmp(buf, contents, size)); }, Eq(true)), _) ).Times(AtMost(1)) .WillRepeatedly(Invoke( ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = size; }) )); } }; sig_atomic_t Write::s_sigxfsz = 0; class Write_7_8: public FuseTest { public: virtual void SetUp() { m_kernel_minor_version = 8; FuseTest::SetUp(); } void expect_lookup(const char *relpath, uint64_t ino, uint64_t size) { FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, size, 1); } }; class AioWrite: public Write { virtual void SetUp() { 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; WriteBack::SetUp(); } }; class TimeGran: public WriteBackAsync, public WithParamInterface { 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 = 1 << 25; // 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 * m_maxbcachebuf) GTEST_SKIP() << "MAXPHYS must be at least twice maxbcachebuf" << " for this test"; } }; void sigxfsz_handler(int __unused sig) { Write::s_sigxfsz = 1; } /* AIO writes need to set the header's pid field correctly */ /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */ TEST_F(AioWrite, DISABLED_aio_write) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; uint64_t offset = 4096; int fd; ssize_t bufsize = strlen(CONTENTS); struct aiocb iocb, *piocb; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_write(ino, offset, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_WRONLY); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + 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 = (char*)calloc(1, oldsize); ASSERT_NE(nullptr, oldcontents) << strerror(errno); oldbuf = (char*)malloc(oldsize); ASSERT_NE(nullptr, oldbuf) << strerror(errno); expect_lookup(RELPATH, ino, oldsize); expect_open(ino, 0, 1); expect_read(ino, 0, oldsize, oldsize, oldcontents); maybe_expect_write(ino, oldsize, BUFSIZE, CONTENTS); /* Must open O_RDWR or fuse(4) implicitly sets direct_io */ fd = open(FULLPATH, O_RDWR | O_APPEND); - EXPECT_LE(0, fd) << strerror(errno); + 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); free(oldbuf); free(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); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); leak(fd); } /* * When the direct_io option is used, filesystems are allowed to write less * data than requested. We should return the short write to userland. */ TEST_F(Write, direct_io_short_write) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefghijklmnop"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); ssize_t halfbufsize = bufsize / 2; expect_lookup(RELPATH, ino, 0); expect_open(ino, FOPEN_DIRECT_IO, 1); expect_write(ino, 0, bufsize, halfbufsize, CONTENTS); fd = open(FULLPATH, O_WRONLY); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(halfbufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); leak(fd); } /* * An insidious edge case: the filesystem returns a short write, and the * difference between what we requested and what it actually wrote crosses an * iov element boundary */ TEST_F(Write, direct_io_short_write_iov) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS0 = "abcdefgh"; const char *CONTENTS1 = "ijklmnop"; const char *EXPECTED0 = "abcdefghijklmnop"; uint64_t ino = 42; int fd; ssize_t size0 = strlen(CONTENTS0) - 1; ssize_t size1 = strlen(CONTENTS1) + 1; ssize_t totalsize = size0 + size1; struct iovec iov[2]; expect_lookup(RELPATH, ino, 0); expect_open(ino, FOPEN_DIRECT_IO, 1); expect_write(ino, 0, totalsize, size0, EXPECTED0); fd = open(FULLPATH, O_WRONLY); - EXPECT_LE(0, fd) << strerror(errno); + 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_F(Write, rlimit_fsize) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; struct rlimit rl; ssize_t bufsize = strlen(CONTENTS); off_t offset = 1'000'000'000; uint64_t ino = 42; int fd; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); rl.rlim_cur = offset; rl.rlim_max = 10 * offset; ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); fd = open(FULLPATH, O_WRONLY); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(-1, pwrite(fd, CONTENTS, bufsize, offset)); EXPECT_EQ(EFBIG, errno); EXPECT_EQ(1, s_sigxfsz); leak(fd); } /* * A short read indicates EOF. Test that nothing bad happens if we get EOF * during the R of a RMW operation. */ TEST_F(Write, eof_during_rmw) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; const char *INITIAL = "XXXXXXXXXX"; uint64_t ino = 42; uint64_t offset = 1; ssize_t bufsize = strlen(CONTENTS); off_t orig_fsize = 10; off_t truncated_fsize = 5; off_t final_fsize = bufsize; int fd; FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, orig_fsize, 1); expect_open(ino, 0, 1); expect_read(ino, 0, orig_fsize, truncated_fsize, INITIAL, O_RDWR); expect_getattr(ino, truncated_fsize); expect_read(ino, 0, final_fsize, final_fsize, INITIAL, O_RDWR); maybe_expect_write(ino, offset, bufsize, CONTENTS); fd = open(FULLPATH, O_RDWR); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, pwrite(fd, CONTENTS, bufsize, offset)) << strerror(errno); leak(fd); } /* * If the kernel cannot be sure which uid, gid, or pid was responsible for a * write, then it must set the FUSE_WRITE_CACHE bit */ /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236378 */ TEST_F(Write, mmap) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); void *p; uint64_t offset = 10; size_t len; void *zeros, *expected; len = getpagesize(); zeros = calloc(1, len); ASSERT_NE(nullptr, zeros); expected = calloc(1, len); ASSERT_NE(nullptr, expected); memmove((uint8_t*)expected + offset, CONTENTS, bufsize); expect_lookup(RELPATH, ino, len); expect_open(ino, 0, 1); expect_read(ino, 0, len, len, zeros); /* * Writes from the pager may or may not be associated with the correct * pid, so they must set FUSE_WRITE_CACHE. */ FuseTest::expect_write(ino, 0, len, len, FUSE_WRITE_CACHE, 0, expected); expect_flush(ino, 1, ReturnErrno(0)); expect_release(ino, ReturnErrno(0)); fd = open(FULLPATH, O_RDWR); - EXPECT_LE(0, fd) << strerror(errno); + 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 free(expected); free(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); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + 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(Write, write_large) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; int *contents; uint64_t ino = 42; int fd; ssize_t halfbufsize, bufsize; halfbufsize = m_mock->m_maxwrite; bufsize = halfbufsize * 2; contents = (int*)malloc(bufsize); ASSERT_NE(nullptr, contents); for (int i = 0; i < (int)bufsize / (int)sizeof(i); i++) { contents[i] = i; } expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); maybe_expect_write(ino, 0, halfbufsize, contents); maybe_expect_write(ino, halfbufsize, halfbufsize, &contents[halfbufsize / sizeof(int)]); fd = open(FULLPATH, O_WRONLY); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, contents, bufsize)) << strerror(errno); leak(fd); free(contents); } TEST_F(Write, write_nothing) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = ""; uint64_t ino = 42; int fd; ssize_t bufsize = 0; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); fd = open(FULLPATH, O_WRONLY); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); leak(fd); } TEST_F(Write_7_8, write) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_write_7_8(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_WRONLY); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); leak(fd); } /* In writeback mode, dirty data should be written on close */ TEST_F(WriteBackAsync, close) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_write(ino, 0, bufsize, bufsize, CONTENTS); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_SETATTR); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid }))); expect_flush(ino, 1, ReturnErrno(0)); expect_release(ino, ReturnErrno(0)); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); close(fd); } /* In writeback mode, adjacent writes will be clustered together */ TEST_F(WriteCluster, clustering) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int i, fd; void *wbuf, *wbuf2x; ssize_t bufsize = m_maxbcachebuf; off_t filesize = 5 * bufsize; wbuf = malloc(bufsize); ASSERT_NE(nullptr, wbuf) << strerror(errno); memset(wbuf, 'X', bufsize); wbuf2x = malloc(2 * bufsize); ASSERT_NE(nullptr, wbuf2x) << strerror(errno); memset(wbuf2x, 'X', 2 * bufsize); expect_lookup(RELPATH, ino, filesize); expect_open(ino, 0, 1); /* * Writes of bufsize-bytes each should be clustered into greater sizes. * The amount of clustering is adaptive, so the first write actually * issued will be 2x bufsize and subsequent writes may be larger */ expect_write(ino, 0, 2 * bufsize, 2 * bufsize, wbuf2x); expect_write(ino, 2 * bufsize, 2 * bufsize, 2 * bufsize, wbuf2x); expect_flush(ino, 1, ReturnErrno(0)); expect_release(ino, ReturnErrno(0)); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); for (i = 0; i < 4; i++) { ASSERT_EQ(bufsize, write(fd, wbuf, bufsize)) << strerror(errno); } close(fd); free(wbuf2x); free(wbuf); } /* * When clustering writes, an I/O error to any of the cluster's children should * not panic the system on unmount */ /* * Disabled because it panics. * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=238565 */ TEST_F(WriteCluster, DISABLED_cluster_write_err) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int i, fd; void *wbuf; ssize_t bufsize = m_maxbcachebuf; off_t filesize = 4 * bufsize; wbuf = malloc(bufsize); ASSERT_NE(nullptr, wbuf) << strerror(errno); memset(wbuf, 'X', bufsize); expect_lookup(RELPATH, ino, filesize); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_WRITE); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnErrno(EIO))); expect_flush(ino, 1, ReturnErrno(0)); expect_release(ino, ReturnErrno(0)); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); for (i = 0; i < 3; i++) { ASSERT_EQ(bufsize, write(fd, wbuf, bufsize)) << strerror(errno); } close(fd); free(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); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + 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); void *p, *zeros; len = getpagesize(); zeros = calloc(1, len); ASSERT_NE(nullptr, zeros); 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); - EXPECT_LE(0, fd) << strerror(errno); + 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 free(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); - EXPECT_LE(0, fd) << strerror(errno); + 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]; void *zeros; zeros = calloc(1, m_maxbcachebuf); ASSERT_NE(nullptr, zeros); expect_lookup(RELPATH, ino, fsize); expect_open(ino, 0, 1); expect_read(ino, 0, m_maxbcachebuf, m_maxbcachebuf, zeros); FuseTest::expect_write(ino, m_maxbcachebuf, bufsize, bufsize, 0, 0, CONTENTS1); fd = open(FULLPATH, O_RDWR); - EXPECT_LE(0, fd) << strerror(errno); + 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); free(zeros); } /* * If a direct io write partially overlaps one or two blocks of dirty cached * data, No dirty data should be lost. Admittedly this is a weird test, * because it would be unusual to use O_DIRECT and the writeback cache. */ TEST_F(WriteBackAsync, direct_io_partially_overlaps_cached_block) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; off_t bs = m_maxbcachebuf; ssize_t fsize = 3 * bs; void *readbuf, *zeros, *ones, *zeroones, *onezeros; readbuf = malloc(bs); ASSERT_NE(nullptr, readbuf) << strerror(errno); zeros = calloc(1, 3 * bs); ASSERT_NE(nullptr, zeros); ones = calloc(1, 2 * bs); ASSERT_NE(nullptr, ones); memset(ones, 1, 2 * bs); zeroones = calloc(1, bs); ASSERT_NE(nullptr, zeroones); memset((uint8_t*)zeroones + bs / 2, 1, bs / 2); onezeros = calloc(1, bs); ASSERT_NE(nullptr, onezeros); memset(onezeros, 1, bs / 2); expect_lookup(RELPATH, ino, fsize); expect_open(ino, 0, 1); fd = open(FULLPATH, O_RDWR); - EXPECT_LE(0, fd) << strerror(errno); + 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); free(zeroones); free(onezeros); free(ones); free(zeros); free(readbuf); } /* * In WriteBack mode, writes may be cached beyond what the server thinks is the * EOF. In this case, a short read at EOF should _not_ cause fusefs to update * the file's size. */ TEST_F(WriteBackAsync, eof) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS0 = "abcdefgh"; const char *CONTENTS1 = "ijklmnop"; uint64_t ino = 42; int fd; off_t offset = m_maxbcachebuf; ssize_t wbufsize = strlen(CONTENTS1); off_t old_filesize = (off_t)strlen(CONTENTS0); ssize_t rbufsize = 2 * old_filesize; char readbuf[rbufsize]; size_t holesize = rbufsize - old_filesize; char hole[holesize]; struct stat sb; ssize_t r; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_read(ino, 0, m_maxbcachebuf, old_filesize, CONTENTS0); fd = open(FULLPATH, O_RDWR); - EXPECT_LE(0, fd) << strerror(errno); + 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); } /* * When a file has dirty writes that haven't been flushed, the server's notion * of its mtime and ctime will be wrong. The kernel should ignore those if it * gets them from a FUSE_GETATTR before flushing. */ TEST_F(WriteBackAsync, timestamps) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; ssize_t bufsize = strlen(CONTENTS); uint64_t ino = 42; uint64_t attr_valid = 0; uint64_t attr_valid_nsec = 0; uint64_t server_time = 12345; mode_t mode = S_IFREG | 0644; int fd; struct stat sb; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillRepeatedly(Invoke( ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = mode; out.body.entry.nodeid = ino; out.body.entry.attr.nlink = 1; out.body.entry.attr_valid = attr_valid; out.body.entry.attr_valid_nsec = attr_valid_nsec; }))); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_GETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).WillRepeatedly(Invoke( ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = mode; out.body.attr.attr_valid = attr_valid; out.body.attr.attr_valid_nsec = attr_valid_nsec; out.body.attr.attr.atime = server_time; out.body.attr.attr.mtime = server_time; out.body.attr.attr.ctime = server_time; }))); fd = open(FULLPATH, O_RDWR); - EXPECT_LE(0, fd) << strerror(errno); + ASSERT_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); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + 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_CASE_P(RA, TimeGran, Range(0u, 10u)); /* * Without direct_io, writes should be committed to cache */ TEST_F(Write, writethrough) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); 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); - EXPECT_LE(0, fd) << strerror(errno); + 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); - EXPECT_LE(0, fd) << strerror(errno); + 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); } Index: stable/12 =================================================================== --- stable/12 (revision 368595) +++ stable/12 (revision 368596) Property changes on: stable/12 ___________________________________________________________________ Modified: svn:mergeinfo ## -0,0 +0,1 ## Merged /head:r366365