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