xref: /freebsd/tests/sys/fs/fusefs/access.cc (revision db33c6f3ae9d1231087710068ee4ea5398aacca7)
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/types.h>
33 #include <sys/extattr.h>
34 
35 #include <fcntl.h>
36 #include <unistd.h>
37 }
38 
39 #include "mockfs.hh"
40 #include "utils.hh"
41 
42 using namespace testing;
43 
44 class Access: public FuseTest {
45 public:
46 virtual void SetUp() {
47 	FuseTest::SetUp();
48 	// Clear the default FUSE_ACCESS expectation
49 	Mock::VerifyAndClearExpectations(m_mock);
50 }
51 
52 void expect_lookup(const char *relpath, uint64_t ino)
53 {
54 	FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1);
55 }
56 
57 /*
58  * Expect that FUSE_ACCESS will never be called for the given inode, with any
59  * bits in the supplied access_mask set
60  */
61 void expect_noaccess(uint64_t ino, mode_t access_mask)
62 {
63 	EXPECT_CALL(*m_mock, process(
64 		ResultOf([=](auto in) {
65 			return (in.header.opcode == FUSE_ACCESS &&
66 				in.header.nodeid == ino &&
67 				in.body.access.mask & access_mask);
68 		}, Eq(true)),
69 		_)
70 	).Times(0);
71 }
72 
73 };
74 
75 class RofsAccess: public Access {
76 public:
77 virtual void SetUp() {
78 	m_ro = true;
79 	Access::SetUp();
80 }
81 };
82 
83 /*
84  * Change the mode of a file.
85  *
86  * There should never be a FUSE_ACCESS sent for this operation, except for
87  * search permissions on the parent directory.
88  * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689
89  */
90 TEST_F(Access, chmod)
91 {
92 	const char FULLPATH[] = "mountpoint/some_file.txt";
93 	const char RELPATH[] = "some_file.txt";
94 	const uint64_t ino = 42;
95 	const mode_t newmode = 0644;
96 
97 	expect_access(FUSE_ROOT_ID, X_OK, 0);
98 	expect_lookup(RELPATH, ino);
99 	expect_noaccess(ino, 0);
100 	EXPECT_CALL(*m_mock, process(
101 		ResultOf([](auto in) {
102 			return (in.header.opcode == FUSE_SETATTR &&
103 				in.header.nodeid == ino);
104 		}, Eq(true)),
105 		_)
106 	).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
107 		SET_OUT_HEADER_LEN(out, attr);
108 		out.body.attr.attr.ino = ino;	// Must match nodeid
109 		out.body.attr.attr.mode = S_IFREG | newmode;
110 	})));
111 
112 	EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
113 }
114 
115 /*
116  * Create a new file
117  *
118  * There should never be a FUSE_ACCESS sent for this operation, except for
119  * search permissions on the parent directory.
120  * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689
121  */
122 TEST_F(Access, create)
123 {
124 	const char FULLPATH[] = "mountpoint/some_file.txt";
125 	const char RELPATH[] = "some_file.txt";
126 	mode_t mode = S_IFREG | 0755;
127 	uint64_t ino = 42;
128 
129 	expect_access(FUSE_ROOT_ID, X_OK, 0);
130 	expect_noaccess(FUSE_ROOT_ID, R_OK | W_OK);
131 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
132 		.WillOnce(Invoke(ReturnErrno(ENOENT)));
133 	expect_noaccess(ino, 0);
134 	EXPECT_CALL(*m_mock, process(
135 		ResultOf([=](auto in) {
136 			return (in.header.opcode == FUSE_CREATE);
137 		}, Eq(true)),
138 		_)
139 	).WillOnce(ReturnErrno(EPERM));
140 
141 	EXPECT_EQ(-1, open(FULLPATH, O_CREAT | O_EXCL, mode));
142 	EXPECT_EQ(EPERM, errno);
143 }
144 
145 /* The error case of FUSE_ACCESS.  */
146 TEST_F(Access, eaccess)
147 {
148 	const char FULLPATH[] = "mountpoint/some_file.txt";
149 	const char RELPATH[] = "some_file.txt";
150 	uint64_t ino = 42;
151 	mode_t	access_mode = X_OK;
152 
153 	expect_access(FUSE_ROOT_ID, X_OK, 0);
154 	expect_lookup(RELPATH, ino);
155 	expect_access(ino, access_mode, EACCES);
156 
157 	ASSERT_NE(0, access(FULLPATH, access_mode));
158 	ASSERT_EQ(EACCES, errno);
159 }
160 
161 /*
162  * If the filesystem returns ENOSYS, then it is treated as a permanent success,
163  * and subsequent VOP_ACCESS calls will succeed automatically without querying
164  * the daemon.
165  */
166 TEST_F(Access, enosys)
167 {
168 	const char FULLPATH[] = "mountpoint/some_file.txt";
169 	const char RELPATH[] = "some_file.txt";
170 	uint64_t ino = 42;
171 	mode_t	access_mode = R_OK;
172 
173 	expect_access(FUSE_ROOT_ID, X_OK, ENOSYS);
174 	FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 2);
175 
176 	ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
177 	ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
178 }
179 
180 TEST_F(RofsAccess, erofs)
181 {
182 	const char FULLPATH[] = "mountpoint/some_file.txt";
183 	const char RELPATH[] = "some_file.txt";
184 	uint64_t ino = 42;
185 	mode_t	access_mode = W_OK;
186 
187 	expect_access(FUSE_ROOT_ID, X_OK, 0);
188 	expect_lookup(RELPATH, ino);
189 
190 	ASSERT_NE(0, access(FULLPATH, access_mode));
191 	ASSERT_EQ(EROFS, errno);
192 }
193 
194 
195 /*
196  * Lookup an extended attribute
197  *
198  * There should never be a FUSE_ACCESS sent for this operation, except for
199  * search permissions on the parent directory.
200  * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689
201  */
202 TEST_F(Access, Getxattr)
203 {
204 	const char FULLPATH[] = "mountpoint/some_file.txt";
205 	const char RELPATH[] = "some_file.txt";
206 	uint64_t ino = 42;
207 	char data[80];
208 	int ns = EXTATTR_NAMESPACE_USER;
209 	ssize_t r;
210 
211 	expect_access(FUSE_ROOT_ID, X_OK, 0);
212 	expect_lookup(RELPATH, ino);
213 	expect_noaccess(ino, 0);
214 	expect_getxattr(ino, "user.foo", ReturnErrno(ENOATTR));
215 
216 	r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
217 	ASSERT_EQ(-1, r);
218 	ASSERT_EQ(ENOATTR, errno);
219 }
220 
221 /* The successful case of FUSE_ACCESS.  */
222 TEST_F(Access, ok)
223 {
224 	const char FULLPATH[] = "mountpoint/some_file.txt";
225 	const char RELPATH[] = "some_file.txt";
226 	uint64_t ino = 42;
227 	mode_t	access_mode = R_OK;
228 
229 	expect_access(FUSE_ROOT_ID, X_OK, 0);
230 	expect_lookup(RELPATH, ino);
231 	expect_access(ino, access_mode, 0);
232 
233 	ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
234 }
235 
236 /*
237  * Unlink a file
238  *
239  * There should never be a FUSE_ACCESS sent for this operation, except for
240  * search permissions on the parent directory.
241  * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689
242  */
243 TEST_F(Access, unlink)
244 {
245 	const char FULLPATH[] = "mountpoint/some_file.txt";
246 	const char RELPATH[] = "some_file.txt";
247 	uint64_t ino = 42;
248 
249 	expect_access(FUSE_ROOT_ID, X_OK, 0);
250 	expect_noaccess(FUSE_ROOT_ID, W_OK | R_OK);
251 	expect_noaccess(ino, 0);
252 	expect_lookup(RELPATH, ino);
253 	expect_unlink(1, RELPATH, EPERM);
254 
255 	ASSERT_NE(0, unlink(FULLPATH));
256 	ASSERT_EQ(EPERM, errno);
257 }
258 
259 /*
260  * Unlink a file whose parent diretory's sticky bit is set
261  *
262  * There should never be a FUSE_ACCESS sent for this operation, except for
263  * search permissions on the parent directory.
264  * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=245689
265  */
266 TEST_F(Access, unlink_sticky_directory)
267 {
268 	const char FULLPATH[] = "mountpoint/some_file.txt";
269 	const char RELPATH[] = "some_file.txt";
270 	uint64_t ino = 42;
271 
272 	expect_access(FUSE_ROOT_ID, X_OK, 0);
273 	expect_noaccess(FUSE_ROOT_ID, W_OK | R_OK);
274 	expect_noaccess(ino, 0);
275 	EXPECT_CALL(*m_mock, process(
276 		ResultOf([=](auto in) {
277 			return (in.header.opcode == FUSE_GETATTR &&
278 				in.header.nodeid == FUSE_ROOT_ID);
279 		}, Eq(true)),
280 		_)
281 	).WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out)
282 	{
283 		SET_OUT_HEADER_LEN(out, attr);
284 		out.body.attr.attr.ino = FUSE_ROOT_ID;
285 		out.body.attr.attr.mode = S_IFDIR | 01777;
286 		out.body.attr.attr.uid = 0;
287 		out.body.attr.attr_valid = UINT64_MAX;
288 	})));
289 	EXPECT_CALL(*m_mock, process(
290 		ResultOf([=](auto in) {
291 			return (in.header.opcode == FUSE_ACCESS &&
292 				in.header.nodeid == ino);
293 		}, Eq(true)),
294 		_)
295 	).Times(0);
296 	expect_lookup(RELPATH, ino);
297 	expect_unlink(FUSE_ROOT_ID, RELPATH, EPERM);
298 
299 	ASSERT_EQ(-1, unlink(FULLPATH));
300 	ASSERT_EQ(EPERM, errno);
301 }
302