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 "default_permissions" mount option. They must be in their own 35 * file so they can be run as an unprivileged user 36 */ 37 38 extern "C" { 39 #include <sys/types.h> 40 #include <sys/extattr.h> 41 42 #include <fcntl.h> 43 #include <semaphore.h> 44 #include <unistd.h> 45 } 46 47 #include "mockfs.hh" 48 #include "utils.hh" 49 50 using namespace testing; 51 52 class DefaultPermissions: public FuseTest { 53 54 virtual void SetUp() { 55 m_default_permissions = true; 56 FuseTest::SetUp(); 57 if (HasFatalFailure() || IsSkipped()) 58 return; 59 60 if (geteuid() == 0) { 61 GTEST_SKIP() << "This test requires an unprivileged user"; 62 } 63 64 /* With -o default_permissions, FUSE_ACCESS should never be called */ 65 EXPECT_CALL(*m_mock, process( 66 ResultOf([=](auto in) { 67 return (in.header.opcode == FUSE_ACCESS); 68 }, Eq(true)), 69 _) 70 ).Times(0); 71 } 72 73 public: 74 void expect_chmod(uint64_t ino, mode_t mode, uint64_t size = 0) 75 { 76 EXPECT_CALL(*m_mock, process( 77 ResultOf([=](auto in) { 78 return (in.header.opcode == FUSE_SETATTR && 79 in.header.nodeid == ino && 80 in.body.setattr.valid == FATTR_MODE && 81 in.body.setattr.mode == mode); 82 }, Eq(true)), 83 _) 84 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 85 SET_OUT_HEADER_LEN(out, attr); 86 out.body.attr.attr.ino = ino; // Must match nodeid 87 out.body.attr.attr.mode = S_IFREG | mode; 88 out.body.attr.attr.size = size; 89 out.body.attr.attr_valid = UINT64_MAX; 90 }))); 91 } 92 93 void expect_create(const char *relpath, uint64_t ino) 94 { 95 EXPECT_CALL(*m_mock, process( 96 ResultOf([=](auto in) { 97 const char *name = (const char*)in.body.bytes + 98 sizeof(fuse_create_in); 99 return (in.header.opcode == FUSE_CREATE && 100 (0 == strcmp(relpath, name))); 101 }, Eq(true)), 102 _) 103 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 104 SET_OUT_HEADER_LEN(out, create); 105 out.body.create.entry.attr.mode = S_IFREG | 0644; 106 out.body.create.entry.nodeid = ino; 107 out.body.create.entry.entry_valid = UINT64_MAX; 108 out.body.create.entry.attr_valid = UINT64_MAX; 109 }))); 110 } 111 112 void expect_copy_file_range(uint64_t ino_in, uint64_t off_in, uint64_t ino_out, 113 uint64_t off_out, uint64_t len) 114 { 115 EXPECT_CALL(*m_mock, process( 116 ResultOf([=](auto in) { 117 return (in.header.opcode == FUSE_COPY_FILE_RANGE && 118 in.header.nodeid == ino_in && 119 in.body.copy_file_range.off_in == off_in && 120 in.body.copy_file_range.nodeid_out == ino_out && 121 in.body.copy_file_range.off_out == off_out && 122 in.body.copy_file_range.len == len); 123 }, Eq(true)), 124 _) 125 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 126 SET_OUT_HEADER_LEN(out, write); 127 out.body.write.size = len; 128 }))); 129 } 130 131 void expect_getattr(uint64_t ino, mode_t mode, uint64_t attr_valid, int times, 132 uid_t uid = 0, gid_t gid = 0) 133 { 134 EXPECT_CALL(*m_mock, process( 135 ResultOf([=](auto in) { 136 return (in.header.opcode == FUSE_GETATTR && 137 in.header.nodeid == ino); 138 }, Eq(true)), 139 _) 140 ).Times(times) 141 .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 142 SET_OUT_HEADER_LEN(out, attr); 143 out.body.attr.attr.ino = ino; // Must match nodeid 144 out.body.attr.attr.mode = mode; 145 out.body.attr.attr.size = 0; 146 out.body.attr.attr.uid = uid; 147 out.body.attr.attr.gid = gid; 148 out.body.attr.attr_valid = attr_valid; 149 }))); 150 } 151 152 void expect_lookup(const char *relpath, uint64_t ino, mode_t mode, 153 uint64_t attr_valid, uid_t uid = 0, gid_t gid = 0) 154 { 155 FuseTest::expect_lookup(relpath, ino, mode, 0, 1, attr_valid, uid, gid); 156 } 157 158 }; 159 160 class Access: public DefaultPermissions {}; 161 class Chown: public DefaultPermissions {}; 162 class Chgrp: public DefaultPermissions {}; 163 class CopyFileRange: public DefaultPermissions {}; 164 class Fspacectl: public DefaultPermissions {}; 165 class Lookup: public DefaultPermissions {}; 166 class Open: public DefaultPermissions {}; 167 class PosixFallocate: public DefaultPermissions {}; 168 class Setattr: public DefaultPermissions {}; 169 class Unlink: public DefaultPermissions {}; 170 class Utimensat: public DefaultPermissions {}; 171 class Write: public DefaultPermissions {}; 172 173 /* 174 * Test permission handling during create, mkdir, mknod, link, symlink, and 175 * rename vops (they all share a common path for permission checks in 176 * VOP_LOOKUP) 177 */ 178 class Create: public DefaultPermissions {}; 179 180 class Deleteextattr: public DefaultPermissions { 181 public: 182 void expect_removexattr() 183 { 184 EXPECT_CALL(*m_mock, process( 185 ResultOf([=](auto in) { 186 return (in.header.opcode == FUSE_REMOVEXATTR); 187 }, Eq(true)), 188 _) 189 ).WillOnce(Invoke(ReturnErrno(0))); 190 } 191 }; 192 193 class Getextattr: public DefaultPermissions { 194 public: 195 void expect_getxattr(ProcessMockerT r) 196 { 197 EXPECT_CALL(*m_mock, process( 198 ResultOf([=](auto in) { 199 return (in.header.opcode == FUSE_GETXATTR); 200 }, Eq(true)), 201 _) 202 ).WillOnce(Invoke(r)); 203 } 204 }; 205 206 class Listextattr: public DefaultPermissions { 207 public: 208 void expect_listxattr() 209 { 210 EXPECT_CALL(*m_mock, process( 211 ResultOf([=](auto in) { 212 return (in.header.opcode == FUSE_LISTXATTR); 213 }, Eq(true)), 214 _) 215 ).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) { 216 out.body.listxattr.size = 0; 217 SET_OUT_HEADER_LEN(out, listxattr); 218 }))); 219 } 220 }; 221 222 class Rename: public DefaultPermissions { 223 public: 224 /* 225 * Expect a rename and respond with the given error. Don't both to 226 * validate arguments; the tests in rename.cc do that. 227 */ 228 void expect_rename(int error) 229 { 230 EXPECT_CALL(*m_mock, process( 231 ResultOf([=](auto in) { 232 return (in.header.opcode == FUSE_RENAME); 233 }, Eq(true)), 234 _) 235 ).WillOnce(Invoke(ReturnErrno(error))); 236 } 237 }; 238 239 class Setextattr: public DefaultPermissions { 240 public: 241 void expect_setxattr(int error) 242 { 243 EXPECT_CALL(*m_mock, process( 244 ResultOf([=](auto in) { 245 return (in.header.opcode == FUSE_SETXATTR); 246 }, Eq(true)), 247 _) 248 ).WillOnce(Invoke(ReturnErrno(error))); 249 } 250 }; 251 252 /* Return a group to which this user does not belong */ 253 static gid_t excluded_group() 254 { 255 int i, ngroups = 64; 256 gid_t newgid, groups[ngroups]; 257 258 getgrouplist(getlogin(), getegid(), groups, &ngroups); 259 for (newgid = 0; ; newgid++) { 260 bool belongs = false; 261 262 for (i = 0; i < ngroups; i++) { 263 if (groups[i] == newgid) 264 belongs = true; 265 } 266 if (!belongs) 267 break; 268 } 269 /* newgid is now a group to which the current user does not belong */ 270 return newgid; 271 } 272 273 TEST_F(Access, eacces) 274 { 275 const char FULLPATH[] = "mountpoint/some_file.txt"; 276 const char RELPATH[] = "some_file.txt"; 277 uint64_t ino = 42; 278 mode_t access_mode = X_OK; 279 280 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 281 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX); 282 283 ASSERT_NE(0, access(FULLPATH, access_mode)); 284 ASSERT_EQ(EACCES, errno); 285 } 286 287 TEST_F(Access, eacces_no_cached_attrs) 288 { 289 const char FULLPATH[] = "mountpoint/some_file.txt"; 290 const char RELPATH[] = "some_file.txt"; 291 uint64_t ino = 42; 292 mode_t access_mode = X_OK; 293 294 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, 0, 1); 295 expect_lookup(RELPATH, ino, S_IFREG | 0644, 0); 296 expect_getattr(ino, S_IFREG | 0644, 0, 1); 297 /* 298 * Once default_permissions is properly implemented, there might be 299 * another FUSE_GETATTR or something in here. But there should not be 300 * a FUSE_ACCESS 301 */ 302 303 ASSERT_NE(0, access(FULLPATH, access_mode)); 304 ASSERT_EQ(EACCES, errno); 305 } 306 307 TEST_F(Access, ok) 308 { 309 const char FULLPATH[] = "mountpoint/some_file.txt"; 310 const char RELPATH[] = "some_file.txt"; 311 uint64_t ino = 42; 312 mode_t access_mode = R_OK; 313 314 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 315 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX); 316 /* 317 * Once default_permissions is properly implemented, there might be 318 * another FUSE_GETATTR or something in here. 319 */ 320 321 ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno); 322 } 323 324 /* Unprivileged users may chown a file to their own uid */ 325 TEST_F(Chown, chown_to_self) 326 { 327 const char FULLPATH[] = "mountpoint/some_file.txt"; 328 const char RELPATH[] = "some_file.txt"; 329 const uint64_t ino = 42; 330 const mode_t mode = 0755; 331 uid_t uid; 332 333 uid = geteuid(); 334 335 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid); 336 expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid); 337 /* The OS may optimize chown by omitting the redundant setattr */ 338 EXPECT_CALL(*m_mock, process( 339 ResultOf([](auto in) { 340 return (in.header.opcode == FUSE_SETATTR); 341 }, Eq(true)), 342 _) 343 ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out){ 344 SET_OUT_HEADER_LEN(out, attr); 345 out.body.attr.attr.mode = S_IFREG | mode; 346 out.body.attr.attr.uid = uid; 347 }))); 348 349 EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno); 350 } 351 352 /* 353 * A successful chown by a non-privileged non-owner should clear a file's SUID 354 * bit 355 */ 356 TEST_F(Chown, clear_suid) 357 { 358 const char FULLPATH[] = "mountpoint/some_file.txt"; 359 const char RELPATH[] = "some_file.txt"; 360 uint64_t ino = 42; 361 const mode_t oldmode = 06755; 362 const mode_t newmode = 0755; 363 uid_t uid = geteuid(); 364 uint32_t valid = FATTR_UID | FATTR_MODE; 365 366 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid); 367 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid); 368 EXPECT_CALL(*m_mock, process( 369 ResultOf([=](auto in) { 370 return (in.header.opcode == FUSE_SETATTR && 371 in.header.nodeid == ino && 372 in.body.setattr.valid == valid && 373 in.body.setattr.mode == newmode); 374 }, Eq(true)), 375 _) 376 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 377 SET_OUT_HEADER_LEN(out, attr); 378 out.body.attr.attr.ino = ino; // Must match nodeid 379 out.body.attr.attr.mode = S_IFREG | newmode; 380 out.body.attr.attr_valid = UINT64_MAX; 381 }))); 382 383 EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno); 384 } 385 386 387 /* Only root may change a file's owner */ 388 TEST_F(Chown, eperm) 389 { 390 const char FULLPATH[] = "mountpoint/some_file.txt"; 391 const char RELPATH[] = "some_file.txt"; 392 const uint64_t ino = 42; 393 const mode_t mode = 0755; 394 395 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, geteuid()); 396 expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, geteuid()); 397 EXPECT_CALL(*m_mock, process( 398 ResultOf([](auto in) { 399 return (in.header.opcode == FUSE_SETATTR); 400 }, Eq(true)), 401 _) 402 ).Times(0); 403 404 EXPECT_NE(0, chown(FULLPATH, 0, -1)); 405 EXPECT_EQ(EPERM, errno); 406 } 407 408 /* 409 * A successful chgrp by a non-privileged non-owner should clear a file's SUID 410 * bit 411 */ 412 TEST_F(Chgrp, clear_suid) 413 { 414 const char FULLPATH[] = "mountpoint/some_file.txt"; 415 const char RELPATH[] = "some_file.txt"; 416 uint64_t ino = 42; 417 const mode_t oldmode = 06755; 418 const mode_t newmode = 0755; 419 uid_t uid = geteuid(); 420 gid_t gid = getegid(); 421 uint32_t valid = FATTR_GID | FATTR_MODE; 422 423 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid); 424 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid); 425 EXPECT_CALL(*m_mock, process( 426 ResultOf([=](auto in) { 427 return (in.header.opcode == FUSE_SETATTR && 428 in.header.nodeid == ino && 429 in.body.setattr.valid == valid && 430 in.body.setattr.mode == newmode); 431 }, Eq(true)), 432 _) 433 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 434 SET_OUT_HEADER_LEN(out, attr); 435 out.body.attr.attr.ino = ino; // Must match nodeid 436 out.body.attr.attr.mode = S_IFREG | newmode; 437 out.body.attr.attr_valid = UINT64_MAX; 438 }))); 439 440 EXPECT_EQ(0, chown(FULLPATH, -1, gid)) << strerror(errno); 441 } 442 443 /* non-root users may only chgrp a file to a group they belong to */ 444 TEST_F(Chgrp, eperm) 445 { 446 const char FULLPATH[] = "mountpoint/some_file.txt"; 447 const char RELPATH[] = "some_file.txt"; 448 const uint64_t ino = 42; 449 const mode_t mode = 0755; 450 uid_t uid; 451 gid_t gid, newgid; 452 453 uid = geteuid(); 454 gid = getegid(); 455 newgid = excluded_group(); 456 457 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid); 458 expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid); 459 EXPECT_CALL(*m_mock, process( 460 ResultOf([](auto in) { 461 return (in.header.opcode == FUSE_SETATTR); 462 }, Eq(true)), 463 _) 464 ).Times(0); 465 466 EXPECT_NE(0, chown(FULLPATH, -1, newgid)); 467 EXPECT_EQ(EPERM, errno); 468 } 469 470 TEST_F(Chgrp, ok) 471 { 472 const char FULLPATH[] = "mountpoint/some_file.txt"; 473 const char RELPATH[] = "some_file.txt"; 474 const uint64_t ino = 42; 475 const mode_t mode = 0755; 476 uid_t uid; 477 gid_t gid, newgid; 478 479 uid = geteuid(); 480 gid = 0; 481 newgid = getegid(); 482 483 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid); 484 expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid); 485 /* The OS may optimize chgrp by omitting the redundant setattr */ 486 EXPECT_CALL(*m_mock, process( 487 ResultOf([](auto in) { 488 return (in.header.opcode == FUSE_SETATTR && 489 in.header.nodeid == ino); 490 }, Eq(true)), 491 _) 492 ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out){ 493 SET_OUT_HEADER_LEN(out, attr); 494 out.body.attr.attr.mode = S_IFREG | mode; 495 out.body.attr.attr.uid = uid; 496 out.body.attr.attr.gid = newgid; 497 }))); 498 499 EXPECT_EQ(0, chown(FULLPATH, -1, newgid)) << strerror(errno); 500 } 501 502 /* A write by a non-owner should clear a file's SGID bit */ 503 TEST_F(CopyFileRange, clear_sgid) 504 { 505 const char FULLPATH_IN[] = "mountpoint/in.txt"; 506 const char RELPATH_IN[] = "in.txt"; 507 const char FULLPATH_OUT[] = "mountpoint/out.txt"; 508 const char RELPATH_OUT[] = "out.txt"; 509 struct stat sb; 510 uint64_t ino_in = 42; 511 uint64_t ino_out = 43; 512 mode_t oldmode = 02777; 513 mode_t newmode = 0777; 514 off_t fsize = 16; 515 off_t off_in = 0; 516 off_t off_out = 8; 517 off_t len = 8; 518 int fd_in, fd_out; 519 520 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 521 FuseTest::expect_lookup(RELPATH_IN, ino_in, S_IFREG | oldmode, fsize, 1, 522 UINT64_MAX, 0, 0); 523 expect_open(ino_in, 0, 1); 524 FuseTest::expect_lookup(RELPATH_OUT, ino_out, S_IFREG | oldmode, fsize, 525 1, UINT64_MAX, 0, 0); 526 expect_open(ino_out, 0, 1); 527 expect_copy_file_range(ino_in, off_in, ino_out, off_out, len); 528 expect_chmod(ino_out, newmode, fsize); 529 530 fd_in = open(FULLPATH_IN, O_RDONLY); 531 ASSERT_LE(0, fd_in) << strerror(errno); 532 fd_out = open(FULLPATH_OUT, O_WRONLY); 533 ASSERT_LE(0, fd_out) << strerror(errno); 534 ASSERT_EQ(len, 535 copy_file_range(fd_in, &off_in, fd_out, &off_out, len, 0)) 536 << strerror(errno); 537 ASSERT_EQ(0, fstat(fd_out, &sb)) << strerror(errno); 538 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 539 ASSERT_EQ(0, fstat(fd_in, &sb)) << strerror(errno); 540 EXPECT_EQ(S_IFREG | oldmode, sb.st_mode); 541 542 leak(fd_in); 543 leak(fd_out); 544 } 545 546 /* A write by a non-owner should clear a file's SUID bit */ 547 TEST_F(CopyFileRange, clear_suid) 548 { 549 const char FULLPATH_IN[] = "mountpoint/in.txt"; 550 const char RELPATH_IN[] = "in.txt"; 551 const char FULLPATH_OUT[] = "mountpoint/out.txt"; 552 const char RELPATH_OUT[] = "out.txt"; 553 struct stat sb; 554 uint64_t ino_in = 42; 555 uint64_t ino_out = 43; 556 mode_t oldmode = 04777; 557 mode_t newmode = 0777; 558 off_t fsize = 16; 559 off_t off_in = 0; 560 off_t off_out = 8; 561 off_t len = 8; 562 int fd_in, fd_out; 563 564 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 565 FuseTest::expect_lookup(RELPATH_IN, ino_in, S_IFREG | oldmode, fsize, 1, 566 UINT64_MAX, 0, 0); 567 expect_open(ino_in, 0, 1); 568 FuseTest::expect_lookup(RELPATH_OUT, ino_out, S_IFREG | oldmode, fsize, 569 1, UINT64_MAX, 0, 0); 570 expect_open(ino_out, 0, 1); 571 expect_copy_file_range(ino_in, off_in, ino_out, off_out, len); 572 expect_chmod(ino_out, newmode, fsize); 573 574 fd_in = open(FULLPATH_IN, O_RDONLY); 575 ASSERT_LE(0, fd_in) << strerror(errno); 576 fd_out = open(FULLPATH_OUT, O_WRONLY); 577 ASSERT_LE(0, fd_out) << strerror(errno); 578 ASSERT_EQ(len, 579 copy_file_range(fd_in, &off_in, fd_out, &off_out, len, 0)) 580 << strerror(errno); 581 ASSERT_EQ(0, fstat(fd_out, &sb)) << strerror(errno); 582 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 583 ASSERT_EQ(0, fstat(fd_in, &sb)) << strerror(errno); 584 EXPECT_EQ(S_IFREG | oldmode, sb.st_mode); 585 586 leak(fd_in); 587 leak(fd_out); 588 } 589 590 TEST_F(Create, ok) 591 { 592 const char FULLPATH[] = "mountpoint/some_file.txt"; 593 const char RELPATH[] = "some_file.txt"; 594 uint64_t ino = 42; 595 int fd; 596 597 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); 598 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 599 .WillOnce(Invoke(ReturnErrno(ENOENT))); 600 expect_create(RELPATH, ino); 601 602 fd = open(FULLPATH, O_CREAT | O_EXCL, 0644); 603 ASSERT_LE(0, fd) << strerror(errno); 604 leak(fd); 605 } 606 607 TEST_F(Create, eacces) 608 { 609 const char FULLPATH[] = "mountpoint/some_file.txt"; 610 const char RELPATH[] = "some_file.txt"; 611 612 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 613 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 614 .WillOnce(Invoke(ReturnErrno(ENOENT))); 615 616 ASSERT_EQ(-1, open(FULLPATH, O_CREAT | O_EXCL, 0644)); 617 EXPECT_EQ(EACCES, errno); 618 } 619 620 TEST_F(Deleteextattr, eacces) 621 { 622 const char FULLPATH[] = "mountpoint/some_file.txt"; 623 const char RELPATH[] = "some_file.txt"; 624 uint64_t ino = 42; 625 int ns = EXTATTR_NAMESPACE_USER; 626 627 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 628 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0); 629 630 ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo")); 631 ASSERT_EQ(EACCES, errno); 632 } 633 634 TEST_F(Deleteextattr, ok) 635 { 636 const char FULLPATH[] = "mountpoint/some_file.txt"; 637 const char RELPATH[] = "some_file.txt"; 638 uint64_t ino = 42; 639 int ns = EXTATTR_NAMESPACE_USER; 640 641 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 642 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); 643 expect_removexattr(); 644 645 ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo")) 646 << strerror(errno); 647 } 648 649 /* Delete system attributes requires superuser privilege */ 650 TEST_F(Deleteextattr, system) 651 { 652 const char FULLPATH[] = "mountpoint/some_file.txt"; 653 const char RELPATH[] = "some_file.txt"; 654 uint64_t ino = 42; 655 int ns = EXTATTR_NAMESPACE_SYSTEM; 656 657 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 658 expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid()); 659 660 ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo")); 661 ASSERT_EQ(EPERM, errno); 662 } 663 664 /* Anybody with write permission can set both timestamps to UTIME_NOW */ 665 TEST_F(Utimensat, utime_now) 666 { 667 const char FULLPATH[] = "mountpoint/some_file.txt"; 668 const char RELPATH[] = "some_file.txt"; 669 const uint64_t ino = 42; 670 /* Write permissions for everybody */ 671 const mode_t mode = 0666; 672 uid_t owner = 0; 673 const timespec times[2] = { 674 {.tv_sec = 0, .tv_nsec = UTIME_NOW}, 675 {.tv_sec = 0, .tv_nsec = UTIME_NOW}, 676 }; 677 678 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 679 expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, owner); 680 EXPECT_CALL(*m_mock, process( 681 ResultOf([](auto in) { 682 return (in.header.opcode == FUSE_SETATTR && 683 in.header.nodeid == ino && 684 in.body.setattr.valid & FATTR_ATIME && 685 in.body.setattr.valid & FATTR_MTIME); 686 }, Eq(true)), 687 _) 688 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { 689 SET_OUT_HEADER_LEN(out, attr); 690 out.body.attr.attr.mode = S_IFREG | mode; 691 }))); 692 693 ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, ×[0], 0)) 694 << strerror(errno); 695 } 696 697 /* Anybody can set both timestamps to UTIME_OMIT */ 698 TEST_F(Utimensat, utime_omit) 699 { 700 const char FULLPATH[] = "mountpoint/some_file.txt"; 701 const char RELPATH[] = "some_file.txt"; 702 const uint64_t ino = 42; 703 /* Write permissions for no one */ 704 const mode_t mode = 0444; 705 uid_t owner = 0; 706 const timespec times[2] = { 707 {.tv_sec = 0, .tv_nsec = UTIME_OMIT}, 708 {.tv_sec = 0, .tv_nsec = UTIME_OMIT}, 709 }; 710 711 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 712 expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, owner); 713 714 ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, ×[0], 0)) 715 << strerror(errno); 716 } 717 718 /* Deleting user attributes merely requires WRITE privilege */ 719 TEST_F(Deleteextattr, user) 720 { 721 const char FULLPATH[] = "mountpoint/some_file.txt"; 722 const char RELPATH[] = "some_file.txt"; 723 uint64_t ino = 42; 724 int ns = EXTATTR_NAMESPACE_USER; 725 726 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 727 expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0); 728 expect_removexattr(); 729 730 ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo")) 731 << strerror(errno); 732 } 733 734 TEST_F(Getextattr, eacces) 735 { 736 const char FULLPATH[] = "mountpoint/some_file.txt"; 737 const char RELPATH[] = "some_file.txt"; 738 uint64_t ino = 42; 739 char data[80]; 740 int ns = EXTATTR_NAMESPACE_USER; 741 742 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 743 expect_lookup(RELPATH, ino, S_IFREG | 0600, UINT64_MAX, 0); 744 745 ASSERT_EQ(-1, 746 extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data))); 747 ASSERT_EQ(EACCES, errno); 748 } 749 750 TEST_F(Getextattr, ok) 751 { 752 const char FULLPATH[] = "mountpoint/some_file.txt"; 753 const char RELPATH[] = "some_file.txt"; 754 uint64_t ino = 42; 755 char data[80]; 756 const char value[] = "whatever"; 757 ssize_t value_len = strlen(value) + 1; 758 int ns = EXTATTR_NAMESPACE_USER; 759 ssize_t r; 760 761 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 762 /* Getting user attributes only requires read access */ 763 expect_lookup(RELPATH, ino, S_IFREG | 0444, UINT64_MAX, 0); 764 expect_getxattr( 765 ReturnImmediate([&](auto in __unused, auto& out) { 766 memcpy((void*)out.body.bytes, value, value_len); 767 out.header.len = sizeof(out.header) + value_len; 768 }) 769 ); 770 771 r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)); 772 ASSERT_EQ(value_len, r) << strerror(errno); 773 EXPECT_STREQ(value, data); 774 } 775 776 /* Getting system attributes requires superuser privileges */ 777 TEST_F(Getextattr, system) 778 { 779 const char FULLPATH[] = "mountpoint/some_file.txt"; 780 const char RELPATH[] = "some_file.txt"; 781 uint64_t ino = 42; 782 char data[80]; 783 int ns = EXTATTR_NAMESPACE_SYSTEM; 784 785 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 786 expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid()); 787 788 ASSERT_EQ(-1, 789 extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data))); 790 ASSERT_EQ(EPERM, errno); 791 } 792 793 TEST_F(Listextattr, eacces) 794 { 795 const char FULLPATH[] = "mountpoint/some_file.txt"; 796 const char RELPATH[] = "some_file.txt"; 797 uint64_t ino = 42; 798 int ns = EXTATTR_NAMESPACE_USER; 799 800 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); 801 expect_lookup(RELPATH, ino, S_IFREG | 0600, UINT64_MAX, 0); 802 803 ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0)); 804 ASSERT_EQ(EACCES, errno); 805 } 806 807 TEST_F(Listextattr, ok) 808 { 809 const char FULLPATH[] = "mountpoint/some_file.txt"; 810 const char RELPATH[] = "some_file.txt"; 811 uint64_t ino = 42; 812 int ns = EXTATTR_NAMESPACE_USER; 813 814 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); 815 /* Listing user extended attributes merely requires read access */ 816 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0); 817 expect_listxattr(); 818 819 ASSERT_EQ(0, extattr_list_file(FULLPATH, ns, NULL, 0)) 820 << strerror(errno); 821 } 822 823 /* Listing system xattrs requires superuser privileges */ 824 TEST_F(Listextattr, system) 825 { 826 const char FULLPATH[] = "mountpoint/some_file.txt"; 827 const char RELPATH[] = "some_file.txt"; 828 uint64_t ino = 42; 829 int ns = EXTATTR_NAMESPACE_SYSTEM; 830 831 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); 832 /* Listing user extended attributes merely requires read access */ 833 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); 834 835 ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0)); 836 ASSERT_EQ(EPERM, errno); 837 } 838 839 /* A write by a non-owner should clear a file's SGID bit */ 840 TEST_F(Fspacectl, clear_sgid) 841 { 842 const char FULLPATH[] = "mountpoint/file.txt"; 843 const char RELPATH[] = "file.txt"; 844 struct stat sb; 845 struct spacectl_range rqsr; 846 uint64_t ino = 42; 847 mode_t oldmode = 02777; 848 mode_t newmode = 0777; 849 off_t fsize = 16; 850 off_t off = 8; 851 off_t len = 8; 852 int fd; 853 854 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 855 FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize, 856 1, UINT64_MAX, 0, 0); 857 expect_open(ino, 0, 1); 858 expect_fallocate(ino, off, len, 859 FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); 860 expect_chmod(ino, newmode, fsize); 861 862 fd = open(FULLPATH, O_WRONLY); 863 ASSERT_LE(0, fd) << strerror(errno); 864 rqsr.r_len = len; 865 rqsr.r_offset = off; 866 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); 867 ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); 868 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 869 870 leak(fd); 871 } 872 873 /* A write by a non-owner should clear a file's SUID bit */ 874 TEST_F(Fspacectl, clear_suid) 875 { 876 const char FULLPATH[] = "mountpoint/file.txt"; 877 const char RELPATH[] = "file.txt"; 878 struct stat sb; 879 struct spacectl_range rqsr; 880 uint64_t ino = 42; 881 mode_t oldmode = 04777; 882 mode_t newmode = 0777; 883 off_t fsize = 16; 884 off_t off = 8; 885 off_t len = 8; 886 int fd; 887 888 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 889 FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize, 890 1, UINT64_MAX, 0, 0); 891 expect_open(ino, 0, 1); 892 expect_fallocate(ino, off, len, 893 FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); 894 expect_chmod(ino, newmode, fsize); 895 896 fd = open(FULLPATH, O_WRONLY); 897 ASSERT_LE(0, fd) << strerror(errno); 898 rqsr.r_len = len; 899 rqsr.r_offset = off; 900 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); 901 ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); 902 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 903 904 leak(fd); 905 } 906 907 /* 908 * fspacectl() of a file without writable permissions should succeed as 909 * long as the file descriptor is writable. This is important when combined 910 * with O_CREAT 911 */ 912 TEST_F(Fspacectl, posix_fallocate_of_newly_created_file) 913 { 914 const char FULLPATH[] = "mountpoint/some_file.txt"; 915 const char RELPATH[] = "some_file.txt"; 916 struct spacectl_range rqsr; 917 const uint64_t ino = 42; 918 off_t off = 8; 919 off_t len = 8; 920 int fd; 921 922 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); 923 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 924 .WillOnce(Invoke(ReturnErrno(ENOENT))); 925 expect_create(RELPATH, ino); 926 expect_fallocate(ino, off, len, 927 FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); 928 929 fd = open(FULLPATH, O_CREAT | O_RDWR, 0); 930 ASSERT_LE(0, fd) << strerror(errno); 931 rqsr.r_len = len; 932 rqsr.r_offset = off; 933 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); 934 leak(fd); 935 } 936 937 /* A component of the search path lacks execute permissions */ 938 TEST_F(Lookup, eacces) 939 { 940 const char FULLPATH[] = "mountpoint/some_dir/some_file.txt"; 941 const char RELDIRPATH[] = "some_dir"; 942 uint64_t dir_ino = 42; 943 944 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 945 expect_lookup(RELDIRPATH, dir_ino, S_IFDIR | 0700, UINT64_MAX, 0); 946 947 EXPECT_EQ(-1, access(FULLPATH, F_OK)); 948 EXPECT_EQ(EACCES, errno); 949 } 950 951 TEST_F(Open, eacces) 952 { 953 const char FULLPATH[] = "mountpoint/some_file.txt"; 954 const char RELPATH[] = "some_file.txt"; 955 uint64_t ino = 42; 956 957 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 958 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX); 959 960 EXPECT_EQ(-1, open(FULLPATH, O_RDWR)); 961 EXPECT_EQ(EACCES, errno); 962 } 963 964 TEST_F(Open, ok) 965 { 966 const char FULLPATH[] = "mountpoint/some_file.txt"; 967 const char RELPATH[] = "some_file.txt"; 968 uint64_t ino = 42; 969 int fd; 970 971 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 972 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX); 973 expect_open(ino, 0, 1); 974 975 fd = open(FULLPATH, O_RDONLY); 976 ASSERT_LE(0, fd) << strerror(errno); 977 leak(fd); 978 } 979 980 /* A write by a non-owner should clear a file's SGID bit */ 981 TEST_F(PosixFallocate, clear_sgid) 982 { 983 const char FULLPATH[] = "mountpoint/file.txt"; 984 const char RELPATH[] = "file.txt"; 985 struct stat sb; 986 uint64_t ino = 42; 987 mode_t oldmode = 02777; 988 mode_t newmode = 0777; 989 off_t fsize = 16; 990 off_t off = 8; 991 off_t len = 8; 992 int fd; 993 994 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 995 FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize, 996 1, UINT64_MAX, 0, 0); 997 expect_open(ino, 0, 1); 998 expect_fallocate(ino, off, len, 0, 0); 999 expect_chmod(ino, newmode, fsize); 1000 1001 fd = open(FULLPATH, O_WRONLY); 1002 ASSERT_LE(0, fd) << strerror(errno); 1003 EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno); 1004 ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); 1005 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 1006 1007 leak(fd); 1008 } 1009 1010 /* A write by a non-owner should clear a file's SUID bit */ 1011 TEST_F(PosixFallocate, clear_suid) 1012 { 1013 const char FULLPATH[] = "mountpoint/file.txt"; 1014 const char RELPATH[] = "file.txt"; 1015 struct stat sb; 1016 uint64_t ino = 42; 1017 mode_t oldmode = 04777; 1018 mode_t newmode = 0777; 1019 off_t fsize = 16; 1020 off_t off = 8; 1021 off_t len = 8; 1022 int fd; 1023 1024 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1025 FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize, 1026 1, UINT64_MAX, 0, 0); 1027 expect_open(ino, 0, 1); 1028 expect_fallocate(ino, off, len, 0, 0); 1029 expect_chmod(ino, newmode, fsize); 1030 1031 fd = open(FULLPATH, O_WRONLY); 1032 ASSERT_LE(0, fd) << strerror(errno); 1033 EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno); 1034 ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); 1035 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 1036 1037 leak(fd); 1038 } 1039 1040 /* 1041 * posix_fallocate() of a file without writable permissions should succeed as 1042 * long as the file descriptor is writable. This is important when combined 1043 * with O_CREAT 1044 */ 1045 TEST_F(PosixFallocate, posix_fallocate_of_newly_created_file) 1046 { 1047 const char FULLPATH[] = "mountpoint/some_file.txt"; 1048 const char RELPATH[] = "some_file.txt"; 1049 const uint64_t ino = 42; 1050 off_t off = 8; 1051 off_t len = 8; 1052 int fd; 1053 1054 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); 1055 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 1056 .WillOnce(Invoke(ReturnErrno(ENOENT))); 1057 expect_create(RELPATH, ino); 1058 expect_fallocate(ino, off, len, 0, 0); 1059 1060 fd = open(FULLPATH, O_CREAT | O_RDWR, 0); 1061 ASSERT_LE(0, fd) << strerror(errno); 1062 EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno); 1063 leak(fd); 1064 } 1065 1066 TEST_F(Rename, eacces_on_srcdir) 1067 { 1068 const char FULLDST[] = "mountpoint/d/dst"; 1069 const char RELDST[] = "d/dst"; 1070 const char FULLSRC[] = "mountpoint/src"; 1071 const char RELSRC[] = "src"; 1072 uint64_t ino = 42; 1073 1074 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, 0); 1075 expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX); 1076 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) 1077 .Times(AnyNumber()) 1078 .WillRepeatedly(Invoke(ReturnErrno(ENOENT))); 1079 1080 ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); 1081 ASSERT_EQ(EACCES, errno); 1082 } 1083 1084 TEST_F(Rename, eacces_on_dstdir_for_creating) 1085 { 1086 const char FULLDST[] = "mountpoint/d/dst"; 1087 const char RELDSTDIR[] = "d"; 1088 const char RELDST[] = "dst"; 1089 const char FULLSRC[] = "mountpoint/src"; 1090 const char RELSRC[] = "src"; 1091 uint64_t src_ino = 42; 1092 uint64_t dstdir_ino = 43; 1093 1094 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); 1095 expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX); 1096 expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX); 1097 EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT))); 1098 1099 ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); 1100 ASSERT_EQ(EACCES, errno); 1101 } 1102 1103 TEST_F(Rename, eacces_on_dstdir_for_removing) 1104 { 1105 const char FULLDST[] = "mountpoint/d/dst"; 1106 const char RELDSTDIR[] = "d"; 1107 const char RELDST[] = "dst"; 1108 const char FULLSRC[] = "mountpoint/src"; 1109 const char RELSRC[] = "src"; 1110 uint64_t src_ino = 42; 1111 uint64_t dstdir_ino = 43; 1112 1113 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); 1114 expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX); 1115 expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX); 1116 EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT))); 1117 1118 ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); 1119 ASSERT_EQ(EACCES, errno); 1120 } 1121 1122 TEST_F(Rename, eperm_on_sticky_srcdir) 1123 { 1124 const char FULLDST[] = "mountpoint/d/dst"; 1125 const char FULLSRC[] = "mountpoint/src"; 1126 const char RELSRC[] = "src"; 1127 uint64_t ino = 42; 1128 1129 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0); 1130 expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX); 1131 1132 ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); 1133 ASSERT_EQ(EPERM, errno); 1134 } 1135 1136 /* 1137 * A user cannot move out a subdirectory that he does not own, because that 1138 * would require changing the subdirectory's ".." dirent 1139 */ 1140 TEST_F(Rename, eperm_for_subdirectory) 1141 { 1142 const char FULLDST[] = "mountpoint/d/dst"; 1143 const char FULLSRC[] = "mountpoint/src"; 1144 const char RELDSTDIR[] = "d"; 1145 const char RELDST[] = "dst"; 1146 const char RELSRC[] = "src"; 1147 uint64_t ino = 42; 1148 uint64_t dstdir_ino = 43; 1149 1150 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); 1151 expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0); 1152 expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0777, UINT64_MAX, 0); 1153 EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT))); 1154 1155 ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); 1156 ASSERT_EQ(EACCES, errno); 1157 } 1158 1159 /* 1160 * A user _can_ rename a subdirectory to which he lacks write permissions, if 1161 * it will keep the same parent 1162 */ 1163 TEST_F(Rename, subdirectory_to_same_dir) 1164 { 1165 const char FULLDST[] = "mountpoint/dst"; 1166 const char FULLSRC[] = "mountpoint/src"; 1167 const char RELDST[] = "dst"; 1168 const char RELSRC[] = "src"; 1169 uint64_t ino = 42; 1170 1171 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); 1172 expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0); 1173 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) 1174 .WillOnce(Invoke(ReturnErrno(ENOENT))); 1175 expect_rename(0); 1176 1177 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); 1178 } 1179 1180 TEST_F(Rename, eperm_on_sticky_dstdir) 1181 { 1182 const char FULLDST[] = "mountpoint/d/dst"; 1183 const char RELDSTDIR[] = "d"; 1184 const char RELDST[] = "dst"; 1185 const char FULLSRC[] = "mountpoint/src"; 1186 const char RELSRC[] = "src"; 1187 uint64_t src_ino = 42; 1188 uint64_t dstdir_ino = 43; 1189 uint64_t dst_ino = 44; 1190 1191 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); 1192 expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX); 1193 expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 01777, UINT64_MAX); 1194 EXPECT_LOOKUP(dstdir_ino, RELDST) 1195 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 1196 SET_OUT_HEADER_LEN(out, entry); 1197 out.body.entry.attr.mode = S_IFREG | 0644; 1198 out.body.entry.nodeid = dst_ino; 1199 out.body.entry.attr_valid = UINT64_MAX; 1200 out.body.entry.entry_valid = UINT64_MAX; 1201 out.body.entry.attr.uid = 0; 1202 }))); 1203 1204 ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); 1205 ASSERT_EQ(EPERM, errno); 1206 } 1207 1208 /* Successfully rename a file, overwriting the destination */ 1209 TEST_F(Rename, ok) 1210 { 1211 const char FULLDST[] = "mountpoint/dst"; 1212 const char RELDST[] = "dst"; 1213 const char FULLSRC[] = "mountpoint/src"; 1214 const char RELSRC[] = "src"; 1215 // The inode of the already-existing destination file 1216 uint64_t dst_ino = 2; 1217 uint64_t ino = 42; 1218 1219 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, geteuid()); 1220 expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX); 1221 expect_lookup(RELDST, dst_ino, S_IFREG | 0644, UINT64_MAX); 1222 expect_rename(0); 1223 1224 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); 1225 } 1226 1227 TEST_F(Rename, ok_to_remove_src_because_of_stickiness) 1228 { 1229 const char FULLDST[] = "mountpoint/dst"; 1230 const char RELDST[] = "dst"; 1231 const char FULLSRC[] = "mountpoint/src"; 1232 const char RELSRC[] = "src"; 1233 uint64_t ino = 42; 1234 1235 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0); 1236 expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); 1237 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) 1238 .WillOnce(Invoke(ReturnErrno(ENOENT))); 1239 expect_rename(0); 1240 1241 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); 1242 } 1243 1244 TEST_F(Setattr, ok) 1245 { 1246 const char FULLPATH[] = "mountpoint/some_file.txt"; 1247 const char RELPATH[] = "some_file.txt"; 1248 const uint64_t ino = 42; 1249 const mode_t oldmode = 0755; 1250 const mode_t newmode = 0644; 1251 1252 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1253 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid()); 1254 EXPECT_CALL(*m_mock, process( 1255 ResultOf([](auto in) { 1256 return (in.header.opcode == FUSE_SETATTR && 1257 in.header.nodeid == ino && 1258 in.body.setattr.mode == newmode); 1259 }, Eq(true)), 1260 _) 1261 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { 1262 SET_OUT_HEADER_LEN(out, attr); 1263 out.body.attr.attr.mode = S_IFREG | newmode; 1264 }))); 1265 1266 EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); 1267 } 1268 1269 TEST_F(Setattr, eacces) 1270 { 1271 const char FULLPATH[] = "mountpoint/some_file.txt"; 1272 const char RELPATH[] = "some_file.txt"; 1273 const uint64_t ino = 42; 1274 const mode_t oldmode = 0755; 1275 const mode_t newmode = 0644; 1276 1277 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1278 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, 0); 1279 EXPECT_CALL(*m_mock, process( 1280 ResultOf([](auto in) { 1281 return (in.header.opcode == FUSE_SETATTR); 1282 }, Eq(true)), 1283 _) 1284 ).Times(0); 1285 1286 EXPECT_NE(0, chmod(FULLPATH, newmode)); 1287 EXPECT_EQ(EPERM, errno); 1288 } 1289 1290 /* 1291 * ftruncate() of a file without writable permissions should succeed as long as 1292 * the file descriptor is writable. This is important when combined with 1293 * O_CREAT 1294 */ 1295 TEST_F(Setattr, ftruncate_of_newly_created_file) 1296 { 1297 const char FULLPATH[] = "mountpoint/some_file.txt"; 1298 const char RELPATH[] = "some_file.txt"; 1299 const uint64_t ino = 42; 1300 const mode_t mode = 0000; 1301 int fd; 1302 1303 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); 1304 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 1305 .WillOnce(Invoke(ReturnErrno(ENOENT))); 1306 expect_create(RELPATH, ino); 1307 EXPECT_CALL(*m_mock, process( 1308 ResultOf([](auto in) { 1309 return (in.header.opcode == FUSE_SETATTR && 1310 in.header.nodeid == ino && 1311 (in.body.setattr.valid & FATTR_SIZE)); 1312 }, Eq(true)), 1313 _) 1314 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 1315 SET_OUT_HEADER_LEN(out, attr); 1316 out.body.attr.attr.ino = ino; 1317 out.body.attr.attr.mode = S_IFREG | mode; 1318 out.body.attr.attr_valid = UINT64_MAX; 1319 }))); 1320 1321 fd = open(FULLPATH, O_CREAT | O_RDWR, 0); 1322 ASSERT_LE(0, fd) << strerror(errno); 1323 ASSERT_EQ(0, ftruncate(fd, 100)) << strerror(errno); 1324 leak(fd); 1325 } 1326 1327 /* 1328 * Setting the sgid bit should fail for an unprivileged user who doesn't belong 1329 * to the file's group 1330 */ 1331 TEST_F(Setattr, sgid_by_non_group_member) 1332 { 1333 const char FULLPATH[] = "mountpoint/some_file.txt"; 1334 const char RELPATH[] = "some_file.txt"; 1335 const uint64_t ino = 42; 1336 const mode_t oldmode = 0755; 1337 const mode_t newmode = 02755; 1338 uid_t uid = geteuid(); 1339 gid_t gid = excluded_group(); 1340 1341 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1342 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid); 1343 EXPECT_CALL(*m_mock, process( 1344 ResultOf([](auto in) { 1345 return (in.header.opcode == FUSE_SETATTR); 1346 }, Eq(true)), 1347 _) 1348 ).Times(0); 1349 1350 EXPECT_NE(0, chmod(FULLPATH, newmode)); 1351 EXPECT_EQ(EPERM, errno); 1352 } 1353 1354 /* Only the superuser may set the sticky bit on a non-directory */ 1355 TEST_F(Setattr, sticky_regular_file) 1356 { 1357 const char FULLPATH[] = "mountpoint/some_file.txt"; 1358 const char RELPATH[] = "some_file.txt"; 1359 const uint64_t ino = 42; 1360 const mode_t oldmode = 0644; 1361 const mode_t newmode = 01644; 1362 1363 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1364 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid()); 1365 EXPECT_CALL(*m_mock, process( 1366 ResultOf([](auto in) { 1367 return (in.header.opcode == FUSE_SETATTR); 1368 }, Eq(true)), 1369 _) 1370 ).Times(0); 1371 1372 EXPECT_NE(0, chmod(FULLPATH, newmode)); 1373 EXPECT_EQ(EFTYPE, errno); 1374 } 1375 1376 TEST_F(Setextattr, ok) 1377 { 1378 const char FULLPATH[] = "mountpoint/some_file.txt"; 1379 const char RELPATH[] = "some_file.txt"; 1380 uint64_t ino = 42; 1381 const char value[] = "whatever"; 1382 ssize_t value_len = strlen(value) + 1; 1383 int ns = EXTATTR_NAMESPACE_USER; 1384 ssize_t r; 1385 1386 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1387 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); 1388 expect_setxattr(0); 1389 1390 r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value, 1391 value_len); 1392 ASSERT_EQ(value_len, r) << strerror(errno); 1393 } 1394 1395 TEST_F(Setextattr, eacces) 1396 { 1397 const char FULLPATH[] = "mountpoint/some_file.txt"; 1398 const char RELPATH[] = "some_file.txt"; 1399 uint64_t ino = 42; 1400 const char value[] = "whatever"; 1401 ssize_t value_len = strlen(value) + 1; 1402 int ns = EXTATTR_NAMESPACE_USER; 1403 1404 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1405 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0); 1406 1407 ASSERT_EQ(-1, extattr_set_file(FULLPATH, ns, "foo", (const void*)value, 1408 value_len)); 1409 ASSERT_EQ(EACCES, errno); 1410 } 1411 1412 // Setting system attributes requires superuser privileges 1413 TEST_F(Setextattr, system) 1414 { 1415 const char FULLPATH[] = "mountpoint/some_file.txt"; 1416 const char RELPATH[] = "some_file.txt"; 1417 uint64_t ino = 42; 1418 const char value[] = "whatever"; 1419 ssize_t value_len = strlen(value) + 1; 1420 int ns = EXTATTR_NAMESPACE_SYSTEM; 1421 1422 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1423 expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid()); 1424 1425 ASSERT_EQ(-1, extattr_set_file(FULLPATH, ns, "foo", (const void*)value, 1426 value_len)); 1427 ASSERT_EQ(EPERM, errno); 1428 } 1429 1430 // Setting user attributes merely requires write privileges 1431 TEST_F(Setextattr, user) 1432 { 1433 const char FULLPATH[] = "mountpoint/some_file.txt"; 1434 const char RELPATH[] = "some_file.txt"; 1435 uint64_t ino = 42; 1436 const char value[] = "whatever"; 1437 ssize_t value_len = strlen(value) + 1; 1438 int ns = EXTATTR_NAMESPACE_USER; 1439 ssize_t r; 1440 1441 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1442 expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0); 1443 expect_setxattr(0); 1444 1445 r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value, 1446 value_len); 1447 ASSERT_EQ(value_len, r) << strerror(errno); 1448 } 1449 1450 TEST_F(Unlink, ok) 1451 { 1452 const char FULLPATH[] = "mountpoint/some_file.txt"; 1453 const char RELPATH[] = "some_file.txt"; 1454 uint64_t ino = 42; 1455 sem_t sem; 1456 1457 ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); 1458 1459 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); 1460 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); 1461 expect_unlink(FUSE_ROOT_ID, RELPATH, 0); 1462 expect_forget(ino, 1, &sem); 1463 1464 ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno); 1465 1466 sem_wait(&sem); 1467 sem_destroy(&sem); 1468 } 1469 1470 /* 1471 * Ensure that a cached name doesn't cause unlink to bypass permission checks 1472 * in VOP_LOOKUP. 1473 * 1474 * This test should pass because lookup(9) purges the namecache entry by doing 1475 * a vfs_cache_lookup with ~MAKEENTRY when nameiop == DELETE. 1476 */ 1477 TEST_F(Unlink, cached_unwritable_directory) 1478 { 1479 const char FULLPATH[] = "mountpoint/some_file.txt"; 1480 const char RELPATH[] = "some_file.txt"; 1481 uint64_t ino = 42; 1482 1483 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1484 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 1485 .Times(AnyNumber()) 1486 .WillRepeatedly(Invoke( 1487 ReturnImmediate([=](auto i __unused, auto& out) { 1488 SET_OUT_HEADER_LEN(out, entry); 1489 out.body.entry.attr.mode = S_IFREG | 0644; 1490 out.body.entry.nodeid = ino; 1491 out.body.entry.entry_valid = UINT64_MAX; 1492 })) 1493 ); 1494 1495 /* Fill name cache */ 1496 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 1497 /* Despite cached name , unlink should fail */ 1498 ASSERT_EQ(-1, unlink(FULLPATH)); 1499 ASSERT_EQ(EACCES, errno); 1500 } 1501 1502 TEST_F(Unlink, unwritable_directory) 1503 { 1504 const char FULLPATH[] = "mountpoint/some_file.txt"; 1505 const char RELPATH[] = "some_file.txt"; 1506 uint64_t ino = 42; 1507 1508 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1509 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); 1510 1511 ASSERT_EQ(-1, unlink(FULLPATH)); 1512 ASSERT_EQ(EACCES, errno); 1513 } 1514 1515 TEST_F(Unlink, sticky_directory) 1516 { 1517 const char FULLPATH[] = "mountpoint/some_file.txt"; 1518 const char RELPATH[] = "some_file.txt"; 1519 uint64_t ino = 42; 1520 1521 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1); 1522 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0); 1523 1524 ASSERT_EQ(-1, unlink(FULLPATH)); 1525 ASSERT_EQ(EPERM, errno); 1526 } 1527 1528 /* A write by a non-owner should clear a file's SUID bit */ 1529 TEST_F(Write, clear_suid) 1530 { 1531 const char FULLPATH[] = "mountpoint/some_file.txt"; 1532 const char RELPATH[] = "some_file.txt"; 1533 struct stat sb; 1534 uint64_t ino = 42; 1535 mode_t oldmode = 04777; 1536 mode_t newmode = 0777; 1537 char wbuf[1] = {'x'}; 1538 int fd; 1539 1540 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1541 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX); 1542 expect_open(ino, 0, 1); 1543 expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf); 1544 expect_chmod(ino, newmode, sizeof(wbuf)); 1545 1546 fd = open(FULLPATH, O_WRONLY); 1547 ASSERT_LE(0, fd) << strerror(errno); 1548 ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno); 1549 ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); 1550 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 1551 leak(fd); 1552 } 1553 1554 /* A write by a non-owner should clear a file's SGID bit */ 1555 TEST_F(Write, clear_sgid) 1556 { 1557 const char FULLPATH[] = "mountpoint/some_file.txt"; 1558 const char RELPATH[] = "some_file.txt"; 1559 struct stat sb; 1560 uint64_t ino = 42; 1561 mode_t oldmode = 02777; 1562 mode_t newmode = 0777; 1563 char wbuf[1] = {'x'}; 1564 int fd; 1565 1566 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1567 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX); 1568 expect_open(ino, 0, 1); 1569 expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf); 1570 expect_chmod(ino, newmode, sizeof(wbuf)); 1571 1572 fd = open(FULLPATH, O_WRONLY); 1573 ASSERT_LE(0, fd) << strerror(errno); 1574 ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno); 1575 ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); 1576 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 1577 leak(fd); 1578 } 1579 1580 /* Regression test for a specific recurse-of-nonrecursive-lock panic 1581 * 1582 * With writeback caching, we can't call vtruncbuf from fuse_io_strategy, or it 1583 * may panic. That happens if the FUSE_SETATTR response indicates that the 1584 * file's size has changed since the write. 1585 */ 1586 TEST_F(Write, recursion_panic_while_clearing_suid) 1587 { 1588 const char FULLPATH[] = "mountpoint/some_file.txt"; 1589 const char RELPATH[] = "some_file.txt"; 1590 uint64_t ino = 42; 1591 mode_t oldmode = 04777; 1592 mode_t newmode = 0777; 1593 char wbuf[1] = {'x'}; 1594 int fd; 1595 1596 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1597 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX); 1598 expect_open(ino, 0, 1); 1599 expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf); 1600 /* XXX Return a smaller file size than what we just wrote! */ 1601 expect_chmod(ino, newmode, 0); 1602 1603 fd = open(FULLPATH, O_WRONLY); 1604 ASSERT_LE(0, fd) << strerror(errno); 1605 ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno); 1606 leak(fd); 1607 } 1608