xref: /freebsd/tests/sys/fs/fusefs/copy_file_range.cc (revision d7d962ead0b6e5e8a39202d0590022082bf5bfb6)
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  * If the server doesn't support FUSE_COPY_FILE_RANGE, the kernel should
176  * fallback to a read/write based implementation.
177  */
178 TEST_F(CopyFileRange, fallback)
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 	const uint64_t ino1 = 42;
185 	const uint64_t ino2 = 43;
186 	const uint64_t fh1 = 0xdeadbeef1a7ebabe;
187 	const uint64_t fh2 = 0xdeadc0de88c0ffee;
188 	off_t fsize2 = 0;
189 	off_t start1 = 0;
190 	off_t start2 = 0;
191 	const char *contents = "Hello, world!";
192 	ssize_t len;
193 	int fd1, fd2;
194 
195 	len = strlen(contents);
196 
197 	/*
198 	 * Ensure that we read to EOF, just so the buffer cache's read size is
199 	 * predictable.
200 	 */
201 	expect_lookup(RELPATH1, ino1, S_IFREG | 0644, start1 + len, 1);
202 	expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
203 	expect_open(ino1, 0, 1, fh1);
204 	expect_open(ino2, 0, 1, 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(ReturnErrno(ENOSYS)));
219 	expect_maybe_lseek(ino1);
220 	expect_read(ino1, start1, len, len, contents, 0);
221 	expect_write(ino2, start2, len, len, contents);
222 
223 	fd1 = open(FULLPATH1, O_RDONLY);
224 	ASSERT_GE(fd1, 0);
225 	fd2 = open(FULLPATH2, O_WRONLY);
226 	ASSERT_GE(fd2, 0);
227 	ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
228 }
229 
230 /* fusefs should respect RLIMIT_FSIZE */
231 TEST_F(CopyFileRange, rlimit_fsize)
232 {
233 	const char FULLPATH1[] = "mountpoint/src.txt";
234 	const char RELPATH1[] = "src.txt";
235 	const char FULLPATH2[] = "mountpoint/dst.txt";
236 	const char RELPATH2[] = "dst.txt";
237 	struct rlimit rl;
238 	const uint64_t ino1 = 42;
239 	const uint64_t ino2 = 43;
240 	const uint64_t fh1 = 0xdeadbeef1a7ebabe;
241 	const uint64_t fh2 = 0xdeadc0de88c0ffee;
242 	off_t fsize1 = 1 << 20;		/* 1 MiB */
243 	off_t fsize2 = 1 << 19;		/* 512 KiB */
244 	off_t start1 = 1 << 18;
245 	off_t start2 = fsize2;
246 	ssize_t len = 65536;
247 	int fd1, fd2;
248 
249 	expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1);
250 	expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
251 	expect_open(ino1, 0, 1, fh1);
252 	expect_open(ino2, 0, 1, fh2);
253 	EXPECT_CALL(*m_mock, process(
254 		ResultOf([=](auto in) {
255 			return (in.header.opcode == FUSE_COPY_FILE_RANGE);
256 		}, Eq(true)),
257 		_)
258 	).Times(0);
259 
260 	rl.rlim_cur = fsize2;
261 	rl.rlim_max = 10 * fsize2;
262 	ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno);
263 	ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno);
264 
265 	fd1 = open(FULLPATH1, O_RDONLY);
266 	fd2 = open(FULLPATH2, O_WRONLY);
267 	ASSERT_EQ(-1, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
268 	EXPECT_EQ(EFBIG, errno);
269 	EXPECT_EQ(1, s_sigxfsz);
270 }
271 
272 TEST_F(CopyFileRange, ok)
273 {
274 	const char FULLPATH1[] = "mountpoint/src.txt";
275 	const char RELPATH1[] = "src.txt";
276 	const char FULLPATH2[] = "mountpoint/dst.txt";
277 	const char RELPATH2[] = "dst.txt";
278 	const uint64_t ino1 = 42;
279 	const uint64_t ino2 = 43;
280 	const uint64_t fh1 = 0xdeadbeef1a7ebabe;
281 	const uint64_t fh2 = 0xdeadc0de88c0ffee;
282 	off_t fsize1 = 1 << 20;		/* 1 MiB */
283 	off_t fsize2 = 1 << 19;		/* 512 KiB */
284 	off_t start1 = 1 << 18;
285 	off_t start2 = 3 << 17;
286 	ssize_t len = 65536;
287 	int fd1, fd2;
288 
289 	expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1);
290 	expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
291 	expect_open(ino1, 0, 1, fh1);
292 	expect_open(ino2, 0, 1, fh2);
293 	EXPECT_CALL(*m_mock, process(
294 		ResultOf([=](auto in) {
295 			return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
296 				in.header.nodeid == ino1 &&
297 				in.body.copy_file_range.fh_in == fh1 &&
298 				(off_t)in.body.copy_file_range.off_in == start1 &&
299 				in.body.copy_file_range.nodeid_out == ino2 &&
300 				in.body.copy_file_range.fh_out == fh2 &&
301 				(off_t)in.body.copy_file_range.off_out == start2 &&
302 				in.body.copy_file_range.len == (size_t)len &&
303 				in.body.copy_file_range.flags == 0);
304 		}, Eq(true)),
305 		_)
306 	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
307 		SET_OUT_HEADER_LEN(out, write);
308 		out.body.write.size = len;
309 	})));
310 
311 	fd1 = open(FULLPATH1, O_RDONLY);
312 	fd2 = open(FULLPATH2, O_WRONLY);
313 	ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
314 }
315 
316 /*
317  * copy_file_range can make copies within a single file, as long as the ranges
318  * don't overlap.
319  * */
320 TEST_F(CopyFileRange, same_file)
321 {
322 	const char FULLPATH[] = "mountpoint/src.txt";
323 	const char RELPATH[] = "src.txt";
324 	const uint64_t ino = 4;
325 	const uint64_t fh = 0xdeadbeefa7ebabe;
326 	off_t fsize = 1 << 20;		/* 1 MiB */
327 	off_t off_in = 1 << 18;
328 	off_t off_out = 3 << 17;
329 	ssize_t len = 65536;
330 	int fd;
331 
332 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
333 	expect_open(ino, 0, 1, fh);
334 	EXPECT_CALL(*m_mock, process(
335 		ResultOf([=](auto in) {
336 			return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
337 				in.header.nodeid == ino &&
338 				in.body.copy_file_range.fh_in == fh &&
339 				(off_t)in.body.copy_file_range.off_in == off_in &&
340 				in.body.copy_file_range.nodeid_out == ino &&
341 				in.body.copy_file_range.fh_out == fh &&
342 				(off_t)in.body.copy_file_range.off_out == off_out &&
343 				in.body.copy_file_range.len == (size_t)len &&
344 				in.body.copy_file_range.flags == 0);
345 		}, Eq(true)),
346 		_)
347 	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
348 		SET_OUT_HEADER_LEN(out, write);
349 		out.body.write.size = len;
350 	})));
351 
352 	fd = open(FULLPATH, O_RDWR);
353 	ASSERT_EQ(len, copy_file_range(fd, &off_in, fd, &off_out, len, 0));
354 }
355 
356 /* With older protocol versions, no FUSE_COPY_FILE_RANGE should be attempted */
357 TEST_F(CopyFileRange_7_27, fallback)
358 {
359 	const char FULLPATH1[] = "mountpoint/src.txt";
360 	const char RELPATH1[] = "src.txt";
361 	const char FULLPATH2[] = "mountpoint/dst.txt";
362 	const char RELPATH2[] = "dst.txt";
363 	const uint64_t ino1 = 42;
364 	const uint64_t ino2 = 43;
365 	const uint64_t fh1 = 0xdeadbeef1a7ebabe;
366 	const uint64_t fh2 = 0xdeadc0de88c0ffee;
367 	off_t fsize2 = 0;
368 	off_t start1 = 0;
369 	off_t start2 = 0;
370 	const char *contents = "Hello, world!";
371 	ssize_t len;
372 	int fd1, fd2;
373 
374 	len = strlen(contents);
375 
376 	/*
377 	 * Ensure that we read to EOF, just so the buffer cache's read size is
378 	 * predictable.
379 	 */
380 	expect_lookup(RELPATH1, ino1, S_IFREG | 0644, start1 + len, 1);
381 	expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
382 	expect_open(ino1, 0, 1, fh1);
383 	expect_open(ino2, 0, 1, fh2);
384 	EXPECT_CALL(*m_mock, process(
385 		ResultOf([=](auto in) {
386 			return (in.header.opcode == FUSE_COPY_FILE_RANGE);
387 		}, Eq(true)),
388 		_)
389 	).Times(0);
390 	expect_maybe_lseek(ino1);
391 	expect_read(ino1, start1, len, len, contents, 0);
392 	expect_write(ino2, start2, len, len, contents);
393 
394 	fd1 = open(FULLPATH1, O_RDONLY);
395 	ASSERT_GE(fd1, 0);
396 	fd2 = open(FULLPATH2, O_WRONLY);
397 	ASSERT_GE(fd2, 0);
398 	ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
399 }
400 
401 
402