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