1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause 3 * 4 * Copyright (c) 2019 The FreeBSD Foundation 5 * 6 * This software was developed by BFF Storage Systems, LLC under sponsorship 7 * from the FreeBSD Foundation. 8 * 9 * Redistribution and use in source and binary forms, with or without 10 * modification, are permitted provided that the following conditions 11 * are met: 12 * 1. Redistributions of source code must retain the above copyright 13 * notice, this list of conditions and the following disclaimer. 14 * 2. Redistributions in binary form must reproduce the above copyright 15 * notice, this list of conditions and the following disclaimer in the 16 * documentation and/or other materials provided with the distribution. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 19 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 22 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 24 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 25 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 26 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 27 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 28 * SUCH DAMAGE. 29 */ 30 31 /* 32 * Tests for the "default_permissions" mount option. They must be in their own 33 * file so they can be run as an unprivileged user. 34 */ 35 36 extern "C" { 37 #include <sys/types.h> 38 #include <sys/extattr.h> 39 40 #include <fcntl.h> 41 #include <semaphore.h> 42 #include <unistd.h> 43 } 44 45 #include "mockfs.hh" 46 #include "utils.hh" 47 48 using namespace testing; 49 50 class DefaultPermissions: public FuseTest { 51 52 virtual void SetUp() { 53 m_default_permissions = true; 54 FuseTest::SetUp(); 55 if (HasFatalFailure() || IsSkipped()) 56 return; 57 58 if (geteuid() == 0) { 59 GTEST_SKIP() << "This test requires an unprivileged user"; 60 } 61 62 /* With -o default_permissions, FUSE_ACCESS should never be called */ 63 EXPECT_CALL(*m_mock, process( 64 ResultOf([=](auto in) { 65 return (in.header.opcode == FUSE_ACCESS); 66 }, Eq(true)), 67 _) 68 ).Times(0); 69 } 70 71 public: 72 void expect_chmod(uint64_t ino, mode_t mode, uint64_t size = 0) 73 { 74 EXPECT_CALL(*m_mock, process( 75 ResultOf([=](auto in) { 76 return (in.header.opcode == FUSE_SETATTR && 77 in.header.nodeid == ino && 78 in.body.setattr.valid == FATTR_MODE && 79 in.body.setattr.mode == mode); 80 }, Eq(true)), 81 _) 82 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 83 SET_OUT_HEADER_LEN(out, attr); 84 out.body.attr.attr.ino = ino; // Must match nodeid 85 out.body.attr.attr.mode = S_IFREG | mode; 86 out.body.attr.attr.size = size; 87 out.body.attr.attr_valid = UINT64_MAX; 88 }))); 89 } 90 91 void expect_create(const char *relpath, uint64_t ino) 92 { 93 EXPECT_CALL(*m_mock, process( 94 ResultOf([=](auto in) { 95 const char *name = (const char*)in.body.bytes + 96 sizeof(fuse_create_in); 97 return (in.header.opcode == FUSE_CREATE && 98 (0 == strcmp(relpath, name))); 99 }, Eq(true)), 100 _) 101 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 102 SET_OUT_HEADER_LEN(out, create); 103 out.body.create.entry.attr.mode = S_IFREG | 0644; 104 out.body.create.entry.nodeid = ino; 105 out.body.create.entry.entry_valid = UINT64_MAX; 106 out.body.create.entry.attr_valid = UINT64_MAX; 107 }))); 108 } 109 110 void expect_copy_file_range(uint64_t ino_in, uint64_t off_in, uint64_t ino_out, 111 uint64_t off_out, uint64_t len) 112 { 113 EXPECT_CALL(*m_mock, process( 114 ResultOf([=](auto in) { 115 return (in.header.opcode == FUSE_COPY_FILE_RANGE && 116 in.header.nodeid == ino_in && 117 in.body.copy_file_range.off_in == off_in && 118 in.body.copy_file_range.nodeid_out == ino_out && 119 in.body.copy_file_range.off_out == off_out && 120 in.body.copy_file_range.len == len); 121 }, Eq(true)), 122 _) 123 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 124 SET_OUT_HEADER_LEN(out, write); 125 out.body.write.size = len; 126 }))); 127 } 128 129 void expect_getattr(uint64_t ino, mode_t mode, uint64_t attr_valid, int times, 130 uid_t uid = 0, gid_t gid = 0) 131 { 132 EXPECT_CALL(*m_mock, process( 133 ResultOf([=](auto in) { 134 return (in.header.opcode == FUSE_GETATTR && 135 in.header.nodeid == ino); 136 }, Eq(true)), 137 _) 138 ).Times(times) 139 .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 140 SET_OUT_HEADER_LEN(out, attr); 141 out.body.attr.attr.ino = ino; // Must match nodeid 142 out.body.attr.attr.mode = mode; 143 out.body.attr.attr.size = 0; 144 out.body.attr.attr.uid = uid; 145 out.body.attr.attr.gid = gid; 146 out.body.attr.attr_valid = attr_valid; 147 }))); 148 } 149 150 void expect_lookup(const char *relpath, uint64_t ino, mode_t mode, 151 uint64_t attr_valid, uid_t uid = 0, gid_t gid = 0) 152 { 153 FuseTest::expect_lookup(relpath, ino, mode, 0, 1, attr_valid, uid, gid); 154 } 155 156 }; 157 158 class Access: public DefaultPermissions {}; 159 class Chown: public DefaultPermissions {}; 160 class Chgrp: public DefaultPermissions {}; 161 class CopyFileRange: public DefaultPermissions {}; 162 class Fspacectl: public DefaultPermissions {}; 163 class Lookup: public DefaultPermissions {}; 164 class Open: public DefaultPermissions {}; 165 class PosixFallocate: public DefaultPermissions {}; 166 class Read: 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 write by a non-owner should clear a file's SGID bit */ 839 TEST_F(Fspacectl, clear_sgid) 840 { 841 const char FULLPATH[] = "mountpoint/file.txt"; 842 const char RELPATH[] = "file.txt"; 843 struct stat sb; 844 struct spacectl_range rqsr; 845 uint64_t ino = 42; 846 mode_t oldmode = 02777; 847 mode_t newmode = 0777; 848 off_t fsize = 16; 849 off_t off = 8; 850 off_t len = 8; 851 int fd; 852 853 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 854 FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize, 855 1, UINT64_MAX, 0, 0); 856 expect_open(ino, 0, 1); 857 expect_fallocate(ino, off, len, 858 FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); 859 expect_chmod(ino, newmode, fsize); 860 861 fd = open(FULLPATH, O_WRONLY); 862 ASSERT_LE(0, fd) << strerror(errno); 863 rqsr.r_len = len; 864 rqsr.r_offset = off; 865 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); 866 ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); 867 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 868 869 leak(fd); 870 } 871 872 /* A write by a non-owner should clear a file's SUID bit */ 873 TEST_F(Fspacectl, clear_suid) 874 { 875 const char FULLPATH[] = "mountpoint/file.txt"; 876 const char RELPATH[] = "file.txt"; 877 struct stat sb; 878 struct spacectl_range rqsr; 879 uint64_t ino = 42; 880 mode_t oldmode = 04777; 881 mode_t newmode = 0777; 882 off_t fsize = 16; 883 off_t off = 8; 884 off_t len = 8; 885 int fd; 886 887 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 888 FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize, 889 1, UINT64_MAX, 0, 0); 890 expect_open(ino, 0, 1); 891 expect_fallocate(ino, off, len, 892 FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); 893 expect_chmod(ino, newmode, fsize); 894 895 fd = open(FULLPATH, O_WRONLY); 896 ASSERT_LE(0, fd) << strerror(errno); 897 rqsr.r_len = len; 898 rqsr.r_offset = off; 899 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); 900 ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); 901 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 902 903 leak(fd); 904 } 905 906 /* 907 * fspacectl() of a file without writable permissions should succeed as 908 * long as the file descriptor is writable. This is important when combined 909 * with O_CREAT 910 */ 911 TEST_F(Fspacectl, posix_fallocate_of_newly_created_file) 912 { 913 const char FULLPATH[] = "mountpoint/some_file.txt"; 914 const char RELPATH[] = "some_file.txt"; 915 struct spacectl_range rqsr; 916 const uint64_t ino = 42; 917 off_t off = 8; 918 off_t len = 8; 919 int fd; 920 921 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); 922 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 923 .WillOnce(Invoke(ReturnErrno(ENOENT))); 924 expect_create(RELPATH, ino); 925 expect_fallocate(ino, off, len, 926 FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0); 927 928 fd = open(FULLPATH, O_CREAT | O_RDWR, 0); 929 ASSERT_LE(0, fd) << strerror(errno); 930 rqsr.r_len = len; 931 rqsr.r_offset = off; 932 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); 933 leak(fd); 934 } 935 936 /* A component of the search path lacks execute permissions */ 937 TEST_F(Lookup, eacces) 938 { 939 const char FULLPATH[] = "mountpoint/some_dir/some_file.txt"; 940 const char RELDIRPATH[] = "some_dir"; 941 uint64_t dir_ino = 42; 942 943 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 944 expect_lookup(RELDIRPATH, dir_ino, S_IFDIR | 0700, UINT64_MAX, 0); 945 946 EXPECT_EQ(-1, access(FULLPATH, F_OK)); 947 EXPECT_EQ(EACCES, errno); 948 } 949 950 TEST_F(Open, eacces) 951 { 952 const char FULLPATH[] = "mountpoint/some_file.txt"; 953 const char RELPATH[] = "some_file.txt"; 954 uint64_t ino = 42; 955 956 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 957 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX); 958 959 EXPECT_EQ(-1, open(FULLPATH, O_RDWR)); 960 EXPECT_EQ(EACCES, errno); 961 } 962 963 TEST_F(Open, ok) 964 { 965 const char FULLPATH[] = "mountpoint/some_file.txt"; 966 const char RELPATH[] = "some_file.txt"; 967 uint64_t ino = 42; 968 int fd; 969 970 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 971 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX); 972 expect_open(ino, 0, 1); 973 974 fd = open(FULLPATH, O_RDONLY); 975 ASSERT_LE(0, fd) << strerror(errno); 976 leak(fd); 977 } 978 979 /* A write by a non-owner should clear a file's SGID bit */ 980 TEST_F(PosixFallocate, clear_sgid) 981 { 982 const char FULLPATH[] = "mountpoint/file.txt"; 983 const char RELPATH[] = "file.txt"; 984 struct stat sb; 985 uint64_t ino = 42; 986 mode_t oldmode = 02777; 987 mode_t newmode = 0777; 988 off_t fsize = 16; 989 off_t off = 8; 990 off_t len = 8; 991 int fd; 992 993 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 994 FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize, 995 1, UINT64_MAX, 0, 0); 996 expect_open(ino, 0, 1); 997 expect_fallocate(ino, off, len, 0, 0); 998 expect_chmod(ino, newmode, fsize); 999 1000 fd = open(FULLPATH, O_WRONLY); 1001 ASSERT_LE(0, fd) << strerror(errno); 1002 EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno); 1003 ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); 1004 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 1005 1006 leak(fd); 1007 } 1008 1009 /* A write by a non-owner should clear a file's SUID bit */ 1010 TEST_F(PosixFallocate, clear_suid) 1011 { 1012 const char FULLPATH[] = "mountpoint/file.txt"; 1013 const char RELPATH[] = "file.txt"; 1014 struct stat sb; 1015 uint64_t ino = 42; 1016 mode_t oldmode = 04777; 1017 mode_t newmode = 0777; 1018 off_t fsize = 16; 1019 off_t off = 8; 1020 off_t len = 8; 1021 int fd; 1022 1023 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1024 FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize, 1025 1, UINT64_MAX, 0, 0); 1026 expect_open(ino, 0, 1); 1027 expect_fallocate(ino, off, len, 0, 0); 1028 expect_chmod(ino, newmode, fsize); 1029 1030 fd = open(FULLPATH, O_WRONLY); 1031 ASSERT_LE(0, fd) << strerror(errno); 1032 EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno); 1033 ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); 1034 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 1035 1036 leak(fd); 1037 } 1038 1039 /* 1040 * posix_fallocate() of a file without writable permissions should succeed as 1041 * long as the file descriptor is writable. This is important when combined 1042 * with O_CREAT 1043 */ 1044 TEST_F(PosixFallocate, posix_fallocate_of_newly_created_file) 1045 { 1046 const char FULLPATH[] = "mountpoint/some_file.txt"; 1047 const char RELPATH[] = "some_file.txt"; 1048 const uint64_t ino = 42; 1049 off_t off = 8; 1050 off_t len = 8; 1051 int fd; 1052 1053 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); 1054 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 1055 .WillOnce(Invoke(ReturnErrno(ENOENT))); 1056 expect_create(RELPATH, ino); 1057 expect_fallocate(ino, off, len, 0, 0); 1058 1059 fd = open(FULLPATH, O_CREAT | O_RDWR, 0); 1060 ASSERT_LE(0, fd) << strerror(errno); 1061 EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno); 1062 leak(fd); 1063 } 1064 1065 TEST_F(Rename, eacces_on_srcdir) 1066 { 1067 const char FULLDST[] = "mountpoint/d/dst"; 1068 const char RELDST[] = "d/dst"; 1069 const char FULLSRC[] = "mountpoint/src"; 1070 const char RELSRC[] = "src"; 1071 uint64_t ino = 42; 1072 1073 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, 0); 1074 expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX); 1075 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) 1076 .Times(AnyNumber()) 1077 .WillRepeatedly(Invoke(ReturnErrno(ENOENT))); 1078 1079 ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); 1080 ASSERT_EQ(EACCES, errno); 1081 } 1082 1083 TEST_F(Rename, eacces_on_dstdir_for_creating) 1084 { 1085 const char FULLDST[] = "mountpoint/d/dst"; 1086 const char RELDSTDIR[] = "d"; 1087 const char RELDST[] = "dst"; 1088 const char FULLSRC[] = "mountpoint/src"; 1089 const char RELSRC[] = "src"; 1090 uint64_t src_ino = 42; 1091 uint64_t dstdir_ino = 43; 1092 1093 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); 1094 expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX); 1095 expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX); 1096 EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT))); 1097 1098 ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); 1099 ASSERT_EQ(EACCES, errno); 1100 } 1101 1102 TEST_F(Rename, eacces_on_dstdir_for_removing) 1103 { 1104 const char FULLDST[] = "mountpoint/d/dst"; 1105 const char RELDSTDIR[] = "d"; 1106 const char RELDST[] = "dst"; 1107 const char FULLSRC[] = "mountpoint/src"; 1108 const char RELSRC[] = "src"; 1109 uint64_t src_ino = 42; 1110 uint64_t dstdir_ino = 43; 1111 1112 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); 1113 expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX); 1114 expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX); 1115 EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT))); 1116 1117 ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); 1118 ASSERT_EQ(EACCES, errno); 1119 } 1120 1121 TEST_F(Rename, eperm_on_sticky_srcdir) 1122 { 1123 const char FULLDST[] = "mountpoint/d/dst"; 1124 const char FULLSRC[] = "mountpoint/src"; 1125 const char RELSRC[] = "src"; 1126 uint64_t ino = 42; 1127 1128 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0); 1129 expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX); 1130 1131 ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); 1132 ASSERT_EQ(EPERM, errno); 1133 } 1134 1135 /* 1136 * A user cannot move out a subdirectory that he does not own, because that 1137 * would require changing the subdirectory's ".." dirent 1138 */ 1139 TEST_F(Rename, eperm_for_subdirectory) 1140 { 1141 const char FULLDST[] = "mountpoint/d/dst"; 1142 const char FULLSRC[] = "mountpoint/src"; 1143 const char RELDSTDIR[] = "d"; 1144 const char RELDST[] = "dst"; 1145 const char RELSRC[] = "src"; 1146 uint64_t ino = 42; 1147 uint64_t dstdir_ino = 43; 1148 1149 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); 1150 expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0); 1151 expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0777, UINT64_MAX, 0); 1152 EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT))); 1153 1154 ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); 1155 ASSERT_EQ(EACCES, errno); 1156 } 1157 1158 /* 1159 * A user _can_ rename a subdirectory to which he lacks write permissions, if 1160 * it will keep the same parent 1161 */ 1162 TEST_F(Rename, subdirectory_to_same_dir) 1163 { 1164 const char FULLDST[] = "mountpoint/dst"; 1165 const char FULLSRC[] = "mountpoint/src"; 1166 const char RELDST[] = "dst"; 1167 const char RELSRC[] = "src"; 1168 uint64_t ino = 42; 1169 1170 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); 1171 expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0); 1172 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) 1173 .WillOnce(Invoke(ReturnErrno(ENOENT))); 1174 expect_rename(0); 1175 1176 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); 1177 } 1178 1179 TEST_F(Rename, eperm_on_sticky_dstdir) 1180 { 1181 const char FULLDST[] = "mountpoint/d/dst"; 1182 const char RELDSTDIR[] = "d"; 1183 const char RELDST[] = "dst"; 1184 const char FULLSRC[] = "mountpoint/src"; 1185 const char RELSRC[] = "src"; 1186 uint64_t src_ino = 42; 1187 uint64_t dstdir_ino = 43; 1188 uint64_t dst_ino = 44; 1189 1190 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0); 1191 expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX); 1192 expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 01777, UINT64_MAX); 1193 EXPECT_LOOKUP(dstdir_ino, RELDST) 1194 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 1195 SET_OUT_HEADER_LEN(out, entry); 1196 out.body.entry.attr.mode = S_IFREG | 0644; 1197 out.body.entry.nodeid = dst_ino; 1198 out.body.entry.attr_valid = UINT64_MAX; 1199 out.body.entry.entry_valid = UINT64_MAX; 1200 out.body.entry.attr.uid = 0; 1201 }))); 1202 1203 ASSERT_EQ(-1, rename(FULLSRC, FULLDST)); 1204 ASSERT_EQ(EPERM, errno); 1205 } 1206 1207 /* Successfully rename a file, overwriting the destination */ 1208 TEST_F(Rename, ok) 1209 { 1210 const char FULLDST[] = "mountpoint/dst"; 1211 const char RELDST[] = "dst"; 1212 const char FULLSRC[] = "mountpoint/src"; 1213 const char RELSRC[] = "src"; 1214 // The inode of the already-existing destination file 1215 uint64_t dst_ino = 2; 1216 uint64_t ino = 42; 1217 1218 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, geteuid()); 1219 expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX); 1220 expect_lookup(RELDST, dst_ino, S_IFREG | 0644, UINT64_MAX); 1221 expect_rename(0); 1222 1223 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); 1224 } 1225 1226 TEST_F(Rename, ok_to_remove_src_because_of_stickiness) 1227 { 1228 const char FULLDST[] = "mountpoint/dst"; 1229 const char RELDST[] = "dst"; 1230 const char FULLSRC[] = "mountpoint/src"; 1231 const char RELSRC[] = "src"; 1232 uint64_t ino = 42; 1233 1234 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0); 1235 expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); 1236 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) 1237 .WillOnce(Invoke(ReturnErrno(ENOENT))); 1238 expect_rename(0); 1239 1240 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); 1241 } 1242 1243 // Don't update atime during close after read, if we lack permissions to write 1244 // that file. 1245 TEST_F(Read, atime_during_close) 1246 { 1247 const char FULLPATH[] = "mountpoint/some_file.txt"; 1248 const char RELPATH[] = "some_file.txt"; 1249 uint64_t ino = 42; 1250 int fd; 1251 ssize_t bufsize = 100; 1252 uint8_t buf[bufsize]; 1253 const char *CONTENTS = "abcdefgh"; 1254 ssize_t fsize = sizeof(CONTENTS); 1255 1256 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1257 FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0755, fsize, 1258 1, UINT64_MAX, 0, 0); 1259 expect_open(ino, 0, 1); 1260 expect_read(ino, 0, fsize, fsize, CONTENTS); 1261 EXPECT_CALL(*m_mock, process( 1262 ResultOf([&](auto in) { 1263 return (in.header.opcode == FUSE_SETATTR); 1264 }, Eq(true)), 1265 _) 1266 ).Times(0); 1267 expect_flush(ino, 1, ReturnErrno(0)); 1268 expect_release(ino, FuseTest::FH); 1269 1270 fd = open(FULLPATH, O_RDONLY); 1271 ASSERT_LE(0, fd) << strerror(errno); 1272 1273 /* Ensure atime will be different than during lookup */ 1274 nap(); 1275 1276 ASSERT_EQ(fsize, read(fd, buf, bufsize)) << strerror(errno); 1277 1278 close(fd); 1279 } 1280 1281 TEST_F(Setattr, ok) 1282 { 1283 const char FULLPATH[] = "mountpoint/some_file.txt"; 1284 const char RELPATH[] = "some_file.txt"; 1285 const uint64_t ino = 42; 1286 const mode_t oldmode = 0755; 1287 const mode_t newmode = 0644; 1288 1289 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1290 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid()); 1291 EXPECT_CALL(*m_mock, process( 1292 ResultOf([](auto in) { 1293 return (in.header.opcode == FUSE_SETATTR && 1294 in.header.nodeid == ino && 1295 in.body.setattr.mode == newmode); 1296 }, Eq(true)), 1297 _) 1298 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { 1299 SET_OUT_HEADER_LEN(out, attr); 1300 out.body.attr.attr.mode = S_IFREG | newmode; 1301 }))); 1302 1303 EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); 1304 } 1305 1306 TEST_F(Setattr, eacces) 1307 { 1308 const char FULLPATH[] = "mountpoint/some_file.txt"; 1309 const char RELPATH[] = "some_file.txt"; 1310 const uint64_t ino = 42; 1311 const mode_t oldmode = 0755; 1312 const mode_t newmode = 0644; 1313 1314 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1315 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, 0); 1316 EXPECT_CALL(*m_mock, process( 1317 ResultOf([](auto in) { 1318 return (in.header.opcode == FUSE_SETATTR); 1319 }, Eq(true)), 1320 _) 1321 ).Times(0); 1322 1323 EXPECT_NE(0, chmod(FULLPATH, newmode)); 1324 EXPECT_EQ(EPERM, errno); 1325 } 1326 1327 /* 1328 * ftruncate() of a file without writable permissions should succeed as long as 1329 * the file descriptor is writable. This is important when combined with 1330 * O_CREAT 1331 */ 1332 TEST_F(Setattr, ftruncate_of_newly_created_file) 1333 { 1334 const char FULLPATH[] = "mountpoint/some_file.txt"; 1335 const char RELPATH[] = "some_file.txt"; 1336 const uint64_t ino = 42; 1337 const mode_t mode = 0000; 1338 int fd; 1339 1340 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); 1341 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 1342 .WillOnce(Invoke(ReturnErrno(ENOENT))); 1343 expect_create(RELPATH, ino); 1344 EXPECT_CALL(*m_mock, process( 1345 ResultOf([](auto in) { 1346 return (in.header.opcode == FUSE_SETATTR && 1347 in.header.nodeid == ino && 1348 (in.body.setattr.valid & FATTR_SIZE)); 1349 }, Eq(true)), 1350 _) 1351 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 1352 SET_OUT_HEADER_LEN(out, attr); 1353 out.body.attr.attr.ino = ino; 1354 out.body.attr.attr.mode = S_IFREG | mode; 1355 out.body.attr.attr_valid = UINT64_MAX; 1356 }))); 1357 1358 fd = open(FULLPATH, O_CREAT | O_RDWR, 0); 1359 ASSERT_LE(0, fd) << strerror(errno); 1360 ASSERT_EQ(0, ftruncate(fd, 100)) << strerror(errno); 1361 leak(fd); 1362 } 1363 1364 /* 1365 * Setting the sgid bit should fail for an unprivileged user who doesn't belong 1366 * to the file's group 1367 */ 1368 TEST_F(Setattr, sgid_by_non_group_member) 1369 { 1370 const char FULLPATH[] = "mountpoint/some_file.txt"; 1371 const char RELPATH[] = "some_file.txt"; 1372 const uint64_t ino = 42; 1373 const mode_t oldmode = 0755; 1374 const mode_t newmode = 02755; 1375 uid_t uid = geteuid(); 1376 gid_t gid = excluded_group(); 1377 1378 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1379 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid); 1380 EXPECT_CALL(*m_mock, process( 1381 ResultOf([](auto in) { 1382 return (in.header.opcode == FUSE_SETATTR); 1383 }, Eq(true)), 1384 _) 1385 ).Times(0); 1386 1387 EXPECT_NE(0, chmod(FULLPATH, newmode)); 1388 EXPECT_EQ(EPERM, errno); 1389 } 1390 1391 /* Only the superuser may set the sticky bit on a non-directory */ 1392 TEST_F(Setattr, sticky_regular_file) 1393 { 1394 const char FULLPATH[] = "mountpoint/some_file.txt"; 1395 const char RELPATH[] = "some_file.txt"; 1396 const uint64_t ino = 42; 1397 const mode_t oldmode = 0644; 1398 const mode_t newmode = 01644; 1399 1400 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1401 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid()); 1402 EXPECT_CALL(*m_mock, process( 1403 ResultOf([](auto in) { 1404 return (in.header.opcode == FUSE_SETATTR); 1405 }, Eq(true)), 1406 _) 1407 ).Times(0); 1408 1409 EXPECT_NE(0, chmod(FULLPATH, newmode)); 1410 EXPECT_EQ(EFTYPE, errno); 1411 } 1412 1413 TEST_F(Setextattr, ok) 1414 { 1415 const char FULLPATH[] = "mountpoint/some_file.txt"; 1416 const char RELPATH[] = "some_file.txt"; 1417 uint64_t ino = 42; 1418 const char value[] = "whatever"; 1419 ssize_t value_len = strlen(value) + 1; 1420 int ns = EXTATTR_NAMESPACE_USER; 1421 ssize_t r; 1422 1423 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1424 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); 1425 expect_setxattr(0); 1426 1427 r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value, 1428 value_len); 1429 ASSERT_EQ(value_len, r) << strerror(errno); 1430 } 1431 1432 TEST_F(Setextattr, eacces) 1433 { 1434 const char FULLPATH[] = "mountpoint/some_file.txt"; 1435 const char RELPATH[] = "some_file.txt"; 1436 uint64_t ino = 42; 1437 const char value[] = "whatever"; 1438 ssize_t value_len = strlen(value) + 1; 1439 int ns = EXTATTR_NAMESPACE_USER; 1440 1441 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1442 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0); 1443 1444 ASSERT_EQ(-1, extattr_set_file(FULLPATH, ns, "foo", (const void*)value, 1445 value_len)); 1446 ASSERT_EQ(EACCES, errno); 1447 } 1448 1449 // Setting system attributes requires superuser privileges 1450 TEST_F(Setextattr, system) 1451 { 1452 const char FULLPATH[] = "mountpoint/some_file.txt"; 1453 const char RELPATH[] = "some_file.txt"; 1454 uint64_t ino = 42; 1455 const char value[] = "whatever"; 1456 ssize_t value_len = strlen(value) + 1; 1457 int ns = EXTATTR_NAMESPACE_SYSTEM; 1458 1459 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1460 expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid()); 1461 1462 ASSERT_EQ(-1, extattr_set_file(FULLPATH, ns, "foo", (const void*)value, 1463 value_len)); 1464 ASSERT_EQ(EPERM, errno); 1465 } 1466 1467 // Setting user attributes merely requires write privileges 1468 TEST_F(Setextattr, user) 1469 { 1470 const char FULLPATH[] = "mountpoint/some_file.txt"; 1471 const char RELPATH[] = "some_file.txt"; 1472 uint64_t ino = 42; 1473 const char value[] = "whatever"; 1474 ssize_t value_len = strlen(value) + 1; 1475 int ns = EXTATTR_NAMESPACE_USER; 1476 ssize_t r; 1477 1478 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1479 expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0); 1480 expect_setxattr(0); 1481 1482 r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value, 1483 value_len); 1484 ASSERT_EQ(value_len, r) << strerror(errno); 1485 } 1486 1487 TEST_F(Unlink, ok) 1488 { 1489 const char FULLPATH[] = "mountpoint/some_file.txt"; 1490 const char RELPATH[] = "some_file.txt"; 1491 uint64_t ino = 42; 1492 sem_t sem; 1493 1494 ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); 1495 1496 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1); 1497 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); 1498 expect_unlink(FUSE_ROOT_ID, RELPATH, 0); 1499 expect_forget(ino, 1, &sem); 1500 1501 ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno); 1502 1503 sem_wait(&sem); 1504 sem_destroy(&sem); 1505 } 1506 1507 /* 1508 * Ensure that a cached name doesn't cause unlink to bypass permission checks 1509 * in VOP_LOOKUP. 1510 * 1511 * This test should pass because lookup(9) purges the namecache entry by doing 1512 * a vfs_cache_lookup with ~MAKEENTRY when nameiop == DELETE. 1513 */ 1514 TEST_F(Unlink, cached_unwritable_directory) 1515 { 1516 const char FULLPATH[] = "mountpoint/some_file.txt"; 1517 const char RELPATH[] = "some_file.txt"; 1518 uint64_t ino = 42; 1519 1520 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1521 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 1522 .Times(AnyNumber()) 1523 .WillRepeatedly(Invoke( 1524 ReturnImmediate([=](auto i __unused, auto& out) { 1525 SET_OUT_HEADER_LEN(out, entry); 1526 out.body.entry.attr.mode = S_IFREG | 0644; 1527 out.body.entry.nodeid = ino; 1528 out.body.entry.entry_valid = UINT64_MAX; 1529 })) 1530 ); 1531 1532 /* Fill name cache */ 1533 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 1534 /* Despite cached name , unlink should fail */ 1535 ASSERT_EQ(-1, unlink(FULLPATH)); 1536 ASSERT_EQ(EACCES, errno); 1537 } 1538 1539 TEST_F(Unlink, unwritable_directory) 1540 { 1541 const char FULLPATH[] = "mountpoint/some_file.txt"; 1542 const char RELPATH[] = "some_file.txt"; 1543 uint64_t ino = 42; 1544 1545 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1546 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid()); 1547 1548 ASSERT_EQ(-1, unlink(FULLPATH)); 1549 ASSERT_EQ(EACCES, errno); 1550 } 1551 1552 TEST_F(Unlink, sticky_directory) 1553 { 1554 const char FULLPATH[] = "mountpoint/some_file.txt"; 1555 const char RELPATH[] = "some_file.txt"; 1556 uint64_t ino = 42; 1557 1558 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1); 1559 expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0); 1560 1561 ASSERT_EQ(-1, unlink(FULLPATH)); 1562 ASSERT_EQ(EPERM, errno); 1563 } 1564 1565 /* A write by a non-owner should clear a file's SUID bit */ 1566 TEST_F(Write, clear_suid) 1567 { 1568 const char FULLPATH[] = "mountpoint/some_file.txt"; 1569 const char RELPATH[] = "some_file.txt"; 1570 struct stat sb; 1571 uint64_t ino = 42; 1572 mode_t oldmode = 04777; 1573 mode_t newmode = 0777; 1574 char wbuf[1] = {'x'}; 1575 int fd; 1576 1577 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1578 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX); 1579 expect_open(ino, 0, 1); 1580 expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf); 1581 expect_chmod(ino, newmode, sizeof(wbuf)); 1582 1583 fd = open(FULLPATH, O_WRONLY); 1584 ASSERT_LE(0, fd) << strerror(errno); 1585 ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno); 1586 ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); 1587 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 1588 leak(fd); 1589 } 1590 1591 /* A write by a non-owner should clear a file's SGID bit */ 1592 TEST_F(Write, clear_sgid) 1593 { 1594 const char FULLPATH[] = "mountpoint/some_file.txt"; 1595 const char RELPATH[] = "some_file.txt"; 1596 struct stat sb; 1597 uint64_t ino = 42; 1598 mode_t oldmode = 02777; 1599 mode_t newmode = 0777; 1600 char wbuf[1] = {'x'}; 1601 int fd; 1602 1603 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1604 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX); 1605 expect_open(ino, 0, 1); 1606 expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf); 1607 expect_chmod(ino, newmode, sizeof(wbuf)); 1608 1609 fd = open(FULLPATH, O_WRONLY); 1610 ASSERT_LE(0, fd) << strerror(errno); 1611 ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno); 1612 ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno); 1613 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 1614 leak(fd); 1615 } 1616 1617 /* Regression test for a specific recurse-of-nonrecursive-lock panic 1618 * 1619 * With writeback caching, we can't call vtruncbuf from fuse_io_strategy, or it 1620 * may panic. That happens if the FUSE_SETATTR response indicates that the 1621 * file's size has changed since the write. 1622 */ 1623 TEST_F(Write, recursion_panic_while_clearing_suid) 1624 { 1625 const char FULLPATH[] = "mountpoint/some_file.txt"; 1626 const char RELPATH[] = "some_file.txt"; 1627 uint64_t ino = 42; 1628 mode_t oldmode = 04777; 1629 mode_t newmode = 0777; 1630 char wbuf[1] = {'x'}; 1631 int fd; 1632 1633 expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1); 1634 expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX); 1635 expect_open(ino, 0, 1); 1636 expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf); 1637 /* XXX Return a smaller file size than what we just wrote! */ 1638 expect_chmod(ino, newmode, 0); 1639 1640 fd = open(FULLPATH, O_WRONLY); 1641 ASSERT_LE(0, fd) << strerror(errno); 1642 ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno); 1643 leak(fd); 1644 } 1645