xref: /freebsd/tests/sys/fs/fusefs/unlink.cc (revision ae41709ab46305df80f7f35bb478a3c8ebf22ebb)
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