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 extern "C" { 34 #include <sys/stat.h> 35 36 #include <fcntl.h> 37 #include <semaphore.h> 38 } 39 40 #include "mockfs.hh" 41 #include "utils.hh" 42 43 using namespace testing; 44 45 class Setattr : public FuseTest {}; 46 47 class RofsSetattr: public Setattr { 48 public: 49 virtual void SetUp() { 50 m_ro = true; 51 Setattr::SetUp(); 52 } 53 }; 54 55 class Setattr_7_8: public Setattr { 56 public: 57 virtual void SetUp() { 58 m_kernel_minor_version = 8; 59 Setattr::SetUp(); 60 } 61 }; 62 63 64 /* 65 * If setattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs 66 * should use the cached attributes, rather than query the daemon 67 */ 68 TEST_F(Setattr, attr_cache) 69 { 70 const char FULLPATH[] = "mountpoint/some_file.txt"; 71 const char RELPATH[] = "some_file.txt"; 72 const uint64_t ino = 42; 73 struct stat sb; 74 const mode_t newmode = 0644; 75 76 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 77 .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 78 SET_OUT_HEADER_LEN(out, entry); 79 out.body.entry.attr.mode = S_IFREG | 0644; 80 out.body.entry.nodeid = ino; 81 out.body.entry.entry_valid = UINT64_MAX; 82 }))); 83 84 EXPECT_CALL(*m_mock, process( 85 ResultOf([](auto in) { 86 return (in.header.opcode == FUSE_SETATTR && 87 in.header.nodeid == ino); 88 }, Eq(true)), 89 _) 90 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { 91 SET_OUT_HEADER_LEN(out, attr); 92 out.body.attr.attr.ino = ino; // Must match nodeid 93 out.body.attr.attr.mode = S_IFREG | newmode; 94 out.body.attr.attr_valid = UINT64_MAX; 95 }))); 96 EXPECT_CALL(*m_mock, process( 97 ResultOf([](auto in) { 98 return (in.header.opcode == FUSE_GETATTR); 99 }, Eq(true)), 100 _) 101 ).Times(0); 102 103 /* Set an attribute with SETATTR */ 104 ASSERT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); 105 106 /* The stat(2) should use cached attributes */ 107 ASSERT_EQ(0, stat(FULLPATH, &sb)); 108 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 109 } 110 111 /* Change the mode of a file */ 112 TEST_F(Setattr, chmod) 113 { 114 const char FULLPATH[] = "mountpoint/some_file.txt"; 115 const char RELPATH[] = "some_file.txt"; 116 const uint64_t ino = 42; 117 const mode_t oldmode = 0755; 118 const mode_t newmode = 0644; 119 120 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 121 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 122 SET_OUT_HEADER_LEN(out, entry); 123 out.body.entry.attr.mode = S_IFREG | oldmode; 124 out.body.entry.nodeid = ino; 125 }))); 126 127 EXPECT_CALL(*m_mock, process( 128 ResultOf([](auto in) { 129 uint32_t valid = FATTR_MODE; 130 return (in.header.opcode == FUSE_SETATTR && 131 in.header.nodeid == ino && 132 in.body.setattr.valid == valid && 133 in.body.setattr.mode == newmode); 134 }, Eq(true)), 135 _) 136 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { 137 SET_OUT_HEADER_LEN(out, attr); 138 out.body.attr.attr.ino = ino; // Must match nodeid 139 out.body.attr.attr.mode = S_IFREG | newmode; 140 }))); 141 EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); 142 } 143 144 /* 145 * Chmod a multiply-linked file with cached attributes. Check that both files' 146 * attributes have changed. 147 */ 148 TEST_F(Setattr, chmod_multiply_linked) 149 { 150 const char FULLPATH0[] = "mountpoint/some_file.txt"; 151 const char RELPATH0[] = "some_file.txt"; 152 const char FULLPATH1[] = "mountpoint/other_file.txt"; 153 const char RELPATH1[] = "other_file.txt"; 154 struct stat sb; 155 const uint64_t ino = 42; 156 const mode_t oldmode = 0777; 157 const mode_t newmode = 0666; 158 159 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH0) 160 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 161 SET_OUT_HEADER_LEN(out, entry); 162 out.body.entry.attr.mode = S_IFREG | oldmode; 163 out.body.entry.nodeid = ino; 164 out.body.entry.attr.nlink = 2; 165 out.body.entry.attr_valid = UINT64_MAX; 166 out.body.entry.entry_valid = UINT64_MAX; 167 }))); 168 169 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH1) 170 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 171 SET_OUT_HEADER_LEN(out, entry); 172 out.body.entry.attr.mode = S_IFREG | oldmode; 173 out.body.entry.nodeid = ino; 174 out.body.entry.attr.nlink = 2; 175 out.body.entry.attr_valid = UINT64_MAX; 176 out.body.entry.entry_valid = UINT64_MAX; 177 }))); 178 179 EXPECT_CALL(*m_mock, process( 180 ResultOf([](auto in) { 181 uint32_t valid = FATTR_MODE; 182 return (in.header.opcode == FUSE_SETATTR && 183 in.header.nodeid == ino && 184 in.body.setattr.valid == valid && 185 in.body.setattr.mode == newmode); 186 }, Eq(true)), 187 _) 188 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { 189 SET_OUT_HEADER_LEN(out, attr); 190 out.body.attr.attr.ino = ino; 191 out.body.attr.attr.mode = S_IFREG | newmode; 192 out.body.attr.attr.nlink = 2; 193 out.body.attr.attr_valid = UINT64_MAX; 194 }))); 195 196 /* For a lookup of the 2nd file to get it into the cache*/ 197 ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno); 198 EXPECT_EQ(S_IFREG | oldmode, sb.st_mode); 199 200 ASSERT_EQ(0, chmod(FULLPATH0, newmode)) << strerror(errno); 201 ASSERT_EQ(0, stat(FULLPATH0, &sb)) << strerror(errno); 202 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 203 ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno); 204 EXPECT_EQ(S_IFREG | newmode, sb.st_mode); 205 } 206 207 208 /* Change the owner and group of a file */ 209 TEST_F(Setattr, chown) 210 { 211 const char FULLPATH[] = "mountpoint/some_file.txt"; 212 const char RELPATH[] = "some_file.txt"; 213 const uint64_t ino = 42; 214 const gid_t oldgroup = 66; 215 const gid_t newgroup = 99; 216 const uid_t olduser = 33; 217 const uid_t newuser = 44; 218 219 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 220 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 221 SET_OUT_HEADER_LEN(out, entry); 222 out.body.entry.attr.mode = S_IFREG | 0644; 223 out.body.entry.nodeid = ino; 224 out.body.entry.attr.gid = oldgroup; 225 out.body.entry.attr.uid = olduser; 226 }))); 227 228 EXPECT_CALL(*m_mock, process( 229 ResultOf([](auto in) { 230 uint32_t valid = FATTR_GID | FATTR_UID; 231 return (in.header.opcode == FUSE_SETATTR && 232 in.header.nodeid == ino && 233 in.body.setattr.valid == valid && 234 in.body.setattr.uid == newuser && 235 in.body.setattr.gid == newgroup); 236 }, Eq(true)), 237 _) 238 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { 239 SET_OUT_HEADER_LEN(out, attr); 240 out.body.attr.attr.ino = ino; // Must match nodeid 241 out.body.attr.attr.mode = S_IFREG | 0644; 242 out.body.attr.attr.uid = newuser; 243 out.body.attr.attr.gid = newgroup; 244 }))); 245 EXPECT_EQ(0, chown(FULLPATH, newuser, newgroup)) << strerror(errno); 246 } 247 248 249 250 /* 251 * FUSE daemons are allowed to check permissions however they like. If the 252 * daemon returns EPERM, even if the file permissions "should" grant access, 253 * then fuse(4) should return EPERM too. 254 */ 255 TEST_F(Setattr, eperm) 256 { 257 const char FULLPATH[] = "mountpoint/some_file.txt"; 258 const char RELPATH[] = "some_file.txt"; 259 const uint64_t ino = 42; 260 261 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 262 .WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { 263 SET_OUT_HEADER_LEN(out, entry); 264 out.body.entry.attr.mode = S_IFREG | 0777; 265 out.body.entry.nodeid = ino; 266 out.body.entry.attr.uid = in.header.uid; 267 out.body.entry.attr.gid = in.header.gid; 268 }))); 269 270 EXPECT_CALL(*m_mock, process( 271 ResultOf([](auto in) { 272 return (in.header.opcode == FUSE_SETATTR && 273 in.header.nodeid == ino); 274 }, Eq(true)), 275 _) 276 ).WillOnce(Invoke(ReturnErrno(EPERM))); 277 EXPECT_NE(0, truncate(FULLPATH, 10)); 278 EXPECT_EQ(EPERM, errno); 279 } 280 281 /* Change the mode of an open file, by its file descriptor */ 282 TEST_F(Setattr, fchmod) 283 { 284 const char FULLPATH[] = "mountpoint/some_file.txt"; 285 const char RELPATH[] = "some_file.txt"; 286 uint64_t ino = 42; 287 int fd; 288 const mode_t oldmode = 0755; 289 const mode_t newmode = 0644; 290 291 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 292 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 293 SET_OUT_HEADER_LEN(out, entry); 294 out.body.entry.attr.mode = S_IFREG | oldmode; 295 out.body.entry.nodeid = ino; 296 out.body.entry.attr_valid = UINT64_MAX; 297 }))); 298 299 EXPECT_CALL(*m_mock, process( 300 ResultOf([=](auto in) { 301 return (in.header.opcode == FUSE_OPEN && 302 in.header.nodeid == ino); 303 }, Eq(true)), 304 _) 305 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 306 out.header.len = sizeof(out.header); 307 SET_OUT_HEADER_LEN(out, open); 308 }))); 309 310 EXPECT_CALL(*m_mock, process( 311 ResultOf([=](auto in) { 312 uint32_t valid = FATTR_MODE; 313 return (in.header.opcode == FUSE_SETATTR && 314 in.header.nodeid == ino && 315 in.body.setattr.valid == valid && 316 in.body.setattr.mode == newmode); 317 }, Eq(true)), 318 _) 319 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 320 SET_OUT_HEADER_LEN(out, attr); 321 out.body.attr.attr.ino = ino; // Must match nodeid 322 out.body.attr.attr.mode = S_IFREG | newmode; 323 }))); 324 325 fd = open(FULLPATH, O_RDONLY); 326 ASSERT_LE(0, fd) << strerror(errno); 327 ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno); 328 leak(fd); 329 } 330 331 /* Change the size of an open file, by its file descriptor */ 332 TEST_F(Setattr, ftruncate) 333 { 334 const char FULLPATH[] = "mountpoint/some_file.txt"; 335 const char RELPATH[] = "some_file.txt"; 336 uint64_t ino = 42; 337 int fd; 338 uint64_t fh = 0xdeadbeef1a7ebabe; 339 const off_t oldsize = 99; 340 const off_t newsize = 12345; 341 342 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 343 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 344 SET_OUT_HEADER_LEN(out, entry); 345 out.body.entry.attr.mode = S_IFREG | 0755; 346 out.body.entry.nodeid = ino; 347 out.body.entry.attr_valid = UINT64_MAX; 348 out.body.entry.attr.size = oldsize; 349 }))); 350 351 EXPECT_CALL(*m_mock, process( 352 ResultOf([=](auto in) { 353 return (in.header.opcode == FUSE_OPEN && 354 in.header.nodeid == ino); 355 }, Eq(true)), 356 _) 357 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 358 out.header.len = sizeof(out.header); 359 SET_OUT_HEADER_LEN(out, open); 360 out.body.open.fh = fh; 361 }))); 362 363 EXPECT_CALL(*m_mock, process( 364 ResultOf([=](auto in) { 365 uint32_t valid = FATTR_SIZE | FATTR_FH; 366 return (in.header.opcode == FUSE_SETATTR && 367 in.header.nodeid == ino && 368 in.body.setattr.valid == valid && 369 in.body.setattr.fh == fh); 370 }, Eq(true)), 371 _) 372 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 373 SET_OUT_HEADER_LEN(out, attr); 374 out.body.attr.attr.ino = ino; // Must match nodeid 375 out.body.attr.attr.mode = S_IFREG | 0755; 376 out.body.attr.attr.size = newsize; 377 }))); 378 379 fd = open(FULLPATH, O_RDWR); 380 ASSERT_LE(0, fd) << strerror(errno); 381 ASSERT_EQ(0, ftruncate(fd, newsize)) << strerror(errno); 382 leak(fd); 383 } 384 385 /* Change the size of the file */ 386 TEST_F(Setattr, truncate) { 387 const char FULLPATH[] = "mountpoint/some_file.txt"; 388 const char RELPATH[] = "some_file.txt"; 389 const uint64_t ino = 42; 390 const uint64_t oldsize = 100'000'000; 391 const uint64_t newsize = 20'000'000; 392 393 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 394 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 395 SET_OUT_HEADER_LEN(out, entry); 396 out.body.entry.attr.mode = S_IFREG | 0644; 397 out.body.entry.nodeid = ino; 398 out.body.entry.attr.size = oldsize; 399 }))); 400 401 EXPECT_CALL(*m_mock, process( 402 ResultOf([](auto in) { 403 uint32_t valid = FATTR_SIZE; 404 return (in.header.opcode == FUSE_SETATTR && 405 in.header.nodeid == ino && 406 in.body.setattr.valid == valid && 407 in.body.setattr.size == newsize); 408 }, Eq(true)), 409 _) 410 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { 411 SET_OUT_HEADER_LEN(out, attr); 412 out.body.attr.attr.ino = ino; // Must match nodeid 413 out.body.attr.attr.mode = S_IFREG | 0644; 414 out.body.attr.attr.size = newsize; 415 }))); 416 EXPECT_EQ(0, truncate(FULLPATH, newsize)) << strerror(errno); 417 } 418 419 /* 420 * Truncating a file should discard cached data past the truncation point. 421 * This is a regression test for bug 233783. 422 * 423 * There are two distinct failure modes. The first one is a failure to zero 424 * the portion of the file's final buffer past EOF. It can be reproduced by 425 * fsx -WR -P /tmp -S10 fsx.bin 426 * 427 * The second is a failure to drop buffers beyond that. It can be reproduced by 428 * fsx -WR -P /tmp -S18 -n fsx.bin 429 * Also reproducible in sh with: 430 * $> /path/to/libfuse/build/example/passthrough -d /tmp/mnt 431 * $> cd /tmp/mnt/tmp 432 * $> dd if=/dev/random of=randfile bs=1k count=192 433 * $> truncate -s 1k randfile && truncate -s 192k randfile 434 * $> xxd randfile | less # xxd will wrongly show random data at offset 0x8000 435 */ 436 TEST_F(Setattr, truncate_discards_cached_data) { 437 const char FULLPATH[] = "mountpoint/some_file.txt"; 438 const char RELPATH[] = "some_file.txt"; 439 void *w0buf, *r0buf, *r1buf, *expected; 440 off_t w0_offset = 0; 441 size_t w0_size = 0x30000; 442 off_t r0_offset = 0; 443 off_t r0_size = w0_size; 444 size_t trunc0_size = 0x400; 445 size_t trunc1_size = w0_size; 446 off_t r1_offset = trunc0_size; 447 off_t r1_size = w0_size - trunc0_size; 448 size_t cur_size = 0; 449 const uint64_t ino = 42; 450 mode_t mode = S_IFREG | 0644; 451 int fd, r; 452 bool should_have_data = false; 453 454 w0buf = malloc(w0_size); 455 ASSERT_NE(nullptr, w0buf) << strerror(errno); 456 memset(w0buf, 'X', w0_size); 457 458 r0buf = malloc(r0_size); 459 ASSERT_NE(nullptr, r0buf) << strerror(errno); 460 r1buf = malloc(r1_size); 461 ASSERT_NE(nullptr, r1buf) << strerror(errno); 462 463 expected = malloc(r1_size); 464 ASSERT_NE(nullptr, expected) << strerror(errno); 465 memset(expected, 0, r1_size); 466 467 expect_lookup(RELPATH, ino, mode, 0, 1); 468 expect_open(ino, O_RDWR, 1); 469 EXPECT_CALL(*m_mock, process( 470 ResultOf([=](auto in) { 471 return (in.header.opcode == FUSE_GETATTR && 472 in.header.nodeid == ino); 473 }, Eq(true)), 474 _) 475 ).WillRepeatedly(Invoke(ReturnImmediate([&](auto i __unused, auto& out) { 476 SET_OUT_HEADER_LEN(out, attr); 477 out.body.attr.attr.ino = ino; 478 out.body.attr.attr.mode = mode; 479 out.body.attr.attr.size = cur_size; 480 }))); 481 EXPECT_CALL(*m_mock, process( 482 ResultOf([=](auto in) { 483 return (in.header.opcode == FUSE_WRITE); 484 }, Eq(true)), 485 _) 486 ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) { 487 SET_OUT_HEADER_LEN(out, write); 488 out.body.attr.attr.ino = ino; 489 out.body.write.size = in.body.write.size; 490 cur_size = std::max(static_cast<uint64_t>(cur_size), 491 in.body.write.size + in.body.write.offset); 492 }))); 493 494 EXPECT_CALL(*m_mock, process( 495 ResultOf([=](auto in) { 496 return (in.header.opcode == FUSE_SETATTR && 497 in.header.nodeid == ino && 498 (in.body.setattr.valid & FATTR_SIZE)); 499 }, Eq(true)), 500 _) 501 ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) { 502 auto trunc_size = in.body.setattr.size; 503 SET_OUT_HEADER_LEN(out, attr); 504 out.body.attr.attr.ino = ino; 505 out.body.attr.attr.mode = mode; 506 out.body.attr.attr.size = trunc_size; 507 cur_size = trunc_size; 508 }))); 509 510 EXPECT_CALL(*m_mock, process( 511 ResultOf([=](auto in) { 512 return (in.header.opcode == FUSE_READ); 513 }, Eq(true)), 514 _) 515 ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) { 516 auto osize = std::min( 517 static_cast<uint64_t>(cur_size) - in.body.read.offset, 518 static_cast<uint64_t>(in.body.read.size)); 519 out.header.len = sizeof(struct fuse_out_header) + osize; 520 if (should_have_data) 521 memset(out.body.bytes, 'X', osize); 522 else 523 bzero(out.body.bytes, osize); 524 }))); 525 526 fd = open(FULLPATH, O_RDWR, 0644); 527 ASSERT_LE(0, fd) << strerror(errno); 528 529 /* Fill the file with Xs */ 530 ASSERT_EQ(static_cast<ssize_t>(w0_size), 531 pwrite(fd, w0buf, w0_size, w0_offset)); 532 should_have_data = true; 533 /* Fill the cache */ 534 ASSERT_EQ(static_cast<ssize_t>(r0_size), 535 pread(fd, r0buf, r0_size, r0_offset)); 536 /* 1st truncate should discard cached data */ 537 EXPECT_EQ(0, ftruncate(fd, trunc0_size)) << strerror(errno); 538 should_have_data = false; 539 /* 2nd truncate extends file into previously cached data */ 540 EXPECT_EQ(0, ftruncate(fd, trunc1_size)) << strerror(errno); 541 /* Read should return all zeros */ 542 ASSERT_EQ(static_cast<ssize_t>(r1_size), 543 pread(fd, r1buf, r1_size, r1_offset)); 544 545 r = memcmp(expected, r1buf, r1_size); 546 ASSERT_EQ(0, r); 547 548 free(expected); 549 free(r1buf); 550 free(r0buf); 551 free(w0buf); 552 553 leak(fd); 554 } 555 556 /* Change a file's timestamps */ 557 TEST_F(Setattr, utimensat) { 558 const char FULLPATH[] = "mountpoint/some_file.txt"; 559 const char RELPATH[] = "some_file.txt"; 560 const uint64_t ino = 42; 561 const timespec oldtimes[2] = { 562 {.tv_sec = 1, .tv_nsec = 2}, 563 {.tv_sec = 3, .tv_nsec = 4}, 564 }; 565 const timespec newtimes[2] = { 566 {.tv_sec = 5, .tv_nsec = 6}, 567 {.tv_sec = 7, .tv_nsec = 8}, 568 }; 569 570 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 571 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 572 SET_OUT_HEADER_LEN(out, entry); 573 out.body.entry.attr.mode = S_IFREG | 0644; 574 out.body.entry.nodeid = ino; 575 out.body.entry.attr_valid = UINT64_MAX; 576 out.body.entry.attr.atime = oldtimes[0].tv_sec; 577 out.body.entry.attr.atimensec = oldtimes[0].tv_nsec; 578 out.body.entry.attr.mtime = oldtimes[1].tv_sec; 579 out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec; 580 }))); 581 582 EXPECT_CALL(*m_mock, process( 583 ResultOf([=](auto in) { 584 uint32_t valid = FATTR_ATIME | FATTR_MTIME; 585 return (in.header.opcode == FUSE_SETATTR && 586 in.header.nodeid == ino && 587 in.body.setattr.valid == valid && 588 (time_t)in.body.setattr.atime == 589 newtimes[0].tv_sec && 590 (long)in.body.setattr.atimensec == 591 newtimes[0].tv_nsec && 592 (time_t)in.body.setattr.mtime == 593 newtimes[1].tv_sec && 594 (long)in.body.setattr.mtimensec == 595 newtimes[1].tv_nsec); 596 }, Eq(true)), 597 _) 598 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 599 SET_OUT_HEADER_LEN(out, attr); 600 out.body.attr.attr.ino = ino; // Must match nodeid 601 out.body.attr.attr.mode = S_IFREG | 0644; 602 out.body.attr.attr.atime = newtimes[0].tv_sec; 603 out.body.attr.attr.atimensec = newtimes[0].tv_nsec; 604 out.body.attr.attr.mtime = newtimes[1].tv_sec; 605 out.body.attr.attr.mtimensec = newtimes[1].tv_nsec; 606 }))); 607 EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0)) 608 << strerror(errno); 609 } 610 611 /* Change a file mtime but not its atime */ 612 TEST_F(Setattr, utimensat_mtime_only) { 613 const char FULLPATH[] = "mountpoint/some_file.txt"; 614 const char RELPATH[] = "some_file.txt"; 615 const uint64_t ino = 42; 616 const timespec oldtimes[2] = { 617 {.tv_sec = 1, .tv_nsec = 2}, 618 {.tv_sec = 3, .tv_nsec = 4}, 619 }; 620 const timespec newtimes[2] = { 621 {.tv_sec = 5, .tv_nsec = UTIME_OMIT}, 622 {.tv_sec = 7, .tv_nsec = 8}, 623 }; 624 625 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 626 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 627 SET_OUT_HEADER_LEN(out, entry); 628 out.body.entry.attr.mode = S_IFREG | 0644; 629 out.body.entry.nodeid = ino; 630 out.body.entry.attr_valid = UINT64_MAX; 631 out.body.entry.attr.atime = oldtimes[0].tv_sec; 632 out.body.entry.attr.atimensec = oldtimes[0].tv_nsec; 633 out.body.entry.attr.mtime = oldtimes[1].tv_sec; 634 out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec; 635 }))); 636 637 EXPECT_CALL(*m_mock, process( 638 ResultOf([=](auto in) { 639 uint32_t valid = FATTR_MTIME; 640 return (in.header.opcode == FUSE_SETATTR && 641 in.header.nodeid == ino && 642 in.body.setattr.valid == valid && 643 (time_t)in.body.setattr.mtime == 644 newtimes[1].tv_sec && 645 (long)in.body.setattr.mtimensec == 646 newtimes[1].tv_nsec); 647 }, Eq(true)), 648 _) 649 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 650 SET_OUT_HEADER_LEN(out, attr); 651 out.body.attr.attr.ino = ino; // Must match nodeid 652 out.body.attr.attr.mode = S_IFREG | 0644; 653 out.body.attr.attr.atime = oldtimes[0].tv_sec; 654 out.body.attr.attr.atimensec = oldtimes[0].tv_nsec; 655 out.body.attr.attr.mtime = newtimes[1].tv_sec; 656 out.body.attr.attr.mtimensec = newtimes[1].tv_nsec; 657 }))); 658 EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0)) 659 << strerror(errno); 660 } 661 662 /* 663 * Set a file's mtime and atime to now 664 * 665 * The design of FreeBSD's VFS does not allow fusefs to set just one of atime 666 * or mtime to UTIME_NOW; it's both or neither. 667 */ 668 TEST_F(Setattr, utimensat_utime_now) { 669 const char FULLPATH[] = "mountpoint/some_file.txt"; 670 const char RELPATH[] = "some_file.txt"; 671 const uint64_t ino = 42; 672 const timespec oldtimes[2] = { 673 {.tv_sec = 1, .tv_nsec = 2}, 674 {.tv_sec = 3, .tv_nsec = 4}, 675 }; 676 const timespec newtimes[2] = { 677 {.tv_sec = 0, .tv_nsec = UTIME_NOW}, 678 {.tv_sec = 0, .tv_nsec = UTIME_NOW}, 679 }; 680 /* "now" is whatever the server says it is */ 681 const timespec now[2] = { 682 {.tv_sec = 5, .tv_nsec = 7}, 683 {.tv_sec = 6, .tv_nsec = 8}, 684 }; 685 struct stat sb; 686 687 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 688 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 689 SET_OUT_HEADER_LEN(out, entry); 690 out.body.entry.attr.mode = S_IFREG | 0644; 691 out.body.entry.nodeid = ino; 692 out.body.entry.attr_valid = UINT64_MAX; 693 out.body.entry.entry_valid = UINT64_MAX; 694 out.body.entry.attr.atime = oldtimes[0].tv_sec; 695 out.body.entry.attr.atimensec = oldtimes[0].tv_nsec; 696 out.body.entry.attr.mtime = oldtimes[1].tv_sec; 697 out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec; 698 }))); 699 700 EXPECT_CALL(*m_mock, process( 701 ResultOf([=](auto in) { 702 uint32_t valid = FATTR_ATIME | FATTR_ATIME_NOW | 703 FATTR_MTIME | FATTR_MTIME_NOW; 704 return (in.header.opcode == FUSE_SETATTR && 705 in.header.nodeid == ino && 706 in.body.setattr.valid == valid); 707 }, Eq(true)), 708 _) 709 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 710 SET_OUT_HEADER_LEN(out, attr); 711 out.body.attr.attr.ino = ino; // Must match nodeid 712 out.body.attr.attr.mode = S_IFREG | 0644; 713 out.body.attr.attr.atime = now[0].tv_sec; 714 out.body.attr.attr.atimensec = now[0].tv_nsec; 715 out.body.attr.attr.mtime = now[1].tv_sec; 716 out.body.attr.attr.mtimensec = now[1].tv_nsec; 717 out.body.attr.attr_valid = UINT64_MAX; 718 }))); 719 ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0)) 720 << strerror(errno); 721 ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); 722 EXPECT_EQ(now[0].tv_sec, sb.st_atim.tv_sec); 723 EXPECT_EQ(now[0].tv_nsec, sb.st_atim.tv_nsec); 724 EXPECT_EQ(now[1].tv_sec, sb.st_mtim.tv_sec); 725 EXPECT_EQ(now[1].tv_nsec, sb.st_mtim.tv_nsec); 726 } 727 728 /* 729 * FUSE_SETATTR returns a different file type, even though the entry cache 730 * hasn't expired. This is a server bug! It probably means that the server 731 * removed the file and recreated it with the same inode but a different vtyp. 732 * The best thing fusefs can do is return ENOENT to the caller. After all, the 733 * entry must not have existed recently. 734 */ 735 TEST_F(Setattr, vtyp_conflict) 736 { 737 const char FULLPATH[] = "mountpoint/some_file.txt"; 738 const char RELPATH[] = "some_file.txt"; 739 const uint64_t ino = 42; 740 uid_t newuser = 12345; 741 sem_t sem; 742 743 ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); 744 745 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 746 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 747 SET_OUT_HEADER_LEN(out, entry); 748 out.body.entry.attr.mode = S_IFREG | 0777; 749 out.body.entry.nodeid = ino; 750 out.body.entry.entry_valid = UINT64_MAX; 751 }))); 752 753 EXPECT_CALL(*m_mock, process( 754 ResultOf([](auto in) { 755 return (in.header.opcode == FUSE_SETATTR && 756 in.header.nodeid == ino); 757 }, Eq(true)), 758 _) 759 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 760 SET_OUT_HEADER_LEN(out, attr); 761 out.body.attr.attr.ino = ino; 762 out.body.attr.attr.mode = S_IFDIR | 0777; // Changed! 763 out.body.attr.attr.uid = newuser; 764 }))); 765 // We should reclaim stale vnodes 766 expect_forget(ino, 1, &sem); 767 768 EXPECT_NE(0, chown(FULLPATH, newuser, -1)); 769 EXPECT_EQ(ENOENT, errno); 770 771 sem_wait(&sem); 772 sem_destroy(&sem); 773 } 774 775 /* On a read-only mount, no attributes may be changed */ 776 TEST_F(RofsSetattr, erofs) 777 { 778 const char FULLPATH[] = "mountpoint/some_file.txt"; 779 const char RELPATH[] = "some_file.txt"; 780 const uint64_t ino = 42; 781 const mode_t oldmode = 0755; 782 const mode_t newmode = 0644; 783 784 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 785 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 786 SET_OUT_HEADER_LEN(out, entry); 787 out.body.entry.attr.mode = S_IFREG | oldmode; 788 out.body.entry.nodeid = ino; 789 }))); 790 791 ASSERT_EQ(-1, chmod(FULLPATH, newmode)); 792 ASSERT_EQ(EROFS, errno); 793 } 794 795 /* Change the mode of a file */ 796 TEST_F(Setattr_7_8, chmod) 797 { 798 const char FULLPATH[] = "mountpoint/some_file.txt"; 799 const char RELPATH[] = "some_file.txt"; 800 const uint64_t ino = 42; 801 const mode_t oldmode = 0755; 802 const mode_t newmode = 0644; 803 804 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 805 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 806 SET_OUT_HEADER_LEN(out, entry_7_8); 807 out.body.entry.attr.mode = S_IFREG | oldmode; 808 out.body.entry.nodeid = ino; 809 }))); 810 811 EXPECT_CALL(*m_mock, process( 812 ResultOf([](auto in) { 813 uint32_t valid = FATTR_MODE; 814 return (in.header.opcode == FUSE_SETATTR && 815 in.header.nodeid == ino && 816 in.body.setattr.valid == valid && 817 in.body.setattr.mode == newmode); 818 }, Eq(true)), 819 _) 820 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { 821 SET_OUT_HEADER_LEN(out, attr_7_8); 822 out.body.attr.attr.ino = ino; // Must match nodeid 823 out.body.attr.attr.mode = S_IFREG | newmode; 824 }))); 825 EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); 826 } 827