xref: /freebsd/tests/sys/fs/fusefs/create.cc (revision 2d445be156fcfc69e8c80c54150be21b6a8515e8)
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 <fcntl.h>
33 }
34 
35 #include "mockfs.hh"
36 #include "utils.hh"
37 
38 using namespace testing;
39 
40 class Create: public FuseTest {
41 public:
42 
43 void expect_create(const char *relpath, ProcessMockerT r)
44 {
45 	EXPECT_CALL(*m_mock, process(
46 		ResultOf([=](auto in) {
47 			const char *name = (const char*)in->body.bytes +
48 				sizeof(fuse_open_in);
49 			return (in->header.opcode == FUSE_CREATE &&
50 				(0 == strcmp(relpath, name)));
51 		}, Eq(true)),
52 		_)
53 	).WillOnce(Invoke(r));
54 }
55 
56 };
57 
58 /*
59  * If FUSE_CREATE sets the attr_valid, then subsequent GETATTRs should use the
60  * attribute cache
61  */
62 /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=235775 */
63 TEST_F(Create, DISABLED_attr_cache)
64 {
65 	const char FULLPATH[] = "mountpoint/some_file.txt";
66 	const char RELPATH[] = "some_file.txt";
67 	mode_t mode = 0755;
68 	uint64_t ino = 42;
69 	int fd;
70 
71 	EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT)));
72 	expect_create(RELPATH, ReturnImmediate([=](auto in __unused, auto out) {
73 		SET_OUT_HEADER_LEN(out, create);
74 		out->body.create.entry.attr.mode = S_IFREG | mode;
75 		out->body.create.entry.nodeid = ino;
76 		out->body.create.entry.entry_valid = UINT64_MAX;
77 		out->body.create.entry.attr_valid = UINT64_MAX;
78 	}));
79 
80 	EXPECT_CALL(*m_mock, process(
81 		ResultOf([=](auto in) {
82 			return (in->header.opcode == FUSE_GETATTR &&
83 				in->header.nodeid == ino);
84 		}, Eq(true)),
85 		_)
86 	).Times(0);
87 
88 	fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
89 	EXPECT_LE(0, fd) << strerror(errno);
90 	/* Deliberately leak fd.  close(2) will be tested in release.cc */
91 }
92 
93 /*
94  * The fuse daemon fails the request with EEXIST.  This usually indicates a
95  * race condition: some other FUSE client created the file in between when the
96  * kernel checked for it with lookup and tried to create it with create
97  */
98 TEST_F(Create, eexist)
99 {
100 	const char FULLPATH[] = "mountpoint/some_file.txt";
101 	const char RELPATH[] = "some_file.txt";
102 	mode_t mode = 0755;
103 
104 	EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT)));
105 	expect_create(RELPATH, ReturnErrno(EEXIST));
106 	EXPECT_NE(0, open(FULLPATH, O_CREAT | O_EXCL, mode));
107 	EXPECT_EQ(EEXIST, errno);
108 }
109 
110 /*
111  * If the daemon doesn't implement FUSE_CREATE, then the kernel should fallback
112  * to FUSE_MKNOD/FUSE_OPEN
113  */
114 TEST_F(Create, Enosys)
115 {
116 	const char FULLPATH[] = "mountpoint/some_file.txt";
117 	const char RELPATH[] = "some_file.txt";
118 	mode_t mode = 0755;
119 	uint64_t ino = 42;
120 	int fd;
121 
122 	EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT)));
123 	expect_create(RELPATH, ReturnErrno(ENOSYS));
124 
125 	EXPECT_CALL(*m_mock, process(
126 		ResultOf([=](auto in) {
127 			const char *name = (const char*)in->body.bytes +
128 				sizeof(fuse_mknod_in);
129 			return (in->header.opcode == FUSE_MKNOD &&
130 				in->body.mknod.mode == (S_IFREG | mode) &&
131 				in->body.mknod.rdev == 0 &&
132 				(0 == strcmp(RELPATH, name)));
133 		}, Eq(true)),
134 		_)
135 	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) {
136 		SET_OUT_HEADER_LEN(out, entry);
137 		out->body.entry.attr.mode = S_IFREG | mode;
138 		out->body.entry.nodeid = ino;
139 		out->body.entry.entry_valid = UINT64_MAX;
140 		out->body.entry.attr_valid = UINT64_MAX;
141 	})));
142 
143 	EXPECT_CALL(*m_mock, process(
144 		ResultOf([=](auto in) {
145 			return (in->header.opcode == FUSE_OPEN &&
146 				in->header.nodeid == ino);
147 		}, Eq(true)),
148 		_)
149 	).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto out) {
150 		out->header.len = sizeof(out->header);
151 		SET_OUT_HEADER_LEN(out, open);
152 	})));
153 
154 	/* Until the attr cache is working, we may send an additional GETATTR */
155 	EXPECT_CALL(*m_mock, process(
156 		ResultOf([=](auto in) {
157 			return (in->header.opcode == FUSE_GETATTR &&
158 				in->header.nodeid == ino);
159 		}, Eq(true)),
160 		_)
161 	).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto out) {
162 		SET_OUT_HEADER_LEN(out, attr);
163 		out->body.attr.attr.ino = ino;	// Must match nodeid
164 		out->body.attr.attr.mode = S_IFREG | 0644;
165 	})));
166 
167 	fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
168 	EXPECT_LE(0, fd) << strerror(errno);
169 	/* Deliberately leak fd.  close(2) will be tested in release.cc */
170 }
171 
172 /*
173  * Creating a new file after FUSE_LOOKUP returned a negative cache entry
174  */
175 TEST_F(Create, entry_cache_negative)
176 {
177 	const char FULLPATH[] = "mountpoint/some_file.txt";
178 	const char RELPATH[] = "some_file.txt";
179 	mode_t mode = 0755;
180 	uint64_t ino = 42;
181 	int fd;
182 	/*
183 	 * Set entry_valid = 0 because this test isn't concerned with whether
184 	 * or not we actually cache negative entries, only with whether we
185 	 * interpret negative cache responses correctly.
186 	 */
187 	struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0};
188 
189 	/* create will first do a LOOKUP, adding a negative cache entry */
190 	EXPECT_LOOKUP(1, RELPATH).WillOnce(ReturnNegativeCache(&entry_valid));
191 	expect_create(RELPATH, ReturnImmediate([=](auto in __unused, auto out) {
192 		SET_OUT_HEADER_LEN(out, create);
193 		out->body.create.entry.attr.mode = S_IFREG | mode;
194 		out->body.create.entry.nodeid = ino;
195 		out->body.create.entry.entry_valid = UINT64_MAX;
196 		out->body.create.entry.attr_valid = UINT64_MAX;
197 	}));
198 
199 	/* Until the attr cache is working, we may send an additional GETATTR */
200 	EXPECT_CALL(*m_mock, process(
201 		ResultOf([=](auto in) {
202 			return (in->header.opcode == FUSE_GETATTR &&
203 				in->header.nodeid == ino);
204 		}, Eq(true)),
205 		_)
206 	).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto out) {
207 		SET_OUT_HEADER_LEN(out, attr);
208 		out->body.attr.attr.ino = ino;	// Must match nodeid
209 		out->body.attr.attr.mode = S_IFREG | 0644;
210 	})));
211 
212 	fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
213 	ASSERT_LE(0, fd) << strerror(errno);
214 	/* Deliberately leak fd.  close(2) will be tested in release.cc */
215 }
216 
217 /*
218  * Creating a new file should purge any negative namecache entries
219  */
220 TEST_F(Create, entry_cache_negative_purge)
221 {
222 	const char FULLPATH[] = "mountpoint/some_file.txt";
223 	const char RELPATH[] = "some_file.txt";
224 	mode_t mode = 0755;
225 	uint64_t ino = 42;
226 	int fd;
227 	struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0};
228 
229 	/* create will first do a LOOKUP, adding a negative cache entry */
230 	EXPECT_LOOKUP(1, RELPATH).Times(1)
231 	.WillOnce(Invoke(ReturnNegativeCache(&entry_valid)))
232 	.RetiresOnSaturation();
233 
234 	/* Then the CREATE should purge the negative cache entry */
235 	expect_create(RELPATH, ReturnImmediate([=](auto in __unused, auto out) {
236 		SET_OUT_HEADER_LEN(out, create);
237 		out->body.create.entry.attr.mode = S_IFREG | mode;
238 		out->body.create.entry.nodeid = ino;
239 		out->body.create.entry.attr_valid = UINT64_MAX;
240 	}));
241 
242 	/* Until the attr cache is working, we may send an additional GETATTR */
243 	EXPECT_CALL(*m_mock, process(
244 		ResultOf([=](auto in) {
245 			return (in->header.opcode == FUSE_GETATTR &&
246 				in->header.nodeid == ino);
247 		}, Eq(true)),
248 		_)
249 	).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto out) {
250 		SET_OUT_HEADER_LEN(out, attr);
251 		out->body.attr.attr.ino = ino;	// Must match nodeid
252 		out->body.attr.attr.mode = S_IFREG | 0644;
253 	})));
254 
255 	fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
256 	ASSERT_LE(0, fd) << strerror(errno);
257 
258 	/* Finally, a subsequent lookup should query the daemon */
259 	expect_lookup(RELPATH, ino, S_IFREG | mode, 0, 1);
260 
261 	ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
262 	/* Deliberately leak fd.  close(2) will be tested in release.cc */
263 }
264 
265 /*
266  * The daemon is responsible for checking file permissions (unless the
267  * default_permissions mount option was used)
268  */
269 TEST_F(Create, eperm)
270 {
271 	const char FULLPATH[] = "mountpoint/some_file.txt";
272 	const char RELPATH[] = "some_file.txt";
273 	mode_t mode = 0755;
274 
275 	EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT)));
276 	expect_create(RELPATH, ReturnErrno(EPERM));
277 
278 	EXPECT_NE(0, open(FULLPATH, O_CREAT | O_EXCL, mode));
279 	EXPECT_EQ(EPERM, errno);
280 }
281 
282 TEST_F(Create, ok)
283 {
284 	const char FULLPATH[] = "mountpoint/some_file.txt";
285 	const char RELPATH[] = "some_file.txt";
286 	mode_t mode = 0755;
287 	uint64_t ino = 42;
288 	int fd;
289 
290 	EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT)));
291 	expect_create(RELPATH, ReturnImmediate([=](auto in __unused, auto out) {
292 		SET_OUT_HEADER_LEN(out, create);
293 		out->body.create.entry.attr.mode = S_IFREG | mode;
294 		out->body.create.entry.nodeid = ino;
295 		out->body.create.entry.entry_valid = UINT64_MAX;
296 		out->body.create.entry.attr_valid = UINT64_MAX;
297 	}));
298 
299 	/* Until the attr cache is working, we may send an additional GETATTR */
300 	EXPECT_CALL(*m_mock, process(
301 		ResultOf([=](auto in) {
302 			return (in->header.opcode == FUSE_GETATTR &&
303 				in->header.nodeid == ino);
304 		}, Eq(true)),
305 		_)
306 	).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto out) {
307 		SET_OUT_HEADER_LEN(out, attr);
308 		out->body.attr.attr.ino = ino;	// Must match nodeid
309 		out->body.attr.attr.mode = S_IFREG | mode;
310 	})));
311 
312 	fd = open(FULLPATH, O_CREAT | O_EXCL, mode);
313 	EXPECT_LE(0, fd) << strerror(errno);
314 	/* Deliberately leak fd.  close(2) will be tested in release.cc */
315 }
316 
317 /*
318  * A regression test for a bug that affected old FUSE implementations:
319  * open(..., O_WRONLY | O_CREAT, 0444) should work despite the seeming
320  * contradiction between O_WRONLY and 0444
321  *
322  * For example:
323  * https://bugs.launchpad.net/ubuntu/+source/sshfs-fuse/+bug/44886
324  */
325 TEST_F(Create, wronly_0444)
326 {
327 	const char FULLPATH[] = "mountpoint/some_file.txt";
328 	const char RELPATH[] = "some_file.txt";
329 	mode_t mode = 0444;
330 	uint64_t ino = 42;
331 	int fd;
332 
333 	EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT)));
334 	expect_create(RELPATH, ReturnImmediate([=](auto in __unused, auto out) {
335 		SET_OUT_HEADER_LEN(out, create);
336 		out->body.create.entry.attr.mode = S_IFREG | mode;
337 		out->body.create.entry.nodeid = ino;
338 		out->body.create.entry.entry_valid = UINT64_MAX;
339 		out->body.create.entry.attr_valid = UINT64_MAX;
340 	}));
341 
342 	/* Until the attr cache is working, we may send an additional GETATTR */
343 	EXPECT_CALL(*m_mock, process(
344 		ResultOf([=](auto in) {
345 			return (in->header.opcode == FUSE_GETATTR &&
346 				in->header.nodeid == ino);
347 		}, Eq(true)),
348 		_)
349 	).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto out) {
350 		SET_OUT_HEADER_LEN(out, attr);
351 		out->body.attr.attr.ino = ino;	// Must match nodeid
352 		out->body.attr.attr.mode = S_IFREG | mode;
353 	})));
354 
355 	fd = open(FULLPATH, O_CREAT | O_WRONLY, mode);
356 	EXPECT_LE(0, fd) << strerror(errno);
357 	/* Deliberately leak fd.  close(2) will be tested in release.cc */
358 }
359