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 38 extern "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 49 using namespace testing; 50 51 const static char FULLPATH[] = "mountpoint/some_file.txt"; 52 const static char RELPATH[] = "some_file.txt"; 53 54 class NoAllowOther: public FuseTest { 55 56 public: 57 /* Unprivileged user id */ 58 int m_uid; 59 60 virtual void SetUp() { 61 if (geteuid() != 0) { 62 GTEST_SKIP() << "This test must be run as root"; 63 } 64 65 FuseTest::SetUp(); 66 } 67 }; 68 69 class AllowOther: public NoAllowOther { 70 71 public: 72 virtual void SetUp() { 73 m_allow_other = true; 74 NoAllowOther::SetUp(); 75 } 76 }; 77 78 TEST_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 */ 104 TEST_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 */ 134 TEST_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 EXPECT_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 193 TEST_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 */ 221 TEST_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 */ 266 TEST_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