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