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