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