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