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 /* This file tests functionality needed by NFS servers */ 32 extern "C" { 33 #include <sys/param.h> 34 #include <sys/mount.h> 35 36 #include <dirent.h> 37 #include <fcntl.h> 38 #include <unistd.h> 39 } 40 41 #include "mockfs.hh" 42 #include "utils.hh" 43 44 using namespace std; 45 using namespace testing; 46 47 48 class Nfs: public FuseTest { 49 public: 50 virtual void SetUp() { 51 if (geteuid() != 0) 52 GTEST_SKIP() << "This test requires a privileged user"; 53 FuseTest::SetUp(); 54 } 55 }; 56 57 class Exportable: public Nfs { 58 public: 59 virtual void SetUp() { 60 m_init_flags = FUSE_EXPORT_SUPPORT; 61 Nfs::SetUp(); 62 } 63 }; 64 65 class Fhstat: public Exportable {}; 66 class FhstatNotExportable: public Nfs {}; 67 class Getfh: public Exportable {}; 68 class Readdir: public Exportable {}; 69 70 /* If the server returns a different generation number, then file is stale */ 71 TEST_F(Fhstat, estale) 72 { 73 const char FULLPATH[] = "mountpoint/some_dir/."; 74 const char RELDIRPATH[] = "some_dir"; 75 fhandle_t fhp; 76 struct stat sb; 77 const uint64_t ino = 42; 78 const mode_t mode = S_IFDIR | 0755; 79 Sequence seq; 80 81 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 82 .InSequence(seq) 83 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 84 SET_OUT_HEADER_LEN(out, entry); 85 out.body.entry.attr.mode = mode; 86 out.body.entry.nodeid = ino; 87 out.body.entry.attr.ino = ino; 88 out.body.entry.generation = 1; 89 out.body.entry.attr_valid = UINT64_MAX; 90 out.body.entry.entry_valid = 0; 91 }))); 92 93 EXPECT_LOOKUP(ino, ".") 94 .InSequence(seq) 95 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 96 SET_OUT_HEADER_LEN(out, entry); 97 out.body.entry.attr.mode = mode; 98 out.body.entry.nodeid = ino; 99 out.body.entry.attr.ino = ino; 100 out.body.entry.generation = 2; 101 out.body.entry.attr_valid = UINT64_MAX; 102 out.body.entry.entry_valid = 0; 103 }))); 104 105 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 106 ASSERT_EQ(-1, fhstat(&fhp, &sb)); 107 EXPECT_EQ(ESTALE, errno); 108 } 109 110 /* If we must lookup an entry from the server, send a LOOKUP request for "." */ 111 TEST_F(Fhstat, lookup_dot) 112 { 113 const char FULLPATH[] = "mountpoint/some_dir/."; 114 const char RELDIRPATH[] = "some_dir"; 115 fhandle_t fhp; 116 struct stat sb; 117 const uint64_t ino = 42; 118 const mode_t mode = S_IFDIR | 0755; 119 const uid_t uid = 12345; 120 121 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 122 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 123 SET_OUT_HEADER_LEN(out, entry); 124 out.body.entry.attr.mode = mode; 125 out.body.entry.nodeid = ino; 126 out.body.entry.attr.ino = ino; 127 out.body.entry.generation = 1; 128 out.body.entry.attr.uid = uid; 129 out.body.entry.attr_valid = UINT64_MAX; 130 out.body.entry.entry_valid = 0; 131 }))); 132 133 EXPECT_LOOKUP(ino, ".") 134 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 135 SET_OUT_HEADER_LEN(out, entry); 136 out.body.entry.attr.mode = mode; 137 out.body.entry.nodeid = ino; 138 out.body.entry.attr.ino = ino; 139 out.body.entry.generation = 1; 140 out.body.entry.attr.uid = uid; 141 out.body.entry.attr_valid = UINT64_MAX; 142 out.body.entry.entry_valid = 0; 143 }))); 144 145 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 146 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 147 EXPECT_EQ(uid, sb.st_uid); 148 EXPECT_EQ(mode, sb.st_mode); 149 } 150 151 /* Gracefully handle failures to lookup ".". */ 152 TEST_F(Fhstat, lookup_dot_error) 153 { 154 const char FULLPATH[] = "mountpoint/some_dir/."; 155 const char RELDIRPATH[] = "some_dir"; 156 fhandle_t fhp; 157 struct stat sb; 158 const uint64_t ino = 42; 159 const mode_t mode = S_IFDIR | 0755; 160 const uid_t uid = 12345; 161 162 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 163 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 164 SET_OUT_HEADER_LEN(out, entry); 165 out.body.entry.attr.mode = mode; 166 out.body.entry.nodeid = ino; 167 out.body.entry.attr.ino = ino; 168 out.body.entry.generation = 1; 169 out.body.entry.attr.uid = uid; 170 out.body.entry.attr_valid = UINT64_MAX; 171 out.body.entry.entry_valid = 0; 172 }))); 173 174 EXPECT_LOOKUP(ino, ".") 175 .WillOnce(Invoke(ReturnErrno(EDOOFUS))); 176 177 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 178 ASSERT_EQ(-1, fhstat(&fhp, &sb)); 179 EXPECT_EQ(EDOOFUS, errno); 180 } 181 182 /* Use a file handle whose entry is still cached */ 183 TEST_F(Fhstat, cached) 184 { 185 const char FULLPATH[] = "mountpoint/some_dir/."; 186 const char RELDIRPATH[] = "some_dir"; 187 fhandle_t fhp; 188 struct stat sb; 189 const uint64_t ino = 42; 190 const mode_t mode = S_IFDIR | 0755; 191 192 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 193 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 194 SET_OUT_HEADER_LEN(out, entry); 195 out.body.entry.attr.mode = mode; 196 out.body.entry.nodeid = ino; 197 out.body.entry.attr.ino = ino; 198 out.body.entry.generation = 1; 199 out.body.entry.attr.ino = ino; 200 out.body.entry.attr_valid = UINT64_MAX; 201 out.body.entry.entry_valid = UINT64_MAX; 202 }))); 203 204 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 205 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 206 EXPECT_EQ(ino, sb.st_ino); 207 } 208 209 /* File handle entries should expire from the cache, too */ 210 TEST_F(Fhstat, cache_expired) 211 { 212 const char FULLPATH[] = "mountpoint/some_dir/."; 213 const char RELDIRPATH[] = "some_dir"; 214 fhandle_t fhp; 215 struct stat sb; 216 const uint64_t ino = 42; 217 const mode_t mode = S_IFDIR | 0755; 218 219 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 220 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 221 SET_OUT_HEADER_LEN(out, entry); 222 out.body.entry.attr.mode = mode; 223 out.body.entry.nodeid = ino; 224 out.body.entry.attr.ino = ino; 225 out.body.entry.generation = 1; 226 out.body.entry.attr.ino = ino; 227 out.body.entry.attr_valid = UINT64_MAX; 228 out.body.entry.entry_valid_nsec = NAP_NS / 2; 229 }))); 230 231 EXPECT_LOOKUP(ino, ".") 232 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 233 SET_OUT_HEADER_LEN(out, entry); 234 out.body.entry.attr.mode = mode; 235 out.body.entry.nodeid = ino; 236 out.body.entry.attr.ino = ino; 237 out.body.entry.generation = 1; 238 out.body.entry.attr.ino = ino; 239 out.body.entry.attr_valid = UINT64_MAX; 240 out.body.entry.entry_valid = 0; 241 }))); 242 243 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 244 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 245 EXPECT_EQ(ino, sb.st_ino); 246 247 nap(); 248 249 /* Cache should be expired; fuse should issue a FUSE_LOOKUP */ 250 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 251 EXPECT_EQ(ino, sb.st_ino); 252 } 253 254 /* 255 * If the server returns a FUSE_LOOKUP response for a nodeid that we didn't 256 * lookup, it's a bug. But we should handle it gracefully. 257 */ 258 TEST_F(Fhstat, inconsistent_nodeid) 259 { 260 const char FULLPATH[] = "mountpoint/some_dir/."; 261 const char RELDIRPATH[] = "some_dir"; 262 fhandle_t fhp; 263 struct stat sb; 264 const uint64_t ino_in = 42; 265 const uint64_t ino_out = 43; 266 const mode_t mode = S_IFDIR | 0755; 267 const uid_t uid = 12345; 268 269 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 270 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 271 SET_OUT_HEADER_LEN(out, entry); 272 out.body.entry.nodeid = ino_in; 273 out.body.entry.attr.ino = ino_in; 274 out.body.entry.attr.mode = mode; 275 out.body.entry.generation = 1; 276 out.body.entry.attr.uid = uid; 277 out.body.entry.attr_valid = UINT64_MAX; 278 out.body.entry.entry_valid = 0; 279 }))); 280 281 EXPECT_LOOKUP(ino_in, ".") 282 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 283 SET_OUT_HEADER_LEN(out, entry); 284 out.body.entry.nodeid = ino_out; 285 out.body.entry.attr.ino = ino_out; 286 out.body.entry.attr.mode = mode; 287 out.body.entry.generation = 1; 288 out.body.entry.attr.uid = uid; 289 out.body.entry.attr_valid = UINT64_MAX; 290 out.body.entry.entry_valid = 0; 291 }))); 292 293 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 294 EXPECT_NE(0, fhstat(&fhp, &sb)) << strerror(errno); 295 EXPECT_EQ(EIO, errno); 296 } 297 298 /* 299 * If the server returns a FUSE_LOOKUP response where the nodeid doesn't match 300 * the inode number, and the file system is exported, it's a bug. But we 301 * should handle it gracefully. 302 */ 303 TEST_F(Fhstat, inconsistent_ino) 304 { 305 const char FULLPATH[] = "mountpoint/some_dir/."; 306 const char RELDIRPATH[] = "some_dir"; 307 fhandle_t fhp; 308 struct stat sb; 309 const uint64_t nodeid = 42; 310 const uint64_t ino = 711; // Could be anything that != nodeid 311 const mode_t mode = S_IFDIR | 0755; 312 const uid_t uid = 12345; 313 314 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 315 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 316 SET_OUT_HEADER_LEN(out, entry); 317 out.body.entry.nodeid = nodeid; 318 out.body.entry.attr.ino = nodeid; 319 out.body.entry.attr.mode = mode; 320 out.body.entry.generation = 1; 321 out.body.entry.attr.uid = uid; 322 out.body.entry.attr_valid = UINT64_MAX; 323 out.body.entry.entry_valid = 0; 324 }))); 325 326 EXPECT_LOOKUP(nodeid, ".") 327 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 328 SET_OUT_HEADER_LEN(out, entry); 329 out.body.entry.nodeid = nodeid; 330 out.body.entry.attr.ino = ino; 331 out.body.entry.attr.mode = mode; 332 out.body.entry.generation = 1; 333 out.body.entry.attr.uid = uid; 334 out.body.entry.attr_valid = UINT64_MAX; 335 out.body.entry.entry_valid = 0; 336 }))); 337 338 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 339 /* 340 * The fhstat operation will actually succeed. But future operations 341 * will likely fail. 342 */ 343 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 344 EXPECT_EQ(ino, sb.st_ino); 345 } 346 347 /* 348 * If the server doesn't set FUSE_EXPORT_SUPPORT, then we can't do NFS-style 349 * lookups 350 */ 351 TEST_F(FhstatNotExportable, lookup_dot) 352 { 353 const char FULLPATH[] = "mountpoint/some_dir/."; 354 const char RELDIRPATH[] = "some_dir"; 355 fhandle_t fhp; 356 const uint64_t ino = 42; 357 const mode_t mode = S_IFDIR | 0755; 358 359 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 360 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 361 SET_OUT_HEADER_LEN(out, entry); 362 out.body.entry.attr.mode = mode; 363 out.body.entry.nodeid = ino; 364 out.body.entry.attr.ino = ino; 365 out.body.entry.generation = 1; 366 out.body.entry.attr_valid = UINT64_MAX; 367 out.body.entry.entry_valid = 0; 368 }))); 369 370 ASSERT_EQ(-1, getfh(FULLPATH, &fhp)); 371 ASSERT_EQ(EOPNOTSUPP, errno); 372 } 373 374 /* FreeBSD's fid struct doesn't have enough space for 64-bit generations */ 375 TEST_F(Getfh, eoverflow) 376 { 377 const char FULLPATH[] = "mountpoint/some_dir/."; 378 const char RELDIRPATH[] = "some_dir"; 379 fhandle_t fhp; 380 uint64_t ino = 42; 381 382 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 383 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 384 SET_OUT_HEADER_LEN(out, entry); 385 out.body.entry.attr.mode = S_IFDIR | 0755; 386 out.body.entry.nodeid = ino; 387 out.body.entry.attr.ino = ino; 388 out.body.entry.generation = (uint64_t)UINT32_MAX + 1; 389 out.body.entry.attr_valid = UINT64_MAX; 390 out.body.entry.entry_valid = UINT64_MAX; 391 }))); 392 393 ASSERT_NE(0, getfh(FULLPATH, &fhp)); 394 EXPECT_EQ(EOVERFLOW, errno); 395 } 396 397 /* Get an NFS file handle */ 398 TEST_F(Getfh, ok) 399 { 400 const char FULLPATH[] = "mountpoint/some_dir/."; 401 const char RELDIRPATH[] = "some_dir"; 402 fhandle_t fhp; 403 uint64_t ino = 42; 404 405 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 406 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 407 SET_OUT_HEADER_LEN(out, entry); 408 out.body.entry.attr.mode = S_IFDIR | 0755; 409 out.body.entry.nodeid = ino; 410 out.body.entry.attr.ino = ino; 411 out.body.entry.attr_valid = UINT64_MAX; 412 out.body.entry.entry_valid = UINT64_MAX; 413 }))); 414 415 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 416 } 417 418 /* 419 * Call readdir via a file handle. 420 * 421 * This is how a userspace nfs server like nfs-ganesha or unfs3 would call 422 * readdir. The in-kernel NFS server never does any equivalent of open. I 423 * haven't discovered a way to mimic nfsd's behavior short of actually running 424 * nfsd. 425 */ 426 TEST_F(Readdir, getdirentries) 427 { 428 const char FULLPATH[] = "mountpoint/some_dir"; 429 const char RELPATH[] = "some_dir"; 430 uint64_t ino = 42; 431 mode_t mode = S_IFDIR | 0755; 432 fhandle_t fhp; 433 int fd; 434 char buf[8192]; 435 ssize_t r; 436 437 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 438 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 439 SET_OUT_HEADER_LEN(out, entry); 440 out.body.entry.attr.mode = mode; 441 out.body.entry.nodeid = ino; 442 out.body.entry.attr.ino = ino; 443 out.body.entry.generation = 1; 444 out.body.entry.attr_valid = UINT64_MAX; 445 out.body.entry.entry_valid = 0; 446 }))); 447 448 EXPECT_LOOKUP(ino, ".") 449 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 450 SET_OUT_HEADER_LEN(out, entry); 451 out.body.entry.attr.mode = mode; 452 out.body.entry.nodeid = ino; 453 out.body.entry.attr.ino = ino; 454 out.body.entry.generation = 1; 455 out.body.entry.attr_valid = UINT64_MAX; 456 out.body.entry.entry_valid = 0; 457 }))); 458 459 expect_opendir(ino); 460 461 EXPECT_CALL(*m_mock, process( 462 ResultOf([=](auto in) { 463 return (in.header.opcode == FUSE_READDIR && 464 in.header.nodeid == ino && 465 in.body.readdir.size == sizeof(buf)); 466 }, Eq(true)), 467 _) 468 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 469 out.header.error = 0; 470 out.header.len = sizeof(out.header); 471 }))); 472 473 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 474 fd = fhopen(&fhp, O_DIRECTORY); 475 ASSERT_LE(0, fd) << strerror(errno); 476 r = getdirentries(fd, buf, sizeof(buf), 0); 477 ASSERT_EQ(0, r) << strerror(errno); 478 479 leak(fd); 480 } 481