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:
SetUp()46 virtual void SetUp() {
47 FuseTest::SetUp();
48 // Clear the default FUSE_ACCESS expectation
49 Mock::VerifyAndClearExpectations(m_mock);
50 }
51
expect_lookup(const char * relpath,uint64_t ino)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 */
expect_noaccess(uint64_t ino,mode_t access_mask)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:
SetUp()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 */
TEST_F(Access,chmod)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 */
TEST_F(Access,create)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. */
TEST_F(Access,eaccess)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 */
TEST_F(Access,enosys)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
TEST_F(RofsAccess,erofs)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 */
TEST_F(Access,Getxattr)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. */
TEST_F(Access,ok)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 */
TEST_F(Access,unlink)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 */
TEST_F(Access,unlink_sticky_directory)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