diff --git a/tests/sys/fs/fusefs/allow_other.cc b/tests/sys/fs/fusefs/allow_other.cc index 0ddcc920f4af..30488dbbb0eb 100644 --- a/tests/sys/fs/fusefs/allow_other.cc +++ b/tests/sys/fs/fusefs/allow_other.cc @@ -1,307 +1,311 @@ /*- * 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); } + + leak(fd); return 0; } ); ASSERT_EQ(0, WEXITSTATUS(status)); } /* Check that fusefs uses the correct credentials for FUSE operations */ TEST_F(AllowOther, creds) { int status; uid_t uid; gid_t gid; get_unprivileged_id(&uid, &gid); fork(true, &status, [=] { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LOOKUP && in.header.uid == uid && in.header.gid == gid); }, Eq(true)), _) ).Times(1) .WillOnce(Invoke(ReturnErrno(ENOENT))); }, []() { eaccess(FULLPATH, F_OK); return 0; } ); ASSERT_EQ(0, WEXITSTATUS(status)); } /* * A variation of the Open.multiple_creds test showing how the bug can lead to a * privilege elevation. The first process is privileged and opens a file only * visible to root. The second process is unprivileged and shouldn't be able * to open the file, but does thanks to the bug */ TEST_F(AllowOther, privilege_escalation) { int fd1, status; const static uint64_t ino = 42; const static uint64_t fh = 100; /* Fork a child to open the file with different credentials */ fork(true, &status, [&] { expect_lookup(RELPATH, ino, S_IFREG | 0600, 0, 2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_OPEN && in.header.pid == (uint32_t)getpid() && in.header.uid == (uint32_t)geteuid() && in.header.nodeid == ino); }, Eq(true)), _) ).WillOnce(Invoke( ReturnImmediate([](auto in __unused, auto& out) { out.body.open.fh = fh; out.header.len = sizeof(out.header); SET_OUT_HEADER_LEN(out, open); }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_OPEN && in.header.pid != (uint32_t)getpid() && in.header.uid != (uint32_t)geteuid() && in.header.nodeid == ino); }, Eq(true)), _) ).Times(AnyNumber()) .WillRepeatedly(Invoke(ReturnErrno(EPERM))); fd1 = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd1) << strerror(errno); }, [] { int fd0; fd0 = open(FULLPATH, O_RDONLY); if (fd0 >= 0) { fprintf(stderr, "Privilege escalation!\n"); return 1; } if (errno != EPERM) { fprintf(stderr, "Unexpected error %s\n", strerror(errno)); return 1; } leak(fd0); return 0; } ); ASSERT_EQ(0, WEXITSTATUS(status)); leak(fd1); } TEST_F(NoAllowOther, disallowed) { int status; fork(true, &status, [] { }, []() { int fd; fd = open(FULLPATH, O_RDONLY); if (fd >= 0) { fprintf(stderr, "open should've failed\n"); + leak(fd); return(1); } else if (errno != EPERM) { fprintf(stderr, "Unexpected error: %s\n", strerror(errno)); return(1); } return 0; } ); ASSERT_EQ(0, WEXITSTATUS(status)); } /* * When -o allow_other is not used, users other than the owner aren't allowed * to open anything inside of the mount point, not just the mountpoint itself * This is a regression test for bug 237052 */ TEST_F(NoAllowOther, disallowed_beneath_root) { const static char RELPATH2[] = "other_dir"; const static uint64_t ino = 42; const static uint64_t ino2 = 43; int dfd, status; expect_lookup(RELPATH, ino, S_IFDIR | 0755, 0, 1); EXPECT_LOOKUP(ino, RELPATH2) .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | 0644; out.body.entry.nodeid = ino2; out.body.entry.attr.nlink = 1; out.body.entry.attr_valid = UINT64_MAX; }))); expect_opendir(ino); dfd = open(FULLPATH, O_DIRECTORY); ASSERT_LE(0, dfd) << strerror(errno); fork(true, &status, [] { }, [&]() { int fd; fd = openat(dfd, RELPATH2, O_RDONLY); if (fd >= 0) { fprintf(stderr, "openat should've failed\n"); + leak(fd); return(1); } else if (errno != EPERM) { fprintf(stderr, "Unexpected error: %s\n", strerror(errno)); return(1); } return 0; } ); ASSERT_EQ(0, WEXITSTATUS(status)); leak(dfd); } /* * Provide coverage for the extattr methods, which have a slightly different * code path */ TEST_F(NoAllowOther, setextattr) { int ino = 42, status; fork(true, &status, [&] { EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke( ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = UINT64_MAX; out.body.entry.attr.mode = S_IFREG | 0644; out.body.entry.nodeid = ino; }))); /* * lookup the file to get it into the cache. * Otherwise, the unprivileged lookup will fail with * EACCES */ ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); }, [&]() { const char value[] = "whatever"; ssize_t value_len = strlen(value) + 1; int ns = EXTATTR_NAMESPACE_USER; ssize_t r; r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value, value_len); if (r >= 0) { fprintf(stderr, "should've failed\n"); return(1); } else if (errno != EPERM) { fprintf(stderr, "Unexpected error: %s\n", strerror(errno)); return(1); } return 0; } ); ASSERT_EQ(0, WEXITSTATUS(status)); } diff --git a/tests/sys/fs/fusefs/bmap.cc b/tests/sys/fs/fusefs/bmap.cc index c635f4d7e46f..91c9c4f85b64 100644 --- a/tests/sys/fs/fusefs/bmap.cc +++ b/tests/sys/fs/fusefs/bmap.cc @@ -1,253 +1,257 @@ /*- * 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 "mockfs.hh" #include "utils.hh" using namespace testing; const static char FULLPATH[] = "mountpoint/foo"; const static char RELPATH[] = "foo"; class Bmap: public FuseTest { public: virtual void SetUp() { m_maxreadahead = UINT32_MAX; FuseTest::SetUp(); } void expect_bmap(uint64_t ino, uint64_t lbn, uint32_t blocksize, uint64_t pbn) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_BMAP && in.header.nodeid == ino && in.body.bmap.block == lbn && in.body.bmap.blocksize == blocksize); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, bmap); out.body.bmap.block = pbn; }))); } void expect_lookup(const char *relpath, uint64_t ino, off_t size) { FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, size, 1, UINT64_MAX); } }; class BmapEof: public Bmap, public WithParamInterface {}; /* * Test FUSE_BMAP * XXX The FUSE protocol does not include the runp and runb variables, so those * must be guessed in-kernel. */ TEST_F(Bmap, bmap) { struct fiobmap2_arg arg; /* * Pick fsize and lbn large enough that max length runs won't reach * either beginning or end of file */ const off_t filesize = 1 << 30; int64_t lbn = 100; int64_t pbn = 12345; const ino_t ino = 42; int fd; expect_lookup(RELPATH, 42, filesize); expect_open(ino, 0, 1); expect_bmap(ino, lbn, m_maxbcachebuf, pbn); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); arg.bn = lbn; arg.runp = -1; arg.runb = -1; ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); EXPECT_EQ(arg.bn, pbn); EXPECT_EQ(arg.runp, m_maxphys / m_maxbcachebuf - 1); EXPECT_EQ(arg.runb, m_maxphys / m_maxbcachebuf - 1); + + leak(fd); } /* * If the daemon does not implement VOP_BMAP, fusefs should return sensible * defaults. */ TEST_F(Bmap, default_) { struct fiobmap2_arg arg; const off_t filesize = 1 << 30; const ino_t ino = 42; int64_t lbn; int fd; expect_lookup(RELPATH, 42, filesize); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_BMAP); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(ENOSYS))); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); /* First block */ lbn = 0; arg.bn = lbn; arg.runp = -1; arg.runb = -1; ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); EXPECT_EQ(arg.bn, 0); EXPECT_EQ(arg.runp, m_maxphys / m_maxbcachebuf - 1); EXPECT_EQ(arg.runb, 0); /* In the middle */ lbn = filesize / m_maxbcachebuf / 2; arg.bn = lbn; arg.runp = -1; arg.runb = -1; ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); EXPECT_EQ(arg.bn, lbn * m_maxbcachebuf / DEV_BSIZE); EXPECT_EQ(arg.runp, m_maxphys / m_maxbcachebuf - 1); EXPECT_EQ(arg.runb, m_maxphys / m_maxbcachebuf - 1); /* Last block */ lbn = filesize / m_maxbcachebuf - 1; arg.bn = lbn; arg.runp = -1; arg.runb = -1; ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); EXPECT_EQ(arg.bn, lbn * m_maxbcachebuf / DEV_BSIZE); EXPECT_EQ(arg.runp, 0); EXPECT_EQ(arg.runb, m_maxphys / m_maxbcachebuf - 1); leak(fd); } /* * VOP_BMAP should not query the server for the file's size, even if its cached * attributes have expired. * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=256937 */ TEST_P(BmapEof, eof) { /* * Outline: * 1) lookup the file, setting attr_valid=0 * 2) Read more than one block, causing the kernel to issue VOP_BMAP to * plan readahead. * 3) Nothing should panic * 4) Repeat the tests, truncating the file after different numbers of * GETATTR operations. */ Sequence seq; const off_t filesize = 2 * m_maxbcachebuf; const ino_t ino = 42; mode_t mode = S_IFREG | 0644; void *contents, *buf; int fd; int ngetattrs; ngetattrs = GetParam(); contents = calloc(1, filesize); FuseTest::expect_lookup(RELPATH, ino, mode, filesize, 1, 0); expect_open(ino, 0, 1); // Depending on ngetattrs, FUSE_READ could be called with either // filesize or filesize / 2 . EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ && in.header.nodeid == ino && in.body.read.offset == 0 && ( in.body.read.size == filesize || in.body.read.size == filesize / 2)); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { size_t osize = in.body.read.size; out.header.len = sizeof(struct fuse_out_header) + osize; bzero(out.body.bytes, osize); }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_GETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).Times(Between(ngetattrs - 1, ngetattrs)) .InSequence(seq) .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr_valid = 0; out.body.attr.attr.ino = ino; out.body.attr.attr.mode = S_IFREG | 0644; out.body.attr.attr.size = filesize; }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_GETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).InSequence(seq) .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr_valid = 0; out.body.attr.attr.ino = ino; out.body.attr.attr.mode = S_IFREG | 0644; out.body.attr.attr.size = filesize / 2; }))); buf = calloc(1, filesize); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); read(fd, buf, filesize); + + leak(fd); } INSTANTIATE_TEST_CASE_P(BE, BmapEof, Values(1, 2, 3) ); diff --git a/tests/sys/fs/fusefs/copy_file_range.cc b/tests/sys/fs/fusefs/copy_file_range.cc index 974dc474f77b..7e1189648de3 100644 --- a/tests/sys/fs/fusefs/copy_file_range.cc +++ b/tests/sys/fs/fusefs/copy_file_range.cc @@ -1,668 +1,673 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2020 Alan Somers * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ extern "C" { #include #include #include #include #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; class CopyFileRange: 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_maybe_lseek(uint64_t ino) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LSEEK && in.header.nodeid == ino); }, Eq(true)), _) ).Times(AtMost(1)) .WillRepeatedly(Invoke(ReturnErrno(ENOSYS))); } void expect_open(uint64_t ino, uint32_t flags, int times, uint64_t fh) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_OPEN && in.header.nodeid == ino); }, Eq(true)), _) ).Times(times) .WillRepeatedly(Invoke( ReturnImmediate([=](auto in __unused, auto& out) { out.header.len = sizeof(out.header); SET_OUT_HEADER_LEN(out, open); out.body.open.fh = fh; out.body.open.open_flags = flags; }))); } void expect_write(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, const void *contents) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { const char *buf = (const char*)in.body.bytes + sizeof(struct fuse_write_in); return (in.header.opcode == FUSE_WRITE && in.header.nodeid == ino && in.body.write.offset == offset && in.body.write.size == isize && 0 == bcmp(buf, contents, isize)); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = osize; }))); } }; sig_atomic_t CopyFileRange::s_sigxfsz = 0; void sigxfsz_handler(int __unused sig) { CopyFileRange::s_sigxfsz = 1; } class CopyFileRange_7_27: public CopyFileRange { public: virtual void SetUp() { m_kernel_minor_version = 27; CopyFileRange::SetUp(); } }; class CopyFileRangeNoAtime: public CopyFileRange { public: virtual void SetUp() { m_noatime = true; CopyFileRange::SetUp(); } }; TEST_F(CopyFileRange, eio) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize1 = 1 << 20; /* 1 MiB */ off_t fsize2 = 1 << 19; /* 512 KiB */ off_t start1 = 1 << 18; off_t start2 = 3 << 17; ssize_t len = 65536; int fd1, fd2; expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino1 && in.body.copy_file_range.fh_in == fh1 && (off_t)in.body.copy_file_range.off_in == start1 && in.body.copy_file_range.nodeid_out == ino2 && in.body.copy_file_range.fh_out == fh2 && (off_t)in.body.copy_file_range.off_out == start2 && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(EIO))); fd1 = open(FULLPATH1, O_RDONLY); fd2 = open(FULLPATH2, O_WRONLY); ASSERT_EQ(-1, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); EXPECT_EQ(EIO, errno); } /* * copy_file_range should evict cached data for the modified region of the * destination file. */ TEST_F(CopyFileRange, evicts_cache) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; void *buf0, *buf1, *buf; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize1 = 1 << 20; /* 1 MiB */ off_t fsize2 = 1 << 19; /* 512 KiB */ off_t start1 = 1 << 18; off_t start2 = 3 << 17; ssize_t len = m_maxbcachebuf; int fd1, fd2; buf0 = malloc(m_maxbcachebuf); memset(buf0, 42, m_maxbcachebuf); expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); expect_read(ino2, start2, m_maxbcachebuf, m_maxbcachebuf, buf0, -1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino1 && in.body.copy_file_range.fh_in == fh1 && (off_t)in.body.copy_file_range.off_in == start1 && in.body.copy_file_range.nodeid_out == ino2 && in.body.copy_file_range.fh_out == fh2 && (off_t)in.body.copy_file_range.off_out == start2 && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = len; }))); fd1 = open(FULLPATH1, O_RDONLY); fd2 = open(FULLPATH2, O_RDWR); // Prime cache buf = malloc(m_maxbcachebuf); ASSERT_EQ(m_maxbcachebuf, pread(fd2, buf, m_maxbcachebuf, start2)) << strerror(errno); EXPECT_EQ(0, memcmp(buf0, buf, m_maxbcachebuf)); // Tell the FUSE server overwrite the region we just read ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); // Read again. This should bypass the cache and read direct from server buf1 = malloc(m_maxbcachebuf); memset(buf1, 69, m_maxbcachebuf); start2 -= len; expect_read(ino2, start2, m_maxbcachebuf, m_maxbcachebuf, buf1, -1, fh2); ASSERT_EQ(m_maxbcachebuf, pread(fd2, buf, m_maxbcachebuf, start2)) << strerror(errno); EXPECT_EQ(0, memcmp(buf1, buf, m_maxbcachebuf)); free(buf1); free(buf0); free(buf); leak(fd1); leak(fd2); } /* * If the server doesn't support FUSE_COPY_FILE_RANGE, the kernel should * fallback to a read/write based implementation. */ TEST_F(CopyFileRange, fallback) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize2 = 0; off_t start1 = 0; off_t start2 = 0; const char *contents = "Hello, world!"; ssize_t len; int fd1, fd2; len = strlen(contents); /* * Ensure that we read to EOF, just so the buffer cache's read size is * predictable. */ expect_lookup(RELPATH1, ino1, S_IFREG | 0644, start1 + len, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino1 && in.body.copy_file_range.fh_in == fh1 && (off_t)in.body.copy_file_range.off_in == start1 && in.body.copy_file_range.nodeid_out == ino2 && in.body.copy_file_range.fh_out == fh2 && (off_t)in.body.copy_file_range.off_out == start2 && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(ENOSYS))); expect_maybe_lseek(ino1); expect_read(ino1, start1, len, len, contents, 0); expect_write(ino2, start2, len, len, contents); fd1 = open(FULLPATH1, O_RDONLY); ASSERT_GE(fd1, 0); fd2 = open(FULLPATH2, O_WRONLY); ASSERT_GE(fd2, 0); ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); } /* fusefs should respect RLIMIT_FSIZE */ TEST_F(CopyFileRange, rlimit_fsize) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; struct rlimit rl; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize1 = 1 << 20; /* 1 MiB */ off_t fsize2 = 1 << 19; /* 512 KiB */ off_t start1 = 1 << 18; off_t start2 = fsize2; ssize_t len = 65536; int fd1, fd2; expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE); }, Eq(true)), _) ).Times(0); rl.rlim_cur = fsize2; rl.rlim_max = 10 * fsize2; ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); fd1 = open(FULLPATH1, O_RDONLY); fd2 = open(FULLPATH2, O_WRONLY); ASSERT_EQ(-1, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); EXPECT_EQ(EFBIG, errno); EXPECT_EQ(1, s_sigxfsz); } TEST_F(CopyFileRange, ok) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize1 = 1 << 20; /* 1 MiB */ off_t fsize2 = 1 << 19; /* 512 KiB */ off_t start1 = 1 << 18; off_t start2 = 3 << 17; ssize_t len = 65536; int fd1, fd2; expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino1 && in.body.copy_file_range.fh_in == fh1 && (off_t)in.body.copy_file_range.off_in == start1 && in.body.copy_file_range.nodeid_out == ino2 && in.body.copy_file_range.fh_out == fh2 && (off_t)in.body.copy_file_range.off_out == start2 && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = len; }))); fd1 = open(FULLPATH1, O_RDONLY); fd2 = open(FULLPATH2, O_WRONLY); ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); } /* * copy_file_range can make copies within a single file, as long as the ranges * don't overlap. * */ TEST_F(CopyFileRange, same_file) { const char FULLPATH[] = "mountpoint/src.txt"; const char RELPATH[] = "src.txt"; const uint64_t ino = 4; const uint64_t fh = 0xdeadbeefa7ebabe; off_t fsize = 1 << 20; /* 1 MiB */ off_t off_in = 1 << 18; off_t off_out = 3 << 17; ssize_t len = 65536; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1, fh); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino && in.body.copy_file_range.fh_in == fh && (off_t)in.body.copy_file_range.off_in == off_in && in.body.copy_file_range.nodeid_out == ino && in.body.copy_file_range.fh_out == fh && (off_t)in.body.copy_file_range.off_out == off_out && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = len; }))); fd = open(FULLPATH, O_RDWR); ASSERT_EQ(len, copy_file_range(fd, &off_in, fd, &off_out, len, 0)); + + leak(fd); } /* * copy_file_range should update the destination's mtime and ctime, and * the source's atime. */ TEST_F(CopyFileRange, timestamps) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; struct stat sb1a, sb1b, sb2a, sb2b; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize1 = 1 << 20; /* 1 MiB */ off_t fsize2 = 1 << 19; /* 512 KiB */ off_t start1 = 1 << 18; off_t start2 = 3 << 17; ssize_t len = 65536; int fd1, fd2; expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino1 && in.body.copy_file_range.fh_in == fh1 && (off_t)in.body.copy_file_range.off_in == start1 && in.body.copy_file_range.nodeid_out == ino2 && in.body.copy_file_range.fh_out == fh2 && (off_t)in.body.copy_file_range.off_out == start2 && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = len; }))); fd1 = open(FULLPATH1, O_RDONLY); ASSERT_GE(fd1, 0); fd2 = open(FULLPATH2, O_WRONLY); ASSERT_GE(fd2, 0); ASSERT_EQ(0, fstat(fd1, &sb1a)) << strerror(errno); ASSERT_EQ(0, fstat(fd2, &sb2a)) << strerror(errno); nap(); ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); ASSERT_EQ(0, fstat(fd1, &sb1b)) << strerror(errno); ASSERT_EQ(0, fstat(fd2, &sb2b)) << strerror(errno); EXPECT_NE(sb1a.st_atime, sb1b.st_atime); EXPECT_EQ(sb1a.st_mtime, sb1b.st_mtime); EXPECT_EQ(sb1a.st_ctime, sb1b.st_ctime); EXPECT_EQ(sb2a.st_atime, sb2b.st_atime); EXPECT_NE(sb2a.st_mtime, sb2b.st_mtime); EXPECT_NE(sb2a.st_ctime, sb2b.st_ctime); leak(fd1); leak(fd2); } /* * copy_file_range can extend the size of a file * */ TEST_F(CopyFileRange, extend) { const char FULLPATH[] = "mountpoint/src.txt"; const char RELPATH[] = "src.txt"; struct stat sb; const uint64_t ino = 4; const uint64_t fh = 0xdeadbeefa7ebabe; off_t fsize = 65536; off_t off_in = 0; off_t off_out = 65536; ssize_t len = 65536; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1, fh); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino && in.body.copy_file_range.fh_in == fh && (off_t)in.body.copy_file_range.off_in == off_in && in.body.copy_file_range.nodeid_out == ino && in.body.copy_file_range.fh_out == fh && (off_t)in.body.copy_file_range.off_out == off_out && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = len; }))); fd = open(FULLPATH, O_RDWR); ASSERT_GE(fd, 0); ASSERT_EQ(len, copy_file_range(fd, &off_in, fd, &off_out, len, 0)); /* Check that cached attributes were updated appropriately */ ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); EXPECT_EQ(fsize + len, sb.st_size); leak(fd); } /* With older protocol versions, no FUSE_COPY_FILE_RANGE should be attempted */ TEST_F(CopyFileRange_7_27, fallback) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize2 = 0; off_t start1 = 0; off_t start2 = 0; const char *contents = "Hello, world!"; ssize_t len; int fd1, fd2; len = strlen(contents); /* * Ensure that we read to EOF, just so the buffer cache's read size is * predictable. */ expect_lookup(RELPATH1, ino1, S_IFREG | 0644, start1 + len, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE); }, Eq(true)), _) ).Times(0); expect_maybe_lseek(ino1); expect_read(ino1, start1, len, len, contents, 0); expect_write(ino2, start2, len, len, contents); fd1 = open(FULLPATH1, O_RDONLY); ASSERT_GE(fd1, 0); fd2 = open(FULLPATH2, O_WRONLY); ASSERT_GE(fd2, 0); ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); + + leak(fd1); + leak(fd2); } /* * With -o noatime, copy_file_range should update the destination's mtime and * ctime, but not the source's atime. */ TEST_F(CopyFileRangeNoAtime, timestamps) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; struct stat sb1a, sb1b, sb2a, sb2b; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize1 = 1 << 20; /* 1 MiB */ off_t fsize2 = 1 << 19; /* 512 KiB */ off_t start1 = 1 << 18; off_t start2 = 3 << 17; ssize_t len = 65536; int fd1, fd2; expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino1 && in.body.copy_file_range.fh_in == fh1 && (off_t)in.body.copy_file_range.off_in == start1 && in.body.copy_file_range.nodeid_out == ino2 && in.body.copy_file_range.fh_out == fh2 && (off_t)in.body.copy_file_range.off_out == start2 && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = len; }))); fd1 = open(FULLPATH1, O_RDONLY); ASSERT_GE(fd1, 0); fd2 = open(FULLPATH2, O_WRONLY); ASSERT_GE(fd2, 0); ASSERT_EQ(0, fstat(fd1, &sb1a)) << strerror(errno); ASSERT_EQ(0, fstat(fd2, &sb2a)) << strerror(errno); nap(); ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); ASSERT_EQ(0, fstat(fd1, &sb1b)) << strerror(errno); ASSERT_EQ(0, fstat(fd2, &sb2b)) << strerror(errno); EXPECT_EQ(sb1a.st_atime, sb1b.st_atime); EXPECT_EQ(sb1a.st_mtime, sb1b.st_mtime); EXPECT_EQ(sb1a.st_ctime, sb1b.st_ctime); EXPECT_EQ(sb2a.st_atime, sb2b.st_atime); EXPECT_NE(sb2a.st_mtime, sb2b.st_mtime); EXPECT_NE(sb2a.st_ctime, sb2b.st_ctime); leak(fd1); leak(fd2); } diff --git a/tests/sys/fs/fusefs/last_local_modify.cc b/tests/sys/fs/fusefs/last_local_modify.cc index 9826296c80c3..ce2e155af49a 100644 --- a/tests/sys/fs/fusefs/last_local_modify.cc +++ b/tests/sys/fs/fusefs/last_local_modify.cc @@ -1,514 +1,516 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2021 Alan Somers * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ extern "C" { #include #include #include #include #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; /* * "Last Local Modify" bugs * * This file tests a class of race conditions caused by one thread fetching a * file's size with FUSE_LOOKUP while another thread simultaneously modifies it * with FUSE_SETATTR, FUSE_WRITE, FUSE_COPY_FILE_RANGE or similar. It's * possible for the second thread to start later yet finish first. If that * happens, the first thread must not override the size set by the second * thread. * * FUSE_GETATTR is not vulnerable to the same race, because it is always called * with the vnode lock held. * * A few other operations like FUSE_LINK can also trigger the same race but * with the file's ctime instead of size. However, the consequences of an * incorrect ctime are much less disastrous than an incorrect size, so fusefs * does not attempt to prevent such races. */ enum Mutator { VOP_ALLOCATE, VOP_COPY_FILE_RANGE, VOP_SETATTR, VOP_WRITE, }; /* * Translate a poll method's string representation to the enum value. * Using strings with ::testing::Values gives better output with * --gtest_list_tests */ enum Mutator writer_from_str(const char* s) { if (0 == strcmp("VOP_ALLOCATE", s)) return VOP_ALLOCATE; else if (0 == strcmp("VOP_COPY_FILE_RANGE", s)) return VOP_COPY_FILE_RANGE; else if (0 == strcmp("VOP_SETATTR", s)) return VOP_SETATTR; else return VOP_WRITE; } uint32_t fuse_op_from_mutator(enum Mutator mutator) { switch(mutator) { case VOP_ALLOCATE: return(FUSE_FALLOCATE); case VOP_COPY_FILE_RANGE: return(FUSE_COPY_FILE_RANGE); case VOP_SETATTR: return(FUSE_SETATTR); case VOP_WRITE: return(FUSE_WRITE); } } class LastLocalModify: public FuseTest, public WithParamInterface { public: virtual void SetUp() { m_init_flags = FUSE_EXPORT_SUPPORT; FuseTest::SetUp(); } }; static void* allocate_th(void* arg) { int fd; ssize_t r; sem_t *sem = (sem_t*) arg; if (sem) sem_wait(sem); fd = open("mountpoint/some_file.txt", O_RDWR); if (fd < 0) return (void*)(intptr_t)errno; r = posix_fallocate(fd, 0, 15); + LastLocalModify::leak(fd); if (r >= 0) return 0; else return (void*)(intptr_t)errno; } static void* copy_file_range_th(void* arg) { ssize_t r; int fd; sem_t *sem = (sem_t*) arg; off_t off_in = 0; off_t off_out = 10; ssize_t len = 5; if (sem) sem_wait(sem); fd = open("mountpoint/some_file.txt", O_RDWR); if (fd < 0) return (void*)(intptr_t)errno; r = copy_file_range(fd, &off_in, fd, &off_out, len, 0); if (r >= 0) { LastLocalModify::leak(fd); return 0; } else return (void*)(intptr_t)errno; } static void* setattr_th(void* arg) { int fd; ssize_t r; sem_t *sem = (sem_t*) arg; if (sem) sem_wait(sem); fd = open("mountpoint/some_file.txt", O_RDWR); if (fd < 0) return (void*)(intptr_t)errno; r = ftruncate(fd, 15); + LastLocalModify::leak(fd); if (r >= 0) return 0; else return (void*)(intptr_t)errno; } static void* write_th(void* arg) { ssize_t r; int fd; sem_t *sem = (sem_t*) arg; const char BUF[] = "abcdefghijklmn"; if (sem) sem_wait(sem); fd = open("mountpoint/some_file.txt", O_RDWR); if (fd < 0) return (void*)(intptr_t)errno; r = write(fd, BUF, sizeof(BUF)); if (r >= 0) { LastLocalModify::leak(fd); return 0; } else return (void*)(intptr_t)errno; } /* * VOP_LOOKUP should discard attributes returned by the server if they were * modified by another VOP while the VOP_LOOKUP was in progress. * * Sequence of operations: * * Thread 1 calls a mutator like ftruncate, which acquires the vnode lock * exclusively. * * Thread 2 calls stat, which does VOP_LOOKUP, which sends FUSE_LOOKUP to the * server. The server replies with the old file length. Thread 2 blocks * waiting for the vnode lock. * * Thread 1 sends the mutator operation like FUSE_SETATTR that changes the * file's size and updates the attribute cache. Then it releases the vnode * lock. * * Thread 2 acquires the vnode lock. At this point it must not add the * now-stale file size to the attribute cache. * * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071 */ TEST_P(LastLocalModify, lookup) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; Sequence seq; uint64_t ino = 3; uint64_t mutator_unique; const uint64_t oldsize = 10; const uint64_t newsize = 15; pthread_t th0; void *thr0_value; struct stat sb; static sem_t sem; Mutator mutator; uint32_t mutator_op; size_t mutator_size; mutator = writer_from_str(GetParam()); mutator_op = fuse_op_from_mutator(mutator); ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .InSequence(seq) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { /* Called by the mutator, caches attributes but not entries */ SET_OUT_HEADER_LEN(out, entry); out.body.entry.nodeid = ino; out.body.entry.attr.size = oldsize; out.body.entry.nodeid = ino; out.body.entry.attr_valid_nsec = NAP_NS / 2; out.body.entry.attr.ino = ino; out.body.entry.attr.mode = S_IFREG | 0644; }))); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == mutator_op && in.header.nodeid == ino); }, Eq(true)), _) ).InSequence(seq) .WillOnce(Invoke([&](auto in, auto &out __unused) { /* * The mutator changes the file size, but in order to simulate * a race, don't reply. Instead, just save the unique for * later. */ mutator_unique = in.header.unique; switch(mutator) { case VOP_WRITE: mutator_size = in.body.write.size; break; case VOP_COPY_FILE_RANGE: mutator_size = in.body.copy_file_range.len; break; default: break; } /* Allow the lookup thread to proceed */ sem_post(&sem); })); EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .InSequence(seq) .WillOnce(Invoke([&](auto in __unused, auto& out) { std::unique_ptr out0(new mockfs_buf_out); std::unique_ptr out1(new mockfs_buf_out); /* First complete the lookup request, returning the old size */ out0->header.unique = in.header.unique; SET_OUT_HEADER_LEN(*out0, entry); out0->body.entry.attr.mode = S_IFREG | 0644; out0->body.entry.nodeid = ino; out0->body.entry.entry_valid = UINT64_MAX; out0->body.entry.attr_valid = UINT64_MAX; out0->body.entry.attr.size = oldsize; out.push_back(std::move(out0)); /* Then, respond to the mutator request */ out1->header.unique = mutator_unique; switch(mutator) { case VOP_ALLOCATE: out1->header.error = 0; out1->header.len = sizeof(out1->header); break; case VOP_COPY_FILE_RANGE: SET_OUT_HEADER_LEN(*out1, write); out1->body.write.size = mutator_size; break; case VOP_SETATTR: SET_OUT_HEADER_LEN(*out1, attr); out1->body.attr.attr.ino = ino; out1->body.attr.attr.mode = S_IFREG | 0644; out1->body.attr.attr.size = newsize; // Changed size out1->body.attr.attr_valid = UINT64_MAX; break; case VOP_WRITE: SET_OUT_HEADER_LEN(*out1, write); out1->body.write.size = mutator_size; break; } out.push_back(std::move(out1)); })); /* Start the mutator thread */ switch(mutator) { case VOP_ALLOCATE: ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th, NULL)) << strerror(errno); break; case VOP_COPY_FILE_RANGE: ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th, NULL)) << strerror(errno); break; case VOP_SETATTR: ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th, NULL)) << strerror(errno); break; case VOP_WRITE: ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, NULL)) << strerror(errno); break; } /* Wait for FUSE_SETATTR to be sent */ sem_wait(&sem); /* Lookup again, which will race with setattr */ ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); ASSERT_EQ((off_t)newsize, sb.st_size); /* ftruncate should've completed without error */ pthread_join(th0, &thr0_value); EXPECT_EQ(0, (intptr_t)thr0_value); } /* * VFS_VGET should discard attributes returned by the server if they were * modified by another VOP while the VFS_VGET was in progress. * * Sequence of operations: * * Thread 1 calls fhstat, entering VFS_VGET, and issues FUSE_LOOKUP * * Thread 2 calls a mutator like ftruncate, which acquires the vnode lock * exclusively and issues a FUSE op like FUSE_SETATTR. * * Thread 1's FUSE_LOOKUP returns with the old size, but the thread blocks * waiting for the vnode lock. * * Thread 2's FUSE op returns, and that thread sets the file's new size * in the attribute cache. Finally it releases the vnode lock. * * The vnode lock acquired, thread 1 must not overwrite the attr cache's size * with the old value. * * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071 */ TEST_P(LastLocalModify, vfs_vget) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; Sequence seq; uint64_t ino = 3; uint64_t lookup_unique; const uint64_t oldsize = 10; const uint64_t newsize = 15; pthread_t th0; void *thr0_value; struct stat sb; static sem_t sem; fhandle_t fhp; Mutator mutator; uint32_t mutator_op; if (geteuid() != 0) GTEST_SKIP() << "This test requires a privileged user"; mutator = writer_from_str(GetParam()); mutator_op = fuse_op_from_mutator(mutator); ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .Times(1) .InSequence(seq) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { /* Called by getfh, caches attributes but not entries */ SET_OUT_HEADER_LEN(out, entry); out.body.entry.nodeid = ino; out.body.entry.attr.size = oldsize; out.body.entry.nodeid = ino; out.body.entry.attr_valid_nsec = NAP_NS / 2; out.body.entry.attr.ino = ino; out.body.entry.attr.mode = S_IFREG | 0644; }))); EXPECT_LOOKUP(ino, ".") .InSequence(seq) .WillOnce(Invoke([&](auto in, auto &out __unused) { /* Called by fhstat. Block to simulate a race */ lookup_unique = in.header.unique; sem_post(&sem); })); EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .Times(1) .InSequence(seq) .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { /* Called by VOP_SETATTR, caches attributes but not entries */ SET_OUT_HEADER_LEN(out, entry); out.body.entry.nodeid = ino; out.body.entry.attr.size = oldsize; out.body.entry.nodeid = ino; out.body.entry.attr_valid_nsec = NAP_NS / 2; out.body.entry.attr.ino = ino; out.body.entry.attr.mode = S_IFREG | 0644; }))); /* Called by the mutator thread */ expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == mutator_op && in.header.nodeid == ino); }, Eq(true)), _) ).InSequence(seq) .WillOnce(Invoke([&](auto in __unused, auto& out) { std::unique_ptr out0(new mockfs_buf_out); std::unique_ptr out1(new mockfs_buf_out); /* First complete the lookup request, returning the old size */ out0->header.unique = lookup_unique; SET_OUT_HEADER_LEN(*out0, entry); out0->body.entry.attr.mode = S_IFREG | 0644; out0->body.entry.nodeid = ino; out0->body.entry.entry_valid = UINT64_MAX; out0->body.entry.attr_valid = UINT64_MAX; out0->body.entry.attr.size = oldsize; out.push_back(std::move(out0)); /* Then, respond to the mutator request */ out1->header.unique = in.header.unique; switch(mutator) { case VOP_ALLOCATE: out1->header.error = 0; out1->header.len = sizeof(out1->header); break; case VOP_COPY_FILE_RANGE: SET_OUT_HEADER_LEN(*out1, write); out1->body.write.size = in.body.copy_file_range.len; break; case VOP_SETATTR: SET_OUT_HEADER_LEN(*out1, attr); out1->body.attr.attr.ino = ino; out1->body.attr.attr.mode = S_IFREG | 0644; out1->body.attr.attr.size = newsize; // Changed size out1->body.attr.attr_valid = UINT64_MAX; break; case VOP_WRITE: SET_OUT_HEADER_LEN(*out1, write); out1->body.write.size = in.body.write.size; break; } out.push_back(std::move(out1)); })); /* First get a file handle */ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); /* Start the mutator thread */ switch(mutator) { case VOP_ALLOCATE: ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th, (void*)&sem)) << strerror(errno); break; case VOP_COPY_FILE_RANGE: ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th, (void*)&sem)) << strerror(errno); break; case VOP_SETATTR: ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th, (void*)&sem)) << strerror(errno); break; case VOP_WRITE: ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, (void*)&sem)) << strerror(errno); break; } /* Lookup again, which will race with setattr */ ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); ASSERT_EQ((off_t)newsize, sb.st_size); /* mutator should've completed without error */ pthread_join(th0, &thr0_value); EXPECT_EQ(0, (intptr_t)thr0_value); } INSTANTIATE_TEST_CASE_P(LLM, LastLocalModify, Values( "VOP_ALLOCATE", "VOP_COPY_FILE_RANGE", "VOP_SETATTR", "VOP_WRITE" ) ); diff --git a/tests/sys/fs/fusefs/lseek.cc b/tests/sys/fs/fusefs/lseek.cc index 089b0f86a7f6..5ba176f0776d 100644 --- a/tests/sys/fs/fusefs/lseek.cc +++ b/tests/sys/fs/fusefs/lseek.cc @@ -1,360 +1,380 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2020 Alan Somers * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ extern "C" { #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; class Lseek: public FuseTest {}; class LseekPathconf: public Lseek {}; class LseekPathconf_7_23: public LseekPathconf { public: virtual void SetUp() { m_kernel_minor_version = 23; FuseTest::SetUp(); } }; class LseekSeekHole: public Lseek {}; class LseekSeekData: public Lseek {}; /* * If a previous lseek operation has already returned enosys, then pathconf can * return EINVAL immediately. */ TEST_F(LseekPathconf, already_enosys) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; off_t fsize = 1 << 30; /* 1 GiB */ off_t offset_in = 1 << 28; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LSEEK); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(ENOSYS))); fd = open(FULLPATH, O_RDONLY); EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA)); EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); EXPECT_EQ(EINVAL, errno); + + leak(fd); } /* * If a previous lseek operation has already returned successfully, then * pathconf can return 1 immediately. 1 means "holes are reported, but size is * not specified". */ TEST_F(LseekPathconf, already_seeked) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; off_t fsize = 1 << 30; /* 1 GiB */ off_t offset = 1 << 28; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LSEEK); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto i, auto& out) { SET_OUT_HEADER_LEN(out, lseek); out.body.lseek.offset = i.body.lseek.offset; }))); fd = open(FULLPATH, O_RDONLY); EXPECT_EQ(offset, lseek(fd, offset, SEEK_DATA)); EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); + + leak(fd); } /* * If no FUSE_LSEEK operation has been attempted since mount, try once as soon * as a pathconf request comes in. */ TEST_F(LseekPathconf, enosys_now) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; off_t fsize = 1 << 30; /* 1 GiB */ int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LSEEK); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(ENOSYS))); fd = open(FULLPATH, O_RDONLY); EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); EXPECT_EQ(EINVAL, errno); + + leak(fd); } /* * If no FUSE_LSEEK operation has been attempted since mount, try one as soon * as a pathconf request comes in. This is the typical pattern of bsdtar. It * will only try SEEK_HOLE/SEEK_DATA if fpathconf says they're supported. */ TEST_F(LseekPathconf, seek_now) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; off_t fsize = 1 << 30; /* 1 GiB */ off_t offset_initial = 1 << 27; off_t offset_out = 1 << 29; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LSEEK); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, lseek); out.body.lseek.offset = offset_out; }))); fd = open(FULLPATH, O_RDONLY); EXPECT_EQ(offset_initial, lseek(fd, offset_initial, SEEK_SET)); EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); /* And check that the file pointer hasn't changed */ EXPECT_EQ(offset_initial, lseek(fd, 0, SEEK_CUR)); + + leak(fd); } /* * For servers using older protocol versions, no FUSE_LSEEK should be attempted */ TEST_F(LseekPathconf_7_23, already_enosys) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; off_t fsize = 1 << 30; /* 1 GiB */ int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LSEEK); }, Eq(true)), _) ).Times(0); fd = open(FULLPATH, O_RDONLY); EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); EXPECT_EQ(EINVAL, errno); + + leak(fd); } TEST_F(LseekSeekData, ok) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; off_t fsize = 1 << 30; /* 1 GiB */ off_t offset_in = 1 << 28; off_t offset_out = 1 << 29; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LSEEK && in.header.nodeid == ino && in.body.lseek.fh == FH && (off_t)in.body.lseek.offset == offset_in && in.body.lseek.whence == SEEK_DATA); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, lseek); out.body.lseek.offset = offset_out; }))); fd = open(FULLPATH, O_RDONLY); EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_DATA)); EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR)); + + leak(fd); } /* * If the server returns ENOSYS, fusefs should fall back to the default * behavior, and never query the server again. */ TEST_F(LseekSeekData, enosys) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; off_t fsize = 1 << 30; /* 1 GiB */ off_t offset_in = 1 << 28; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LSEEK && in.header.nodeid == ino && in.body.lseek.fh == FH && (off_t)in.body.lseek.offset == offset_in && in.body.lseek.whence == SEEK_DATA); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(ENOSYS))); fd = open(FULLPATH, O_RDONLY); /* * Default behavior: ENXIO if offset is < 0 or >= fsize, offset * otherwise. */ EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA)); EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE)); EXPECT_EQ(ENXIO, errno); EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE)); EXPECT_EQ(ENXIO, errno); + + leak(fd); } TEST_F(LseekSeekHole, ok) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; off_t fsize = 1 << 30; /* 1 GiB */ off_t offset_in = 1 << 28; off_t offset_out = 1 << 29; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LSEEK && in.header.nodeid == ino && in.body.lseek.fh == FH && (off_t)in.body.lseek.offset == offset_in && in.body.lseek.whence == SEEK_HOLE); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, lseek); out.body.lseek.offset = offset_out; }))); fd = open(FULLPATH, O_RDONLY); EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_HOLE)); EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR)); + + leak(fd); } /* * If the server returns ENOSYS, fusefs should fall back to the default * behavior, and never query the server again. */ TEST_F(LseekSeekHole, enosys) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; off_t fsize = 1 << 30; /* 1 GiB */ off_t offset_in = 1 << 28; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LSEEK && in.header.nodeid == ino && in.body.lseek.fh == FH && (off_t)in.body.lseek.offset == offset_in && in.body.lseek.whence == SEEK_HOLE); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(ENOSYS))); fd = open(FULLPATH, O_RDONLY); /* * Default behavior: ENXIO if offset is < 0 or >= fsize, fsize * otherwise. */ EXPECT_EQ(fsize, lseek(fd, offset_in, SEEK_HOLE)); EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE)); EXPECT_EQ(ENXIO, errno); EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE)); EXPECT_EQ(ENXIO, errno); + + leak(fd); } /* lseek should return ENXIO when offset points to EOF */ TEST_F(LseekSeekHole, enxio) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; off_t fsize = 1 << 30; /* 1 GiB */ off_t offset_in = fsize; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LSEEK && in.header.nodeid == ino && in.body.lseek.fh == FH && (off_t)in.body.lseek.offset == offset_in && in.body.lseek.whence == SEEK_HOLE); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(ENXIO))); fd = open(FULLPATH, O_RDONLY); EXPECT_EQ(-1, lseek(fd, offset_in, SEEK_HOLE)); EXPECT_EQ(ENXIO, errno); + + leak(fd); } diff --git a/tests/sys/fs/fusefs/open.cc b/tests/sys/fs/fusefs/open.cc index 7ac177a65d14..c1314fc0d02a 100644 --- a/tests/sys/fs/fusefs/open.cc +++ b/tests/sys/fs/fusefs/open.cc @@ -1,342 +1,343 @@ /*- * 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); ASSERT_LE(0, fd) << strerror(errno); leak(fd); } }; class OpenNoOpenSupport: public FuseTest { virtual void SetUp() { m_init_flags = FUSE_NO_OPEN_SUPPORT; FuseTest::SetUp(); } }; /* * 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); ASSERT_LE(0, fd1) << strerror(errno); }, [] { int fd0; fd0 = open(FULLPATH, O_RDONLY); if (fd0 < 0) { perror("open"); return(1); } + leak(fd0); return 0; } ); ASSERT_EQ(0, WEXITSTATUS(status)); close(fd1); } /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */ TEST_F(Open, DISABLED_o_append) { test_ok(O_WRONLY | O_APPEND, O_WRONLY | O_APPEND); } /* The kernel is supposed to filter out this flag */ TEST_F(Open, o_creat) { test_ok(O_WRONLY | O_CREAT, O_WRONLY); } /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */ TEST_F(Open, DISABLED_o_direct) { test_ok(O_WRONLY | O_DIRECT, O_WRONLY | O_DIRECT); } /* The kernel is supposed to filter out this flag */ TEST_F(Open, o_excl) { test_ok(O_WRONLY | O_EXCL, O_WRONLY); } TEST_F(Open, o_exec) { test_ok(O_EXEC, O_EXEC); } /* The kernel is supposed to filter out this flag */ TEST_F(Open, o_noctty) { test_ok(O_WRONLY | O_NOCTTY, O_WRONLY); } TEST_F(Open, o_rdonly) { test_ok(O_RDONLY, O_RDONLY); } /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */ TEST_F(Open, DISABLED_o_trunc) { test_ok(O_WRONLY | O_TRUNC, O_WRONLY | O_TRUNC); } TEST_F(Open, o_wronly) { test_ok(O_WRONLY, O_WRONLY); } TEST_F(Open, o_rdwr) { test_ok(O_RDWR, O_RDWR); } /* * Without FUSE_NO_OPEN_SUPPORT, returning ENOSYS is an error */ TEST_F(Open, enosys) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_OPEN && in.body.open.flags == (uint32_t)O_RDONLY && in.header.nodeid == ino); }, Eq(true)), _) ).Times(1) .WillOnce(Invoke(ReturnErrno(ENOSYS))); fd = open(FULLPATH, O_RDONLY); ASSERT_EQ(-1, fd) << strerror(errno); EXPECT_EQ(ENOSYS, errno); } /* * If a fuse server sets FUSE_NO_OPEN_SUPPORT and returns ENOSYS to a * FUSE_OPEN, then it and subsequent FUSE_OPEN and FUSE_RELEASE operations will * also succeed automatically without being sent to the server. */ TEST_F(OpenNoOpenSupport, enosys) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_OPEN && in.body.open.flags == (uint32_t)O_RDONLY && in.header.nodeid == ino); }, Eq(true)), _) ).Times(1) .WillOnce(Invoke(ReturnErrno(ENOSYS))); expect_flush(ino, 1, ReturnErrno(ENOSYS)); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); close(fd); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); leak(fd); }