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