xref: /freebsd/tests/sys/fs/fusefs/lookup.cc (revision 77a1348b3c1cfe8547be49a121b56299a1e18b69)
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 extern "C" {
34 #include <unistd.h>
35 }
36 
37 #include "mockfs.hh"
38 #include "utils.hh"
39 
40 using namespace testing;
41 
42 class Lookup: public FuseTest {};
43 class Lookup_7_8: public Lookup {
44 public:
45 virtual void SetUp() {
46 	m_kernel_minor_version = 8;
47 	Lookup::SetUp();
48 }
49 };
50 
51 /*
52  * If lookup returns a non-zero cache timeout, then subsequent VOP_GETATTRs
53  * should use the cached attributes, rather than query the daemon
54  */
55 TEST_F(Lookup, attr_cache)
56 {
57 	const char FULLPATH[] = "mountpoint/some_file.txt";
58 	const char RELPATH[] = "some_file.txt";
59 	const uint64_t ino = 42;
60 	const uint64_t generation = 13;
61 	struct stat sb;
62 
63 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
64 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
65 		SET_OUT_HEADER_LEN(out, entry);
66 		out.body.entry.nodeid = ino;
67 		out.body.entry.attr_valid = UINT64_MAX;
68 		out.body.entry.attr.ino = ino;	// Must match nodeid
69 		out.body.entry.attr.mode = S_IFREG | 0644;
70 		out.body.entry.attr.size = 1;
71 		out.body.entry.attr.blocks = 2;
72 		out.body.entry.attr.atime = 3;
73 		out.body.entry.attr.mtime = 4;
74 		out.body.entry.attr.ctime = 5;
75 		out.body.entry.attr.atimensec = 6;
76 		out.body.entry.attr.mtimensec = 7;
77 		out.body.entry.attr.ctimensec = 8;
78 		out.body.entry.attr.nlink = 9;
79 		out.body.entry.attr.uid = 10;
80 		out.body.entry.attr.gid = 11;
81 		out.body.entry.attr.rdev = 12;
82 		out.body.entry.generation = generation;
83 	})));
84 	/* stat(2) issues a VOP_LOOKUP followed by a VOP_GETATTR */
85 	ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
86 	EXPECT_EQ(1, sb.st_size);
87 	EXPECT_EQ(2, sb.st_blocks);
88 	EXPECT_EQ(3, sb.st_atim.tv_sec);
89 	EXPECT_EQ(6, sb.st_atim.tv_nsec);
90 	EXPECT_EQ(4, sb.st_mtim.tv_sec);
91 	EXPECT_EQ(7, sb.st_mtim.tv_nsec);
92 	EXPECT_EQ(5, sb.st_ctim.tv_sec);
93 	EXPECT_EQ(8, sb.st_ctim.tv_nsec);
94 	EXPECT_EQ(9ull, sb.st_nlink);
95 	EXPECT_EQ(10ul, sb.st_uid);
96 	EXPECT_EQ(11ul, sb.st_gid);
97 	EXPECT_EQ(12ul, sb.st_rdev);
98 	EXPECT_EQ(ino, sb.st_ino);
99 	EXPECT_EQ(S_IFREG | 0644, sb.st_mode);
100 
101 	// fuse(4) does not _yet_ support inode generations
102 	//EXPECT_EQ(generation, sb.st_gen);
103 
104 	//st_birthtim and st_flags are not supported by protocol 7.8.  They're
105 	//only supported as OS-specific extensions to OSX.
106 	//EXPECT_EQ(, sb.st_birthtim);
107 	//EXPECT_EQ(, sb.st_flags);
108 
109 	//FUSE can't set st_blksize until protocol 7.9
110 }
111 
112 /*
113  * If lookup returns a finite but non-zero cache timeout, then we should discard
114  * the cached attributes and requery the daemon.
115  */
116 TEST_F(Lookup, attr_cache_timeout)
117 {
118 	const char FULLPATH[] = "mountpoint/some_file.txt";
119 	const char RELPATH[] = "some_file.txt";
120 	const uint64_t ino = 42;
121 	struct stat sb;
122 
123 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
124 	.Times(2)
125 	.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
126 		SET_OUT_HEADER_LEN(out, entry);
127 		out.body.entry.nodeid = ino;
128 		out.body.entry.attr_valid_nsec = NAP_NS / 2;
129 		out.body.entry.attr.ino = ino;	// Must match nodeid
130 		out.body.entry.attr.mode = S_IFREG | 0644;
131 	})));
132 
133 	/* access(2) will issue a VOP_LOOKUP and fill the attr cache */
134 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
135 	/* Next access(2) will use the cached attributes */
136 	nap();
137 	/* The cache has timed out; VOP_GETATTR should query the daemon*/
138 	ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
139 }
140 
141 TEST_F(Lookup, dot)
142 {
143 	const char FULLPATH[] = "mountpoint/some_dir/.";
144 	const char RELDIRPATH[] = "some_dir";
145 	uint64_t ino = 42;
146 
147 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
148 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
149 		SET_OUT_HEADER_LEN(out, entry);
150 		out.body.entry.attr.mode = S_IFDIR | 0755;
151 		out.body.entry.nodeid = ino;
152 		out.body.entry.attr_valid = UINT64_MAX;
153 		out.body.entry.entry_valid = UINT64_MAX;
154 	})));
155 
156 	/*
157 	 * access(2) is one of the few syscalls that will not (always) follow
158 	 * up a successful VOP_LOOKUP with another VOP.
159 	 */
160 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
161 }
162 
163 TEST_F(Lookup, dotdot)
164 {
165 	const char FULLPATH[] = "mountpoint/some_dir/..";
166 	const char RELDIRPATH[] = "some_dir";
167 
168 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
169 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
170 		SET_OUT_HEADER_LEN(out, entry);
171 		out.body.entry.attr.mode = S_IFDIR | 0755;
172 		out.body.entry.nodeid = 14;
173 		out.body.entry.attr_valid = UINT64_MAX;
174 		out.body.entry.entry_valid = UINT64_MAX;
175 	})));
176 
177 	/*
178 	 * access(2) is one of the few syscalls that will not (always) follow
179 	 * up a successful VOP_LOOKUP with another VOP.
180 	 */
181 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
182 }
183 
184 TEST_F(Lookup, enoent)
185 {
186 	const char FULLPATH[] = "mountpoint/does_not_exist";
187 	const char RELPATH[] = "does_not_exist";
188 
189 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
190 	.WillOnce(Invoke(ReturnErrno(ENOENT)));
191 	EXPECT_NE(0, access(FULLPATH, F_OK));
192 	EXPECT_EQ(ENOENT, errno);
193 }
194 
195 TEST_F(Lookup, enotdir)
196 {
197 	const char FULLPATH[] = "mountpoint/not_a_dir/some_file.txt";
198 	const char RELPATH[] = "not_a_dir";
199 
200 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
201 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
202 		SET_OUT_HEADER_LEN(out, entry);
203 		out.body.entry.entry_valid = UINT64_MAX;
204 		out.body.entry.attr.mode = S_IFREG | 0644;
205 		out.body.entry.nodeid = 42;
206 	})));
207 
208 	ASSERT_EQ(-1, access(FULLPATH, F_OK));
209 	ASSERT_EQ(ENOTDIR, errno);
210 }
211 
212 /*
213  * If lookup returns a non-zero entry timeout, then subsequent VOP_LOOKUPs
214  * should use the cached inode rather than requery the daemon
215  */
216 TEST_F(Lookup, entry_cache)
217 {
218 	const char FULLPATH[] = "mountpoint/some_file.txt";
219 	const char RELPATH[] = "some_file.txt";
220 
221 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
222 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
223 		SET_OUT_HEADER_LEN(out, entry);
224 		out.body.entry.entry_valid = UINT64_MAX;
225 		out.body.entry.attr.mode = S_IFREG | 0644;
226 		out.body.entry.nodeid = 14;
227 	})));
228 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
229 	/* The second access(2) should use the cache */
230 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
231 }
232 
233 /*
234  * If the daemon returns an error of 0 and an inode of 0, that's a flag for
235  * "ENOENT and cache it" with the given entry_timeout
236  */
237 TEST_F(Lookup, entry_cache_negative)
238 {
239 	struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0};
240 
241 	EXPECT_LOOKUP(FUSE_ROOT_ID, "does_not_exist")
242 	.Times(1)
243 	.WillOnce(Invoke(ReturnNegativeCache(&entry_valid)));
244 
245 	EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK));
246 	EXPECT_EQ(ENOENT, errno);
247 	EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK));
248 	EXPECT_EQ(ENOENT, errno);
249 }
250 
251 /* Negative entry caches should timeout, too */
252 TEST_F(Lookup, entry_cache_negative_timeout)
253 {
254 	const char *RELPATH = "does_not_exist";
255 	const char *FULLPATH = "mountpoint/does_not_exist";
256 	struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = NAP_NS / 2};
257 
258 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
259 	.Times(2)
260 	.WillRepeatedly(Invoke(ReturnNegativeCache(&entry_valid)));
261 
262 	EXPECT_NE(0, access(FULLPATH, F_OK));
263 	EXPECT_EQ(ENOENT, errno);
264 
265 	nap();
266 
267 	/* The cache has timed out; VOP_LOOKUP should requery the daemon*/
268 	EXPECT_NE(0, access(FULLPATH, F_OK));
269 	EXPECT_EQ(ENOENT, errno);
270 }
271 
272 /*
273  * If lookup returns a finite but non-zero entry cache timeout, then we should
274  * discard the cached inode and requery the daemon
275  */
276 TEST_F(Lookup, entry_cache_timeout)
277 {
278 	const char FULLPATH[] = "mountpoint/some_file.txt";
279 	const char RELPATH[] = "some_file.txt";
280 
281 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
282 	.Times(2)
283 	.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
284 		SET_OUT_HEADER_LEN(out, entry);
285 		out.body.entry.entry_valid_nsec = NAP_NS / 2;
286 		out.body.entry.attr.mode = S_IFREG | 0644;
287 		out.body.entry.nodeid = 14;
288 	})));
289 
290 	/* access(2) will issue a VOP_LOOKUP and fill the entry cache */
291 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
292 	/* Next access(2) will use the cached entry */
293 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
294 	nap();
295 	/* The cache has timed out; VOP_LOOKUP should requery the daemon*/
296 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
297 }
298 
299 TEST_F(Lookup, ok)
300 {
301 	const char FULLPATH[] = "mountpoint/some_file.txt";
302 	const char RELPATH[] = "some_file.txt";
303 
304 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
305 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
306 		SET_OUT_HEADER_LEN(out, entry);
307 		out.body.entry.attr.mode = S_IFREG | 0644;
308 		out.body.entry.nodeid = 14;
309 	})));
310 	/*
311 	 * access(2) is one of the few syscalls that will not (always) follow
312 	 * up a successful VOP_LOOKUP with another VOP.
313 	 */
314 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
315 }
316 
317 // Lookup in a subdirectory of the fuse mount
318 TEST_F(Lookup, subdir)
319 {
320 	const char FULLPATH[] = "mountpoint/some_dir/some_file.txt";
321 	const char DIRPATH[] = "some_dir";
322 	const char RELPATH[] = "some_file.txt";
323 	uint64_t dir_ino = 2;
324 	uint64_t file_ino = 3;
325 
326 	EXPECT_LOOKUP(FUSE_ROOT_ID, DIRPATH)
327 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
328 		SET_OUT_HEADER_LEN(out, entry);
329 		out.body.entry.attr.mode = S_IFDIR | 0755;
330 		out.body.entry.nodeid = dir_ino;
331 	})));
332 	EXPECT_LOOKUP(dir_ino, RELPATH)
333 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
334 		SET_OUT_HEADER_LEN(out, entry);
335 		out.body.entry.attr.mode = S_IFREG | 0644;
336 		out.body.entry.nodeid = file_ino;
337 	})));
338 	/*
339 	 * access(2) is one of the few syscalls that will not (always) follow
340 	 * up a successful VOP_LOOKUP with another VOP.
341 	 */
342 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
343 }
344 
345 /*
346  * The server returns two different vtypes for the same nodeid.  This is a bad
347  * server!  But we shouldn't crash.
348  */
349 TEST_F(Lookup, vtype_conflict)
350 {
351 	const char FIRSTFULLPATH[] = "mountpoint/foo";
352 	const char SECONDFULLPATH[] = "mountpoint/bar";
353 	const char FIRSTRELPATH[] = "foo";
354 	const char SECONDRELPATH[] = "bar";
355 	uint64_t ino = 42;
356 
357 	expect_lookup(FIRSTRELPATH, ino, S_IFREG | 0644, 0, 1, UINT64_MAX);
358 	expect_lookup(SECONDRELPATH, ino, S_IFDIR | 0755, 0, 1, UINT64_MAX);
359 
360 	ASSERT_EQ(0, access(FIRSTFULLPATH, F_OK)) << strerror(errno);
361 	ASSERT_EQ(-1, access(SECONDFULLPATH, F_OK));
362 	ASSERT_EQ(EAGAIN, errno);
363 }
364 
365 TEST_F(Lookup_7_8, ok)
366 {
367 	const char FULLPATH[] = "mountpoint/some_file.txt";
368 	const char RELPATH[] = "some_file.txt";
369 
370 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
371 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
372 		SET_OUT_HEADER_LEN(out, entry_7_8);
373 		out.body.entry.attr.mode = S_IFREG | 0644;
374 		out.body.entry.nodeid = 14;
375 	})));
376 	/*
377 	 * access(2) is one of the few syscalls that will not (always) follow
378 	 * up a successful VOP_LOOKUP with another VOP.
379 	 */
380 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
381 }
382 
383 
384