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 extern "C" { 32 #include <sys/param.h> 33 #include <sys/mount.h> 34 35 #include <fcntl.h> 36 #include <unistd.h> 37 } 38 39 #include "mockfs.hh" 40 #include "utils.hh" 41 42 using namespace testing; 43 44 class Lookup: public FuseTest {}; 45 46 class Lookup_7_8: public Lookup { 47 public: 48 virtual void SetUp() { 49 m_kernel_minor_version = 8; 50 Lookup::SetUp(); 51 } 52 }; 53 54 class LookupExportable: public Lookup { 55 public: 56 virtual void SetUp() { 57 m_init_flags = FUSE_EXPORT_SUPPORT; 58 Lookup::SetUp(); 59 } 60 }; 61 62 /* 63 * If lookup returns a non-zero cache timeout, then subsequent VOP_GETATTRs 64 * should use the cached attributes, rather than query the daemon 65 */ 66 TEST_F(Lookup, attr_cache) 67 { 68 const char FULLPATH[] = "mountpoint/some_file.txt"; 69 const char RELPATH[] = "some_file.txt"; 70 const uint64_t ino = 42; 71 const uint64_t generation = 13; 72 struct stat sb; 73 74 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 75 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 76 SET_OUT_HEADER_LEN(out, entry); 77 out.body.entry.nodeid = ino; 78 out.body.entry.attr_valid = UINT64_MAX; 79 out.body.entry.attr.ino = ino; // Must match nodeid 80 out.body.entry.attr.mode = S_IFREG | 0644; 81 out.body.entry.attr.size = 1; 82 out.body.entry.attr.blocks = 2; 83 out.body.entry.attr.atime = 3; 84 out.body.entry.attr.mtime = 4; 85 out.body.entry.attr.ctime = 5; 86 out.body.entry.attr.atimensec = 6; 87 out.body.entry.attr.mtimensec = 7; 88 out.body.entry.attr.ctimensec = 8; 89 out.body.entry.attr.nlink = 9; 90 out.body.entry.attr.uid = 10; 91 out.body.entry.attr.gid = 11; 92 out.body.entry.attr.rdev = 12; 93 out.body.entry.generation = generation; 94 }))); 95 /* stat(2) issues a VOP_LOOKUP followed by a VOP_GETATTR */ 96 ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); 97 EXPECT_EQ(1, sb.st_size); 98 EXPECT_EQ(2, sb.st_blocks); 99 EXPECT_EQ(3, sb.st_atim.tv_sec); 100 EXPECT_EQ(6, sb.st_atim.tv_nsec); 101 EXPECT_EQ(4, sb.st_mtim.tv_sec); 102 EXPECT_EQ(7, sb.st_mtim.tv_nsec); 103 EXPECT_EQ(5, sb.st_ctim.tv_sec); 104 EXPECT_EQ(8, sb.st_ctim.tv_nsec); 105 EXPECT_EQ(9ull, sb.st_nlink); 106 EXPECT_EQ(10ul, sb.st_uid); 107 EXPECT_EQ(11ul, sb.st_gid); 108 EXPECT_EQ(12ul, sb.st_rdev); 109 EXPECT_EQ(ino, sb.st_ino); 110 EXPECT_EQ(S_IFREG | 0644, sb.st_mode); 111 112 // fuse(4) does not _yet_ support inode generations 113 //EXPECT_EQ(generation, sb.st_gen); 114 115 //st_birthtim and st_flags are not supported by protocol 7.8. They're 116 //only supported as OS-specific extensions to OSX. 117 //EXPECT_EQ(, sb.st_birthtim); 118 //EXPECT_EQ(, sb.st_flags); 119 120 //FUSE can't set st_blksize until protocol 7.9 121 } 122 123 /* 124 * If lookup returns a finite but non-zero cache timeout, then we should discard 125 * the cached attributes and requery the daemon. 126 */ 127 TEST_F(Lookup, attr_cache_timeout) 128 { 129 const char FULLPATH[] = "mountpoint/some_file.txt"; 130 const char RELPATH[] = "some_file.txt"; 131 const uint64_t ino = 42; 132 struct stat sb; 133 134 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 135 .Times(2) 136 .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 137 SET_OUT_HEADER_LEN(out, entry); 138 out.body.entry.nodeid = ino; 139 out.body.entry.attr_valid_nsec = NAP_NS / 2; 140 out.body.entry.attr.ino = ino; // Must match nodeid 141 out.body.entry.attr.mode = S_IFREG | 0644; 142 }))); 143 144 /* access(2) will issue a VOP_LOOKUP and fill the attr cache */ 145 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 146 /* Next access(2) will use the cached attributes */ 147 nap(); 148 /* The cache has timed out; VOP_GETATTR should query the daemon*/ 149 ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); 150 } 151 152 TEST_F(Lookup, dot) 153 { 154 const char FULLPATH[] = "mountpoint/some_dir/."; 155 const char RELDIRPATH[] = "some_dir"; 156 uint64_t ino = 42; 157 158 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 159 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 160 SET_OUT_HEADER_LEN(out, entry); 161 out.body.entry.attr.mode = S_IFDIR | 0755; 162 out.body.entry.nodeid = ino; 163 out.body.entry.attr_valid = UINT64_MAX; 164 out.body.entry.entry_valid = UINT64_MAX; 165 }))); 166 167 /* 168 * access(2) is one of the few syscalls that will not (always) follow 169 * up a successful VOP_LOOKUP with another VOP. 170 */ 171 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 172 } 173 174 TEST_F(Lookup, dotdot) 175 { 176 const char FULLPATH[] = "mountpoint/some_dir/.."; 177 const char RELDIRPATH[] = "some_dir"; 178 179 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 180 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 181 SET_OUT_HEADER_LEN(out, entry); 182 out.body.entry.attr.mode = S_IFDIR | 0755; 183 out.body.entry.nodeid = 14; 184 out.body.entry.attr_valid = UINT64_MAX; 185 out.body.entry.entry_valid = UINT64_MAX; 186 }))); 187 188 /* 189 * access(2) is one of the few syscalls that will not (always) follow 190 * up a successful VOP_LOOKUP with another VOP. 191 */ 192 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 193 } 194 195 /* 196 * Lookup ".." when that vnode's entry cache has timed out, but its child's 197 * hasn't. Since this file system doesn't set FUSE_EXPORT_SUPPORT, we have no 198 * choice but to use the cached entry, even though it expired. 199 */ 200 TEST_F(Lookup, dotdot_entry_cache_timeout) 201 { 202 uint64_t foo_ino = 42; 203 uint64_t bar_ino = 43; 204 205 EXPECT_LOOKUP(FUSE_ROOT_ID, "foo") 206 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 207 SET_OUT_HEADER_LEN(out, entry); 208 out.body.entry.attr.mode = S_IFDIR | 0755; 209 out.body.entry.nodeid = foo_ino; 210 out.body.entry.attr_valid = UINT64_MAX; 211 out.body.entry.entry_valid = 0; // immediate timeout 212 }))); 213 EXPECT_LOOKUP(foo_ino, "bar") 214 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 215 SET_OUT_HEADER_LEN(out, entry); 216 out.body.entry.attr.mode = S_IFDIR | 0755; 217 out.body.entry.nodeid = bar_ino; 218 out.body.entry.attr_valid = UINT64_MAX; 219 out.body.entry.entry_valid = UINT64_MAX; 220 }))); 221 expect_opendir(bar_ino); 222 223 int fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY); 224 ASSERT_LE(0, fd) << strerror(errno); 225 EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno); 226 } 227 228 /* 229 * Lookup ".." for a vnode with no valid parent nid 230 * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259974 231 * Since the file system is not exportable, we have no choice but to return an 232 * error. 233 */ 234 TEST_F(Lookup, dotdot_no_parent_nid) 235 { 236 uint64_t foo_ino = 42; 237 uint64_t bar_ino = 43; 238 int fd; 239 240 EXPECT_LOOKUP(FUSE_ROOT_ID, "foo") 241 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 242 SET_OUT_HEADER_LEN(out, entry); 243 out.body.entry.attr.mode = S_IFDIR | 0755; 244 out.body.entry.nodeid = foo_ino; 245 out.body.entry.attr_valid = UINT64_MAX; 246 out.body.entry.entry_valid = UINT64_MAX; 247 }))); 248 EXPECT_LOOKUP(foo_ino, "bar") 249 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 250 SET_OUT_HEADER_LEN(out, entry); 251 out.body.entry.attr.mode = S_IFDIR | 0755; 252 out.body.entry.nodeid = bar_ino; 253 out.body.entry.attr_valid = UINT64_MAX; 254 out.body.entry.entry_valid = UINT64_MAX; 255 }))); 256 EXPECT_CALL(*m_mock, process( 257 ResultOf([=](auto in) { 258 return (in.header.opcode == FUSE_OPENDIR); 259 }, Eq(true)), 260 _) 261 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 262 SET_OUT_HEADER_LEN(out, open); 263 }))); 264 expect_forget(foo_ino, 1, NULL); 265 266 fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY); 267 ASSERT_LE(0, fd) << strerror(errno); 268 // Try (and fail) to unmount the file system, to reclaim the mountpoint 269 // and foo vnodes. 270 ASSERT_NE(0, unmount("mountpoint", 0)); 271 EXPECT_EQ(EBUSY, errno); 272 nap(); // Because vnode reclamation is asynchronous 273 EXPECT_NE(0, faccessat(fd, "../..", F_OK, 0)); 274 EXPECT_EQ(ESTALE, errno); 275 } 276 277 /* 278 * A daemon that returns an illegal error value should be handled gracefully. 279 * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=263220 280 */ 281 TEST_F(Lookup, ejustreturn) 282 { 283 const char FULLPATH[] = "mountpoint/does_not_exist"; 284 const char RELPATH[] = "does_not_exist"; 285 286 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 287 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 288 out.header.len = sizeof(out.header); 289 out.header.error = 2; 290 out.expected_errno = EINVAL; 291 }))); 292 293 EXPECT_NE(0, access(FULLPATH, F_OK)); 294 295 EXPECT_EQ(EIO, errno); 296 } 297 298 TEST_F(Lookup, enoent) 299 { 300 const char FULLPATH[] = "mountpoint/does_not_exist"; 301 const char RELPATH[] = "does_not_exist"; 302 303 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 304 .WillOnce(Invoke(ReturnErrno(ENOENT))); 305 EXPECT_NE(0, access(FULLPATH, F_OK)); 306 EXPECT_EQ(ENOENT, errno); 307 } 308 309 TEST_F(Lookup, enotdir) 310 { 311 const char FULLPATH[] = "mountpoint/not_a_dir/some_file.txt"; 312 const char RELPATH[] = "not_a_dir"; 313 314 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 315 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 316 SET_OUT_HEADER_LEN(out, entry); 317 out.body.entry.entry_valid = UINT64_MAX; 318 out.body.entry.attr.mode = S_IFREG | 0644; 319 out.body.entry.nodeid = 42; 320 }))); 321 322 ASSERT_EQ(-1, access(FULLPATH, F_OK)); 323 ASSERT_EQ(ENOTDIR, errno); 324 } 325 326 /* 327 * If lookup returns a non-zero entry timeout, then subsequent VOP_LOOKUPs 328 * should use the cached inode rather than requery the daemon 329 */ 330 TEST_F(Lookup, entry_cache) 331 { 332 const char FULLPATH[] = "mountpoint/some_file.txt"; 333 const char RELPATH[] = "some_file.txt"; 334 335 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 336 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 337 SET_OUT_HEADER_LEN(out, entry); 338 out.body.entry.entry_valid = UINT64_MAX; 339 out.body.entry.attr.mode = S_IFREG | 0644; 340 out.body.entry.nodeid = 14; 341 }))); 342 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 343 /* The second access(2) should use the cache */ 344 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 345 } 346 347 /* 348 * If the daemon returns an error of 0 and an inode of 0, that's a flag for 349 * "ENOENT and cache it" with the given entry_timeout 350 */ 351 TEST_F(Lookup, entry_cache_negative) 352 { 353 struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0}; 354 355 EXPECT_LOOKUP(FUSE_ROOT_ID, "does_not_exist") 356 .Times(1) 357 .WillOnce(Invoke(ReturnNegativeCache(&entry_valid))); 358 359 EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK)); 360 EXPECT_EQ(ENOENT, errno); 361 EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK)); 362 EXPECT_EQ(ENOENT, errno); 363 } 364 365 /* Negative entry caches should timeout, too */ 366 TEST_F(Lookup, entry_cache_negative_timeout) 367 { 368 const char *RELPATH = "does_not_exist"; 369 const char *FULLPATH = "mountpoint/does_not_exist"; 370 struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = NAP_NS / 2}; 371 372 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 373 .Times(2) 374 .WillRepeatedly(Invoke(ReturnNegativeCache(&entry_valid))); 375 376 EXPECT_NE(0, access(FULLPATH, F_OK)); 377 EXPECT_EQ(ENOENT, errno); 378 379 nap(); 380 381 /* The cache has timed out; VOP_LOOKUP should requery the daemon*/ 382 EXPECT_NE(0, access(FULLPATH, F_OK)); 383 EXPECT_EQ(ENOENT, errno); 384 } 385 386 /* 387 * If lookup returns a finite but non-zero entry cache timeout, then we should 388 * discard the cached inode and requery the daemon 389 */ 390 TEST_F(Lookup, entry_cache_timeout) 391 { 392 const char FULLPATH[] = "mountpoint/some_file.txt"; 393 const char RELPATH[] = "some_file.txt"; 394 395 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 396 .Times(2) 397 .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 398 SET_OUT_HEADER_LEN(out, entry); 399 out.body.entry.entry_valid_nsec = NAP_NS / 2; 400 out.body.entry.attr.mode = S_IFREG | 0644; 401 out.body.entry.nodeid = 14; 402 }))); 403 404 /* access(2) will issue a VOP_LOOKUP and fill the entry cache */ 405 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 406 /* Next access(2) will use the cached entry */ 407 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 408 nap(); 409 /* The cache has timed out; VOP_LOOKUP should requery the daemon*/ 410 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 411 } 412 413 TEST_F(Lookup, ok) 414 { 415 const char FULLPATH[] = "mountpoint/some_file.txt"; 416 const char RELPATH[] = "some_file.txt"; 417 418 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 419 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 420 SET_OUT_HEADER_LEN(out, entry); 421 out.body.entry.attr.mode = S_IFREG | 0644; 422 out.body.entry.nodeid = 14; 423 }))); 424 /* 425 * access(2) is one of the few syscalls that will not (always) follow 426 * up a successful VOP_LOOKUP with another VOP. 427 */ 428 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 429 } 430 431 /* 432 * Lookup in a subdirectory of the fuse mount. The naughty server returns the 433 * same inode for the child as for the parent. 434 */ 435 TEST_F(Lookup, parent_inode) 436 { 437 const char FULLPATH[] = "mountpoint/some_dir/some_file.txt"; 438 const char DIRPATH[] = "some_dir"; 439 const char RELPATH[] = "some_file.txt"; 440 uint64_t dir_ino = 2; 441 442 EXPECT_LOOKUP(FUSE_ROOT_ID, DIRPATH) 443 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 444 SET_OUT_HEADER_LEN(out, entry); 445 out.body.entry.attr.mode = S_IFDIR | 0755; 446 out.body.entry.nodeid = dir_ino; 447 }))); 448 EXPECT_LOOKUP(dir_ino, RELPATH) 449 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 450 SET_OUT_HEADER_LEN(out, entry); 451 out.body.entry.attr.mode = S_IFREG | 0644; 452 out.body.entry.nodeid = dir_ino; 453 }))); 454 /* 455 * access(2) is one of the few syscalls that will not (always) follow 456 * up a successful VOP_LOOKUP with another VOP. 457 */ 458 ASSERT_EQ(-1, access(FULLPATH, F_OK)); 459 ASSERT_EQ(EIO, errno); 460 } 461 462 // Lookup in a subdirectory of the fuse mount 463 TEST_F(Lookup, subdir) 464 { 465 const char FULLPATH[] = "mountpoint/some_dir/some_file.txt"; 466 const char DIRPATH[] = "some_dir"; 467 const char RELPATH[] = "some_file.txt"; 468 uint64_t dir_ino = 2; 469 uint64_t file_ino = 3; 470 471 EXPECT_LOOKUP(FUSE_ROOT_ID, DIRPATH) 472 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 473 SET_OUT_HEADER_LEN(out, entry); 474 out.body.entry.attr.mode = S_IFDIR | 0755; 475 out.body.entry.nodeid = dir_ino; 476 }))); 477 EXPECT_LOOKUP(dir_ino, RELPATH) 478 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 479 SET_OUT_HEADER_LEN(out, entry); 480 out.body.entry.attr.mode = S_IFREG | 0644; 481 out.body.entry.nodeid = file_ino; 482 }))); 483 /* 484 * access(2) is one of the few syscalls that will not (always) follow 485 * up a successful VOP_LOOKUP with another VOP. 486 */ 487 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 488 } 489 490 /* 491 * The server returns two different vtypes for the same nodeid. This is 492 * technically allowed if the entry's cache has already expired. 493 * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=258022 494 */ 495 TEST_F(Lookup, vtype_conflict) 496 { 497 const char FIRSTFULLPATH[] = "mountpoint/foo"; 498 const char SECONDFULLPATH[] = "mountpoint/bar"; 499 const char FIRSTRELPATH[] = "foo"; 500 const char SECONDRELPATH[] = "bar"; 501 uint64_t ino = 42; 502 503 EXPECT_LOOKUP(FUSE_ROOT_ID, FIRSTRELPATH) 504 .WillOnce(Invoke( 505 ReturnImmediate([=](auto in __unused, auto& out) { 506 SET_OUT_HEADER_LEN(out, entry); 507 out.body.entry.attr.mode = S_IFDIR | 0644; 508 out.body.entry.nodeid = ino; 509 out.body.entry.attr.nlink = 1; 510 }))); 511 expect_lookup(SECONDRELPATH, ino, S_IFREG | 0755, 0, 1, UINT64_MAX); 512 // VOP_FORGET happens asynchronously, so it may or may not arrive 513 // before the test completes. 514 EXPECT_CALL(*m_mock, process( 515 ResultOf([=](auto in) { 516 return (in.header.opcode == FUSE_FORGET && 517 in.header.nodeid == ino && 518 in.body.forget.nlookup == 1); 519 }, Eq(true)), 520 _) 521 ).Times(AtMost(1)) 522 .WillOnce(Invoke([=](auto in __unused, auto &out __unused) { })); 523 524 ASSERT_EQ(0, access(FIRSTFULLPATH, F_OK)) << strerror(errno); 525 EXPECT_EQ(0, access(SECONDFULLPATH, F_OK)) << strerror(errno); 526 } 527 528 TEST_F(Lookup_7_8, ok) 529 { 530 const char FULLPATH[] = "mountpoint/some_file.txt"; 531 const char RELPATH[] = "some_file.txt"; 532 533 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 534 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 535 SET_OUT_HEADER_LEN(out, entry_7_8); 536 out.body.entry.attr.mode = S_IFREG | 0644; 537 out.body.entry.nodeid = 14; 538 }))); 539 /* 540 * access(2) is one of the few syscalls that will not (always) follow 541 * up a successful VOP_LOOKUP with another VOP. 542 */ 543 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 544 } 545 546 /* 547 * Lookup ".." when that vnode's entry cache has timed out, but its child's 548 * hasn't. 549 */ 550 TEST_F(LookupExportable, dotdot_entry_cache_timeout) 551 { 552 uint64_t foo_ino = 42; 553 uint64_t bar_ino = 43; 554 555 EXPECT_LOOKUP(FUSE_ROOT_ID, "foo") 556 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 557 SET_OUT_HEADER_LEN(out, entry); 558 out.body.entry.attr.mode = S_IFDIR | 0755; 559 out.body.entry.nodeid = foo_ino; 560 out.body.entry.attr_valid = UINT64_MAX; 561 out.body.entry.entry_valid = 0; // immediate timeout 562 }))); 563 EXPECT_LOOKUP(foo_ino, "bar") 564 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 565 SET_OUT_HEADER_LEN(out, entry); 566 out.body.entry.attr.mode = S_IFDIR | 0755; 567 out.body.entry.nodeid = bar_ino; 568 out.body.entry.attr_valid = UINT64_MAX; 569 out.body.entry.entry_valid = UINT64_MAX; 570 }))); 571 expect_opendir(bar_ino); 572 EXPECT_LOOKUP(foo_ino, "..") 573 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 574 SET_OUT_HEADER_LEN(out, entry); 575 out.body.entry.attr.mode = S_IFDIR | 0755; 576 out.body.entry.nodeid = FUSE_ROOT_ID; 577 out.body.entry.attr_valid = UINT64_MAX; 578 out.body.entry.entry_valid = UINT64_MAX; 579 }))); 580 581 int fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY); 582 ASSERT_LE(0, fd) << strerror(errno); 583 /* FreeBSD's fusefs driver always uses the same cache expiration time 584 * for ".." as for the directory itself. So we need to look up two 585 * levels to find an expired ".." cache entry. 586 */ 587 EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno); 588 } 589 590 /* 591 * Lookup ".." for a vnode with no valid parent nid 592 * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259974 593 * Since the file system is exportable, we should resolve the problem by 594 * sending a FUSE_LOOKUP for "..". 595 */ 596 TEST_F(LookupExportable, dotdot_no_parent_nid) 597 { 598 uint64_t foo_ino = 42; 599 uint64_t bar_ino = 43; 600 int fd; 601 602 EXPECT_LOOKUP(FUSE_ROOT_ID, "foo") 603 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 604 SET_OUT_HEADER_LEN(out, entry); 605 out.body.entry.attr.mode = S_IFDIR | 0755; 606 out.body.entry.nodeid = foo_ino; 607 out.body.entry.attr_valid = UINT64_MAX; 608 out.body.entry.entry_valid = UINT64_MAX; 609 }))); 610 EXPECT_LOOKUP(foo_ino, "bar") 611 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 612 SET_OUT_HEADER_LEN(out, entry); 613 out.body.entry.attr.mode = S_IFDIR | 0755; 614 out.body.entry.nodeid = bar_ino; 615 out.body.entry.attr_valid = UINT64_MAX; 616 out.body.entry.entry_valid = UINT64_MAX; 617 }))); 618 EXPECT_CALL(*m_mock, process( 619 ResultOf([=](auto in) { 620 return (in.header.opcode == FUSE_OPENDIR); 621 }, Eq(true)), 622 _) 623 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 624 SET_OUT_HEADER_LEN(out, open); 625 }))); 626 expect_forget(foo_ino, 1, NULL); 627 EXPECT_LOOKUP(bar_ino, "..") 628 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 629 SET_OUT_HEADER_LEN(out, entry); 630 out.body.entry.attr.mode = S_IFDIR | 0755; 631 out.body.entry.nodeid = foo_ino; 632 out.body.entry.attr_valid = UINT64_MAX; 633 out.body.entry.entry_valid = UINT64_MAX; 634 }))); 635 EXPECT_LOOKUP(foo_ino, "..") 636 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 637 SET_OUT_HEADER_LEN(out, entry); 638 out.body.entry.attr.mode = S_IFDIR | 0755; 639 out.body.entry.nodeid = FUSE_ROOT_ID; 640 out.body.entry.attr_valid = UINT64_MAX; 641 out.body.entry.entry_valid = UINT64_MAX; 642 }))); 643 644 fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY); 645 ASSERT_LE(0, fd) << strerror(errno); 646 // Try (and fail) to unmount the file system, to reclaim the mountpoint 647 // and foo vnodes. 648 ASSERT_NE(0, unmount("mountpoint", 0)); 649 EXPECT_EQ(EBUSY, errno); 650 nap(); // Because vnode reclamation is asynchronous 651 EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno); 652 } 653