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 <fcntl.h> 33 } 34 35 #include "mockfs.hh" 36 #include "utils.hh" 37 38 using namespace testing; 39 40 class Create: public FuseTest { 41 public: 42 43 void expect_create(const char *relpath, mode_t mode, ProcessMockerT r) 44 { 45 EXPECT_CALL(*m_mock, process( 46 ResultOf([=](auto in) { 47 const char *name = (const char*)in->body.bytes + 48 sizeof(fuse_open_in); 49 return (in->header.opcode == FUSE_CREATE && 50 in->body.open.mode == mode && 51 (0 == strcmp(relpath, name))); 52 }, Eq(true)), 53 _) 54 ).WillOnce(Invoke(r)); 55 } 56 57 }; 58 59 /* 60 * If FUSE_CREATE sets attr_valid, then subsequent GETATTRs should use the 61 * attribute cache 62 */ 63 TEST_F(Create, attr_cache) 64 { 65 const char FULLPATH[] = "mountpoint/some_file.txt"; 66 const char RELPATH[] = "some_file.txt"; 67 mode_t mode = S_IFREG | 0755; 68 uint64_t ino = 42; 69 int fd; 70 71 EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT))); 72 expect_create(RELPATH, mode, 73 ReturnImmediate([=](auto in __unused, auto out) { 74 SET_OUT_HEADER_LEN(out, create); 75 out->body.create.entry.attr.mode = mode; 76 out->body.create.entry.nodeid = ino; 77 out->body.create.entry.entry_valid = UINT64_MAX; 78 out->body.create.entry.attr_valid = UINT64_MAX; 79 })); 80 81 EXPECT_CALL(*m_mock, process( 82 ResultOf([=](auto in) { 83 return (in->header.opcode == FUSE_GETATTR && 84 in->header.nodeid == ino); 85 }, Eq(true)), 86 _) 87 ).Times(0); 88 89 fd = open(FULLPATH, O_CREAT | O_EXCL, mode); 90 EXPECT_LE(0, fd) << strerror(errno); 91 /* Deliberately leak fd. close(2) will be tested in release.cc */ 92 } 93 94 /* A successful CREATE operation should purge the parent dir's attr cache */ 95 TEST_F(Create, clear_attr_cache) 96 { 97 const char FULLPATH[] = "mountpoint/src"; 98 const char RELPATH[] = "src"; 99 mode_t mode = S_IFREG | 0755; 100 uint64_t ino = 42; 101 int fd; 102 struct stat sb; 103 104 EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT))); 105 EXPECT_CALL(*m_mock, process( 106 ResultOf([=](auto in) { 107 return (in->header.opcode == FUSE_GETATTR && 108 in->header.nodeid == 1); 109 }, Eq(true)), 110 _) 111 ).Times(2) 112 .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto out) { 113 SET_OUT_HEADER_LEN(out, attr); 114 out->body.attr.attr.ino = 1; 115 out->body.attr.attr.mode = S_IFDIR | 0755; 116 out->body.attr.attr_valid = UINT64_MAX; 117 }))); 118 119 expect_create(RELPATH, mode, 120 ReturnImmediate([=](auto in __unused, auto out) { 121 SET_OUT_HEADER_LEN(out, create); 122 out->body.create.entry.attr.mode = mode; 123 out->body.create.entry.nodeid = ino; 124 out->body.create.entry.entry_valid = UINT64_MAX; 125 out->body.create.entry.attr_valid = UINT64_MAX; 126 })); 127 128 EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); 129 fd = open(FULLPATH, O_CREAT | O_EXCL, mode); 130 EXPECT_LE(0, fd) << strerror(errno); 131 EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); 132 133 /* Deliberately leak fd. close(2) will be tested in release.cc */ 134 } 135 136 /* 137 * The fuse daemon fails the request with EEXIST. This usually indicates a 138 * race condition: some other FUSE client created the file in between when the 139 * kernel checked for it with lookup and tried to create it with create 140 */ 141 TEST_F(Create, eexist) 142 { 143 const char FULLPATH[] = "mountpoint/some_file.txt"; 144 const char RELPATH[] = "some_file.txt"; 145 mode_t mode = S_IFREG | 0755; 146 147 EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT))); 148 expect_create(RELPATH, mode, ReturnErrno(EEXIST)); 149 EXPECT_NE(0, open(FULLPATH, O_CREAT | O_EXCL, mode)); 150 EXPECT_EQ(EEXIST, errno); 151 } 152 153 /* 154 * If the daemon doesn't implement FUSE_CREATE, then the kernel should fallback 155 * to FUSE_MKNOD/FUSE_OPEN 156 */ 157 TEST_F(Create, Enosys) 158 { 159 const char FULLPATH[] = "mountpoint/some_file.txt"; 160 const char RELPATH[] = "some_file.txt"; 161 mode_t mode = S_IFREG | 0755; 162 uint64_t ino = 42; 163 int fd; 164 165 EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT))); 166 expect_create(RELPATH, mode, ReturnErrno(ENOSYS)); 167 168 EXPECT_CALL(*m_mock, process( 169 ResultOf([=](auto in) { 170 const char *name = (const char*)in->body.bytes + 171 sizeof(fuse_mknod_in); 172 return (in->header.opcode == FUSE_MKNOD && 173 in->body.mknod.mode == (S_IFREG | mode) && 174 in->body.mknod.rdev == 0 && 175 (0 == strcmp(RELPATH, name))); 176 }, Eq(true)), 177 _) 178 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) { 179 SET_OUT_HEADER_LEN(out, entry); 180 out->body.entry.attr.mode = mode; 181 out->body.entry.nodeid = ino; 182 out->body.entry.entry_valid = UINT64_MAX; 183 out->body.entry.attr_valid = UINT64_MAX; 184 }))); 185 186 EXPECT_CALL(*m_mock, process( 187 ResultOf([=](auto in) { 188 return (in->header.opcode == FUSE_OPEN && 189 in->header.nodeid == ino); 190 }, Eq(true)), 191 _) 192 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto out) { 193 out->header.len = sizeof(out->header); 194 SET_OUT_HEADER_LEN(out, open); 195 }))); 196 197 fd = open(FULLPATH, O_CREAT | O_EXCL, mode); 198 EXPECT_LE(0, fd) << strerror(errno); 199 /* Deliberately leak fd. close(2) will be tested in release.cc */ 200 } 201 202 /* 203 * Creating a new file after FUSE_LOOKUP returned a negative cache entry 204 */ 205 TEST_F(Create, entry_cache_negative) 206 { 207 const char FULLPATH[] = "mountpoint/some_file.txt"; 208 const char RELPATH[] = "some_file.txt"; 209 mode_t mode = S_IFREG | 0755; 210 uint64_t ino = 42; 211 int fd; 212 /* 213 * Set entry_valid = 0 because this test isn't concerned with whether 214 * or not we actually cache negative entries, only with whether we 215 * interpret negative cache responses correctly. 216 */ 217 struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0}; 218 219 /* create will first do a LOOKUP, adding a negative cache entry */ 220 EXPECT_LOOKUP(1, RELPATH).WillOnce(ReturnNegativeCache(&entry_valid)); 221 expect_create(RELPATH, mode, 222 ReturnImmediate([=](auto in __unused, auto out) { 223 SET_OUT_HEADER_LEN(out, create); 224 out->body.create.entry.attr.mode = mode; 225 out->body.create.entry.nodeid = ino; 226 out->body.create.entry.entry_valid = UINT64_MAX; 227 out->body.create.entry.attr_valid = UINT64_MAX; 228 })); 229 230 fd = open(FULLPATH, O_CREAT | O_EXCL, mode); 231 ASSERT_LE(0, fd) << strerror(errno); 232 /* Deliberately leak fd. close(2) will be tested in release.cc */ 233 } 234 235 /* 236 * Creating a new file should purge any negative namecache entries 237 */ 238 TEST_F(Create, entry_cache_negative_purge) 239 { 240 const char FULLPATH[] = "mountpoint/some_file.txt"; 241 const char RELPATH[] = "some_file.txt"; 242 mode_t mode = S_IFREG | 0755; 243 uint64_t ino = 42; 244 int fd; 245 struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0}; 246 247 /* create will first do a LOOKUP, adding a negative cache entry */ 248 EXPECT_LOOKUP(1, RELPATH).Times(1) 249 .WillOnce(Invoke(ReturnNegativeCache(&entry_valid))) 250 .RetiresOnSaturation(); 251 252 /* Then the CREATE should purge the negative cache entry */ 253 expect_create(RELPATH, mode, 254 ReturnImmediate([=](auto in __unused, auto out) { 255 SET_OUT_HEADER_LEN(out, create); 256 out->body.create.entry.attr.mode = mode; 257 out->body.create.entry.nodeid = ino; 258 out->body.create.entry.attr_valid = UINT64_MAX; 259 })); 260 261 fd = open(FULLPATH, O_CREAT | O_EXCL, mode); 262 ASSERT_LE(0, fd) << strerror(errno); 263 264 /* Finally, a subsequent lookup should query the daemon */ 265 expect_lookup(RELPATH, ino, S_IFREG | mode, 0, 1); 266 267 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 268 /* Deliberately leak fd. close(2) will be tested in release.cc */ 269 } 270 271 /* 272 * The daemon is responsible for checking file permissions (unless the 273 * default_permissions mount option was used) 274 */ 275 TEST_F(Create, eperm) 276 { 277 const char FULLPATH[] = "mountpoint/some_file.txt"; 278 const char RELPATH[] = "some_file.txt"; 279 mode_t mode = S_IFREG | 0755; 280 281 EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT))); 282 expect_create(RELPATH, mode, ReturnErrno(EPERM)); 283 284 EXPECT_NE(0, open(FULLPATH, O_CREAT | O_EXCL, mode)); 285 EXPECT_EQ(EPERM, errno); 286 } 287 288 TEST_F(Create, ok) 289 { 290 const char FULLPATH[] = "mountpoint/some_file.txt"; 291 const char RELPATH[] = "some_file.txt"; 292 mode_t mode = S_IFREG | 0755; 293 uint64_t ino = 42; 294 int fd; 295 296 EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT))); 297 expect_create(RELPATH, mode, 298 ReturnImmediate([=](auto in __unused, auto out) { 299 SET_OUT_HEADER_LEN(out, create); 300 out->body.create.entry.attr.mode = mode; 301 out->body.create.entry.nodeid = ino; 302 out->body.create.entry.entry_valid = UINT64_MAX; 303 out->body.create.entry.attr_valid = UINT64_MAX; 304 })); 305 306 fd = open(FULLPATH, O_CREAT | O_EXCL, mode); 307 EXPECT_LE(0, fd) << strerror(errno); 308 /* Deliberately leak fd. close(2) will be tested in release.cc */ 309 } 310 311 /* 312 * A regression test for a bug that affected old FUSE implementations: 313 * open(..., O_WRONLY | O_CREAT, 0444) should work despite the seeming 314 * contradiction between O_WRONLY and 0444 315 * 316 * For example: 317 * https://bugs.launchpad.net/ubuntu/+source/sshfs-fuse/+bug/44886 318 */ 319 TEST_F(Create, wronly_0444) 320 { 321 const char FULLPATH[] = "mountpoint/some_file.txt"; 322 const char RELPATH[] = "some_file.txt"; 323 mode_t mode = S_IFREG | 0444; 324 uint64_t ino = 42; 325 int fd; 326 327 EXPECT_LOOKUP(1, RELPATH).WillOnce(Invoke(ReturnErrno(ENOENT))); 328 expect_create(RELPATH, mode, 329 ReturnImmediate([=](auto in __unused, auto out) { 330 SET_OUT_HEADER_LEN(out, create); 331 out->body.create.entry.attr.mode = mode; 332 out->body.create.entry.nodeid = ino; 333 out->body.create.entry.entry_valid = UINT64_MAX; 334 out->body.create.entry.attr_valid = UINT64_MAX; 335 })); 336 337 fd = open(FULLPATH, O_CREAT | O_WRONLY, mode); 338 EXPECT_LE(0, fd) << strerror(errno); 339 /* Deliberately leak fd. close(2) will be tested in release.cc */ 340 } 341