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