xref: /freebsd/tests/sys/fs/fusefs/lseek.cc (revision 088cc7d221bb0743fc5ec12de983559b812366bd)
1 /*-
2  * SPDX-License-Identifier: BSD-2-Clause
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 
28 extern "C" {
29 #include <sys/param.h>
30 
31 #include <fcntl.h>
32 }
33 
34 #include "mockfs.hh"
35 #include "utils.hh"
36 
37 using namespace testing;
38 
39 class Lseek: public FuseTest {};
40 class LseekPathconf: public Lseek {};
41 class LseekPathconf_7_23: public LseekPathconf {
42 public:
43 virtual void SetUp() {
44 	m_kernel_minor_version = 23;
45 	FuseTest::SetUp();
46 }
47 };
48 class LseekSeekHole: public Lseek {};
49 class LseekSeekData: public Lseek {};
50 
51 /*
52  * If a previous lseek operation has already returned enosys, then pathconf can
53  * return EINVAL immediately.
54  */
55 TEST_F(LseekPathconf, already_enosys)
56 {
57 	const char FULLPATH[] = "mountpoint/some_file.txt";
58 	const char RELPATH[] = "some_file.txt";
59 	const uint64_t ino = 42;
60 	off_t fsize = 1 << 30;	/* 1 GiB */
61 	off_t offset_in = 1 << 28;
62 	int fd;
63 
64 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
65 	expect_open(ino, 0, 1);
66 	EXPECT_CALL(*m_mock, process(
67 		ResultOf([=](auto in) {
68 			return (in.header.opcode == FUSE_LSEEK);
69 		}, Eq(true)),
70 		_)
71 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
72 
73 	fd = open(FULLPATH, O_RDONLY);
74 
75 	EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA));
76 	EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
77 	EXPECT_EQ(EINVAL, errno);
78 
79 	leak(fd);
80 }
81 
82 /*
83  * If a previous lseek operation has already returned successfully, then
84  * pathconf can return 1 immediately.  1 means "holes are reported, but size is
85  * not specified".
86  */
87 TEST_F(LseekPathconf, already_seeked)
88 {
89 	const char FULLPATH[] = "mountpoint/some_file.txt";
90 	const char RELPATH[] = "some_file.txt";
91 	const uint64_t ino = 42;
92 	off_t fsize = 1 << 30;	/* 1 GiB */
93 	off_t offset = 1 << 28;
94 	int fd;
95 
96 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
97 	expect_open(ino, 0, 1);
98 	EXPECT_CALL(*m_mock, process(
99 		ResultOf([=](auto in) {
100 			return (in.header.opcode == FUSE_LSEEK);
101 		}, Eq(true)),
102 		_)
103 	).WillOnce(Invoke(ReturnImmediate([=](auto i, auto& out) {
104 		SET_OUT_HEADER_LEN(out, lseek);
105 		out.body.lseek.offset = i.body.lseek.offset;
106 	})));
107 	fd = open(FULLPATH, O_RDONLY);
108 	EXPECT_EQ(offset, lseek(fd, offset, SEEK_DATA));
109 
110 	EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
111 
112 	leak(fd);
113 }
114 
115 /*
116  * Use pathconf on a file not already opened.  The server returns EACCES when
117  * the kernel tries to open it.  The kernel should return EACCES, and make no
118  * judgement about whether the server does or does not support FUSE_LSEEK.
119  */
120 TEST_F(LseekPathconf, eacces)
121 {
122 	const char FULLPATH[] = "mountpoint/some_file.txt";
123 	const char RELPATH[] = "some_file.txt";
124 	const uint64_t ino = 42;
125 	off_t fsize = 1 << 30;	/* 1 GiB */
126 
127 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
128 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
129 		SET_OUT_HEADER_LEN(out, entry);
130 		out.body.entry.entry_valid = UINT64_MAX;
131 		out.body.entry.attr.mode = S_IFREG | 0644;
132 		out.body.entry.nodeid = ino;
133 		out.body.entry.attr.size = fsize;
134 	})));
135 	EXPECT_CALL(*m_mock, process(
136 		ResultOf([=](auto in) {
137 			return (in.header.opcode == FUSE_OPEN &&
138 				in.header.nodeid == ino);
139 		}, Eq(true)),
140 		_)
141 	).Times(2)
142 	.WillRepeatedly(Invoke(ReturnErrno(EACCES)));
143 
144 	EXPECT_EQ(-1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE));
145 	EXPECT_EQ(EACCES, errno);
146 	/* Check again, to ensure that the kernel didn't record the response */
147 	EXPECT_EQ(-1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE));
148 	EXPECT_EQ(EACCES, errno);
149 }
150 
151 /*
152  * If the server returns some weird error when we try FUSE_LSEEK, send that to
153  * the caller but don't record the answer.
154  */
155 TEST_F(LseekPathconf, eio)
156 {
157 	const char FULLPATH[] = "mountpoint/some_file.txt";
158 	const char RELPATH[] = "some_file.txt";
159 	const uint64_t ino = 42;
160 	off_t fsize = 1 << 30;	/* 1 GiB */
161 	int fd;
162 
163 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
164 	expect_open(ino, 0, 1);
165 	EXPECT_CALL(*m_mock, process(
166 		ResultOf([=](auto in) {
167 			return (in.header.opcode == FUSE_LSEEK);
168 		}, Eq(true)),
169 		_)
170 	).Times(2)
171 	.WillRepeatedly(Invoke(ReturnErrno(EIO)));
172 
173 	fd = open(FULLPATH, O_RDONLY);
174 
175 	EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
176 	EXPECT_EQ(EIO, errno);
177 	/* Check again, to ensure that the kernel didn't record the response */
178 	EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
179 	EXPECT_EQ(EIO, errno);
180 
181 	leak(fd);
182 }
183 
184 /*
185  * If no FUSE_LSEEK operation has been attempted since mount, try once as soon
186  * as a pathconf request comes in.
187  */
188 TEST_F(LseekPathconf, enosys_now)
189 {
190 	const char FULLPATH[] = "mountpoint/some_file.txt";
191 	const char RELPATH[] = "some_file.txt";
192 	const uint64_t ino = 42;
193 	off_t fsize = 1 << 30;	/* 1 GiB */
194 	int fd;
195 
196 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
197 	expect_open(ino, 0, 1);
198 	EXPECT_CALL(*m_mock, process(
199 		ResultOf([=](auto in) {
200 			return (in.header.opcode == FUSE_LSEEK);
201 		}, Eq(true)),
202 		_)
203 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
204 
205 	fd = open(FULLPATH, O_RDONLY);
206 
207 	EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
208 	EXPECT_EQ(EINVAL, errno);
209 
210 	leak(fd);
211 }
212 
213 /*
214  * Use pathconf, rather than fpathconf, on a file not already opened.
215  * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=278135
216  */
217 TEST_F(LseekPathconf, pathconf)
218 {
219 	const char FULLPATH[] = "mountpoint/some_file.txt";
220 	const char RELPATH[] = "some_file.txt";
221 	const uint64_t ino = 42;
222 	off_t fsize = 1 << 30;	/* 1 GiB */
223 	off_t offset_out = 1 << 29;
224 
225 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
226 	expect_open(ino, 0, 1);
227 	EXPECT_CALL(*m_mock, process(
228 		ResultOf([=](auto in) {
229 			return (in.header.opcode == FUSE_LSEEK);
230 		}, Eq(true)),
231 		_)
232 	).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
233 		SET_OUT_HEADER_LEN(out, lseek);
234 		out.body.lseek.offset = offset_out;
235 	})));
236 	expect_release(ino, FuseTest::FH);
237 
238 	EXPECT_EQ(1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE)) << strerror(errno);
239 }
240 
241 /*
242  * If no FUSE_LSEEK operation has been attempted since mount, try one as soon
243  * as a pathconf request comes in.  This is the typical pattern of bsdtar.  It
244  * will only try SEEK_HOLE/SEEK_DATA if fpathconf says they're supported.
245  */
246 TEST_F(LseekPathconf, seek_now)
247 {
248 	const char FULLPATH[] = "mountpoint/some_file.txt";
249 	const char RELPATH[] = "some_file.txt";
250 	const uint64_t ino = 42;
251 	off_t fsize = 1 << 30;	/* 1 GiB */
252 	off_t offset_initial = 1 << 27;
253 	off_t offset_out = 1 << 29;
254 	int fd;
255 
256 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
257 	expect_open(ino, 0, 1);
258 	EXPECT_CALL(*m_mock, process(
259 		ResultOf([=](auto in) {
260 			return (in.header.opcode == FUSE_LSEEK);
261 		}, Eq(true)),
262 		_)
263 	).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
264 		SET_OUT_HEADER_LEN(out, lseek);
265 		out.body.lseek.offset = offset_out;
266 	})));
267 
268 	fd = open(FULLPATH, O_RDONLY);
269 	EXPECT_EQ(offset_initial, lseek(fd, offset_initial, SEEK_SET));
270 	EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
271 	/* And check that the file pointer hasn't changed */
272 	EXPECT_EQ(offset_initial, lseek(fd, 0, SEEK_CUR));
273 
274 	leak(fd);
275 }
276 
277 /*
278  * If the user calls pathconf(_, _PC_MIN_HOLE_SIZE) on a fully sparse or
279  * zero-length file, then SEEK_DATA will return ENXIO.  That should be
280  * interpreted as success.
281  */
282 TEST_F(LseekPathconf, zerolength)
283 {
284 	const char FULLPATH[] = "mountpoint/some_file.txt";
285 	const char RELPATH[] = "some_file.txt";
286 	const uint64_t ino = 42;
287 	off_t fsize = 0;
288 	int fd;
289 
290 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
291 	expect_open(ino, 0, 1);
292 	EXPECT_CALL(*m_mock, process(
293 		ResultOf([=](auto in) {
294 			return (in.header.opcode == FUSE_LSEEK &&
295 				in.header.nodeid == ino &&
296 				in.body.lseek.whence == SEEK_DATA);
297 		}, Eq(true)),
298 		_)
299 	).WillOnce(Invoke(ReturnErrno(ENXIO)));
300 
301 	fd = open(FULLPATH, O_RDONLY);
302 	EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
303 	/* Check again, to ensure that the kernel recorded the response */
304 	EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
305 
306 	leak(fd);
307 }
308 
309 /*
310  * For servers using older protocol versions, no FUSE_LSEEK should be attempted
311  */
312 TEST_F(LseekPathconf_7_23, already_enosys)
313 {
314 	const char FULLPATH[] = "mountpoint/some_file.txt";
315 	const char RELPATH[] = "some_file.txt";
316 	const uint64_t ino = 42;
317 	off_t fsize = 1 << 30;	/* 1 GiB */
318 	int fd;
319 
320 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
321 	expect_open(ino, 0, 1);
322 	EXPECT_CALL(*m_mock, process(
323 		ResultOf([=](auto in) {
324 			return (in.header.opcode == FUSE_LSEEK);
325 		}, Eq(true)),
326 		_)
327 	).Times(0);
328 
329 	fd = open(FULLPATH, O_RDONLY);
330 	EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
331 	EXPECT_EQ(EINVAL, errno);
332 
333 	leak(fd);
334 }
335 
336 TEST_F(LseekSeekData, ok)
337 {
338 	const char FULLPATH[] = "mountpoint/some_file.txt";
339 	const char RELPATH[] = "some_file.txt";
340 	const uint64_t ino = 42;
341 	off_t fsize = 1 << 30;	/* 1 GiB */
342 	off_t offset_in = 1 << 28;
343 	off_t offset_out = 1 << 29;
344 	int fd;
345 
346 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
347 	expect_open(ino, 0, 1);
348 	EXPECT_CALL(*m_mock, process(
349 		ResultOf([=](auto in) {
350 			return (in.header.opcode == FUSE_LSEEK &&
351 				in.header.nodeid == ino &&
352 				in.body.lseek.fh == FH &&
353 				(off_t)in.body.lseek.offset == offset_in &&
354 				in.body.lseek.whence == SEEK_DATA);
355 		}, Eq(true)),
356 		_)
357 	).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
358 		SET_OUT_HEADER_LEN(out, lseek);
359 		out.body.lseek.offset = offset_out;
360 	})));
361 	fd = open(FULLPATH, O_RDONLY);
362 	EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_DATA));
363 	EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR));
364 
365 	leak(fd);
366 }
367 
368 /*
369  * If the server returns ENOSYS, fusefs should fall back to the default
370  * behavior, and never query the server again.
371  */
372 TEST_F(LseekSeekData, enosys)
373 {
374 	const char FULLPATH[] = "mountpoint/some_file.txt";
375 	const char RELPATH[] = "some_file.txt";
376 	const uint64_t ino = 42;
377 	off_t fsize = 1 << 30;	/* 1 GiB */
378 	off_t offset_in = 1 << 28;
379 	int fd;
380 
381 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
382 	expect_open(ino, 0, 1);
383 	EXPECT_CALL(*m_mock, process(
384 		ResultOf([=](auto in) {
385 			return (in.header.opcode == FUSE_LSEEK &&
386 				in.header.nodeid == ino &&
387 				in.body.lseek.fh == FH &&
388 				(off_t)in.body.lseek.offset == offset_in &&
389 				in.body.lseek.whence == SEEK_DATA);
390 		}, Eq(true)),
391 		_)
392 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
393 	fd = open(FULLPATH, O_RDONLY);
394 
395 	/*
396 	 * Default behavior: ENXIO if offset is < 0 or >= fsize, offset
397 	 * otherwise.
398 	 */
399 	EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA));
400 	EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE));
401 	EXPECT_EQ(ENXIO, errno);
402 	EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE));
403 	EXPECT_EQ(ENXIO, errno);
404 
405 	leak(fd);
406 }
407 
408 TEST_F(LseekSeekHole, ok)
409 {
410 	const char FULLPATH[] = "mountpoint/some_file.txt";
411 	const char RELPATH[] = "some_file.txt";
412 	const uint64_t ino = 42;
413 	off_t fsize = 1 << 30;	/* 1 GiB */
414 	off_t offset_in = 1 << 28;
415 	off_t offset_out = 1 << 29;
416 	int fd;
417 
418 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
419 	expect_open(ino, 0, 1);
420 	EXPECT_CALL(*m_mock, process(
421 		ResultOf([=](auto in) {
422 			return (in.header.opcode == FUSE_LSEEK &&
423 				in.header.nodeid == ino &&
424 				in.body.lseek.fh == FH &&
425 				(off_t)in.body.lseek.offset == offset_in &&
426 				in.body.lseek.whence == SEEK_HOLE);
427 		}, Eq(true)),
428 		_)
429 	).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
430 		SET_OUT_HEADER_LEN(out, lseek);
431 		out.body.lseek.offset = offset_out;
432 	})));
433 	fd = open(FULLPATH, O_RDONLY);
434 	EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_HOLE));
435 	EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR));
436 
437 	leak(fd);
438 }
439 
440 /*
441  * If the server returns ENOSYS, fusefs should fall back to the default
442  * behavior, and never query the server again.
443  */
444 TEST_F(LseekSeekHole, enosys)
445 {
446 	const char FULLPATH[] = "mountpoint/some_file.txt";
447 	const char RELPATH[] = "some_file.txt";
448 	const uint64_t ino = 42;
449 	off_t fsize = 1 << 30;	/* 1 GiB */
450 	off_t offset_in = 1 << 28;
451 	int fd;
452 
453 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
454 	expect_open(ino, 0, 1);
455 	EXPECT_CALL(*m_mock, process(
456 		ResultOf([=](auto in) {
457 			return (in.header.opcode == FUSE_LSEEK &&
458 				in.header.nodeid == ino &&
459 				in.body.lseek.fh == FH &&
460 				(off_t)in.body.lseek.offset == offset_in &&
461 				in.body.lseek.whence == SEEK_HOLE);
462 		}, Eq(true)),
463 		_)
464 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
465 	fd = open(FULLPATH, O_RDONLY);
466 
467 	/*
468 	 * Default behavior: ENXIO if offset is < 0 or >= fsize, fsize
469 	 * otherwise.
470 	 */
471 	EXPECT_EQ(fsize, lseek(fd, offset_in, SEEK_HOLE));
472 	EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE));
473 	EXPECT_EQ(ENXIO, errno);
474 	EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE));
475 	EXPECT_EQ(ENXIO, errno);
476 
477 	leak(fd);
478 }
479 
480 /* lseek should return ENXIO when offset points to EOF */
481 TEST_F(LseekSeekHole, enxio)
482 {
483 	const char FULLPATH[] = "mountpoint/some_file.txt";
484 	const char RELPATH[] = "some_file.txt";
485 	const uint64_t ino = 42;
486 	off_t fsize = 1 << 30;	/* 1 GiB */
487 	off_t offset_in = fsize;
488 	int fd;
489 
490 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
491 	expect_open(ino, 0, 1);
492 	EXPECT_CALL(*m_mock, process(
493 		ResultOf([=](auto in) {
494 			return (in.header.opcode == FUSE_LSEEK &&
495 				in.header.nodeid == ino &&
496 				in.body.lseek.fh == FH &&
497 				(off_t)in.body.lseek.offset == offset_in &&
498 				in.body.lseek.whence == SEEK_HOLE);
499 		}, Eq(true)),
500 		_)
501 	).WillOnce(Invoke(ReturnErrno(ENXIO)));
502 	fd = open(FULLPATH, O_RDONLY);
503 	EXPECT_EQ(-1, lseek(fd, offset_in, SEEK_HOLE));
504 	EXPECT_EQ(ENXIO, errno);
505 
506 	leak(fd);
507 }
508