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
31extern "C" {
32#include <sys/param.h>
33
34#include <semaphore.h>
35}
36
37#include "mockfs.hh"
38#include "utils.hh"
39
40using namespace testing;
41
42class Getattr : public FuseTest {
43public:
44void expect_lookup(const char *relpath, uint64_t ino, mode_t mode,
45	uint64_t size, int times, uint64_t attr_valid, uint32_t attr_valid_nsec)
46{
47	EXPECT_LOOKUP(FUSE_ROOT_ID, relpath)
48	.Times(times)
49	.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
50		SET_OUT_HEADER_LEN(out, entry);
51		out.body.entry.attr.mode = mode;
52		out.body.entry.nodeid = ino;
53		out.body.entry.attr.nlink = 1;
54		out.body.entry.attr_valid = attr_valid;
55		out.body.entry.attr_valid_nsec = attr_valid_nsec;
56		out.body.entry.attr.size = size;
57		out.body.entry.entry_valid = UINT64_MAX;
58	})));
59}
60};
61
62class Getattr_7_8: public FuseTest {
63public:
64virtual void SetUp() {
65	m_kernel_minor_version = 8;
66	FuseTest::SetUp();
67}
68};
69
70/*
71 * If getattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs
72 * should use the cached attributes, rather than query the daemon
73 */
74TEST_F(Getattr, attr_cache)
75{
76	const char FULLPATH[] = "mountpoint/some_file.txt";
77	const char RELPATH[] = "some_file.txt";
78	const uint64_t ino = 42;
79	struct stat sb;
80
81	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
82	.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
83		SET_OUT_HEADER_LEN(out, entry);
84		out.body.entry.attr.mode = S_IFREG | 0644;
85		out.body.entry.nodeid = ino;
86		out.body.entry.entry_valid = UINT64_MAX;
87	})));
88	EXPECT_CALL(*m_mock, process(
89		ResultOf([](auto in) {
90			return (in.header.opcode == FUSE_GETATTR &&
91				in.header.nodeid == ino);
92		}, Eq(true)),
93		_)
94	).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
95		SET_OUT_HEADER_LEN(out, attr);
96		out.body.attr.attr_valid = UINT64_MAX;
97		out.body.attr.attr.ino = ino;	// Must match nodeid
98		out.body.attr.attr.mode = S_IFREG | 0644;
99	})));
100	EXPECT_EQ(0, stat(FULLPATH, &sb));
101	/* The second stat(2) should use cached attributes */
102	EXPECT_EQ(0, stat(FULLPATH, &sb));
103}
104
105/*
106 * If getattr returns a finite but non-zero cache timeout, then we should
107 * discard the cached attributes and requery the daemon after the timeout
108 * period passes.
109 */
110TEST_F(Getattr, attr_cache_timeout)
111{
112	const char FULLPATH[] = "mountpoint/some_file.txt";
113	const char RELPATH[] = "some_file.txt";
114	const uint64_t ino = 42;
115	struct stat sb;
116
117	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1, 0, 0);
118	EXPECT_CALL(*m_mock, process(
119		ResultOf([](auto in) {
120			return (in.header.opcode == FUSE_GETATTR &&
121				in.header.nodeid == ino);
122		}, Eq(true)),
123		_)
124	).Times(2)
125	.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
126		SET_OUT_HEADER_LEN(out, attr);
127		out.body.attr.attr_valid_nsec = NAP_NS / 2;
128		out.body.attr.attr_valid = 0;
129		out.body.attr.attr.ino = ino;	// Must match nodeid
130		out.body.attr.attr.mode = S_IFREG | 0644;
131	})));
132
133	EXPECT_EQ(0, stat(FULLPATH, &sb));
134	nap();
135	/* Timeout has expired. stat(2) should requery the daemon */
136	EXPECT_EQ(0, stat(FULLPATH, &sb));
137}
138
139/*
140 * If attr.blksize is zero, then the kernel should use a default value for
141 * st_blksize
142 */
143TEST_F(Getattr, blksize_zero)
144{
145	const char FULLPATH[] = "mountpoint/some_file.txt";
146	const char RELPATH[] = "some_file.txt";
147	const uint64_t ino = 42;
148	struct stat sb;
149
150	expect_lookup(RELPATH, ino, S_IFREG | 0644, 1, 1, 0, 0);
151	EXPECT_CALL(*m_mock, process(
152		ResultOf([](auto in) {
153			return (in.header.opcode == FUSE_GETATTR &&
154				in.header.nodeid == ino);
155		}, Eq(true)),
156		_)
157	).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
158		SET_OUT_HEADER_LEN(out, attr);
159		out.body.attr.attr.mode = S_IFREG | 0644;
160		out.body.attr.attr.ino = ino;	// Must match nodeid
161		out.body.attr.attr.blksize = 0;
162		out.body.attr.attr.size = 1;
163	})));
164
165	ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
166	EXPECT_EQ((blksize_t)PAGE_SIZE, sb.st_blksize);
167}
168
169TEST_F(Getattr, enoent)
170{
171	const char FULLPATH[] = "mountpoint/some_file.txt";
172	const char RELPATH[] = "some_file.txt";
173	struct stat sb;
174	const uint64_t ino = 42;
175	sem_t sem;
176
177	ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
178
179	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1, 0, 0);
180	EXPECT_CALL(*m_mock, process(
181		ResultOf([](auto in) {
182			return (in.header.opcode == FUSE_GETATTR &&
183				in.header.nodeid == ino);
184		}, Eq(true)),
185		_)
186	).WillOnce(Invoke(ReturnErrno(ENOENT)));
187	// Since FUSE_GETATTR returns ENOENT, the kernel will reclaim the vnode
188	// and send a FUSE_FORGET
189	expect_forget(ino, 1, &sem);
190
191	EXPECT_NE(0, stat(FULLPATH, &sb));
192	EXPECT_EQ(ENOENT, errno);
193
194	sem_wait(&sem);
195	sem_destroy(&sem);
196}
197
198TEST_F(Getattr, ok)
199{
200	const char FULLPATH[] = "mountpoint/some_file.txt";
201	const char RELPATH[] = "some_file.txt";
202	const uint64_t ino = 42;
203	struct stat sb;
204
205	expect_lookup(RELPATH, ino, S_IFREG | 0644, 1, 1, 0, 0);
206	EXPECT_CALL(*m_mock, process(
207		ResultOf([](auto in) {
208			return (in.header.opcode == FUSE_GETATTR &&
209				in.body.getattr.getattr_flags == 0 &&
210				in.header.nodeid == ino);
211		}, Eq(true)),
212		_)
213	).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
214		SET_OUT_HEADER_LEN(out, attr);
215		out.body.attr.attr.ino = ino;	// Must match nodeid
216		out.body.attr.attr.mode = S_IFREG | 0644;
217		out.body.attr.attr.size = 1;
218		out.body.attr.attr.blocks = 2;
219		out.body.attr.attr.atime = 3;
220		out.body.attr.attr.mtime = 4;
221		out.body.attr.attr.ctime = 5;
222		out.body.attr.attr.atimensec = 6;
223		out.body.attr.attr.mtimensec = 7;
224		out.body.attr.attr.ctimensec = 8;
225		out.body.attr.attr.nlink = 9;
226		out.body.attr.attr.uid = 10;
227		out.body.attr.attr.gid = 11;
228		out.body.attr.attr.rdev = 12;
229		out.body.attr.attr.blksize = 12345;
230	})));
231
232	ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
233	EXPECT_EQ(1, sb.st_size);
234	EXPECT_EQ(2, sb.st_blocks);
235	EXPECT_EQ(3, sb.st_atim.tv_sec);
236	EXPECT_EQ(6, sb.st_atim.tv_nsec);
237	EXPECT_EQ(4, sb.st_mtim.tv_sec);
238	EXPECT_EQ(7, sb.st_mtim.tv_nsec);
239	EXPECT_EQ(5, sb.st_ctim.tv_sec);
240	EXPECT_EQ(8, sb.st_ctim.tv_nsec);
241	EXPECT_EQ(9ull, sb.st_nlink);
242	EXPECT_EQ(10ul, sb.st_uid);
243	EXPECT_EQ(11ul, sb.st_gid);
244	EXPECT_EQ(12ul, sb.st_rdev);
245	EXPECT_EQ((blksize_t)12345, sb.st_blksize);
246	EXPECT_EQ(ino, sb.st_ino);
247	EXPECT_EQ(S_IFREG | 0644, sb.st_mode);
248
249	/*
250	 * st_birthtim and st_flags are not supported by the fuse protocol.
251	 * They're only supported as OS-specific extensions to OSX.  For
252	 * birthtime, the convention for "not supported" is "negative one
253	 * second".
254	 */
255	EXPECT_EQ(-1, sb.st_birthtim.tv_sec);
256	EXPECT_EQ(0, sb.st_birthtim.tv_nsec);
257	EXPECT_EQ(0u, sb.st_flags);
258}
259
260/*
261 * FUSE_GETATTR returns a different file type, even though the entry cache
262 * hasn't expired.  This is a server bug!  It probably means that the server
263 * removed the file and recreated it with the same inode but a different vtyp.
264 * The best thing fusefs can do is return ENOENT to the caller.  After all, the
265 * entry must not have existed recently.
266 */
267TEST_F(Getattr, vtyp_conflict)
268{
269	const char FULLPATH[] = "mountpoint/some_file.txt";
270	const char RELPATH[] = "some_file.txt";
271	const uint64_t ino = 42;
272	struct stat sb;
273	sem_t sem;
274
275	ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
276
277	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
278	.WillOnce(Invoke(
279		ReturnImmediate([=](auto in __unused, auto& out) {
280		SET_OUT_HEADER_LEN(out, entry);
281		out.body.entry.attr.mode = S_IFREG | 0644;
282		out.body.entry.nodeid = ino;
283		out.body.entry.attr.nlink = 1;
284		out.body.entry.attr_valid = 0;
285		out.body.entry.entry_valid = UINT64_MAX;
286	})));
287	EXPECT_CALL(*m_mock, process(
288		ResultOf([](auto in) {
289			return (in.header.opcode == FUSE_GETATTR &&
290				in.body.getattr.getattr_flags == 0 &&
291				in.header.nodeid == ino);
292		}, Eq(true)),
293		_)
294	).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
295		SET_OUT_HEADER_LEN(out, attr);
296		out.body.attr.attr.ino = ino;	// Must match nodeid
297		out.body.attr.attr.mode = S_IFDIR | 0755;	// Changed!
298		out.body.attr.attr.nlink = 2;
299	})));
300	// We should reclaim stale vnodes
301	expect_forget(ino, 1, &sem);
302
303	ASSERT_NE(0, stat(FULLPATH, &sb));
304	EXPECT_EQ(errno, ENOENT);
305
306	sem_wait(&sem);
307	sem_destroy(&sem);
308}
309
310TEST_F(Getattr_7_8, ok)
311{
312	const char FULLPATH[] = "mountpoint/some_file.txt";
313	const char RELPATH[] = "some_file.txt";
314	const uint64_t ino = 42;
315	struct stat sb;
316
317	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
318	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
319		SET_OUT_HEADER_LEN(out, entry_7_8);
320		out.body.entry.attr.mode = S_IFREG | 0644;
321		out.body.entry.nodeid = ino;
322		out.body.entry.attr.nlink = 1;
323		out.body.entry.attr.size = 1;
324	})));
325	EXPECT_CALL(*m_mock, process(
326		ResultOf([](auto in) {
327			return (in.header.opcode == FUSE_GETATTR &&
328				in.header.nodeid == ino);
329		}, Eq(true)),
330		_)
331	).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
332		SET_OUT_HEADER_LEN(out, attr_7_8);
333		out.body.attr.attr.ino = ino;	// Must match nodeid
334		out.body.attr.attr.mode = S_IFREG | 0644;
335		out.body.attr.attr.size = 1;
336		out.body.attr.attr.blocks = 2;
337		out.body.attr.attr.atime = 3;
338		out.body.attr.attr.mtime = 4;
339		out.body.attr.attr.ctime = 5;
340		out.body.attr.attr.atimensec = 6;
341		out.body.attr.attr.mtimensec = 7;
342		out.body.attr.attr.ctimensec = 8;
343		out.body.attr.attr.nlink = 9;
344		out.body.attr.attr.uid = 10;
345		out.body.attr.attr.gid = 11;
346		out.body.attr.attr.rdev = 12;
347	})));
348
349	ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
350	EXPECT_EQ(1, sb.st_size);
351	EXPECT_EQ(2, sb.st_blocks);
352	EXPECT_EQ(3, sb.st_atim.tv_sec);
353	EXPECT_EQ(6, sb.st_atim.tv_nsec);
354	EXPECT_EQ(4, sb.st_mtim.tv_sec);
355	EXPECT_EQ(7, sb.st_mtim.tv_nsec);
356	EXPECT_EQ(5, sb.st_ctim.tv_sec);
357	EXPECT_EQ(8, sb.st_ctim.tv_nsec);
358	EXPECT_EQ(9ull, sb.st_nlink);
359	EXPECT_EQ(10ul, sb.st_uid);
360	EXPECT_EQ(11ul, sb.st_gid);
361	EXPECT_EQ(12ul, sb.st_rdev);
362	EXPECT_EQ(ino, sb.st_ino);
363	EXPECT_EQ(S_IFREG | 0644, sb.st_mode);
364
365	//st_birthtim and st_flags are not supported by protocol 7.8.  They're
366	//only supported as OS-specific extensions to OSX.
367}
368