xref: /freebsd/tests/sys/fs/fusefs/copy_file_range.cc (revision 963f5dc7a30624e95d72fb7f87b8892651164e46)
1 /*-
2  * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
3  *
4  * Copyright (c) 2020 Alan Somers
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18  * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25  * SUCH DAMAGE.
26  *
27  * $FreeBSD$
28  */
29 
30 extern "C" {
31 #include <sys/param.h>
32 #include <sys/time.h>
33 #include <sys/resource.h>
34 
35 #include <fcntl.h>
36 #include <signal.h>
37 #include <unistd.h>
38 }
39 
40 #include "mockfs.hh"
41 #include "utils.hh"
42 
43 using namespace testing;
44 
45 class CopyFileRange: public FuseTest {
46 public:
47 static sig_atomic_t s_sigxfsz;
48 
49 void SetUp() {
50 	s_sigxfsz = 0;
51 	FuseTest::SetUp();
52 }
53 
54 void TearDown() {
55 	struct sigaction sa;
56 
57 	bzero(&sa, sizeof(sa));
58 	sa.sa_handler = SIG_DFL;
59 	sigaction(SIGXFSZ, &sa, NULL);
60 
61 	FuseTest::TearDown();
62 }
63 
64 void expect_maybe_lseek(uint64_t ino)
65 {
66 	EXPECT_CALL(*m_mock, process(
67 		ResultOf([=](auto in) {
68 			return (in.header.opcode == FUSE_LSEEK &&
69 				in.header.nodeid == ino);
70 		}, Eq(true)),
71 		_)
72 	).Times(AtMost(1))
73 	.WillRepeatedly(Invoke(ReturnErrno(ENOSYS)));
74 }
75 
76 void expect_open(uint64_t ino, uint32_t flags, int times, uint64_t fh)
77 {
78 	EXPECT_CALL(*m_mock, process(
79 		ResultOf([=](auto in) {
80 			return (in.header.opcode == FUSE_OPEN &&
81 				in.header.nodeid == ino);
82 		}, Eq(true)),
83 		_)
84 	).Times(times)
85 	.WillRepeatedly(Invoke(
86 		ReturnImmediate([=](auto in __unused, auto& out) {
87 		out.header.len = sizeof(out.header);
88 		SET_OUT_HEADER_LEN(out, open);
89 		out.body.open.fh = fh;
90 		out.body.open.open_flags = flags;
91 	})));
92 }
93 
94 void expect_write(uint64_t ino, uint64_t offset, uint64_t isize,
95 	uint64_t osize, const void *contents)
96 {
97 	EXPECT_CALL(*m_mock, process(
98 		ResultOf([=](auto in) {
99 			const char *buf = (const char*)in.body.bytes +
100 				sizeof(struct fuse_write_in);
101 
102 			return (in.header.opcode == FUSE_WRITE &&
103 				in.header.nodeid == ino &&
104 				in.body.write.offset == offset  &&
105 				in.body.write.size == isize &&
106 				0 == bcmp(buf, contents, isize));
107 		}, Eq(true)),
108 		_)
109 	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
110 		SET_OUT_HEADER_LEN(out, write);
111 		out.body.write.size = osize;
112 	})));
113 }
114 
115 };
116 
117 sig_atomic_t CopyFileRange::s_sigxfsz = 0;
118 
119 void sigxfsz_handler(int __unused sig) {
120 	CopyFileRange::s_sigxfsz = 1;
121 }
122 
123 
124 class CopyFileRange_7_27: public CopyFileRange {
125 public:
126 virtual void SetUp() {
127 	m_kernel_minor_version = 27;
128 	CopyFileRange::SetUp();
129 }
130 };
131 
132 TEST_F(CopyFileRange, eio)
133 {
134 	const char FULLPATH1[] = "mountpoint/src.txt";
135 	const char RELPATH1[] = "src.txt";
136 	const char FULLPATH2[] = "mountpoint/dst.txt";
137 	const char RELPATH2[] = "dst.txt";
138 	const uint64_t ino1 = 42;
139 	const uint64_t ino2 = 43;
140 	const uint64_t fh1 = 0xdeadbeef1a7ebabe;
141 	const uint64_t fh2 = 0xdeadc0de88c0ffee;
142 	off_t fsize1 = 1 << 20;		/* 1 MiB */
143 	off_t fsize2 = 1 << 19;		/* 512 KiB */
144 	off_t start1 = 1 << 18;
145 	off_t start2 = 3 << 17;
146 	ssize_t len = 65536;
147 	int fd1, fd2;
148 
149 	expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1);
150 	expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
151 	expect_open(ino1, 0, 1, fh1);
152 	expect_open(ino2, 0, 1, fh2);
153 	EXPECT_CALL(*m_mock, process(
154 		ResultOf([=](auto in) {
155 			return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
156 				in.header.nodeid == ino1 &&
157 				in.body.copy_file_range.fh_in == fh1 &&
158 				(off_t)in.body.copy_file_range.off_in == start1 &&
159 				in.body.copy_file_range.nodeid_out == ino2 &&
160 				in.body.copy_file_range.fh_out == fh2 &&
161 				(off_t)in.body.copy_file_range.off_out == start2 &&
162 				in.body.copy_file_range.len == (size_t)len &&
163 				in.body.copy_file_range.flags == 0);
164 		}, Eq(true)),
165 		_)
166 	).WillOnce(Invoke(ReturnErrno(EIO)));
167 
168 	fd1 = open(FULLPATH1, O_RDONLY);
169 	fd2 = open(FULLPATH2, O_WRONLY);
170 	ASSERT_EQ(-1, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
171 	EXPECT_EQ(EIO, errno);
172 }
173 
174 /*
175  * copy_file_range should evict cached data for the modified region of the
176  * destination file.
177  */
178 TEST_F(CopyFileRange, evicts_cache)
179 {
180 	const char FULLPATH1[] = "mountpoint/src.txt";
181 	const char RELPATH1[] = "src.txt";
182 	const char FULLPATH2[] = "mountpoint/dst.txt";
183 	const char RELPATH2[] = "dst.txt";
184 	void *buf0, *buf1, *buf;
185 	const uint64_t ino1 = 42;
186 	const uint64_t ino2 = 43;
187 	const uint64_t fh1 = 0xdeadbeef1a7ebabe;
188 	const uint64_t fh2 = 0xdeadc0de88c0ffee;
189 	off_t fsize1 = 1 << 20;		/* 1 MiB */
190 	off_t fsize2 = 1 << 19;		/* 512 KiB */
191 	off_t start1 = 1 << 18;
192 	off_t start2 = 3 << 17;
193 	ssize_t len = m_maxbcachebuf;
194 	int fd1, fd2;
195 
196 	buf0 = malloc(m_maxbcachebuf);
197 	memset(buf0, 42, m_maxbcachebuf);
198 
199 	expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1);
200 	expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
201 	expect_open(ino1, 0, 1, fh1);
202 	expect_open(ino2, 0, 1, fh2);
203 	expect_read(ino2, start2, m_maxbcachebuf, m_maxbcachebuf, buf0, -1,
204 		fh2);
205 	EXPECT_CALL(*m_mock, process(
206 		ResultOf([=](auto in) {
207 			return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
208 				in.header.nodeid == ino1 &&
209 				in.body.copy_file_range.fh_in == fh1 &&
210 				(off_t)in.body.copy_file_range.off_in == start1 &&
211 				in.body.copy_file_range.nodeid_out == ino2 &&
212 				in.body.copy_file_range.fh_out == fh2 &&
213 				(off_t)in.body.copy_file_range.off_out == start2 &&
214 				in.body.copy_file_range.len == (size_t)len &&
215 				in.body.copy_file_range.flags == 0);
216 		}, Eq(true)),
217 		_)
218 	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
219 		SET_OUT_HEADER_LEN(out, write);
220 		out.body.write.size = len;
221 	})));
222 
223 	fd1 = open(FULLPATH1, O_RDONLY);
224 	fd2 = open(FULLPATH2, O_RDWR);
225 
226 	// Prime cache
227 	buf = malloc(m_maxbcachebuf);
228 	ASSERT_EQ(m_maxbcachebuf, pread(fd2, buf, m_maxbcachebuf, start2))
229 		<< strerror(errno);
230 	EXPECT_EQ(0, memcmp(buf0, buf, m_maxbcachebuf));
231 
232 	// Tell the FUSE server overwrite the region we just read
233 	ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
234 
235 	// Read again.  This should bypass the cache and read direct from server
236 	buf1 = malloc(m_maxbcachebuf);
237 	memset(buf1, 69, m_maxbcachebuf);
238 	start2 -= len;
239 	expect_read(ino2, start2, m_maxbcachebuf, m_maxbcachebuf, buf1, -1,
240 		fh2);
241 	ASSERT_EQ(m_maxbcachebuf, pread(fd2, buf, m_maxbcachebuf, start2))
242 		<< strerror(errno);
243 	EXPECT_EQ(0, memcmp(buf1, buf, m_maxbcachebuf));
244 
245 	free(buf1);
246 	free(buf0);
247 	free(buf);
248 	leak(fd1);
249 	leak(fd2);
250 }
251 
252 /*
253  * If the server doesn't support FUSE_COPY_FILE_RANGE, the kernel should
254  * fallback to a read/write based implementation.
255  */
256 TEST_F(CopyFileRange, fallback)
257 {
258 	const char FULLPATH1[] = "mountpoint/src.txt";
259 	const char RELPATH1[] = "src.txt";
260 	const char FULLPATH2[] = "mountpoint/dst.txt";
261 	const char RELPATH2[] = "dst.txt";
262 	const uint64_t ino1 = 42;
263 	const uint64_t ino2 = 43;
264 	const uint64_t fh1 = 0xdeadbeef1a7ebabe;
265 	const uint64_t fh2 = 0xdeadc0de88c0ffee;
266 	off_t fsize2 = 0;
267 	off_t start1 = 0;
268 	off_t start2 = 0;
269 	const char *contents = "Hello, world!";
270 	ssize_t len;
271 	int fd1, fd2;
272 
273 	len = strlen(contents);
274 
275 	/*
276 	 * Ensure that we read to EOF, just so the buffer cache's read size is
277 	 * predictable.
278 	 */
279 	expect_lookup(RELPATH1, ino1, S_IFREG | 0644, start1 + len, 1);
280 	expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
281 	expect_open(ino1, 0, 1, fh1);
282 	expect_open(ino2, 0, 1, fh2);
283 	EXPECT_CALL(*m_mock, process(
284 		ResultOf([=](auto in) {
285 			return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
286 				in.header.nodeid == ino1 &&
287 				in.body.copy_file_range.fh_in == fh1 &&
288 				(off_t)in.body.copy_file_range.off_in == start1 &&
289 				in.body.copy_file_range.nodeid_out == ino2 &&
290 				in.body.copy_file_range.fh_out == fh2 &&
291 				(off_t)in.body.copy_file_range.off_out == start2 &&
292 				in.body.copy_file_range.len == (size_t)len &&
293 				in.body.copy_file_range.flags == 0);
294 		}, Eq(true)),
295 		_)
296 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
297 	expect_maybe_lseek(ino1);
298 	expect_read(ino1, start1, len, len, contents, 0);
299 	expect_write(ino2, start2, len, len, contents);
300 
301 	fd1 = open(FULLPATH1, O_RDONLY);
302 	ASSERT_GE(fd1, 0);
303 	fd2 = open(FULLPATH2, O_WRONLY);
304 	ASSERT_GE(fd2, 0);
305 	ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
306 }
307 
308 /* fusefs should respect RLIMIT_FSIZE */
309 TEST_F(CopyFileRange, rlimit_fsize)
310 {
311 	const char FULLPATH1[] = "mountpoint/src.txt";
312 	const char RELPATH1[] = "src.txt";
313 	const char FULLPATH2[] = "mountpoint/dst.txt";
314 	const char RELPATH2[] = "dst.txt";
315 	struct rlimit rl;
316 	const uint64_t ino1 = 42;
317 	const uint64_t ino2 = 43;
318 	const uint64_t fh1 = 0xdeadbeef1a7ebabe;
319 	const uint64_t fh2 = 0xdeadc0de88c0ffee;
320 	off_t fsize1 = 1 << 20;		/* 1 MiB */
321 	off_t fsize2 = 1 << 19;		/* 512 KiB */
322 	off_t start1 = 1 << 18;
323 	off_t start2 = fsize2;
324 	ssize_t len = 65536;
325 	int fd1, fd2;
326 
327 	expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1);
328 	expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
329 	expect_open(ino1, 0, 1, fh1);
330 	expect_open(ino2, 0, 1, fh2);
331 	EXPECT_CALL(*m_mock, process(
332 		ResultOf([=](auto in) {
333 			return (in.header.opcode == FUSE_COPY_FILE_RANGE);
334 		}, Eq(true)),
335 		_)
336 	).Times(0);
337 
338 	rl.rlim_cur = fsize2;
339 	rl.rlim_max = 10 * fsize2;
340 	ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno);
341 	ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno);
342 
343 	fd1 = open(FULLPATH1, O_RDONLY);
344 	fd2 = open(FULLPATH2, O_WRONLY);
345 	ASSERT_EQ(-1, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
346 	EXPECT_EQ(EFBIG, errno);
347 	EXPECT_EQ(1, s_sigxfsz);
348 }
349 
350 TEST_F(CopyFileRange, ok)
351 {
352 	const char FULLPATH1[] = "mountpoint/src.txt";
353 	const char RELPATH1[] = "src.txt";
354 	const char FULLPATH2[] = "mountpoint/dst.txt";
355 	const char RELPATH2[] = "dst.txt";
356 	const uint64_t ino1 = 42;
357 	const uint64_t ino2 = 43;
358 	const uint64_t fh1 = 0xdeadbeef1a7ebabe;
359 	const uint64_t fh2 = 0xdeadc0de88c0ffee;
360 	off_t fsize1 = 1 << 20;		/* 1 MiB */
361 	off_t fsize2 = 1 << 19;		/* 512 KiB */
362 	off_t start1 = 1 << 18;
363 	off_t start2 = 3 << 17;
364 	ssize_t len = 65536;
365 	int fd1, fd2;
366 
367 	expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1);
368 	expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
369 	expect_open(ino1, 0, 1, fh1);
370 	expect_open(ino2, 0, 1, fh2);
371 	EXPECT_CALL(*m_mock, process(
372 		ResultOf([=](auto in) {
373 			return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
374 				in.header.nodeid == ino1 &&
375 				in.body.copy_file_range.fh_in == fh1 &&
376 				(off_t)in.body.copy_file_range.off_in == start1 &&
377 				in.body.copy_file_range.nodeid_out == ino2 &&
378 				in.body.copy_file_range.fh_out == fh2 &&
379 				(off_t)in.body.copy_file_range.off_out == start2 &&
380 				in.body.copy_file_range.len == (size_t)len &&
381 				in.body.copy_file_range.flags == 0);
382 		}, Eq(true)),
383 		_)
384 	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
385 		SET_OUT_HEADER_LEN(out, write);
386 		out.body.write.size = len;
387 	})));
388 
389 	fd1 = open(FULLPATH1, O_RDONLY);
390 	fd2 = open(FULLPATH2, O_WRONLY);
391 	ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
392 }
393 
394 /*
395  * copy_file_range can make copies within a single file, as long as the ranges
396  * don't overlap.
397  * */
398 TEST_F(CopyFileRange, same_file)
399 {
400 	const char FULLPATH[] = "mountpoint/src.txt";
401 	const char RELPATH[] = "src.txt";
402 	const uint64_t ino = 4;
403 	const uint64_t fh = 0xdeadbeefa7ebabe;
404 	off_t fsize = 1 << 20;		/* 1 MiB */
405 	off_t off_in = 1 << 18;
406 	off_t off_out = 3 << 17;
407 	ssize_t len = 65536;
408 	int fd;
409 
410 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
411 	expect_open(ino, 0, 1, fh);
412 	EXPECT_CALL(*m_mock, process(
413 		ResultOf([=](auto in) {
414 			return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
415 				in.header.nodeid == ino &&
416 				in.body.copy_file_range.fh_in == fh &&
417 				(off_t)in.body.copy_file_range.off_in == off_in &&
418 				in.body.copy_file_range.nodeid_out == ino &&
419 				in.body.copy_file_range.fh_out == fh &&
420 				(off_t)in.body.copy_file_range.off_out == off_out &&
421 				in.body.copy_file_range.len == (size_t)len &&
422 				in.body.copy_file_range.flags == 0);
423 		}, Eq(true)),
424 		_)
425 	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
426 		SET_OUT_HEADER_LEN(out, write);
427 		out.body.write.size = len;
428 	})));
429 
430 	fd = open(FULLPATH, O_RDWR);
431 	ASSERT_EQ(len, copy_file_range(fd, &off_in, fd, &off_out, len, 0));
432 }
433 
434 /*
435  * copy_file_range can extend the size of a file
436  * */
437 TEST_F(CopyFileRange, extend)
438 {
439 	const char FULLPATH[] = "mountpoint/src.txt";
440 	const char RELPATH[] = "src.txt";
441 	struct stat sb;
442 	const uint64_t ino = 4;
443 	const uint64_t fh = 0xdeadbeefa7ebabe;
444 	off_t fsize = 65536;
445 	off_t off_in = 0;
446 	off_t off_out = 65536;
447 	ssize_t len = 65536;
448 	int fd;
449 
450 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
451 	expect_open(ino, 0, 1, fh);
452 	EXPECT_CALL(*m_mock, process(
453 		ResultOf([=](auto in) {
454 			return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
455 				in.header.nodeid == ino &&
456 				in.body.copy_file_range.fh_in == fh &&
457 				(off_t)in.body.copy_file_range.off_in == off_in &&
458 				in.body.copy_file_range.nodeid_out == ino &&
459 				in.body.copy_file_range.fh_out == fh &&
460 				(off_t)in.body.copy_file_range.off_out == off_out &&
461 				in.body.copy_file_range.len == (size_t)len &&
462 				in.body.copy_file_range.flags == 0);
463 		}, Eq(true)),
464 		_)
465 	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
466 		SET_OUT_HEADER_LEN(out, write);
467 		out.body.write.size = len;
468 	})));
469 
470 	fd = open(FULLPATH, O_RDWR);
471 	ASSERT_GE(fd, 0);
472 	ASSERT_EQ(len, copy_file_range(fd, &off_in, fd, &off_out, len, 0));
473 
474 	/* Check that cached attributes were updated appropriately */
475 	ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
476 	EXPECT_EQ(fsize + len, sb.st_size);
477 
478 	leak(fd);
479 }
480 
481 /* With older protocol versions, no FUSE_COPY_FILE_RANGE should be attempted */
482 TEST_F(CopyFileRange_7_27, fallback)
483 {
484 	const char FULLPATH1[] = "mountpoint/src.txt";
485 	const char RELPATH1[] = "src.txt";
486 	const char FULLPATH2[] = "mountpoint/dst.txt";
487 	const char RELPATH2[] = "dst.txt";
488 	const uint64_t ino1 = 42;
489 	const uint64_t ino2 = 43;
490 	const uint64_t fh1 = 0xdeadbeef1a7ebabe;
491 	const uint64_t fh2 = 0xdeadc0de88c0ffee;
492 	off_t fsize2 = 0;
493 	off_t start1 = 0;
494 	off_t start2 = 0;
495 	const char *contents = "Hello, world!";
496 	ssize_t len;
497 	int fd1, fd2;
498 
499 	len = strlen(contents);
500 
501 	/*
502 	 * Ensure that we read to EOF, just so the buffer cache's read size is
503 	 * predictable.
504 	 */
505 	expect_lookup(RELPATH1, ino1, S_IFREG | 0644, start1 + len, 1);
506 	expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
507 	expect_open(ino1, 0, 1, fh1);
508 	expect_open(ino2, 0, 1, fh2);
509 	EXPECT_CALL(*m_mock, process(
510 		ResultOf([=](auto in) {
511 			return (in.header.opcode == FUSE_COPY_FILE_RANGE);
512 		}, Eq(true)),
513 		_)
514 	).Times(0);
515 	expect_maybe_lseek(ino1);
516 	expect_read(ino1, start1, len, len, contents, 0);
517 	expect_write(ino2, start2, len, len, contents);
518 
519 	fd1 = open(FULLPATH1, O_RDONLY);
520 	ASSERT_GE(fd1, 0);
521 	fd2 = open(FULLPATH2, O_WRONLY);
522 	ASSERT_GE(fd2, 0);
523 	ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
524 }
525 
526 
527