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
33extern "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
44using namespace testing;
45
46class Access: public FuseTest {
47public:
48virtual void SetUp() {
49	FuseTest::SetUp();
50	// Clear the default FUSE_ACCESS expectation
51	Mock::VerifyAndClearExpectations(m_mock);
52}
53
54void 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 */
63void 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
77class RofsAccess: public Access {
78public:
79virtual 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 */
92TEST_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 */
124TEST_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.  */
148TEST_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 */
168TEST_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
182TEST_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 */
204TEST_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.  */
224TEST_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 */
245TEST_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 */
268TEST_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