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