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: 55 /* Unprivileged user id */ 56 int m_uid; 57 58 virtual void SetUp() { 59 if (geteuid() != 0) { 60 GTEST_SKIP() << "This test must be run as root"; 61 } 62 63 FuseTest::SetUp(); 64 } 65 }; 66 67 class AllowOther: public NoAllowOther { 68 69 public: 70 virtual void SetUp() { 71 m_allow_other = true; 72 NoAllowOther::SetUp(); 73 } 74 }; 75 76 TEST_F(AllowOther, allowed) 77 { 78 int status; 79 80 fork(true, &status, [&] { 81 uint64_t ino = 42; 82 83 expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); 84 expect_open(ino, 0, 1); 85 expect_flush(ino, 1, ReturnErrno(0)); 86 expect_release(ino, FH); 87 }, []() { 88 int fd; 89 90 fd = open(FULLPATH, O_RDONLY); 91 if (fd < 0) { 92 perror("open"); 93 return(1); 94 } 95 96 leak(fd); 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 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 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 leak(fd); 205 return(1); 206 } else if (errno != EPERM) { 207 fprintf(stderr, "Unexpected error: %s\n", 208 strerror(errno)); 209 return(1); 210 } 211 return 0; 212 } 213 ); 214 ASSERT_EQ(0, WEXITSTATUS(status)); 215 } 216 217 /* 218 * When -o allow_other is not used, users other than the owner aren't allowed 219 * to open anything inside of the mount point, not just the mountpoint itself 220 * This is a regression test for bug 237052 221 */ 222 TEST_F(NoAllowOther, disallowed_beneath_root) 223 { 224 const static char RELPATH2[] = "other_dir"; 225 const static uint64_t ino = 42; 226 const static uint64_t ino2 = 43; 227 int dfd, status; 228 229 expect_lookup(RELPATH, ino, S_IFDIR | 0755, 0, 1); 230 EXPECT_LOOKUP(ino, RELPATH2) 231 .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 232 SET_OUT_HEADER_LEN(out, entry); 233 out.body.entry.attr.mode = S_IFREG | 0644; 234 out.body.entry.nodeid = ino2; 235 out.body.entry.attr.nlink = 1; 236 out.body.entry.attr_valid = UINT64_MAX; 237 }))); 238 expect_opendir(ino); 239 dfd = open(FULLPATH, O_DIRECTORY); 240 ASSERT_LE(0, dfd) << strerror(errno); 241 242 fork(true, &status, [] { 243 }, [&]() { 244 int fd; 245 246 fd = openat(dfd, RELPATH2, O_RDONLY); 247 if (fd >= 0) { 248 fprintf(stderr, "openat should've failed\n"); 249 leak(fd); 250 return(1); 251 } else if (errno != EPERM) { 252 fprintf(stderr, "Unexpected error: %s\n", 253 strerror(errno)); 254 return(1); 255 } 256 return 0; 257 } 258 ); 259 ASSERT_EQ(0, WEXITSTATUS(status)); 260 261 leak(dfd); 262 } 263 264 /* 265 * Provide coverage for the extattr methods, which have a slightly different 266 * code path 267 */ 268 TEST_F(NoAllowOther, setextattr) 269 { 270 int ino = 42, status; 271 272 fork(true, &status, [&] { 273 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 274 .WillOnce(Invoke( 275 ReturnImmediate([=](auto in __unused, auto& out) { 276 SET_OUT_HEADER_LEN(out, entry); 277 out.body.entry.attr_valid = UINT64_MAX; 278 out.body.entry.entry_valid = UINT64_MAX; 279 out.body.entry.attr.mode = S_IFREG | 0644; 280 out.body.entry.nodeid = ino; 281 }))); 282 283 /* 284 * lookup the file to get it into the cache. 285 * Otherwise, the unprivileged lookup will fail with 286 * EACCES 287 */ 288 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 289 }, [&]() { 290 const char value[] = "whatever"; 291 ssize_t value_len = strlen(value) + 1; 292 int ns = EXTATTR_NAMESPACE_USER; 293 ssize_t r; 294 295 r = extattr_set_file(FULLPATH, ns, "foo", 296 (const void*)value, value_len); 297 if (r >= 0) { 298 fprintf(stderr, "should've failed\n"); 299 return(1); 300 } else if (errno != EPERM) { 301 fprintf(stderr, "Unexpected error: %s\n", 302 strerror(errno)); 303 return(1); 304 } 305 return 0; 306 } 307 ); 308 ASSERT_EQ(0, WEXITSTATUS(status)); 309 } 310