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.generation = 1; 88 out.body.entry.attr_valid = UINT64_MAX; 89 out.body.entry.entry_valid = 0; 90 }))); 91 92 EXPECT_LOOKUP(ino, ".") 93 .InSequence(seq) 94 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 95 SET_OUT_HEADER_LEN(out, entry); 96 out.body.entry.attr.mode = mode; 97 out.body.entry.nodeid = ino; 98 out.body.entry.generation = 2; 99 out.body.entry.attr_valid = UINT64_MAX; 100 out.body.entry.entry_valid = 0; 101 }))); 102 103 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 104 ASSERT_EQ(-1, fhstat(&fhp, &sb)); 105 EXPECT_EQ(ESTALE, errno); 106 } 107 108 /* If we must lookup an entry from the server, send a LOOKUP request for "." */ 109 TEST_F(Fhstat, lookup_dot) 110 { 111 const char FULLPATH[] = "mountpoint/some_dir/."; 112 const char RELDIRPATH[] = "some_dir"; 113 fhandle_t fhp; 114 struct stat sb; 115 const uint64_t ino = 42; 116 const mode_t mode = S_IFDIR | 0755; 117 const uid_t uid = 12345; 118 119 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 120 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 121 SET_OUT_HEADER_LEN(out, entry); 122 out.body.entry.attr.mode = mode; 123 out.body.entry.nodeid = ino; 124 out.body.entry.generation = 1; 125 out.body.entry.attr.uid = uid; 126 out.body.entry.attr_valid = UINT64_MAX; 127 out.body.entry.entry_valid = 0; 128 }))); 129 130 EXPECT_LOOKUP(ino, ".") 131 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 132 SET_OUT_HEADER_LEN(out, entry); 133 out.body.entry.attr.mode = mode; 134 out.body.entry.nodeid = ino; 135 out.body.entry.generation = 1; 136 out.body.entry.attr.uid = uid; 137 out.body.entry.attr_valid = UINT64_MAX; 138 out.body.entry.entry_valid = 0; 139 }))); 140 141 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 142 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 143 EXPECT_EQ(uid, sb.st_uid); 144 EXPECT_EQ(mode, sb.st_mode); 145 } 146 147 /* Gracefully handle failures to lookup ".". */ 148 TEST_F(Fhstat, lookup_dot_error) 149 { 150 const char FULLPATH[] = "mountpoint/some_dir/."; 151 const char RELDIRPATH[] = "some_dir"; 152 fhandle_t fhp; 153 struct stat sb; 154 const uint64_t ino = 42; 155 const mode_t mode = S_IFDIR | 0755; 156 const uid_t uid = 12345; 157 158 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 159 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 160 SET_OUT_HEADER_LEN(out, entry); 161 out.body.entry.attr.mode = mode; 162 out.body.entry.nodeid = ino; 163 out.body.entry.generation = 1; 164 out.body.entry.attr.uid = uid; 165 out.body.entry.attr_valid = UINT64_MAX; 166 out.body.entry.entry_valid = 0; 167 }))); 168 169 EXPECT_LOOKUP(ino, ".") 170 .WillOnce(Invoke(ReturnErrno(EDOOFUS))); 171 172 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 173 ASSERT_EQ(-1, fhstat(&fhp, &sb)); 174 EXPECT_EQ(EDOOFUS, errno); 175 } 176 177 /* Use a file handle whose entry is still cached */ 178 TEST_F(Fhstat, cached) 179 { 180 const char FULLPATH[] = "mountpoint/some_dir/."; 181 const char RELDIRPATH[] = "some_dir"; 182 fhandle_t fhp; 183 struct stat sb; 184 const uint64_t ino = 42; 185 const mode_t mode = S_IFDIR | 0755; 186 187 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 188 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 189 SET_OUT_HEADER_LEN(out, entry); 190 out.body.entry.attr.mode = mode; 191 out.body.entry.nodeid = ino; 192 out.body.entry.generation = 1; 193 out.body.entry.attr.ino = ino; 194 out.body.entry.attr_valid = UINT64_MAX; 195 out.body.entry.entry_valid = UINT64_MAX; 196 }))); 197 198 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 199 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 200 EXPECT_EQ(ino, sb.st_ino); 201 } 202 203 /* File handle entries should expire from the cache, too */ 204 TEST_F(Fhstat, cache_expired) 205 { 206 const char FULLPATH[] = "mountpoint/some_dir/."; 207 const char RELDIRPATH[] = "some_dir"; 208 fhandle_t fhp; 209 struct stat sb; 210 const uint64_t ino = 42; 211 const mode_t mode = S_IFDIR | 0755; 212 213 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 214 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 215 SET_OUT_HEADER_LEN(out, entry); 216 out.body.entry.attr.mode = mode; 217 out.body.entry.nodeid = ino; 218 out.body.entry.generation = 1; 219 out.body.entry.attr.ino = ino; 220 out.body.entry.attr_valid = UINT64_MAX; 221 out.body.entry.entry_valid_nsec = NAP_NS / 2; 222 }))); 223 224 EXPECT_LOOKUP(ino, ".") 225 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 226 SET_OUT_HEADER_LEN(out, entry); 227 out.body.entry.attr.mode = mode; 228 out.body.entry.nodeid = ino; 229 out.body.entry.generation = 1; 230 out.body.entry.attr.ino = ino; 231 out.body.entry.attr_valid = UINT64_MAX; 232 out.body.entry.entry_valid = 0; 233 }))); 234 235 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 236 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 237 EXPECT_EQ(ino, sb.st_ino); 238 239 nap(); 240 241 /* Cache should be expired; fuse should issue a FUSE_LOOKUP */ 242 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 243 EXPECT_EQ(ino, sb.st_ino); 244 } 245 246 /* 247 * If the server doesn't set FUSE_EXPORT_SUPPORT, then we can't do NFS-style 248 * lookups 249 */ 250 TEST_F(FhstatNotExportable, lookup_dot) 251 { 252 const char FULLPATH[] = "mountpoint/some_dir/."; 253 const char RELDIRPATH[] = "some_dir"; 254 fhandle_t fhp; 255 const uint64_t ino = 42; 256 const mode_t mode = S_IFDIR | 0755; 257 258 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 259 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 260 SET_OUT_HEADER_LEN(out, entry); 261 out.body.entry.attr.mode = mode; 262 out.body.entry.nodeid = ino; 263 out.body.entry.generation = 1; 264 out.body.entry.attr_valid = UINT64_MAX; 265 out.body.entry.entry_valid = 0; 266 }))); 267 268 ASSERT_EQ(-1, getfh(FULLPATH, &fhp)); 269 ASSERT_EQ(EOPNOTSUPP, errno); 270 } 271 272 /* FreeBSD's fid struct doesn't have enough space for 64-bit generations */ 273 TEST_F(Getfh, eoverflow) 274 { 275 const char FULLPATH[] = "mountpoint/some_dir/."; 276 const char RELDIRPATH[] = "some_dir"; 277 fhandle_t fhp; 278 uint64_t ino = 42; 279 280 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 281 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 282 SET_OUT_HEADER_LEN(out, entry); 283 out.body.entry.attr.mode = S_IFDIR | 0755; 284 out.body.entry.nodeid = ino; 285 out.body.entry.generation = (uint64_t)UINT32_MAX + 1; 286 out.body.entry.attr_valid = UINT64_MAX; 287 out.body.entry.entry_valid = UINT64_MAX; 288 }))); 289 290 ASSERT_NE(0, getfh(FULLPATH, &fhp)); 291 EXPECT_EQ(EOVERFLOW, errno); 292 } 293 294 /* Get an NFS file handle */ 295 TEST_F(Getfh, ok) 296 { 297 const char FULLPATH[] = "mountpoint/some_dir/."; 298 const char RELDIRPATH[] = "some_dir"; 299 fhandle_t fhp; 300 uint64_t ino = 42; 301 302 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 303 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 304 SET_OUT_HEADER_LEN(out, entry); 305 out.body.entry.attr.mode = S_IFDIR | 0755; 306 out.body.entry.nodeid = ino; 307 out.body.entry.attr_valid = UINT64_MAX; 308 out.body.entry.entry_valid = UINT64_MAX; 309 }))); 310 311 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 312 } 313 314 /* 315 * Call readdir via a file handle. 316 * 317 * This is how a userspace nfs server like nfs-ganesha or unfs3 would call 318 * readdir. The in-kernel NFS server never does any equivalent of open. I 319 * haven't discovered a way to mimic nfsd's behavior short of actually running 320 * nfsd. 321 */ 322 TEST_F(Readdir, getdirentries) 323 { 324 const char FULLPATH[] = "mountpoint/some_dir"; 325 const char RELPATH[] = "some_dir"; 326 uint64_t ino = 42; 327 mode_t mode = S_IFDIR | 0755; 328 fhandle_t fhp; 329 int fd; 330 char buf[8192]; 331 ssize_t r; 332 333 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 334 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 335 SET_OUT_HEADER_LEN(out, entry); 336 out.body.entry.attr.mode = mode; 337 out.body.entry.nodeid = ino; 338 out.body.entry.generation = 1; 339 out.body.entry.attr_valid = UINT64_MAX; 340 out.body.entry.entry_valid = 0; 341 }))); 342 343 EXPECT_LOOKUP(ino, ".") 344 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 345 SET_OUT_HEADER_LEN(out, entry); 346 out.body.entry.attr.mode = mode; 347 out.body.entry.nodeid = ino; 348 out.body.entry.generation = 1; 349 out.body.entry.attr_valid = UINT64_MAX; 350 out.body.entry.entry_valid = 0; 351 }))); 352 353 expect_opendir(ino); 354 355 EXPECT_CALL(*m_mock, process( 356 ResultOf([=](auto in) { 357 return (in.header.opcode == FUSE_READDIR && 358 in.header.nodeid == ino && 359 in.body.readdir.size == sizeof(buf)); 360 }, Eq(true)), 361 _) 362 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 363 out.header.error = 0; 364 out.header.len = sizeof(out.header); 365 }))); 366 367 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 368 fd = fhopen(&fhp, O_DIRECTORY); 369 ASSERT_LE(0, fd) << strerror(errno); 370 r = getdirentries(fd, buf, sizeof(buf), 0); 371 ASSERT_EQ(0, r) << strerror(errno); 372 373 leak(fd); 374 } 375