aboutsummaryrefslogtreecommitdiff
path: root/tests/sys/fs/fusefs/nfs.cc
diff options
context:
space:
mode:
Diffstat (limited to 'tests/sys/fs/fusefs/nfs.cc')
-rw-r--r--tests/sys/fs/fusefs/nfs.cc480
1 files changed, 480 insertions, 0 deletions
diff --git a/tests/sys/fs/fusefs/nfs.cc b/tests/sys/fs/fusefs/nfs.cc
new file mode 100644
index 000000000000..2fa2b290f383
--- /dev/null
+++ b/tests/sys/fs/fusefs/nfs.cc
@@ -0,0 +1,480 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2019 The FreeBSD Foundation
+ *
+ * This software was developed by BFF Storage Systems, LLC under sponsorship
+ * from the FreeBSD Foundation.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/* This file tests functionality needed by NFS servers */
+extern "C" {
+#include <sys/param.h>
+#include <sys/mount.h>
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <unistd.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace std;
+using namespace testing;
+
+
+class Nfs: public FuseTest {
+public:
+virtual void SetUp() {
+ if (geteuid() != 0)
+ GTEST_SKIP() << "This test requires a privileged user";
+ FuseTest::SetUp();
+}
+};
+
+class Exportable: public Nfs {
+public:
+virtual void SetUp() {
+ m_init_flags = FUSE_EXPORT_SUPPORT;
+ Nfs::SetUp();
+}
+};
+
+class Fhstat: public Exportable {};
+class FhstatNotExportable: public Nfs {};
+class Getfh: public Exportable {};
+class Readdir: public Exportable {};
+
+/* If the server returns a different generation number, then file is stale */
+TEST_F(Fhstat, estale)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t ino = 42;
+ const mode_t mode = S_IFDIR | 0755;
+ Sequence seq;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .InSequence(seq)
+ .WillOnce(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.generation = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ EXPECT_LOOKUP(ino, ".")
+ .InSequence(seq)
+ .WillOnce(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.generation = 2;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ ASSERT_EQ(-1, fhstat(&fhp, &sb));
+ EXPECT_EQ(ESTALE, errno);
+}
+
+/* If we must lookup an entry from the server, send a LOOKUP request for "." */
+TEST_F(Fhstat, lookup_dot)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t ino = 42;
+ const mode_t mode = S_IFDIR | 0755;
+ const uid_t uid = 12345;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(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.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ EXPECT_LOOKUP(ino, ".")
+ .WillOnce(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.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
+ EXPECT_EQ(uid, sb.st_uid);
+ EXPECT_EQ(mode, sb.st_mode);
+}
+
+/* Gracefully handle failures to lookup ".". */
+TEST_F(Fhstat, lookup_dot_error)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t ino = 42;
+ const mode_t mode = S_IFDIR | 0755;
+ const uid_t uid = 12345;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(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.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ EXPECT_LOOKUP(ino, ".")
+ .WillOnce(Invoke(ReturnErrno(EDOOFUS)));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ ASSERT_EQ(-1, fhstat(&fhp, &sb));
+ EXPECT_EQ(EDOOFUS, errno);
+}
+
+/* Use a file handle whose entry is still cached */
+TEST_F(Fhstat, cached)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t ino = 42;
+ const mode_t mode = S_IFDIR | 0755;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(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.generation = 1;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
+ EXPECT_EQ(ino, sb.st_ino);
+}
+
+/* File handle entries should expire from the cache, too */
+TEST_F(Fhstat, cache_expired)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t ino = 42;
+ const mode_t mode = S_IFDIR | 0755;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(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.generation = 1;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid_nsec = NAP_NS / 2;
+ })));
+
+ EXPECT_LOOKUP(ino, ".")
+ .WillOnce(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.generation = 1;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
+ EXPECT_EQ(ino, sb.st_ino);
+
+ nap();
+
+ /* Cache should be expired; fuse should issue a FUSE_LOOKUP */
+ ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
+ EXPECT_EQ(ino, sb.st_ino);
+}
+
+/*
+ * If the server returns a FUSE_LOOKUP response for a nodeid that we didn't
+ * lookup, it's a bug. But we should handle it gracefully.
+ */
+TEST_F(Fhstat, inconsistent_nodeid)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t ino_in = 42;
+ const uint64_t ino_out = 43;
+ const mode_t mode = S_IFDIR | 0755;
+ const uid_t uid = 12345;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = ino_in;
+ out.body.entry.attr.ino = ino_in;
+ out.body.entry.attr.mode = mode;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ EXPECT_LOOKUP(ino_in, ".")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = ino_out;
+ out.body.entry.attr.ino = ino_out;
+ out.body.entry.attr.mode = mode;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ EXPECT_NE(0, fhstat(&fhp, &sb)) << strerror(errno);
+ EXPECT_EQ(EIO, errno);
+}
+
+/*
+ * If the server returns a FUSE_LOOKUP response where the nodeid doesn't match
+ * the inode number, and the file system is exported, it's a bug. But we
+ * should handle it gracefully.
+ */
+TEST_F(Fhstat, inconsistent_ino)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t nodeid = 42;
+ const uint64_t ino = 711; // Could be anything that != nodeid
+ const mode_t mode = S_IFDIR | 0755;
+ const uid_t uid = 12345;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = nodeid;
+ out.body.entry.attr.ino = nodeid;
+ out.body.entry.attr.mode = mode;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ EXPECT_LOOKUP(nodeid, ".")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = nodeid;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.attr.mode = mode;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ /*
+ * The fhstat operation will actually succeed. But future operations
+ * will likely fail.
+ */
+ ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
+ EXPECT_EQ(ino, sb.st_ino);
+}
+
+/*
+ * If the server doesn't set FUSE_EXPORT_SUPPORT, then we can't do NFS-style
+ * lookups
+ */
+TEST_F(FhstatNotExportable, lookup_dot)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ const uint64_t ino = 42;
+ const mode_t mode = S_IFDIR | 0755;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(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.generation = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ ASSERT_EQ(-1, getfh(FULLPATH, &fhp));
+ ASSERT_EQ(EOPNOTSUPP, errno);
+}
+
+/* FreeBSD's fid struct doesn't have enough space for 64-bit generations */
+TEST_F(Getfh, eoverflow)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.generation = (uint64_t)UINT32_MAX + 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ ASSERT_NE(0, getfh(FULLPATH, &fhp));
+ EXPECT_EQ(EOVERFLOW, errno);
+}
+
+/* Get an NFS file handle */
+TEST_F(Getfh, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ uint64_t ino = 42;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = S_IFDIR | 0755;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = UINT64_MAX;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+}
+
+/*
+ * Call readdir via a file handle.
+ *
+ * This is how a userspace nfs server like nfs-ganesha or unfs3 would call
+ * readdir. The in-kernel NFS server never does any equivalent of open. I
+ * haven't discovered a way to mimic nfsd's behavior short of actually running
+ * nfsd.
+ */
+TEST_F(Readdir, getdirentries)
+{
+ const char FULLPATH[] = "mountpoint/some_dir";
+ const char RELPATH[] = "some_dir";
+ uint64_t ino = 42;
+ mode_t mode = S_IFDIR | 0755;
+ fhandle_t fhp;
+ int fd;
+ char buf[8192];
+ ssize_t r;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.generation = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ EXPECT_LOOKUP(ino, ".")
+ .WillOnce(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.generation = 1;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ expect_opendir(ino);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_READDIR &&
+ in.header.nodeid == ino &&
+ in.body.readdir.size == sizeof(buf));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ out.header.error = 0;
+ out.header.len = sizeof(out.header);
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ fd = fhopen(&fhp, O_DIRECTORY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ r = getdirentries(fd, buf, sizeof(buf), 0);
+ ASSERT_EQ(0, r) << strerror(errno);
+
+ leak(fd);
+}