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