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