1 /*-
2 * SPDX-License-Identifier: BSD-2-Clause
3 *
4 * Copyright (c) 2019 The FreeBSD Foundation
5 *
6 * This software was developed by BFF Storage Systems, LLC under sponsorship
7 * from the FreeBSD Foundation.
8 *
9 * Redistribution and use in source and binary forms, with or without
10 * modification, are permitted provided that the following conditions
11 * are met:
12 * 1. Redistributions of source code must retain the above copyright
13 * notice, this list of conditions and the following disclaimer.
14 * 2. Redistributions in binary form must reproduce the above copyright
15 * notice, this list of conditions and the following disclaimer in the
16 * documentation and/or other materials provided with the distribution.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
19 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
22 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
28 * SUCH DAMAGE.
29 */
30
31 extern "C" {
32 #include <stdlib.h>
33 #include <unistd.h>
34 }
35
36 #include "mockfs.hh"
37 #include "utils.hh"
38
39 using namespace testing;
40
41 class Rename: public FuseTest {
42 public:
43 int tmpfd = -1;
44 char tmpfile[80] = "/tmp/fuse.rename.XXXXXX";
45
TearDown()46 virtual void TearDown() {
47 if (tmpfd >= 0) {
48 close(tmpfd);
49 unlink(tmpfile);
50 }
51
52 FuseTest::TearDown();
53 }
54 };
55
56 // EINVAL, dst is subdir of src
TEST_F(Rename,einval)57 TEST_F(Rename, einval)
58 {
59 const char FULLDST[] = "mountpoint/src/dst";
60 const char RELDST[] = "dst";
61 const char FULLSRC[] = "mountpoint/src";
62 const char RELSRC[] = "src";
63 uint64_t src_ino = 42;
64
65 expect_lookup(RELSRC, src_ino, S_IFDIR | 0755, 0, 2);
66 EXPECT_LOOKUP(src_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
67
68 ASSERT_NE(0, rename(FULLSRC, FULLDST));
69 ASSERT_EQ(EINVAL, errno);
70 }
71
72 // source does not exist
TEST_F(Rename,enoent)73 TEST_F(Rename, enoent)
74 {
75 const char FULLDST[] = "mountpoint/dst";
76 const char FULLSRC[] = "mountpoint/src";
77 const char RELSRC[] = "src";
78 // FUSE hardcodes the mountpoint to inode 1
79
80 EXPECT_LOOKUP(FUSE_ROOT_ID, RELSRC)
81 .WillOnce(Invoke(ReturnErrno(ENOENT)));
82
83 ASSERT_NE(0, rename(FULLSRC, FULLDST));
84 ASSERT_EQ(ENOENT, errno);
85 }
86
87 /*
88 * Renaming a file after FUSE_LOOKUP returned a negative cache entry for dst
89 */
TEST_F(Rename,entry_cache_negative)90 TEST_F(Rename, entry_cache_negative)
91 {
92 const char FULLDST[] = "mountpoint/dst";
93 const char RELDST[] = "dst";
94 const char FULLSRC[] = "mountpoint/src";
95 const char RELSRC[] = "src";
96 uint64_t dst_dir_ino = FUSE_ROOT_ID;
97 uint64_t ino = 42;
98 /*
99 * Set entry_valid = 0 because this test isn't concerned with whether
100 * or not we actually cache negative entries, only with whether we
101 * interpret negative cache responses correctly.
102 */
103 struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0};
104
105 expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
106 /* LOOKUP returns a negative cache entry for dst */
107 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
108 .WillOnce(ReturnNegativeCache(&entry_valid));
109
110 EXPECT_CALL(*m_mock, process(
111 ResultOf([=](auto in) {
112 const char *src = (const char*)in.body.bytes +
113 sizeof(fuse_rename_in);
114 const char *dst = src + strlen(src) + 1;
115 return (in.header.opcode == FUSE_RENAME &&
116 in.body.rename.newdir == dst_dir_ino &&
117 (0 == strcmp(RELDST, dst)) &&
118 (0 == strcmp(RELSRC, src)));
119 }, Eq(true)),
120 _)
121 ).WillOnce(Invoke(ReturnErrno(0)));
122
123 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
124 }
125
126 /*
127 * Renaming a file should purge any negative namecache entries for the dst
128 */
TEST_F(Rename,entry_cache_negative_purge)129 TEST_F(Rename, entry_cache_negative_purge)
130 {
131 const char FULLDST[] = "mountpoint/dst";
132 const char RELDST[] = "dst";
133 const char FULLSRC[] = "mountpoint/src";
134 const char RELSRC[] = "src";
135 uint64_t dst_dir_ino = FUSE_ROOT_ID;
136 uint64_t ino = 42;
137 struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0};
138
139 expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
140 /* LOOKUP returns a negative cache entry for dst */
141 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
142 .WillOnce(ReturnNegativeCache(&entry_valid))
143 .RetiresOnSaturation();
144
145 EXPECT_CALL(*m_mock, process(
146 ResultOf([=](auto in) {
147 const char *src = (const char*)in.body.bytes +
148 sizeof(fuse_rename_in);
149 const char *dst = src + strlen(src) + 1;
150 return (in.header.opcode == FUSE_RENAME &&
151 in.body.rename.newdir == dst_dir_ino &&
152 (0 == strcmp(RELDST, dst)) &&
153 (0 == strcmp(RELSRC, src)));
154 }, Eq(true)),
155 _)
156 ).WillOnce(Invoke(ReturnErrno(0)));
157
158 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
159
160 /* Finally, a subsequent lookup should query the daemon */
161 expect_lookup(RELDST, ino, S_IFREG | 0644, 0, 1);
162
163 ASSERT_EQ(0, access(FULLDST, F_OK)) << strerror(errno);
164 }
165
166 static volatile int stopit = 0;
167
setattr_th(void * arg)168 static void* setattr_th(void* arg) {
169 char *path = (char*)arg;
170
171 while (stopit == 0)
172 chmod(path, 0777);
173 return 0;
174 }
175
176 /*
177 * Rename restarts the syscall to avoid a LOR
178 *
179 * This test triggers a race: the chmod() calls VOP_SETATTR, which locks its
180 * vnode, but fuse_vnop_rename also tries to lock the same vnode. The result
181 * is that, in order to avoid a LOR, fuse_vnop_rename returns ERELOOKUP to
182 * restart the syscall.
183 *
184 * To verify that the race is hit, watch the fusefs:fusefs:vnops:erelookup
185 * dtrace probe while the test is running. On my system, that probe fires more
186 * than 100 times per second during this test.
187 */
TEST_F(Rename,erelookup)188 TEST_F(Rename, erelookup)
189 {
190 const char FULLDST[] = "mountpoint/dstdir/dst";
191 const char RELDSTDIR[] = "dstdir";
192 const char RELDST[] = "dst";
193 const char FULLSRC[] = "mountpoint/src";
194 const char RELSRC[] = "src";
195 pthread_t th0;
196 uint64_t ino = 42;
197 uint64_t dst_dir_ino = 43;
198 uint32_t mode = S_IFDIR | 0644;
199 struct timespec now, timeout;
200
201 EXPECT_LOOKUP(FUSE_ROOT_ID, RELSRC)
202 .WillRepeatedly(Invoke(
203 ReturnImmediate([=](auto i __unused, auto& out) {
204 SET_OUT_HEADER_LEN(out, entry);
205 out.body.entry.attr.mode = mode;
206 out.body.entry.nodeid = ino;
207 out.body.entry.attr.nlink = 2;
208 out.body.entry.attr_valid = UINT64_MAX;
209 out.body.entry.attr.uid = 0;
210 out.body.entry.attr.gid = 0;
211 out.body.entry.entry_valid = UINT64_MAX;
212 }))
213 );
214 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDSTDIR)
215 .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
216 {
217 SET_OUT_HEADER_LEN(out, entry);
218 out.body.entry.nodeid = dst_dir_ino;
219 out.body.entry.entry_valid = UINT64_MAX;
220 out.body.entry.attr_valid = UINT64_MAX;
221 out.body.entry.attr.mode = S_IFDIR | 0755;
222 out.body.entry.attr.ino = dst_dir_ino;
223 out.body.entry.attr.uid = geteuid();
224 out.body.entry.attr.gid = getegid();
225 })));
226 EXPECT_LOOKUP(dst_dir_ino, RELDST)
227 .WillRepeatedly(Invoke(ReturnErrno(ENOENT)));
228 EXPECT_CALL(*m_mock, process(
229 ResultOf([=](auto in) {
230 return (in.header.opcode == FUSE_SETATTR &&
231 in.header.nodeid == ino);
232 }, Eq(true)),
233 _)
234 ).WillRepeatedly(Invoke(ReturnErrno(EIO)));
235 EXPECT_CALL(*m_mock, process(
236 ResultOf([=](auto in) {
237 return (in.header.opcode == FUSE_RENAME);
238 }, Eq(true)),
239 _)
240 ).WillRepeatedly(Invoke(ReturnErrno(EIO)));
241
242 ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th, (void*)FULLSRC))
243 << strerror(errno);
244
245 ASSERT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &timeout));
246 timeout.tv_nsec += NAP_NS;
247 do {
248 ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
249 EXPECT_EQ(EIO, errno);
250 ASSERT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &now));
251 } while (timespeccmp(&now, &timeout, <));
252 stopit = 1;
253 pthread_join(th0, NULL);
254 }
255
TEST_F(Rename,exdev)256 TEST_F(Rename, exdev)
257 {
258 const char FULLB[] = "mountpoint/src";
259 const char RELB[] = "src";
260 // FUSE hardcodes the mountpoint to inode 1
261 uint64_t b_ino = 42;
262
263 tmpfd = mkstemp(tmpfile);
264 ASSERT_LE(0, tmpfd) << strerror(errno);
265
266 expect_lookup(RELB, b_ino, S_IFREG | 0644, 0, 2);
267
268 ASSERT_NE(0, rename(tmpfile, FULLB));
269 ASSERT_EQ(EXDEV, errno);
270
271 ASSERT_NE(0, rename(FULLB, tmpfile));
272 ASSERT_EQ(EXDEV, errno);
273 }
274
TEST_F(Rename,ok)275 TEST_F(Rename, ok)
276 {
277 const char FULLDST[] = "mountpoint/dst";
278 const char RELDST[] = "dst";
279 const char FULLSRC[] = "mountpoint/src";
280 const char RELSRC[] = "src";
281 uint64_t dst_dir_ino = FUSE_ROOT_ID;
282 uint64_t ino = 42;
283
284 expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
285 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
286 .WillOnce(Invoke(ReturnErrno(ENOENT)));
287
288 EXPECT_CALL(*m_mock, process(
289 ResultOf([=](auto in) {
290 const char *src = (const char*)in.body.bytes +
291 sizeof(fuse_rename_in);
292 const char *dst = src + strlen(src) + 1;
293 return (in.header.opcode == FUSE_RENAME &&
294 in.body.rename.newdir == dst_dir_ino &&
295 (0 == strcmp(RELDST, dst)) &&
296 (0 == strcmp(RELSRC, src)));
297 }, Eq(true)),
298 _)
299 ).WillOnce(Invoke(ReturnErrno(0)));
300
301 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
302 }
303
304 /* When moving a file to a new directory, update its parent */
TEST_F(Rename,parent)305 TEST_F(Rename, parent)
306 {
307 const char FULLDST[] = "mountpoint/dstdir/dst";
308 const char RELDSTDIR[] = "dstdir";
309 const char RELDST[] = "dst";
310 const char FULLSRC[] = "mountpoint/src";
311 const char RELSRC[] = "src";
312 const char FULLDSTPARENT[] = "mountpoint/dstdir";
313 const char FULLDSTDOTDOT[] = "mountpoint/dstdir/dst/..";
314 Sequence seq;
315 uint64_t dst_dir_ino = 43;
316 uint64_t ino = 42;
317 struct stat sb;
318
319 expect_lookup(RELSRC, ino, S_IFDIR | 0755, 0, 1);
320 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDSTDIR)
321 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
322 SET_OUT_HEADER_LEN(out, entry);
323 out.body.entry.nodeid = dst_dir_ino;
324 out.body.entry.entry_valid = UINT64_MAX;
325 out.body.entry.attr_valid = UINT64_MAX;
326 out.body.entry.attr.mode = S_IFDIR | 0755;
327 out.body.entry.attr.ino = dst_dir_ino;
328 out.body.entry.attr.nlink = 2;
329 })));
330 EXPECT_LOOKUP(dst_dir_ino, RELDST)
331 .InSequence(seq)
332 .WillOnce(Invoke(ReturnErrno(ENOENT)));
333 EXPECT_CALL(*m_mock, process(
334 ResultOf([=](auto in) {
335 const char *src = (const char*)in.body.bytes +
336 sizeof(fuse_rename_in);
337 const char *dst = src + strlen(src) + 1;
338 return (in.header.opcode == FUSE_RENAME &&
339 in.body.rename.newdir == dst_dir_ino &&
340 (0 == strcmp(RELDST, dst)) &&
341 (0 == strcmp(RELSRC, src)));
342 }, Eq(true)),
343 _)
344 ).WillOnce(Invoke(ReturnErrno(0)));
345 EXPECT_CALL(*m_mock, process(
346 ResultOf([](auto in) {
347 return (in.header.opcode == FUSE_GETATTR &&
348 in.header.nodeid == 1);
349 }, Eq(true)),
350 _)
351 ).InSequence(seq)
352 .WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
353 SET_OUT_HEADER_LEN(out, attr);
354 out.body.attr.attr_valid = UINT64_MAX;
355 out.body.attr.attr.ino = 1;
356 out.body.attr.attr.mode = S_IFDIR | 0755;
357 out.body.attr.attr.nlink = 2;
358 })));
359 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDSTDIR)
360 .InSequence(seq)
361 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
362 SET_OUT_HEADER_LEN(out, entry);
363 out.body.entry.nodeid = dst_dir_ino;
364 out.body.entry.entry_valid = UINT64_MAX;
365 out.body.entry.attr_valid = UINT64_MAX;
366 out.body.entry.attr.mode = S_IFDIR | 0755;
367 out.body.entry.attr.ino = dst_dir_ino;
368 out.body.entry.attr.nlink = 3;
369 })));
370 EXPECT_LOOKUP(dst_dir_ino, RELDST)
371 .InSequence(seq)
372 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
373 SET_OUT_HEADER_LEN(out, entry);
374 out.body.entry.attr.mode = S_IFDIR | 0755;
375 out.body.entry.nodeid = ino;
376 out.body.entry.entry_valid = UINT64_MAX;
377 out.body.entry.attr_valid = UINT64_MAX;
378 })));
379
380 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
381
382 ASSERT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
383 EXPECT_EQ(2ul, sb.st_nlink);
384
385 ASSERT_EQ(0, stat(FULLDSTPARENT, &sb)) << strerror(errno);
386 EXPECT_EQ(3ul, sb.st_nlink);
387
388 ASSERT_EQ(0, stat(FULLDSTDOTDOT, &sb)) << strerror(errno);
389 ASSERT_EQ(dst_dir_ino, sb.st_ino);
390 }
391
392 // Rename overwrites an existing destination file
TEST_F(Rename,overwrite)393 TEST_F(Rename, overwrite)
394 {
395 const char FULLDST[] = "mountpoint/dst";
396 const char RELDST[] = "dst";
397 const char FULLSRC[] = "mountpoint/src";
398 const char RELSRC[] = "src";
399 // The inode of the already-existing destination file
400 uint64_t dst_ino = 2;
401 uint64_t dst_dir_ino = FUSE_ROOT_ID;
402 uint64_t ino = 42;
403
404 expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
405 expect_lookup(RELDST, dst_ino, S_IFREG | 0644, 0, 1);
406 EXPECT_CALL(*m_mock, process(
407 ResultOf([=](auto in) {
408 const char *src = (const char*)in.body.bytes +
409 sizeof(fuse_rename_in);
410 const char *dst = src + strlen(src) + 1;
411 return (in.header.opcode == FUSE_RENAME &&
412 in.body.rename.newdir == dst_dir_ino &&
413 (0 == strcmp(RELDST, dst)) &&
414 (0 == strcmp(RELSRC, src)));
415 }, Eq(true)),
416 _)
417 ).WillOnce(Invoke(ReturnErrno(0)));
418
419 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
420 }
421