diff --git a/tests/sys/fs/fusefs/mockfs.cc b/tests/sys/fs/fusefs/mockfs.cc index ddfb5527ef13..4eebbc2200b0 100644 --- a/tests/sys/fs/fusefs/mockfs.cc +++ b/tests/sys/fs/fusefs/mockfs.cc @@ -1,1034 +1,1039 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ extern "C" { #include #include #include #include #include #include #include #include #include #include #include #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 char* table[] = { "Unknown (opcode 0)", "LOOKUP", "FORGET", "GETATTR", "SETATTR", "READLINK", "SYMLINK", "Unknown (opcode 7)", "MKNOD", "MKDIR", "UNLINK", "RMDIR", "RENAME", "LINK", "OPEN", "READ", "WRITE", "STATFS", "RELEASE", "Unknown (opcode 19)", "FSYNC", "SETXATTR", "GETXATTR", "LISTXATTR", "REMOVEXATTR", "FLUSH", "INIT", "OPENDIR", "READDIR", "RELEASEDIR", "FSYNCDIR", "GETLK", "SETLK", "SETLKW", "ACCESS", "CREATE", "INTERRUPT", "BMAP", "DESTROY", "IOCTL", "POLL", "NOTIFY_REPLY", "BATCH_FORGET", "FALLOCATE", "READDIRPLUS", "RENAME2", "LSEEK", "COPY_FILE_RANGE", }; if (opcode >= nitems(table)) return ("Unknown (opcode > max)"); else return (table[opcode]); } ProcessMockerT ReturnErrno(int error) { return([=](auto in, auto &out) { std::unique_ptr out0(new mockfs_buf_out); out0->header.unique = in.header.unique; out0->header.error = -error; out0->header.len = sizeof(out0->header); out.push_back(std::move(out0)); }); } /* Helper function used for returning negative cache entries for LOOKUP */ ProcessMockerT ReturnNegativeCache(const struct timespec *entry_valid) { return([=](auto in, auto &out) { /* nodeid means ENOENT and cache it */ std::unique_ptr out0(new mockfs_buf_out); out0->body.entry.nodeid = 0; out0->header.unique = in.header.unique; out0->header.error = 0; out0->body.entry.entry_valid = entry_valid->tv_sec; out0->body.entry.entry_valid_nsec = entry_valid->tv_nsec; SET_OUT_HEADER_LEN(*out0, entry); out.push_back(std::move(out0)); }); } ProcessMockerT ReturnImmediate(std::function f) { return([=](auto& in, auto &out) { std::unique_ptr out0(new mockfs_buf_out); out0->header.unique = in.header.unique; f(in, *out0); out.push_back(std::move(out0)); }); } void sigint_handler(int __unused sig) { // Don't do anything except interrupt the daemon's read(2) call } void MockFS::debug_request(const mockfs_buf_in &in, ssize_t buflen) { printf("%-11s ino=%2" PRIu64, opcode2opname(in.header.opcode), in.header.nodeid); if (verbosity > 1) { printf(" uid=%5u gid=%5u pid=%5u unique=%" PRIu64 " len=%u" " buflen=%zd", in.header.uid, in.header.gid, in.header.pid, in.header.unique, in.header.len, buflen); } switch (in.header.opcode) { const char *name, *value; case FUSE_ACCESS: printf(" mask=%#x", in.body.access.mask); break; case FUSE_BMAP: printf(" block=%" PRIx64 " blocksize=%#x", in.body.bmap.block, in.body.bmap.blocksize); break; case FUSE_COPY_FILE_RANGE: printf(" off_in=%" PRIu64 " ino_out=%" PRIu64 " off_out=%" PRIu64 " size=%" PRIu64, in.body.copy_file_range.off_in, in.body.copy_file_range.nodeid_out, in.body.copy_file_range.off_out, in.body.copy_file_range.len); if (verbosity > 1) printf(" fh_in=%" PRIu64 " fh_out=%" PRIu64 " flags=%" PRIx64, in.body.copy_file_range.fh_in, in.body.copy_file_range.fh_out, in.body.copy_file_range.flags); break; case FUSE_CREATE: if (m_kernel_minor_version >= 12) name = (const char*)in.body.bytes + sizeof(fuse_create_in); else name = (const char*)in.body.bytes + sizeof(fuse_open_in); printf(" flags=%#x name=%s", in.body.open.flags, name); break; case FUSE_FALLOCATE: printf(" fh=%#" PRIx64 " offset=%" PRIu64 " length=%" PRIx64 " mode=%#x", in.body.fallocate.fh, in.body.fallocate.offset, in.body.fallocate.length, in.body.fallocate.mode); break; case FUSE_FLUSH: printf(" fh=%#" PRIx64 " lock_owner=%" PRIu64, in.body.flush.fh, in.body.flush.lock_owner); break; case FUSE_FORGET: printf(" nlookup=%" PRIu64, in.body.forget.nlookup); break; case FUSE_FSYNC: printf(" flags=%#x", in.body.fsync.fsync_flags); break; case FUSE_FSYNCDIR: printf(" flags=%#x", in.body.fsyncdir.fsync_flags); break; case FUSE_INTERRUPT: printf(" unique=%" PRIu64, in.body.interrupt.unique); break; case FUSE_LINK: printf(" oldnodeid=%" PRIu64, in.body.link.oldnodeid); break; case FUSE_LISTXATTR: printf(" size=%" PRIu32, in.body.listxattr.size); break; case FUSE_LOOKUP: printf(" %s", in.body.lookup); break; case FUSE_LSEEK: switch (in.body.lseek.whence) { case SEEK_HOLE: printf(" SEEK_HOLE offset=%jd", in.body.lseek.offset); break; case SEEK_DATA: printf(" SEEK_DATA offset=%jd", in.body.lseek.offset); break; default: printf(" whence=%u offset=%jd", in.body.lseek.whence, in.body.lseek.offset); break; } break; case FUSE_MKDIR: name = (const char*)in.body.bytes + sizeof(fuse_mkdir_in); printf(" name=%s mode=%#o umask=%#o", name, in.body.mkdir.mode, in.body.mkdir.umask); break; case FUSE_MKNOD: if (m_kernel_minor_version >= 12) name = (const char*)in.body.bytes + sizeof(fuse_mknod_in); else name = (const char*)in.body.bytes + FUSE_COMPAT_MKNOD_IN_SIZE; printf(" mode=%#o rdev=%x umask=%#o name=%s", in.body.mknod.mode, in.body.mknod.rdev, in.body.mknod.umask, name); break; case FUSE_OPEN: printf(" flags=%#x", in.body.open.flags); break; case FUSE_OPENDIR: printf(" flags=%#x", in.body.opendir.flags); break; case FUSE_READ: printf(" offset=%" PRIu64 " size=%u", in.body.read.offset, in.body.read.size); if (verbosity > 1) printf(" flags=%#x", in.body.read.flags); break; case FUSE_READDIR: printf(" fh=%#" PRIx64 " offset=%" PRIu64 " size=%u", in.body.readdir.fh, in.body.readdir.offset, in.body.readdir.size); break; case FUSE_RELEASE: printf(" fh=%#" PRIx64 " flags=%#x lock_owner=%" PRIu64, in.body.release.fh, in.body.release.flags, in.body.release.lock_owner); break; case FUSE_RENAME: { const char *src = (const char*)in.body.bytes + sizeof(fuse_rename_in); const char *dst = src + strlen(src) + 1; printf(" src=%s newdir=%" PRIu64 " dst=%s", src, in.body.rename.newdir, dst); } break; case FUSE_SETATTR: if (verbosity <= 1) { printf(" valid=%#x", in.body.setattr.valid); break; } if (in.body.setattr.valid & FATTR_MODE) printf(" mode=%#o", in.body.setattr.mode); if (in.body.setattr.valid & FATTR_UID) printf(" uid=%u", in.body.setattr.uid); if (in.body.setattr.valid & FATTR_GID) printf(" gid=%u", in.body.setattr.gid); if (in.body.setattr.valid & FATTR_SIZE) printf(" size=%" PRIu64, in.body.setattr.size); if (in.body.setattr.valid & FATTR_ATIME) printf(" atime=%" PRIu64 ".%u", in.body.setattr.atime, in.body.setattr.atimensec); if (in.body.setattr.valid & FATTR_MTIME) printf(" mtime=%" PRIu64 ".%u", in.body.setattr.mtime, in.body.setattr.mtimensec); if (in.body.setattr.valid & FATTR_FH) printf(" fh=%" PRIu64 "", in.body.setattr.fh); break; case FUSE_SETLK: printf(" fh=%#" PRIx64 " owner=%" PRIu64 " type=%u pid=%u", in.body.setlk.fh, in.body.setlk.owner, in.body.setlk.lk.type, in.body.setlk.lk.pid); if (verbosity >= 2) { printf(" range=[%" 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=%#" PRIx64 " offset=%" PRIu64 " size=%u write_flags=%u", in.body.write.fh, in.body.write.offset, in.body.write.size, in.body.write.write_flags); if (verbosity > 1) printf(" flags=%#x", in.body.write.flags); break; default: break; } printf("\n"); } /* * Debug a FUSE response. * * This is mostly useful for asynchronous notifications, which don't correspond * to any request */ void MockFS::debug_response(const mockfs_buf_out &out) { const char *name; if (verbosity == 0) return; switch (out.header.error) { case FUSE_NOTIFY_INVAL_ENTRY: name = (const char*)out.body.bytes + sizeof(fuse_notify_inval_entry_out); printf("<- INVAL_ENTRY parent=%" PRIu64 " %s\n", out.body.inval_entry.parent, name); break; case FUSE_NOTIFY_INVAL_INODE: printf("<- INVAL_INODE ino=%" PRIu64 " off=%" PRIi64 " len=%" PRIi64 "\n", out.body.inval_inode.ino, out.body.inval_inode.off, out.body.inval_inode.len); break; case FUSE_NOTIFY_STORE: printf("<- STORE ino=%" PRIu64 " off=%" PRIu64 " size=%" PRIu32 "\n", out.body.store.nodeid, out.body.store.offset, out.body.store.size); break; default: break; } } MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions, bool push_symlinks_in, bool ro, enum poll_method pm, uint32_t flags, uint32_t kernel_minor_version, uint32_t max_write, bool async, - bool noclusterr, unsigned time_gran, bool nointr, bool noatime) + bool noclusterr, unsigned time_gran, bool nointr, bool noatime, + const char *subtype) { struct sigaction sa; struct iovec *iov = NULL; int iovlen = 0; char fdstr[15]; const bool trueval = true; m_daemon_id = NULL; m_expected_write_errno = 0; m_kernel_minor_version = kernel_minor_version; m_maxreadahead = max_readahead; m_maxwrite = MIN(max_write, max_max_write); m_nready = -1; m_pm = pm; m_time_gran = time_gran; m_quit = false; m_last_unique = 0; 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 (async) { build_iovec(&iov, &iovlen, "async", __DECONST(void*, &trueval), sizeof(bool)); } if (noatime) { build_iovec(&iov, &iovlen, "noatime", __DECONST(void*, &trueval), sizeof(bool)); } if (noclusterr) { build_iovec(&iov, &iovlen, "noclusterr", __DECONST(void*, &trueval), sizeof(bool)); } if (nointr) { build_iovec(&iov, &iovlen, "nointr", __DECONST(void*, &trueval), sizeof(bool)); } else { build_iovec(&iov, &iovlen, "intr", __DECONST(void*, &trueval), sizeof(bool)); } + if (*subtype) { + build_iovec(&iov, &iovlen, "subtype=", + __DECONST(void*, subtype), -1); + } if (nmount(iov, iovlen, 0)) throw(std::system_error(errno, std::system_category(), "Couldn't mount filesystem")); // 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::audit_request(const mockfs_buf_in &in, ssize_t buflen) { uint32_t inlen = in.header.len; size_t fih = sizeof(in.header); switch (in.header.opcode) { case FUSE_LOOKUP: case FUSE_RMDIR: case FUSE_SYMLINK: case FUSE_UNLINK: EXPECT_GT(inlen, fih) << "Missing request filename"; // No redundant information for checking buflen break; case FUSE_FORGET: EXPECT_EQ(inlen, fih + sizeof(in.body.forget)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_GETATTR: EXPECT_EQ(inlen, fih + sizeof(in.body.getattr)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_SETATTR: EXPECT_EQ(inlen, fih + sizeof(in.body.setattr)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_READLINK: EXPECT_EQ(inlen, fih) << "Unexpected request body"; EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_MKNOD: { size_t s; if (m_kernel_minor_version >= 12) s = sizeof(in.body.mknod); else s = FUSE_COMPAT_MKNOD_IN_SIZE; EXPECT_GE(inlen, fih + s) << "Missing request body"; EXPECT_GT(inlen, fih + s) << "Missing request filename"; // No redundant information for checking buflen break; } case FUSE_MKDIR: EXPECT_GE(inlen, fih + sizeof(in.body.mkdir)) << "Missing request body"; EXPECT_GT(inlen, fih + sizeof(in.body.mkdir)) << "Missing request filename"; // No redundant information for checking buflen break; case FUSE_RENAME: EXPECT_GE(inlen, fih + sizeof(in.body.rename)) << "Missing request body"; EXPECT_GT(inlen, fih + sizeof(in.body.rename)) << "Missing request filename"; // No redundant information for checking buflen break; case FUSE_LINK: EXPECT_GE(inlen, fih + sizeof(in.body.link)) << "Missing request body"; EXPECT_GT(inlen, fih + sizeof(in.body.link)) << "Missing request filename"; // No redundant information for checking buflen break; case FUSE_OPEN: EXPECT_EQ(inlen, fih + sizeof(in.body.open)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_READ: EXPECT_EQ(inlen, fih + sizeof(in.body.read)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_WRITE: { size_t s; if (m_kernel_minor_version >= 9) s = sizeof(in.body.write); else s = FUSE_COMPAT_WRITE_IN_SIZE; // I suppose a 0-byte write should be allowed EXPECT_GE(inlen, fih + s) << "Missing request body"; EXPECT_EQ((size_t)buflen, fih + s + in.body.write.size); break; } case FUSE_DESTROY: case FUSE_STATFS: EXPECT_EQ(inlen, fih); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_RELEASE: EXPECT_EQ(inlen, fih + sizeof(in.body.release)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_FSYNC: case FUSE_FSYNCDIR: EXPECT_EQ(inlen, fih + sizeof(in.body.fsync)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_SETXATTR: EXPECT_GE(inlen, fih + sizeof(in.body.setxattr)) << "Missing request body"; EXPECT_GT(inlen, fih + sizeof(in.body.setxattr)) << "Missing request attribute name"; // No redundant information for checking buflen break; case FUSE_GETXATTR: EXPECT_GE(inlen, fih + sizeof(in.body.getxattr)) << "Missing request body"; EXPECT_GT(inlen, fih + sizeof(in.body.getxattr)) << "Missing request attribute name"; // No redundant information for checking buflen break; case FUSE_LISTXATTR: EXPECT_EQ(inlen, fih + sizeof(in.body.listxattr)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_REMOVEXATTR: EXPECT_GT(inlen, fih) << "Missing request attribute name"; // No redundant information for checking buflen break; case FUSE_FLUSH: EXPECT_EQ(inlen, fih + sizeof(in.body.flush)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_INIT: EXPECT_EQ(inlen, fih + sizeof(in.body.init)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_OPENDIR: EXPECT_EQ(inlen, fih + sizeof(in.body.opendir)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_READDIR: EXPECT_EQ(inlen, fih + sizeof(in.body.readdir)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_RELEASEDIR: EXPECT_EQ(inlen, fih + sizeof(in.body.releasedir)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_GETLK: EXPECT_EQ(inlen, fih + sizeof(in.body.getlk)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_SETLK: case FUSE_SETLKW: EXPECT_EQ(inlen, fih + sizeof(in.body.setlk)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_ACCESS: EXPECT_EQ(inlen, fih + sizeof(in.body.access)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_CREATE: EXPECT_GE(inlen, fih + sizeof(in.body.create)) << "Missing request body"; EXPECT_GT(inlen, fih + sizeof(in.body.create)) << "Missing request filename"; // No redundant information for checking buflen break; case FUSE_INTERRUPT: EXPECT_EQ(inlen, fih + sizeof(in.body.interrupt)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_FALLOCATE: EXPECT_EQ(inlen, fih + sizeof(in.body.fallocate)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_BMAP: EXPECT_EQ(inlen, fih + sizeof(in.body.bmap)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_LSEEK: EXPECT_EQ(inlen, fih + sizeof(in.body.lseek)); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_COPY_FILE_RANGE: EXPECT_EQ(inlen, fih + sizeof(in.body.copy_file_range)); EXPECT_EQ(0ul, in.body.copy_file_range.flags); EXPECT_EQ((size_t)buflen, inlen); break; case FUSE_NOTIFY_REPLY: case FUSE_BATCH_FORGET: case FUSE_IOCTL: case FUSE_POLL: case FUSE_READDIRPLUS: FAIL() << "Unsupported opcode?"; default: FAIL() << "Unknown opcode " << in.header.opcode; } /* * Check that the ticket's unique value is sequential. Technically it * doesn't need to be sequential, merely unique. But the current * fusefs driver _does_ make it sequential, and that's easy to check * for. */ if (in.header.unique != ++m_last_unique) FAIL() << "Non-sequential unique value"; } void MockFS::init(uint32_t flags) { ssize_t buflen; std::unique_ptr in(new mockfs_buf_in); std::unique_ptr out(new mockfs_buf_out); read_request(*in, buflen); if (verbosity > 0) debug_request(*in, buflen); audit_request(*in, buflen); ASSERT_EQ(FUSE_INIT, in->header.opcode); out->header.unique = in->header.unique; out->header.error = 0; out->body.init.major = FUSE_KERNEL_VERSION; out->body.init.minor = m_kernel_minor_version;; out->body.init.flags = in->body.init.flags & flags; out->body.init.max_write = m_maxwrite; out->body.init.max_readahead = m_maxreadahead; if (m_kernel_minor_version < 23) { SET_OUT_HEADER_LEN(*out, init_7_22); } else { out->body.init.time_gran = m_time_gran; SET_OUT_HEADER_LEN(*out, init); } write(m_fuse_fd, out.get(), out->header.len); } void MockFS::kill_daemon() { m_quit = true; if (m_daemon_id != NULL) pthread_kill(m_daemon_id, SIGUSR1); // Closing the /dev/fuse file descriptor first allows unmount to // succeed even if the daemon doesn't correctly respond to commands // during the unmount sequence. close(m_fuse_fd); m_fuse_fd = -1; } void MockFS::loop() { std::vector> out; std::unique_ptr in(new mockfs_buf_in); ASSERT_TRUE(in != NULL); while (!m_quit) { ssize_t buflen; bzero(in.get(), sizeof(*in)); read_request(*in, buflen); m_expected_write_errno = 0; if (m_quit) break; if (verbosity > 0) debug_request(*in, buflen); audit_request(*in, buflen); if (pid_ok((pid_t)in->header.pid)) { process(*in, out); } else { /* * Reject any requests from unknown processes. Because * we actually do mount a filesystem, plenty of * unrelated system daemons may try to access it. */ if (verbosity > 1) printf("\tREJECTED (wrong pid %d)\n", in->header.pid); process_default(*in, out); } for (auto &it: out) write_response(*it); out.clear(); } } int MockFS::notify_inval_entry(ino_t parent, const char *name, size_t namelen) { std::unique_ptr out(new mockfs_buf_out); out->header.unique = 0; /* 0 means asynchronous notification */ out->header.error = FUSE_NOTIFY_INVAL_ENTRY; out->body.inval_entry.parent = parent; out->body.inval_entry.namelen = namelen; strlcpy((char*)&out->body.bytes + sizeof(out->body.inval_entry), name, sizeof(out->body.bytes) - sizeof(out->body.inval_entry)); out->header.len = sizeof(out->header) + sizeof(out->body.inval_entry) + namelen; debug_response(*out); write_response(*out); return 0; } int MockFS::notify_inval_inode(ino_t ino, off_t off, ssize_t len) { std::unique_ptr out(new mockfs_buf_out); out->header.unique = 0; /* 0 means asynchronous notification */ out->header.error = FUSE_NOTIFY_INVAL_INODE; out->body.inval_inode.ino = ino; out->body.inval_inode.off = off; out->body.inval_inode.len = len; out->header.len = sizeof(out->header) + sizeof(out->body.inval_inode); debug_response(*out); write_response(*out); return 0; } int MockFS::notify_store(ino_t ino, off_t off, const void* data, ssize_t size) { std::unique_ptr out(new mockfs_buf_out); out->header.unique = 0; /* 0 means asynchronous notification */ out->header.error = FUSE_NOTIFY_STORE; out->body.store.nodeid = ino; out->body.store.offset = off; out->body.store.size = size; bcopy(data, (char*)&out->body.bytes + sizeof(out->body.store), size); out->header.len = sizeof(out->header) + sizeof(out->body.store) + size; debug_response(*out); write_response(*out); return 0; } bool MockFS::pid_ok(pid_t pid) { if (pid == m_pid) { return (true); } else if (pid == m_child_pid) { return (true); } else { struct kinfo_proc *ki; bool ok = false; ki = kinfo_getproc(pid); if (ki == NULL) return (false); /* * Allow access by the aio daemon processes so that our tests * can use aio functions */ if (0 == strncmp("aiod", ki->ki_comm, 4)) ok = true; free(ki); return (ok); } } void MockFS::process_default(const mockfs_buf_in& in, std::vector> &out) { std::unique_ptr out0(new mockfs_buf_out); out0->header.unique = in.header.unique; out0->header.error = -EOPNOTSUPP; out0->header.len = sizeof(out0->header); out.push_back(std::move(out0)); } void MockFS::read_request(mockfs_buf_in &in, ssize_t &res) { int nready = 0; fd_set readfds; pollfd fds[1]; struct kevent changes[1]; struct kevent events[1]; struct timespec timeout_ts; struct timeval timeout_tv; const int timeout_ms = 999; int timeout_int, nfds; int fuse_fd; switch (m_pm) { case BLOCKING: break; case KQ: timeout_ts.tv_sec = 0; timeout_ts.tv_nsec = timeout_ms * 1'000'000; while (nready == 0) { EV_SET(&changes[0], m_fuse_fd, EVFILT_READ, EV_ADD | EV_ONESHOT, 0, 0, 0); nready = kevent(m_kq, &changes[0], 1, &events[0], 1, &timeout_ts); if (m_quit) return; } ASSERT_LE(0, nready) << strerror(errno); ASSERT_EQ(events[0].ident, (uintptr_t)m_fuse_fd); if (events[0].flags & EV_ERROR) FAIL() << strerror(events[0].data); else if (events[0].flags & EV_EOF) FAIL() << strerror(events[0].fflags); m_nready = events[0].data; break; case POLL: timeout_int = timeout_ms; fds[0].fd = m_fuse_fd; fds[0].events = POLLIN; while (nready == 0) { nready = poll(fds, 1, timeout_int); if (m_quit) return; } ASSERT_LE(0, nready) << strerror(errno); ASSERT_TRUE(fds[0].revents & POLLIN); break; case SELECT: fuse_fd = m_fuse_fd; if (fuse_fd < 0) break; timeout_tv.tv_sec = 0; timeout_tv.tv_usec = timeout_ms * 1'000; nfds = fuse_fd + 1; while (nready == 0) { FD_ZERO(&readfds); FD_SET(fuse_fd, &readfds); nready = select(nfds, &readfds, NULL, NULL, &timeout_tv); if (m_quit) return; } ASSERT_LE(0, nready) << strerror(errno); ASSERT_TRUE(FD_ISSET(fuse_fd, &readfds)); break; default: FAIL() << "not yet implemented"; } res = read(m_fuse_fd, &in, sizeof(in)); if (res < 0 && !m_quit) { m_quit = true; FAIL() << "read: " << strerror(errno); } ASSERT_TRUE(res >= static_cast(sizeof(in.header)) || m_quit); /* * Inconsistently, fuse_in_header.len is the size of the entire * request,including header, even though fuse_out_header.len excludes * the size of the header. */ ASSERT_TRUE(res == static_cast(in.header.len) || m_quit); } void MockFS::write_response(const mockfs_buf_out &out) { fd_set writefds; pollfd fds[1]; struct kevent changes[1]; struct kevent events[1]; int nready, nfds; ssize_t r; switch (m_pm) { case BLOCKING: break; case KQ: EV_SET(&changes[0], m_fuse_fd, EVFILT_WRITE, EV_ADD | EV_ONESHOT, 0, 0, 0); nready = kevent(m_kq, &changes[0], 1, &events[0], 1, NULL); ASSERT_LE(0, nready) << strerror(errno); ASSERT_EQ(events[0].ident, (uintptr_t)m_fuse_fd); if (events[0].flags & EV_ERROR) FAIL() << strerror(events[0].data); else if (events[0].flags & EV_EOF) FAIL() << strerror(events[0].fflags); m_nready = events[0].data; break; case POLL: fds[0].fd = m_fuse_fd; fds[0].events = POLLOUT; nready = poll(fds, 1, INFTIM); ASSERT_LE(0, nready) << strerror(errno); ASSERT_EQ(1, nready) << "NULL timeout expired?"; ASSERT_TRUE(fds[0].revents & POLLOUT); break; case SELECT: FD_ZERO(&writefds); FD_SET(m_fuse_fd, &writefds); nfds = m_fuse_fd + 1; nready = select(nfds, NULL, &writefds, NULL, NULL); ASSERT_LE(0, nready) << strerror(errno); ASSERT_EQ(1, nready) << "NULL timeout expired?"; ASSERT_TRUE(FD_ISSET(m_fuse_fd, &writefds)); break; default: FAIL() << "not yet implemented"; } r = write(m_fuse_fd, &out, out.header.len); if (m_expected_write_errno) { ASSERT_EQ(-1, r); ASSERT_EQ(m_expected_write_errno, errno) << strerror(errno); } else { ASSERT_TRUE(r > 0 || errno == EAGAIN) << strerror(errno); } } void* MockFS::service(void *pthr_data) { MockFS *mock_fs = (MockFS*)pthr_data; mock_fs->loop(); return (NULL); } void MockFS::unmount() { ::unmount("mountpoint", 0); } diff --git a/tests/sys/fs/fusefs/mockfs.hh b/tests/sys/fs/fusefs/mockfs.hh index d471491ea455..3c8a78e85072 100644 --- a/tests/sys/fs/fusefs/mockfs.hh +++ b/tests/sys/fs/fusefs/mockfs.hh @@ -1,432 +1,432 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ extern "C" { #include #include #include "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; /* * The maximum that a test case can set max_write, limited by the buffer * supplied when reading from /dev/fuse. This limitation is imposed by * fusefs-libs, but not by the FUSE protocol. */ const uint32_t max_max_write = 0x20000; /* This struct isn't defined by fuse_kernel.h or libfuse, but it should be */ struct fuse_create_out { struct fuse_entry_out entry; struct fuse_open_out open; }; /* Protocol 7.8 version of struct fuse_attr */ struct fuse_attr_7_8 { uint64_t ino; uint64_t size; uint64_t blocks; uint64_t atime; uint64_t mtime; uint64_t ctime; uint32_t atimensec; uint32_t mtimensec; uint32_t ctimensec; uint32_t mode; uint32_t nlink; uint32_t uid; uint32_t gid; uint32_t rdev; }; /* Protocol 7.8 version of struct fuse_attr_out */ struct fuse_attr_out_7_8 { uint64_t attr_valid; uint32_t attr_valid_nsec; uint32_t dummy; struct fuse_attr_7_8 attr; }; /* Protocol 7.8 version of struct fuse_entry_out */ struct fuse_entry_out_7_8 { uint64_t nodeid; /* Inode ID */ uint64_t generation; /* Inode generation: nodeid:gen must be unique for the fs's lifetime */ uint64_t entry_valid; /* Cache timeout for the name */ uint64_t attr_valid; /* Cache timeout for the attributes */ uint32_t entry_valid_nsec; uint32_t attr_valid_nsec; struct fuse_attr_7_8 attr; }; /* Output struct for FUSE_CREATE for protocol 7.8 servers */ struct fuse_create_out_7_8 { struct fuse_entry_out_7_8 entry; struct fuse_open_out open; }; /* Output struct for FUSE_INIT for protocol 7.22 and earlier servers */ struct fuse_init_out_7_22 { uint32_t major; uint32_t minor; uint32_t max_readahead; uint32_t flags; uint16_t max_background; uint16_t congestion_threshold; uint32_t max_write; }; union fuse_payloads_in { fuse_access_in access; fuse_bmap_in bmap; /* * In fusefs-libs 3.4.2 and below the buffer size is fixed at 0x21000 * minus the header sizes. fusefs-libs 3.4.3 (and FUSE Protocol 7.29) * add a FUSE_MAX_PAGES option that allows it to be greater. * * See fuse_kern_chan.c in fusefs-libs 2.9.9 and below, or * FUSE_DEFAULT_MAX_PAGES_PER_REQ in fusefs-libs 3.4.3 and above. */ uint8_t bytes[ max_max_write + 0x1000 - sizeof(struct fuse_in_header) ]; fuse_copy_file_range_in copy_file_range; fuse_create_in create; fuse_fallocate_in fallocate; fuse_flush_in flush; fuse_fsync_in fsync; fuse_fsync_in fsyncdir; fuse_forget_in forget; fuse_getattr_in getattr; fuse_interrupt_in interrupt; fuse_lk_in getlk; fuse_getxattr_in getxattr; fuse_init_in init; fuse_link_in link; fuse_listxattr_in listxattr; char lookup[0]; fuse_lseek_in lseek; fuse_mkdir_in mkdir; fuse_mknod_in mknod; fuse_open_in open; fuse_open_in opendir; fuse_read_in read; fuse_read_in readdir; fuse_release_in release; fuse_release_in releasedir; fuse_rename_in rename; char rmdir[0]; fuse_setattr_in setattr; fuse_setxattr_in setxattr; fuse_lk_in setlk; fuse_lk_in setlkw; char unlink[0]; fuse_write_in write; }; struct mockfs_buf_in { fuse_in_header header; union fuse_payloads_in body; }; union fuse_payloads_out { fuse_attr_out attr; fuse_attr_out_7_8 attr_7_8; fuse_bmap_out bmap; fuse_create_out create; fuse_create_out_7_8 create_7_8; /* * The protocol places no limits on the size of bytes. Choose * a size big enough for anything we'll test. */ uint8_t bytes[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_init_out_7_22 init_7_22; fuse_lseek_out lseek; /* The inval_entry structure should be followed by the entry's name */ fuse_notify_inval_entry_out inval_entry; fuse_notify_inval_inode_out inval_inode; /* The store structure should be followed by the data to store */ fuse_notify_store_out store; fuse_listxattr_out listxattr; fuse_open_out open; fuse_statfs_out statfs; /* * The protocol places no limits on the length of the string. This is * merely convenient for testing. */ char str[80]; fuse_write_out write; }; struct mockfs_buf_out { fuse_out_header header; union fuse_payloads_out body; /* 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 */ volatile int m_fuse_fd; /* The minor version of the kernel API that this mock daemon targets */ uint32_t m_kernel_minor_version; int m_kq; /* The max_readahead file system option */ uint32_t m_maxreadahead; /* pid of the test process */ pid_t m_pid; /* The unique value of the header of the last received operation */ uint64_t m_last_unique; /* Method the daemon should use for I/O to and from /dev/fuse */ enum poll_method m_pm; /* Timestamp granularity in nanoseconds */ unsigned m_time_gran; void audit_request(const mockfs_buf_in &in, ssize_t buflen); void debug_request(const mockfs_buf_in&, ssize_t buflen); void debug_response(const mockfs_buf_out&); /* Initialize a session after mounting */ void init(uint32_t flags); /* Is pid from a process that might be involved in the test? */ bool pid_ok(pid_t pid); /* Default request handler */ void process_default(const mockfs_buf_in&, std::vector>&); /* Entry point for the daemon thread */ static void* service(void*); /* * Read, but do not process, a single request from the kernel * * @param in Return storage for the FUSE request * @param res Return value of read(2). If positive, the amount of * data read from the fuse device. */ void read_request(mockfs_buf_in& in, ssize_t& res); /* Write a single response back to the kernel */ void write_response(const mockfs_buf_out &out); public: /* pid of child process, for two-process test cases */ pid_t m_child_pid; /* the expected errno of the next write to /dev/fuse */ int m_expected_write_errno; /* Maximum size of a FUSE_WRITE write */ uint32_t m_maxwrite; /* * Number of events that were available from /dev/fuse after the last * kevent call. Only valid when m_pm = KQ. */ int m_nready; /* Tell the daemon to shut down ASAP */ bool m_quit; /* 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, uint32_t max_write, bool async, bool no_clusterr, unsigned time_gran, bool nointr, - bool noatime); + bool noatime, const char *subtype); virtual ~MockFS(); /* Kill the filesystem daemon without unmounting the filesystem */ void kill_daemon(); /* Process FUSE requests endlessly */ void loop(); /* * Send an asynchronous notification to invalidate a directory entry. * Similar to libfuse's fuse_lowlevel_notify_inval_entry * * This method will block until the client has responded, so it should * generally be run in a separate thread from request processing. * * @param parent Parent directory's inode number * @param name name of dirent to invalidate * @param namelen size of name, including the NUL */ int notify_inval_entry(ino_t parent, const char *name, size_t namelen); /* * Send an asynchronous notification to invalidate an inode's cached * data and/or attributes. Similar to libfuse's * fuse_lowlevel_notify_inval_inode. * * This method will block until the client has responded, so it should * generally be run in a separate thread from request processing. * * @param ino File's inode number * @param off offset at which to begin invalidation. A * negative offset means to invalidate attributes * only. * @param len Size of region of data to invalidate. 0 means * to invalidate all cached data. */ int notify_inval_inode(ino_t ino, off_t off, ssize_t len); /* * Send an asynchronous notification to store data directly into an * inode's cache. Similar to libfuse's fuse_lowlevel_notify_store. * * This method will block until the client has responded, so it should * generally be run in a separate thread from request processing. * * @param ino File's inode number * @param off Offset at which to store data * @param data Pointer to the data to cache * @param len Size of data */ int notify_store(ino_t ino, off_t off, const void* data, ssize_t size); /* * Request handler * * This method is expected to provide the responses to each FUSE * operation. For an immediate response, push one buffer into out. * For a delayed response, push nothing. For an immediate response * plus a delayed response to an earlier operation, push two bufs. * Test cases must define each response using Googlemock expectations */ MOCK_METHOD2(process, void(const mockfs_buf_in&, std::vector>&)); /* Gracefully unmount */ void unmount(); }; diff --git a/tests/sys/fs/fusefs/mount.cc b/tests/sys/fs/fusefs/mount.cc index 217fd1d554e8..d736377536cb 100644 --- a/tests/sys/fs/fusefs/mount.cc +++ b/tests/sys/fs/fusefs/mount.cc @@ -1,154 +1,184 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ extern "C" { #include #include #include #include "mntopts.h" // for build_iovec } #include "mockfs.hh" #include "utils.hh" using namespace testing; -class UpdateOk: public FuseTest, public WithParamInterface {}; -class UpdateErr: public FuseTest, public WithParamInterface {}; +class Mount: public FuseTest { +public: +void expect_statfs() { + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_STATFS); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, statfs); + }))); +} +}; +class Subtype: public Mount { + void SetUp() { + m_subtype = "myfs"; + Mount::SetUp(); + } +}; + +class UpdateOk: public Mount, public WithParamInterface {}; +class UpdateErr: public Mount, public WithParamInterface {}; int mntflag_from_string(const char *s) { if (0 == strcmp("MNT_RDONLY", s)) return MNT_RDONLY; else if (0 == strcmp("MNT_NOEXEC", s)) return MNT_NOEXEC; else if (0 == strcmp("MNT_NOSUID", s)) return MNT_NOSUID; else if (0 == strcmp("MNT_NOATIME", s)) return MNT_NOATIME; else if (0 == strcmp("MNT_SUIDDIR", s)) return MNT_SUIDDIR; else if (0 == strcmp("MNT_USER", s)) return MNT_USER; else return 0; } +TEST_F(Subtype, subtype) +{ + struct statfs statbuf; + + expect_statfs(); + + ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); + ASSERT_EQ(0, strcmp("fusefs.myfs", statbuf.f_fstypename)); +} + /* Some mount options can be changed by mount -u */ TEST_P(UpdateOk, update) { struct statfs statbuf; struct iovec *iov = NULL; int iovlen = 0; int flag; int newflags = MNT_UPDATE | MNT_SYNCHRONOUS; flag = mntflag_from_string(GetParam()); if (flag == MNT_NOSUID && 0 != geteuid()) GTEST_SKIP() << "Only root may clear MNT_NOSUID"; if (flag == MNT_SUIDDIR && 0 != geteuid()) GTEST_SKIP() << "Only root may set MNT_SUIDDIR"; EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_STATFS); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { /* * All of the fields except f_flags are don't care, and f_flags is set by * the VFS */ SET_OUT_HEADER_LEN(out, statfs); }))); ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); newflags = (statbuf.f_flags | MNT_UPDATE) ^ flag; build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1); build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1); build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1); ASSERT_EQ(0, nmount(iov, iovlen, newflags)) << strerror(errno); ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); EXPECT_FALSE((newflags ^ statbuf.f_flags) & flag); } /* Some mount options cannnot be changed by mount -u */ TEST_P(UpdateErr, update) { struct statfs statbuf; struct iovec *iov = NULL; int iovlen = 0; int flag; int newflags = MNT_UPDATE | MNT_SYNCHRONOUS; flag = mntflag_from_string(GetParam()); EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_STATFS); }, Eq(true)), _) ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { /* * All of the fields except f_flags are don't care, and f_flags is set by * the VFS */ SET_OUT_HEADER_LEN(out, statfs); }))); ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); newflags = (statbuf.f_flags | MNT_UPDATE) ^ flag; build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1); build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1); build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1); /* * Don't check nmount's return value, because vfs_domount may "fix" the * options for us. The important thing is to check the final value of * statbuf.f_flags below. */ (void)nmount(iov, iovlen, newflags); ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno); EXPECT_TRUE((newflags ^ statbuf.f_flags) & flag); } INSTANTIATE_TEST_CASE_P(Mount, UpdateOk, ::testing::Values("MNT_RDONLY", "MNT_NOEXEC", "MNT_NOSUID", "MNT_NOATIME", "MNT_SUIDDIR") ); INSTANTIATE_TEST_CASE_P(Mount, UpdateErr, ::testing::Values( "MNT_USER"); ); diff --git a/tests/sys/fs/fusefs/utils.cc b/tests/sys/fs/fusefs/utils.cc index 65738f4b19a4..0d42f418c369 100644 --- a/tests/sys/fs/fusefs/utils.cc +++ b/tests/sys/fs/fusefs/utils.cc @@ -1,676 +1,676 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ extern "C" { #include #include #include #include #include #include #include #include #include #include #include } #include #include "mockfs.hh" #include "utils.hh" using namespace testing; /* * The default max_write is set to this formula in libfuse, though * individual filesystems can lower it. The "- 4096" was added in * commit 154ffe2, with the commit message "fix". */ const uint32_t libfuse_max_write = 32 * getpagesize() + 0x1000 - 4096; /* Check that fusefs(4) is accessible and the current user can mount(2) */ void check_environment() { const char *devnode = "/dev/fuse"; const char *bsdextended_node = "security.mac.bsdextended.enabled"; int bsdextended_val = 0; size_t bsdextended_size = sizeof(bsdextended_val); int bsdextended_found; const char *usermount_node = "vfs.usermount"; int usermount_val = 0; size_t usermount_size = sizeof(usermount_val); if (eaccess(devnode, R_OK | W_OK)) { if (errno == ENOENT) { GTEST_SKIP() << devnode << " does not exist"; } else if (errno == EACCES) { GTEST_SKIP() << devnode << " is not accessible by the current user"; } else { GTEST_SKIP() << strerror(errno); } } // mac_bsdextended(4), when enabled, generates many more GETATTR // operations. The fusefs tests' expectations don't account for those, // and adding extra code to handle them obfuscates the real purpose of // the tests. Better just to skip the fusefs tests if mac_bsdextended // is enabled. bsdextended_found = sysctlbyname(bsdextended_node, &bsdextended_val, &bsdextended_size, NULL, 0); if (bsdextended_found == 0 && bsdextended_val != 0) GTEST_SKIP() << "The fusefs tests are incompatible with mac_bsdextended."; ASSERT_EQ(sysctlbyname(usermount_node, &usermount_val, &usermount_size, NULL, 0), 0); if (geteuid() != 0 && !usermount_val) GTEST_SKIP() << "current user is not allowed to mount"; } const char *cache_mode_to_s(enum cache_mode cm) { switch (cm) { case Uncached: return "Uncached"; case Writethrough: return "Writethrough"; case Writeback: return "Writeback"; case WritebackAsync: return "WritebackAsync"; default: return "Unknown"; } } bool is_unsafe_aio_enabled(void) { const char *node = "vfs.aio.enable_unsafe"; int val = 0; size_t size = sizeof(val); if (sysctlbyname(node, &val, &size, NULL, 0)) { perror("sysctlbyname"); return (false); } return (val != 0); } class FuseEnv: public Environment { virtual void SetUp() { } }; void FuseTest::SetUp() { const char *maxbcachebuf_node = "vfs.maxbcachebuf"; const char *maxphys_node = "kern.maxphys"; 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(maxbcachebuf_node, &val, &size, NULL, 0)) << strerror(errno); m_maxbcachebuf = val; ASSERT_EQ(0, sysctlbyname(maxphys_node, &val, &size, NULL, 0)) << strerror(errno); m_maxphys = val; /* * Set the default max_write to a distinct value from MAXPHYS to catch * bugs that confuse the two. */ if (m_maxwrite == 0) m_maxwrite = MIN(libfuse_max_write, (uint32_t)m_maxphys / 2); try { m_mock = new MockFS(m_maxreadahead, m_allow_other, m_default_permissions, m_push_symlinks_in, m_ro, m_pm, m_init_flags, m_kernel_minor_version, m_maxwrite, m_async, m_noclusterr, m_time_gran, - m_nointr, m_noatime); + m_nointr, m_noatime, m_subtype); /* * FUSE_ACCESS is called almost universally. Expecting it in * each test case would be super-annoying. Instead, set a * default expectation for FUSE_ACCESS and return ENOSYS. * * Individual test cases can override this expectation since * googlemock evaluates expectations in LIFO order. */ EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_ACCESS); }, Eq(true)), _) ).Times(AnyNumber()) .WillRepeatedly(Invoke(ReturnErrno(ENOSYS))); /* * FUSE_BMAP is called for most test cases that read data. Set * a default expectation and return ENOSYS. * * Individual test cases can override this expectation since * googlemock evaluates expectations in LIFO order. */ EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_BMAP); }, Eq(true)), _) ).Times(AnyNumber()) .WillRepeatedly(Invoke(ReturnErrno(ENOSYS))); } catch (std::system_error err) { FAIL() << err.what(); } } void FuseTest::expect_access(uint64_t ino, mode_t access_mode, int error) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_ACCESS && in.header.nodeid == ino && in.body.access.mask == access_mode); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(error))); } void FuseTest::expect_destroy(int error) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_DESTROY); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { m_mock->m_quit = true; out.header.len = sizeof(out.header); out.header.unique = in.header.unique; out.header.error = -error; }))); } void FuseTest::expect_fallocate(uint64_t ino, uint64_t offset, uint64_t length, uint32_t mode, int error, int times) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_FALLOCATE && in.header.nodeid == ino && in.body.fallocate.offset == offset && in.body.fallocate.length == length && in.body.fallocate.mode == mode); }, Eq(true)), _) ).Times(times) .WillRepeatedly(Invoke(ReturnErrno(error))); } void FuseTest::expect_flush(uint64_t ino, int times, ProcessMockerT r) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_FLUSH && in.header.nodeid == ino); }, Eq(true)), _) ).Times(times) .WillRepeatedly(Invoke(r)); } void FuseTest::expect_forget(uint64_t ino, uint64_t nlookup, sem_t *sem) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_FORGET && in.header.nodeid == ino && in.body.forget.nlookup == nlookup); }, Eq(true)), _) ).WillOnce(Invoke([=](auto in __unused, auto &out __unused) { if (sem != NULL) sem_post(sem); /* FUSE_FORGET has no response! */ })); } void FuseTest::expect_getattr(uint64_t ino, uint64_t size) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_GETATTR && in.header.nodeid == ino); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, attr); out.body.attr.attr.ino = ino; // Must match nodeid out.body.attr.attr.mode = S_IFREG | 0644; out.body.attr.attr.size = size; out.body.attr.attr_valid = UINT64_MAX; }))); } void FuseTest::expect_getxattr(uint64_t ino, const char *attr, ProcessMockerT r) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { const char *a = (const char*)in.body.bytes + sizeof(fuse_getxattr_in); return (in.header.opcode == FUSE_GETXATTR && in.header.nodeid == ino && 0 == strcmp(attr, a)); }, Eq(true)), _) ).WillOnce(Invoke(r)); } void FuseTest::expect_lookup(const char *relpath, uint64_t ino, mode_t mode, uint64_t size, int times, uint64_t attr_valid, uid_t uid, gid_t gid) { EXPECT_LOOKUP(FUSE_ROOT_ID, relpath) .Times(times) .WillRepeatedly(Invoke( ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = mode; out.body.entry.nodeid = ino; out.body.entry.attr.nlink = 1; out.body.entry.attr_valid = attr_valid; out.body.entry.attr.size = size; out.body.entry.attr.uid = uid; out.body.entry.attr.gid = gid; }))); } void FuseTest::expect_lookup_7_8(const char *relpath, uint64_t ino, mode_t mode, uint64_t size, int times, uint64_t attr_valid, uid_t uid, gid_t gid) { EXPECT_LOOKUP(FUSE_ROOT_ID, relpath) .Times(times) .WillRepeatedly(Invoke( ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, entry_7_8); out.body.entry.attr.mode = mode; out.body.entry.nodeid = ino; out.body.entry.attr.nlink = 1; out.body.entry.attr_valid = attr_valid; out.body.entry.attr.size = size; out.body.entry.attr.uid = uid; out.body.entry.attr.gid = gid; }))); } void FuseTest::expect_open(uint64_t ino, uint32_t flags, int times) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_OPEN && in.header.nodeid == ino); }, Eq(true)), _) ).Times(times) .WillRepeatedly(Invoke( ReturnImmediate([=](auto in __unused, auto& out) { out.header.len = sizeof(out.header); SET_OUT_HEADER_LEN(out, open); out.body.open.fh = FH; out.body.open.open_flags = flags; }))); } void FuseTest::expect_opendir(uint64_t ino) { /* opendir(3) calls fstatfs */ EXPECT_CALL(*m_mock, process( ResultOf([](auto in) { return (in.header.opcode == FUSE_STATFS); }, Eq(true)), _) ).WillRepeatedly(Invoke( ReturnImmediate([=](auto i __unused, auto& out) { SET_OUT_HEADER_LEN(out, statfs); }))); EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_OPENDIR && in.header.nodeid == ino); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { out.header.len = sizeof(out.header); SET_OUT_HEADER_LEN(out, open); out.body.open.fh = FH; }))); } void FuseTest::expect_read(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, const void *contents, int flags, uint64_t fh) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_READ && in.header.nodeid == ino && in.body.read.fh == fh && in.body.read.offset == offset && in.body.read.size == isize && (flags == -1 ? (in.body.read.flags == O_RDONLY || in.body.read.flags == O_RDWR) : in.body.read.flags == (uint32_t)flags)); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 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); int i = 0; out.header.error = 0; out.header.len = 0; for (const auto& it: ents) { size_t entlen, entsize; fde->ino = it.d_fileno; fde->off = it.d_off; fde->type = it.d_type; fde->namelen = it.d_namlen; strncpy(fde->name, it.d_name, it.d_namlen); entlen = FUSE_NAME_OFFSET + fde->namelen; entsize = FUSE_DIRENT_SIZE(fde); /* * The FUSE protocol does not require zeroing out the * unused portion of the name. But it's a good * practice to prevent information disclosure to the * FUSE client, even though the client is usually the * kernel */ memset(fde->name + fde->namelen, 0, entsize - entlen); if (out.header.len + entsize > in.body.read.size) { printf("Overflow in readdir expectation: i=%d\n" , i); break; } out.header.len += entsize; fde = (struct fuse_dirent*) ((intmax_t*)fde + entsize / sizeof(intmax_t)); i++; } out.header.len += sizeof(out.header); }))); } void FuseTest::expect_release(uint64_t ino, uint64_t fh) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_RELEASE && in.header.nodeid == ino && in.body.release.fh == fh); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(0))); } void FuseTest::expect_releasedir(uint64_t ino, ProcessMockerT r) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_RELEASEDIR && in.header.nodeid == ino && in.body.release.fh == FH); }, Eq(true)), _) ).WillOnce(Invoke(r)); } void FuseTest::expect_unlink(uint64_t parent, const char *path, int error) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { return (in.header.opcode == FUSE_UNLINK && 0 == strcmp(path, in.body.unlink) && in.header.nodeid == parent); }, Eq(true)), _) ).WillOnce(Invoke(ReturnErrno(error))); } void FuseTest::expect_write(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, uint32_t flags_set, uint32_t flags_unset, const void *contents) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { const char *buf = (const char*)in.body.bytes + sizeof(struct fuse_write_in); bool pid_ok; uint32_t wf = in.body.write.write_flags; if (wf & FUSE_WRITE_CACHE) pid_ok = true; else pid_ok = (pid_t)in.header.pid == getpid(); return (in.header.opcode == FUSE_WRITE && in.header.nodeid == ino && in.body.write.fh == FH && in.body.write.offset == offset && in.body.write.size == isize && pid_ok && (wf & flags_set) == flags_set && (wf & flags_unset) == 0 && (in.body.write.flags == O_WRONLY || in.body.write.flags == O_RDWR) && 0 == bcmp(buf, contents, isize)); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = osize; }))); } void FuseTest::expect_write_7_8(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, const void *contents) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { const char *buf = (const char*)in.body.bytes + FUSE_COMPAT_WRITE_IN_SIZE; bool pid_ok = (pid_t)in.header.pid == getpid(); return (in.header.opcode == FUSE_WRITE && in.header.nodeid == ino && in.body.write.fh == FH && in.body.write.offset == offset && in.body.write.size == isize && pid_ok && 0 == bcmp(buf, contents, isize)); }, Eq(true)), _) ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { SET_OUT_HEADER_LEN(out, write); out.body.write.size = osize; }))); } void get_unprivileged_id(uid_t *uid, gid_t *gid) { struct passwd *pw; struct group *gr; /* * First try "tests", Kyua's default unprivileged user. XXX after * GoogleTest gains a proper Kyua wrapper, get this with the Kyua API */ pw = getpwnam("tests"); if (pw == NULL) { /* Fall back to "nobody" */ pw = getpwnam("nobody"); } if (pw == NULL) GTEST_SKIP() << "Test requires an unprivileged user"; /* Use group "nobody", which is Kyua's default unprivileged group */ gr = getgrnam("nobody"); if (gr == NULL) GTEST_SKIP() << "Test requires an unprivileged group"; *uid = pw->pw_uid; *gid = gr->gr_gid; } void FuseTest::fork(bool drop_privs, int *child_status, std::function 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; } void FuseTest::reclaim_vnode(const char *path) { int err; err = sysctlbyname(reclaim_mib, NULL, 0, path, strlen(path) + 1); ASSERT_EQ(0, err) << strerror(errno); } static void usage(char* progname) { fprintf(stderr, "Usage: %s [-v]\n\t-v increase verbosity\n", progname); exit(2); } int main(int argc, char **argv) { int ch; FuseEnv *fuse_env = new FuseEnv; InitGoogleTest(&argc, argv); AddGlobalTestEnvironment(fuse_env); while ((ch = getopt(argc, argv, "v")) != -1) { switch (ch) { case 'v': verbosity++; break; default: usage(argv[0]); break; } } return (RUN_ALL_TESTS()); } diff --git a/tests/sys/fs/fusefs/utils.hh b/tests/sys/fs/fusefs/utils.hh index 1cd7bad97998..7d30f2b233ab 100644 --- a/tests/sys/fs/fusefs/utils.hh +++ b/tests/sys/fs/fusefs/utils.hh @@ -1,271 +1,273 @@ /*- * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2019 The FreeBSD Foundation * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $FreeBSD$ */ struct _sem; typedef struct _sem sem_t; struct _dirdesc; typedef struct _dirdesc DIR; /* Nanoseconds to sleep, for tests that must */ #define NAP_NS (100'000'000) void get_unprivileged_id(uid_t *uid, gid_t *gid); inline void nap() { usleep(NAP_NS / 1000); } enum cache_mode { Uncached, Writethrough, Writeback, WritebackAsync }; const char *cache_mode_to_s(enum cache_mode cm); bool is_unsafe_aio_enabled(void); extern const uint32_t libfuse_max_write; class FuseTest : public ::testing::Test { protected: uint32_t m_maxreadahead; uint32_t m_maxwrite; uint32_t m_init_flags; bool m_allow_other; bool m_default_permissions; uint32_t m_kernel_minor_version; enum poll_method m_pm; bool m_noatime; bool m_push_symlinks_in; bool m_ro; bool m_async; bool m_noclusterr; bool m_nointr; unsigned m_time_gran; MockFS *m_mock = NULL; const static uint64_t FH = 0xdeadbeef1a7ebabe; const char *reclaim_mib = "debug.try_reclaim_vnode"; + const char *m_subtype; public: int m_maxbcachebuf; int m_maxphys; FuseTest(): m_maxreadahead(0), m_maxwrite(0), m_init_flags(0), m_allow_other(false), m_default_permissions(false), m_kernel_minor_version(FUSE_KERNEL_MINOR_VERSION), m_pm(BLOCKING), m_noatime(false), m_push_symlinks_in(false), m_ro(false), m_async(false), m_noclusterr(false), m_nointr(false), m_time_gran(1), + m_subtype(""), m_maxbcachebuf(0), m_maxphys(0) {} virtual void SetUp(); virtual void TearDown() { if (m_mock) delete m_mock; } /* * Create an expectation that FUSE_ACCESS will be called once for the * given inode with the given access_mode, returning the given errno */ void expect_access(uint64_t ino, mode_t access_mode, int error); /* Expect FUSE_DESTROY and shutdown the daemon */ void expect_destroy(int error); /* * Create an expectation that FUSE_FALLOCATE will be called with the * given inode, offset, length, and mode, exactly times times and * returning error */ void expect_fallocate(uint64_t ino, uint64_t offset, uint64_t length, uint32_t mode, int error, int times=1); /* * Create an expectation that FUSE_FLUSH will be called times times for * the given inode */ void expect_flush(uint64_t ino, int times, ProcessMockerT r); /* * Create an expectation that FUSE_FORGET will be called for the given * inode. There will be no response. If sem is provided, it will be * posted after the operation is received by the daemon. */ void expect_forget(uint64_t ino, uint64_t nlookup, sem_t *sem = NULL); /* * Create an expectation that FUSE_GETATTR will be called for the given * inode any number of times. It will respond with a few basic * attributes, like the given size and the mode S_IFREG | 0644 */ void expect_getattr(uint64_t ino, uint64_t size); /* * Create an expectation that FUSE_GETXATTR will be called once for the * given inode. */ void expect_getxattr(uint64_t ino, const char *attr, ProcessMockerT r); /* * Create an expectation that FUSE_LOOKUP will be called for the given * path exactly times times and cache validity period. It will respond * with inode ino, mode mode, filesize size. */ void expect_lookup(const char *relpath, uint64_t ino, mode_t mode, uint64_t size, int times, uint64_t attr_valid = UINT64_MAX, uid_t uid = 0, gid_t gid = 0); /* The protocol 7.8 version of expect_lookup */ void expect_lookup_7_8(const char *relpath, uint64_t ino, mode_t mode, uint64_t size, int times, uint64_t attr_valid = UINT64_MAX, uid_t uid = 0, gid_t gid = 0); /* * Create an expectation that FUSE_OPEN will be called for the given * inode exactly times times. It will return with open_flags flags and * file handle FH. */ void expect_open(uint64_t ino, uint32_t flags, int times); /* * Create an expectation that FUSE_OPENDIR will be called exactly once * for inode ino. */ void expect_opendir(uint64_t ino); /* * Create an expectation that FUSE_READ will be called exactly once for * the given inode, at offset offset and with size isize. It will * return the first osize bytes from contents * * Protocol 7.8 tests can use this same expectation method because * nothing currently validates the size of the fuse_read_in struct. */ void expect_read(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, const void *contents, int flags = -1, uint64_t fh = FH); /* * Create an expectation that FUSE_READIR will be called any number of * times on the given ino with the given offset, returning (by copy) * the provided entries */ void expect_readdir(uint64_t ino, uint64_t off, std::vector &ents); /* * Create an expectation that FUSE_RELEASE will be called exactly once * for the given inode and filehandle, returning success */ void expect_release(uint64_t ino, uint64_t fh); /* * Create an expectation that FUSE_RELEASEDIR will be called exactly * once for the given inode */ void expect_releasedir(uint64_t ino, ProcessMockerT r); /* * Create an expectation that FUSE_UNLINK will be called exactly once * for the given path, returning an errno */ void expect_unlink(uint64_t parent, const char *path, int error); /* * Create an expectation that FUSE_WRITE will be called exactly once * for the given inode, at offset offset, with size isize and buffer * contents. Any flags present in flags_set must be set, and any * present in flags_unset must not be set. Other flags are don't care. * It will return osize. */ void expect_write(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, uint32_t flags_set, uint32_t flags_unset, const void *contents); /* Protocol 7.8 version of expect_write */ void expect_write_7_8(uint64_t ino, uint64_t offset, uint64_t isize, uint64_t osize, const void *contents); /* * Helper that runs code in a child process. * * First, parent_func runs in the parent process. * Then, child_func runs in the child process, dropping privileges if * desired. * Finally, fusetest_fork returns. * * # Returns * * fusetest_fork may SKIP the test, which the caller should detect with * the IsSkipped() method. If not, then the child's exit status will * be returned in status. */ void fork(bool drop_privs, int *status, std::function parent_func, std::function child_func); /* * Deliberately leak a file descriptor. * * Closing a file descriptor on fusefs would cause the server to * receive FUSE_CLOSE and possibly FUSE_INACTIVE. Handling those * operations would needlessly complicate most tests. So most tests * deliberately leak the file descriptors instead. This method serves * to document the leakage, and provide a single point of suppression * for static analyzers. */ /* coverity[+close: arg-0] */ static void leak(int fd __unused) {} /* * Deliberately leak a DIR* pointer * * See comments for FuseTest::leak */ static void leakdir(DIR* dirp __unused) {} /* Manually reclaim a vnode. Requires root privileges. */ void reclaim_vnode(const char *fullpath); };