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