1/*-
2 * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
3 *
4 * Copyright (c) 2019 The FreeBSD Foundation
5 *
6 * This software was developed by BFF Storage Systems, LLC under sponsorship
7 * from the FreeBSD Foundation.
8 *
9 * Redistribution and use in source and binary forms, with or without
10 * modification, are permitted provided that the following conditions
11 * are met:
12 * 1. Redistributions of source code must retain the above copyright
13 *    notice, this list of conditions and the following disclaimer.
14 * 2. Redistributions in binary form must reproduce the above copyright
15 *    notice, this list of conditions and the following disclaimer in the
16 *    documentation and/or other materials provided with the distribution.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
19 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
22 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
28 * SUCH DAMAGE.
29 *
30 * $FreeBSD$
31 */
32
33/* This file tests functionality needed by NFS servers */
34extern "C" {
35#include <sys/param.h>
36#include <sys/mount.h>
37
38#include <dirent.h>
39#include <fcntl.h>
40#include <unistd.h>
41}
42
43#include "mockfs.hh"
44#include "utils.hh"
45
46using namespace std;
47using namespace testing;
48
49
50class Nfs: public FuseTest {
51public:
52virtual void SetUp() {
53	if (geteuid() != 0)
54                GTEST_SKIP() << "This test requires a privileged user";
55	FuseTest::SetUp();
56}
57};
58
59class Exportable: public Nfs {
60public:
61virtual void SetUp() {
62	m_init_flags = FUSE_EXPORT_SUPPORT;
63	Nfs::SetUp();
64}
65};
66
67class Fhstat: public Exportable {};
68class FhstatNotExportable: public Nfs {};
69class Getfh: public Exportable {};
70class Readdir: public Exportable {};
71
72/* If the server returns a different generation number, then file is stale */
73TEST_F(Fhstat, estale)
74{
75	const char FULLPATH[] = "mountpoint/some_dir/.";
76	const char RELDIRPATH[] = "some_dir";
77	fhandle_t fhp;
78	struct stat sb;
79	const uint64_t ino = 42;
80	const mode_t mode = S_IFDIR | 0755;
81	Sequence seq;
82
83	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
84	.InSequence(seq)
85	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
86		SET_OUT_HEADER_LEN(out, entry);
87		out.body.entry.attr.mode = mode;
88		out.body.entry.nodeid = ino;
89		out.body.entry.generation = 1;
90		out.body.entry.attr_valid = UINT64_MAX;
91		out.body.entry.entry_valid = 0;
92	})));
93
94	EXPECT_LOOKUP(ino, ".")
95	.InSequence(seq)
96	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
97		SET_OUT_HEADER_LEN(out, entry);
98		out.body.entry.attr.mode = mode;
99		out.body.entry.nodeid = ino;
100		out.body.entry.generation = 2;
101		out.body.entry.attr_valid = UINT64_MAX;
102		out.body.entry.entry_valid = 0;
103	})));
104
105	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
106	ASSERT_EQ(-1, fhstat(&fhp, &sb));
107	EXPECT_EQ(ESTALE, errno);
108}
109
110/* If we must lookup an entry from the server, send a LOOKUP request for "." */
111TEST_F(Fhstat, lookup_dot)
112{
113	const char FULLPATH[] = "mountpoint/some_dir/.";
114	const char RELDIRPATH[] = "some_dir";
115	fhandle_t fhp;
116	struct stat sb;
117	const uint64_t ino = 42;
118	const mode_t mode = S_IFDIR | 0755;
119	const uid_t uid = 12345;
120
121	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
122	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
123		SET_OUT_HEADER_LEN(out, entry);
124		out.body.entry.attr.mode = mode;
125		out.body.entry.nodeid = ino;
126		out.body.entry.generation = 1;
127		out.body.entry.attr.uid = uid;
128		out.body.entry.attr_valid = UINT64_MAX;
129		out.body.entry.entry_valid = 0;
130	})));
131
132	EXPECT_LOOKUP(ino, ".")
133	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
134		SET_OUT_HEADER_LEN(out, entry);
135		out.body.entry.attr.mode = mode;
136		out.body.entry.nodeid = ino;
137		out.body.entry.generation = 1;
138		out.body.entry.attr.uid = uid;
139		out.body.entry.attr_valid = UINT64_MAX;
140		out.body.entry.entry_valid = 0;
141	})));
142
143	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
144	ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
145	EXPECT_EQ(uid, sb.st_uid);
146	EXPECT_EQ(mode, sb.st_mode);
147}
148
149/* Use a file handle whose entry is still cached */
150TEST_F(Fhstat, cached)
151{
152	const char FULLPATH[] = "mountpoint/some_dir/.";
153	const char RELDIRPATH[] = "some_dir";
154	fhandle_t fhp;
155	struct stat sb;
156	const uint64_t ino = 42;
157	const mode_t mode = S_IFDIR | 0755;
158
159	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
160	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
161		SET_OUT_HEADER_LEN(out, entry);
162		out.body.entry.attr.mode = mode;
163		out.body.entry.nodeid = ino;
164		out.body.entry.generation = 1;
165		out.body.entry.attr.ino = ino;
166		out.body.entry.attr_valid = UINT64_MAX;
167		out.body.entry.entry_valid = UINT64_MAX;
168	})));
169
170	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
171	ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
172	EXPECT_EQ(ino, sb.st_ino);
173}
174
175/* File handle entries should expire from the cache, too */
176TEST_F(Fhstat, cache_expired)
177{
178	const char FULLPATH[] = "mountpoint/some_dir/.";
179	const char RELDIRPATH[] = "some_dir";
180	fhandle_t fhp;
181	struct stat sb;
182	const uint64_t ino = 42;
183	const mode_t mode = S_IFDIR | 0755;
184
185	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
186	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
187		SET_OUT_HEADER_LEN(out, entry);
188		out.body.entry.attr.mode = mode;
189		out.body.entry.nodeid = ino;
190		out.body.entry.generation = 1;
191		out.body.entry.attr.ino = ino;
192		out.body.entry.attr_valid = UINT64_MAX;
193		out.body.entry.entry_valid_nsec = NAP_NS / 2;
194	})));
195
196	EXPECT_LOOKUP(ino, ".")
197	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
198		SET_OUT_HEADER_LEN(out, entry);
199		out.body.entry.attr.mode = mode;
200		out.body.entry.nodeid = ino;
201		out.body.entry.generation = 1;
202		out.body.entry.attr.ino = ino;
203		out.body.entry.attr_valid = UINT64_MAX;
204		out.body.entry.entry_valid = 0;
205	})));
206
207	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
208	ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
209	EXPECT_EQ(ino, sb.st_ino);
210
211	nap();
212
213	/* Cache should be expired; fuse should issue a FUSE_LOOKUP */
214	ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
215	EXPECT_EQ(ino, sb.st_ino);
216}
217
218/*
219 * If the server doesn't set FUSE_EXPORT_SUPPORT, then we can't do NFS-style
220 * lookups
221 */
222TEST_F(FhstatNotExportable, lookup_dot)
223{
224	const char FULLPATH[] = "mountpoint/some_dir/.";
225	const char RELDIRPATH[] = "some_dir";
226	fhandle_t fhp;
227	const uint64_t ino = 42;
228	const mode_t mode = S_IFDIR | 0755;
229
230	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
231	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
232		SET_OUT_HEADER_LEN(out, entry);
233		out.body.entry.attr.mode = mode;
234		out.body.entry.nodeid = ino;
235		out.body.entry.generation = 1;
236		out.body.entry.attr_valid = UINT64_MAX;
237		out.body.entry.entry_valid = 0;
238	})));
239
240	ASSERT_EQ(-1, getfh(FULLPATH, &fhp));
241	ASSERT_EQ(EOPNOTSUPP, errno);
242}
243
244/* FreeBSD's fid struct doesn't have enough space for 64-bit generations */
245TEST_F(Getfh, eoverflow)
246{
247	const char FULLPATH[] = "mountpoint/some_dir/.";
248	const char RELDIRPATH[] = "some_dir";
249	fhandle_t fhp;
250	uint64_t ino = 42;
251
252	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
253	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
254		SET_OUT_HEADER_LEN(out, entry);
255		out.body.entry.attr.mode = S_IFDIR | 0755;
256		out.body.entry.nodeid = ino;
257		out.body.entry.generation = (uint64_t)UINT32_MAX + 1;
258		out.body.entry.attr_valid = UINT64_MAX;
259		out.body.entry.entry_valid = UINT64_MAX;
260	})));
261
262	ASSERT_NE(0, getfh(FULLPATH, &fhp));
263	EXPECT_EQ(EOVERFLOW, errno);
264}
265
266/* Get an NFS file handle */
267TEST_F(Getfh, ok)
268{
269	const char FULLPATH[] = "mountpoint/some_dir/.";
270	const char RELDIRPATH[] = "some_dir";
271	fhandle_t fhp;
272	uint64_t ino = 42;
273
274	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
275	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
276		SET_OUT_HEADER_LEN(out, entry);
277		out.body.entry.attr.mode = S_IFDIR | 0755;
278		out.body.entry.nodeid = ino;
279		out.body.entry.attr_valid = UINT64_MAX;
280		out.body.entry.entry_valid = UINT64_MAX;
281	})));
282
283	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
284}
285
286/*
287 * Call readdir via a file handle.
288 *
289 * This is how a userspace nfs server like nfs-ganesha or unfs3 would call
290 * readdir.  The in-kernel NFS server never does any equivalent of open.  I
291 * haven't discovered a way to mimic nfsd's behavior short of actually running
292 * nfsd.
293 */
294TEST_F(Readdir, getdirentries)
295{
296	const char FULLPATH[] = "mountpoint/some_dir";
297	const char RELPATH[] = "some_dir";
298	uint64_t ino = 42;
299	mode_t mode = S_IFDIR | 0755;
300	fhandle_t fhp;
301	int fd;
302	char buf[8192];
303	ssize_t r;
304
305	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
306	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
307		SET_OUT_HEADER_LEN(out, entry);
308		out.body.entry.attr.mode = mode;
309		out.body.entry.nodeid = ino;
310		out.body.entry.generation = 1;
311		out.body.entry.attr_valid = UINT64_MAX;
312		out.body.entry.entry_valid = 0;
313	})));
314
315	EXPECT_LOOKUP(ino, ".")
316	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
317		SET_OUT_HEADER_LEN(out, entry);
318		out.body.entry.attr.mode = mode;
319		out.body.entry.nodeid = ino;
320		out.body.entry.generation = 1;
321		out.body.entry.attr_valid = UINT64_MAX;
322		out.body.entry.entry_valid = 0;
323	})));
324
325	expect_opendir(ino);
326
327	EXPECT_CALL(*m_mock, process(
328		ResultOf([=](auto in) {
329			return (in.header.opcode == FUSE_READDIR &&
330				in.header.nodeid == ino &&
331				in.body.readdir.size == sizeof(buf));
332		}, Eq(true)),
333		_)
334	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
335		out.header.error = 0;
336		out.header.len = sizeof(out.header);
337	})));
338
339	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
340	fd = fhopen(&fhp, O_DIRECTORY);
341	ASSERT_LE(0, fd) << strerror(errno);
342	r = getdirentries(fd, buf, sizeof(buf), 0);
343	ASSERT_EQ(0, r) << strerror(errno);
344
345	leak(fd);
346}
347