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 TEST_F(Lookup, enoent) 280 { 281 const char FULLPATH[] = "mountpoint/does_not_exist"; 282 const char RELPATH[] = "does_not_exist"; 283 284 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 285 .WillOnce(Invoke(ReturnErrno(ENOENT))); 286 EXPECT_NE(0, access(FULLPATH, F_OK)); 287 EXPECT_EQ(ENOENT, errno); 288 } 289 290 TEST_F(Lookup, enotdir) 291 { 292 const char FULLPATH[] = "mountpoint/not_a_dir/some_file.txt"; 293 const char RELPATH[] = "not_a_dir"; 294 295 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 296 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 297 SET_OUT_HEADER_LEN(out, entry); 298 out.body.entry.entry_valid = UINT64_MAX; 299 out.body.entry.attr.mode = S_IFREG | 0644; 300 out.body.entry.nodeid = 42; 301 }))); 302 303 ASSERT_EQ(-1, access(FULLPATH, F_OK)); 304 ASSERT_EQ(ENOTDIR, errno); 305 } 306 307 /* 308 * If lookup returns a non-zero entry timeout, then subsequent VOP_LOOKUPs 309 * should use the cached inode rather than requery the daemon 310 */ 311 TEST_F(Lookup, entry_cache) 312 { 313 const char FULLPATH[] = "mountpoint/some_file.txt"; 314 const char RELPATH[] = "some_file.txt"; 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 = 14; 322 }))); 323 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 324 /* The second access(2) should use the cache */ 325 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 326 } 327 328 /* 329 * If the daemon returns an error of 0 and an inode of 0, that's a flag for 330 * "ENOENT and cache it" with the given entry_timeout 331 */ 332 TEST_F(Lookup, entry_cache_negative) 333 { 334 struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0}; 335 336 EXPECT_LOOKUP(FUSE_ROOT_ID, "does_not_exist") 337 .Times(1) 338 .WillOnce(Invoke(ReturnNegativeCache(&entry_valid))); 339 340 EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK)); 341 EXPECT_EQ(ENOENT, errno); 342 EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK)); 343 EXPECT_EQ(ENOENT, errno); 344 } 345 346 /* Negative entry caches should timeout, too */ 347 TEST_F(Lookup, entry_cache_negative_timeout) 348 { 349 const char *RELPATH = "does_not_exist"; 350 const char *FULLPATH = "mountpoint/does_not_exist"; 351 struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = NAP_NS / 2}; 352 353 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 354 .Times(2) 355 .WillRepeatedly(Invoke(ReturnNegativeCache(&entry_valid))); 356 357 EXPECT_NE(0, access(FULLPATH, F_OK)); 358 EXPECT_EQ(ENOENT, errno); 359 360 nap(); 361 362 /* The cache has timed out; VOP_LOOKUP should requery the daemon*/ 363 EXPECT_NE(0, access(FULLPATH, F_OK)); 364 EXPECT_EQ(ENOENT, errno); 365 } 366 367 /* 368 * If lookup returns a finite but non-zero entry cache timeout, then we should 369 * discard the cached inode and requery the daemon 370 */ 371 TEST_F(Lookup, entry_cache_timeout) 372 { 373 const char FULLPATH[] = "mountpoint/some_file.txt"; 374 const char RELPATH[] = "some_file.txt"; 375 376 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 377 .Times(2) 378 .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 379 SET_OUT_HEADER_LEN(out, entry); 380 out.body.entry.entry_valid_nsec = NAP_NS / 2; 381 out.body.entry.attr.mode = S_IFREG | 0644; 382 out.body.entry.nodeid = 14; 383 }))); 384 385 /* access(2) will issue a VOP_LOOKUP and fill the entry cache */ 386 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 387 /* Next access(2) will use the cached entry */ 388 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 389 nap(); 390 /* The cache has timed out; VOP_LOOKUP should requery the daemon*/ 391 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 392 } 393 394 TEST_F(Lookup, ok) 395 { 396 const char FULLPATH[] = "mountpoint/some_file.txt"; 397 const char RELPATH[] = "some_file.txt"; 398 399 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 400 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 401 SET_OUT_HEADER_LEN(out, entry); 402 out.body.entry.attr.mode = S_IFREG | 0644; 403 out.body.entry.nodeid = 14; 404 }))); 405 /* 406 * access(2) is one of the few syscalls that will not (always) follow 407 * up a successful VOP_LOOKUP with another VOP. 408 */ 409 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 410 } 411 412 // Lookup in a subdirectory of the fuse mount 413 TEST_F(Lookup, subdir) 414 { 415 const char FULLPATH[] = "mountpoint/some_dir/some_file.txt"; 416 const char DIRPATH[] = "some_dir"; 417 const char RELPATH[] = "some_file.txt"; 418 uint64_t dir_ino = 2; 419 uint64_t file_ino = 3; 420 421 EXPECT_LOOKUP(FUSE_ROOT_ID, DIRPATH) 422 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 423 SET_OUT_HEADER_LEN(out, entry); 424 out.body.entry.attr.mode = S_IFDIR | 0755; 425 out.body.entry.nodeid = dir_ino; 426 }))); 427 EXPECT_LOOKUP(dir_ino, RELPATH) 428 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 429 SET_OUT_HEADER_LEN(out, entry); 430 out.body.entry.attr.mode = S_IFREG | 0644; 431 out.body.entry.nodeid = file_ino; 432 }))); 433 /* 434 * access(2) is one of the few syscalls that will not (always) follow 435 * up a successful VOP_LOOKUP with another VOP. 436 */ 437 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 438 } 439 440 /* 441 * The server returns two different vtypes for the same nodeid. This is 442 * technically allowed if the entry's cache has already expired. 443 * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=258022 444 */ 445 TEST_F(Lookup, vtype_conflict) 446 { 447 const char FIRSTFULLPATH[] = "mountpoint/foo"; 448 const char SECONDFULLPATH[] = "mountpoint/bar"; 449 const char FIRSTRELPATH[] = "foo"; 450 const char SECONDRELPATH[] = "bar"; 451 uint64_t ino = 42; 452 453 EXPECT_LOOKUP(FUSE_ROOT_ID, FIRSTRELPATH) 454 .WillOnce(Invoke( 455 ReturnImmediate([=](auto in __unused, auto& out) { 456 SET_OUT_HEADER_LEN(out, entry); 457 out.body.entry.attr.mode = S_IFDIR | 0644; 458 out.body.entry.nodeid = ino; 459 out.body.entry.attr.nlink = 1; 460 }))); 461 expect_lookup(SECONDRELPATH, ino, S_IFREG | 0755, 0, 1, UINT64_MAX); 462 // VOP_FORGET happens asynchronously, so it may or may not arrive 463 // before the test completes. 464 EXPECT_CALL(*m_mock, process( 465 ResultOf([=](auto in) { 466 return (in.header.opcode == FUSE_FORGET && 467 in.header.nodeid == ino && 468 in.body.forget.nlookup == 1); 469 }, Eq(true)), 470 _) 471 ).Times(AtMost(1)) 472 .WillOnce(Invoke([=](auto in __unused, auto &out __unused) { })); 473 474 ASSERT_EQ(0, access(FIRSTFULLPATH, F_OK)) << strerror(errno); 475 EXPECT_EQ(0, access(SECONDFULLPATH, F_OK)) << strerror(errno); 476 } 477 478 TEST_F(Lookup_7_8, ok) 479 { 480 const char FULLPATH[] = "mountpoint/some_file.txt"; 481 const char RELPATH[] = "some_file.txt"; 482 483 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 484 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 485 SET_OUT_HEADER_LEN(out, entry_7_8); 486 out.body.entry.attr.mode = S_IFREG | 0644; 487 out.body.entry.nodeid = 14; 488 }))); 489 /* 490 * access(2) is one of the few syscalls that will not (always) follow 491 * up a successful VOP_LOOKUP with another VOP. 492 */ 493 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 494 } 495 496 /* 497 * Lookup ".." when that vnode's entry cache has timed out, but its child's 498 * hasn't. 499 */ 500 TEST_F(LookupExportable, dotdot_entry_cache_timeout) 501 { 502 uint64_t foo_ino = 42; 503 uint64_t bar_ino = 43; 504 505 EXPECT_LOOKUP(FUSE_ROOT_ID, "foo") 506 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 507 SET_OUT_HEADER_LEN(out, entry); 508 out.body.entry.attr.mode = S_IFDIR | 0755; 509 out.body.entry.nodeid = foo_ino; 510 out.body.entry.attr_valid = UINT64_MAX; 511 out.body.entry.entry_valid = 0; // immediate timeout 512 }))); 513 EXPECT_LOOKUP(foo_ino, "bar") 514 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 515 SET_OUT_HEADER_LEN(out, entry); 516 out.body.entry.attr.mode = S_IFDIR | 0755; 517 out.body.entry.nodeid = bar_ino; 518 out.body.entry.attr_valid = UINT64_MAX; 519 out.body.entry.entry_valid = UINT64_MAX; 520 }))); 521 expect_opendir(bar_ino); 522 EXPECT_LOOKUP(foo_ino, "..") 523 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 524 SET_OUT_HEADER_LEN(out, entry); 525 out.body.entry.attr.mode = S_IFDIR | 0755; 526 out.body.entry.nodeid = FUSE_ROOT_ID; 527 out.body.entry.attr_valid = UINT64_MAX; 528 out.body.entry.entry_valid = UINT64_MAX; 529 }))); 530 531 int fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY); 532 ASSERT_LE(0, fd) << strerror(errno); 533 /* FreeBSD's fusefs driver always uses the same cache expiration time 534 * for ".." as for the directory itself. So we need to look up two 535 * levels to find an expired ".." cache entry. 536 */ 537 EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno); 538 } 539 540 /* 541 * Lookup ".." for a vnode with no valid parent nid 542 * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259974 543 * Since the file system is exportable, we should resolve the problem by 544 * sending a FUSE_LOOKUP for "..". 545 */ 546 TEST_F(LookupExportable, dotdot_no_parent_nid) 547 { 548 uint64_t foo_ino = 42; 549 uint64_t bar_ino = 43; 550 int fd; 551 552 EXPECT_LOOKUP(FUSE_ROOT_ID, "foo") 553 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 554 SET_OUT_HEADER_LEN(out, entry); 555 out.body.entry.attr.mode = S_IFDIR | 0755; 556 out.body.entry.nodeid = foo_ino; 557 out.body.entry.attr_valid = UINT64_MAX; 558 out.body.entry.entry_valid = UINT64_MAX; 559 }))); 560 EXPECT_LOOKUP(foo_ino, "bar") 561 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 562 SET_OUT_HEADER_LEN(out, entry); 563 out.body.entry.attr.mode = S_IFDIR | 0755; 564 out.body.entry.nodeid = bar_ino; 565 out.body.entry.attr_valid = UINT64_MAX; 566 out.body.entry.entry_valid = UINT64_MAX; 567 }))); 568 EXPECT_CALL(*m_mock, process( 569 ResultOf([=](auto in) { 570 return (in.header.opcode == FUSE_OPENDIR); 571 }, Eq(true)), 572 _) 573 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 574 SET_OUT_HEADER_LEN(out, open); 575 }))); 576 expect_forget(foo_ino, 1, NULL); 577 EXPECT_LOOKUP(bar_ino, "..") 578 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 579 SET_OUT_HEADER_LEN(out, entry); 580 out.body.entry.attr.mode = S_IFDIR | 0755; 581 out.body.entry.nodeid = foo_ino; 582 out.body.entry.attr_valid = UINT64_MAX; 583 out.body.entry.entry_valid = UINT64_MAX; 584 }))); 585 EXPECT_LOOKUP(foo_ino, "..") 586 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 587 SET_OUT_HEADER_LEN(out, entry); 588 out.body.entry.attr.mode = S_IFDIR | 0755; 589 out.body.entry.nodeid = FUSE_ROOT_ID; 590 out.body.entry.attr_valid = UINT64_MAX; 591 out.body.entry.entry_valid = UINT64_MAX; 592 }))); 593 594 fd = open("mountpoint/foo/bar", O_EXEC| O_DIRECTORY); 595 ASSERT_LE(0, fd) << strerror(errno); 596 // Try (and fail) to unmount the file system, to reclaim the mountpoint 597 // and foo vnodes. 598 ASSERT_NE(0, unmount("mountpoint", 0)); 599 EXPECT_EQ(EBUSY, errno); 600 nap(); // Because vnode reclamation is asynchronous 601 EXPECT_EQ(0, faccessat(fd, "../..", F_OK, 0)) << strerror(errno); 602 } 603