diff options
Diffstat (limited to 'tests/sys/fs/fusefs/nfs.cc')
| -rw-r--r-- | tests/sys/fs/fusefs/nfs.cc | 480 | 
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); +} | 
