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 /* Use a file handle whose entry is still cached */ 148 TEST_F(Fhstat, cached) 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 157 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 158 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 159 SET_OUT_HEADER_LEN(out, entry); 160 out.body.entry.attr.mode = mode; 161 out.body.entry.nodeid = ino; 162 out.body.entry.generation = 1; 163 out.body.entry.attr.ino = ino; 164 out.body.entry.attr_valid = UINT64_MAX; 165 out.body.entry.entry_valid = UINT64_MAX; 166 }))); 167 168 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 169 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 170 EXPECT_EQ(ino, sb.st_ino); 171 } 172 173 /* File handle entries should expire from the cache, too */ 174 TEST_F(Fhstat, cache_expired) 175 { 176 const char FULLPATH[] = "mountpoint/some_dir/."; 177 const char RELDIRPATH[] = "some_dir"; 178 fhandle_t fhp; 179 struct stat sb; 180 const uint64_t ino = 42; 181 const mode_t mode = S_IFDIR | 0755; 182 183 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 184 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 185 SET_OUT_HEADER_LEN(out, entry); 186 out.body.entry.attr.mode = mode; 187 out.body.entry.nodeid = ino; 188 out.body.entry.generation = 1; 189 out.body.entry.attr.ino = ino; 190 out.body.entry.attr_valid = UINT64_MAX; 191 out.body.entry.entry_valid_nsec = NAP_NS / 2; 192 }))); 193 194 EXPECT_LOOKUP(ino, ".") 195 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 196 SET_OUT_HEADER_LEN(out, entry); 197 out.body.entry.attr.mode = mode; 198 out.body.entry.nodeid = ino; 199 out.body.entry.generation = 1; 200 out.body.entry.attr.ino = ino; 201 out.body.entry.attr_valid = UINT64_MAX; 202 out.body.entry.entry_valid = 0; 203 }))); 204 205 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 206 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 207 EXPECT_EQ(ino, sb.st_ino); 208 209 nap(); 210 211 /* Cache should be expired; fuse should issue a FUSE_LOOKUP */ 212 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 213 EXPECT_EQ(ino, sb.st_ino); 214 } 215 216 /* 217 * If the server doesn't set FUSE_EXPORT_SUPPORT, then we can't do NFS-style 218 * lookups 219 */ 220 TEST_F(FhstatNotExportable, lookup_dot) 221 { 222 const char FULLPATH[] = "mountpoint/some_dir/."; 223 const char RELDIRPATH[] = "some_dir"; 224 fhandle_t fhp; 225 const uint64_t ino = 42; 226 const mode_t mode = S_IFDIR | 0755; 227 228 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 229 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 230 SET_OUT_HEADER_LEN(out, entry); 231 out.body.entry.attr.mode = mode; 232 out.body.entry.nodeid = ino; 233 out.body.entry.generation = 1; 234 out.body.entry.attr_valid = UINT64_MAX; 235 out.body.entry.entry_valid = 0; 236 }))); 237 238 ASSERT_EQ(-1, getfh(FULLPATH, &fhp)); 239 ASSERT_EQ(EOPNOTSUPP, errno); 240 } 241 242 /* FreeBSD's fid struct doesn't have enough space for 64-bit generations */ 243 TEST_F(Getfh, eoverflow) 244 { 245 const char FULLPATH[] = "mountpoint/some_dir/."; 246 const char RELDIRPATH[] = "some_dir"; 247 fhandle_t fhp; 248 uint64_t ino = 42; 249 250 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 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 = ino; 255 out.body.entry.generation = (uint64_t)UINT32_MAX + 1; 256 out.body.entry.attr_valid = UINT64_MAX; 257 out.body.entry.entry_valid = UINT64_MAX; 258 }))); 259 260 ASSERT_NE(0, getfh(FULLPATH, &fhp)); 261 EXPECT_EQ(EOVERFLOW, errno); 262 } 263 264 /* Get an NFS file handle */ 265 TEST_F(Getfh, ok) 266 { 267 const char FULLPATH[] = "mountpoint/some_dir/."; 268 const char RELDIRPATH[] = "some_dir"; 269 fhandle_t fhp; 270 uint64_t ino = 42; 271 272 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 273 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 274 SET_OUT_HEADER_LEN(out, entry); 275 out.body.entry.attr.mode = S_IFDIR | 0755; 276 out.body.entry.nodeid = ino; 277 out.body.entry.attr_valid = UINT64_MAX; 278 out.body.entry.entry_valid = UINT64_MAX; 279 }))); 280 281 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 282 } 283 284 /* 285 * Call readdir via a file handle. 286 * 287 * This is how a userspace nfs server like nfs-ganesha or unfs3 would call 288 * readdir. The in-kernel NFS server never does any equivalent of open. I 289 * haven't discovered a way to mimic nfsd's behavior short of actually running 290 * nfsd. 291 */ 292 TEST_F(Readdir, getdirentries) 293 { 294 const char FULLPATH[] = "mountpoint/some_dir"; 295 const char RELPATH[] = "some_dir"; 296 uint64_t ino = 42; 297 mode_t mode = S_IFDIR | 0755; 298 fhandle_t fhp; 299 int fd; 300 char buf[8192]; 301 ssize_t r; 302 303 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 304 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 305 SET_OUT_HEADER_LEN(out, entry); 306 out.body.entry.attr.mode = mode; 307 out.body.entry.nodeid = ino; 308 out.body.entry.generation = 1; 309 out.body.entry.attr_valid = UINT64_MAX; 310 out.body.entry.entry_valid = 0; 311 }))); 312 313 EXPECT_LOOKUP(ino, ".") 314 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 315 SET_OUT_HEADER_LEN(out, entry); 316 out.body.entry.attr.mode = mode; 317 out.body.entry.nodeid = ino; 318 out.body.entry.generation = 1; 319 out.body.entry.attr_valid = UINT64_MAX; 320 out.body.entry.entry_valid = 0; 321 }))); 322 323 expect_opendir(ino); 324 325 EXPECT_CALL(*m_mock, process( 326 ResultOf([=](auto in) { 327 return (in.header.opcode == FUSE_READDIR && 328 in.header.nodeid == ino && 329 in.body.readdir.size == sizeof(buf)); 330 }, Eq(true)), 331 _) 332 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 333 out.header.error = 0; 334 out.header.len = sizeof(out.header); 335 }))); 336 337 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 338 fd = fhopen(&fhp, O_DIRECTORY); 339 ASSERT_LE(0, fd) << strerror(errno); 340 r = getdirentries(fd, buf, sizeof(buf), 0); 341 ASSERT_EQ(0, r) << strerror(errno); 342 343 leak(fd); 344 } 345