xref: /freebsd/tests/sys/fs/fusefs/lookup.cc (revision e0c4386e7e71d93b0edc0c8fa156263fc4a8b0b6)
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 extern "C" {
32 #include <sys/param.h>
33 #include <sys/mount.h>
34 
35 #include <fcntl.h>
36 #include <unistd.h>
37 }
38 
39 #include "mockfs.hh"
40 #include "utils.hh"
41 
42 using namespace testing;
43 
44 class Lookup: public FuseTest {};
45 
46 class Lookup_7_8: public Lookup {
47 public:
48 virtual void SetUp() {
49 	m_kernel_minor_version = 8;
50 	Lookup::SetUp();
51 }
52 };
53 
54 class LookupExportable: public Lookup {
55 public:
56 virtual void SetUp() {
57 	m_init_flags = FUSE_EXPORT_SUPPORT;
58 	Lookup::SetUp();
59 }
60 };
61 
62 /*
63  * If lookup returns a non-zero cache timeout, then subsequent VOP_GETATTRs
64  * should use the cached attributes, rather than query the daemon
65  */
66 TEST_F(Lookup, attr_cache)
67 {
68 	const char FULLPATH[] = "mountpoint/some_file.txt";
69 	const char RELPATH[] = "some_file.txt";
70 	const uint64_t ino = 42;
71 	const uint64_t generation = 13;
72 	struct stat sb;
73 
74 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
75 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
76 		SET_OUT_HEADER_LEN(out, entry);
77 		out.body.entry.nodeid = ino;
78 		out.body.entry.attr_valid = UINT64_MAX;
79 		out.body.entry.attr.ino = ino;	// Must match nodeid
80 		out.body.entry.attr.mode = S_IFREG | 0644;
81 		out.body.entry.attr.size = 1;
82 		out.body.entry.attr.blocks = 2;
83 		out.body.entry.attr.atime = 3;
84 		out.body.entry.attr.mtime = 4;
85 		out.body.entry.attr.ctime = 5;
86 		out.body.entry.attr.atimensec = 6;
87 		out.body.entry.attr.mtimensec = 7;
88 		out.body.entry.attr.ctimensec = 8;
89 		out.body.entry.attr.nlink = 9;
90 		out.body.entry.attr.uid = 10;
91 		out.body.entry.attr.gid = 11;
92 		out.body.entry.attr.rdev = 12;
93 		out.body.entry.generation = generation;
94 	})));
95 	/* stat(2) issues a VOP_LOOKUP followed by a VOP_GETATTR */
96 	ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
97 	EXPECT_EQ(1, sb.st_size);
98 	EXPECT_EQ(2, sb.st_blocks);
99 	EXPECT_EQ(3, sb.st_atim.tv_sec);
100 	EXPECT_EQ(6, sb.st_atim.tv_nsec);
101 	EXPECT_EQ(4, sb.st_mtim.tv_sec);
102 	EXPECT_EQ(7, sb.st_mtim.tv_nsec);
103 	EXPECT_EQ(5, sb.st_ctim.tv_sec);
104 	EXPECT_EQ(8, sb.st_ctim.tv_nsec);
105 	EXPECT_EQ(9ull, sb.st_nlink);
106 	EXPECT_EQ(10ul, sb.st_uid);
107 	EXPECT_EQ(11ul, sb.st_gid);
108 	EXPECT_EQ(12ul, sb.st_rdev);
109 	EXPECT_EQ(ino, sb.st_ino);
110 	EXPECT_EQ(S_IFREG | 0644, sb.st_mode);
111 
112 	// fuse(4) does not _yet_ support inode generations
113 	//EXPECT_EQ(generation, sb.st_gen);
114 
115 	//st_birthtim and st_flags are not supported by protocol 7.8.  They're
116 	//only supported as OS-specific extensions to OSX.
117 	//EXPECT_EQ(, sb.st_birthtim);
118 	//EXPECT_EQ(, sb.st_flags);
119 
120 	//FUSE can't set st_blksize until protocol 7.9
121 }
122 
123 /*
124  * If lookup returns a finite but non-zero cache timeout, then we should discard
125  * the cached attributes and requery the daemon.
126  */
127 TEST_F(Lookup, attr_cache_timeout)
128 {
129 	const char FULLPATH[] = "mountpoint/some_file.txt";
130 	const char RELPATH[] = "some_file.txt";
131 	const uint64_t ino = 42;
132 	struct stat sb;
133 
134 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
135 	.Times(2)
136 	.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
137 		SET_OUT_HEADER_LEN(out, entry);
138 		out.body.entry.nodeid = ino;
139 		out.body.entry.attr_valid_nsec = NAP_NS / 2;
140 		out.body.entry.attr.ino = ino;	// Must match nodeid
141 		out.body.entry.attr.mode = S_IFREG | 0644;
142 	})));
143 
144 	/* access(2) will issue a VOP_LOOKUP and fill the attr cache */
145 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
146 	/* Next access(2) will use the cached attributes */
147 	nap();
148 	/* The cache has timed out; VOP_GETATTR should query the daemon*/
149 	ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
150 }
151 
152 TEST_F(Lookup, dot)
153 {
154 	const char FULLPATH[] = "mountpoint/some_dir/.";
155 	const char RELDIRPATH[] = "some_dir";
156 	uint64_t ino = 42;
157 
158 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
159 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
160 		SET_OUT_HEADER_LEN(out, entry);
161 		out.body.entry.attr.mode = S_IFDIR | 0755;
162 		out.body.entry.nodeid = ino;
163 		out.body.entry.attr_valid = UINT64_MAX;
164 		out.body.entry.entry_valid = UINT64_MAX;
165 	})));
166 
167 	/*
168 	 * access(2) is one of the few syscalls that will not (always) follow
169 	 * up a successful VOP_LOOKUP with another VOP.
170 	 */
171 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
172 }
173 
174 TEST_F(Lookup, dotdot)
175 {
176 	const char FULLPATH[] = "mountpoint/some_dir/..";
177 	const char RELDIRPATH[] = "some_dir";
178 
179 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
180 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
181 		SET_OUT_HEADER_LEN(out, entry);
182 		out.body.entry.attr.mode = S_IFDIR | 0755;
183 		out.body.entry.nodeid = 14;
184 		out.body.entry.attr_valid = UINT64_MAX;
185 		out.body.entry.entry_valid = UINT64_MAX;
186 	})));
187 
188 	/*
189 	 * access(2) is one of the few syscalls that will not (always) follow
190 	 * up a successful VOP_LOOKUP with another VOP.
191 	 */
192 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
193 }
194 
195 /*
196  * Lookup ".." when that vnode's entry cache has timed out, but its child's
197  * hasn't.  Since this file system doesn't set FUSE_EXPORT_SUPPORT, we have no
198  * choice but to use the cached entry, even though it expired.
199  */
200 TEST_F(Lookup, dotdot_entry_cache_timeout)
201 {
202 	uint64_t foo_ino = 42;
203 	uint64_t bar_ino = 43;
204 
205 	EXPECT_LOOKUP(FUSE_ROOT_ID, "foo")
206 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
207 		SET_OUT_HEADER_LEN(out, entry);
208 		out.body.entry.attr.mode = S_IFDIR | 0755;
209 		out.body.entry.nodeid = foo_ino;
210 		out.body.entry.attr_valid = UINT64_MAX;
211 		out.body.entry.entry_valid = 0;	// immediate timeout
212 	})));
213 	EXPECT_LOOKUP(foo_ino, "bar")
214 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
215 		SET_OUT_HEADER_LEN(out, entry);
216 		out.body.entry.attr.mode = S_IFDIR | 0755;
217 		out.body.entry.nodeid = bar_ino;
218 		out.body.entry.attr_valid = UINT64_MAX;
219 		out.body.entry.entry_valid = UINT64_MAX;
220 	})));
221 	expect_opendir(bar_ino);
222 
223 	int fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY);
224 	ASSERT_LE(0, fd) << strerror(errno);
225 	EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno);
226 }
227 
228 /*
229  * Lookup ".." for a vnode with no valid parent nid
230  * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259974
231  * Since the file system is not exportable, we have no choice but to return an
232  * error.
233  */
234 TEST_F(Lookup, dotdot_no_parent_nid)
235 {
236 	uint64_t foo_ino = 42;
237 	uint64_t bar_ino = 43;
238 	int fd;
239 
240 	EXPECT_LOOKUP(FUSE_ROOT_ID, "foo")
241 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
242 		SET_OUT_HEADER_LEN(out, entry);
243 		out.body.entry.attr.mode = S_IFDIR | 0755;
244 		out.body.entry.nodeid = foo_ino;
245 		out.body.entry.attr_valid = UINT64_MAX;
246 		out.body.entry.entry_valid = UINT64_MAX;
247 	})));
248 	EXPECT_LOOKUP(foo_ino, "bar")
249 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
250 		SET_OUT_HEADER_LEN(out, entry);
251 		out.body.entry.attr.mode = S_IFDIR | 0755;
252 		out.body.entry.nodeid = bar_ino;
253 		out.body.entry.attr_valid = UINT64_MAX;
254 		out.body.entry.entry_valid = UINT64_MAX;
255 	})));
256 	EXPECT_CALL(*m_mock, process(
257 		ResultOf([=](auto in) {
258 			return (in.header.opcode == FUSE_OPENDIR);
259 		}, Eq(true)),
260 		_)
261 	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
262 		SET_OUT_HEADER_LEN(out, open);
263 	})));
264 	expect_forget(foo_ino, 1, NULL);
265 
266 	fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY);
267 	ASSERT_LE(0, fd) << strerror(errno);
268 	// Try (and fail) to unmount the file system, to reclaim the mountpoint
269 	// and foo vnodes.
270 	ASSERT_NE(0, unmount("mountpoint", 0));
271 	EXPECT_EQ(EBUSY, errno);
272 	nap();		// Because vnode reclamation is asynchronous
273 	EXPECT_NE(0, faccessat(fd, "../..", F_OK, 0));
274 	EXPECT_EQ(ESTALE, errno);
275 }
276 
277 /*
278  * A daemon that returns an illegal error value should be handled gracefully.
279  * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263220
280  */
281 TEST_F(Lookup, ejustreturn)
282 {
283 	const char FULLPATH[] = "mountpoint/does_not_exist";
284 	const char RELPATH[] = "does_not_exist";
285 
286 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
287 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
288 		out.header.len = sizeof(out.header);
289 		out.header.error = 2;
290 		out.expected_errno = EINVAL;
291 	})));
292 
293 	EXPECT_NE(0, access(FULLPATH, F_OK));
294 
295 	EXPECT_EQ(EIO, errno);
296 }
297 
298 TEST_F(Lookup, enoent)
299 {
300 	const char FULLPATH[] = "mountpoint/does_not_exist";
301 	const char RELPATH[] = "does_not_exist";
302 
303 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
304 	.WillOnce(Invoke(ReturnErrno(ENOENT)));
305 	EXPECT_NE(0, access(FULLPATH, F_OK));
306 	EXPECT_EQ(ENOENT, errno);
307 }
308 
309 TEST_F(Lookup, enotdir)
310 {
311 	const char FULLPATH[] = "mountpoint/not_a_dir/some_file.txt";
312 	const char RELPATH[] = "not_a_dir";
313 
314 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
315 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
316 		SET_OUT_HEADER_LEN(out, entry);
317 		out.body.entry.entry_valid = UINT64_MAX;
318 		out.body.entry.attr.mode = S_IFREG | 0644;
319 		out.body.entry.nodeid = 42;
320 	})));
321 
322 	ASSERT_EQ(-1, access(FULLPATH, F_OK));
323 	ASSERT_EQ(ENOTDIR, errno);
324 }
325 
326 /*
327  * If lookup returns a non-zero entry timeout, then subsequent VOP_LOOKUPs
328  * should use the cached inode rather than requery the daemon
329  */
330 TEST_F(Lookup, entry_cache)
331 {
332 	const char FULLPATH[] = "mountpoint/some_file.txt";
333 	const char RELPATH[] = "some_file.txt";
334 
335 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
336 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
337 		SET_OUT_HEADER_LEN(out, entry);
338 		out.body.entry.entry_valid = UINT64_MAX;
339 		out.body.entry.attr.mode = S_IFREG | 0644;
340 		out.body.entry.nodeid = 14;
341 	})));
342 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
343 	/* The second access(2) should use the cache */
344 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
345 }
346 
347 /*
348  * If the daemon returns an error of 0 and an inode of 0, that's a flag for
349  * "ENOENT and cache it" with the given entry_timeout
350  */
351 TEST_F(Lookup, entry_cache_negative)
352 {
353 	struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0};
354 
355 	EXPECT_LOOKUP(FUSE_ROOT_ID, "does_not_exist")
356 	.Times(1)
357 	.WillOnce(Invoke(ReturnNegativeCache(&entry_valid)));
358 
359 	EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK));
360 	EXPECT_EQ(ENOENT, errno);
361 	EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK));
362 	EXPECT_EQ(ENOENT, errno);
363 }
364 
365 /* Negative entry caches should timeout, too */
366 TEST_F(Lookup, entry_cache_negative_timeout)
367 {
368 	const char *RELPATH = "does_not_exist";
369 	const char *FULLPATH = "mountpoint/does_not_exist";
370 	struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = NAP_NS / 2};
371 
372 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
373 	.Times(2)
374 	.WillRepeatedly(Invoke(ReturnNegativeCache(&entry_valid)));
375 
376 	EXPECT_NE(0, access(FULLPATH, F_OK));
377 	EXPECT_EQ(ENOENT, errno);
378 
379 	nap();
380 
381 	/* The cache has timed out; VOP_LOOKUP should requery the daemon*/
382 	EXPECT_NE(0, access(FULLPATH, F_OK));
383 	EXPECT_EQ(ENOENT, errno);
384 }
385 
386 /*
387  * If lookup returns a finite but non-zero entry cache timeout, then we should
388  * discard the cached inode and requery the daemon
389  */
390 TEST_F(Lookup, entry_cache_timeout)
391 {
392 	const char FULLPATH[] = "mountpoint/some_file.txt";
393 	const char RELPATH[] = "some_file.txt";
394 
395 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
396 	.Times(2)
397 	.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
398 		SET_OUT_HEADER_LEN(out, entry);
399 		out.body.entry.entry_valid_nsec = NAP_NS / 2;
400 		out.body.entry.attr.mode = S_IFREG | 0644;
401 		out.body.entry.nodeid = 14;
402 	})));
403 
404 	/* access(2) will issue a VOP_LOOKUP and fill the entry cache */
405 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
406 	/* Next access(2) will use the cached entry */
407 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
408 	nap();
409 	/* The cache has timed out; VOP_LOOKUP should requery the daemon*/
410 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
411 }
412 
413 TEST_F(Lookup, ok)
414 {
415 	const char FULLPATH[] = "mountpoint/some_file.txt";
416 	const char RELPATH[] = "some_file.txt";
417 
418 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
419 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
420 		SET_OUT_HEADER_LEN(out, entry);
421 		out.body.entry.attr.mode = S_IFREG | 0644;
422 		out.body.entry.nodeid = 14;
423 	})));
424 	/*
425 	 * access(2) is one of the few syscalls that will not (always) follow
426 	 * up a successful VOP_LOOKUP with another VOP.
427 	 */
428 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
429 }
430 
431 /*
432  * Lookup in a subdirectory of the fuse mount.  The naughty server returns the
433  * same inode for the child as for the parent.
434  */
435 TEST_F(Lookup, parent_inode)
436 {
437 	const char FULLPATH[] = "mountpoint/some_dir/some_file.txt";
438 	const char DIRPATH[] = "some_dir";
439 	const char RELPATH[] = "some_file.txt";
440 	uint64_t dir_ino = 2;
441 
442 	EXPECT_LOOKUP(FUSE_ROOT_ID, DIRPATH)
443 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
444 		SET_OUT_HEADER_LEN(out, entry);
445 		out.body.entry.attr.mode = S_IFDIR | 0755;
446 		out.body.entry.nodeid = dir_ino;
447 	})));
448 	EXPECT_LOOKUP(dir_ino, RELPATH)
449 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
450 		SET_OUT_HEADER_LEN(out, entry);
451 		out.body.entry.attr.mode = S_IFREG | 0644;
452 		out.body.entry.nodeid = dir_ino;
453 	})));
454 	/*
455 	 * access(2) is one of the few syscalls that will not (always) follow
456 	 * up a successful VOP_LOOKUP with another VOP.
457 	 */
458 	ASSERT_EQ(-1, access(FULLPATH, F_OK));
459 	ASSERT_EQ(EIO, errno);
460 }
461 
462 // Lookup in a subdirectory of the fuse mount
463 TEST_F(Lookup, subdir)
464 {
465 	const char FULLPATH[] = "mountpoint/some_dir/some_file.txt";
466 	const char DIRPATH[] = "some_dir";
467 	const char RELPATH[] = "some_file.txt";
468 	uint64_t dir_ino = 2;
469 	uint64_t file_ino = 3;
470 
471 	EXPECT_LOOKUP(FUSE_ROOT_ID, DIRPATH)
472 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
473 		SET_OUT_HEADER_LEN(out, entry);
474 		out.body.entry.attr.mode = S_IFDIR | 0755;
475 		out.body.entry.nodeid = dir_ino;
476 	})));
477 	EXPECT_LOOKUP(dir_ino, RELPATH)
478 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
479 		SET_OUT_HEADER_LEN(out, entry);
480 		out.body.entry.attr.mode = S_IFREG | 0644;
481 		out.body.entry.nodeid = file_ino;
482 	})));
483 	/*
484 	 * access(2) is one of the few syscalls that will not (always) follow
485 	 * up a successful VOP_LOOKUP with another VOP.
486 	 */
487 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
488 }
489 
490 /*
491  * The server returns two different vtypes for the same nodeid.  This is
492  * technically allowed if the entry's cache has already expired.
493  * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=258022
494  */
495 TEST_F(Lookup, vtype_conflict)
496 {
497 	const char FIRSTFULLPATH[] = "mountpoint/foo";
498 	const char SECONDFULLPATH[] = "mountpoint/bar";
499 	const char FIRSTRELPATH[] = "foo";
500 	const char SECONDRELPATH[] = "bar";
501 	uint64_t ino = 42;
502 
503 	EXPECT_LOOKUP(FUSE_ROOT_ID, FIRSTRELPATH)
504 	.WillOnce(Invoke(
505 		ReturnImmediate([=](auto in __unused, auto& out) {
506 		SET_OUT_HEADER_LEN(out, entry);
507 		out.body.entry.attr.mode = S_IFDIR | 0644;
508 		out.body.entry.nodeid = ino;
509 		out.body.entry.attr.nlink = 1;
510 	})));
511 	expect_lookup(SECONDRELPATH, ino, S_IFREG | 0755, 0, 1, UINT64_MAX);
512 	// VOP_FORGET happens asynchronously, so it may or may not arrive
513 	// before the test completes.
514 	EXPECT_CALL(*m_mock, process(
515 		ResultOf([=](auto in) {
516 			return (in.header.opcode == FUSE_FORGET &&
517 				in.header.nodeid == ino &&
518 				in.body.forget.nlookup == 1);
519 		}, Eq(true)),
520 		_)
521 	).Times(AtMost(1))
522 	.WillOnce(Invoke([=](auto in __unused, auto &out __unused) { }));
523 
524 	ASSERT_EQ(0, access(FIRSTFULLPATH, F_OK)) << strerror(errno);
525 	EXPECT_EQ(0, access(SECONDFULLPATH, F_OK)) << strerror(errno);
526 }
527 
528 TEST_F(Lookup_7_8, ok)
529 {
530 	const char FULLPATH[] = "mountpoint/some_file.txt";
531 	const char RELPATH[] = "some_file.txt";
532 
533 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
534 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
535 		SET_OUT_HEADER_LEN(out, entry_7_8);
536 		out.body.entry.attr.mode = S_IFREG | 0644;
537 		out.body.entry.nodeid = 14;
538 	})));
539 	/*
540 	 * access(2) is one of the few syscalls that will not (always) follow
541 	 * up a successful VOP_LOOKUP with another VOP.
542 	 */
543 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
544 }
545 
546 /*
547  * Lookup ".." when that vnode's entry cache has timed out, but its child's
548  * hasn't.
549  */
550 TEST_F(LookupExportable, dotdot_entry_cache_timeout)
551 {
552 	uint64_t foo_ino = 42;
553 	uint64_t bar_ino = 43;
554 
555 	EXPECT_LOOKUP(FUSE_ROOT_ID, "foo")
556 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
557 		SET_OUT_HEADER_LEN(out, entry);
558 		out.body.entry.attr.mode = S_IFDIR | 0755;
559 		out.body.entry.nodeid = foo_ino;
560 		out.body.entry.attr_valid = UINT64_MAX;
561 		out.body.entry.entry_valid = 0;	// immediate timeout
562 	})));
563 	EXPECT_LOOKUP(foo_ino, "bar")
564 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
565 		SET_OUT_HEADER_LEN(out, entry);
566 		out.body.entry.attr.mode = S_IFDIR | 0755;
567 		out.body.entry.nodeid = bar_ino;
568 		out.body.entry.attr_valid = UINT64_MAX;
569 		out.body.entry.entry_valid = UINT64_MAX;
570 	})));
571 	expect_opendir(bar_ino);
572 	EXPECT_LOOKUP(foo_ino, "..")
573 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
574 		SET_OUT_HEADER_LEN(out, entry);
575 		out.body.entry.attr.mode = S_IFDIR | 0755;
576 		out.body.entry.nodeid = FUSE_ROOT_ID;
577 		out.body.entry.attr_valid = UINT64_MAX;
578 		out.body.entry.entry_valid = UINT64_MAX;
579 	})));
580 
581 	int fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY);
582 	ASSERT_LE(0, fd) << strerror(errno);
583 	/* FreeBSD's fusefs driver always uses the same cache expiration time
584 	 * for ".." as for the directory itself.  So we need to look up two
585 	 * levels to find an expired ".." cache entry.
586 	 */
587 	EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno);
588 }
589 
590 /*
591  * Lookup ".." for a vnode with no valid parent nid
592  * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259974
593  * Since the file system is exportable, we should resolve the problem by
594  * sending a FUSE_LOOKUP for "..".
595  */
596 TEST_F(LookupExportable, dotdot_no_parent_nid)
597 {
598 	uint64_t foo_ino = 42;
599 	uint64_t bar_ino = 43;
600 	int fd;
601 
602 	EXPECT_LOOKUP(FUSE_ROOT_ID, "foo")
603 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
604 		SET_OUT_HEADER_LEN(out, entry);
605 		out.body.entry.attr.mode = S_IFDIR | 0755;
606 		out.body.entry.nodeid = foo_ino;
607 		out.body.entry.attr_valid = UINT64_MAX;
608 		out.body.entry.entry_valid = UINT64_MAX;
609 	})));
610 	EXPECT_LOOKUP(foo_ino, "bar")
611 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
612 		SET_OUT_HEADER_LEN(out, entry);
613 		out.body.entry.attr.mode = S_IFDIR | 0755;
614 		out.body.entry.nodeid = bar_ino;
615 		out.body.entry.attr_valid = UINT64_MAX;
616 		out.body.entry.entry_valid = UINT64_MAX;
617 	})));
618 	EXPECT_CALL(*m_mock, process(
619 		ResultOf([=](auto in) {
620 			return (in.header.opcode == FUSE_OPENDIR);
621 		}, Eq(true)),
622 		_)
623 	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
624 		SET_OUT_HEADER_LEN(out, open);
625 	})));
626 	expect_forget(foo_ino, 1, NULL);
627 	EXPECT_LOOKUP(bar_ino, "..")
628 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
629 		SET_OUT_HEADER_LEN(out, entry);
630 		out.body.entry.attr.mode = S_IFDIR | 0755;
631 		out.body.entry.nodeid = foo_ino;
632 		out.body.entry.attr_valid = UINT64_MAX;
633 		out.body.entry.entry_valid = UINT64_MAX;
634 	})));
635 	EXPECT_LOOKUP(foo_ino, "..")
636 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
637 		SET_OUT_HEADER_LEN(out, entry);
638 		out.body.entry.attr.mode = S_IFDIR | 0755;
639 		out.body.entry.nodeid = FUSE_ROOT_ID;
640 		out.body.entry.attr_valid = UINT64_MAX;
641 		out.body.entry.entry_valid = UINT64_MAX;
642 	})));
643 
644 	fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY);
645 	ASSERT_LE(0, fd) << strerror(errno);
646 	// Try (and fail) to unmount the file system, to reclaim the mountpoint
647 	// and foo vnodes.
648 	ASSERT_NE(0, unmount("mountpoint", 0));
649 	EXPECT_EQ(EBUSY, errno);
650 	nap();		// Because vnode reclamation is asynchronous
651 	EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno);
652 }
653