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 <unistd.h> 35 } 36 37 #include "mockfs.hh" 38 #include "utils.hh" 39 40 using namespace testing; 41 42 class Lookup: public FuseTest {}; 43 class Lookup_7_8: public Lookup { 44 public: 45 virtual void SetUp() { 46 m_kernel_minor_version = 8; 47 Lookup::SetUp(); 48 } 49 }; 50 51 /* 52 * If lookup returns a non-zero cache timeout, then subsequent VOP_GETATTRs 53 * should use the cached attributes, rather than query the daemon 54 */ 55 TEST_F(Lookup, attr_cache) 56 { 57 const char FULLPATH[] = "mountpoint/some_file.txt"; 58 const char RELPATH[] = "some_file.txt"; 59 const uint64_t ino = 42; 60 const uint64_t generation = 13; 61 struct stat sb; 62 63 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 64 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 65 SET_OUT_HEADER_LEN(out, entry); 66 out.body.entry.nodeid = ino; 67 out.body.entry.attr_valid = UINT64_MAX; 68 out.body.entry.attr.ino = ino; // Must match nodeid 69 out.body.entry.attr.mode = S_IFREG | 0644; 70 out.body.entry.attr.size = 1; 71 out.body.entry.attr.blocks = 2; 72 out.body.entry.attr.atime = 3; 73 out.body.entry.attr.mtime = 4; 74 out.body.entry.attr.ctime = 5; 75 out.body.entry.attr.atimensec = 6; 76 out.body.entry.attr.mtimensec = 7; 77 out.body.entry.attr.ctimensec = 8; 78 out.body.entry.attr.nlink = 9; 79 out.body.entry.attr.uid = 10; 80 out.body.entry.attr.gid = 11; 81 out.body.entry.attr.rdev = 12; 82 out.body.entry.generation = generation; 83 }))); 84 /* stat(2) issues a VOP_LOOKUP followed by a VOP_GETATTR */ 85 ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); 86 EXPECT_EQ(1, sb.st_size); 87 EXPECT_EQ(2, sb.st_blocks); 88 EXPECT_EQ(3, sb.st_atim.tv_sec); 89 EXPECT_EQ(6, sb.st_atim.tv_nsec); 90 EXPECT_EQ(4, sb.st_mtim.tv_sec); 91 EXPECT_EQ(7, sb.st_mtim.tv_nsec); 92 EXPECT_EQ(5, sb.st_ctim.tv_sec); 93 EXPECT_EQ(8, sb.st_ctim.tv_nsec); 94 EXPECT_EQ(9ull, sb.st_nlink); 95 EXPECT_EQ(10ul, sb.st_uid); 96 EXPECT_EQ(11ul, sb.st_gid); 97 EXPECT_EQ(12ul, sb.st_rdev); 98 EXPECT_EQ(ino, sb.st_ino); 99 EXPECT_EQ(S_IFREG | 0644, sb.st_mode); 100 101 // fuse(4) does not _yet_ support inode generations 102 //EXPECT_EQ(generation, sb.st_gen); 103 104 //st_birthtim and st_flags are not supported by protocol 7.8. They're 105 //only supported as OS-specific extensions to OSX. 106 //EXPECT_EQ(, sb.st_birthtim); 107 //EXPECT_EQ(, sb.st_flags); 108 109 //FUSE can't set st_blksize until protocol 7.9 110 } 111 112 /* 113 * If lookup returns a finite but non-zero cache timeout, then we should discard 114 * the cached attributes and requery the daemon. 115 */ 116 TEST_F(Lookup, attr_cache_timeout) 117 { 118 const char FULLPATH[] = "mountpoint/some_file.txt"; 119 const char RELPATH[] = "some_file.txt"; 120 const uint64_t ino = 42; 121 struct stat sb; 122 123 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 124 .Times(2) 125 .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 126 SET_OUT_HEADER_LEN(out, entry); 127 out.body.entry.nodeid = ino; 128 out.body.entry.attr_valid_nsec = NAP_NS / 2; 129 out.body.entry.attr.ino = ino; // Must match nodeid 130 out.body.entry.attr.mode = S_IFREG | 0644; 131 }))); 132 133 /* access(2) will issue a VOP_LOOKUP and fill the attr cache */ 134 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 135 /* Next access(2) will use the cached attributes */ 136 nap(); 137 /* The cache has timed out; VOP_GETATTR should query the daemon*/ 138 ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); 139 } 140 141 TEST_F(Lookup, dot) 142 { 143 const char FULLPATH[] = "mountpoint/some_dir/."; 144 const char RELDIRPATH[] = "some_dir"; 145 uint64_t ino = 42; 146 147 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 148 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 149 SET_OUT_HEADER_LEN(out, entry); 150 out.body.entry.attr.mode = S_IFDIR | 0755; 151 out.body.entry.nodeid = ino; 152 out.body.entry.attr_valid = UINT64_MAX; 153 out.body.entry.entry_valid = UINT64_MAX; 154 }))); 155 156 /* 157 * access(2) is one of the few syscalls that will not (always) follow 158 * up a successful VOP_LOOKUP with another VOP. 159 */ 160 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 161 } 162 163 TEST_F(Lookup, dotdot) 164 { 165 const char FULLPATH[] = "mountpoint/some_dir/.."; 166 const char RELDIRPATH[] = "some_dir"; 167 168 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 169 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 170 SET_OUT_HEADER_LEN(out, entry); 171 out.body.entry.attr.mode = S_IFDIR | 0755; 172 out.body.entry.nodeid = 14; 173 out.body.entry.attr_valid = UINT64_MAX; 174 out.body.entry.entry_valid = UINT64_MAX; 175 }))); 176 177 /* 178 * access(2) is one of the few syscalls that will not (always) follow 179 * up a successful VOP_LOOKUP with another VOP. 180 */ 181 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 182 } 183 184 TEST_F(Lookup, enoent) 185 { 186 const char FULLPATH[] = "mountpoint/does_not_exist"; 187 const char RELPATH[] = "does_not_exist"; 188 189 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 190 .WillOnce(Invoke(ReturnErrno(ENOENT))); 191 EXPECT_NE(0, access(FULLPATH, F_OK)); 192 EXPECT_EQ(ENOENT, errno); 193 } 194 195 TEST_F(Lookup, enotdir) 196 { 197 const char FULLPATH[] = "mountpoint/not_a_dir/some_file.txt"; 198 const char RELPATH[] = "not_a_dir"; 199 200 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 201 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 202 SET_OUT_HEADER_LEN(out, entry); 203 out.body.entry.entry_valid = UINT64_MAX; 204 out.body.entry.attr.mode = S_IFREG | 0644; 205 out.body.entry.nodeid = 42; 206 }))); 207 208 ASSERT_EQ(-1, access(FULLPATH, F_OK)); 209 ASSERT_EQ(ENOTDIR, errno); 210 } 211 212 /* 213 * If lookup returns a non-zero entry timeout, then subsequent VOP_LOOKUPs 214 * should use the cached inode rather than requery the daemon 215 */ 216 TEST_F(Lookup, entry_cache) 217 { 218 const char FULLPATH[] = "mountpoint/some_file.txt"; 219 const char RELPATH[] = "some_file.txt"; 220 221 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 222 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 223 SET_OUT_HEADER_LEN(out, entry); 224 out.body.entry.entry_valid = UINT64_MAX; 225 out.body.entry.attr.mode = S_IFREG | 0644; 226 out.body.entry.nodeid = 14; 227 }))); 228 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 229 /* The second access(2) should use the cache */ 230 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 231 } 232 233 /* 234 * If the daemon returns an error of 0 and an inode of 0, that's a flag for 235 * "ENOENT and cache it" with the given entry_timeout 236 */ 237 TEST_F(Lookup, entry_cache_negative) 238 { 239 struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0}; 240 241 EXPECT_LOOKUP(FUSE_ROOT_ID, "does_not_exist") 242 .Times(1) 243 .WillOnce(Invoke(ReturnNegativeCache(&entry_valid))); 244 245 EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK)); 246 EXPECT_EQ(ENOENT, errno); 247 EXPECT_NE(0, access("mountpoint/does_not_exist", F_OK)); 248 EXPECT_EQ(ENOENT, errno); 249 } 250 251 /* Negative entry caches should timeout, too */ 252 TEST_F(Lookup, entry_cache_negative_timeout) 253 { 254 const char *RELPATH = "does_not_exist"; 255 const char *FULLPATH = "mountpoint/does_not_exist"; 256 struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = NAP_NS / 2}; 257 258 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 259 .Times(2) 260 .WillRepeatedly(Invoke(ReturnNegativeCache(&entry_valid))); 261 262 EXPECT_NE(0, access(FULLPATH, F_OK)); 263 EXPECT_EQ(ENOENT, errno); 264 265 nap(); 266 267 /* The cache has timed out; VOP_LOOKUP should requery the daemon*/ 268 EXPECT_NE(0, access(FULLPATH, F_OK)); 269 EXPECT_EQ(ENOENT, errno); 270 } 271 272 /* 273 * If lookup returns a finite but non-zero entry cache timeout, then we should 274 * discard the cached inode and requery the daemon 275 */ 276 TEST_F(Lookup, entry_cache_timeout) 277 { 278 const char FULLPATH[] = "mountpoint/some_file.txt"; 279 const char RELPATH[] = "some_file.txt"; 280 281 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 282 .Times(2) 283 .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 284 SET_OUT_HEADER_LEN(out, entry); 285 out.body.entry.entry_valid_nsec = NAP_NS / 2; 286 out.body.entry.attr.mode = S_IFREG | 0644; 287 out.body.entry.nodeid = 14; 288 }))); 289 290 /* access(2) will issue a VOP_LOOKUP and fill the entry cache */ 291 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 292 /* Next access(2) will use the cached entry */ 293 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 294 nap(); 295 /* The cache has timed out; VOP_LOOKUP should requery the daemon*/ 296 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 297 } 298 299 TEST_F(Lookup, ok) 300 { 301 const char FULLPATH[] = "mountpoint/some_file.txt"; 302 const char RELPATH[] = "some_file.txt"; 303 304 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 305 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 306 SET_OUT_HEADER_LEN(out, entry); 307 out.body.entry.attr.mode = S_IFREG | 0644; 308 out.body.entry.nodeid = 14; 309 }))); 310 /* 311 * access(2) is one of the few syscalls that will not (always) follow 312 * up a successful VOP_LOOKUP with another VOP. 313 */ 314 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 315 } 316 317 // Lookup in a subdirectory of the fuse mount 318 TEST_F(Lookup, subdir) 319 { 320 const char FULLPATH[] = "mountpoint/some_dir/some_file.txt"; 321 const char DIRPATH[] = "some_dir"; 322 const char RELPATH[] = "some_file.txt"; 323 uint64_t dir_ino = 2; 324 uint64_t file_ino = 3; 325 326 EXPECT_LOOKUP(FUSE_ROOT_ID, DIRPATH) 327 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 328 SET_OUT_HEADER_LEN(out, entry); 329 out.body.entry.attr.mode = S_IFDIR | 0755; 330 out.body.entry.nodeid = dir_ino; 331 }))); 332 EXPECT_LOOKUP(dir_ino, RELPATH) 333 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 334 SET_OUT_HEADER_LEN(out, entry); 335 out.body.entry.attr.mode = S_IFREG | 0644; 336 out.body.entry.nodeid = file_ino; 337 }))); 338 /* 339 * access(2) is one of the few syscalls that will not (always) follow 340 * up a successful VOP_LOOKUP with another VOP. 341 */ 342 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 343 } 344 345 /* 346 * The server returns two different vtypes for the same nodeid. This is a bad 347 * server! But we shouldn't crash. 348 */ 349 TEST_F(Lookup, vtype_conflict) 350 { 351 const char FIRSTFULLPATH[] = "mountpoint/foo"; 352 const char SECONDFULLPATH[] = "mountpoint/bar"; 353 const char FIRSTRELPATH[] = "foo"; 354 const char SECONDRELPATH[] = "bar"; 355 uint64_t ino = 42; 356 357 expect_lookup(FIRSTRELPATH, ino, S_IFREG | 0644, 0, 1, UINT64_MAX); 358 expect_lookup(SECONDRELPATH, ino, S_IFDIR | 0755, 0, 1, UINT64_MAX); 359 360 ASSERT_EQ(0, access(FIRSTFULLPATH, F_OK)) << strerror(errno); 361 ASSERT_EQ(-1, access(SECONDFULLPATH, F_OK)); 362 ASSERT_EQ(EAGAIN, errno); 363 } 364 365 TEST_F(Lookup_7_8, ok) 366 { 367 const char FULLPATH[] = "mountpoint/some_file.txt"; 368 const char RELPATH[] = "some_file.txt"; 369 370 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 371 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 372 SET_OUT_HEADER_LEN(out, entry_7_8); 373 out.body.entry.attr.mode = S_IFREG | 0644; 374 out.body.entry.nodeid = 14; 375 }))); 376 /* 377 * access(2) is one of the few syscalls that will not (always) follow 378 * up a successful VOP_LOOKUP with another VOP. 379 */ 380 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 381 } 382 383 384