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 /* This file tests functionality needed by NFS servers */ 34 extern "C" { 35 #include <sys/param.h> 36 #include <sys/mount.h> 37 38 #include <dirent.h> 39 #include <fcntl.h> 40 #include <unistd.h> 41 } 42 43 #include "mockfs.hh" 44 #include "utils.hh" 45 46 using namespace std; 47 using namespace testing; 48 49 50 class Nfs: public FuseTest { 51 public: 52 virtual void SetUp() { 53 if (geteuid() != 0) 54 GTEST_SKIP() << "This test requires a privileged user"; 55 FuseTest::SetUp(); 56 } 57 }; 58 59 class Exportable: public Nfs { 60 public: 61 virtual void SetUp() { 62 m_init_flags = FUSE_EXPORT_SUPPORT; 63 Nfs::SetUp(); 64 } 65 }; 66 67 class Fhstat: public Exportable {}; 68 class FhstatNotExportable: public Nfs {}; 69 class Getfh: public Exportable {}; 70 class Readdir: public Exportable {}; 71 72 /* If the server returns a different generation number, then file is stale */ 73 TEST_F(Fhstat, estale) 74 { 75 const char FULLPATH[] = "mountpoint/some_dir/."; 76 const char RELDIRPATH[] = "some_dir"; 77 fhandle_t fhp; 78 struct stat sb; 79 const uint64_t ino = 42; 80 const mode_t mode = S_IFDIR | 0755; 81 Sequence seq; 82 83 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 84 .InSequence(seq) 85 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 86 SET_OUT_HEADER_LEN(out, entry); 87 out.body.entry.attr.mode = mode; 88 out.body.entry.nodeid = ino; 89 out.body.entry.generation = 1; 90 out.body.entry.attr_valid = UINT64_MAX; 91 out.body.entry.entry_valid = 0; 92 }))); 93 94 EXPECT_LOOKUP(ino, ".") 95 .InSequence(seq) 96 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 97 SET_OUT_HEADER_LEN(out, entry); 98 out.body.entry.attr.mode = mode; 99 out.body.entry.nodeid = 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.generation = 1; 127 out.body.entry.attr.uid = uid; 128 out.body.entry.attr_valid = UINT64_MAX; 129 out.body.entry.entry_valid = 0; 130 }))); 131 132 EXPECT_LOOKUP(ino, ".") 133 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 134 SET_OUT_HEADER_LEN(out, entry); 135 out.body.entry.attr.mode = mode; 136 out.body.entry.nodeid = ino; 137 out.body.entry.generation = 1; 138 out.body.entry.attr.uid = uid; 139 out.body.entry.attr_valid = UINT64_MAX; 140 out.body.entry.entry_valid = 0; 141 }))); 142 143 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 144 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 145 EXPECT_EQ(uid, sb.st_uid); 146 EXPECT_EQ(mode, sb.st_mode); 147 } 148 149 /* Use a file handle whose entry is still cached */ 150 TEST_F(Fhstat, cached) 151 { 152 const char FULLPATH[] = "mountpoint/some_dir/."; 153 const char RELDIRPATH[] = "some_dir"; 154 fhandle_t fhp; 155 struct stat sb; 156 const uint64_t ino = 42; 157 const mode_t mode = S_IFDIR | 0755; 158 159 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 160 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 161 SET_OUT_HEADER_LEN(out, entry); 162 out.body.entry.attr.mode = mode; 163 out.body.entry.nodeid = ino; 164 out.body.entry.generation = 1; 165 out.body.entry.attr.ino = ino; 166 out.body.entry.attr_valid = UINT64_MAX; 167 out.body.entry.entry_valid = UINT64_MAX; 168 }))); 169 170 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 171 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 172 EXPECT_EQ(ino, sb.st_ino); 173 } 174 175 /* File handle entries should expire from the cache, too */ 176 TEST_F(Fhstat, cache_expired) 177 { 178 const char FULLPATH[] = "mountpoint/some_dir/."; 179 const char RELDIRPATH[] = "some_dir"; 180 fhandle_t fhp; 181 struct stat sb; 182 const uint64_t ino = 42; 183 const mode_t mode = S_IFDIR | 0755; 184 185 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 186 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 187 SET_OUT_HEADER_LEN(out, entry); 188 out.body.entry.attr.mode = mode; 189 out.body.entry.nodeid = ino; 190 out.body.entry.generation = 1; 191 out.body.entry.attr.ino = ino; 192 out.body.entry.attr_valid = UINT64_MAX; 193 out.body.entry.entry_valid_nsec = NAP_NS / 2; 194 }))); 195 196 EXPECT_LOOKUP(ino, ".") 197 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 198 SET_OUT_HEADER_LEN(out, entry); 199 out.body.entry.attr.mode = mode; 200 out.body.entry.nodeid = ino; 201 out.body.entry.generation = 1; 202 out.body.entry.attr.ino = ino; 203 out.body.entry.attr_valid = UINT64_MAX; 204 out.body.entry.entry_valid = 0; 205 }))); 206 207 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 208 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 209 EXPECT_EQ(ino, sb.st_ino); 210 211 nap(); 212 213 /* Cache should be expired; fuse should issue a FUSE_LOOKUP */ 214 ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); 215 EXPECT_EQ(ino, sb.st_ino); 216 } 217 218 /* 219 * If the server doesn't set FUSE_EXPORT_SUPPORT, then we can't do NFS-style 220 * lookups 221 */ 222 TEST_F(FhstatNotExportable, lookup_dot) 223 { 224 const char FULLPATH[] = "mountpoint/some_dir/."; 225 const char RELDIRPATH[] = "some_dir"; 226 fhandle_t fhp; 227 const uint64_t ino = 42; 228 const mode_t mode = S_IFDIR | 0755; 229 230 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 231 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 232 SET_OUT_HEADER_LEN(out, entry); 233 out.body.entry.attr.mode = mode; 234 out.body.entry.nodeid = ino; 235 out.body.entry.generation = 1; 236 out.body.entry.attr_valid = UINT64_MAX; 237 out.body.entry.entry_valid = 0; 238 }))); 239 240 ASSERT_EQ(-1, getfh(FULLPATH, &fhp)); 241 ASSERT_EQ(EOPNOTSUPP, errno); 242 } 243 244 /* FreeBSD's fid struct doesn't have enough space for 64-bit generations */ 245 TEST_F(Getfh, eoverflow) 246 { 247 const char FULLPATH[] = "mountpoint/some_dir/."; 248 const char RELDIRPATH[] = "some_dir"; 249 fhandle_t fhp; 250 uint64_t ino = 42; 251 252 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 253 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 254 SET_OUT_HEADER_LEN(out, entry); 255 out.body.entry.attr.mode = S_IFDIR | 0755; 256 out.body.entry.nodeid = ino; 257 out.body.entry.generation = (uint64_t)UINT32_MAX + 1; 258 out.body.entry.attr_valid = UINT64_MAX; 259 out.body.entry.entry_valid = UINT64_MAX; 260 }))); 261 262 ASSERT_NE(0, getfh(FULLPATH, &fhp)); 263 EXPECT_EQ(EOVERFLOW, errno); 264 } 265 266 /* Get an NFS file handle */ 267 TEST_F(Getfh, ok) 268 { 269 const char FULLPATH[] = "mountpoint/some_dir/."; 270 const char RELDIRPATH[] = "some_dir"; 271 fhandle_t fhp; 272 uint64_t ino = 42; 273 274 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) 275 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 276 SET_OUT_HEADER_LEN(out, entry); 277 out.body.entry.attr.mode = S_IFDIR | 0755; 278 out.body.entry.nodeid = ino; 279 out.body.entry.attr_valid = UINT64_MAX; 280 out.body.entry.entry_valid = UINT64_MAX; 281 }))); 282 283 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 284 } 285 286 /* 287 * Call readdir via a file handle. 288 * 289 * This is how a userspace nfs server like nfs-ganesha or unfs3 would call 290 * readdir. The in-kernel NFS server never does any equivalent of open. I 291 * haven't discovered a way to mimic nfsd's behavior short of actually running 292 * nfsd. 293 */ 294 TEST_F(Readdir, getdirentries) 295 { 296 const char FULLPATH[] = "mountpoint/some_dir"; 297 const char RELPATH[] = "some_dir"; 298 uint64_t ino = 42; 299 mode_t mode = S_IFDIR | 0755; 300 fhandle_t fhp; 301 int fd; 302 char buf[8192]; 303 ssize_t r; 304 305 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) 306 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 307 SET_OUT_HEADER_LEN(out, entry); 308 out.body.entry.attr.mode = mode; 309 out.body.entry.nodeid = ino; 310 out.body.entry.generation = 1; 311 out.body.entry.attr_valid = UINT64_MAX; 312 out.body.entry.entry_valid = 0; 313 }))); 314 315 EXPECT_LOOKUP(ino, ".") 316 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 317 SET_OUT_HEADER_LEN(out, entry); 318 out.body.entry.attr.mode = mode; 319 out.body.entry.nodeid = ino; 320 out.body.entry.generation = 1; 321 out.body.entry.attr_valid = UINT64_MAX; 322 out.body.entry.entry_valid = 0; 323 }))); 324 325 expect_opendir(ino); 326 327 EXPECT_CALL(*m_mock, process( 328 ResultOf([=](auto in) { 329 return (in.header.opcode == FUSE_READDIR && 330 in.header.nodeid == ino && 331 in.body.readdir.size == sizeof(buf)); 332 }, Eq(true)), 333 _) 334 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 335 out.header.error = 0; 336 out.header.len = sizeof(out.header); 337 }))); 338 339 ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); 340 fd = fhopen(&fhp, O_DIRECTORY); 341 ASSERT_LE(0, fd) << strerror(errno); 342 r = getdirentries(fd, buf, sizeof(buf), 0); 343 ASSERT_EQ(0, r) << strerror(errno); 344 345 leak(fd); 346 } 347