diff --git a/sys/fs/fuse/fuse_file.c b/sys/fs/fuse/fuse_file.c --- a/sys/fs/fuse/fuse_file.c +++ b/sys/fs/fuse/fuse_file.c @@ -221,6 +221,14 @@ out: counter_u64_add(fuse_fh_count, -1); LIST_REMOVE(fufh, next); +#ifdef INVARIANTS + fufh->fufh_type = FUFH_INVALID; + fufh->fh_id = 0xdeadc0de; + fufh->fuse_open_flags = -1; + fufh->gid = -1; + fufh->pid = -1; + fufh->uid = -1; +#endif free(fufh, M_FUSE_FILEHANDLE); return err; diff --git a/sys/fs/fuse/fuse_vfsops.c b/sys/fs/fuse/fuse_vfsops.c --- a/sys/fs/fuse/fuse_vfsops.c +++ b/sys/fs/fuse/fuse_vfsops.c @@ -274,7 +274,7 @@ if (!(fuse_get_mpdata(mp)->dataflags & FSESS_EXPORT_SUPPORT)) return EOPNOTSUPP; - error = VFS_VGET(mp, ffhp->nid, LK_EXCLUSIVE, &nvp); + error = VFS_VGET(mp, ffhp->nid, flags, &nvp); if (error) { *vpp = NULLVP; return (error); diff --git a/tests/sys/fs/fusefs/Makefile b/tests/sys/fs/fusefs/Makefile --- a/tests/sys/fs/fusefs/Makefile +++ b/tests/sys/fs/fusefs/Makefile @@ -4,7 +4,7 @@ TESTSDIR= ${TESTSBASE}/sys/fs/fusefs -# We could simply link all of these files into a single executable. But since +# We could simply link most of these files into a single executable. But since # Kyua treats googletest programs as plain tests, it's better to separate them # out, so we get more granular reporting. GTESTS+= access @@ -36,6 +36,7 @@ GTESTS+= mknod GTESTS+= mount GTESTS+= nfs +GTESTS+= nfsd GTESTS+= notify GTESTS+= open GTESTS+= opendir @@ -64,6 +65,7 @@ TEST_METADATA.default_permissions_privileged+= required_user="root" TEST_METADATA.mknod+= required_user="root" TEST_METADATA.nfs+= required_user="root" +TEST_METADATA.nfsd+= required_user="root" TEST_METADATA+= timeout=10 @@ -94,5 +96,6 @@ LIBADD+= pthread LIBADD+= gmock gtest LIBADD+= util +LIBADD.nfsd+= jail .include diff --git a/tests/sys/fs/fusefs/mockfs.cc b/tests/sys/fs/fusefs/mockfs.cc --- a/tests/sys/fs/fusefs/mockfs.cc +++ b/tests/sys/fs/fusefs/mockfs.cc @@ -295,7 +295,8 @@ in.body.read.offset, in.body.read.size); if (verbosity > 1) - printf(" flags=%#x", in.body.read.flags); + printf(" flags=%#x fh=%#" PRIx64, + in.body.read.flags, in.body.read.fh); break; case FUSE_READDIR: printf(" fh=%#" PRIx64 " offset=%" PRIu64 " size=%u", @@ -888,6 +889,14 @@ */ if (0 == strncmp("aiod", ki->ki_comm, 4)) ok = true; + + /* + * Allow access by mountd and nfsd, needed by the nfsd tests + */ + if (0 == strncmp("mountd", ki->ki_comm, 6) || + 0 == strncmp("nfsd", ki->ki_comm, 4)) + ok = true; + free(ki); return (ok); } diff --git a/tests/sys/fs/fusefs/nfsd.cc b/tests/sys/fs/fusefs/nfsd.cc new file mode 100644 --- /dev/null +++ b/tests/sys/fs/fusefs/nfsd.cc @@ -0,0 +1,602 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 ConnectWise + * + * 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. + */ + +/* This file tests access via the in-kernel nfsd process */ + +/* TODO: + * * read a file twice + * * readdir with seekdir/telldir + * * file system implements FUSE_OPEN + * * file system implements FUSE_OPENDIR + * * write a file twice + * * race when fuse_vnop_write opens the file handle, if we ever set + * MNTK_SHARED_WRITES in fusefs. + * * deallocate twice + * * race when fuse_vnop_deallocate opens the file handle + * * getfhat + * * stat a file in a subdirectory. Does this call VP_VPTOFH? + */ + +extern "C" { +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include "mntopts.h" // for build_iovec +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace std; +using namespace testing; + +/* Like /usr/sbin/jexec */ +template +static int +jexec(int jid, const char *cmd, Args... args) +{ + int pid, r, status; + + errno = 0; + switch (pid = fork()) { + case -1: + /* fork failed */ + return -1; + case 0: + /* In child */ + if ((r = jail_attach(jid)) < 0) { + perror("jail_attach"); + _exit(r); + } + if ((r = execlp(cmd, cmd, args..., nullptr)) < 0) { + perror("execlp"); + _exit(r); + } + break; + default: + /* In parent */ + r = waitpid(pid, &status, WEXITED); + return (r < 0 || WEXITSTATUS(status) != 0); + } + return 0; +} + +class Nfsd: public FuseTest { +int m_jid; +int m_s; +struct ifreq m_ifr; + +public: +char *m_mntpnt; + +Nfsd(): m_jid(-1), m_s(-1), m_mntpnt(NULL) +{ + bzero(&m_ifr, sizeof(m_ifr)); + m_ifr.ifr_data = (caddr_t)-1; + m_init_flags |= FUSE_ASYNC_READ | FUSE_EXPORT_SUPPORT; + m_default_permissions = true; + m_allow_other = true; + m_maxread = 32768; + strlcpy(m_ifr.ifr_name, "epair", sizeof(m_ifr.ifr_name)); +}; + +virtual void SetUp() { + const char *nodename = "kern.features.vimage"; + const char *jailname = "fusefs_nfsd"; + char mountd_args[MAXPATHLEN]; + char ifconfig_args[MAXPATHLEN]; + char pwd[MAXPATHLEN]; + char epairb[16]; + char *alpha_idx, *cmd, *exports; + int val = 0; + size_t size = sizeof(val); + int modid; + int exportsfd, r; + + // Verify prerequistes + if (geteuid() != 0) + GTEST_SKIP() << "This test requires a privileged user"; + if ((modid = modfind("nfsd")) < 0) + GTEST_SKIP() << + "This test requires the nfsd module to be available"; + if ((modid = modfind("if_epair")) < 0) + GTEST_SKIP() << + "This test requires the if_epair module to be available"; + if (0 != sysctlbyname(nodename, &val, &size, NULL, 0)) + GTEST_SKIP() << "This test requires VIMAGE"; + + // Setup general expectations + FuseTest::SetUp(); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN); + }, Eq(true)), + _) + ).Times(AtMost(1)) + .WillOnce(Invoke(ReturnErrno(ENOSYS))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPENDIR); + }, Eq(true)), + _) + ).Times(AtMost(1)) + .WillOnce(Invoke(ReturnErrno(ENOSYS))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == FUSE_ROOT_ID); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) + { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = FUSE_ROOT_ID; + out.body.attr.attr.mode = S_IFDIR | 0755; + out.body.attr.attr_valid = UINT64_MAX; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_STATFS); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) + { + SET_OUT_HEADER_LEN(out, statfs); + }))); + EXPECT_LOOKUP(FUSE_ROOT_ID, ".") + .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = FUSE_ROOT_ID; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr.ino = FUSE_ROOT_ID; + out.body.entry.attr.mode = S_IFDIR | 0755; + }))); + + + // Write the exports file + getcwd(pwd, sizeof(pwd)); + asprintf(&exports, + "V4: %s/mountpoint -sec=sys\n" + "%s/mountpoint -maproot=root -network 192.0.2.1/24\n", pwd, pwd); + exportsfd = open("exports", O_RDWR | O_CREAT | O_TRUNC, 0644); + ASSERT_LE(0, exportsfd) << strerror(errno); + r = write(exportsfd, exports, strlen(exports)); + ASSERT_LT(0, r); + close(exportsfd); + free(exports); + + // create epair interfaces + ASSERT_LE(0, m_s = socket(AF_INET, SOCK_DGRAM, 0)); + ASSERT_LE(0, ioctl(m_s, SIOCIFCREATE2, &m_ifr)); + strlcpy(epairb, m_ifr.ifr_name, sizeof(epairb)); + alpha_idx = strrchr(epairb, 'a'); + *alpha_idx++ = 'b'; + *alpha_idx = '\0'; + + // Create the jail + m_jid = jail_setv(JAIL_CREATE, + "name", jailname, + "vnet", "new", //epairb, + "host", NULL, + "persist", NULL, + "allow.mount", "true", + "allow.mount.fusefs", "true", + "allow.nfsd", "true", + "enforce_statfs", "1", + NULL); + ASSERT_LE(0, m_jid) << jail_errmsg; + sprintf(ifconfig_args, "/sbin/ifconfig %s vnet %s", epairb, jailname); + ASSERT_EQ(0, system(ifconfig_args)); + ASSERT_EQ(0, jexec(m_jid, "/sbin/sysctl", "-q", + "vfs.nfsd.testing_disable_grace=1")); + + // Assign IP addresses + sprintf(ifconfig_args, "/sbin/ifconfig %s 192.0.2.1/24 up", + m_ifr.ifr_name); + ASSERT_EQ(0, system(ifconfig_args)); + ASSERT_EQ(0, jexec(m_jid, "/sbin/ifconfig", epairb, "192.0.2.2/24", "up")); + + // Start rpcbind, mountd, and nfsd + // XXX starting rpcbind within the jail fails if it's already running + // outside of the jail. How to fix? + ASSERT_EQ(0, jexec(m_jid, "/usr/sbin/rpcbind")) << strerror(errno); + sprintf(mountd_args, "%s/exports", getcwd(pwd, sizeof(pwd))); + ASSERT_EQ(0, jexec(m_jid, "/usr/sbin/mountd", mountd_args)); + ASSERT_EQ(0, jexec(m_jid, "/usr/sbin/nfsd", "-t")); + + // Mount the NFS client + ASSERT_EQ(0, mkdir("client", 0755)); + asprintf(&m_mntpnt, "%s/client", pwd); + /* + * Mounting NFS directly with nmount(2) requires sending RPCs to + * rpcbind. It's much easier to to use /sbin/mount. + */ + asprintf(&cmd, + "/sbin/mount -t nfs -o nfsv4,minorversion=2 192.0.2.2:/ %s", + m_mntpnt); + system(cmd); + free(cmd); +} + +void TearDown() { + if (m_mntpnt) { + unmount(m_mntpnt, MNT_FORCE); + free(m_mntpnt); + } + rmdir("client"); + if (m_jid >= 0) + jail_remove(m_jid); + if (m_s >= 0 && strlen(m_ifr.ifr_name) > 0) + ioctl(m_s, SIOCIFDESTROY, &m_ifr); + FuseTest::TearDown(); +} + +void expect_fsync(uint64_t ino) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FSYNC && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).WillRepeatedly(Invoke(ReturnErrno(0))); +} + +void expect_lookup(uint64_t parent, const char *path, uint64_t ino, + mode_t mode, uint64_t size) +{ + EXPECT_LOOKUP(parent, path) + .WillRepeatedly(Invoke( + ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.attr.mode = mode; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.attr.size = size; + out.body.entry.entry_valid = UINT64_MAX; + }))); + // nfsd often does lookups for "." when all it knows is the inode. So + // set an expectation for that, too. + EXPECT_LOOKUP(ino, ".") + .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.ino = ino; + out.body.entry.attr.nlink = 1; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.attr.size = size; + out.body.entry.entry_valid = UINT64_MAX; + }))); +} + +}; + +/* punch a hole in a file via nfsd, without calling VOP_OPEN */ +TEST_F(Nfsd, fspacectl) +{ + const char FULLPATH[] = "client/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + struct spacectl_range rqsr, rmsr; + uint64_t ino = 42; + uint64_t fsize = 2000; + uint64_t offset = 500; + uint64_t length = 1000; + int fd; + + expect_lookup(FUSE_ROOT_ID, RELPATH, ino, S_IFREG | 0644, fsize); + expect_fallocate(ino, offset, length, + FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); + // nfsd sets IO_SYNC during deallocate, for some reason + expect_fsync(ino); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + rqsr.r_offset = offset; + rqsr.r_len = length; + EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr)); + EXPECT_EQ(0, rmsr.r_len); + EXPECT_EQ((off_t)(offset + length), rmsr.r_offset); + + leak(fd); +} + +/* Read a file via nfsd, without calling VOP_OPEN */ +TEST_F(Nfsd, read) +{ + const char FULLPATH[] = "client/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + uint8_t buf[bufsize]; + + expect_lookup(FUSE_ROOT_ID, RELPATH, ino, S_IFREG | 0644, bufsize); + expect_read(ino, 0, bufsize, bufsize, CONTENTS, 0, 0); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, read(fd, buf, bufsize)) << strerror(errno); + ASSERT_EQ(0, memcmp(buf, CONTENTS, bufsize)); + + leak(fd); +} + +/* + * Read a file via nfsd from two threads in a way that can trigger a + * use-after-free of a fuse file handle. + * + * Outline: + * - Thread 1 enters fuse_vnop_read, opens a fuse file file handle, and sets + * closefufh = true. + * - Thread 1 sends FUSE_READ and blocks waiting on the server. + * - Thread 2 enters fuse_vnop_read and gets a reference to the fufh created by + * thread 1. + * - Thread 2 blocks waiting on FUSE_READ from the server + * - Thread 1 resumes, and closes the fufh + * - Thread 2 resumes. But since it is trying to read a large amount of data, + * it must send a second FUSE_READ. It dereferences the now-closed fuse file + * handle. + * + * The NFS client itself will split read operations up into m_maxbcachebuf + * sized chunks. So we have to set m_maxread to something less, so that Thread + * 2's read gets split up too. That's necessary to trigger the bug + * deterministically, but it could happen by chance even without setting + * m_maxread. + * + * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=283448 + */ +TEST_F(Nfsd, read_race) +{ + const char FULLPATH[] = "client/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + uint64_t read_unique1 = 9999, read_unique2 = 9998; + int fd; + ssize_t buf1size = 40; + ssize_t buf2size = 65536; + ssize_t buf3size = m_maxread; + char *buf1, *buf2, *buf3; + off_t off1 = 0; + off_t off2 = 65536; + off_t fsize = 4 * m_maxread; + struct aiocb iocb1, iocb2, *paiocb; + uint32_t read_size1 = 0, read_size2 = 0; + mockfs_buf_out out2; + sem_t sem; + + buf1 = new char[buf1size](); + buf2 = new char[buf2size](); + buf3 = new char[buf3size](); + bzero(buf3, buf3size); + ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + + expect_lookup(FUSE_ROOT_ID, RELPATH, ino, S_IFREG | 0644, fsize); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.offset == (uint64_t)off1); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { + uint32_t size = in.body.read.size; + + out.header.len = sizeof(struct fuse_out_header) + size; + bzero(out.body.bytes, size); + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.offset == (uint64_t)off1 + m_maxread); + }, Eq(true)), + _) + ).WillOnce(Invoke([&](auto in, auto &out __unused) { + read_unique1 = in.header.unique; + read_size1 = in.body.read.size; + sem_post(&sem); + })); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.header.nodeid == ino && + in.body.read.offset == (uint64_t)off2); + }, Eq(true)), + _) + ).WillOnce(Invoke([&](auto in, auto &out) { + read_unique2 = in.header.unique; + read_size2 = in.body.read.size; + + // Complete the first read request + std::unique_ptr out0(new mockfs_buf_out); + out0->header.unique = read_unique1; + out0->header.len = sizeof(struct fuse_out_header) + read_size1; + out0->header.error = 0; + bzero(out0->body.bytes, in.body.read.size); + out.push_back(std::move(out0)); + })); + expect_read(ino, off2 + m_maxread, m_maxread, m_maxread, buf3, 0, 0); + /* This is the clearest symptom of bug 283448: invalid FH */ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_READ && + in.body.read.fh != 0); + }, Eq(true)), + _) + ).Times(0); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + // Disable readahead, which makes the test less deterministic. + ASSERT_EQ(0, posix_fadvise(fd, 0, fsize, POSIX_FADV_RANDOM)); + + iocb1.aio_nbytes = buf1size; + 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); + + sem_wait(&sem); + + iocb2.aio_nbytes = buf2size; + iocb2.aio_fildes = fd; + iocb2.aio_buf = buf2; + iocb2.aio_offset = off2; + iocb2.aio_sigevent.sigev_notify = SIGEV_NONE; + ASSERT_EQ(0, aio_read(&iocb2)) << strerror(errno); + + /* Wait for the first operation to complete */ + (void) aio_waitcomplete(&paiocb, NULL); + + /* Now finish the second operation */ + out2.header.len = sizeof(out2.header) + read_size2; + out2.header.error = 0; + out2.header.unique = read_unique2; + out2.expected_errno = 0; + bzero(out2.body.bytes, read_size2); + m_mock->write_response(out2); + + (void) aio_waitcomplete(&paiocb, NULL); + + sem_destroy(&sem); + delete[] buf3; + delete[] buf2; + delete[] buf1; +} + +/* FUSE_READDIR returns nothing but "." and ".." */ +TEST_F(Nfsd, readdir) +{ + const char FULLPATH[] = "client/some_dir"; + const char RELPATH[] = "some_dir"; + const char dot[] = "."; + const char dotdot[] = ".."; + uint64_t ino = 42; + DIR *dir; + struct dirent *de; + vector ents(2); + vector empty_ents(0); + + expect_lookup(FUSE_ROOT_ID, RELPATH, ino, S_IFDIR | 0755, 0); + ents[0].d_fileno = FUSE_ROOT_ID; + ents[0].d_off = 2000; + ents[0].d_namlen = 2; + ents[0].d_type = DT_DIR; + strncpy(ents[0].d_name, dotdot, ents[0].d_namlen); + ents[1].d_fileno = ino; + ents[1].d_off = 3000; + ents[1].d_namlen = 1; + ents[1].d_type = DT_DIR; + strncpy(ents[1].d_name, dot, 1); + expect_readdir(ino, 0, ents, 0); + expect_readdir(ino, 3000, empty_ents, 0); + // nfsd does a lookup on every dirent, for some reason + expect_lookup(ino, dotdot, FUSE_ROOT_ID, S_IFDIR | 0755, 0); + expect_lookup(ino, dot, ino, S_IFDIR | 0755, 0); + + errno = 0; + dir = opendir(FULLPATH); + ASSERT_NE(nullptr, dir) << strerror(errno); + + errno = 0; + de = readdir(dir); + ASSERT_NE(nullptr, de) << strerror(errno); + EXPECT_EQ(ino, de->d_fileno); + EXPECT_EQ(DT_DIR, de->d_type); + EXPECT_EQ(1, de->d_namlen); + EXPECT_EQ(0, strcmp(dot, de->d_name)); + + errno = 0; + de = readdir(dir); + ASSERT_NE(nullptr, de) << strerror(errno); + EXPECT_EQ(DT_DIR, de->d_type); + EXPECT_EQ(2, de->d_namlen); + EXPECT_EQ(0, strcmp(dotdot, de->d_name)); + + ASSERT_EQ(nullptr, readdir(dir)); + ASSERT_EQ(0, errno); + + leakdir(dir); +} + +/* Write a file via nfsd, without calling VOP_OPEN */ +TEST_F(Nfsd, write) +{ + const char FULLPATH[] = "client/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const char *CONTENTS = "abcdefgh"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = strlen(CONTENTS); + + expect_lookup(FUSE_ROOT_ID, RELPATH, ino, S_IFREG | 0644, 0); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *buf = (const char*)in.body.bytes + + sizeof(struct fuse_write_in); + + return (in.header.opcode == FUSE_WRITE && + in.header.nodeid == ino && + in.body.write.fh == 0 && + in.body.write.offset == 0 && + in.body.write.size == bufsize && + 0 == bcmp(buf, CONTENTS, bufsize)); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, write); + out.body.write.size = bufsize; + }))); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno); + close(fd); /* Close file to flush writes to fuse server */ +} diff --git a/tests/sys/fs/fusefs/utils.hh b/tests/sys/fs/fusefs/utils.hh --- a/tests/sys/fs/fusefs/utils.hh +++ b/tests/sys/fs/fusefs/utils.hh @@ -197,7 +197,7 @@ * the provided entries */ void expect_readdir(uint64_t ino, uint64_t off, - std::vector &ents); + std::vector &ents, uint64_t fh=FH); /* * Create an expectation that FUSE_RELEASE will be called exactly once @@ -226,7 +226,7 @@ */ 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); + const void *contents, uint64_t fh = FH); /* Protocol 7.8 version of expect_write */ void expect_write_7_8(uint64_t ino, uint64_t offset, uint64_t isize, diff --git a/tests/sys/fs/fusefs/utils.cc b/tests/sys/fs/fusefs/utils.cc --- a/tests/sys/fs/fusefs/utils.cc +++ b/tests/sys/fs/fusefs/utils.cc @@ -404,13 +404,13 @@ } void FuseTest::expect_readdir(uint64_t ino, uint64_t off, - std::vector &ents) + std::vector &ents, uint64_t fh) { 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.fh == fh && in.body.readdir.offset == off); }, Eq(true)), _) @@ -491,7 +491,7 @@ 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) + const void *contents, uint64_t fh) { EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) { @@ -509,7 +509,7 @@ return (in.header.opcode == FUSE_WRITE && in.header.nodeid == ino && - in.body.write.fh == FH && + in.body.write.fh == fh && in.body.write.offset == offset && in.body.write.size == isize && pid_ok &&