1 /*- 2 * Copyright (c) 2019 The FreeBSD Foundation 3 * All rights reserved. 4 * 5 * This software was developed by BFF Storage Systems, LLC under sponsorship 6 * from the FreeBSD Foundation. 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 1. Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 2. Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in the 15 * documentation and/or other materials provided with the distribution. 16 * 17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 18 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 21 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 23 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 25 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 26 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27 * SUCH DAMAGE. 28 */ 29 30 extern "C" { 31 #include <fcntl.h> 32 #include <semaphore.h> 33 } 34 35 #include "mockfs.hh" 36 #include "utils.hh" 37 38 using namespace testing; 39 40 class Unlink: public FuseTest { 41 public: 42 void expect_getattr(uint64_t ino, mode_t mode) 43 { 44 EXPECT_CALL(*m_mock, process( 45 ResultOf([=](auto in) { 46 return (in.header.opcode == FUSE_GETATTR && 47 in.header.nodeid == ino); 48 }, Eq(true)), 49 _) 50 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 51 SET_OUT_HEADER_LEN(out, attr); 52 out.body.attr.attr.ino = ino; // Must match nodeid 53 out.body.attr.attr.mode = mode; 54 out.body.attr.attr_valid = UINT64_MAX; 55 }))); 56 } 57 58 void expect_lookup(const char *relpath, uint64_t ino, int times, int nlink=1) 59 { 60 EXPECT_LOOKUP(FUSE_ROOT_ID, relpath) 61 .Times(times) 62 .WillRepeatedly(Invoke( 63 ReturnImmediate([=](auto in __unused, auto& out) { 64 SET_OUT_HEADER_LEN(out, entry); 65 out.body.entry.attr.mode = S_IFREG | 0644; 66 out.body.entry.nodeid = ino; 67 out.body.entry.attr.nlink = nlink; 68 out.body.entry.attr_valid = UINT64_MAX; 69 out.body.entry.attr.size = 0; 70 }))); 71 } 72 73 }; 74 75 /* 76 * Unlinking a multiply linked file should update its ctime and nlink. This 77 * could be handled simply by invalidating the attributes, necessitating a new 78 * GETATTR, but we implement it in-kernel for efficiency's sake. 79 */ 80 TEST_F(Unlink, attr_cache) 81 { 82 const char FULLPATH0[] = "mountpoint/some_file.txt"; 83 const char RELPATH0[] = "some_file.txt"; 84 const char FULLPATH1[] = "mountpoint/other_file.txt"; 85 const char RELPATH1[] = "other_file.txt"; 86 uint64_t ino = 42; 87 struct stat sb_old, sb_new; 88 int fd1; 89 90 expect_getattr(1, S_IFDIR | 0755); 91 expect_lookup(RELPATH0, ino, 1, 2); 92 expect_lookup(RELPATH1, ino, 1, 2); 93 expect_open(ino, 0, 1); 94 expect_unlink(1, RELPATH0, 0); 95 96 fd1 = open(FULLPATH1, O_RDONLY); 97 ASSERT_LE(0, fd1) << strerror(errno); 98 99 ASSERT_EQ(0, fstat(fd1, &sb_old)) << strerror(errno); 100 ASSERT_EQ(0, unlink(FULLPATH0)) << strerror(errno); 101 ASSERT_EQ(0, fstat(fd1, &sb_new)) << strerror(errno); 102 EXPECT_NE(sb_old.st_ctime, sb_new.st_ctime); 103 EXPECT_EQ(1u, sb_new.st_nlink); 104 105 leak(fd1); 106 } 107 108 /* 109 * A successful unlink should clear the parent directory's attribute cache, 110 * because the fuse daemon should update its mtime and ctime 111 */ 112 TEST_F(Unlink, parent_attr_cache) 113 { 114 const char FULLPATH[] = "mountpoint/some_file.txt"; 115 const char RELPATH[] = "some_file.txt"; 116 struct stat sb; 117 uint64_t ino = 42; 118 119 EXPECT_CALL(*m_mock, process( 120 ResultOf([=](auto in) { 121 return (in.header.opcode == FUSE_GETATTR && 122 in.header.nodeid == FUSE_ROOT_ID); 123 }, Eq(true)), 124 _) 125 ).Times(2) 126 .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 127 SET_OUT_HEADER_LEN(out, attr); 128 out.body.attr.attr.ino = ino; // Must match nodeid 129 out.body.attr.attr.mode = S_IFDIR | 0755; 130 out.body.attr.attr_valid = UINT64_MAX; 131 }))); 132 /* Use nlink=2 so we don't get a FUSE_FORGET */ 133 expect_lookup(RELPATH, ino, 1, 2); 134 expect_unlink(1, RELPATH, 0); 135 136 ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno); 137 EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); 138 } 139 140 TEST_F(Unlink, eperm) 141 { 142 const char FULLPATH[] = "mountpoint/some_file.txt"; 143 const char RELPATH[] = "some_file.txt"; 144 uint64_t ino = 42; 145 146 expect_getattr(1, S_IFDIR | 0755); 147 expect_lookup(RELPATH, ino, 1); 148 expect_unlink(1, RELPATH, EPERM); 149 150 ASSERT_NE(0, unlink(FULLPATH)); 151 ASSERT_EQ(EPERM, errno); 152 } 153 154 /* 155 * Unlinking a file should expire its entry cache, even if it's multiply linked 156 */ 157 TEST_F(Unlink, entry_cache) 158 { 159 const char FULLPATH[] = "mountpoint/some_file.txt"; 160 const char RELPATH[] = "some_file.txt"; 161 uint64_t ino = 42; 162 163 expect_getattr(1, S_IFDIR | 0755); 164 expect_lookup(RELPATH, ino, 2, 2); 165 expect_unlink(1, RELPATH, 0); 166 167 ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno); 168 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 169 } 170 171 /* 172 * Unlink a multiply-linked file. There should be no FUSE_FORGET because the 173 * file is still linked. 174 */ 175 TEST_F(Unlink, multiply_linked) 176 { 177 const char FULLPATH0[] = "mountpoint/some_file.txt"; 178 const char RELPATH0[] = "some_file.txt"; 179 const char FULLPATH1[] = "mountpoint/other_file.txt"; 180 const char RELPATH1[] = "other_file.txt"; 181 uint64_t ino = 42; 182 183 expect_getattr(1, S_IFDIR | 0755); 184 expect_lookup(RELPATH0, ino, 1, 2); 185 expect_unlink(1, RELPATH0, 0); 186 EXPECT_CALL(*m_mock, process( 187 ResultOf([=](auto in) { 188 return (in.header.opcode == FUSE_FORGET && 189 in.header.nodeid == ino); 190 }, Eq(true)), 191 _) 192 ).Times(0); 193 expect_lookup(RELPATH1, ino, 1, 1); 194 195 ASSERT_EQ(0, unlink(FULLPATH0)) << strerror(errno); 196 197 /* 198 * The final syscall simply ensures that no FUSE_FORGET was ever sent, 199 * by scheduling an arbitrary different operation after a FUSE_FORGET 200 * would've been sent. 201 */ 202 ASSERT_EQ(0, access(FULLPATH1, F_OK)) << strerror(errno); 203 } 204 205 TEST_F(Unlink, ok) 206 { 207 const char FULLPATH[] = "mountpoint/some_file.txt"; 208 const char RELPATH[] = "some_file.txt"; 209 uint64_t ino = 42; 210 sem_t sem; 211 212 ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); 213 214 expect_getattr(1, S_IFDIR | 0755); 215 expect_lookup(RELPATH, ino, 1); 216 expect_unlink(1, RELPATH, 0); 217 expect_forget(ino, 1, &sem); 218 219 ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno); 220 sem_wait(&sem); 221 sem_destroy(&sem); 222 } 223 224 /* Unlink an open file */ 225 TEST_F(Unlink, open_but_deleted) 226 { 227 const char FULLPATH0[] = "mountpoint/some_file.txt"; 228 const char RELPATH0[] = "some_file.txt"; 229 const char FULLPATH1[] = "mountpoint/other_file.txt"; 230 const char RELPATH1[] = "other_file.txt"; 231 uint64_t ino = 42; 232 int fd; 233 234 expect_getattr(1, S_IFDIR | 0755); 235 expect_lookup(RELPATH0, ino, 2); 236 expect_open(ino, 0, 1); 237 expect_unlink(1, RELPATH0, 0); 238 expect_lookup(RELPATH1, ino, 1, 1); 239 240 fd = open(FULLPATH0, O_RDWR); 241 ASSERT_LE(0, fd) << strerror(errno); 242 ASSERT_EQ(0, unlink(FULLPATH0)) << strerror(errno); 243 244 /* 245 * The final syscall simply ensures that no FUSE_FORGET was ever sent, 246 * by scheduling an arbitrary different operation after a FUSE_FORGET 247 * would've been sent. 248 */ 249 ASSERT_EQ(0, access(FULLPATH1, F_OK)) << strerror(errno); 250 leak(fd); 251 } 252