xref: /freebsd/tests/sys/fs/fusefs/lseek.cc (revision b306c604df541dede4d0f3cc96188bbf5b6719fe)
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  * $FreeBSD$
28  */
29 
30 extern "C" {
31 #include <sys/param.h>
32 
33 #include <fcntl.h>
34 }
35 
36 #include "mockfs.hh"
37 #include "utils.hh"
38 
39 using namespace testing;
40 
41 class Lseek: public FuseTest {};
42 class LseekPathconf: public Lseek {};
43 class LseekPathconf_7_23: public LseekPathconf {
44 public:
45 virtual void SetUp() {
46 	m_kernel_minor_version = 23;
47 	FuseTest::SetUp();
48 }
49 };
50 class LseekSeekHole: public Lseek {};
51 class LseekSeekData: public Lseek {};
52 
53 /*
54  * If a previous lseek operation has already returned enosys, then pathconf can
55  * return EINVAL immediately.
56  */
57 TEST_F(LseekPathconf, already_enosys)
58 {
59 	const char FULLPATH[] = "mountpoint/some_file.txt";
60 	const char RELPATH[] = "some_file.txt";
61 	const uint64_t ino = 42;
62 	off_t fsize = 1 << 30;	/* 1 GiB */
63 	off_t offset_in = 1 << 28;
64 	int fd;
65 
66 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
67 	expect_open(ino, 0, 1);
68 	EXPECT_CALL(*m_mock, process(
69 		ResultOf([=](auto in) {
70 			return (in.header.opcode == FUSE_LSEEK);
71 		}, Eq(true)),
72 		_)
73 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
74 
75 	fd = open(FULLPATH, O_RDONLY);
76 
77 	EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA));
78 	EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
79 	EXPECT_EQ(EINVAL, errno);
80 
81 	leak(fd);
82 }
83 
84 /*
85  * If a previous lseek operation has already returned successfully, then
86  * pathconf can return 1 immediately.  1 means "holes are reported, but size is
87  * not specified".
88  */
89 TEST_F(LseekPathconf, already_seeked)
90 {
91 	const char FULLPATH[] = "mountpoint/some_file.txt";
92 	const char RELPATH[] = "some_file.txt";
93 	const uint64_t ino = 42;
94 	off_t fsize = 1 << 30;	/* 1 GiB */
95 	off_t offset = 1 << 28;
96 	int fd;
97 
98 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
99 	expect_open(ino, 0, 1);
100 	EXPECT_CALL(*m_mock, process(
101 		ResultOf([=](auto in) {
102 			return (in.header.opcode == FUSE_LSEEK);
103 		}, Eq(true)),
104 		_)
105 	).WillOnce(Invoke(ReturnImmediate([=](auto i, auto& out) {
106 		SET_OUT_HEADER_LEN(out, lseek);
107 		out.body.lseek.offset = i.body.lseek.offset;
108 	})));
109 	fd = open(FULLPATH, O_RDONLY);
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  * If no FUSE_LSEEK operation has been attempted since mount, try once as soon
119  * as a pathconf request comes in.
120  */
121 TEST_F(LseekPathconf, enosys_now)
122 {
123 	const char FULLPATH[] = "mountpoint/some_file.txt";
124 	const char RELPATH[] = "some_file.txt";
125 	const uint64_t ino = 42;
126 	off_t fsize = 1 << 30;	/* 1 GiB */
127 	int fd;
128 
129 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
130 	expect_open(ino, 0, 1);
131 	EXPECT_CALL(*m_mock, process(
132 		ResultOf([=](auto in) {
133 			return (in.header.opcode == FUSE_LSEEK);
134 		}, Eq(true)),
135 		_)
136 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
137 
138 	fd = open(FULLPATH, O_RDONLY);
139 
140 	EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
141 	EXPECT_EQ(EINVAL, errno);
142 
143 	leak(fd);
144 }
145 
146 /*
147  * If no FUSE_LSEEK operation has been attempted since mount, try one as soon
148  * as a pathconf request comes in.  This is the typical pattern of bsdtar.  It
149  * will only try SEEK_HOLE/SEEK_DATA if fpathconf says they're supported.
150  */
151 TEST_F(LseekPathconf, seek_now)
152 {
153 	const char FULLPATH[] = "mountpoint/some_file.txt";
154 	const char RELPATH[] = "some_file.txt";
155 	const uint64_t ino = 42;
156 	off_t fsize = 1 << 30;	/* 1 GiB */
157 	off_t offset_initial = 1 << 27;
158 	off_t offset_out = 1 << 29;
159 	int fd;
160 
161 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
162 	expect_open(ino, 0, 1);
163 	EXPECT_CALL(*m_mock, process(
164 		ResultOf([=](auto in) {
165 			return (in.header.opcode == FUSE_LSEEK);
166 		}, Eq(true)),
167 		_)
168 	).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
169 		SET_OUT_HEADER_LEN(out, lseek);
170 		out.body.lseek.offset = offset_out;
171 	})));
172 
173 	fd = open(FULLPATH, O_RDONLY);
174 	EXPECT_EQ(offset_initial, lseek(fd, offset_initial, SEEK_SET));
175 	EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
176 	/* And check that the file pointer hasn't changed */
177 	EXPECT_EQ(offset_initial, lseek(fd, 0, SEEK_CUR));
178 
179 	leak(fd);
180 }
181 
182 /*
183  * For servers using older protocol versions, no FUSE_LSEEK should be attempted
184  */
185 TEST_F(LseekPathconf_7_23, already_enosys)
186 {
187 	const char FULLPATH[] = "mountpoint/some_file.txt";
188 	const char RELPATH[] = "some_file.txt";
189 	const uint64_t ino = 42;
190 	off_t fsize = 1 << 30;	/* 1 GiB */
191 	int fd;
192 
193 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
194 	expect_open(ino, 0, 1);
195 	EXPECT_CALL(*m_mock, process(
196 		ResultOf([=](auto in) {
197 			return (in.header.opcode == FUSE_LSEEK);
198 		}, Eq(true)),
199 		_)
200 	).Times(0);
201 
202 	fd = open(FULLPATH, O_RDONLY);
203 	EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
204 	EXPECT_EQ(EINVAL, errno);
205 
206 	leak(fd);
207 }
208 
209 TEST_F(LseekSeekData, ok)
210 {
211 	const char FULLPATH[] = "mountpoint/some_file.txt";
212 	const char RELPATH[] = "some_file.txt";
213 	const uint64_t ino = 42;
214 	off_t fsize = 1 << 30;	/* 1 GiB */
215 	off_t offset_in = 1 << 28;
216 	off_t offset_out = 1 << 29;
217 	int fd;
218 
219 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
220 	expect_open(ino, 0, 1);
221 	EXPECT_CALL(*m_mock, process(
222 		ResultOf([=](auto in) {
223 			return (in.header.opcode == FUSE_LSEEK &&
224 				in.header.nodeid == ino &&
225 				in.body.lseek.fh == FH &&
226 				(off_t)in.body.lseek.offset == offset_in &&
227 				in.body.lseek.whence == SEEK_DATA);
228 		}, Eq(true)),
229 		_)
230 	).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
231 		SET_OUT_HEADER_LEN(out, lseek);
232 		out.body.lseek.offset = offset_out;
233 	})));
234 	fd = open(FULLPATH, O_RDONLY);
235 	EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_DATA));
236 	EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR));
237 
238 	leak(fd);
239 }
240 
241 /*
242  * If the server returns ENOSYS, fusefs should fall back to the default
243  * behavior, and never query the server again.
244  */
245 TEST_F(LseekSeekData, enosys)
246 {
247 	const char FULLPATH[] = "mountpoint/some_file.txt";
248 	const char RELPATH[] = "some_file.txt";
249 	const uint64_t ino = 42;
250 	off_t fsize = 1 << 30;	/* 1 GiB */
251 	off_t offset_in = 1 << 28;
252 	int fd;
253 
254 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
255 	expect_open(ino, 0, 1);
256 	EXPECT_CALL(*m_mock, process(
257 		ResultOf([=](auto in) {
258 			return (in.header.opcode == FUSE_LSEEK &&
259 				in.header.nodeid == ino &&
260 				in.body.lseek.fh == FH &&
261 				(off_t)in.body.lseek.offset == offset_in &&
262 				in.body.lseek.whence == SEEK_DATA);
263 		}, Eq(true)),
264 		_)
265 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
266 	fd = open(FULLPATH, O_RDONLY);
267 
268 	/*
269 	 * Default behavior: ENXIO if offset is < 0 or >= fsize, offset
270 	 * otherwise.
271 	 */
272 	EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA));
273 	EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE));
274 	EXPECT_EQ(ENXIO, errno);
275 	EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE));
276 	EXPECT_EQ(ENXIO, errno);
277 
278 	leak(fd);
279 }
280 
281 TEST_F(LseekSeekHole, ok)
282 {
283 	const char FULLPATH[] = "mountpoint/some_file.txt";
284 	const char RELPATH[] = "some_file.txt";
285 	const uint64_t ino = 42;
286 	off_t fsize = 1 << 30;	/* 1 GiB */
287 	off_t offset_in = 1 << 28;
288 	off_t offset_out = 1 << 29;
289 	int fd;
290 
291 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
292 	expect_open(ino, 0, 1);
293 	EXPECT_CALL(*m_mock, process(
294 		ResultOf([=](auto in) {
295 			return (in.header.opcode == FUSE_LSEEK &&
296 				in.header.nodeid == ino &&
297 				in.body.lseek.fh == FH &&
298 				(off_t)in.body.lseek.offset == offset_in &&
299 				in.body.lseek.whence == SEEK_HOLE);
300 		}, Eq(true)),
301 		_)
302 	).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
303 		SET_OUT_HEADER_LEN(out, lseek);
304 		out.body.lseek.offset = offset_out;
305 	})));
306 	fd = open(FULLPATH, O_RDONLY);
307 	EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_HOLE));
308 	EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR));
309 
310 	leak(fd);
311 }
312 
313 /*
314  * If the server returns ENOSYS, fusefs should fall back to the default
315  * behavior, and never query the server again.
316  */
317 TEST_F(LseekSeekHole, enosys)
318 {
319 	const char FULLPATH[] = "mountpoint/some_file.txt";
320 	const char RELPATH[] = "some_file.txt";
321 	const uint64_t ino = 42;
322 	off_t fsize = 1 << 30;	/* 1 GiB */
323 	off_t offset_in = 1 << 28;
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 				in.header.nodeid == ino &&
332 				in.body.lseek.fh == FH &&
333 				(off_t)in.body.lseek.offset == offset_in &&
334 				in.body.lseek.whence == SEEK_HOLE);
335 		}, Eq(true)),
336 		_)
337 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
338 	fd = open(FULLPATH, O_RDONLY);
339 
340 	/*
341 	 * Default behavior: ENXIO if offset is < 0 or >= fsize, fsize
342 	 * otherwise.
343 	 */
344 	EXPECT_EQ(fsize, lseek(fd, offset_in, SEEK_HOLE));
345 	EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE));
346 	EXPECT_EQ(ENXIO, errno);
347 	EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE));
348 	EXPECT_EQ(ENXIO, errno);
349 
350 	leak(fd);
351 }
352 
353 /* lseek should return ENXIO when offset points to EOF */
354 TEST_F(LseekSeekHole, enxio)
355 {
356 	const char FULLPATH[] = "mountpoint/some_file.txt";
357 	const char RELPATH[] = "some_file.txt";
358 	const uint64_t ino = 42;
359 	off_t fsize = 1 << 30;	/* 1 GiB */
360 	off_t offset_in = fsize;
361 	int fd;
362 
363 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
364 	expect_open(ino, 0, 1);
365 	EXPECT_CALL(*m_mock, process(
366 		ResultOf([=](auto in) {
367 			return (in.header.opcode == FUSE_LSEEK &&
368 				in.header.nodeid == ino &&
369 				in.body.lseek.fh == FH &&
370 				(off_t)in.body.lseek.offset == offset_in &&
371 				in.body.lseek.whence == SEEK_HOLE);
372 		}, Eq(true)),
373 		_)
374 	).WillOnce(Invoke(ReturnErrno(ENXIO)));
375 	fd = open(FULLPATH, O_RDONLY);
376 	EXPECT_EQ(-1, lseek(fd, offset_in, SEEK_HOLE));
377 	EXPECT_EQ(ENXIO, errno);
378 
379 	leak(fd);
380 }
381