Index: projects/fuse2/tests/sys/fs/fusefs/mockfs.cc =================================================================== --- projects/fuse2/tests/sys/fs/fusefs/mockfs.cc (revision 348284) +++ projects/fuse2/tests/sys/fs/fusefs/mockfs.cc (revision 348285) @@ -1,618 +1,624 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ extern "C" { #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mntopts.h" // for build_iovec } +#include + #include #include "mockfs.hh" using namespace testing; int verbosity = 0; const char* opcode2opname(uint32_t opcode) { const int NUM_OPS = 39; const char* table[NUM_OPS] = { "Unknown (opcode 0)", "LOOKUP", "FORGET", "GETATTR", "SETATTR", "READLINK", "SYMLINK", "Unknown (opcode 7)", "MKNOD", "MKDIR", "UNLINK", "RMDIR", "RENAME", "LINK", "OPEN", "READ", "WRITE", "STATFS", "RELEASE", "Unknown (opcode 19)", "FSYNC", "SETXATTR", "GETXATTR", "LISTXATTR", "REMOVEXATTR", "FLUSH", "INIT", "OPENDIR", "READDIR", "RELEASEDIR", "FSYNCDIR", "GETLK", "SETLK", "SETLKW", "ACCESS", "CREATE", "INTERRUPT", "BMAP", "DESTROY" }; if (opcode >= NUM_OPS) return ("Unknown (opcode > max)"); else return (table[opcode]); } ProcessMockerT ReturnErrno(int error) { return([=](auto in, auto &out) { auto out0 = new mockfs_buf_out; out0->header.unique = in->header.unique; out0->header.error = -error; out0->header.len = sizeof(out0->header); out.push_back(out0); }); } /* Helper function used for returning negative cache entries for LOOKUP */ ProcessMockerT ReturnNegativeCache(const struct timespec *entry_valid) { return([=](auto in, auto &out) { /* nodeid means ENOENT and cache it */ auto out0 = new mockfs_buf_out; out0->body.entry.nodeid = 0; out0->header.unique = in->header.unique; out0->header.error = 0; out0->body.entry.entry_valid = entry_valid->tv_sec; out0->body.entry.entry_valid_nsec = entry_valid->tv_nsec; SET_OUT_HEADER_LEN(out0, entry); out.push_back(out0); }); } ProcessMockerT ReturnImmediate(std::function f) { return([=](auto in, auto &out) { auto out0 = new mockfs_buf_out; out0->header.unique = in->header.unique; f(in, out0); out.push_back(out0); }); } void sigint_handler(int __unused sig) { // Don't do anything except interrupt the daemon's read(2) call } void debug_fuseop(const mockfs_buf_in *in) { - printf("%-11s ino=%2lu", opcode2opname(in->header.opcode), + printf("%-11s ino=%2" PRIu64, opcode2opname(in->header.opcode), in->header.nodeid); if (verbosity > 1) { - printf(" uid=%5u gid=%5u pid=%5u unique=%lu len=%u", + printf(" uid=%5u gid=%5u pid=%5u unique=%" PRIu64 " len=%u", in->header.uid, in->header.gid, in->header.pid, in->header.unique, in->header.len); } switch (in->header.opcode) { const char *name, *value; case FUSE_ACCESS: printf(" mask=%#x", in->body.access.mask); break; case FUSE_CREATE: name = (const char*)in->body.bytes + sizeof(fuse_open_in); printf(" flags=%#x name=%s", in->body.open.flags, name); break; case FUSE_FLUSH: - printf(" fh=%#lx lock_owner=%lu", in->body.flush.fh, + printf(" fh=%#" PRIx64 " lock_owner=%" PRIu64, + in->body.flush.fh, in->body.flush.lock_owner); break; case FUSE_FORGET: - printf(" nlookup=%lu", in->body.forget.nlookup); + printf(" nlookup=%" PRIu64, in->body.forget.nlookup); break; case FUSE_FSYNC: printf(" flags=%#x", in->body.fsync.fsync_flags); break; case FUSE_FSYNCDIR: printf(" flags=%#x", in->body.fsyncdir.fsync_flags); break; case FUSE_INTERRUPT: - printf(" unique=%lu", in->body.interrupt.unique); + printf(" unique=%" PRIu64, in->body.interrupt.unique); break; case FUSE_LINK: - printf(" oldnodeid=%lu", in->body.link.oldnodeid); + printf(" oldnodeid=%" PRIu64, in->body.link.oldnodeid); break; case FUSE_LOOKUP: printf(" %s", in->body.lookup); break; case FUSE_MKDIR: name = (const char*)in->body.bytes + sizeof(fuse_mkdir_in); printf(" name=%s mode=%#o", name, in->body.mkdir.mode); break; case FUSE_MKNOD: printf(" mode=%#o rdev=%x", in->body.mknod.mode, in->body.mknod.rdev); break; case FUSE_OPEN: printf(" flags=%#x mode=%#o", in->body.open.flags, in->body.open.mode); break; case FUSE_OPENDIR: printf(" flags=%#x mode=%#o", in->body.opendir.flags, in->body.opendir.mode); break; case FUSE_READ: - printf(" offset=%lu size=%u", in->body.read.offset, + printf(" offset=%" PRIu64 " size=%u", + in->body.read.offset, in->body.read.size); break; case FUSE_READDIR: - printf(" fh=%#lx offset=%lu size=%u", + printf(" fh=%#" PRIx64 " offset=%" PRIu64 " size=%u", in->body.readdir.fh, in->body.readdir.offset, in->body.readdir.size); break; case FUSE_RELEASE: - printf(" fh=%#lx flags=%#x lock_owner=%lu", + printf(" fh=%#" PRIx64 " flags=%#x lock_owner=%" PRIu64, in->body.release.fh, in->body.release.flags, in->body.release.lock_owner); break; case FUSE_SETATTR: if (verbosity <= 1) { printf(" valid=%#x", in->body.setattr.valid); break; } if (in->body.setattr.valid & FATTR_MODE) printf(" mode=%#o", in->body.setattr.mode); if (in->body.setattr.valid & FATTR_UID) printf(" uid=%u", in->body.setattr.uid); if (in->body.setattr.valid & FATTR_GID) printf(" gid=%u", in->body.setattr.gid); if (in->body.setattr.valid & FATTR_SIZE) - printf(" size=%zu", in->body.setattr.size); + printf(" size=%" PRIu64, in->body.setattr.size); if (in->body.setattr.valid & FATTR_ATIME) - printf(" atime=%zu.%u", + printf(" atime=%" PRIu64 ".%u", in->body.setattr.atime, in->body.setattr.atimensec); if (in->body.setattr.valid & FATTR_MTIME) - printf(" mtime=%zu.%u", + printf(" mtime=%" PRIu64 ".%u", in->body.setattr.mtime, in->body.setattr.mtimensec); if (in->body.setattr.valid & FATTR_FH) - printf(" fh=%zu", in->body.setattr.fh); + printf(" fh=%" PRIu64 "", in->body.setattr.fh); break; case FUSE_SETLK: - printf(" fh=%#lx owner=%lu type=%u pid=%u", + printf(" fh=%#" PRIx64 " owner=%" PRIu64 + " type=%u pid=%u", in->body.setlk.fh, in->body.setlk.owner, in->body.setlk.lk.type, in->body.setlk.lk.pid); if (verbosity >= 2) { - printf(" range=[%lu-%lu]", + printf(" range=[%" PRIu64 "-%" PRIu64 "]", in->body.setlk.lk.start, in->body.setlk.lk.end); } break; case FUSE_SETXATTR: /* * In theory neither the xattr name and value need be * ASCII, but in this test suite they always are. */ name = (const char*)in->body.bytes + sizeof(fuse_setxattr_in); value = name + strlen(name) + 1; printf(" %s=%s", name, value); break; case FUSE_WRITE: - printf(" fh=%#lx offset=%lu size=%u flags=%u", + printf(" fh=%#" PRIx64 " offset=%" PRIu64 + " size=%u flags=%u", in->body.write.fh, in->body.write.offset, in->body.write.size, in->body.write.write_flags); break; default: break; } printf("\n"); } MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions, bool push_symlinks_in, bool ro, enum poll_method pm, uint32_t flags, uint32_t kernel_minor_version) { struct sigaction sa; struct iovec *iov = NULL; int iovlen = 0; char fdstr[15]; const bool trueval = true; m_daemon_id = NULL; m_kernel_minor_version = kernel_minor_version; m_maxreadahead = max_readahead; m_nready = -1; m_pm = pm; m_quit = false; if (m_pm == KQ) m_kq = kqueue(); else m_kq = -1; /* * Kyua sets pwd to a testcase-unique tempdir; no need to use * mkdtemp */ /* * googletest doesn't allow ASSERT_ in constructors, so we must throw * instead. */ if (mkdir("mountpoint" , 0755) && errno != EEXIST) throw(std::system_error(errno, std::system_category(), "Couldn't make mountpoint directory")); switch (m_pm) { case BLOCKING: m_fuse_fd = open("/dev/fuse", O_CLOEXEC | O_RDWR); break; default: m_fuse_fd = open("/dev/fuse", O_CLOEXEC | O_RDWR | O_NONBLOCK); break; } if (m_fuse_fd < 0) throw(std::system_error(errno, std::system_category(), "Couldn't open /dev/fuse")); m_pid = getpid(); m_child_pid = -1; build_iovec(&iov, &iovlen, "fstype", __DECONST(void *, "fusefs"), -1); build_iovec(&iov, &iovlen, "fspath", __DECONST(void *, "mountpoint"), -1); build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1); sprintf(fdstr, "%d", m_fuse_fd); build_iovec(&iov, &iovlen, "fd", fdstr, -1); if (allow_other) { build_iovec(&iov, &iovlen, "allow_other", __DECONST(void*, &trueval), sizeof(bool)); } if (default_permissions) { build_iovec(&iov, &iovlen, "default_permissions", __DECONST(void*, &trueval), sizeof(bool)); } if (push_symlinks_in) { build_iovec(&iov, &iovlen, "push_symlinks_in", __DECONST(void*, &trueval), sizeof(bool)); } if (ro) { build_iovec(&iov, &iovlen, "ro", __DECONST(void*, &trueval), sizeof(bool)); } if (nmount(iov, iovlen, 0)) throw(std::system_error(errno, std::system_category(), "Couldn't mount filesystem")); // Setup default handler ON_CALL(*this, process(_, _)) .WillByDefault(Invoke(this, &MockFS::process_default)); init(flags); bzero(&sa, sizeof(sa)); sa.sa_handler = sigint_handler; sa.sa_flags = 0; /* Don't set SA_RESTART! */ if (0 != sigaction(SIGUSR1, &sa, NULL)) throw(std::system_error(errno, std::system_category(), "Couldn't handle SIGUSR1")); if (pthread_create(&m_daemon_id, NULL, service, (void*)this)) throw(std::system_error(errno, std::system_category(), "Couldn't Couldn't start fuse thread")); } MockFS::~MockFS() { kill_daemon(); if (m_daemon_id != NULL) { pthread_join(m_daemon_id, NULL); m_daemon_id = NULL; } ::unmount("mountpoint", MNT_FORCE); rmdir("mountpoint"); if (m_kq >= 0) close(m_kq); } void MockFS::init(uint32_t flags) { mockfs_buf_in *in; mockfs_buf_out *out; - in = (mockfs_buf_in*) malloc(sizeof(*in)); + in = new mockfs_buf_in; ASSERT_TRUE(in != NULL); - out = (mockfs_buf_out*) malloc(sizeof(*out)); + out = new mockfs_buf_out; ASSERT_TRUE(out != NULL); read_request(in); ASSERT_EQ(FUSE_INIT, in->header.opcode); - memset(out, 0, sizeof(*out)); out->header.unique = in->header.unique; out->header.error = 0; out->body.init.major = FUSE_KERNEL_VERSION; out->body.init.minor = m_kernel_minor_version;; out->body.init.flags = in->body.init.flags & flags; /* * The default max_write is set to this formula in libfuse, though * individual filesystems can lower it. The "- 4096" was added in * commit 154ffe2, with the commit message "fix". */ uint32_t default_max_write = 32 * getpagesize() + 0x1000 - 4096; /* For testing purposes, it should be distinct from MAXPHYS */ m_max_write = MIN(default_max_write, MAXPHYS / 2); out->body.init.max_write = m_max_write; out->body.init.max_readahead = m_maxreadahead; SET_OUT_HEADER_LEN(out, init); write(m_fuse_fd, out, out->header.len); - free(in); + delete out; + delete in; } void MockFS::kill_daemon() { m_quit = true; if (m_daemon_id != NULL) pthread_kill(m_daemon_id, SIGUSR1); // Closing the /dev/fuse file descriptor first allows unmount to // succeed even if the daemon doesn't correctly respond to commands // during the unmount sequence. close(m_fuse_fd); m_fuse_fd = -1; } void MockFS::loop() { mockfs_buf_in *in; std::vector out; in = (mockfs_buf_in*) malloc(sizeof(*in)); ASSERT_TRUE(in != NULL); while (!m_quit) { bzero(in, sizeof(*in)); read_request(in); if (m_quit) break; if (verbosity > 0) debug_fuseop(in); if (pid_ok((pid_t)in->header.pid)) { process(in, out); } else { /* * Reject any requests from unknown processes. Because * we actually do mount a filesystem, plenty of * unrelated system daemons may try to access it. */ if (verbosity > 1) printf("\tREJECTED (wrong pid %d)\n", in->header.pid); process_default(in, out); } for (auto &it: out) { write_response(it); delete it; } out.clear(); } free(in); } bool MockFS::pid_ok(pid_t pid) { if (pid == m_pid) { return (true); } else if (pid == m_child_pid) { return (true); } else { struct kinfo_proc *ki; bool ok = false; ki = kinfo_getproc(pid); if (ki == NULL) return (false); /* * Allow access by the aio daemon processes so that our tests * can use aio functions */ if (0 == strncmp("aiod", ki->ki_comm, 4)) ok = true; free(ki); return (ok); } } void MockFS::process_default(const mockfs_buf_in *in, std::vector &out) { auto out0 = new mockfs_buf_out; out0->header.unique = in->header.unique; out0->header.error = -EOPNOTSUPP; out0->header.len = sizeof(out0->header); out.push_back(out0); } void MockFS::read_request(mockfs_buf_in *in) { ssize_t res; int nready = 0; fd_set readfds; pollfd fds[1]; struct kevent changes[1]; struct kevent events[1]; struct timespec timeout_ts; struct timeval timeout_tv; const int timeout_ms = 999; int timeout_int, nfds; switch (m_pm) { case BLOCKING: break; case KQ: timeout_ts.tv_sec = 0; timeout_ts.tv_nsec = timeout_ms * 1'000'000; while (nready == 0) { EV_SET(&changes[0], m_fuse_fd, EVFILT_READ, EV_ADD, 0, 0, 0); nready = kevent(m_kq, &changes[0], 1, &events[0], 1, &timeout_ts); if (m_quit) return; } ASSERT_LE(0, nready) << strerror(errno); ASSERT_EQ(events[0].ident, (uintptr_t)m_fuse_fd); if (events[0].flags & EV_ERROR) FAIL() << strerror(events[0].data); else if (events[0].flags & EV_EOF) FAIL() << strerror(events[0].fflags); m_nready = events[0].data; break; case POLL: timeout_int = timeout_ms; fds[0].fd = m_fuse_fd; fds[0].events = POLLIN; while (nready == 0) { nready = poll(fds, 1, timeout_int); if (m_quit) return; } ASSERT_LE(0, nready) << strerror(errno); ASSERT_TRUE(fds[0].revents & POLLIN); break; case SELECT: timeout_tv.tv_sec = 0; timeout_tv.tv_usec = timeout_ms * 1'000; nfds = m_fuse_fd + 1; while (nready == 0) { FD_ZERO(&readfds); FD_SET(m_fuse_fd, &readfds); nready = select(nfds, &readfds, NULL, NULL, &timeout_tv); if (m_quit) return; } ASSERT_LE(0, nready) << strerror(errno); ASSERT_TRUE(FD_ISSET(m_fuse_fd, &readfds)); break; default: FAIL() << "not yet implemented"; } res = read(m_fuse_fd, in, sizeof(*in)); if (res < 0 && !m_quit) perror("read"); ASSERT_TRUE(res >= (ssize_t)sizeof(in->header) || m_quit); } void MockFS::write_response(mockfs_buf_out *out) { fd_set writefds; pollfd fds[1]; int nready, nfds; ssize_t r; switch (m_pm) { case BLOCKING: case KQ: /* EVFILT_WRITE is not supported */ break; case POLL: fds[0].fd = m_fuse_fd; fds[0].events = POLLOUT; nready = poll(fds, 1, INFTIM); ASSERT_LE(0, nready) << strerror(errno); ASSERT_EQ(1, nready) << "NULL timeout expired?"; ASSERT_TRUE(fds[0].revents & POLLOUT); break; case SELECT: FD_ZERO(&writefds); FD_SET(m_fuse_fd, &writefds); nfds = m_fuse_fd + 1; nready = select(nfds, NULL, &writefds, NULL, NULL); ASSERT_LE(0, nready) << strerror(errno); ASSERT_EQ(1, nready) << "NULL timeout expired?"; ASSERT_TRUE(FD_ISSET(m_fuse_fd, &writefds)); break; default: FAIL() << "not yet implemented"; } r = write(m_fuse_fd, out, out->header.len); ASSERT_TRUE(r > 0 || errno == EAGAIN) << strerror(errno); } void* MockFS::service(void *pthr_data) { MockFS *mock_fs = (MockFS*)pthr_data; mock_fs->loop(); return (NULL); } void MockFS::unmount() { ::unmount("mountpoint", 0); } Index: projects/fuse2/tests/sys/fs/fusefs/mockfs.hh =================================================================== --- projects/fuse2/tests/sys/fs/fusefs/mockfs.hh (revision 348284) +++ projects/fuse2/tests/sys/fs/fusefs/mockfs.hh (revision 348285) @@ -1,320 +1,320 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ extern "C" { #include #include #include "fuse_kernel.h" } #include #define TIME_T_MAX (std::numeric_limits::max()) /* * A pseudo-fuse errno used indicate that a fuse operation should have no * response, at least not immediately */ #define FUSE_NORESPONSE 9999 #define SET_OUT_HEADER_LEN(out, variant) { \ (out)->header.len = (sizeof((out)->header) + \ sizeof((out)->body.variant)); \ } /* * Create an expectation on FUSE_LOOKUP and return it so the caller can set * actions. * * This must be a macro instead of a method because EXPECT_CALL returns a type * with a deleted constructor. */ #define EXPECT_LOOKUP(parent, path) \ EXPECT_CALL(*m_mock, process( \ ResultOf([=](auto in) { \ return (in->header.opcode == FUSE_LOOKUP && \ in->header.nodeid == (parent) && \ strcmp(in->body.lookup, (path)) == 0); \ }, Eq(true)), \ _) \ ) extern int verbosity; /* This struct isn't defined by fuse_kernel.h or libfuse, but it should be */ struct fuse_create_out { struct fuse_entry_out entry; struct fuse_open_out open; }; /* Protocol 7.8 version of struct fuse_attr */ struct fuse_attr_7_8 { __u64 ino; __u64 size; __u64 blocks; __u64 atime; __u64 mtime; __u64 ctime; __u32 atimensec; __u32 mtimensec; __u32 ctimensec; __u32 mode; __u32 nlink; __u32 uid; __u32 gid; __u32 rdev; }; /* Protocol 7.8 version of struct fuse_attr_out */ struct fuse_attr_out_7_8 { __u64 attr_valid; __u32 attr_valid_nsec; __u32 dummy; struct fuse_attr_7_8 attr; }; /* Protocol 7.8 version of struct fuse_entry_out */ struct fuse_entry_out_7_8 { __u64 nodeid; /* Inode ID */ __u64 generation; /* Inode generation: nodeid:gen must be unique for the fs's lifetime */ __u64 entry_valid; /* Cache timeout for the name */ __u64 attr_valid; /* Cache timeout for the attributes */ __u32 entry_valid_nsec; __u32 attr_valid_nsec; struct fuse_attr_7_8 attr; }; /* Output struct for FUSE_CREATE for protocol 7.8 servers */ struct fuse_create_out_7_8 { struct fuse_entry_out_7_8 entry; struct fuse_open_out open; }; union fuse_payloads_in { fuse_access_in access; /* value is from fuse_kern_chan.c in fusefs-libs */ uint8_t bytes[0x21000 - sizeof(struct fuse_in_header)]; fuse_flush_in flush; fuse_fsync_in fsync; fuse_fsync_in fsyncdir; fuse_forget_in forget; fuse_interrupt_in interrupt; fuse_lk_in getlk; fuse_getxattr_in getxattr; fuse_init_in init; fuse_link_in link; fuse_listxattr_in listxattr; char lookup[0]; fuse_mkdir_in mkdir; fuse_mknod_in mknod; fuse_open_in open; fuse_open_in opendir; fuse_read_in read; fuse_read_in readdir; fuse_release_in release; fuse_release_in releasedir; fuse_rename_in rename; char rmdir[0]; fuse_setattr_in setattr; fuse_setxattr_in setxattr; fuse_lk_in setlk; fuse_lk_in setlkw; char unlink[0]; fuse_write_in write; }; struct mockfs_buf_in { fuse_in_header header; union fuse_payloads_in body; }; union fuse_payloads_out { fuse_attr_out attr; fuse_attr_out_7_8 attr_7_8; fuse_create_out create; fuse_create_out_7_8 create_7_8; /* The protocol places no limits on the size of bytes */ - uint8_t bytes[0x20000]; + uint8_t bytes[0x20000]; fuse_entry_out entry; fuse_entry_out_7_8 entry_7_8; fuse_lk_out getlk; fuse_getxattr_out getxattr; fuse_init_out init; fuse_listxattr_out listxattr; fuse_open_out open; fuse_statfs_out statfs; /* * The protocol places no limits on the length of the string. This is * merely convenient for testing. */ char str[80]; fuse_write_out write; }; struct mockfs_buf_out { fuse_out_header header; union fuse_payloads_out body; /* Default constructor: zero everything */ mockfs_buf_out() { memset(this, 0, sizeof(*this)); } }; /* A function that can be invoked in place of MockFS::process */ typedef std::function &out)> ProcessMockerT; /* * Helper function used for setting an error expectation for any fuse operation. * The operation will return the supplied error */ ProcessMockerT ReturnErrno(int error); /* Helper function used for returning negative cache entries for LOOKUP */ ProcessMockerT ReturnNegativeCache(const struct timespec *entry_valid); /* Helper function used for returning a single immediate response */ ProcessMockerT ReturnImmediate( std::function f); /* How the daemon should check /dev/fuse for readiness */ enum poll_method { BLOCKING, SELECT, POLL, KQ }; /* * Fake FUSE filesystem * * "Mounts" a filesystem to a temporary directory and services requests * according to the programmed expectations. * * Operates directly on the fusefs(4) kernel API, not the libfuse(3) user api. */ class MockFS { /* * thread id of the fuse daemon thread * * It must run in a separate thread so it doesn't deadlock with the * client test code. */ pthread_t m_daemon_id; /* file descriptor of /dev/fuse control device */ int m_fuse_fd; /* The minor version of the kernel API that this mock daemon targets */ uint32_t m_kernel_minor_version; int m_kq; /* The max_readahead filesystem option */ uint32_t m_maxreadahead; /* pid of the test process */ pid_t m_pid; /* Method the daemon should use for I/O to and from /dev/fuse */ enum poll_method m_pm; /* Initialize a session after mounting */ void init(uint32_t flags); /* Is pid from a process that might be involved in the test? */ bool pid_ok(pid_t pid); /* Default request handler */ void process_default(const mockfs_buf_in*, std::vector&); /* Entry point for the daemon thread */ static void* service(void*); /* Read, but do not process, a single request from the kernel */ void read_request(mockfs_buf_in*); /* Write a single response back to the kernel */ void write_response(mockfs_buf_out *out); public: /* pid of child process, for two-process test cases */ pid_t m_child_pid; /* Maximum size of a FUSE_WRITE write */ uint32_t m_max_write; /* * Number of events that were available from /dev/fuse after the last * kevent call. Only valid when m_pm = KQ. */ int m_nready; /* Tell the daemon to shut down ASAP */ bool m_quit; /* Create a new mockfs and mount it to a tempdir */ MockFS(int max_readahead, bool allow_other, bool default_permissions, bool push_symlinks_in, bool ro, enum poll_method pm, uint32_t flags, uint32_t kernel_minor_version); virtual ~MockFS(); /* Kill the filesystem daemon without unmounting the filesystem */ void kill_daemon(); /* Process FUSE requests endlessly */ void loop(); /* * Request handler * * This method is expected to provide the responses to each FUSE * operation. For an immediate response, push one buffer into out. * For a delayed response, push nothing. For an immediate response * plus a delayed response to an earlier operation, push two bufs. * Test cases must define each response using Googlemock expectations */ MOCK_METHOD2(process, void(const mockfs_buf_in*, std::vector&)); /* Gracefully unmount */ void unmount(); }; Index: projects/fuse2/tests/sys/fs/fusefs/read.cc =================================================================== --- projects/fuse2/tests/sys/fs/fusefs/read.cc (revision 348284) +++ projects/fuse2/tests/sys/fs/fusefs/read.cc (revision 348285) @@ -1,782 +1,782 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ extern "C" { #include #include #include #include #include #include #include #include #include } #include "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 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() { const char *node = "vfs.aio.enable_unsafe"; int val = 0; size_t size = sizeof(val); FuseTest::SetUp(); ASSERT_EQ(0, sysctlbyname(node, &val, &size, NULL, 0)) << strerror(errno); if (!val) GTEST_SKIP() << "vfs.aio.enable_unsafe must be set for this test"; } }; class AsyncRead: public AioRead { virtual void SetUp() { m_init_flags = FUSE_ASYNC_READ; AioRead::SetUp(); } }; class ReadCacheable: public Read { public: virtual void SetUp() { const char *node = "vfs.fusefs.data_cache_mode"; int val = 0; size_t size = sizeof(val); FuseTest::SetUp(); ASSERT_EQ(0, sysctlbyname(node, &val, &size, NULL, 0)) << strerror(errno); if (val == 0) GTEST_SKIP() << "fusefs data caching must be enabled for this test"; } }; class ReadAhead: public ReadCacheable, public WithParamInterface { virtual void SetUp() { m_maxreadahead = GetParam(); Read::SetUp(); } }; /* AIO reads need to set the header's pid field correctly */ /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236379 */ TEST_F(AioRead, aio_read) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); char buf[bufsize]; struct aiocb iocb, *piocb; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); iocb.aio_nbytes = bufsize; iocb.aio_fildes = fd; iocb.aio_buf = buf; iocb.aio_offset = 0; iocb.aio_sigevent.sigev_notify = SIGEV_NONE; ASSERT_EQ(0, aio_read(&iocb)) << strerror(errno); ASSERT_EQ(bufsize, aio_waitcomplete(&piocb, NULL)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); /* Deliberately leak fd. close(2) will be tested in release.cc */ } /* * Without the FUSE_ASYNC_READ mount option, fuse(4) should ensure that there * is at most one outstanding read operation per file handle */ TEST_F(AioRead, async_read_disabled) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; ssize_t bufsize = 50; char buf0[bufsize], buf1[bufsize]; off_t off0 = 0; off_t off1 = 65536; 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); /* Deliberately leak fd. close(2) will be tested in release.cc */ } /* * With the FUSE_ASYNC_READ mount option, fuse(4) may issue multiple * simultaneous read requests on the same file handle. */ 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 = 65536; struct aiocb iocb0, iocb1; sem_t sem; ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); 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)), _) ).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); /* Deliberately leak fd. close(2) will be tested in release.cc */ } /* 0-length reads shouldn't cause any confusion */ TEST_F(Read, direct_io_read_nothing) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; uint64_t offset = 100; char buf[80]; expect_lookup(RELPATH, ino, offset + 1000); expect_open(ino, FOPEN_DIRECT_IO, 1); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, pread(fd, buf, 0, offset)) << strerror(errno); /* Deliberately leak fd. close(2) will be tested in release.cc */ } /* * With direct_io, reads should not fill the cache. They should go straight to * the daemon */ TEST_F(Read, direct_io_pread) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; uint64_t offset = 100; ssize_t bufsize = strlen(CONTENTS); char buf[bufsize]; expect_lookup(RELPATH, ino, offset + bufsize); expect_open(ino, FOPEN_DIRECT_IO, 1); expect_read(ino, offset, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, pread(fd, buf, bufsize, offset)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); /* Deliberately leak fd. close(2) will be tested in release.cc */ } /* * With direct_io, filesystems are allowed to return less data than is * requested. fuse(4) should return a short read to userland. */ TEST_F(Read, direct_io_short_read) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefghijklmnop"; uint64_t ino = 42; int fd; uint64_t offset = 100; ssize_t bufsize = strlen(CONTENTS); ssize_t halfbufsize = bufsize / 2; char buf[bufsize]; expect_lookup(RELPATH, ino, offset + bufsize); expect_open(ino, FOPEN_DIRECT_IO, 1); expect_read(ino, offset, bufsize, halfbufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(halfbufsize, pread(fd, buf, bufsize, offset)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, halfbufsize)); /* Deliberately leak fd. close(2) will be tested in release.cc */ } TEST_F(Read, eio) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); char buf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_READ); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(EIO))); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(-1, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(EIO, errno); /* Deliberately leak fd. close(2) will be tested in release.cc */ } /* * With the keep_cache option, the kernel may keep its read cache across * multiple open(2)s. */ TEST_F(ReadCacheable, keep_cache) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd0, fd1; ssize_t bufsize = strlen(CONTENTS); char buf[bufsize]; FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, bufsize, 2); expect_open(ino, FOPEN_KEEP_CACHE, 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); /* Deliberately leak fd0 and fd1. */ } /* * Without the keep_cache option, the kernel should drop its read caches on * every open */ TEST_F(Read, keep_cache_disabled) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd0, fd1; ssize_t bufsize = strlen(CONTENTS); char buf[bufsize]; FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, bufsize, 2); expect_open(ino, 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); /* Deliberately leak fd0 and fd1. */ } TEST_F(ReadCacheable, mmap) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t len; - ssize_t bufsize = strlen(CONTENTS); + size_t bufsize = strlen(CONTENTS); void *p; - //char buf[bufsize]; len = getpagesize(); expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); /* mmap may legitimately try to read more data than is available */ EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_READ && in->header.nodeid == ino && in->body.read.fh == Read::FH && in->body.read.offset == 0 && in->body.read.size >= bufsize); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { out->header.len = sizeof(struct fuse_out_header) + bufsize; memmove(out->body.bytes, CONTENTS, bufsize); }))); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0); ASSERT_NE(MAP_FAILED, p) << strerror(errno); ASSERT_EQ(0, memcmp(p, CONTENTS, bufsize)); ASSERT_EQ(0, munmap(p, len)) << strerror(errno); /* Deliberately leak fd. close(2) will be tested in release.cc */ } /* * Just as when FOPEN_DIRECT_IO is used, reads with O_DIRECT should bypass * cache and to straight to the daemon */ TEST_F(Read, o_direct) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); char buf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); // Fill the cache ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); // Reads with o_direct should bypass the cache expect_read(ino, 0, bufsize, bufsize, CONTENTS); ASSERT_EQ(0, fcntl(fd, F_SETFL, O_DIRECT)) << strerror(errno); ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno); ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); /* Deliberately leak fd. close(2) will be tested in release.cc */ } TEST_F(Read, pread) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; /* * Set offset to a maxbcachebuf boundary so we'll be sure what offset * to read from. Without this, the read might start at a lower offset. */ uint64_t offset = m_maxbcachebuf; ssize_t bufsize = strlen(CONTENTS); char buf[bufsize]; expect_lookup(RELPATH, ino, offset + bufsize); expect_open(ino, 0, 1); expect_read(ino, offset, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, pread(fd, buf, bufsize, offset)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); /* Deliberately leak fd. close(2) will be tested in release.cc */ } TEST_F(Read, read) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); char buf[bufsize]; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); expect_read(ino, 0, bufsize, bufsize, CONTENTS); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); /* Deliberately leak fd. close(2) will be tested in release.cc */ } 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); char 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)); /* Deliberately leak fd. close(2) will be tested in release.cc */ } /* If the filesystem allows it, the kernel should try to readahead */ TEST_F(ReadCacheable, default_readahead) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS0 = "abcdefghijklmnop"; uint64_t ino = 42; int fd; ssize_t bufsize = 8; /* hard-coded in fuse_internal.c */ size_t default_maxreadahead = 65536; ssize_t filesize = default_maxreadahead * 2; char *contents; char buf[bufsize]; const char *contents1 = CONTENTS0 + bufsize; contents = (char*)calloc(1, filesize); ASSERT_NE(NULL, contents); memmove(contents, CONTENTS0, strlen(CONTENTS0)); expect_lookup(RELPATH, ino, filesize); expect_open(ino, 0, 1); expect_read(ino, 0, default_maxreadahead, default_maxreadahead, contents); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS0, bufsize)); /* A subsequent read should be serviced by cache */ ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, contents1, bufsize)); /* Deliberately leak fd. close(2) will be tested in release.cc */ } /* Reading with sendfile should work (though it obviously won't be 0-copy) */ TEST_F(ReadCacheable, sendfile) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; - ssize_t bufsize = strlen(CONTENTS); + size_t bufsize = strlen(CONTENTS); char buf[bufsize]; int sp[2]; off_t sbytes; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); /* Like mmap, sendfile may request more data than is available */ EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_READ && in->header.nodeid == ino && in->body.read.fh == Read::FH && in->body.read.offset == 0 && in->body.read.size >= bufsize); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { out->header.len = sizeof(struct fuse_out_header) + bufsize; memmove(out->body.bytes, CONTENTS, bufsize); }))); ASSERT_EQ(0, socketpair(PF_LOCAL, SOCK_STREAM, 0, sp)) << strerror(errno); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, sendfile(fd, sp[1], 0, bufsize, NULL, &sbytes, 0)) << strerror(errno); - ASSERT_EQ(bufsize, read(sp[0], buf, bufsize)) << strerror(errno); + ASSERT_EQ((ssize_t)bufsize, read(sp[0], buf, bufsize)) + << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); close(sp[1]); close(sp[0]); /* Deliberately leak fd. close(2) will be tested in release.cc */ } /* sendfile should fail gracefully if fuse declines the read */ /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236466 */ TEST_F(ReadCacheable, DISABLED_sendfile_eio) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS = "abcdefgh"; uint64_t ino = 42; int fd; ssize_t bufsize = strlen(CONTENTS); int sp[2]; off_t sbytes; expect_lookup(RELPATH, ino, bufsize); expect_open(ino, 0, 1); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_READ); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(EIO))); ASSERT_EQ(0, socketpair(PF_LOCAL, SOCK_STREAM, 0, sp)) << strerror(errno); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_NE(0, sendfile(fd, sp[1], 0, bufsize, NULL, &sbytes, 0)); close(sp[1]); close(sp[0]); /* Deliberately leak fd. close(2) will be tested in release.cc */ } /* fuse(4) should honor the filesystem's requested m_readahead parameter */ /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236472 */ TEST_P(ReadAhead, DISABLED_readahead) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const char *CONTENTS0 = "abcdefghijklmnop"; uint64_t ino = 42; int fd; ssize_t bufsize = 8; ssize_t filesize = m_maxbcachebuf * 2; char *contents; char buf[bufsize]; ASSERT_TRUE(GetParam() < (uint32_t)m_maxbcachebuf) << "Test assumes that max_readahead < maxbcachebuf"; contents = (char*)calloc(1, filesize); ASSERT_NE(NULL, contents); memmove(contents, CONTENTS0, strlen(CONTENTS0)); expect_lookup(RELPATH, ino, filesize); expect_open(ino, 0, 1); /* fuse(4) should only read ahead the allowed amount */ expect_read(ino, 0, GetParam(), GetParam(), contents); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); ASSERT_EQ(0, memcmp(buf, CONTENTS0, bufsize)); /* Deliberately leak fd. close(2) will be tested in release.cc */ } INSTANTIATE_TEST_CASE_P(RA, ReadAhead, ::testing::Values(0u, 2048u)); Index: projects/fuse2/tests/sys/fs/fusefs/setattr.cc =================================================================== --- projects/fuse2/tests/sys/fs/fusefs/setattr.cc (revision 348284) +++ projects/fuse2/tests/sys/fs/fusefs/setattr.cc (revision 348285) @@ -1,783 +1,783 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ extern "C" { #include #include } #include "mockfs.hh" #include "utils.hh" using namespace testing; class Setattr : public FuseTest {}; class RofsSetattr: public Setattr { public: virtual void SetUp() { m_ro = true; Setattr::SetUp(); } }; class Setattr_7_8: public Setattr { public: virtual void SetUp() { m_kernel_minor_version = 8; Setattr::SetUp(); } }; /* * 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(1, RELPATH) .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, entry); out->body.entry.attr.mode = S_IFREG | 0644; out->body.entry.nodeid = ino; 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(1, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, entry); out->body.entry.attr.mode = S_IFREG | oldmode; out->body.entry.nodeid = ino; }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { /* In protocol 7.23, ctime will be changed too */ uint32_t valid = FATTR_MODE; return (in->header.opcode == FUSE_SETATTR && in->header.nodeid == ino && in->body.setattr.valid == valid && in->body.setattr.mode == newmode); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, attr); out->body.attr.attr.ino = ino; // Must match nodeid out->body.attr.attr.mode = S_IFREG | newmode; }))); EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); } /* * 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(1, 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(1, 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(1, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, entry); out->body.entry.attr.mode = S_IFREG | 0644; out->body.entry.nodeid = ino; out->body.entry.attr.gid = oldgroup; out->body.entry.attr.uid = olduser; }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { /* In protocol 7.23, ctime will be changed too */ uint32_t valid = FATTR_GID | FATTR_UID; return (in->header.opcode == FUSE_SETATTR && in->header.nodeid == ino && in->body.setattr.valid == valid && in->body.setattr.uid == newuser && in->body.setattr.gid == newgroup); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, attr); out->body.attr.attr.ino = ino; // Must match nodeid out->body.attr.attr.mode = S_IFREG | 0644; out->body.attr.attr.uid = newuser; out->body.attr.attr.gid = newgroup; }))); EXPECT_EQ(0, chown(FULLPATH, newuser, newgroup)) << strerror(errno); } /* * FUSE daemons are allowed to check permissions however they like. If the * daemon returns EPERM, even if the file permissions "should" grant access, * then fuse(4) should return EPERM too. */ TEST_F(Setattr, eperm) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; EXPECT_LOOKUP(1, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in, auto out) { SET_OUT_HEADER_LEN(out, entry); out->body.entry.attr.mode = S_IFREG | 0777; out->body.entry.nodeid = ino; out->body.entry.attr.uid = in->header.uid; out->body.entry.attr.gid = in->header.gid; }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in->header.opcode == FUSE_SETATTR && in->header.nodeid == ino); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(EPERM))); EXPECT_NE(0, truncate(FULLPATH, 10)); EXPECT_EQ(EPERM, errno); } /* Change the mode of an open file, by its file descriptor */ TEST_F(Setattr, fchmod) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; const mode_t oldmode = 0755; const mode_t newmode = 0644; EXPECT_LOOKUP(1, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, entry); out->body.entry.attr.mode = S_IFREG | oldmode; out->body.entry.nodeid = ino; out->body.entry.attr_valid = UINT64_MAX; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_OPEN && in->header.nodeid == ino); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { out->header.len = sizeof(out->header); SET_OUT_HEADER_LEN(out, open); }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { /* In protocol 7.23, ctime will be changed too */ uint32_t valid = FATTR_MODE; return (in->header.opcode == FUSE_SETATTR && in->header.nodeid == ino && in->body.setattr.valid == valid && in->body.setattr.mode == newmode); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, attr); out->body.attr.attr.ino = ino; // Must match nodeid out->body.attr.attr.mode = S_IFREG | newmode; }))); fd = open(FULLPATH, O_RDONLY); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno); /* Deliberately leak fd. close(2) will be tested in release.cc */ } /* Change the size of an open file, by its file descriptor */ TEST_F(Setattr, ftruncate) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; uint64_t ino = 42; int fd; uint64_t fh = 0xdeadbeef1a7ebabe; const off_t oldsize = 99; const off_t newsize = 12345; EXPECT_LOOKUP(1, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, entry); out->body.entry.attr.mode = S_IFREG | 0755; out->body.entry.nodeid = ino; out->body.entry.attr_valid = UINT64_MAX; out->body.entry.attr.size = oldsize; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_OPEN && in->header.nodeid == ino); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { out->header.len = sizeof(out->header); SET_OUT_HEADER_LEN(out, open); out->body.open.fh = fh; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { /* In protocol 7.23, ctime will be changed too */ uint32_t valid = FATTR_SIZE | FATTR_FH; return (in->header.opcode == FUSE_SETATTR && in->header.nodeid == ino && in->body.setattr.valid == valid && in->body.setattr.fh == fh); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, attr); out->body.attr.attr.ino = ino; // Must match nodeid out->body.attr.attr.mode = S_IFREG | 0755; out->body.attr.attr.size = newsize; }))); fd = open(FULLPATH, O_RDWR); ASSERT_LE(0, fd) << strerror(errno); ASSERT_EQ(0, ftruncate(fd, newsize)) << strerror(errno); /* Deliberately leak fd. close(2) will be tested in release.cc */ } /* Change the size of the file */ TEST_F(Setattr, truncate) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const uint64_t oldsize = 100'000'000; const uint64_t newsize = 20'000'000; EXPECT_LOOKUP(1, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, entry); out->body.entry.attr.mode = S_IFREG | 0644; out->body.entry.nodeid = ino; out->body.entry.attr.size = oldsize; }))); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { /* In protocol 7.23, ctime will be changed too */ uint32_t valid = FATTR_SIZE; return (in->header.opcode == FUSE_SETATTR && in->header.nodeid == ino && in->body.setattr.valid == valid && in->body.setattr.size == newsize); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, attr); out->body.attr.attr.ino = ino; // Must match nodeid out->body.attr.attr.mode = S_IFREG | 0644; out->body.attr.attr.size = newsize; }))); EXPECT_EQ(0, truncate(FULLPATH, newsize)) << strerror(errno); } /* * Truncating a file should discard cached data past the truncation point. * This is a regression test for bug 233783. The bug only applies when * vfs.fusefs.data_cache_mode=1 or 2, but the test should pass regardless. * * 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; 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(NULL, w0buf) << strerror(errno); memset(w0buf, 'X', w0_size); r0buf = malloc(r0_size); ASSERT_NE(NULL, r0buf) << strerror(errno); r1buf = malloc(r1_size); ASSERT_NE(NULL, r1buf) << strerror(errno); expected = malloc(r1_size); ASSERT_NE(NULL, expected) << strerror(errno); memset(expected, 0, 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; }))); /* * The exact pattern of FUSE_WRITE operations depends on the setting of * vfs.fusefs.data_cache_mode. But it's not important for this test. * Just set the mocks to accept anything */ 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(cur_size, + cur_size = std::max((uint64_t)cur_size, in->body.write.size + in->body.write.offset); }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_SETATTR && in->header.nodeid == ino && (in->body.setattr.valid & FATTR_SIZE)); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto out) { auto trunc_size = in->body.setattr.size; SET_OUT_HEADER_LEN(out, attr); out->body.attr.attr.ino = ino; out->body.attr.attr.mode = mode; out->body.attr.attr.size = trunc_size; cur_size = trunc_size; }))); /* exact pattern of FUSE_READ depends on vfs.fusefs.data_cache_mode */ 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(cur_size - in->body.read.offset, - (size_t)in->body.read.size); + auto osize = std::min((uint64_t)cur_size - in->body.read.offset, + (uint64_t)in->body.read.size); 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((ssize_t)w0_size, pwrite(fd, w0buf, w0_size, w0_offset)); should_have_data = true; /* Fill the cache, if data_cache_mode == 1 */ ASSERT_EQ((ssize_t)r0_size, pread(fd, r0buf, r0_size, r0_offset)); /* 1st truncate should discard cached data */ EXPECT_EQ(0, ftruncate(fd, trunc0_size)) << strerror(errno); should_have_data = false; /* 2nd truncate extends file into previously cached data */ EXPECT_EQ(0, ftruncate(fd, trunc1_size)) << strerror(errno); /* Read should return all zeros */ ASSERT_EQ((ssize_t)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); } /* Change a file's timestamps */ TEST_F(Setattr, utimensat) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const timespec oldtimes[2] = { {.tv_sec = 1, .tv_nsec = 2}, {.tv_sec = 3, .tv_nsec = 4}, }; const timespec newtimes[2] = { {.tv_sec = 5, .tv_nsec = 6}, {.tv_sec = 7, .tv_nsec = 8}, }; EXPECT_LOOKUP(1, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, entry); out->body.entry.attr.mode = S_IFREG | 0644; out->body.entry.nodeid = ino; out->body.entry.attr_valid = UINT64_MAX; out->body.entry.attr.atime = oldtimes[0].tv_sec; out->body.entry.attr.atimensec = oldtimes[0].tv_nsec; out->body.entry.attr.mtime = oldtimes[1].tv_sec; out->body.entry.attr.mtimensec = oldtimes[1].tv_nsec; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { /* In protocol 7.23, ctime will be changed too */ uint32_t valid = FATTR_ATIME | FATTR_MTIME; return (in->header.opcode == FUSE_SETATTR && in->header.nodeid == ino && in->body.setattr.valid == valid && in->body.setattr.atime == newtimes[0].tv_sec && in->body.setattr.atimensec == newtimes[0].tv_nsec && in->body.setattr.mtime == newtimes[1].tv_sec && in->body.setattr.mtimensec == newtimes[1].tv_nsec); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, attr); out->body.attr.attr.ino = ino; // Must match nodeid out->body.attr.attr.mode = S_IFREG | 0644; out->body.attr.attr.atime = newtimes[0].tv_sec; out->body.attr.attr.atimensec = newtimes[0].tv_nsec; out->body.attr.attr.mtime = newtimes[1].tv_sec; out->body.attr.attr.mtimensec = newtimes[1].tv_nsec; }))); EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0)) << strerror(errno); } /* Change a file mtime but not its atime */ TEST_F(Setattr, utimensat_mtime_only) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; const uint64_t ino = 42; const timespec oldtimes[2] = { {.tv_sec = 1, .tv_nsec = 2}, {.tv_sec = 3, .tv_nsec = 4}, }; const timespec newtimes[2] = { {.tv_sec = 5, .tv_nsec = UTIME_OMIT}, {.tv_sec = 7, .tv_nsec = 8}, }; EXPECT_LOOKUP(1, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, entry); out->body.entry.attr.mode = S_IFREG | 0644; out->body.entry.nodeid = ino; out->body.entry.attr_valid = UINT64_MAX; out->body.entry.attr.atime = oldtimes[0].tv_sec; out->body.entry.attr.atimensec = oldtimes[0].tv_nsec; out->body.entry.attr.mtime = oldtimes[1].tv_sec; out->body.entry.attr.mtimensec = oldtimes[1].tv_nsec; }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { /* In protocol 7.23, ctime will be changed too */ uint32_t valid = FATTR_MTIME; return (in->header.opcode == FUSE_SETATTR && in->header.nodeid == ino && in->body.setattr.valid == valid && in->body.setattr.mtime == newtimes[1].tv_sec && in->body.setattr.mtimensec == newtimes[1].tv_nsec); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, attr); out->body.attr.attr.ino = ino; // Must match nodeid out->body.attr.attr.mode = S_IFREG | 0644; out->body.attr.attr.atime = oldtimes[0].tv_sec; out->body.attr.attr.atimensec = oldtimes[0].tv_nsec; out->body.attr.attr.mtime = newtimes[1].tv_sec; out->body.attr.attr.mtimensec = newtimes[1].tv_nsec; }))); EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0)) << strerror(errno); } /* * 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(1, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, entry); out->body.entry.attr.mode = S_IFREG | 0644; out->body.entry.nodeid = ino; out->body.entry.attr_valid = UINT64_MAX; out->body.entry.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) { /* In protocol 7.23, ctime will be changed too */ 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); } /* 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(1, RELPATH) .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, entry); out->body.entry.attr.mode = S_IFREG | oldmode; out->body.entry.nodeid = ino; }))); 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(1, 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) { /* In protocol 7.23, ctime will be changed too */ uint32_t valid = FATTR_MODE; return (in->header.opcode == FUSE_SETATTR && in->header.nodeid == ino && in->body.setattr.valid == valid && in->body.setattr.mode == newmode); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, attr_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); } Index: projects/fuse2/tests/sys/fs/fusefs/utils.cc =================================================================== --- projects/fuse2/tests/sys/fs/fusefs/utils.cc (revision 348284) +++ projects/fuse2/tests/sys/fs/fusefs/utils.cc (revision 348285) @@ -1,544 +1,544 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ extern "C" { #include #include #include #include #include #include #include #include #include #include } #include #include "mockfs.hh" #include "utils.hh" using namespace testing; /* Check that fusefs(4) is accessible and the current user can mount(2) */ void check_environment() { const char *devnode = "/dev/fuse"; const char *usermount_node = "vfs.usermount"; int usermount_val = 0; size_t usermount_size = sizeof(usermount_val); if (eaccess(devnode, R_OK | W_OK)) { if (errno == ENOENT) { GTEST_SKIP() << devnode << " does not exist"; } else if (errno == EACCES) { GTEST_SKIP() << devnode << " is not accessible by the current user"; } else { GTEST_SKIP() << strerror(errno); } } sysctlbyname(usermount_node, &usermount_val, &usermount_size, NULL, 0); if (geteuid() != 0 && !usermount_val) GTEST_SKIP() << "current user is not allowed to mount"; } class FuseEnv: public Environment { virtual void SetUp() { } }; void FuseTest::SetUp() { const char *node = "vfs.maxbcachebuf"; int val = 0; size_t size = sizeof(val); /* * XXX check_environment should be called from FuseEnv::SetUp, but * can't due to https://github.com/google/googletest/issues/2189 */ check_environment(); if (IsSkipped()) return; ASSERT_EQ(0, sysctlbyname(node, &val, &size, NULL, 0)) << strerror(errno); m_maxbcachebuf = val; try { m_mock = new MockFS(m_maxreadahead, m_allow_other, m_default_permissions, m_push_symlinks_in, m_ro, m_pm, m_init_flags, m_kernel_minor_version); /* * FUSE_ACCESS is called almost universally. Expecting it in * each test case would be super-annoying. Instead, set a * default expectation for FUSE_ACCESS and return ENOSYS. * * Individual test cases can override this expectation since * googlemock evaluates expectations in LIFO order. */ EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_ACCESS); }, Eq(true)), _) ).Times(AnyNumber()) .WillRepeatedly(Invoke(ReturnErrno(ENOSYS))); } catch (std::system_error err) { FAIL() << err.what(); } } void FuseTest::expect_access(uint64_t ino, mode_t access_mode, int error) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_ACCESS && in->header.nodeid == ino && in->body.access.mask == access_mode); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(error))); } void FuseTest::expect_destroy(int error) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_DESTROY); }, Eq(true)), _) ).WillOnce(Invoke( ReturnImmediate([&](auto in, auto out) { m_mock->m_quit = true; out->header.len = sizeof(out->header); out->header.unique = in->header.unique; out->header.error = -error; }))); } void FuseTest::expect_flush(uint64_t ino, int times, ProcessMockerT r) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_FLUSH && in->header.nodeid == ino); }, Eq(true)), _) ).Times(times) .WillRepeatedly(Invoke(r)); } void FuseTest::expect_forget(uint64_t ino, uint64_t nlookup) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_FORGET && in->header.nodeid == ino && in->body.forget.nlookup == nlookup); }, Eq(true)), _) ).WillOnce(Invoke([](auto in __unused, auto &out __unused) { /* FUSE_FORGET has no response! */ })); } void FuseTest::expect_getattr(uint64_t ino, uint64_t size) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_GETATTR && in->header.nodeid == ino); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto out) { SET_OUT_HEADER_LEN(out, attr); out->body.attr.attr.ino = ino; // Must match nodeid out->body.attr.attr.mode = S_IFREG | 0644; out->body.attr.attr.size = size; out->body.attr.attr_valid = UINT64_MAX; }))); } void FuseTest::expect_lookup(const char *relpath, uint64_t ino, mode_t mode, uint64_t size, int times, uint64_t attr_valid, uid_t uid, gid_t gid) { EXPECT_LOOKUP(1, relpath) .Times(times) .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, entry); out->body.entry.attr.mode = mode; out->body.entry.nodeid = ino; out->body.entry.attr.nlink = 1; out->body.entry.attr_valid = attr_valid; out->body.entry.attr.size = size; out->body.entry.attr.uid = uid; out->body.entry.attr.gid = gid; }))); } void FuseTest::expect_lookup_7_8(const char *relpath, uint64_t ino, mode_t mode, uint64_t size, int times, uint64_t attr_valid, uid_t uid, gid_t gid) { EXPECT_LOOKUP(1, relpath) .Times(times) .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, entry_7_8); out->body.entry.attr.mode = mode; out->body.entry.nodeid = ino; out->body.entry.attr.nlink = 1; out->body.entry.attr_valid = attr_valid; out->body.entry.attr.size = size; out->body.entry.attr.uid = uid; out->body.entry.attr.gid = gid; }))); } void FuseTest::expect_open(uint64_t ino, uint32_t flags, int times) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_OPEN && in->header.nodeid == ino); }, Eq(true)), _) ).Times(times) .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto out) { out->header.len = sizeof(out->header); SET_OUT_HEADER_LEN(out, open); out->body.open.fh = FH; out->body.open.open_flags = flags; }))); } void FuseTest::expect_opendir(uint64_t ino) { /* opendir(3) calls fstatfs */ EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in->header.opcode == FUSE_STATFS); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto out) { SET_OUT_HEADER_LEN(out, statfs); }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_OPENDIR && in->header.nodeid == ino); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { out->header.len = sizeof(out->header); SET_OUT_HEADER_LEN(out, open); out->body.open.fh = FH; }))); } void FuseTest::expect_read(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, const void *contents) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_READ && in->header.nodeid == ino && in->body.read.fh == FH && in->body.read.offset == offset && in->body.read.size == isize); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { out->header.len = sizeof(struct fuse_out_header) + osize; memmove(out->body.bytes, contents, osize); }))).RetiresOnSaturation(); } void FuseTest::expect_readdir(uint64_t ino, uint64_t off, std::vector &ents) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_READDIR && in->header.nodeid == ino && in->body.readdir.fh == FH && in->body.readdir.offset == off); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in, auto out) { - struct fuse_dirent *fde = (struct fuse_dirent*)out->body.bytes; + struct fuse_dirent *fde = (struct fuse_dirent*)&(out->body); int i = 0; out->header.error = 0; out->header.len = 0; for (const auto& it: ents) { size_t entlen, entsize; fde->ino = it.d_fileno; fde->off = it.d_off; fde->type = it.d_type; fde->namelen = it.d_namlen; strncpy(fde->name, it.d_name, it.d_namlen); entlen = FUSE_NAME_OFFSET + fde->namelen; entsize = FUSE_DIRENT_SIZE(fde); /* * The FUSE protocol does not require zeroing out the * unused portion of the name. But it's a good * practice to prevent information disclosure to the * FUSE client, even though the client is usually the * kernel */ memset(fde->name + fde->namelen, 0, entsize - entlen); if (out->header.len + entsize > in->body.read.size) { printf("Overflow in readdir expectation: i=%d\n" , i); break; } out->header.len += entsize; fde = (struct fuse_dirent*) ((long*)fde + entsize / sizeof(long)); i++; } out->header.len += sizeof(out->header); }))); } void FuseTest::expect_release(uint64_t ino, uint64_t fh) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_RELEASE && in->header.nodeid == ino && in->body.release.fh == fh); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(0))); } void FuseTest::expect_releasedir(uint64_t ino, ProcessMockerT r) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_RELEASEDIR && in->header.nodeid == ino && in->body.release.fh == FH); }, Eq(true)), _) ).WillOnce(Invoke(r)); } void FuseTest::expect_unlink(uint64_t parent, const char *path, int error) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in->header.opcode == FUSE_UNLINK && 0 == strcmp(path, in->body.unlink) && in->header.nodeid == parent); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(error))); } void FuseTest::expect_write(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, uint32_t flags, const void *contents) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { const char *buf = (const char*)in->body.bytes + sizeof(struct fuse_write_in); bool pid_ok; if (in->body.write.write_flags & FUSE_WRITE_CACHE) pid_ok = true; else pid_ok = (pid_t)in->header.pid == getpid(); return (in->header.opcode == FUSE_WRITE && in->header.nodeid == ino && in->body.write.fh == FH && in->body.write.offset == offset && in->body.write.size == isize && pid_ok && in->body.write.write_flags == flags && 0 == bcmp(buf, contents, isize)); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, write); out->body.write.size = osize; }))); } void FuseTest::expect_write_7_8(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, uint32_t flags, const void *contents) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { const char *buf = (const char*)in->body.bytes + FUSE_COMPAT_WRITE_IN_SIZE; bool pid_ok = (pid_t)in->header.pid == getpid(); return (in->header.opcode == FUSE_WRITE && in->header.nodeid == ino && in->body.write.fh == FH && in->body.write.offset == offset && in->body.write.size == isize && pid_ok && in->body.write.write_flags == flags && 0 == bcmp(buf, contents, isize)); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { SET_OUT_HEADER_LEN(out, write); out->body.write.size = osize; }))); } void get_unprivileged_id(uid_t *uid, gid_t *gid) { struct passwd *pw; struct group *gr; /* * First try "tests", Kyua's default unprivileged user. XXX after * GoogleTest gains a proper Kyua wrapper, get this with the Kyua API */ pw = getpwnam("tests"); if (pw == NULL) { /* Fall back to "nobody" */ pw = getpwnam("nobody"); } if (pw == NULL) GTEST_SKIP() << "Test requires an unprivileged user"; /* Use group "nobody", which is Kyua's default unprivileged group */ gr = getgrnam("nobody"); if (gr == NULL) GTEST_SKIP() << "Test requires an unprivileged group"; *uid = pw->pw_uid; *gid = gr->gr_gid; } void FuseTest::fork(bool drop_privs, int *child_status, std::function parent_func, std::function child_func) { sem_t *sem; int mprot = PROT_READ | PROT_WRITE; int mflags = MAP_ANON | MAP_SHARED; pid_t child; uid_t uid; gid_t gid; if (drop_privs) { get_unprivileged_id(&uid, &gid); if (IsSkipped()) return; } sem = (sem_t*)mmap(NULL, sizeof(*sem), mprot, mflags, -1, 0); ASSERT_NE(MAP_FAILED, sem) << strerror(errno); ASSERT_EQ(0, sem_init(sem, 1, 0)) << strerror(errno); if ((child = ::fork()) == 0) { /* In child */ int err = 0; if (sem_wait(sem)) { perror("sem_wait"); err = 1; goto out; } if (drop_privs && 0 != setegid(gid)) { perror("setegid"); err = 1; goto out; } if (drop_privs && 0 != setreuid(-1, uid)) { perror("setreuid"); err = 1; goto out; } err = child_func(); out: sem_destroy(sem); _exit(err); } else if (child > 0) { /* * In parent. Cleanup must happen here, because it's still * privileged. */ m_mock->m_child_pid = child; ASSERT_NO_FATAL_FAILURE(parent_func()); /* Signal the child process to go */ ASSERT_EQ(0, sem_post(sem)) << strerror(errno); ASSERT_LE(0, wait(child_status)) << strerror(errno); } else { FAIL() << strerror(errno); } munmap(sem, sizeof(*sem)); return; } static void usage(char* progname) { fprintf(stderr, "Usage: %s [-v]\n\t-v increase verbosity\n", progname); exit(2); } int main(int argc, char **argv) { int ch; FuseEnv *fuse_env = new FuseEnv; InitGoogleTest(&argc, argv); AddGlobalTestEnvironment(fuse_env); while ((ch = getopt(argc, argv, "v")) != -1) { switch (ch) { case 'v': verbosity++; break; default: usage(argv[0]); break; } } return (RUN_ALL_TESTS()); }