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 * $FreeBSD$ 30 */ 31 32 extern "C" { 33 #include <fcntl.h> 34 #include <semaphore.h> 35 } 36 37 #include "mockfs.hh" 38 #include "utils.hh" 39 40 using namespace testing; 41 42 class Unlink: public FuseTest { 43 public: 44 void expect_getattr(uint64_t ino, mode_t mode) 45 { 46 EXPECT_CALL(*m_mock, process( 47 ResultOf([=](auto in) { 48 return (in.header.opcode == FUSE_GETATTR && 49 in.header.nodeid == ino); 50 }, Eq(true)), 51 _) 52 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 53 SET_OUT_HEADER_LEN(out, attr); 54 out.body.attr.attr.ino = ino; // Must match nodeid 55 out.body.attr.attr.mode = mode; 56 out.body.attr.attr_valid = UINT64_MAX; 57 }))); 58 } 59 60 void expect_lookup(const char *relpath, uint64_t ino, int times, int nlink=1) 61 { 62 EXPECT_LOOKUP(FUSE_ROOT_ID, relpath) 63 .Times(times) 64 .WillRepeatedly(Invoke( 65 ReturnImmediate([=](auto in __unused, auto& out) { 66 SET_OUT_HEADER_LEN(out, entry); 67 out.body.entry.attr.mode = S_IFREG | 0644; 68 out.body.entry.nodeid = ino; 69 out.body.entry.attr.nlink = nlink; 70 out.body.entry.attr_valid = UINT64_MAX; 71 out.body.entry.attr.size = 0; 72 }))); 73 } 74 75 }; 76 77 /* 78 * Unlinking a multiply linked file should update its ctime and nlink. This 79 * could be handled simply by invalidating the attributes, necessitating a new 80 * GETATTR, but we implement it in-kernel for efficiency's sake. 81 */ 82 TEST_F(Unlink, attr_cache) 83 { 84 const char FULLPATH0[] = "mountpoint/some_file.txt"; 85 const char RELPATH0[] = "some_file.txt"; 86 const char FULLPATH1[] = "mountpoint/other_file.txt"; 87 const char RELPATH1[] = "other_file.txt"; 88 uint64_t ino = 42; 89 struct stat sb_old, sb_new; 90 int fd1; 91 92 expect_getattr(1, S_IFDIR | 0755); 93 expect_lookup(RELPATH0, ino, 1, 2); 94 expect_lookup(RELPATH1, ino, 1, 2); 95 expect_open(ino, 0, 1); 96 expect_unlink(1, RELPATH0, 0); 97 98 fd1 = open(FULLPATH1, O_RDONLY); 99 ASSERT_LE(0, fd1) << strerror(errno); 100 101 ASSERT_EQ(0, fstat(fd1, &sb_old)) << strerror(errno); 102 ASSERT_EQ(0, unlink(FULLPATH0)) << strerror(errno); 103 ASSERT_EQ(0, fstat(fd1, &sb_new)) << strerror(errno); 104 EXPECT_NE(sb_old.st_ctime, sb_new.st_ctime); 105 EXPECT_EQ(1u, sb_new.st_nlink); 106 107 leak(fd1); 108 } 109 110 /* 111 * A successful unlink should clear the parent directory's attribute cache, 112 * because the fuse daemon should update its mtime and ctime 113 */ 114 TEST_F(Unlink, parent_attr_cache) 115 { 116 const char FULLPATH[] = "mountpoint/some_file.txt"; 117 const char RELPATH[] = "some_file.txt"; 118 struct stat sb; 119 uint64_t ino = 42; 120 121 EXPECT_CALL(*m_mock, process( 122 ResultOf([=](auto in) { 123 return (in.header.opcode == FUSE_GETATTR && 124 in.header.nodeid == FUSE_ROOT_ID); 125 }, Eq(true)), 126 _) 127 ).Times(2) 128 .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 129 SET_OUT_HEADER_LEN(out, attr); 130 out.body.attr.attr.ino = ino; // Must match nodeid 131 out.body.attr.attr.mode = S_IFDIR | 0755; 132 out.body.attr.attr_valid = UINT64_MAX; 133 }))); 134 /* Use nlink=2 so we don't get a FUSE_FORGET */ 135 expect_lookup(RELPATH, ino, 1, 2); 136 expect_unlink(1, RELPATH, 0); 137 138 ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno); 139 EXPECT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); 140 } 141 142 TEST_F(Unlink, eperm) 143 { 144 const char FULLPATH[] = "mountpoint/some_file.txt"; 145 const char RELPATH[] = "some_file.txt"; 146 uint64_t ino = 42; 147 148 expect_getattr(1, S_IFDIR | 0755); 149 expect_lookup(RELPATH, ino, 1); 150 expect_unlink(1, RELPATH, EPERM); 151 152 ASSERT_NE(0, unlink(FULLPATH)); 153 ASSERT_EQ(EPERM, errno); 154 } 155 156 /* 157 * Unlinking a file should expire its entry cache, even if it's multiply linked 158 */ 159 TEST_F(Unlink, entry_cache) 160 { 161 const char FULLPATH[] = "mountpoint/some_file.txt"; 162 const char RELPATH[] = "some_file.txt"; 163 uint64_t ino = 42; 164 165 expect_getattr(1, S_IFDIR | 0755); 166 expect_lookup(RELPATH, ino, 2, 2); 167 expect_unlink(1, RELPATH, 0); 168 169 ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno); 170 ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno); 171 } 172 173 /* 174 * Unlink a multiply-linked file. There should be no FUSE_FORGET because the 175 * file is still linked. 176 */ 177 TEST_F(Unlink, multiply_linked) 178 { 179 const char FULLPATH0[] = "mountpoint/some_file.txt"; 180 const char RELPATH0[] = "some_file.txt"; 181 const char FULLPATH1[] = "mountpoint/other_file.txt"; 182 const char RELPATH1[] = "other_file.txt"; 183 uint64_t ino = 42; 184 185 expect_getattr(1, S_IFDIR | 0755); 186 expect_lookup(RELPATH0, ino, 1, 2); 187 expect_unlink(1, RELPATH0, 0); 188 EXPECT_CALL(*m_mock, process( 189 ResultOf([=](auto in) { 190 return (in.header.opcode == FUSE_FORGET && 191 in.header.nodeid == ino); 192 }, Eq(true)), 193 _) 194 ).Times(0); 195 expect_lookup(RELPATH1, ino, 1, 1); 196 197 ASSERT_EQ(0, unlink(FULLPATH0)) << strerror(errno); 198 199 /* 200 * The final syscall simply ensures that no FUSE_FORGET was ever sent, 201 * by scheduling an arbitrary different operation after a FUSE_FORGET 202 * would've been sent. 203 */ 204 ASSERT_EQ(0, access(FULLPATH1, F_OK)) << strerror(errno); 205 } 206 207 TEST_F(Unlink, ok) 208 { 209 const char FULLPATH[] = "mountpoint/some_file.txt"; 210 const char RELPATH[] = "some_file.txt"; 211 uint64_t ino = 42; 212 sem_t sem; 213 214 ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); 215 216 expect_getattr(1, S_IFDIR | 0755); 217 expect_lookup(RELPATH, ino, 1); 218 expect_unlink(1, RELPATH, 0); 219 expect_forget(ino, 1, &sem); 220 221 ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno); 222 sem_wait(&sem); 223 sem_destroy(&sem); 224 } 225 226 /* Unlink an open file */ 227 TEST_F(Unlink, open_but_deleted) 228 { 229 const char FULLPATH0[] = "mountpoint/some_file.txt"; 230 const char RELPATH0[] = "some_file.txt"; 231 const char FULLPATH1[] = "mountpoint/other_file.txt"; 232 const char RELPATH1[] = "other_file.txt"; 233 uint64_t ino = 42; 234 int fd; 235 236 expect_getattr(1, S_IFDIR | 0755); 237 expect_lookup(RELPATH0, ino, 2); 238 expect_open(ino, 0, 1); 239 expect_unlink(1, RELPATH0, 0); 240 expect_lookup(RELPATH1, ino, 1, 1); 241 242 fd = open(FULLPATH0, O_RDWR); 243 ASSERT_LE(0, fd) << strerror(errno); 244 ASSERT_EQ(0, unlink(FULLPATH0)) << strerror(errno); 245 246 /* 247 * The final syscall simply ensures that no FUSE_FORGET was ever sent, 248 * by scheduling an arbitrary different operation after a FUSE_FORGET 249 * would've been sent. 250 */ 251 ASSERT_EQ(0, access(FULLPATH1, F_OK)) << strerror(errno); 252 leak(fd); 253 } 254