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