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