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