xref: /freebsd/tests/sys/fs/fusefs/nfs.cc (revision f1ec3bc06ed276e6e24996515bdce729d51e11d8)
1 /*-
2  * SPDX-License-Identifier: BSD-2-Clause
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 
31 /* This file tests functionality needed by NFS servers */
32 extern "C" {
33 #include <sys/param.h>
34 #include <sys/mount.h>
35 
36 #include <dirent.h>
37 #include <fcntl.h>
38 #include <unistd.h>
39 }
40 
41 #include "mockfs.hh"
42 #include "utils.hh"
43 
44 using namespace std;
45 using namespace testing;
46 
47 
48 class Nfs: public FuseTest {
49 public:
SetUp()50 virtual void SetUp() {
51 	if (geteuid() != 0)
52                 GTEST_SKIP() << "This test requires a privileged user";
53 	FuseTest::SetUp();
54 }
55 };
56 
57 class Exportable: public Nfs {
58 public:
SetUp()59 virtual void SetUp() {
60 	m_init_flags = FUSE_EXPORT_SUPPORT;
61 	Nfs::SetUp();
62 }
63 };
64 
65 class Fhstat: public Exportable {};
66 class FhstatNotExportable: public Nfs {};
67 class Getfh: public Exportable {};
68 class Readdir: public Exportable {};
69 
70 /* If the server returns a different generation number, then file is stale */
TEST_F(Fhstat,estale)71 TEST_F(Fhstat, estale)
72 {
73 	const char FULLPATH[] = "mountpoint/some_dir/.";
74 	const char RELDIRPATH[] = "some_dir";
75 	fhandle_t fhp;
76 	struct stat sb;
77 	const uint64_t ino = 42;
78 	const mode_t mode = S_IFDIR | 0755;
79 	Sequence seq;
80 
81 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
82 	.InSequence(seq)
83 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
84 		SET_OUT_HEADER_LEN(out, entry);
85 		out.body.entry.attr.mode = mode;
86 		out.body.entry.nodeid = ino;
87 		out.body.entry.attr.ino = ino;
88 		out.body.entry.generation = 1;
89 		out.body.entry.attr_valid = UINT64_MAX;
90 		out.body.entry.entry_valid = 0;
91 	})));
92 
93 	EXPECT_LOOKUP(ino, ".")
94 	.InSequence(seq)
95 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
96 		SET_OUT_HEADER_LEN(out, entry);
97 		out.body.entry.attr.mode = mode;
98 		out.body.entry.nodeid = ino;
99 		out.body.entry.attr.ino = 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 "." */
TEST_F(Fhstat,lookup_dot)111 TEST_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.attr.ino = ino;
127 		out.body.entry.generation = 1;
128 		out.body.entry.attr.uid = uid;
129 		out.body.entry.attr_valid = UINT64_MAX;
130 		out.body.entry.entry_valid = 0;
131 	})));
132 
133 	EXPECT_LOOKUP(ino, ".")
134 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
135 		SET_OUT_HEADER_LEN(out, entry);
136 		out.body.entry.attr.mode = mode;
137 		out.body.entry.nodeid = ino;
138 		out.body.entry.attr.ino = ino;
139 		out.body.entry.generation = 1;
140 		out.body.entry.attr.uid = uid;
141 		out.body.entry.attr_valid = UINT64_MAX;
142 		out.body.entry.entry_valid = 0;
143 	})));
144 
145 	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
146 	ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
147 	EXPECT_EQ(uid, sb.st_uid);
148 	EXPECT_EQ(mode, sb.st_mode);
149 }
150 
151 /* Gracefully handle failures to lookup ".". */
TEST_F(Fhstat,lookup_dot_error)152 TEST_F(Fhstat, lookup_dot_error)
153 {
154 	const char FULLPATH[] = "mountpoint/some_dir/.";
155 	const char RELDIRPATH[] = "some_dir";
156 	fhandle_t fhp;
157 	struct stat sb;
158 	const uint64_t ino = 42;
159 	const mode_t mode = S_IFDIR | 0755;
160 	const uid_t uid = 12345;
161 
162 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
163 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
164 		SET_OUT_HEADER_LEN(out, entry);
165 		out.body.entry.attr.mode = mode;
166 		out.body.entry.nodeid = ino;
167 		out.body.entry.attr.ino = ino;
168 		out.body.entry.generation = 1;
169 		out.body.entry.attr.uid = uid;
170 		out.body.entry.attr_valid = UINT64_MAX;
171 		out.body.entry.entry_valid = 0;
172 	})));
173 
174 	EXPECT_LOOKUP(ino, ".")
175 	.WillOnce(Invoke(ReturnErrno(EDOOFUS)));
176 
177 	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
178 	ASSERT_EQ(-1, fhstat(&fhp, &sb));
179 	EXPECT_EQ(EDOOFUS, errno);
180 }
181 
182 /* Use a file handle whose entry is still cached */
TEST_F(Fhstat,cached)183 TEST_F(Fhstat, cached)
184 {
185 	const char FULLPATH[] = "mountpoint/some_dir/.";
186 	const char RELDIRPATH[] = "some_dir";
187 	fhandle_t fhp;
188 	struct stat sb;
189 	const uint64_t ino = 42;
190 	const mode_t mode = S_IFDIR | 0755;
191 
192 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
193 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
194 		SET_OUT_HEADER_LEN(out, entry);
195 		out.body.entry.attr.mode = mode;
196 		out.body.entry.nodeid = ino;
197 		out.body.entry.attr.ino = ino;
198 		out.body.entry.generation = 1;
199 		out.body.entry.attr.ino = ino;
200 		out.body.entry.attr_valid = UINT64_MAX;
201 		out.body.entry.entry_valid = UINT64_MAX;
202 	})));
203 
204 	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
205 	ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
206 	EXPECT_EQ(ino, sb.st_ino);
207 }
208 
209 /* File handle entries should expire from the cache, too */
TEST_F(Fhstat,cache_expired)210 TEST_F(Fhstat, cache_expired)
211 {
212 	const char FULLPATH[] = "mountpoint/some_dir/.";
213 	const char RELDIRPATH[] = "some_dir";
214 	fhandle_t fhp;
215 	struct stat sb;
216 	const uint64_t ino = 42;
217 	const mode_t mode = S_IFDIR | 0755;
218 
219 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
220 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
221 		SET_OUT_HEADER_LEN(out, entry);
222 		out.body.entry.attr.mode = mode;
223 		out.body.entry.nodeid = ino;
224 		out.body.entry.attr.ino = ino;
225 		out.body.entry.generation = 1;
226 		out.body.entry.attr.ino = ino;
227 		out.body.entry.attr_valid = UINT64_MAX;
228 		out.body.entry.entry_valid_nsec = NAP_NS / 2;
229 	})));
230 
231 	EXPECT_LOOKUP(ino, ".")
232 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
233 		SET_OUT_HEADER_LEN(out, entry);
234 		out.body.entry.attr.mode = mode;
235 		out.body.entry.nodeid = ino;
236 		out.body.entry.attr.ino = ino;
237 		out.body.entry.generation = 1;
238 		out.body.entry.attr.ino = ino;
239 		out.body.entry.attr_valid = UINT64_MAX;
240 		out.body.entry.entry_valid = 0;
241 	})));
242 
243 	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
244 	ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
245 	EXPECT_EQ(ino, sb.st_ino);
246 
247 	nap();
248 
249 	/* Cache should be expired; fuse should issue a FUSE_LOOKUP */
250 	ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
251 	EXPECT_EQ(ino, sb.st_ino);
252 }
253 
254 /*
255  * If the server returns a FUSE_LOOKUP response for a nodeid that we didn't
256  * lookup, it's a bug.  But we should handle it gracefully.
257  */
TEST_F(Fhstat,inconsistent_nodeid)258 TEST_F(Fhstat, inconsistent_nodeid)
259 {
260 	const char FULLPATH[] = "mountpoint/some_dir/.";
261 	const char RELDIRPATH[] = "some_dir";
262 	fhandle_t fhp;
263 	struct stat sb;
264 	const uint64_t ino_in = 42;
265 	const uint64_t ino_out = 43;
266 	const mode_t mode = S_IFDIR | 0755;
267 	const uid_t uid = 12345;
268 
269 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
270 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
271 		SET_OUT_HEADER_LEN(out, entry);
272 		out.body.entry.nodeid = ino_in;
273 		out.body.entry.attr.ino = ino_in;
274 		out.body.entry.attr.mode = mode;
275 		out.body.entry.generation = 1;
276 		out.body.entry.attr.uid = uid;
277 		out.body.entry.attr_valid = UINT64_MAX;
278 		out.body.entry.entry_valid = 0;
279 	})));
280 
281 	EXPECT_LOOKUP(ino_in, ".")
282 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
283 		SET_OUT_HEADER_LEN(out, entry);
284 		out.body.entry.nodeid = ino_out;
285 		out.body.entry.attr.ino = ino_out;
286 		out.body.entry.attr.mode = mode;
287 		out.body.entry.generation = 1;
288 		out.body.entry.attr.uid = uid;
289 		out.body.entry.attr_valid = UINT64_MAX;
290 		out.body.entry.entry_valid = 0;
291 	})));
292 
293 	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
294 	EXPECT_NE(0, fhstat(&fhp, &sb)) << strerror(errno);
295 	EXPECT_EQ(EIO, errno);
296 }
297 
298 /*
299  * If the server returns a FUSE_LOOKUP response where the nodeid doesn't match
300  * the inode number, and the file system is exported, it's a bug.  But we
301  * should handle it gracefully.
302  */
TEST_F(Fhstat,inconsistent_ino)303 TEST_F(Fhstat, inconsistent_ino)
304 {
305 	const char FULLPATH[] = "mountpoint/some_dir/.";
306 	const char RELDIRPATH[] = "some_dir";
307 	fhandle_t fhp;
308 	struct stat sb;
309 	const uint64_t nodeid = 42;
310 	const uint64_t ino = 711;	// Could be anything that != nodeid
311 	const mode_t mode = S_IFDIR | 0755;
312 	const uid_t uid = 12345;
313 
314 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
315 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
316 		SET_OUT_HEADER_LEN(out, entry);
317 		out.body.entry.nodeid = nodeid;
318 		out.body.entry.attr.ino = nodeid;
319 		out.body.entry.attr.mode = mode;
320 		out.body.entry.generation = 1;
321 		out.body.entry.attr.uid = uid;
322 		out.body.entry.attr_valid = UINT64_MAX;
323 		out.body.entry.entry_valid = 0;
324 	})));
325 
326 	EXPECT_LOOKUP(nodeid, ".")
327 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
328 		SET_OUT_HEADER_LEN(out, entry);
329 		out.body.entry.nodeid = nodeid;
330 		out.body.entry.attr.ino = ino;
331 		out.body.entry.attr.mode = mode;
332 		out.body.entry.generation = 1;
333 		out.body.entry.attr.uid = uid;
334 		out.body.entry.attr_valid = UINT64_MAX;
335 		out.body.entry.entry_valid = 0;
336 	})));
337 
338 	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
339 	/*
340 	 * The fhstat operation will actually succeed.  But future operations
341 	 * will likely fail.
342 	 */
343 	ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
344 	EXPECT_EQ(ino, sb.st_ino);
345 }
346 
347 /*
348  * If the server doesn't set FUSE_EXPORT_SUPPORT, then we can't do NFS-style
349  * lookups
350  */
TEST_F(FhstatNotExportable,lookup_dot)351 TEST_F(FhstatNotExportable, lookup_dot)
352 {
353 	const char FULLPATH[] = "mountpoint/some_dir/.";
354 	const char RELDIRPATH[] = "some_dir";
355 	fhandle_t fhp;
356 	const uint64_t ino = 42;
357 	const mode_t mode = S_IFDIR | 0755;
358 
359 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
360 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
361 		SET_OUT_HEADER_LEN(out, entry);
362 		out.body.entry.attr.mode = mode;
363 		out.body.entry.nodeid = ino;
364 		out.body.entry.attr.ino = ino;
365 		out.body.entry.generation = 1;
366 		out.body.entry.attr_valid = UINT64_MAX;
367 		out.body.entry.entry_valid = 0;
368 	})));
369 
370 	ASSERT_EQ(-1, getfh(FULLPATH, &fhp));
371 	ASSERT_EQ(EOPNOTSUPP, errno);
372 }
373 
374 /* FreeBSD's fid struct doesn't have enough space for 64-bit generations */
TEST_F(Getfh,eoverflow)375 TEST_F(Getfh, eoverflow)
376 {
377 	const char FULLPATH[] = "mountpoint/some_dir/.";
378 	const char RELDIRPATH[] = "some_dir";
379 	fhandle_t fhp;
380 	uint64_t ino = 42;
381 
382 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
383 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
384 		SET_OUT_HEADER_LEN(out, entry);
385 		out.body.entry.attr.mode = S_IFDIR | 0755;
386 		out.body.entry.nodeid = ino;
387 		out.body.entry.attr.ino = ino;
388 		out.body.entry.generation = (uint64_t)UINT32_MAX + 1;
389 		out.body.entry.attr_valid = UINT64_MAX;
390 		out.body.entry.entry_valid = UINT64_MAX;
391 	})));
392 
393 	ASSERT_NE(0, getfh(FULLPATH, &fhp));
394 	EXPECT_EQ(EOVERFLOW, errno);
395 }
396 
397 /* Get an NFS file handle */
TEST_F(Getfh,ok)398 TEST_F(Getfh, ok)
399 {
400 	const char FULLPATH[] = "mountpoint/some_dir/.";
401 	const char RELDIRPATH[] = "some_dir";
402 	fhandle_t fhp;
403 	uint64_t ino = 42;
404 
405 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
406 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
407 		SET_OUT_HEADER_LEN(out, entry);
408 		out.body.entry.attr.mode = S_IFDIR | 0755;
409 		out.body.entry.nodeid = ino;
410 		out.body.entry.attr.ino = ino;
411 		out.body.entry.attr_valid = UINT64_MAX;
412 		out.body.entry.entry_valid = UINT64_MAX;
413 	})));
414 
415 	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
416 }
417 
418 /*
419  * Call readdir via a file handle.
420  *
421  * This is how a userspace nfs server like nfs-ganesha or unfs3 would call
422  * readdir.  The in-kernel NFS server never does any equivalent of open.  I
423  * haven't discovered a way to mimic nfsd's behavior short of actually running
424  * nfsd.
425  */
TEST_F(Readdir,getdirentries)426 TEST_F(Readdir, getdirentries)
427 {
428 	const char FULLPATH[] = "mountpoint/some_dir";
429 	const char RELPATH[] = "some_dir";
430 	uint64_t ino = 42;
431 	mode_t mode = S_IFDIR | 0755;
432 	fhandle_t fhp;
433 	int fd;
434 	char buf[8192];
435 	ssize_t r;
436 
437 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
438 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
439 		SET_OUT_HEADER_LEN(out, entry);
440 		out.body.entry.attr.mode = mode;
441 		out.body.entry.nodeid = ino;
442 		out.body.entry.attr.ino = ino;
443 		out.body.entry.generation = 1;
444 		out.body.entry.attr_valid = UINT64_MAX;
445 		out.body.entry.entry_valid = 0;
446 	})));
447 
448 	EXPECT_LOOKUP(ino, ".")
449 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
450 		SET_OUT_HEADER_LEN(out, entry);
451 		out.body.entry.attr.mode = mode;
452 		out.body.entry.nodeid = ino;
453 		out.body.entry.attr.ino = ino;
454 		out.body.entry.generation = 1;
455 		out.body.entry.attr_valid = UINT64_MAX;
456 		out.body.entry.entry_valid = 0;
457 	})));
458 
459 	expect_opendir(ino);
460 
461 	EXPECT_CALL(*m_mock, process(
462 		ResultOf([=](auto in) {
463 			return (in.header.opcode == FUSE_READDIR &&
464 				in.header.nodeid == ino &&
465 				in.body.readdir.size == sizeof(buf));
466 		}, Eq(true)),
467 		_)
468 	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
469 		out.header.error = 0;
470 		out.header.len = sizeof(out.header);
471 	})));
472 
473 	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
474 	fd = fhopen(&fhp, O_DIRECTORY);
475 	ASSERT_LE(0, fd) << strerror(errno);
476 	r = getdirentries(fd, buf, sizeof(buf), 0);
477 	ASSERT_EQ(0, r) << strerror(errno);
478 
479 	leak(fd);
480 }
481