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