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 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: 67 virtual void SetUp() { 68 m_allow_other = true; 69 NoAllowOther::SetUp(); 70 } 71 }; 72 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 */ 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 */ 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 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 */ 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 */ 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