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