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