xref: /freebsd/tests/sys/fs/fusefs/open.cc (revision 088cc7d221bb0743fc5ec12de983559b812366bd)
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 
31 extern "C" {
32 #include <sys/wait.h>
33 
34 #include <fcntl.h>
35 #include <semaphore.h>
36 }
37 
38 #include "mockfs.hh"
39 #include "utils.hh"
40 
41 using namespace testing;
42 
43 class Open: public FuseTest {
44 
45 public:
46 
47 /* Test an OK open of a file with the given flags */
test_ok(int os_flags,int fuse_flags)48 void test_ok(int os_flags, int fuse_flags) {
49 	const char FULLPATH[] = "mountpoint/some_file.txt";
50 	const char RELPATH[] = "some_file.txt";
51 	uint64_t ino = 42;
52 	int fd;
53 
54 	FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
55 	EXPECT_CALL(*m_mock, process(
56 		ResultOf([=](auto in) {
57 			return (in.header.opcode == FUSE_OPEN &&
58 				in.body.open.flags == (uint32_t)fuse_flags &&
59 				in.header.nodeid == ino);
60 		}, Eq(true)),
61 		_)
62 	).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
63 		out.header.len = sizeof(out.header);
64 		SET_OUT_HEADER_LEN(out, open);
65 	})));
66 
67 	fd = open(FULLPATH, os_flags);
68 	ASSERT_LE(0, fd) << strerror(errno);
69 	leak(fd);
70 }
71 };
72 
73 
74 class OpenNoOpenSupport: public FuseTest {
SetUp()75 	virtual void SetUp() {
76 		m_init_flags = FUSE_NO_OPEN_SUPPORT;
77 		FuseTest::SetUp();
78 	}
79 };
80 
81 /*
82  * fusefs(4) does not support I/O on device nodes (neither does UFS).  But it
83  * shouldn't crash
84  */
TEST_F(Open,chr)85 TEST_F(Open, chr)
86 {
87 	const char FULLPATH[] = "mountpoint/zero";
88 	const char RELPATH[] = "zero";
89 	uint64_t ino = 42;
90 
91 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
92 	.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
93 		SET_OUT_HEADER_LEN(out, entry);
94 		out.body.entry.attr.mode = S_IFCHR | 0644;
95 		out.body.entry.nodeid = ino;
96 		out.body.entry.attr.nlink = 1;
97 		out.body.entry.attr_valid = UINT64_MAX;
98 		out.body.entry.attr.rdev = 44;	/* /dev/zero's rdev */
99 	})));
100 
101 	ASSERT_EQ(-1, open(FULLPATH, O_RDONLY));
102 	EXPECT_EQ(EOPNOTSUPP, errno);
103 }
104 
105 /*
106  * The fuse daemon fails the request with enoent.  This usually indicates a
107  * race condition: some other FUSE client removed the file in between when the
108  * kernel checked for it with lookup and tried to open it
109  */
TEST_F(Open,enoent)110 TEST_F(Open, enoent)
111 {
112 	const char FULLPATH[] = "mountpoint/some_file.txt";
113 	const char RELPATH[] = "some_file.txt";
114 	uint64_t ino = 42;
115 	sem_t sem;
116 
117 	ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
118 
119 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
120 	EXPECT_CALL(*m_mock, process(
121 		ResultOf([=](auto in) {
122 			return (in.header.opcode == FUSE_OPEN &&
123 				in.header.nodeid == ino);
124 		}, Eq(true)),
125 		_)
126 	).WillOnce(Invoke(ReturnErrno(ENOENT)));
127 	// Since FUSE_OPEN returns ENOENT, the kernel will reclaim the vnode
128 	// and send a FUSE_FORGET
129 	expect_forget(ino, 1, &sem);
130 
131 	ASSERT_EQ(-1, open(FULLPATH, O_RDONLY));
132 	EXPECT_EQ(ENOENT, errno);
133 
134 	sem_wait(&sem);
135 	sem_destroy(&sem);
136 }
137 
138 /*
139  * The daemon is responsible for checking file permissions (unless the
140  * default_permissions mount option was used)
141  */
TEST_F(Open,eperm)142 TEST_F(Open, eperm)
143 {
144 	const char FULLPATH[] = "mountpoint/some_file.txt";
145 	const char RELPATH[] = "some_file.txt";
146 	uint64_t ino = 42;
147 
148 	expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
149 	EXPECT_CALL(*m_mock, process(
150 		ResultOf([=](auto in) {
151 			return (in.header.opcode == FUSE_OPEN &&
152 				in.header.nodeid == ino);
153 		}, Eq(true)),
154 		_)
155 	).WillOnce(Invoke(ReturnErrno(EPERM)));
156 	ASSERT_EQ(-1, open(FULLPATH, O_RDONLY));
157 	EXPECT_EQ(EPERM, errno);
158 }
159 
160 /*
161  * fusefs must issue multiple FUSE_OPEN operations if clients with different
162  * credentials open the same file, even if they use the same mode.  This is
163  * necessary so that the daemon can validate each set of credentials.
164  */
TEST_F(Open,multiple_creds)165 TEST_F(Open, multiple_creds)
166 {
167 	const static char FULLPATH[] = "mountpoint/some_file.txt";
168 	const static char RELPATH[] = "some_file.txt";
169 	int fd1, status;
170 	const static uint64_t ino = 42;
171 	const static uint64_t fh0 = 100, fh1 = 200;
172 
173 	/* Fork a child to open the file with different credentials */
174 	fork(false, &status, [&] {
175 
176 		expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
177 		EXPECT_CALL(*m_mock, process(
178 			ResultOf([=](auto in) {
179 				return (in.header.opcode == FUSE_OPEN &&
180 					in.header.pid == (uint32_t)getpid() &&
181 					in.header.nodeid == ino);
182 			}, Eq(true)),
183 			_)
184 		).WillOnce(Invoke(
185 			ReturnImmediate([](auto in __unused, auto& out) {
186 			out.body.open.fh = fh0;
187 			out.header.len = sizeof(out.header);
188 			SET_OUT_HEADER_LEN(out, open);
189 		})));
190 
191 		EXPECT_CALL(*m_mock, process(
192 			ResultOf([=](auto in) {
193 				return (in.header.opcode == FUSE_OPEN &&
194 					in.header.pid != (uint32_t)getpid() &&
195 					in.header.nodeid == ino);
196 			}, Eq(true)),
197 			_)
198 		).WillOnce(Invoke(
199 			ReturnImmediate([](auto in __unused, auto& out) {
200 			out.body.open.fh = fh1;
201 			out.header.len = sizeof(out.header);
202 			SET_OUT_HEADER_LEN(out, open);
203 		})));
204 		expect_flush(ino, 2, ReturnErrno(0));
205 		expect_release(ino, fh0);
206 		expect_release(ino, fh1);
207 
208 		fd1 = open(FULLPATH, O_RDONLY);
209 		ASSERT_LE(0, fd1) << strerror(errno);
210 	}, [] {
211 		int fd0;
212 
213 		fd0 = open(FULLPATH, O_RDONLY);
214 		if (fd0 < 0) {
215 			perror("open");
216 			return(1);
217 		}
218 		leak(fd0);
219 		return 0;
220 	}
221 	);
222 	ASSERT_EQ(0, WEXITSTATUS(status));
223 
224 	close(fd1);
225 }
226 
227 /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */
TEST_F(Open,DISABLED_o_append)228 TEST_F(Open, DISABLED_o_append)
229 {
230 	test_ok(O_WRONLY | O_APPEND, O_WRONLY | O_APPEND);
231 }
232 
233 /* The kernel is supposed to filter out this flag */
TEST_F(Open,o_creat)234 TEST_F(Open, o_creat)
235 {
236 	test_ok(O_WRONLY | O_CREAT, O_WRONLY);
237 }
238 
239 /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */
TEST_F(Open,DISABLED_o_direct)240 TEST_F(Open, DISABLED_o_direct)
241 {
242 	test_ok(O_WRONLY | O_DIRECT, O_WRONLY | O_DIRECT);
243 }
244 
245 /* The kernel is supposed to filter out this flag */
TEST_F(Open,o_excl)246 TEST_F(Open, o_excl)
247 {
248 	test_ok(O_WRONLY | O_EXCL, O_WRONLY);
249 }
250 
TEST_F(Open,o_exec)251 TEST_F(Open, o_exec)
252 {
253 	test_ok(O_EXEC, O_EXEC);
254 }
255 
256 /* The kernel is supposed to filter out this flag */
TEST_F(Open,o_noctty)257 TEST_F(Open, o_noctty)
258 {
259 	test_ok(O_WRONLY | O_NOCTTY, O_WRONLY);
260 }
261 
TEST_F(Open,o_rdonly)262 TEST_F(Open, o_rdonly)
263 {
264 	test_ok(O_RDONLY, O_RDONLY);
265 }
266 
267 /* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236340 */
TEST_F(Open,DISABLED_o_trunc)268 TEST_F(Open, DISABLED_o_trunc)
269 {
270 	test_ok(O_WRONLY | O_TRUNC, O_WRONLY | O_TRUNC);
271 }
272 
TEST_F(Open,o_wronly)273 TEST_F(Open, o_wronly)
274 {
275 	test_ok(O_WRONLY, O_WRONLY);
276 }
277 
TEST_F(Open,o_rdwr)278 TEST_F(Open, o_rdwr)
279 {
280 	test_ok(O_RDWR, O_RDWR);
281 }
282 
283 /*
284  * Without FUSE_NO_OPEN_SUPPORT, returning ENOSYS is an error
285  */
TEST_F(Open,enosys)286 TEST_F(Open, enosys)
287 {
288 	const char FULLPATH[] = "mountpoint/some_file.txt";
289 	const char RELPATH[] = "some_file.txt";
290 	uint64_t ino = 42;
291 	int fd;
292 
293 	FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
294 	EXPECT_CALL(*m_mock, process(
295 		ResultOf([=](auto in) {
296 			return (in.header.opcode == FUSE_OPEN &&
297 				in.body.open.flags == (uint32_t)O_RDONLY &&
298 				in.header.nodeid == ino);
299 		}, Eq(true)),
300 		_)
301 	).Times(1)
302 	.WillOnce(Invoke(ReturnErrno(ENOSYS)));
303 
304 	fd = open(FULLPATH, O_RDONLY);
305 	ASSERT_EQ(-1, fd) << strerror(errno);
306 	EXPECT_EQ(ENOSYS, errno);
307 }
308 
309 /*
310  * If a fuse server sets FUSE_NO_OPEN_SUPPORT and returns ENOSYS to a
311  * FUSE_OPEN, then it and subsequent FUSE_OPEN and FUSE_RELEASE operations will
312  * also succeed automatically without being sent to the server.
313  */
TEST_F(OpenNoOpenSupport,enosys)314 TEST_F(OpenNoOpenSupport, enosys)
315 {
316 	const char FULLPATH[] = "mountpoint/some_file.txt";
317 	const char RELPATH[] = "some_file.txt";
318 	uint64_t ino = 42;
319 	int fd;
320 
321 	FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
322 	EXPECT_CALL(*m_mock, process(
323 		ResultOf([=](auto in) {
324 			return (in.header.opcode == FUSE_OPEN &&
325 				in.body.open.flags == (uint32_t)O_RDONLY &&
326 				in.header.nodeid == ino);
327 		}, Eq(true)),
328 		_)
329 	).Times(1)
330 	.WillOnce(Invoke(ReturnErrno(ENOSYS)));
331 	expect_flush(ino, 1, ReturnErrno(ENOSYS));
332 
333 	fd = open(FULLPATH, O_RDONLY);
334 	ASSERT_LE(0, fd) << strerror(errno);
335 	close(fd);
336 
337 	fd = open(FULLPATH, O_RDONLY);
338 	ASSERT_LE(0, fd) << strerror(errno);
339 
340 	leak(fd);
341 }
342