diff --git a/tests/sys/fs/fusefs/bmap.cc b/tests/sys/fs/fusefs/bmap.cc index 1ef3dfa00045..4c9edac9360a 100644 --- a/tests/sys/fs/fusefs/bmap.cc +++ b/tests/sys/fs/fusefs/bmap.cc @@ -1,256 +1,257 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ extern "C" { #include #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 *buf; + char *buf; int fd; int ngetattrs; ngetattrs = GetParam(); FuseTest::expect_lookup(RELPATH, ino, mode, filesize, 1, 0); expect_open(ino, 0, 1); // Depending on ngetattrs, FUSE_READ could be called with either // filesize or filesize / 2 . EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ && in.header.nodeid == ino && in.body.read.offset == 0 && ( in.body.read.size == filesize || in.body.read.size == filesize / 2)); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { size_t osize = in.body.read.size; assert(osize < sizeof(out.body.bytes)); out.header.len = sizeof(struct fuse_out_header) + osize; bzero(out.body.bytes, osize); }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_GETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).Times(Between(ngetattrs - 1, ngetattrs)) .InSequence(seq) .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr_valid = 0; out.body.attr.attr.ino = ino; out.body.attr.attr.mode = S_IFREG | 0644; out.body.attr.attr.size = filesize; }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_GETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).InSequence(seq) .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr_valid = 0; out.body.attr.attr.ino = ino; out.body.attr.attr.mode = S_IFREG | 0644; out.body.attr.attr.size = filesize / 2; }))); - buf = calloc(1, filesize); + buf = new char[filesize](); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); read(fd, buf, filesize); + delete[] buf; leak(fd); } INSTANTIATE_TEST_SUITE_P(BE, BmapEof, Values(1, 2, 3) ); diff --git a/tests/sys/fs/fusefs/copy_file_range.cc b/tests/sys/fs/fusefs/copy_file_range.cc index 17b21b888736..806ecf3c3653 100644 --- a/tests/sys/fs/fusefs/copy_file_range.cc +++ b/tests/sys/fs/fusefs/copy_file_range.cc @@ -1,802 +1,802 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2020 Alan Somers * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ extern "C" { #include #include #include #include #include #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; class CopyFileRange: public FuseTest { public: void expect_maybe_lseek(uint64_t ino) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LSEEK && in.header.nodeid == ino); }, Eq(true)), _) ).Times(AtMost(1)) .WillRepeatedly(Invoke(ReturnErrno(ENOSYS))); } void expect_open(uint64_t ino, uint32_t flags, int times, uint64_t fh) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_OPEN && in.header.nodeid == ino); }, Eq(true)), _) ).Times(times) .WillRepeatedly(Invoke( ReturnImmediate([=](auto in __unused, auto& out) { out.header.len = sizeof(out.header); SET_OUT_HEADER_LEN(out, open); out.body.open.fh = fh; out.body.open.open_flags = flags; }))); } void expect_write(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, const void *contents) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { const char *buf = (const char*)in.body.bytes + sizeof(struct fuse_write_in); return (in.header.opcode == FUSE_WRITE && in.header.nodeid == ino && in.body.write.offset == offset && in.body.write.size == isize && 0 == bcmp(buf, contents, isize)); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = osize; }))); } }; class CopyFileRange_7_27: public CopyFileRange { public: virtual void SetUp() { m_kernel_minor_version = 27; CopyFileRange::SetUp(); } }; class CopyFileRangeNoAtime: public CopyFileRange { public: virtual void SetUp() { m_noatime = true; CopyFileRange::SetUp(); } }; class CopyFileRangeRlimitFsize: public CopyFileRange { public: static sig_atomic_t s_sigxfsz; struct rlimit m_initial_limit; virtual void SetUp() { s_sigxfsz = 0; getrlimit(RLIMIT_FSIZE, &m_initial_limit); CopyFileRange::SetUp(); } void TearDown() { struct sigaction sa; setrlimit(RLIMIT_FSIZE, &m_initial_limit); bzero(&sa, sizeof(sa)); sa.sa_handler = SIG_DFL; sigaction(SIGXFSZ, &sa, NULL); FuseTest::TearDown(); } }; sig_atomic_t CopyFileRangeRlimitFsize::s_sigxfsz = 0; void sigxfsz_handler(int __unused sig) { CopyFileRangeRlimitFsize::s_sigxfsz = 1; } TEST_F(CopyFileRange, eio) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize1 = 1 << 20; /* 1 MiB */ off_t fsize2 = 1 << 19; /* 512 KiB */ off_t start1 = 1 << 18; off_t start2 = 3 << 17; ssize_t len = 65536; int fd1, fd2; expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino1 && in.body.copy_file_range.fh_in == fh1 && (off_t)in.body.copy_file_range.off_in == start1 && in.body.copy_file_range.nodeid_out == ino2 && in.body.copy_file_range.fh_out == fh2 && (off_t)in.body.copy_file_range.off_out == start2 && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(EIO))); fd1 = open(FULLPATH1, O_RDONLY); fd2 = open(FULLPATH2, O_WRONLY); ASSERT_EQ(-1, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); EXPECT_EQ(EIO, errno); } /* * copy_file_range should evict cached data for the modified region of the * destination file. */ TEST_F(CopyFileRange, evicts_cache) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; - void *buf0, *buf1, *buf; + char *buf0, *buf1, *buf; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize1 = 1 << 20; /* 1 MiB */ off_t fsize2 = 1 << 19; /* 512 KiB */ off_t start1 = 1 << 18; off_t start2 = 3 << 17; ssize_t len = m_maxbcachebuf; int fd1, fd2; - buf0 = malloc(m_maxbcachebuf); + buf0 = new char[m_maxbcachebuf]; memset(buf0, 42, m_maxbcachebuf); expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); expect_read(ino2, start2, m_maxbcachebuf, m_maxbcachebuf, buf0, -1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino1 && in.body.copy_file_range.fh_in == fh1 && (off_t)in.body.copy_file_range.off_in == start1 && in.body.copy_file_range.nodeid_out == ino2 && in.body.copy_file_range.fh_out == fh2 && (off_t)in.body.copy_file_range.off_out == start2 && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = len; }))); fd1 = open(FULLPATH1, O_RDONLY); fd2 = open(FULLPATH2, O_RDWR); // Prime cache - buf = malloc(m_maxbcachebuf); + buf = new char[m_maxbcachebuf]; ASSERT_EQ(m_maxbcachebuf, pread(fd2, buf, m_maxbcachebuf, start2)) << strerror(errno); EXPECT_EQ(0, memcmp(buf0, buf, m_maxbcachebuf)); // Tell the FUSE server overwrite the region we just read ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); // Read again. This should bypass the cache and read direct from server - buf1 = malloc(m_maxbcachebuf); + buf1 = new char[m_maxbcachebuf]; memset(buf1, 69, m_maxbcachebuf); start2 -= len; expect_read(ino2, start2, m_maxbcachebuf, m_maxbcachebuf, buf1, -1, fh2); ASSERT_EQ(m_maxbcachebuf, pread(fd2, buf, m_maxbcachebuf, start2)) << strerror(errno); EXPECT_EQ(0, memcmp(buf1, buf, m_maxbcachebuf)); - free(buf1); - free(buf0); - free(buf); + delete[] buf1; + delete[] buf0; + delete[] buf; leak(fd1); leak(fd2); } /* * If the server doesn't support FUSE_COPY_FILE_RANGE, the kernel should * fallback to a read/write based implementation. */ TEST_F(CopyFileRange, fallback) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize2 = 0; off_t start1 = 0; off_t start2 = 0; const char *contents = "Hello, world!"; ssize_t len; int fd1, fd2; len = strlen(contents); /* * Ensure that we read to EOF, just so the buffer cache's read size is * predictable. */ expect_lookup(RELPATH1, ino1, S_IFREG | 0644, start1 + len, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino1 && in.body.copy_file_range.fh_in == fh1 && (off_t)in.body.copy_file_range.off_in == start1 && in.body.copy_file_range.nodeid_out == ino2 && in.body.copy_file_range.fh_out == fh2 && (off_t)in.body.copy_file_range.off_out == start2 && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(ENOSYS))); expect_maybe_lseek(ino1); expect_read(ino1, start1, len, len, contents, 0); expect_write(ino2, start2, len, len, contents); fd1 = open(FULLPATH1, O_RDONLY); ASSERT_GE(fd1, 0); fd2 = open(FULLPATH2, O_WRONLY); ASSERT_GE(fd2, 0); ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); } /* * Writes via mmap should not conflict with using copy_file_range. Any dirty * pages that overlap with copy_file_range's input should be flushed before * FUSE_COPY_FILE_RANGE is sent. */ TEST_F(CopyFileRange, mmap_write) { const char FULLPATH[] = "mountpoint/src.txt"; const char RELPATH[] = "src.txt"; uint8_t *wbuf, *fbuf; void *p; size_t fsize = 0x6000; size_t wsize = 0x3000; ssize_t r; off_t offset2_in = 0; off_t offset2_out = wsize; size_t copysize = wsize; const uint64_t ino = 42; const uint64_t fh = 0xdeadbeef1a7ebabe; int fd; const mode_t mode = 0644; - fbuf = (uint8_t*)calloc(1, fsize); - wbuf = (uint8_t*)malloc(wsize); + fbuf = new uint8_t[fsize](); + wbuf = new uint8_t[wsize]; memset(wbuf, 1, wsize); expect_lookup(RELPATH, ino, S_IFREG | mode, fsize, 1); expect_open(ino, 0, 1, fh); /* This read is initiated by the mmap write */ expect_read(ino, 0, fsize, fsize, fbuf, -1, fh); /* This write flushes the buffer filled by the mmap write */ expect_write(ino, 0, wsize, wsize, wbuf); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && (off_t)in.body.copy_file_range.off_in == offset2_in && (off_t)in.body.copy_file_range.off_out == offset2_out && in.body.copy_file_range.len == copysize ); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([&](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = copysize; }))); fd = open(FULLPATH, O_RDWR); /* First, write some data via mmap */ p = mmap(NULL, wsize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); ASSERT_NE(MAP_FAILED, p) << strerror(errno); memmove((uint8_t*)p, wbuf, wsize); ASSERT_EQ(0, munmap(p, wsize)) << strerror(errno); /* * Then copy it around the file via copy_file_range. This should * trigger a FUSE_WRITE to flush the pages written by mmap. */ r = copy_file_range(fd, &offset2_in, fd, &offset2_out, copysize, 0); ASSERT_EQ(copysize, (size_t)r) << strerror(errno); - free(wbuf); - free(fbuf); + delete[] wbuf; + delete[] fbuf; } /* * copy_file_range should send SIGXFSZ and return EFBIG when the operation * would exceed the limit imposed by RLIMIT_FSIZE. */ TEST_F(CopyFileRangeRlimitFsize, signal) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; struct rlimit rl; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize1 = 1 << 20; /* 1 MiB */ off_t fsize2 = 1 << 19; /* 512 KiB */ off_t start1 = 1 << 18; off_t start2 = fsize2; ssize_t len = 65536; int fd1, fd2; expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE); }, Eq(true)), _) ).Times(0); rl.rlim_cur = fsize2; rl.rlim_max = m_initial_limit.rlim_max; ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); fd1 = open(FULLPATH1, O_RDONLY); fd2 = open(FULLPATH2, O_WRONLY); ASSERT_EQ(-1, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); EXPECT_EQ(EFBIG, errno); EXPECT_EQ(1, s_sigxfsz); } /* * When crossing the RLIMIT_FSIZE boundary, writes should be truncated, not * aborted. * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=266611 */ TEST_F(CopyFileRangeRlimitFsize, truncate) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; struct rlimit rl; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize1 = 1 << 20; /* 1 MiB */ off_t fsize2 = 1 << 19; /* 512 KiB */ off_t start1 = 1 << 18; off_t start2 = fsize2; ssize_t len = 65536; off_t limit = start2 + len / 2; int fd1, fd2; expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && (off_t)in.body.copy_file_range.off_out == start2 && in.body.copy_file_range.len == (size_t)len / 2 ); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = len / 2; }))); rl.rlim_cur = limit; rl.rlim_max = m_initial_limit.rlim_max; ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); fd1 = open(FULLPATH1, O_RDONLY); fd2 = open(FULLPATH2, O_WRONLY); ASSERT_EQ(len / 2, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); } TEST_F(CopyFileRange, ok) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize1 = 1 << 20; /* 1 MiB */ off_t fsize2 = 1 << 19; /* 512 KiB */ off_t start1 = 1 << 18; off_t start2 = 3 << 17; ssize_t len = 65536; int fd1, fd2; expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino1 && in.body.copy_file_range.fh_in == fh1 && (off_t)in.body.copy_file_range.off_in == start1 && in.body.copy_file_range.nodeid_out == ino2 && in.body.copy_file_range.fh_out == fh2 && (off_t)in.body.copy_file_range.off_out == start2 && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = len; }))); fd1 = open(FULLPATH1, O_RDONLY); fd2 = open(FULLPATH2, O_WRONLY); ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); } /* * copy_file_range can make copies within a single file, as long as the ranges * don't overlap. * */ TEST_F(CopyFileRange, same_file) { const char FULLPATH[] = "mountpoint/src.txt"; const char RELPATH[] = "src.txt"; const uint64_t ino = 4; const uint64_t fh = 0xdeadbeefa7ebabe; off_t fsize = 1 << 20; /* 1 MiB */ off_t off_in = 1 << 18; off_t off_out = 3 << 17; ssize_t len = 65536; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1, fh); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino && in.body.copy_file_range.fh_in == fh && (off_t)in.body.copy_file_range.off_in == off_in && in.body.copy_file_range.nodeid_out == ino && in.body.copy_file_range.fh_out == fh && (off_t)in.body.copy_file_range.off_out == off_out && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = len; }))); fd = open(FULLPATH, O_RDWR); ASSERT_EQ(len, copy_file_range(fd, &off_in, fd, &off_out, len, 0)); leak(fd); } /* * copy_file_range should update the destination's mtime and ctime, and * the source's atime. */ TEST_F(CopyFileRange, timestamps) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; struct stat sb1a, sb1b, sb2a, sb2b; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize1 = 1 << 20; /* 1 MiB */ off_t fsize2 = 1 << 19; /* 512 KiB */ off_t start1 = 1 << 18; off_t start2 = 3 << 17; ssize_t len = 65536; int fd1, fd2; expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino1 && in.body.copy_file_range.fh_in == fh1 && (off_t)in.body.copy_file_range.off_in == start1 && in.body.copy_file_range.nodeid_out == ino2 && in.body.copy_file_range.fh_out == fh2 && (off_t)in.body.copy_file_range.off_out == start2 && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = len; }))); fd1 = open(FULLPATH1, O_RDONLY); ASSERT_GE(fd1, 0); fd2 = open(FULLPATH2, O_WRONLY); ASSERT_GE(fd2, 0); ASSERT_EQ(0, fstat(fd1, &sb1a)) << strerror(errno); ASSERT_EQ(0, fstat(fd2, &sb2a)) << strerror(errno); nap(); ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); ASSERT_EQ(0, fstat(fd1, &sb1b)) << strerror(errno); ASSERT_EQ(0, fstat(fd2, &sb2b)) << strerror(errno); EXPECT_NE(sb1a.st_atime, sb1b.st_atime); EXPECT_EQ(sb1a.st_mtime, sb1b.st_mtime); EXPECT_EQ(sb1a.st_ctime, sb1b.st_ctime); EXPECT_EQ(sb2a.st_atime, sb2b.st_atime); EXPECT_NE(sb2a.st_mtime, sb2b.st_mtime); EXPECT_NE(sb2a.st_ctime, sb2b.st_ctime); leak(fd1); leak(fd2); } /* * copy_file_range can extend the size of a file * */ TEST_F(CopyFileRange, extend) { const char FULLPATH[] = "mountpoint/src.txt"; const char RELPATH[] = "src.txt"; struct stat sb; const uint64_t ino = 4; const uint64_t fh = 0xdeadbeefa7ebabe; off_t fsize = 65536; off_t off_in = 0; off_t off_out = 65536; ssize_t len = 65536; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1, fh); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino && in.body.copy_file_range.fh_in == fh && (off_t)in.body.copy_file_range.off_in == off_in && in.body.copy_file_range.nodeid_out == ino && in.body.copy_file_range.fh_out == fh && (off_t)in.body.copy_file_range.off_out == off_out && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = len; }))); fd = open(FULLPATH, O_RDWR); ASSERT_GE(fd, 0); ASSERT_EQ(len, copy_file_range(fd, &off_in, fd, &off_out, len, 0)); /* Check that cached attributes were updated appropriately */ ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); EXPECT_EQ(fsize + len, sb.st_size); leak(fd); } /* With older protocol versions, no FUSE_COPY_FILE_RANGE should be attempted */ TEST_F(CopyFileRange_7_27, fallback) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize2 = 0; off_t start1 = 0; off_t start2 = 0; const char *contents = "Hello, world!"; ssize_t len; int fd1, fd2; len = strlen(contents); /* * Ensure that we read to EOF, just so the buffer cache's read size is * predictable. */ expect_lookup(RELPATH1, ino1, S_IFREG | 0644, start1 + len, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE); }, Eq(true)), _) ).Times(0); expect_maybe_lseek(ino1); expect_read(ino1, start1, len, len, contents, 0); expect_write(ino2, start2, len, len, contents); fd1 = open(FULLPATH1, O_RDONLY); ASSERT_GE(fd1, 0); fd2 = open(FULLPATH2, O_WRONLY); ASSERT_GE(fd2, 0); ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); leak(fd1); leak(fd2); } /* * With -o noatime, copy_file_range should update the destination's mtime and * ctime, but not the source's atime. */ TEST_F(CopyFileRangeNoAtime, timestamps) { const char FULLPATH1[] = "mountpoint/src.txt"; const char RELPATH1[] = "src.txt"; const char FULLPATH2[] = "mountpoint/dst.txt"; const char RELPATH2[] = "dst.txt"; struct stat sb1a, sb1b, sb2a, sb2b; const uint64_t ino1 = 42; const uint64_t ino2 = 43; const uint64_t fh1 = 0xdeadbeef1a7ebabe; const uint64_t fh2 = 0xdeadc0de88c0ffee; off_t fsize1 = 1 << 20; /* 1 MiB */ off_t fsize2 = 1 << 19; /* 512 KiB */ off_t start1 = 1 << 18; off_t start2 = 3 << 17; ssize_t len = 65536; int fd1, fd2; expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1); expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1); expect_open(ino1, 0, 1, fh1); expect_open(ino2, 0, 1, fh2); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino1 && in.body.copy_file_range.fh_in == fh1 && (off_t)in.body.copy_file_range.off_in == start1 && in.body.copy_file_range.nodeid_out == ino2 && in.body.copy_file_range.fh_out == fh2 && (off_t)in.body.copy_file_range.off_out == start2 && in.body.copy_file_range.len == (size_t)len && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = len; }))); fd1 = open(FULLPATH1, O_RDONLY); ASSERT_GE(fd1, 0); fd2 = open(FULLPATH2, O_WRONLY); ASSERT_GE(fd2, 0); ASSERT_EQ(0, fstat(fd1, &sb1a)) << strerror(errno); ASSERT_EQ(0, fstat(fd2, &sb2a)) << strerror(errno); nap(); ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0)); ASSERT_EQ(0, fstat(fd1, &sb1b)) << strerror(errno); ASSERT_EQ(0, fstat(fd2, &sb2b)) << strerror(errno); EXPECT_EQ(sb1a.st_atime, sb1b.st_atime); EXPECT_EQ(sb1a.st_mtime, sb1b.st_mtime); EXPECT_EQ(sb1a.st_ctime, sb1b.st_ctime); EXPECT_EQ(sb2a.st_atime, sb2b.st_atime); EXPECT_NE(sb2a.st_mtime, sb2b.st_mtime); EXPECT_NE(sb2a.st_ctime, sb2b.st_ctime); leak(fd1); leak(fd2); } diff --git a/tests/sys/fs/fusefs/fallocate.cc b/tests/sys/fs/fusefs/fallocate.cc index 251552ddc8d0..92e327be5ade 100644 --- a/tests/sys/fs/fusefs/fallocate.cc +++ b/tests/sys/fs/fusefs/fallocate.cc @@ -1,727 +1,727 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2021 Alan Somers * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ extern "C" { #include #include #include #include #include #include #include #include "mntopts.h" // for build_iovec } #include "mockfs.hh" #include "utils.hh" using namespace testing; /* Is buf all zero? */ static bool is_zero(const char *buf, uint64_t size) { return buf[0] == 0 && !memcmp(buf, buf + 1, size - 1); } class Fallocate: public FuseTest { public: /* * expect VOP_DEALLOCATE to be implemented by vop_stddeallocate. */ void expect_vop_stddeallocate(uint64_t ino, uint64_t off, uint64_t length) { /* XXX read offset and size may depend on cache mode */ EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ && in.header.nodeid == ino && in.body.read.offset <= off && in.body.read.offset + in.body.read.size >= off + length); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { assert(in.body.read.size <= sizeof(out.body.bytes)); out.header.len = sizeof(struct fuse_out_header) + in.body.read.size; memset(out.body.bytes, 'X', in.body.read.size); }))).RetiresOnSaturation(); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { const char *buf = (const char*)in.body.bytes + sizeof(struct fuse_write_in); assert(length <= sizeof(in.body.bytes) - sizeof(struct fuse_write_in)); return (in.header.opcode == FUSE_WRITE && in.header.nodeid == ino && in.body.write.offset == off && in.body.write.size == length && is_zero(buf, length)); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = length; }))); } }; class Fspacectl: public Fallocate {}; class Fspacectl_7_18: public Fspacectl { public: virtual void SetUp() { m_kernel_minor_version = 18; Fspacectl::SetUp(); } }; class FspacectlCache: public Fspacectl, public WithParamInterface { public: bool m_direct_io; FspacectlCache(): m_direct_io(false) {}; virtual void SetUp() { int cache_mode = GetParam(); switch (cache_mode) { case Uncached: m_direct_io = true; break; case WritebackAsync: m_async = true; /* FALLTHROUGH */ case Writeback: m_init_flags |= FUSE_WRITEBACK_CACHE; /* FALLTHROUGH */ case Writethrough: break; default: FAIL() << "Unknown cache mode"; } FuseTest::SetUp(); if (IsSkipped()) return; } }; class PosixFallocate: public Fallocate { public: static sig_atomic_t s_sigxfsz; void SetUp() { s_sigxfsz = 0; FuseTest::SetUp(); } void TearDown() { struct sigaction sa; bzero(&sa, sizeof(sa)); sa.sa_handler = SIG_DFL; sigaction(SIGXFSZ, &sa, NULL); Fallocate::TearDown(); } }; sig_atomic_t PosixFallocate::s_sigxfsz = 0; void sigxfsz_handler(int __unused sig) { PosixFallocate::s_sigxfsz = 1; } class PosixFallocate_7_18: public PosixFallocate { public: virtual void SetUp() { m_kernel_minor_version = 18; PosixFallocate::SetUp(); } }; /* * If the server returns ENOSYS, it indicates that the server does not support * FUSE_FALLOCATE. This and future calls should fall back to vop_stddeallocate. */ TEST_F(Fspacectl, enosys) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; off_t fsize = 1 << 20; off_t off0 = 100; off_t len0 = 500; struct spacectl_range rqsr = { .r_offset = off0, .r_len = len0 }; uint64_t ino = 42; uint64_t off1 = fsize; uint64_t len1 = 1000; off_t off2 = fsize / 2; off_t len2 = 500; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); expect_fallocate(ino, off0, len0, FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, ENOSYS); expect_vop_stddeallocate(ino, off0, len0); expect_vop_stddeallocate(ino, off2, len2); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); /* Subsequent calls shouldn't query the daemon either */ rqsr.r_offset = off2; rqsr.r_len = len2; EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); /* Neither should posix_fallocate query the daemon */ EXPECT_EQ(EINVAL, posix_fallocate(fd, off1, len1)); leak(fd); } /* * EOPNOTSUPP means "the file system does not support fallocate with the * supplied mode on this particular file". So we should fallback, but not * assume anything about whether the operation will fail on a different file or * with a different mode. */ TEST_F(Fspacectl, eopnotsupp) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; struct spacectl_range rqsr; uint64_t ino = 42; uint64_t fsize = 1 << 20; uint64_t off0 = 500; uint64_t len = 1000; uint64_t off1 = fsize / 2; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); expect_fallocate(ino, off0, len, FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, EOPNOTSUPP); expect_vop_stddeallocate(ino, off0, len); expect_fallocate(ino, off1, len, FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, EOPNOTSUPP); expect_vop_stddeallocate(ino, off1, len); expect_fallocate(ino, fsize, len, 0, 0); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); /* * Though the FUSE daemon will reject the call, the kernel should fall * back to a read-modify-write approach. */ rqsr.r_offset = off0; rqsr.r_len = len; EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); /* Subsequent calls should still query the daemon */ rqsr.r_offset = off1; rqsr.r_len = len; EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); /* But subsequent posix_fallocate calls _should_ query the daemon */ EXPECT_EQ(0, posix_fallocate(fd, fsize, len)); leak(fd); } TEST_F(Fspacectl, erofs) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; struct statfs statbuf; uint64_t fsize = 2000; struct spacectl_range rqsr = { .r_offset = 0, .r_len = 1 }; struct iovec *iov = NULL; int iovlen = 0; uint64_t ino = 42; int fd; int newflags; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_STATFS); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { /* * All of the fields except f_flags are don't care, and f_flags * is set by the VFS */ SET_OUT_HEADER_LEN(out, statfs); }))); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); /* Remount read-only */ ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); newflags = statbuf.f_flags | MNT_UPDATE | MNT_RDONLY; build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1); build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1); build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1); ASSERT_EQ(0, nmount(iov, iovlen, newflags)) << strerror(errno); EXPECT_EQ(-1, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); EXPECT_EQ(EROFS, errno); leak(fd); } TEST_F(Fspacectl, ok) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; struct spacectl_range rqsr, rmsr; struct stat sb0, sb1; uint64_t ino = 42; uint64_t fsize = 2000; uint64_t offset = 500; uint64_t length = 1000; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); expect_fallocate(ino, offset, length, FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb0)) << strerror(errno); rqsr.r_offset = offset; rqsr.r_len = length; EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr)); EXPECT_EQ(0, rmsr.r_len); EXPECT_EQ((off_t)(offset + length), rmsr.r_offset); /* * The file's attributes should not have been invalidated, so this fstat * will not requery the daemon. */ EXPECT_EQ(0, fstat(fd, &sb1)); EXPECT_EQ(fsize, (uint64_t)sb1.st_size); /* mtime and ctime should be updated */ EXPECT_EQ(sb0.st_atime, sb1.st_atime); EXPECT_NE(sb0.st_mtime, sb1.st_mtime); EXPECT_NE(sb0.st_ctime, sb1.st_ctime); leak(fd); } /* The returned rqsr.r_off should be clipped at EoF */ TEST_F(Fspacectl, past_eof) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; struct spacectl_range rqsr, rmsr; uint64_t ino = 42; uint64_t fsize = 1000; uint64_t offset = 1500; uint64_t length = 1000; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); expect_fallocate(ino, offset, length, FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); rqsr.r_offset = offset; rqsr.r_len = length; EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr)); EXPECT_EQ(0, rmsr.r_len); EXPECT_EQ((off_t)fsize, rmsr.r_offset); leak(fd); } /* The returned rqsr.r_off should be clipped at EoF */ TEST_F(Fspacectl, spans_eof) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; struct spacectl_range rqsr, rmsr; uint64_t ino = 42; uint64_t fsize = 1000; uint64_t offset = 500; uint64_t length = 1000; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); expect_fallocate(ino, offset, length, FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); rqsr.r_offset = offset; rqsr.r_len = length; EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr)); EXPECT_EQ(0, rmsr.r_len); EXPECT_EQ((off_t)fsize, rmsr.r_offset); leak(fd); } /* * With older servers, no FUSE_FALLOCATE should be attempted. The kernel * should fall back to vop_stddeallocate. */ TEST_F(Fspacectl_7_18, ok) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; struct spacectl_range rqsr, rmsr; - void *buf; + char *buf; uint64_t ino = 42; uint64_t fsize = 2000; uint64_t offset = 500; uint64_t length = 1000; int fd; - buf = malloc(length); + buf = new char[length]; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); expect_vop_stddeallocate(ino, offset, length); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); rqsr.r_offset = offset; rqsr.r_len = length; EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr)); EXPECT_EQ(0, rmsr.r_len); EXPECT_EQ((off_t)(offset + length), rmsr.r_offset); leak(fd); - free(buf); + delete[] buf; } /* * A successful fspacectl should clear the zeroed data from the kernel cache. */ TEST_P(FspacectlCache, clears_cache) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefghijklmnopqrstuvwxyz"; struct spacectl_range rqsr, rmsr; uint64_t ino = 42; ssize_t bufsize = strlen(CONTENTS); uint64_t fsize = bufsize; uint8_t buf[bufsize]; char zbuf[bufsize]; uint64_t offset = 0; uint64_t length = bufsize; int fd; bzero(zbuf, bufsize); expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); /* NB: expectations are applied in LIFO order */ expect_read(ino, 0, fsize, fsize, zbuf); expect_read(ino, 0, fsize, fsize, CONTENTS); expect_fallocate(ino, offset, length, FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); /* Populate the cache */ ASSERT_EQ(fsize, (uint64_t)pread(fd, buf, bufsize, 0)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, fsize)); /* Zero the file */ rqsr.r_offset = offset; rqsr.r_len = length; EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr)); EXPECT_EQ(0, rmsr.r_len); EXPECT_EQ((off_t)(offset + length), rmsr.r_offset); /* Read again. This should query the daemon */ ASSERT_EQ(fsize, (uint64_t)pread(fd, buf, bufsize, 0)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, zbuf, fsize)); leak(fd); } INSTANTIATE_TEST_SUITE_P(FspacectlCache, FspacectlCache, Values(Uncached, Writethrough, Writeback) ); /* * If the server returns ENOSYS, it indicates that the server does not support * FUSE_FALLOCATE. This and future calls should return EINVAL. */ TEST_F(PosixFallocate, enosys) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; uint64_t off0 = 0; uint64_t len0 = 1000; off_t off1 = 100; off_t len1 = 200; uint64_t fsize = 500; struct spacectl_range rqsr = { .r_offset = off1, .r_len = len1 }; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); expect_fallocate(ino, off0, len0, 0, ENOSYS); expect_vop_stddeallocate(ino, off1, len1); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); EXPECT_EQ(EINVAL, posix_fallocate(fd, off0, len0)); /* Subsequent calls shouldn't query the daemon*/ EXPECT_EQ(EINVAL, posix_fallocate(fd, off0, len0)); /* Neither should VOP_DEALLOCATE query the daemon */ EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); leak(fd); } /* * EOPNOTSUPP means "the file system does not support fallocate with the * supplied mode on this particular file". So we should fallback, but not * assume anything about whether the operation will fail on a different file or * with a different mode. */ TEST_F(PosixFallocate, eopnotsupp) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; struct spacectl_range rqsr; uint64_t ino = 42; uint64_t fsize = 2000; uint64_t offset = 0; uint64_t length = 1000; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); expect_fallocate(ino, fsize, length, 0, EOPNOTSUPP); expect_fallocate(ino, offset, length, 0, EOPNOTSUPP); expect_fallocate(ino, offset, length, FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); EXPECT_EQ(EINVAL, posix_fallocate(fd, fsize, length)); /* Subsequent calls should still query the daemon*/ EXPECT_EQ(EINVAL, posix_fallocate(fd, offset, length)); /* And subsequent VOP_DEALLOCATE calls should also query the daemon */ rqsr.r_len = length; rqsr.r_offset = offset; EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); leak(fd); } /* EIO is not a permanent error, and may be retried */ TEST_F(PosixFallocate, eio) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; uint64_t offset = 0; uint64_t length = 1000; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); expect_open(ino, 0, 1); expect_fallocate(ino, offset, length, 0, EIO); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); EXPECT_EQ(EIO, posix_fallocate(fd, offset, length)); expect_fallocate(ino, offset, length, 0, 0); EXPECT_EQ(0, posix_fallocate(fd, offset, length)); leak(fd); } TEST_F(PosixFallocate, erofs) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; struct statfs statbuf; struct iovec *iov = NULL; int iovlen = 0; uint64_t ino = 42; uint64_t offset = 0; uint64_t length = 1000; int fd; int newflags; expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_STATFS); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { /* * All of the fields except f_flags are don't care, and f_flags * is set by the VFS */ SET_OUT_HEADER_LEN(out, statfs); }))); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); /* Remount read-only */ ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); newflags = statbuf.f_flags | MNT_UPDATE | MNT_RDONLY; build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1); build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1); build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1); ASSERT_EQ(0, nmount(iov, iovlen, newflags)) << strerror(errno); EXPECT_EQ(EROFS, posix_fallocate(fd, offset, length)); leak(fd); } TEST_F(PosixFallocate, ok) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; struct stat sb0, sb1; uint64_t ino = 42; uint64_t offset = 0; uint64_t length = 1000; int fd; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | 0644; out.body.entry.nodeid = ino; out.body.entry.entry_valid = UINT64_MAX; out.body.entry.attr_valid = UINT64_MAX; }))); expect_open(ino, 0, 1); expect_fallocate(ino, offset, length, 0, 0); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb0)) << strerror(errno); EXPECT_EQ(0, posix_fallocate(fd, offset, length)); /* * Despite the originally cached file size of zero, stat should now * return either the new size or requery the daemon. */ EXPECT_EQ(0, stat(FULLPATH, &sb1)); EXPECT_EQ(length, (uint64_t)sb1.st_size); /* mtime and ctime should be updated */ EXPECT_EQ(sb0.st_atime, sb1.st_atime); EXPECT_NE(sb0.st_mtime, sb1.st_mtime); EXPECT_NE(sb0.st_ctime, sb1.st_ctime); leak(fd); } /* fusefs should respect RLIMIT_FSIZE */ TEST_F(PosixFallocate, rlimit_fsize) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; struct rlimit rl; uint64_t ino = 42; uint64_t offset = 0; uint64_t length = 1'000'000; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); expect_open(ino, 0, 1); rl.rlim_cur = length / 2; rl.rlim_max = 10 * length; ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); EXPECT_EQ(EFBIG, posix_fallocate(fd, offset, length)); EXPECT_EQ(1, s_sigxfsz); leak(fd); } /* With older servers, no FUSE_FALLOCATE should be attempted */ TEST_F(PosixFallocate_7_18, einval) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; uint64_t offset = 0; uint64_t length = 1000; int fd; expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); expect_open(ino, 0, 1); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); EXPECT_EQ(EINVAL, posix_fallocate(fd, offset, length)); leak(fd); } diff --git a/tests/sys/fs/fusefs/io.cc b/tests/sys/fs/fusefs/io.cc index 283b601c9e87..99b5eae34e09 100644 --- a/tests/sys/fs/fusefs/io.cc +++ b/tests/sys/fs/fusefs/io.cc @@ -1,612 +1,608 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ extern "C" { #include #include #include #include #include #include } #include "mockfs.hh" #include "utils.hh" /* * For testing I/O like fsx does, but deterministically and without a real * underlying file system */ using namespace testing; const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; static void compare(const void *tbuf, const void *controlbuf, off_t baseofs, ssize_t size) { int i; for (i = 0; i < size; i++) { if (((const char*)tbuf)[i] != ((const char*)controlbuf)[i]) { off_t ofs = baseofs + i; FAIL() << "miscompare at offset " << std::hex << std::showbase << ofs << ". expected = " << std::setw(2) << (unsigned)((const uint8_t*)controlbuf)[i] << " got = " << (unsigned)((const uint8_t*)tbuf)[i]; } } } typedef tuple IoParam; class Io: public FuseTest, public WithParamInterface { public: int m_backing_fd, m_control_fd, m_test_fd; off_t m_filesize; bool m_direct_io; Io(): m_backing_fd(-1), m_control_fd(-1), m_test_fd(-1), m_filesize(0), m_direct_io(false) {}; void SetUp() { m_backing_fd = open("backing_file", O_RDWR | O_CREAT | O_TRUNC, 0644); if (m_backing_fd < 0) FAIL() << strerror(errno); m_control_fd = open("control", O_RDWR | O_CREAT | O_TRUNC, 0644); if (m_control_fd < 0) FAIL() << strerror(errno); srandom(22'9'1982); // Seed with my birthday if (get<0>(GetParam())) m_init_flags |= FUSE_ASYNC_READ; m_maxwrite = get<1>(GetParam()); switch (get<2>(GetParam())) { case Uncached: m_direct_io = true; break; case WritebackAsync: m_async = true; /* FALLTHROUGH */ case Writeback: m_init_flags |= FUSE_WRITEBACK_CACHE; /* FALLTHROUGH */ case Writethrough: break; default: FAIL() << "Unknown cache mode"; } m_kernel_minor_version = get<3>(GetParam()); m_noatime = true; // To prevent SETATTR for atime on close FuseTest::SetUp(); if (IsSkipped()) return; if (verbosity > 0) { printf("Test Parameters: init_flags=%#x maxwrite=%#x " "%sasync cache=%s kernel_minor_version=%d\n", m_init_flags, m_maxwrite, m_async? "" : "no", cache_mode_to_s(get<2>(GetParam())), m_kernel_minor_version); } expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); expect_open(ino, m_direct_io ? FOPEN_DIRECT_IO : 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_WRITE && in.header.nodeid == ino); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) { const char *buf = (const char*)in.body.bytes + sizeof(struct fuse_write_in); ssize_t isize = in.body.write.size; off_t iofs = in.body.write.offset; assert((size_t)isize <= sizeof(in.body.bytes) - sizeof(struct fuse_write_in)); ASSERT_EQ(isize, pwrite(m_backing_fd, buf, isize, iofs)) << strerror(errno); SET_OUT_HEADER_LEN(out, write); out.body.write.size = isize; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ && in.header.nodeid == ino); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) { ssize_t isize = in.body.write.size; off_t iofs = in.body.write.offset; void *buf = out.body.bytes; ssize_t osize; assert((size_t)isize <= sizeof(out.body.bytes)); osize = pread(m_backing_fd, buf, isize, iofs); ASSERT_LE(0, osize) << strerror(errno); out.header.len = sizeof(struct fuse_out_header) + osize; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && (in.body.setattr.valid & FATTR_SIZE)); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) { ASSERT_EQ(0, ftruncate(m_backing_fd, in.body.setattr.size)) << strerror(errno); SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = S_IFREG | 0755; out.body.attr.attr.size = in.body.setattr.size; out.body.attr.attr_valid = UINT64_MAX; }))); /* Any test that close()s will send FUSE_FLUSH and FUSE_RELEASE */ EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_FLUSH && in.header.nodeid == ino); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnErrno(0))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_RELEASE && in.header.nodeid == ino); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnErrno(0))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_COPY_FILE_RANGE && in.header.nodeid == ino && in.body.copy_file_range.nodeid_out == ino && in.body.copy_file_range.flags == 0); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto& out) { off_t off_in = in.body.copy_file_range.off_in; off_t off_out = in.body.copy_file_range.off_out; ASSERT_EQ((ssize_t)in.body.copy_file_range.len, copy_file_range(m_backing_fd, &off_in, m_backing_fd, &off_out, in.body.copy_file_range.len, 0)); SET_OUT_HEADER_LEN(out, write); out.body.write.size = in.body.copy_file_range.len; }))); /* Claim that we don't support FUSE_LSEEK */ EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_LSEEK); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnErrno(ENOSYS))); m_test_fd = open(FULLPATH, O_RDWR ); EXPECT_LE(0, m_test_fd) << strerror(errno); } void TearDown() { if (m_test_fd >= 0) close(m_test_fd); if (m_backing_fd >= 0) close(m_backing_fd); if (m_control_fd >= 0) close(m_control_fd); FuseTest::TearDown(); leak(m_test_fd); } void do_closeopen() { ASSERT_EQ(0, close(m_test_fd)) << strerror(errno); m_test_fd = open("backing_file", O_RDWR); ASSERT_LE(0, m_test_fd) << strerror(errno); ASSERT_EQ(0, close(m_control_fd)) << strerror(errno); m_control_fd = open("control", O_RDWR); ASSERT_LE(0, m_control_fd) << strerror(errno); } void do_copy_file_range(off_t off_in, off_t off_out, size_t size) { ssize_t r; off_t test_off_in = off_in; off_t test_off_out = off_out; off_t test_size = size; off_t control_off_in = off_in; off_t control_off_out = off_out; off_t control_size = size; while (test_size > 0) { r = copy_file_range(m_test_fd, &test_off_in, m_test_fd, &test_off_out, test_size, 0); ASSERT_GT(r, 0) << strerror(errno); test_size -= r; } while (control_size > 0) { r = copy_file_range(m_control_fd, &control_off_in, m_control_fd, &control_off_out, control_size, 0); ASSERT_GT(r, 0) << strerror(errno); control_size -= r; } m_filesize = std::max(m_filesize, off_out + (off_t)size); } void do_ftruncate(off_t offs) { ASSERT_EQ(0, ftruncate(m_test_fd, offs)) << strerror(errno); ASSERT_EQ(0, ftruncate(m_control_fd, offs)) << strerror(errno); m_filesize = offs; } void do_mapread(off_t offs, ssize_t size) { - void *control_buf, *p; + char *control_buf; + void *p; off_t pg_offset, page_mask; size_t map_size; page_mask = getpagesize() - 1; pg_offset = offs & page_mask; map_size = pg_offset + size; p = mmap(NULL, map_size, PROT_READ, MAP_FILE | MAP_SHARED, m_test_fd, offs - pg_offset); ASSERT_NE(p, MAP_FAILED) << strerror(errno); - control_buf = malloc(size); - ASSERT_NE(nullptr, control_buf) << strerror(errno); + control_buf = new char[size]; ASSERT_EQ(size, pread(m_control_fd, control_buf, size, offs)) << strerror(errno); compare((void*)((char*)p + pg_offset), control_buf, offs, size); ASSERT_EQ(0, munmap(p, map_size)) << strerror(errno); - free(control_buf); + delete[] control_buf; } void do_read(off_t offs, ssize_t size) { - void *test_buf, *control_buf; + char *test_buf, *control_buf; ssize_t r; - test_buf = malloc(size); - ASSERT_NE(nullptr, test_buf) << strerror(errno); - control_buf = malloc(size); - ASSERT_NE(nullptr, control_buf) << strerror(errno); + test_buf = new char[size]; + control_buf = new char[size]; errno = 0; r = pread(m_test_fd, test_buf, size, offs); ASSERT_NE(-1, r) << strerror(errno); ASSERT_EQ(size, r) << "unexpected short read"; r = pread(m_control_fd, control_buf, size, offs); ASSERT_NE(-1, r) << strerror(errno); ASSERT_EQ(size, r) << "unexpected short read"; compare(test_buf, control_buf, offs, size); - free(control_buf); - free(test_buf); + delete[] control_buf; + delete[] test_buf; } void do_mapwrite(off_t offs, ssize_t size) { char *buf; void *p; off_t pg_offset, page_mask; size_t map_size; long i; page_mask = getpagesize() - 1; pg_offset = offs & page_mask; map_size = pg_offset + size; - buf = (char*)malloc(size); - ASSERT_NE(nullptr, buf) << strerror(errno); + buf = new char[size]; for (i=0; i < size; i++) buf[i] = random(); if (offs + size > m_filesize) { /* * Must manually extend. vm_mmap_vnode will not implicitly * extend a vnode */ do_ftruncate(offs + size); } p = mmap(NULL, map_size, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, m_test_fd, offs - pg_offset); ASSERT_NE(p, MAP_FAILED) << strerror(errno); bcopy(buf, (char*)p + pg_offset, size); ASSERT_EQ(size, pwrite(m_control_fd, buf, size, offs)) << strerror(errno); - free(buf); + delete[] buf; ASSERT_EQ(0, munmap(p, map_size)) << strerror(errno); } void do_write(off_t offs, ssize_t size) { char *buf; long i; - buf = (char*)malloc(size); - ASSERT_NE(nullptr, buf) << strerror(errno); + buf = new char[size]; for (i=0; i < size; i++) buf[i] = random(); ASSERT_EQ(size, pwrite(m_test_fd, buf, size, offs )) << strerror(errno); ASSERT_EQ(size, pwrite(m_control_fd, buf, size, offs)) << strerror(errno); m_filesize = std::max(m_filesize, offs + size); - free(buf); + delete[] buf; } }; class IoCacheable: public Io { public: virtual void SetUp() { Io::SetUp(); } }; class IoCopyFileRange: public Io { public: virtual void SetUp() { Io::SetUp(); } }; /* * Extend a file with dirty data in the last page of the last block. * * fsx -WR -P /tmp -S8 -N3 fsx.bin */ TEST_P(Io, extend_from_dirty_page) { off_t wofs = 0x21a0; ssize_t wsize = 0xf0a8; off_t rofs = 0xb284; ssize_t rsize = 0x9b22; off_t truncsize = 0x28702; do_write(wofs, wsize); do_ftruncate(truncsize); do_read(rofs, rsize); } /* * mapwrite into a newly extended part of a file. * * fsx -c 100 -i 100 -l 524288 -o 131072 -N5 -P /tmp -S19 fsx.bin */ TEST_P(IoCacheable, extend_by_mapwrite) { do_mapwrite(0x29a3a, 0x849e); /* [0x29a3a, 0x31ed7] */ do_mapwrite(0x3c7d8, 0x3994); /* [0x3c7d8, 0x4016b] */ do_read(0x30c16, 0xf556); /* [0x30c16, 0x4016b] */ } /* * When writing the last page of a file, it must be written synchronously. * Otherwise the cached page can become invalid by a subsequent extend * operation. * * fsx -WR -P /tmp -S642 -N3 fsx.bin */ TEST_P(Io, last_page) { do_write(0x1134f, 0xcc77); /* [0x1134f, 0x1dfc5] */ do_write(0x2096a, 0xdfa7); /* [0x2096a, 0x2e910] */ do_read(0x1a3aa, 0xb5b7); /* [0x1a3aa, 0x25960] */ } /* * Read a hole using mmap * * fsx -c 100 -i 100 -l 524288 -o 131072 -N11 -P /tmp -S14 fsx.bin */ TEST_P(IoCacheable, mapread_hole) { do_write(0xf205, 0x123b7); /* [0xf205, 0x215bb] */ do_mapread(0x2f4c, 0xeeea); /* [0x2f4c, 0x11e35] */ } /* * Read a hole from a block that contains some cached data. * * fsx -WR -P /tmp -S55 fsx.bin */ TEST_P(Io, read_hole_from_cached_block) { off_t wofs = 0x160c5; ssize_t wsize = 0xa996; off_t rofs = 0x472e; ssize_t rsize = 0xd8d5; do_write(wofs, wsize); do_read(rofs, rsize); } /* * Truncating a file into a dirty buffer should not causing anything untoward * to happen when that buffer is eventually flushed. * * fsx -WR -P /tmp -S839 -d -N6 fsx.bin */ TEST_P(Io, truncate_into_dirty_buffer) { off_t wofs0 = 0x3bad7; ssize_t wsize0 = 0x4529; off_t wofs1 = 0xc30d; ssize_t wsize1 = 0x5f77; off_t truncsize0 = 0x10916; off_t rofs = 0xdf17; ssize_t rsize = 0x29ff; off_t truncsize1 = 0x152b4; do_write(wofs0, wsize0); do_write(wofs1, wsize1); do_ftruncate(truncsize0); do_read(rofs, rsize); do_ftruncate(truncsize1); close(m_test_fd); } /* * Truncating a file into a dirty buffer should not causing anything untoward * to happen when that buffer is eventually flushed, even when the buffer's * dirty_off is > 0. * * Based on this command with a few steps removed: * fsx -WR -P /tmp -S677 -d -N8 fsx.bin */ TEST_P(Io, truncate_into_dirty_buffer2) { off_t truncsize0 = 0x344f3; off_t wofs = 0x2790c; ssize_t wsize = 0xd86a; off_t truncsize1 = 0x2de38; off_t rofs2 = 0x1fd7a; ssize_t rsize2 = 0xc594; off_t truncsize2 = 0x31e71; /* Sets the file size to something larger than the next write */ do_ftruncate(truncsize0); /* * Creates a dirty buffer. The part in lbn 2 doesn't flush * synchronously. */ do_write(wofs, wsize); /* Truncates part of the dirty buffer created in step 2 */ do_ftruncate(truncsize1); /* XXX ?I don't know why this is necessary? */ do_read(rofs2, rsize2); /* Truncates the dirty buffer */ do_ftruncate(truncsize2); close(m_test_fd); } /* * Regression test for a bug introduced in r348931 * * Sequence of operations: * 1) The first write reads lbn so it can modify it * 2) The first write flushes lbn 3 immediately because it's the end of file * 3) The first write then flushes lbn 4 because it's the end of the file * 4) The second write modifies the cached versions of lbn 3 and 4 * 5) The third write's getblkx invalidates lbn 4's B_CACHE because it's * extending the buffer. Then it flushes lbn 4 because B_DELWRI was set but * B_CACHE was clear. * 6) fuse_write_biobackend erroneously called vfs_bio_clrbuf, putting the * buffer into a weird write-only state. All read operations would return * 0. Writes were apparently still processed, because the buffer's contents * were correct when examined in a core dump. * 7) The third write reads lbn 4 because cache is clear * 9) uiomove dutifully copies new data into the buffer * 10) The buffer's dirty is flushed to lbn 4 * 11) The read returns all zeros because of step 6. * * Based on: * fsx -WR -l 524388 -o 131072 -P /tmp -S6456 -q fsx.bin */ TEST_P(Io, resize_a_valid_buffer_while_extending) { do_write(0x36ee6, 0x14530); /* [0x36ee6, 0x4b415] */ do_write(0x33256, 0x1507c); /* [0x33256, 0x482d1] */ do_write(0x4c03d, 0x175c); /* [0x4c03d, 0x4d798] */ do_read(0x3599c, 0xe277); /* [0x3599c, 0x43c12] */ close(m_test_fd); } /* * mmap of a suitable region could trigger a panic. I'm not sure what * combination of size and offset counts as "suitable". Regression test for * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=276191 */ TEST_P(IoCacheable, vnode_pager_generic_putpage_clean_block_at_eof) { do_mapwrite(0x3b4e0, 0x1bbc3); } /* * A copy_file_range that follows an mmap write to the input area needs to * flush the mmap buffer first. */ TEST_P(IoCopyFileRange, copy_file_range_from_mapped_write) { do_mapwrite(0, 0x1000); do_copy_file_range(0, 0x1000, 0x1000); do_read(0x1000, 0x1000); } INSTANTIATE_TEST_SUITE_P(Io, Io, Combine(Bool(), /* async read */ Values(0x1000, 0x10000, 0x20000), /* m_maxwrite */ Values(Uncached, Writethrough, Writeback, WritebackAsync), Values(28) /* kernel_minor_vers */ ) ); INSTANTIATE_TEST_SUITE_P(Io, IoCacheable, Combine(Bool(), /* async read */ Values(0x1000, 0x10000, 0x20000), /* m_maxwrite */ Values(Writethrough, Writeback, WritebackAsync), Values(28) /* kernel_minor_vers */ ) ); INSTANTIATE_TEST_SUITE_P(Io, IoCopyFileRange, Combine(Values(true), /* async read */ Values(0x10000), /* m_maxwrite */ Values(Writethrough, Writeback, WritebackAsync), Values(27, 28) /* kernel_minor_vers */ ) ); diff --git a/tests/sys/fs/fusefs/read.cc b/tests/sys/fs/fusefs/read.cc index 3df0420facb9..373f742d4fd3 100644 --- a/tests/sys/fs/fusefs/read.cc +++ b/tests/sys/fs/fusefs/read.cc @@ -1,1446 +1,1444 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ extern "C" { #include #include #include #include #include #include #include #include #include #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; class Read: public FuseTest { public: void expect_lookup(const char *relpath, uint64_t ino, uint64_t size) { FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, size, 1); } }; class RofsRead: public Read { public: virtual void SetUp() { m_ro = true; Read::SetUp(); } }; class Read_7_8: public FuseTest { public: virtual void SetUp() { m_kernel_minor_version = 8; FuseTest::SetUp(); } void expect_lookup(const char *relpath, uint64_t ino, uint64_t size) { FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, size, 1); } }; class AioRead: public Read { public: virtual void SetUp() { if (!is_unsafe_aio_enabled()) GTEST_SKIP() << "vfs.aio.enable_unsafe must be set for this test"; FuseTest::SetUp(); } }; class AsyncRead: public AioRead { virtual void SetUp() { m_init_flags = FUSE_ASYNC_READ; AioRead::SetUp(); } }; class ReadAhead: public Read, public WithParamInterface> { virtual void SetUp() { int val; const char *node = "vfs.maxbcachebuf"; size_t size = sizeof(val); ASSERT_EQ(0, sysctlbyname(node, &val, &size, NULL, 0)) << strerror(errno); m_maxreadahead = val * get<1>(GetParam()); m_noclusterr = get<0>(GetParam()); Read::SetUp(); } }; class ReadNoatime: public Read { virtual void SetUp() { m_noatime = true; Read::SetUp(); } }; class ReadSigbus: public Read { public: static jmp_buf s_jmpbuf; static void *s_si_addr; void TearDown() { struct sigaction sa; bzero(&sa, sizeof(sa)); sa.sa_handler = SIG_DFL; sigaction(SIGBUS, &sa, NULL); FuseTest::TearDown(); } }; static void handle_sigbus(int signo __unused, siginfo_t *info, void *uap __unused) { ReadSigbus::s_si_addr = info->si_addr; longjmp(ReadSigbus::s_jmpbuf, 1); } jmp_buf ReadSigbus::s_jmpbuf; void *ReadSigbus::s_si_addr; class TimeGran: public Read, public WithParamInterface { public: virtual void SetUp() { m_time_gran = 1 << GetParam(); Read::SetUp(); } }; /* AIO reads need to set the header's pid field correctly */ /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */ TEST_F(AioRead, aio_read) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; struct aiocb iocb, *piocb; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); iocb.aio_nbytes = bufsize; iocb.aio_fildes = fd; iocb.aio_buf = buf; iocb.aio_offset = 0; iocb.aio_sigevent.sigev_notify = SIGEV_NONE; ASSERT_EQ(0, aio_read(&iocb)) << strerror(errno); ASSERT_EQ(bufsize, aio_waitcomplete(&piocb, NULL)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); leak(fd); } /* * Without the FUSE_ASYNC_READ mount option, fuse(4) should ensure that there * is at most one outstanding read operation per file handle */ TEST_F(AioRead, async_read_disabled) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; ssize_t bufsize = 50; char buf0[bufsize], buf1[bufsize]; off_t off0 = 0; off_t off1 = m_maxbcachebuf; struct aiocb iocb0, iocb1; volatile sig_atomic_t read_count = 0; expect_lookup(RELPATH, ino, 131072); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ && in.header.nodeid == ino && in.body.read.fh == FH && in.body.read.offset == (uint64_t)off0); }, Eq(true)), _) ).WillRepeatedly(Invoke([&](auto in __unused, auto &out __unused) { read_count++; /* Filesystem is slow to respond */ })); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ && in.header.nodeid == ino && in.body.read.fh == FH && in.body.read.offset == (uint64_t)off1); }, Eq(true)), _) ).WillRepeatedly(Invoke([&](auto in __unused, auto &out __unused) { read_count++; /* Filesystem is slow to respond */ })); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); /* * Submit two AIO read requests, and respond to neither. If the * filesystem ever gets the second read request, then we failed to * limit outstanding reads. */ iocb0.aio_nbytes = bufsize; iocb0.aio_fildes = fd; iocb0.aio_buf = buf0; iocb0.aio_offset = off0; iocb0.aio_sigevent.sigev_notify = SIGEV_NONE; ASSERT_EQ(0, aio_read(&iocb0)) << strerror(errno); iocb1.aio_nbytes = bufsize; iocb1.aio_fildes = fd; iocb1.aio_buf = buf1; iocb1.aio_offset = off1; iocb1.aio_sigevent.sigev_notify = SIGEV_NONE; ASSERT_EQ(0, aio_read(&iocb1)) << strerror(errno); /* * Sleep for awhile to make sure the kernel has had a chance to issue * the second read, even though the first has not yet returned */ nap(); EXPECT_EQ(read_count, 1); m_mock->kill_daemon(); /* Wait for AIO activity to complete, but ignore errors */ (void)aio_waitcomplete(NULL, NULL); leak(fd); } /* * With the FUSE_ASYNC_READ mount option, fuse(4) may issue multiple * simultaneous read requests on the same file handle. */ TEST_F(AsyncRead, async_read) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; ssize_t bufsize = 50; char buf0[bufsize], buf1[bufsize]; off_t off0 = 0; off_t off1 = m_maxbcachebuf; off_t fsize = 2 * m_maxbcachebuf; struct aiocb iocb0, iocb1; sem_t sem; ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); expect_lookup(RELPATH, ino, fsize); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ && in.header.nodeid == ino && in.body.read.fh == FH && in.body.read.offset == (uint64_t)off0); }, Eq(true)), _) ).WillOnce(Invoke([&](auto in __unused, auto &out __unused) { sem_post(&sem); /* Filesystem is slow to respond */ })); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ && in.header.nodeid == ino && in.body.read.fh == FH && in.body.read.offset == (uint64_t)off1); }, Eq(true)), _) ).WillOnce(Invoke([&](auto in __unused, auto &out __unused) { sem_post(&sem); /* Filesystem is slow to respond */ })); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); /* * Submit two AIO read requests, but respond to neither. Ensure that * we received both. */ iocb0.aio_nbytes = bufsize; iocb0.aio_fildes = fd; iocb0.aio_buf = buf0; iocb0.aio_offset = off0; iocb0.aio_sigevent.sigev_notify = SIGEV_NONE; ASSERT_EQ(0, aio_read(&iocb0)) << strerror(errno); iocb1.aio_nbytes = bufsize; iocb1.aio_fildes = fd; iocb1.aio_buf = buf1; iocb1.aio_offset = off1; iocb1.aio_sigevent.sigev_notify = SIGEV_NONE; ASSERT_EQ(0, aio_read(&iocb1)) << strerror(errno); /* Wait until both reads have reached the daemon */ ASSERT_EQ(0, sem_wait(&sem)) << strerror(errno); ASSERT_EQ(0, sem_wait(&sem)) << strerror(errno); m_mock->kill_daemon(); /* Wait for AIO activity to complete, but ignore errors */ (void)aio_waitcomplete(NULL, NULL); leak(fd); } /* The kernel should update the cached atime attribute during a read */ TEST_F(Read, atime) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; struct stat sb1, sb2; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb1)); /* Ensure atime will be different than it was during lookup */ nap(); ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb2)); /* The kernel should automatically update atime during read */ EXPECT_TRUE(timespeccmp(&sb1.st_atim, &sb2.st_atim, <)); EXPECT_TRUE(timespeccmp(&sb1.st_ctim, &sb2.st_ctim, ==)); EXPECT_TRUE(timespeccmp(&sb1.st_mtim, &sb2.st_mtim, ==)); leak(fd); } /* The kernel should update the cached atime attribute during a cached read */ TEST_F(Read, atime_cached) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; struct stat sb1, sb2; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, pread(fd, buf, bufsize, 0)) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb1)); /* Ensure atime will be different than it was during the first read */ nap(); ASSERT_EQ(bufsize, pread(fd, buf, bufsize, 0)) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb2)); /* The kernel should automatically update atime during read */ EXPECT_TRUE(timespeccmp(&sb1.st_atim, &sb2.st_atim, <)); EXPECT_TRUE(timespeccmp(&sb1.st_ctim, &sb2.st_ctim, ==)); EXPECT_TRUE(timespeccmp(&sb1.st_mtim, &sb2.st_mtim, ==)); leak(fd); } /* dirty atime values should be flushed during close */ TEST_F(Read, atime_during_close) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; struct stat sb; uint64_t ino = 42; const mode_t newmode = 0755; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); EXPECT_CALL(*m_mock, process( ResultOf([&](auto in) { uint32_t valid = FATTR_ATIME; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid && (time_t)in.body.setattr.atime == sb.st_atim.tv_sec && (long)in.body.setattr.atimensec == sb.st_atim.tv_nsec); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = S_IFREG | newmode; }))); expect_flush(ino, 1, ReturnErrno(0)); expect_release(ino, FuseTest::FH); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); /* Ensure atime will be different than during lookup */ nap(); ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb)); close(fd); } /* * When not using -o default_permissions, the daemon may make its own decisions * regarding access permissions, and these may be unpredictable. If it rejects * our attempt to set atime, that should not cause close(2) to fail. */ TEST_F(Read, atime_during_close_eacces) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); EXPECT_CALL(*m_mock, process( ResultOf([&](auto in) { uint32_t valid = FATTR_ATIME; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(EACCES))); expect_flush(ino, 1, ReturnErrno(0)); expect_release(ino, FuseTest::FH); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); /* Ensure atime will be different than during lookup */ nap(); ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, close(fd)); } /* A cached atime should be flushed during FUSE_SETATTR */ TEST_F(Read, atime_during_setattr) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; struct stat sb; uint64_t ino = 42; const mode_t newmode = 0755; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); EXPECT_CALL(*m_mock, process( ResultOf([&](auto in) { uint32_t valid = FATTR_MODE | FATTR_ATIME; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid && (time_t)in.body.setattr.atime == sb.st_atim.tv_sec && (long)in.body.setattr.atimensec == sb.st_atim.tv_nsec); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = S_IFREG | newmode; }))); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); /* Ensure atime will be different than during lookup */ nap(); ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb)); ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno); leak(fd); } /* 0-length reads shouldn't cause any confusion */ TEST_F(Read, direct_io_read_nothing) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; uint64_t offset = 100; char buf[80]; expect_lookup(RELPATH, ino, offset + 1000); expect_open(ino, FOPEN_DIRECT_IO, 1); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, pread(fd, buf, 0, offset)) << strerror(errno); leak(fd); } /* * With direct_io, reads should not fill the cache. They should go straight to * the daemon */ TEST_F(Read, direct_io_pread) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; uint64_t offset = 100; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; expect_lookup(RELPATH, ino, offset + bufsize); expect_open(ino, FOPEN_DIRECT_IO, 1); expect_read(ino, offset, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, pread(fd, buf, bufsize, offset)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); // With FOPEN_DIRECT_IO, the cache should be bypassed. The server will // get a 2nd read request. expect_read(ino, offset, bufsize, bufsize, CONTENTS); ASSERT_EQ(bufsize, pread(fd, buf, bufsize, offset)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); leak(fd); } /* * With direct_io, filesystems are allowed to return less data than is * requested. fuse(4) should return a short read to userland. */ TEST_F(Read, direct_io_short_read) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefghijklmnop"; uint64_t ino = 42; int fd; uint64_t offset = 100; ssize_t bufsize = strlen(CONTENTS); ssize_t halfbufsize = bufsize / 2; uint8_t buf[bufsize]; expect_lookup(RELPATH, ino, offset + bufsize); expect_open(ino, FOPEN_DIRECT_IO, 1); expect_read(ino, offset, bufsize, halfbufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(halfbufsize, pread(fd, buf, bufsize, offset)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, halfbufsize)); leak(fd); } TEST_F(Read, eio) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(EIO))); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(-1, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(EIO, errno); leak(fd); } /* * If the server returns a short read when direct io is not in use, that * indicates EOF, because of a server-side truncation. We should invalidate * all cached attributes. We may update the file size, */ TEST_F(Read, eof) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefghijklmnop"; uint64_t ino = 42; int fd; uint64_t offset = 100; ssize_t bufsize = strlen(CONTENTS); ssize_t partbufsize = 3 * bufsize / 4; ssize_t r; uint8_t buf[bufsize]; struct stat sb; expect_lookup(RELPATH, ino, offset + bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, offset + bufsize, offset + partbufsize, CONTENTS); expect_getattr(ino, offset + partbufsize); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); r = pread(fd, buf, bufsize, offset); ASSERT_LE(0, r) << strerror(errno); EXPECT_EQ(partbufsize, r) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb)); EXPECT_EQ((off_t)(offset + partbufsize), sb.st_size); leak(fd); } /* Like Read.eof, but causes an entire buffer to be invalidated */ TEST_F(Read, eof_of_whole_buffer) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefghijklmnop"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); off_t old_filesize = m_maxbcachebuf * 2 + bufsize; uint8_t buf[bufsize]; struct stat sb; expect_lookup(RELPATH, ino, old_filesize); expect_open(ino, 0, 1); expect_read(ino, 2 * m_maxbcachebuf, bufsize, bufsize, CONTENTS); expect_read(ino, m_maxbcachebuf, m_maxbcachebuf, 0, CONTENTS); expect_getattr(ino, m_maxbcachebuf); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); /* Cache the third block */ ASSERT_EQ(bufsize, pread(fd, buf, bufsize, m_maxbcachebuf * 2)) << strerror(errno); /* Try to read the 2nd block, but it's past EOF */ ASSERT_EQ(0, pread(fd, buf, bufsize, m_maxbcachebuf)) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb)); EXPECT_EQ((off_t)(m_maxbcachebuf), sb.st_size); leak(fd); } /* * With the keep_cache option, the kernel may keep its read cache across * multiple open(2)s. */ TEST_F(Read, keep_cache) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd0, fd1; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, bufsize, 2); expect_open(ino, FOPEN_KEEP_CACHE, 2); expect_read(ino, 0, bufsize, bufsize, CONTENTS); fd0 = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd0) << strerror(errno); ASSERT_EQ(bufsize, read(fd0, buf, bufsize)) << strerror(errno); fd1 = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd1) << strerror(errno); /* * This read should be serviced by cache, even though it's on the other * file descriptor */ ASSERT_EQ(bufsize, read(fd1, buf, bufsize)) << strerror(errno); leak(fd0); leak(fd1); } /* * Without the keep_cache option, the kernel should drop its read caches on * every open */ TEST_F(Read, keep_cache_disabled) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd0, fd1; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, bufsize, 2); expect_open(ino, 0, 2); expect_read(ino, 0, bufsize, bufsize, CONTENTS); fd0 = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd0) << strerror(errno); ASSERT_EQ(bufsize, read(fd0, buf, bufsize)) << strerror(errno); fd1 = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd1) << strerror(errno); /* * This read should not be serviced by cache, even though it's on the * original file descriptor */ expect_read(ino, 0, bufsize, bufsize, CONTENTS); ASSERT_EQ(0, lseek(fd0, 0, SEEK_SET)) << strerror(errno); ASSERT_EQ(bufsize, read(fd0, buf, bufsize)) << strerror(errno); leak(fd0); leak(fd1); } TEST_F(Read, mmap) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t len; size_t bufsize = strlen(CONTENTS); void *p; len = getpagesize(); expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ && in.header.nodeid == ino && in.body.read.fh == Read::FH && in.body.read.offset == 0 && in.body.read.size == bufsize); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { out.header.len = sizeof(struct fuse_out_header) + bufsize; memmove(out.body.bytes, CONTENTS, bufsize); }))); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0); ASSERT_NE(MAP_FAILED, p) << strerror(errno); ASSERT_EQ(0, memcmp(p, CONTENTS, bufsize)); ASSERT_EQ(0, munmap(p, len)) << strerror(errno); leak(fd); } /* * The kernel should not update the cached atime attribute during a read, if * MNT_NOATIME is used. */ TEST_F(ReadNoatime, atime) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; struct stat sb1, sb2; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb1)); nap(); ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb2)); /* The kernel should not update atime during read */ EXPECT_TRUE(timespeccmp(&sb1.st_atim, &sb2.st_atim, ==)); EXPECT_TRUE(timespeccmp(&sb1.st_ctim, &sb2.st_ctim, ==)); EXPECT_TRUE(timespeccmp(&sb1.st_mtim, &sb2.st_mtim, ==)); leak(fd); } /* * The kernel should not update the cached atime attribute during a cached * read, if MNT_NOATIME is used. */ TEST_F(ReadNoatime, atime_cached) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; struct stat sb1, sb2; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, pread(fd, buf, bufsize, 0)) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb1)); nap(); ASSERT_EQ(bufsize, pread(fd, buf, bufsize, 0)) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb2)); /* The kernel should automatically update atime during read */ EXPECT_TRUE(timespeccmp(&sb1.st_atim, &sb2.st_atim, ==)); EXPECT_TRUE(timespeccmp(&sb1.st_ctim, &sb2.st_ctim, ==)); EXPECT_TRUE(timespeccmp(&sb1.st_mtim, &sb2.st_mtim, ==)); leak(fd); } /* Read of an mmap()ed file fails */ TEST_F(ReadSigbus, mmap_eio) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; struct sigaction sa; uint64_t ino = 42; int fd; ssize_t len; size_t bufsize = strlen(CONTENTS); void *p; len = getpagesize(); expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ && in.header.nodeid == ino && in.body.read.fh == Read::FH); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnErrno(EIO))); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0); ASSERT_NE(MAP_FAILED, p) << strerror(errno); /* Accessing the mapped page should return SIGBUS. */ bzero(&sa, sizeof(sa)); sa.sa_handler = SIG_DFL; sa.sa_sigaction = handle_sigbus; sa.sa_flags = SA_RESETHAND | SA_SIGINFO; ASSERT_EQ(0, sigaction(SIGBUS, &sa, NULL)) << strerror(errno); if (setjmp(ReadSigbus::s_jmpbuf) == 0) { atomic_signal_fence(std::memory_order::memory_order_seq_cst); volatile char x __unused = *(volatile char*)p; FAIL() << "shouldn't get here"; } ASSERT_EQ(p, ReadSigbus::s_si_addr); ASSERT_EQ(0, munmap(p, len)) << strerror(errno); leak(fd); } /* * A read via mmap comes up short, indicating that the file was truncated * server-side. */ TEST_F(Read, mmap_eof) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t len; size_t bufsize = strlen(CONTENTS); struct stat sb; void *p; len = getpagesize(); expect_lookup(RELPATH, ino, m_maxbcachebuf); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ && in.header.nodeid == ino && in.body.read.fh == Read::FH && in.body.read.offset == 0 && in.body.read.size == (uint32_t)m_maxbcachebuf); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { out.header.len = sizeof(struct fuse_out_header) + bufsize; memmove(out.body.bytes, CONTENTS, bufsize); }))); expect_getattr(ino, bufsize); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0); ASSERT_NE(MAP_FAILED, p) << strerror(errno); /* The file size should be automatically truncated */ ASSERT_EQ(0, memcmp(p, CONTENTS, bufsize)); ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); EXPECT_EQ((off_t)bufsize, sb.st_size); ASSERT_EQ(0, munmap(p, len)) << strerror(errno); leak(fd); } /* * During VOP_GETPAGES, the FUSE server fails a FUSE_GETATTR operation. This * almost certainly indicates a buggy FUSE server, and our goal should be not * to panic. Instead, generate SIGBUS. */ TEST_F(ReadSigbus, mmap_getblksz_fail) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; struct sigaction sa; Sequence seq; uint64_t ino = 42; int fd; ssize_t len; size_t bufsize = strlen(CONTENTS); mode_t mode = S_IFREG | 0644; void *p; len = getpagesize(); FuseTest::expect_lookup(RELPATH, ino, mode, bufsize, 1, 0); /* Expect two GETATTR calls that succeed, followed by one that fail. */ EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_GETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).Times(2) .InSequence(seq) .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = mode; out.body.attr.attr.size = bufsize; out.body.attr.attr_valid = 0; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_GETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).InSequence(seq) .WillRepeatedly(Invoke(ReturnErrno(EIO))); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ); }, Eq(true)), _) ).Times(0); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0); ASSERT_NE(MAP_FAILED, p) << strerror(errno); /* Accessing the mapped page should return SIGBUS. */ bzero(&sa, sizeof(sa)); sa.sa_handler = SIG_DFL; sa.sa_sigaction = handle_sigbus; sa.sa_flags = SA_RESETHAND | SA_SIGINFO; ASSERT_EQ(0, sigaction(SIGBUS, &sa, NULL)) << strerror(errno); if (setjmp(ReadSigbus::s_jmpbuf) == 0) { atomic_signal_fence(std::memory_order::memory_order_seq_cst); volatile char x __unused = *(volatile char*)p; FAIL() << "shouldn't get here"; } ASSERT_EQ(p, ReadSigbus::s_si_addr); ASSERT_EQ(0, munmap(p, len)) << strerror(errno); leak(fd); } /* * Just as when FOPEN_DIRECT_IO is used, reads with O_DIRECT should bypass * cache and to straight to the daemon */ TEST_F(Read, o_direct) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); // Fill the cache ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); // Reads with o_direct should bypass the cache expect_read(ino, 0, bufsize, bufsize, CONTENTS); ASSERT_EQ(0, fcntl(fd, F_SETFL, O_DIRECT)) << strerror(errno); ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); leak(fd); } TEST_F(Read, pread) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; /* * Set offset to a maxbcachebuf boundary so we'll be sure what offset * to read from. Without this, the read might start at a lower offset. */ uint64_t offset = m_maxbcachebuf; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; expect_lookup(RELPATH, ino, offset + bufsize); expect_open(ino, 0, 1); expect_read(ino, offset, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, pread(fd, buf, bufsize, offset)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); leak(fd); } TEST_F(Read, read) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); leak(fd); } TEST_F(Read_7_8, read) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); leak(fd); } /* * If cacheing is enabled, the kernel should try to read an entire cache block * at a time. */ TEST_F(Read, cache_block) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS0 = "abcdefghijklmnop"; uint64_t ino = 42; int fd; ssize_t bufsize = 8; ssize_t filesize = m_maxbcachebuf * 2; char *contents; char buf[bufsize]; const char *contents1 = CONTENTS0 + bufsize; - contents = (char*)calloc(1, filesize); - ASSERT_NE(nullptr, contents); + contents = new char[filesize](); memmove(contents, CONTENTS0, strlen(CONTENTS0)); expect_lookup(RELPATH, ino, filesize); expect_open(ino, 0, 1); expect_read(ino, 0, m_maxbcachebuf, m_maxbcachebuf, contents); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS0, bufsize)); /* A subsequent read should be serviced by cache */ ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, contents1, bufsize)); leak(fd); - free(contents); + delete[] contents; } /* Reading with sendfile should work (though it obviously won't be 0-copy) */ TEST_F(Read, sendfile) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; size_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; int sp[2]; off_t sbytes; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ && in.header.nodeid == ino && in.body.read.fh == Read::FH && in.body.read.offset == 0 && in.body.read.size == bufsize); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { out.header.len = sizeof(struct fuse_out_header) + bufsize; memmove(out.body.bytes, CONTENTS, bufsize); }))); ASSERT_EQ(0, socketpair(PF_LOCAL, SOCK_STREAM, 0, sp)) << strerror(errno); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, sendfile(fd, sp[1], 0, bufsize, NULL, &sbytes, 0)) << strerror(errno); ASSERT_EQ(static_cast(bufsize), read(sp[0], buf, bufsize)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); close(sp[1]); close(sp[0]); leak(fd); } /* sendfile should fail gracefully if fuse declines the read */ /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236466 */ TEST_F(Read, sendfile_eio) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); int sp[2]; off_t sbytes; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(EIO))); ASSERT_EQ(0, socketpair(PF_LOCAL, SOCK_STREAM, 0, sp)) << strerror(errno); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_NE(0, sendfile(fd, sp[1], 0, bufsize, NULL, &sbytes, 0)); close(sp[1]); close(sp[0]); leak(fd); } /* * Sequential reads should use readahead. And if allowed, large reads should * be clustered. */ TEST_P(ReadAhead, readahead) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd, maxcontig, clustersize; ssize_t bufsize = 4 * m_maxbcachebuf; ssize_t filesize = bufsize; uint64_t len; char *rbuf, *contents; off_t offs; - contents = (char*)malloc(filesize); - ASSERT_NE(nullptr, contents); + contents = new char[filesize]; memset(contents, 'X', filesize); - rbuf = (char*)calloc(1, bufsize); + rbuf = new char[bufsize](); expect_lookup(RELPATH, ino, filesize); expect_open(ino, 0, 1); maxcontig = m_noclusterr ? m_maxbcachebuf : m_maxbcachebuf + m_maxreadahead; clustersize = MIN(maxcontig, m_maxphys); for (offs = 0; offs < bufsize; offs += clustersize) { len = std::min((size_t)clustersize, (size_t)(filesize - offs)); expect_read(ino, offs, len, len, contents + offs); } fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); /* Set the internal readahead counter to a "large" value */ ASSERT_EQ(0, fcntl(fd, F_READAHEAD, 1'000'000'000)) << strerror(errno); ASSERT_EQ(bufsize, read(fd, rbuf, bufsize)) << strerror(errno); ASSERT_EQ(0, memcmp(rbuf, contents, bufsize)); leak(fd); - free(rbuf); - free(contents); + delete[] rbuf; + delete[] contents; } INSTANTIATE_TEST_SUITE_P(RA, ReadAhead, Values(tuple(false, 0), tuple(false, 1), tuple(false, 2), tuple(false, 3), tuple(true, 0), tuple(true, 1), tuple(true, 2))); /* With read-only mounts, fuse should never update atime during close */ TEST_F(RofsRead, atime_during_close) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); EXPECT_CALL(*m_mock, process( ResultOf([&](auto in) { return (in.header.opcode == FUSE_SETATTR); }, Eq(true)), _) ).Times(0); expect_flush(ino, 1, ReturnErrno(0)); expect_release(ino, FuseTest::FH); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); /* Ensure atime will be different than during lookup */ nap(); ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); close(fd); } /* fuse_init_out.time_gran controls the granularity of timestamps */ TEST_P(TimeGran, atime_during_setattr) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; ssize_t bufsize = strlen(CONTENTS); uint8_t buf[bufsize]; uint64_t ino = 42; const mode_t newmode = 0755; int fd; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { uint32_t valid = FATTR_MODE | FATTR_ATIME; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid && in.body.setattr.atimensec % m_time_gran == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = S_IFREG | newmode; }))); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno); leak(fd); } INSTANTIATE_TEST_SUITE_P(TG, TimeGran, Range(0u, 10u)); diff --git a/tests/sys/fs/fusefs/setattr.cc b/tests/sys/fs/fusefs/setattr.cc index 2502286c3f03..79559db33b12 100644 --- a/tests/sys/fs/fusefs/setattr.cc +++ b/tests/sys/fs/fusefs/setattr.cc @@ -1,867 +1,862 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ extern "C" { #include #include #include #include #include #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; class Setattr : public FuseTest { public: static sig_atomic_t s_sigxfsz; }; class RofsSetattr: public Setattr { public: virtual void SetUp() { s_sigxfsz = 0; m_ro = true; Setattr::SetUp(); } }; class Setattr_7_8: public Setattr { public: virtual void SetUp() { m_kernel_minor_version = 8; Setattr::SetUp(); } }; sig_atomic_t Setattr::s_sigxfsz = 0; void sigxfsz_handler(int __unused sig) { Setattr::s_sigxfsz = 1; } /* * If setattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs * should use the cached attributes, rather than query the daemon */ TEST_F(Setattr, attr_cache) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; struct stat sb; const mode_t newmode = 0644; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | 0644; out.body.entry.nodeid = ino; out.body.entry.entry_valid = UINT64_MAX; }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid out.body.attr.attr.mode = S_IFREG | newmode; out.body.attr.attr_valid = UINT64_MAX; }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_GETATTR); }, Eq(true)), _) ).Times(0); /* Set an attribute with SETATTR */ ASSERT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); /* The stat(2) should use cached attributes */ ASSERT_EQ(0, stat(FULLPATH, &sb)); EXPECT_EQ(S_IFREG | newmode, sb.st_mode); } /* Change the mode of a file */ TEST_F(Setattr, chmod) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const mode_t oldmode = 0755; const mode_t newmode = 0644; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | oldmode; out.body.entry.nodeid = ino; }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { uint32_t valid = FATTR_MODE; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid && in.body.setattr.mode == newmode); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid out.body.attr.attr.mode = S_IFREG | newmode; }))); EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); } /* * Chmod a multiply-linked file with cached attributes. Check that both files' * attributes have changed. */ TEST_F(Setattr, chmod_multiply_linked) { const char FULLPATH0[] = "mountpoint/some_file.txt"; const char RELPATH0[] = "some_file.txt"; const char FULLPATH1[] = "mountpoint/other_file.txt"; const char RELPATH1[] = "other_file.txt"; struct stat sb; const uint64_t ino = 42; const mode_t oldmode = 0777; const mode_t newmode = 0666; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH0) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | oldmode; out.body.entry.nodeid = ino; out.body.entry.attr.nlink = 2; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = UINT64_MAX; }))); EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH1) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | oldmode; out.body.entry.nodeid = ino; out.body.entry.attr.nlink = 2; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = UINT64_MAX; }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { uint32_t valid = FATTR_MODE; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid && in.body.setattr.mode == newmode); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = S_IFREG | newmode; out.body.attr.attr.nlink = 2; out.body.attr.attr_valid = UINT64_MAX; }))); /* For a lookup of the 2nd file to get it into the cache*/ ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno); EXPECT_EQ(S_IFREG | oldmode, sb.st_mode); ASSERT_EQ(0, chmod(FULLPATH0, newmode)) << strerror(errno); ASSERT_EQ(0, stat(FULLPATH0, &sb)) << strerror(errno); EXPECT_EQ(S_IFREG | newmode, sb.st_mode); ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno); EXPECT_EQ(S_IFREG | newmode, sb.st_mode); } /* Change the owner and group of a file */ TEST_F(Setattr, chown) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const gid_t oldgroup = 66; const gid_t newgroup = 99; const uid_t olduser = 33; const uid_t newuser = 44; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | 0644; out.body.entry.nodeid = ino; out.body.entry.attr.gid = oldgroup; out.body.entry.attr.uid = olduser; }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { uint32_t valid = FATTR_GID | FATTR_UID; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid && in.body.setattr.uid == newuser && in.body.setattr.gid == newgroup); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid out.body.attr.attr.mode = S_IFREG | 0644; out.body.attr.attr.uid = newuser; out.body.attr.attr.gid = newgroup; }))); EXPECT_EQ(0, chown(FULLPATH, newuser, newgroup)) << strerror(errno); } /* * FUSE daemons are allowed to check permissions however they like. If the * daemon returns EPERM, even if the file permissions "should" grant access, * then fuse(4) should return EPERM too. */ TEST_F(Setattr, eperm) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | 0777; out.body.entry.nodeid = ino; out.body.entry.attr.uid = in.header.uid; out.body.entry.attr.gid = in.header.gid; }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(EPERM))); EXPECT_NE(0, truncate(FULLPATH, 10)); EXPECT_EQ(EPERM, errno); } /* Change the mode of an open file, by its file descriptor */ TEST_F(Setattr, fchmod) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; const mode_t oldmode = 0755; const mode_t newmode = 0644; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | oldmode; out.body.entry.nodeid = ino; out.body.entry.attr_valid = UINT64_MAX; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_OPEN && in.header.nodeid == ino); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { out.header.len = sizeof(out.header); SET_OUT_HEADER_LEN(out, open); }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { uint32_t valid = FATTR_MODE; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid && in.body.setattr.mode == newmode); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid out.body.attr.attr.mode = S_IFREG | newmode; }))); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno); leak(fd); } /* Change the size of an open file, by its file descriptor */ TEST_F(Setattr, ftruncate) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; uint64_t fh = 0xdeadbeef1a7ebabe; const off_t oldsize = 99; const off_t newsize = 12345; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | 0755; out.body.entry.nodeid = ino; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.attr.size = oldsize; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_OPEN && in.header.nodeid == ino); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { out.header.len = sizeof(out.header); SET_OUT_HEADER_LEN(out, open); out.body.open.fh = fh; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { uint32_t valid = FATTR_SIZE | FATTR_FH; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid && in.body.setattr.fh == fh); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid out.body.attr.attr.mode = S_IFREG | 0755; out.body.attr.attr.size = newsize; }))); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, ftruncate(fd, newsize)) << strerror(errno); leak(fd); } /* Change the size of the file */ TEST_F(Setattr, truncate) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const uint64_t oldsize = 100'000'000; const uint64_t newsize = 20'000'000; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | 0644; out.body.entry.nodeid = ino; out.body.entry.attr.size = oldsize; }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { uint32_t valid = FATTR_SIZE; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid && in.body.setattr.size == newsize); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid out.body.attr.attr.mode = S_IFREG | 0644; out.body.attr.attr.size = newsize; }))); EXPECT_EQ(0, truncate(FULLPATH, newsize)) << strerror(errno); } /* * Truncating a file should discard cached data past the truncation point. * This is a regression test for bug 233783. * * There are two distinct failure modes. The first one is a failure to zero * the portion of the file's final buffer past EOF. It can be reproduced by * fsx -WR -P /tmp -S10 fsx.bin * * The second is a failure to drop buffers beyond that. It can be reproduced by * fsx -WR -P /tmp -S18 -n fsx.bin * Also reproducible in sh with: * $> /path/to/libfuse/build/example/passthrough -d /tmp/mnt * $> cd /tmp/mnt/tmp * $> dd if=/dev/random of=randfile bs=1k count=192 * $> truncate -s 1k randfile && truncate -s 192k randfile * $> xxd randfile | less # xxd will wrongly show random data at offset 0x8000 */ TEST_F(Setattr, truncate_discards_cached_data) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; - void *w0buf, *r0buf, *r1buf, *expected; + char *w0buf, *r0buf, *r1buf, *expected; off_t w0_offset = 0; size_t w0_size = 0x30000; off_t r0_offset = 0; off_t r0_size = w0_size; size_t trunc0_size = 0x400; size_t trunc1_size = w0_size; off_t r1_offset = trunc0_size; off_t r1_size = w0_size - trunc0_size; size_t cur_size = 0; const uint64_t ino = 42; mode_t mode = S_IFREG | 0644; int fd, r; bool should_have_data = false; - w0buf = malloc(w0_size); - ASSERT_NE(nullptr, w0buf) << strerror(errno); + w0buf = new char[w0_size]; memset(w0buf, 'X', w0_size); - r0buf = malloc(r0_size); - ASSERT_NE(nullptr, r0buf) << strerror(errno); - r1buf = malloc(r1_size); - ASSERT_NE(nullptr, r1buf) << strerror(errno); + r0buf = new char[r0_size]; + r1buf = new char[r1_size]; - expected = malloc(r1_size); - ASSERT_NE(nullptr, expected) << strerror(errno); - memset(expected, 0, r1_size); + expected = new char[r1_size](); expect_lookup(RELPATH, ino, mode, 0, 1); expect_open(ino, O_RDWR, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_GETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([&](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = mode; out.body.attr.attr.size = cur_size; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_WRITE); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.attr.attr.ino = ino; out.body.write.size = in.body.write.size; cur_size = std::max(static_cast(cur_size), in.body.write.size + in.body.write.offset); }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && (in.body.setattr.valid & FATTR_SIZE)); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) { auto trunc_size = in.body.setattr.size; SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = mode; out.body.attr.attr.size = trunc_size; cur_size = trunc_size; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) { auto osize = std::min( static_cast(cur_size) - in.body.read.offset, static_cast(in.body.read.size)); assert(osize <= sizeof(out.body.bytes)); out.header.len = sizeof(struct fuse_out_header) + osize; if (should_have_data) memset(out.body.bytes, 'X', osize); else bzero(out.body.bytes, osize); }))); fd = open(FULLPATH, O_RDWR, 0644); ASSERT_LE(0, fd) << strerror(errno); /* Fill the file with Xs */ ASSERT_EQ(static_cast(w0_size), pwrite(fd, w0buf, w0_size, w0_offset)); should_have_data = true; /* Fill the cache */ ASSERT_EQ(static_cast(r0_size), pread(fd, r0buf, r0_size, r0_offset)); /* 1st truncate should discard cached data */ EXPECT_EQ(0, ftruncate(fd, trunc0_size)) << strerror(errno); should_have_data = false; /* 2nd truncate extends file into previously cached data */ EXPECT_EQ(0, ftruncate(fd, trunc1_size)) << strerror(errno); /* Read should return all zeros */ ASSERT_EQ(static_cast(r1_size), pread(fd, r1buf, r1_size, r1_offset)); r = memcmp(expected, r1buf, r1_size); ASSERT_EQ(0, r); - free(expected); - free(r1buf); - free(r0buf); - free(w0buf); + delete[] expected; + delete[] r1buf; + delete[] r0buf; + delete[] w0buf; leak(fd); } /* truncate should fail if it would cause the file to exceed RLIMIT_FSIZE */ TEST_F(Setattr, truncate_rlimit_rsize) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; struct rlimit rl; const uint64_t ino = 42; const uint64_t oldsize = 0; const uint64_t newsize = 100'000'000; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | 0644; out.body.entry.nodeid = ino; out.body.entry.attr.size = oldsize; }))); rl.rlim_cur = newsize / 2; rl.rlim_max = 10 * newsize; ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); EXPECT_EQ(-1, truncate(FULLPATH, newsize)); EXPECT_EQ(EFBIG, errno); EXPECT_EQ(1, s_sigxfsz); } /* Change a file's timestamps */ TEST_F(Setattr, utimensat) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const timespec oldtimes[2] = { {.tv_sec = 1, .tv_nsec = 2}, {.tv_sec = 3, .tv_nsec = 4}, }; const timespec newtimes[2] = { {.tv_sec = 5, .tv_nsec = 6}, {.tv_sec = 7, .tv_nsec = 8}, }; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | 0644; out.body.entry.nodeid = ino; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.attr.atime = oldtimes[0].tv_sec; out.body.entry.attr.atimensec = oldtimes[0].tv_nsec; out.body.entry.attr.mtime = oldtimes[1].tv_sec; out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { uint32_t valid = FATTR_ATIME | FATTR_MTIME; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid && (time_t)in.body.setattr.atime == newtimes[0].tv_sec && (long)in.body.setattr.atimensec == newtimes[0].tv_nsec && (time_t)in.body.setattr.mtime == newtimes[1].tv_sec && (long)in.body.setattr.mtimensec == newtimes[1].tv_nsec); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid out.body.attr.attr.mode = S_IFREG | 0644; out.body.attr.attr.atime = newtimes[0].tv_sec; out.body.attr.attr.atimensec = newtimes[0].tv_nsec; out.body.attr.attr.mtime = newtimes[1].tv_sec; out.body.attr.attr.mtimensec = newtimes[1].tv_nsec; }))); EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0)) << strerror(errno); } /* Change a file mtime but not its atime */ TEST_F(Setattr, utimensat_mtime_only) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const timespec oldtimes[2] = { {.tv_sec = 1, .tv_nsec = 2}, {.tv_sec = 3, .tv_nsec = 4}, }; const timespec newtimes[2] = { {.tv_sec = 5, .tv_nsec = UTIME_OMIT}, {.tv_sec = 7, .tv_nsec = 8}, }; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | 0644; out.body.entry.nodeid = ino; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.attr.atime = oldtimes[0].tv_sec; out.body.entry.attr.atimensec = oldtimes[0].tv_nsec; out.body.entry.attr.mtime = oldtimes[1].tv_sec; out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { uint32_t valid = FATTR_MTIME; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid && (time_t)in.body.setattr.mtime == newtimes[1].tv_sec && (long)in.body.setattr.mtimensec == newtimes[1].tv_nsec); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid out.body.attr.attr.mode = S_IFREG | 0644; out.body.attr.attr.atime = oldtimes[0].tv_sec; out.body.attr.attr.atimensec = oldtimes[0].tv_nsec; out.body.attr.attr.mtime = newtimes[1].tv_sec; out.body.attr.attr.mtimensec = newtimes[1].tv_nsec; }))); EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0)) << strerror(errno); } /* * Set a file's mtime and atime to now * * The design of FreeBSD's VFS does not allow fusefs to set just one of atime * or mtime to UTIME_NOW; it's both or neither. */ TEST_F(Setattr, utimensat_utime_now) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const timespec oldtimes[2] = { {.tv_sec = 1, .tv_nsec = 2}, {.tv_sec = 3, .tv_nsec = 4}, }; const timespec newtimes[2] = { {.tv_sec = 0, .tv_nsec = UTIME_NOW}, {.tv_sec = 0, .tv_nsec = UTIME_NOW}, }; /* "now" is whatever the server says it is */ const timespec now[2] = { {.tv_sec = 5, .tv_nsec = 7}, {.tv_sec = 6, .tv_nsec = 8}, }; struct stat sb; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | 0644; out.body.entry.nodeid = ino; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = UINT64_MAX; out.body.entry.attr.atime = oldtimes[0].tv_sec; out.body.entry.attr.atimensec = oldtimes[0].tv_nsec; out.body.entry.attr.mtime = oldtimes[1].tv_sec; out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { uint32_t valid = FATTR_ATIME | FATTR_ATIME_NOW | FATTR_MTIME | FATTR_MTIME_NOW; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid out.body.attr.attr.mode = S_IFREG | 0644; out.body.attr.attr.atime = now[0].tv_sec; out.body.attr.attr.atimensec = now[0].tv_nsec; out.body.attr.attr.mtime = now[1].tv_sec; out.body.attr.attr.mtimensec = now[1].tv_nsec; out.body.attr.attr_valid = UINT64_MAX; }))); ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0)) << strerror(errno); ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); EXPECT_EQ(now[0].tv_sec, sb.st_atim.tv_sec); EXPECT_EQ(now[0].tv_nsec, sb.st_atim.tv_nsec); EXPECT_EQ(now[1].tv_sec, sb.st_mtim.tv_sec); EXPECT_EQ(now[1].tv_nsec, sb.st_mtim.tv_nsec); } /* * FUSE_SETATTR returns a different file type, even though the entry cache * hasn't expired. This is a server bug! It probably means that the server * removed the file and recreated it with the same inode but a different vtyp. * The best thing fusefs can do is return ENOENT to the caller. After all, the * entry must not have existed recently. */ TEST_F(Setattr, vtyp_conflict) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; uid_t newuser = 12345; sem_t sem; ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | 0777; out.body.entry.nodeid = ino; out.body.entry.entry_valid = UINT64_MAX; }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = S_IFDIR | 0777; // Changed! out.body.attr.attr.uid = newuser; }))); // We should reclaim stale vnodes expect_forget(ino, 1, &sem); EXPECT_NE(0, chown(FULLPATH, newuser, -1)); EXPECT_EQ(ENOENT, errno); sem_wait(&sem); sem_destroy(&sem); } /* On a read-only mount, no attributes may be changed */ TEST_F(RofsSetattr, erofs) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const mode_t oldmode = 0755; const mode_t newmode = 0644; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFREG | oldmode; out.body.entry.nodeid = ino; }))); ASSERT_EQ(-1, chmod(FULLPATH, newmode)); ASSERT_EQ(EROFS, errno); } /* Change the mode of a file */ TEST_F(Setattr_7_8, chmod) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const mode_t oldmode = 0755; const mode_t newmode = 0644; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry_7_8); out.body.entry.attr.mode = S_IFREG | oldmode; out.body.entry.nodeid = ino; }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { uint32_t valid = FATTR_MODE; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid && in.body.setattr.mode == newmode); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr_7_8); out.body.attr.attr.ino = ino; // Must match nodeid out.body.attr.attr.mode = S_IFREG | newmode; }))); EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); } diff --git a/tests/sys/fs/fusefs/write.cc b/tests/sys/fs/fusefs/write.cc index 1fb9de70ec6c..f931f350a7c3 100644 --- a/tests/sys/fs/fusefs/write.cc +++ b/tests/sys/fs/fusefs/write.cc @@ -1,1602 +1,1588 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ extern "C" { #include #include #include #include #include #include #include #include #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; class Write: public FuseTest { public: void SetUp() { FuseTest::SetUp(); } void TearDown() { struct sigaction sa; bzero(&sa, sizeof(sa)); sa.sa_handler = SIG_DFL; sigaction(SIGXFSZ, &sa, NULL); FuseTest::TearDown(); } void expect_lookup(const char *relpath, uint64_t ino, uint64_t size) { FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, size, 1); } void expect_release(uint64_t ino, ProcessMockerT r) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_RELEASE && in.header.nodeid == ino); }, Eq(true)), _) ).WillRepeatedly(Invoke(r)); } void expect_write(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, const void *contents) { FuseTest::expect_write(ino, offset, isize, osize, 0, 0, contents); } /* Expect a write that may or may not come, depending on the cache mode */ void maybe_expect_write(uint64_t ino, uint64_t offset, uint64_t size, const void *contents) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { const char *buf = (const char*)in.body.bytes + sizeof(struct fuse_write_in); assert(size <= sizeof(in.body.bytes) - sizeof(struct fuse_write_in)); return (in.header.opcode == FUSE_WRITE && in.header.nodeid == ino && in.body.write.offset == offset && in.body.write.size == size && 0 == bcmp(buf, contents, size)); }, Eq(true)), _) ).Times(AtMost(1)) .WillRepeatedly(Invoke( ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = size; }) )); } }; class Write_7_8: public FuseTest { public: virtual void SetUp() { m_kernel_minor_version = 8; FuseTest::SetUp(); } void expect_lookup(const char *relpath, uint64_t ino, uint64_t size) { FuseTest::expect_lookup_7_8(relpath, ino, S_IFREG | 0644, size, 1); } }; class AioWrite: public Write { virtual void SetUp() { if (!is_unsafe_aio_enabled()) GTEST_SKIP() << "vfs.aio.enable_unsafe must be set for this test"; FuseTest::SetUp(); } }; /* Tests for the writeback cache mode */ class WriteBack: public Write { public: virtual void SetUp() { m_init_flags |= FUSE_WRITEBACK_CACHE; FuseTest::SetUp(); if (IsSkipped()) return; } void expect_write(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, const void *contents) { FuseTest::expect_write(ino, offset, isize, osize, FUSE_WRITE_CACHE, 0, contents); } }; class WriteBackAsync: public WriteBack { public: virtual void SetUp() { m_async = true; m_maxwrite = 65536; WriteBack::SetUp(); } }; class TimeGran: public WriteBackAsync, public WithParamInterface { 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"; } }; /* Tests relating to the server's max_write property */ class WriteMaxWrite: public Write { public: virtual void SetUp() { /* * For this test, m_maxwrite must be less than either m_maxbcachebuf or * maxphys. */ m_maxwrite = 32768; Write::SetUp(); } }; class WriteEofDuringVnopStrategy: public Write, public WithParamInterface {}; class WriteRlimitFsize: public Write, public WithParamInterface { public: static sig_atomic_t s_sigxfsz; struct rlimit m_initial_limit; void SetUp() { s_sigxfsz = 0; getrlimit(RLIMIT_FSIZE, &m_initial_limit); FuseTest::SetUp(); } void TearDown() { setrlimit(RLIMIT_FSIZE, &m_initial_limit); FuseTest::TearDown(); } }; sig_atomic_t WriteRlimitFsize::s_sigxfsz = 0; void sigxfsz_handler(int __unused sig) { WriteRlimitFsize::s_sigxfsz = 1; } /* AIO writes need to set the header's pid field correctly */ /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */ TEST_F(AioWrite, DISABLED_aio_write) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; uint64_t offset = 4096; int fd; ssize_t bufsize = strlen(CONTENTS); struct aiocb iocb, *piocb; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_write(ino, offset, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_WRONLY); ASSERT_LE(0, fd) << strerror(errno); iocb.aio_nbytes = bufsize; iocb.aio_fildes = fd; iocb.aio_buf = __DECONST(void *, CONTENTS); iocb.aio_offset = offset; iocb.aio_sigevent.sigev_notify = SIGEV_NONE; ASSERT_EQ(0, aio_write(&iocb)) << strerror(errno); ASSERT_EQ(bufsize, aio_waitcomplete(&piocb, NULL)) << strerror(errno); leak(fd); } /* * When a file is opened with O_APPEND, we should forward that flag to * FUSE_OPEN (tested by Open.o_append) but still attempt to calculate the * offset internally. That way we'll work both with filesystems that * understand O_APPEND (and ignore the offset) and filesystems that don't (and * simply use the offset). * * Note that verifying the O_APPEND flag in FUSE_OPEN is done in the * Open.o_append test. */ TEST_F(Write, append) { const ssize_t BUFSIZE = 9; const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char CONTENTS[BUFSIZE] = "abcdefgh"; uint64_t ino = 42; /* * Set offset to a maxbcachebuf boundary so we don't need to RMW when * using writeback caching */ uint64_t initial_offset = m_maxbcachebuf; int fd; expect_lookup(RELPATH, ino, initial_offset); expect_open(ino, 0, 1); expect_write(ino, initial_offset, BUFSIZE, BUFSIZE, CONTENTS); /* Must open O_RDWR or fuse(4) implicitly sets direct_io */ fd = open(FULLPATH, O_RDWR | O_APPEND); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(BUFSIZE, write(fd, CONTENTS, BUFSIZE)) << strerror(errno); leak(fd); } /* If a file is cached, then appending to the end should not cause a read */ TEST_F(Write, append_to_cached) { const ssize_t BUFSIZE = 9; const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; char *oldcontents, *oldbuf; const char CONTENTS[BUFSIZE] = "abcdefgh"; uint64_t ino = 42; /* * Set offset in between maxbcachebuf boundary to test buffer handling */ uint64_t oldsize = m_maxbcachebuf / 2; int fd; - oldcontents = (char*)calloc(1, oldsize); - ASSERT_NE(nullptr, oldcontents) << strerror(errno); - oldbuf = (char*)malloc(oldsize); - ASSERT_NE(nullptr, oldbuf) << strerror(errno); + oldcontents = new char[oldsize](); + oldbuf = new char[oldsize]; expect_lookup(RELPATH, ino, oldsize); expect_open(ino, 0, 1); expect_read(ino, 0, oldsize, oldsize, oldcontents); maybe_expect_write(ino, oldsize, BUFSIZE, CONTENTS); /* Must open O_RDWR or fuse(4) implicitly sets direct_io */ fd = open(FULLPATH, O_RDWR | O_APPEND); ASSERT_LE(0, fd) << strerror(errno); /* Read the old data into the cache */ ASSERT_EQ((ssize_t)oldsize, read(fd, oldbuf, oldsize)) << strerror(errno); /* Write the new data. There should be no more read operations */ ASSERT_EQ(BUFSIZE, write(fd, CONTENTS, BUFSIZE)) << strerror(errno); leak(fd); - free(oldbuf); - free(oldcontents); + delete[] oldbuf; + delete[] oldcontents; } TEST_F(Write, append_direct_io) { const ssize_t BUFSIZE = 9; const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char CONTENTS[BUFSIZE] = "abcdefgh"; uint64_t ino = 42; uint64_t initial_offset = 4096; int fd; expect_lookup(RELPATH, ino, initial_offset); expect_open(ino, FOPEN_DIRECT_IO, 1); expect_write(ino, initial_offset, BUFSIZE, BUFSIZE, CONTENTS); fd = open(FULLPATH, O_WRONLY | O_APPEND); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(BUFSIZE, write(fd, CONTENTS, BUFSIZE)) << strerror(errno); leak(fd); } /* A direct write should evict any overlapping cached data */ TEST_F(Write, direct_io_evicts_cache) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char CONTENTS0[] = "abcdefgh"; const char CONTENTS1[] = "ijklmnop"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS0) + 1; char readbuf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS0); expect_write(ino, 0, bufsize, bufsize, CONTENTS1); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); // Prime cache ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno); // Write directly, evicting cache ASSERT_EQ(0, fcntl(fd, F_SETFL, O_DIRECT)) << strerror(errno); ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS1, bufsize)) << strerror(errno); // Read again. Cache should be bypassed expect_read(ino, 0, bufsize, bufsize, CONTENTS1); ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno); ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno); ASSERT_STREQ(readbuf, CONTENTS1); leak(fd); } /* * If the server doesn't return FOPEN_DIRECT_IO during FUSE_OPEN, then it's not * allowed to return a short write for that file handle. However, if it does * then we should still do our darndest to handle it by resending the unwritten * portion. */ TEST_F(Write, indirect_io_short_write) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefghijklmnop"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); ssize_t bufsize0 = 11; ssize_t bufsize1 = strlen(CONTENTS) - bufsize0; const char *contents1 = CONTENTS + bufsize0; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_write(ino, 0, bufsize, bufsize0, CONTENTS); expect_write(ino, bufsize0, bufsize1, bufsize1, contents1); fd = open(FULLPATH, O_WRONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); leak(fd); } /* It is an error if the daemon claims to have written more data than we sent */ TEST_F(Write, indirect_io_long_write) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefghijklmnop"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); ssize_t bufsize_out = 100; off_t some_other_size = 25; struct stat sb; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_write(ino, 0, bufsize, bufsize_out, CONTENTS); expect_getattr(ino, some_other_size); fd = open(FULLPATH, O_WRONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(-1, write(fd, CONTENTS, bufsize)) << strerror(errno); ASSERT_EQ(EINVAL, errno); /* * Following such an error, we should requery the server for the file's * size. */ fstat(fd, &sb); ASSERT_EQ(sb.st_size, some_other_size); leak(fd); } /* * Don't crash if the server returns a write that can't be represented as a * signed 32 bit number. Regression test for * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263263 */ TEST_F(Write, indirect_io_very_long_write) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefghijklmnop"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); ssize_t bufsize_out = 3 << 30; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_write(ino, 0, bufsize, bufsize_out, CONTENTS); fd = open(FULLPATH, O_WRONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(-1, write(fd, CONTENTS, bufsize)) << strerror(errno); ASSERT_EQ(EINVAL, errno); leak(fd); } /* * When the direct_io option is used, filesystems are allowed to write less * data than requested. We should return the short write to userland. */ TEST_F(Write, direct_io_short_write) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefghijklmnop"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); ssize_t halfbufsize = bufsize / 2; expect_lookup(RELPATH, ino, 0); expect_open(ino, FOPEN_DIRECT_IO, 1); expect_write(ino, 0, bufsize, halfbufsize, CONTENTS); fd = open(FULLPATH, O_WRONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(halfbufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); leak(fd); } /* * An insidious edge case: the filesystem returns a short write, and the * difference between what we requested and what it actually wrote crosses an * iov element boundary */ TEST_F(Write, direct_io_short_write_iov) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS0 = "abcdefgh"; const char *CONTENTS1 = "ijklmnop"; const char *EXPECTED0 = "abcdefghijklmnop"; uint64_t ino = 42; int fd; ssize_t size0 = strlen(CONTENTS0) - 1; ssize_t size1 = strlen(CONTENTS1) + 1; ssize_t totalsize = size0 + size1; struct iovec iov[2]; expect_lookup(RELPATH, ino, 0); expect_open(ino, FOPEN_DIRECT_IO, 1); expect_write(ino, 0, totalsize, size0, EXPECTED0); fd = open(FULLPATH, O_WRONLY); ASSERT_LE(0, fd) << strerror(errno); iov[0].iov_base = __DECONST(void*, CONTENTS0); iov[0].iov_len = strlen(CONTENTS0); iov[1].iov_base = __DECONST(void*, CONTENTS1); iov[1].iov_len = strlen(CONTENTS1); ASSERT_EQ(size0, writev(fd, iov, 2)) << strerror(errno); leak(fd); } /* fusefs should respect RLIMIT_FSIZE */ TEST_P(WriteRlimitFsize, rlimit_fsize) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; struct rlimit rl; ssize_t bufsize = strlen(CONTENTS); off_t offset = 1'000'000'000; uint64_t ino = 42; int fd, oflag; oflag = GetParam(); expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); rl.rlim_cur = offset; rl.rlim_max = m_initial_limit.rlim_max; ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); fd = open(FULLPATH, O_WRONLY | oflag); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(-1, pwrite(fd, CONTENTS, bufsize, offset)); EXPECT_EQ(EFBIG, errno); EXPECT_EQ(1, s_sigxfsz); leak(fd); } /* * When crossing the RLIMIT_FSIZE boundary, writes should be truncated, not * aborted. * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=164793 */ TEST_P(WriteRlimitFsize, rlimit_fsize_truncate) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefghijklmnopqrstuvwxyz"; struct rlimit rl; ssize_t bufsize = strlen(CONTENTS); uint64_t ino = 42; off_t offset = 1 << 30; off_t limit = offset + strlen(CONTENTS) / 2; int fd, oflag; oflag = GetParam(); expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_write(ino, offset, bufsize / 2, bufsize / 2, CONTENTS); rl.rlim_cur = limit; rl.rlim_max = m_initial_limit.rlim_max; ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); fd = open(FULLPATH, O_WRONLY | oflag); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize / 2, pwrite(fd, CONTENTS, bufsize, offset)) << strerror(errno); leak(fd); } INSTANTIATE_TEST_SUITE_P(W, WriteRlimitFsize, Values(0, O_DIRECT) ); /* * A short read indicates EOF. Test that nothing bad happens if we get EOF * during the R of a RMW operation. */ TEST_F(Write, eof_during_rmw) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; const char *INITIAL = "XXXXXXXXXX"; uint64_t ino = 42; uint64_t offset = 1; ssize_t bufsize = strlen(CONTENTS) + 1; off_t orig_fsize = 10; off_t truncated_fsize = 5; int fd; FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, orig_fsize, 1); expect_open(ino, 0, 1); expect_read(ino, 0, orig_fsize, truncated_fsize, INITIAL, O_RDWR); maybe_expect_write(ino, offset, bufsize, CONTENTS); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, pwrite(fd, CONTENTS, bufsize, offset)) << strerror(errno); leak(fd); } /* * VOP_STRATEGY should not query the server for the file's size, even if its * cached attributes have expired. * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=256937 */ TEST_P(WriteEofDuringVnopStrategy, eof_during_vop_strategy) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; Sequence seq; const off_t filesize = 2 * m_maxbcachebuf; - void *contents; + char *contents; uint64_t ino = 42; uint64_t attr_valid = 0; uint64_t attr_valid_nsec = 0; mode_t mode = S_IFREG | 0644; int fd; int ngetattrs; ngetattrs = GetParam(); - contents = calloc(1, filesize); + contents = new char[filesize](); EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillRepeatedly(Invoke( ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = mode; out.body.entry.nodeid = ino; out.body.entry.attr.nlink = 1; out.body.entry.attr.size = filesize; out.body.entry.attr_valid = attr_valid; out.body.entry.attr_valid_nsec = attr_valid_nsec; }))); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_GETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).Times(Between(ngetattrs - 1, ngetattrs)) .InSequence(seq) .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = mode; out.body.attr.attr_valid = attr_valid; out.body.attr.attr_valid_nsec = attr_valid_nsec; out.body.attr.attr.size = filesize; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_GETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).InSequence(seq) .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = mode; out.body.attr.attr_valid = attr_valid; out.body.attr.attr_valid_nsec = attr_valid_nsec; out.body.attr.attr.size = filesize / 2; }))); expect_write(ino, 0, filesize / 2, filesize / 2, contents); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(filesize / 2, write(fd, contents, filesize / 2)) << strerror(errno); } INSTANTIATE_TEST_SUITE_P(W, WriteEofDuringVnopStrategy, Values(1, 2, 3) ); /* * If the kernel cannot be sure which uid, gid, or pid was responsible for a * write, then it must set the FUSE_WRITE_CACHE bit */ /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236378 */ TEST_F(Write, mmap) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); void *p; uint64_t offset = 10; size_t len; - void *zeros, *expected; + char *zeros, *expected; len = getpagesize(); - zeros = calloc(1, len); - ASSERT_NE(nullptr, zeros); - expected = calloc(1, len); - ASSERT_NE(nullptr, expected); + zeros = new char[len](); + expected = new char[len](); memmove((uint8_t*)expected + offset, CONTENTS, bufsize); expect_lookup(RELPATH, ino, len); expect_open(ino, 0, 1); expect_read(ino, 0, len, len, zeros); /* * Writes from the pager may or may not be associated with the correct * pid, so they must set FUSE_WRITE_CACHE. */ FuseTest::expect_write(ino, 0, len, len, FUSE_WRITE_CACHE, 0, expected); expect_flush(ino, 1, ReturnErrno(0)); expect_release(ino, ReturnErrno(0)); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); p = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); ASSERT_NE(MAP_FAILED, p) << strerror(errno); memmove((uint8_t*)p + offset, CONTENTS, bufsize); ASSERT_EQ(0, munmap(p, len)) << strerror(errno); close(fd); // Write mmap'd data on close - free(expected); - free(zeros); + delete[] expected; + delete[] zeros; leak(fd); } TEST_F(Write, pwrite) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; uint64_t offset = m_maxbcachebuf; int fd; ssize_t bufsize = strlen(CONTENTS); expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_write(ino, offset, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_WRONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, pwrite(fd, CONTENTS, bufsize, offset)) << strerror(errno); leak(fd); } /* Writing a file should update its cached mtime and ctime */ TEST_F(Write, timestamps) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; ssize_t bufsize = strlen(CONTENTS); uint64_t ino = 42; struct stat sb0, sb1; int fd; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); maybe_expect_write(ino, 0, bufsize, CONTENTS); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb0)) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); nap(); ASSERT_EQ(0, fstat(fd, &sb1)) << strerror(errno); EXPECT_EQ(sb0.st_atime, sb1.st_atime); EXPECT_NE(sb0.st_mtime, sb1.st_mtime); EXPECT_NE(sb0.st_ctime, sb1.st_ctime); leak(fd); } TEST_F(Write, write) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_write(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_WRONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); leak(fd); } /* fuse(4) should not issue writes of greater size than the daemon requests */ TEST_F(WriteMaxWrite, write) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; int *contents; uint64_t ino = 42; int fd; ssize_t halfbufsize, bufsize; halfbufsize = m_mock->m_maxwrite; if (halfbufsize >= m_maxbcachebuf || halfbufsize >= m_maxphys) GTEST_SKIP() << "Must lower m_maxwrite for this test"; bufsize = halfbufsize * 2; - contents = (int*)malloc(bufsize); - ASSERT_NE(nullptr, contents); + contents = new int[bufsize / sizeof(int)]; for (int i = 0; i < (int)bufsize / (int)sizeof(i); i++) { contents[i] = i; } expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); maybe_expect_write(ino, 0, halfbufsize, contents); maybe_expect_write(ino, halfbufsize, halfbufsize, &contents[halfbufsize / sizeof(int)]); fd = open(FULLPATH, O_WRONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, contents, bufsize)) << strerror(errno); leak(fd); - free(contents); + delete[] contents; } TEST_F(Write, write_nothing) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = ""; uint64_t ino = 42; int fd; ssize_t bufsize = 0; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); fd = open(FULLPATH, O_WRONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); leak(fd); } TEST_F(Write_7_8, write) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_write_7_8(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_WRONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); leak(fd); } /* In writeback mode, dirty data should be written on close */ TEST_F(WriteBackAsync, close) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_write(ino, 0, bufsize, bufsize, CONTENTS); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_SETATTR); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid }))); expect_flush(ino, 1, ReturnErrno(0)); expect_release(ino, ReturnErrno(0)); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); close(fd); } /* In writeback mode, adjacent writes will be clustered together */ TEST_F(WriteCluster, clustering) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int i, fd; - void *wbuf, *wbuf2x; + char *wbuf, *wbuf2x; ssize_t bufsize = m_maxbcachebuf; off_t filesize = 5 * bufsize; - wbuf = malloc(bufsize); - ASSERT_NE(nullptr, wbuf) << strerror(errno); + wbuf = new char[bufsize]; memset(wbuf, 'X', bufsize); - wbuf2x = malloc(2 * bufsize); - ASSERT_NE(nullptr, wbuf2x) << strerror(errno); + wbuf2x = new char[2 * bufsize]; memset(wbuf2x, 'X', 2 * bufsize); expect_lookup(RELPATH, ino, filesize); expect_open(ino, 0, 1); /* * Writes of bufsize-bytes each should be clustered into greater sizes. * The amount of clustering is adaptive, so the first write actually * issued will be 2x bufsize and subsequent writes may be larger */ expect_write(ino, 0, 2 * bufsize, 2 * bufsize, wbuf2x); expect_write(ino, 2 * bufsize, 2 * bufsize, 2 * bufsize, wbuf2x); expect_flush(ino, 1, ReturnErrno(0)); expect_release(ino, ReturnErrno(0)); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); for (i = 0; i < 4; i++) { ASSERT_EQ(bufsize, write(fd, wbuf, bufsize)) << strerror(errno); } close(fd); - free(wbuf2x); - free(wbuf); + delete[] wbuf2x; + delete[] wbuf; } /* * When clustering writes, an I/O error to any of the cluster's children should * not panic the system on unmount */ /* * Regression test for bug 238585 * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=238565 */ TEST_F(WriteCluster, cluster_write_err) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int i, fd; - void *wbuf; + char *wbuf; ssize_t bufsize = m_maxbcachebuf; off_t filesize = 4 * bufsize; - wbuf = malloc(bufsize); - ASSERT_NE(nullptr, wbuf) << strerror(errno); + wbuf = new char[bufsize]; memset(wbuf, 'X', bufsize); expect_lookup(RELPATH, ino, filesize); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_WRITE); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnErrno(EIO))); expect_flush(ino, 1, ReturnErrno(0)); expect_release(ino, ReturnErrno(0)); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); for (i = 0; i < 3; i++) { ASSERT_EQ(bufsize, write(fd, wbuf, bufsize)) << strerror(errno); } close(fd); - free(wbuf); + delete[] wbuf; } /* * In writeback mode, writes to an O_WRONLY file could trigger reads from the * server. The FUSE protocol explicitly allows that. */ TEST_F(WriteBack, rmw) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; const char *INITIAL = "XXXXXXXXXX"; uint64_t ino = 42; uint64_t offset = 1; off_t fsize = 10; int fd; ssize_t bufsize = strlen(CONTENTS); FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); expect_open(ino, 0, 1); expect_read(ino, 0, fsize, fsize, INITIAL, O_WRONLY); maybe_expect_write(ino, offset, bufsize, CONTENTS); fd = open(FULLPATH, O_WRONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, pwrite(fd, CONTENTS, bufsize, offset)) << strerror(errno); leak(fd); } /* * Without direct_io, writes should be committed to cache */ TEST_F(WriteBack, cache) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t readbuf[bufsize]; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_write(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); /* * A subsequent read should be serviced by cache, without querying the * filesystem daemon */ ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno); leak(fd); } /* * With O_DIRECT, writes should be not committed to cache. Admittedly this is * an odd test, because it would be unusual to use O_DIRECT for writes but not * reads. */ TEST_F(WriteBack, o_direct) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t readbuf[bufsize]; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); FuseTest::expect_write(ino, 0, bufsize, bufsize, 0, FUSE_WRITE_CACHE, CONTENTS); expect_read(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDWR | O_DIRECT); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); /* A subsequent read must query the daemon because cache is empty */ ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno); ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno); leak(fd); } TEST_F(WriteBack, direct_io) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t readbuf[bufsize]; expect_lookup(RELPATH, ino, 0); expect_open(ino, FOPEN_DIRECT_IO, 1); FuseTest::expect_write(ino, 0, bufsize, bufsize, 0, FUSE_WRITE_CACHE, CONTENTS); expect_read(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); /* A subsequent read must query the daemon because cache is empty */ ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno); ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno); leak(fd); } /* * mmap should still be possible even if the server used direct_io. Mmap will * still use the cache, though. * * Regression test for bug 247276 * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=247276 */ TEST_F(WriteBack, mmap_direct_io) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; size_t len; ssize_t bufsize = strlen(CONTENTS); - void *p, *zeros; + char *zeros; + void *p; len = getpagesize(); - zeros = calloc(1, len); - ASSERT_NE(nullptr, zeros); + zeros = new char[len](); expect_lookup(RELPATH, ino, len); expect_open(ino, FOPEN_DIRECT_IO, 1); expect_read(ino, 0, len, len, zeros); expect_flush(ino, 1, ReturnErrno(0)); FuseTest::expect_write(ino, 0, len, len, FUSE_WRITE_CACHE, 0, zeros); expect_release(ino, ReturnErrno(0)); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); p = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); ASSERT_NE(MAP_FAILED, p) << strerror(errno); memmove((uint8_t*)p, CONTENTS, bufsize); ASSERT_EQ(0, munmap(p, len)) << strerror(errno); close(fd); // Write mmap'd data on close - free(zeros); + delete[] zeros; } /* * When mounted with -o async, the writeback cache mode should delay writes */ TEST_F(WriteBackAsync, delay) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); /* Write should be cached, but FUSE_WRITE shouldn't be sent */ EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_WRITE); }, Eq(true)), _) ).Times(0); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); /* Don't close the file because that would flush the cache */ leak(fd); } /* * A direct write should not evict dirty cached data from outside of its own * byte range. */ TEST_F(WriteBackAsync, direct_io_ignores_unrelated_cached) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char CONTENTS0[] = "abcdefgh"; const char CONTENTS1[] = "ijklmnop"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS0) + 1; ssize_t fsize = 2 * m_maxbcachebuf; char readbuf[bufsize]; - void *zeros; + char *zeros; - zeros = calloc(1, m_maxbcachebuf); - ASSERT_NE(nullptr, zeros); + zeros = new char[m_maxbcachebuf](); expect_lookup(RELPATH, ino, fsize); expect_open(ino, 0, 1); expect_read(ino, 0, m_maxbcachebuf, m_maxbcachebuf, zeros); FuseTest::expect_write(ino, m_maxbcachebuf, bufsize, bufsize, 0, 0, CONTENTS1); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); // Cache first block with dirty data. This will entail first reading // the existing data. ASSERT_EQ(bufsize, pwrite(fd, CONTENTS0, bufsize, 0)) << strerror(errno); // Write directly to second block ASSERT_EQ(0, fcntl(fd, F_SETFL, O_DIRECT)) << strerror(errno); ASSERT_EQ(bufsize, pwrite(fd, CONTENTS1, bufsize, m_maxbcachebuf)) << strerror(errno); // Read from the first block again. Should be serviced by cache. ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno); ASSERT_EQ(bufsize, pread(fd, readbuf, bufsize, 0)) << strerror(errno); ASSERT_STREQ(readbuf, CONTENTS0); leak(fd); - free(zeros); + delete[] zeros; } /* * If a direct io write partially overlaps one or two blocks of dirty cached * data, No dirty data should be lost. Admittedly this is a weird test, * because it would be unusual to use O_DIRECT and the writeback cache. */ TEST_F(WriteBackAsync, direct_io_partially_overlaps_cached_block) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; off_t bs = m_maxbcachebuf; ssize_t fsize = 3 * bs; - 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); + char *readbuf, *zeros, *ones, *zeroones, *onezeros; + + readbuf = new char[bs]; + zeros = new char[3 * bs](); + ones = new char[2 * bs]; memset(ones, 1, 2 * bs); - zeroones = calloc(1, bs); - ASSERT_NE(nullptr, zeroones); + zeroones = new char[bs](); memset((uint8_t*)zeroones + bs / 2, 1, bs / 2); - onezeros = calloc(1, bs); - ASSERT_NE(nullptr, onezeros); + onezeros = new char[bs](); memset(onezeros, 1, bs / 2); expect_lookup(RELPATH, ino, fsize); expect_open(ino, 0, 1); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); /* Cache first and third blocks with dirty data. */ ASSERT_EQ(3 * bs, pwrite(fd, zeros, 3 * bs, 0)) << strerror(errno); /* * Write directly to all three blocks. The partially written blocks * will be flushed because they're dirty. */ FuseTest::expect_write(ino, 0, bs, bs, 0, 0, zeros); FuseTest::expect_write(ino, 2 * bs, bs, bs, 0, 0, zeros); /* The direct write is split in two because of the m_maxwrite value */ FuseTest::expect_write(ino, bs / 2, bs, bs, 0, 0, ones); FuseTest::expect_write(ino, 3 * bs / 2, bs, bs, 0, 0, ones); ASSERT_EQ(0, fcntl(fd, F_SETFL, O_DIRECT)) << strerror(errno); ASSERT_EQ(2 * bs, pwrite(fd, ones, 2 * bs, bs / 2)) << strerror(errno); /* * Read from both the valid and invalid portions of the first and third * blocks again. This will entail FUSE_READ operations because these * blocks were invalidated by the direct write. */ expect_read(ino, 0, bs, bs, zeroones); expect_read(ino, 2 * bs, bs, bs, onezeros); ASSERT_EQ(0, fcntl(fd, F_SETFL, 0)) << strerror(errno); ASSERT_EQ(bs / 2, pread(fd, readbuf, bs / 2, 0)) << strerror(errno); EXPECT_EQ(0, memcmp(zeros, readbuf, bs / 2)); ASSERT_EQ(bs / 2, pread(fd, readbuf, bs / 2, 5 * bs / 2)) << strerror(errno); EXPECT_EQ(0, memcmp(zeros, readbuf, bs / 2)); ASSERT_EQ(bs / 2, pread(fd, readbuf, bs / 2, bs / 2)) << strerror(errno); EXPECT_EQ(0, memcmp(ones, readbuf, bs / 2)); ASSERT_EQ(bs / 2, pread(fd, readbuf, bs / 2, 2 * bs)) << strerror(errno); EXPECT_EQ(0, memcmp(ones, readbuf, bs / 2)); leak(fd); - free(zeroones); - free(onezeros); - free(ones); - free(zeros); - free(readbuf); + delete[] zeroones; + delete[] onezeros; + delete[] ones; + delete[] zeros; + delete[] readbuf; } /* * In WriteBack mode, writes may be cached beyond what the server thinks is the * EOF. In this case, a short read at EOF should _not_ cause fusefs to update * the file's size. */ TEST_F(WriteBackAsync, eof) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS0 = "abcdefgh"; const char *CONTENTS1 = "ijklmnop"; uint64_t ino = 42; int fd; off_t offset = m_maxbcachebuf; ssize_t wbufsize = strlen(CONTENTS1); off_t old_filesize = (off_t)strlen(CONTENTS0); ssize_t rbufsize = 2 * old_filesize; char readbuf[rbufsize]; size_t holesize = rbufsize - old_filesize; char hole[holesize]; struct stat sb; ssize_t r; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_read(ino, 0, m_maxbcachebuf, old_filesize, CONTENTS0); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); /* Write and cache data beyond EOF */ ASSERT_EQ(wbufsize, pwrite(fd, CONTENTS1, wbufsize, offset)) << strerror(errno); /* Read from the old EOF */ r = pread(fd, readbuf, rbufsize, 0); ASSERT_LE(0, r) << strerror(errno); EXPECT_EQ(rbufsize, r) << "read should've synthesized a hole"; EXPECT_EQ(0, memcmp(CONTENTS0, readbuf, old_filesize)); bzero(hole, holesize); EXPECT_EQ(0, memcmp(hole, readbuf + old_filesize, holesize)); /* The file's size should still be what was established by pwrite */ ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); EXPECT_EQ(offset + wbufsize, sb.st_size); leak(fd); } /* * When a file has dirty writes that haven't been flushed, the server's notion * of its mtime and ctime will be wrong. The kernel should ignore those if it * gets them from a FUSE_GETATTR before flushing. */ TEST_F(WriteBackAsync, timestamps) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; ssize_t bufsize = strlen(CONTENTS); uint64_t ino = 42; uint64_t attr_valid = 0; uint64_t attr_valid_nsec = 0; uint64_t server_time = 12345; mode_t mode = S_IFREG | 0644; int fd; struct stat sb; EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) .WillRepeatedly(Invoke( ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = mode; out.body.entry.nodeid = ino; out.body.entry.attr.nlink = 1; out.body.entry.attr_valid = attr_valid; out.body.entry.attr_valid_nsec = attr_valid_nsec; }))); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_GETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).WillRepeatedly(Invoke( ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = mode; out.body.attr.attr_valid = attr_valid; out.body.attr.attr_valid_nsec = attr_valid_nsec; out.body.attr.attr.atime = server_time; out.body.attr.attr.mtime = server_time; out.body.attr.attr.ctime = server_time; }))); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); EXPECT_EQ((time_t)server_time, sb.st_atime); EXPECT_NE((time_t)server_time, sb.st_mtime); EXPECT_NE((time_t)server_time, sb.st_ctime); leak(fd); } /* Any dirty timestamp fields should be flushed during a SETATTR */ TEST_F(WriteBackAsync, timestamps_during_setattr) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; ssize_t bufsize = strlen(CONTENTS); uint64_t ino = 42; const mode_t newmode = 0755; int fd; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { uint32_t valid = FATTR_MODE | FATTR_MTIME | FATTR_CTIME; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = S_IFREG | newmode; }))); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno); leak(fd); } /* fuse_init_out.time_gran controls the granularity of timestamps */ TEST_P(TimeGran, timestamps_during_setattr) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; ssize_t bufsize = strlen(CONTENTS); uint64_t ino = 42; const mode_t newmode = 0755; int fd; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { uint32_t valid = FATTR_MODE | FATTR_MTIME | FATTR_CTIME; return (in.header.opcode == FUSE_SETATTR && in.header.nodeid == ino && in.body.setattr.valid == valid && in.body.setattr.mtimensec % m_time_gran == 0 && in.body.setattr.ctimensec % m_time_gran == 0); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; out.body.attr.attr.mode = S_IFREG | newmode; }))); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno); leak(fd); } INSTANTIATE_TEST_SUITE_P(RA, TimeGran, Range(0u, 10u)); /* * Without direct_io, writes should be committed to cache */ TEST_F(Write, writethrough) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); uint8_t readbuf[bufsize]; expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_write(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); /* * A subsequent read should be serviced by cache, without querying the * filesystem daemon */ ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); ASSERT_EQ(bufsize, read(fd, readbuf, bufsize)) << strerror(errno); leak(fd); } /* Writes that extend a file should update the cached file size */ TEST_F(Write, update_file_size) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; struct stat sb; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); expect_lookup(RELPATH, ino, 0); expect_open(ino, 0, 1); expect_write(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); /* Get cached attributes */ ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); ASSERT_EQ(bufsize, sb.st_size); leak(fd); }