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/*
34 * Tests for the "allow_other" mount option.  They must be in their own
35 * file so they can be run as root
36 */
37
38extern "C" {
39#include <sys/types.h>
40#include <sys/extattr.h>
41#include <sys/wait.h>
42#include <fcntl.h>
43#include <unistd.h>
44}
45
46#include "mockfs.hh"
47#include "utils.hh"
48
49using namespace testing;
50
51const static char FULLPATH[] = "mountpoint/some_file.txt";
52const static char RELPATH[] = "some_file.txt";
53
54class NoAllowOther: public FuseTest {
55
56public:
57/* Unprivileged user id */
58int m_uid;
59
60virtual void SetUp() {
61	if (geteuid() != 0) {
62		GTEST_SKIP() << "This test must be run as root";
63	}
64
65	FuseTest::SetUp();
66}
67};
68
69class AllowOther: public NoAllowOther {
70
71public:
72virtual void SetUp() {
73	m_allow_other = true;
74	NoAllowOther::SetUp();
75}
76};
77
78TEST_F(AllowOther, allowed)
79{
80	int status;
81
82	fork(true, &status, [&] {
83			uint64_t ino = 42;
84
85			expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
86			expect_open(ino, 0, 1);
87			expect_flush(ino, 1, ReturnErrno(0));
88			expect_release(ino, FH);
89		}, []() {
90			int fd;
91
92			fd = open(FULLPATH, O_RDONLY);
93			if (fd < 0) {
94				perror("open");
95				return(1);
96			}
97			return 0;
98		}
99	);
100	ASSERT_EQ(0, WEXITSTATUS(status));
101}
102
103/* Check that fusefs uses the correct credentials for FUSE operations */
104TEST_F(AllowOther, creds)
105{
106	int status;
107	uid_t uid;
108	gid_t gid;
109
110	get_unprivileged_id(&uid, &gid);
111	fork(true, &status, [=] {
112			EXPECT_CALL(*m_mock, process( ResultOf([=](auto in) {
113				return (in.header.opcode == FUSE_LOOKUP &&
114					in.header.uid == uid &&
115					in.header.gid == gid);
116				}, Eq(true)),
117				_)
118			).Times(1)
119			.WillOnce(Invoke(ReturnErrno(ENOENT)));
120		}, []() {
121			eaccess(FULLPATH, F_OK);
122			return 0;
123		}
124	);
125	ASSERT_EQ(0, WEXITSTATUS(status));
126}
127
128/*
129 * A variation of the Open.multiple_creds test showing how the bug can lead to a
130 * privilege elevation.  The first process is privileged and opens a file only
131 * visible to root.  The second process is unprivileged and shouldn't be able
132 * to open the file, but does thanks to the bug
133 */
134TEST_F(AllowOther, privilege_escalation)
135{
136	int fd1, status;
137	const static uint64_t ino = 42;
138	const static uint64_t fh = 100;
139
140	/* Fork a child to open the file with different credentials */
141	fork(true, &status, [&] {
142
143		expect_lookup(RELPATH, ino, S_IFREG | 0600, 0, 2);
144		EXPECT_CALL(*m_mock, process(
145			ResultOf([=](auto in) {
146				return (in.header.opcode == FUSE_OPEN &&
147					in.header.pid == (uint32_t)getpid() &&
148					in.header.uid == (uint32_t)geteuid() &&
149					in.header.nodeid == ino);
150			}, Eq(true)),
151			_)
152		).WillOnce(Invoke(
153			ReturnImmediate([](auto in __unused, auto& out) {
154			out.body.open.fh = fh;
155			out.header.len = sizeof(out.header);
156			SET_OUT_HEADER_LEN(out, open);
157		})));
158
159		EXPECT_CALL(*m_mock, process(
160			ResultOf([=](auto in) {
161				return (in.header.opcode == FUSE_OPEN &&
162					in.header.pid != (uint32_t)getpid() &&
163					in.header.uid != (uint32_t)geteuid() &&
164					in.header.nodeid == ino);
165			}, Eq(true)),
166			_)
167		).Times(AnyNumber())
168		.WillRepeatedly(Invoke(ReturnErrno(EPERM)));
169
170		fd1 = open(FULLPATH, O_RDONLY);
171		ASSERT_LE(0, fd1) << strerror(errno);
172	}, [] {
173		int fd0;
174
175		fd0 = open(FULLPATH, O_RDONLY);
176		if (fd0 >= 0) {
177			fprintf(stderr, "Privilege escalation!\n");
178			return 1;
179		}
180		if (errno != EPERM) {
181			fprintf(stderr, "Unexpected error %s\n",
182				strerror(errno));
183			return 1;
184		}
185		leak(fd0);
186		return 0;
187	}
188	);
189	ASSERT_EQ(0, WEXITSTATUS(status));
190	leak(fd1);
191}
192
193TEST_F(NoAllowOther, disallowed)
194{
195	int status;
196
197	fork(true, &status, [] {
198		}, []() {
199			int fd;
200
201			fd = open(FULLPATH, O_RDONLY);
202			if (fd >= 0) {
203				fprintf(stderr, "open should've failed\n");
204				return(1);
205			} else if (errno != EPERM) {
206				fprintf(stderr, "Unexpected error: %s\n",
207					strerror(errno));
208				return(1);
209			}
210			return 0;
211		}
212	);
213	ASSERT_EQ(0, WEXITSTATUS(status));
214}
215
216/*
217 * When -o allow_other is not used, users other than the owner aren't allowed
218 * to open anything inside of the mount point, not just the mountpoint itself
219 * This is a regression test for bug 237052
220 */
221TEST_F(NoAllowOther, disallowed_beneath_root)
222{
223	const static char RELPATH2[] = "other_dir";
224	const static uint64_t ino = 42;
225	const static uint64_t ino2 = 43;
226	int dfd, status;
227
228	expect_lookup(RELPATH, ino, S_IFDIR | 0755, 0, 1);
229	EXPECT_LOOKUP(ino, RELPATH2)
230	.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
231		SET_OUT_HEADER_LEN(out, entry);
232		out.body.entry.attr.mode = S_IFREG | 0644;
233		out.body.entry.nodeid = ino2;
234		out.body.entry.attr.nlink = 1;
235		out.body.entry.attr_valid = UINT64_MAX;
236	})));
237	expect_opendir(ino);
238	dfd = open(FULLPATH, O_DIRECTORY);
239	ASSERT_LE(0, dfd) << strerror(errno);
240
241	fork(true, &status, [] {
242		}, [&]() {
243			int fd;
244
245			fd = openat(dfd, RELPATH2, O_RDONLY);
246			if (fd >= 0) {
247				fprintf(stderr, "openat should've failed\n");
248				return(1);
249			} else if (errno != EPERM) {
250				fprintf(stderr, "Unexpected error: %s\n",
251					strerror(errno));
252				return(1);
253			}
254			return 0;
255		}
256	);
257	ASSERT_EQ(0, WEXITSTATUS(status));
258
259	leak(dfd);
260}
261
262/*
263 * Provide coverage for the extattr methods, which have a slightly different
264 * code path
265 */
266TEST_F(NoAllowOther, setextattr)
267{
268	int ino = 42, status;
269
270	fork(true, &status, [&] {
271			EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
272			.WillOnce(Invoke(
273			ReturnImmediate([=](auto in __unused, auto& out) {
274				SET_OUT_HEADER_LEN(out, entry);
275				out.body.entry.attr_valid = UINT64_MAX;
276				out.body.entry.entry_valid = UINT64_MAX;
277				out.body.entry.attr.mode = S_IFREG | 0644;
278				out.body.entry.nodeid = ino;
279			})));
280
281			/*
282			 * lookup the file to get it into the cache.
283			 * Otherwise, the unprivileged lookup will fail with
284			 * EACCES
285			 */
286			ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
287		}, [&]() {
288			const char value[] = "whatever";
289			ssize_t value_len = strlen(value) + 1;
290			int ns = EXTATTR_NAMESPACE_USER;
291			ssize_t r;
292
293			r = extattr_set_file(FULLPATH, ns, "foo",
294				(const void*)value, value_len);
295			if (r >= 0) {
296				fprintf(stderr, "should've failed\n");
297				return(1);
298			} else if (errno != EPERM) {
299				fprintf(stderr, "Unexpected error: %s\n",
300					strerror(errno));
301				return(1);
302			}
303			return 0;
304		}
305	);
306	ASSERT_EQ(0, WEXITSTATUS(status));
307}
308