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