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
33extern "C" {
34#include <unistd.h>
35}
36
37#include "mockfs.hh"
38#include "utils.hh"
39
40using namespace testing;
41
42class Lookup: public FuseTest {};
43class Lookup_7_8: public Lookup {
44public:
45virtual 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 */
55TEST_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 */
116TEST_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
141TEST_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
163TEST_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
184TEST_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
195TEST_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 */
216TEST_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 */
237TEST_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 */
252TEST_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 */
276TEST_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
299TEST_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
318TEST_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 */
349TEST_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
365TEST_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