xref: /freebsd/tests/sys/fs/fusefs/lseek.cc (revision 1719886f6d08408b834d270c59ffcfd821c8f63a)
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  * If no FUSE_LSEEK operation has been attempted since mount, try once as soon
117  * as a pathconf request comes in.
118  */
119 TEST_F(LseekPathconf, enosys_now)
120 {
121 	const char FULLPATH[] = "mountpoint/some_file.txt";
122 	const char RELPATH[] = "some_file.txt";
123 	const uint64_t ino = 42;
124 	off_t fsize = 1 << 30;	/* 1 GiB */
125 	int fd;
126 
127 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
128 	expect_open(ino, 0, 1);
129 	EXPECT_CALL(*m_mock, process(
130 		ResultOf([=](auto in) {
131 			return (in.header.opcode == FUSE_LSEEK);
132 		}, Eq(true)),
133 		_)
134 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
135 
136 	fd = open(FULLPATH, O_RDONLY);
137 
138 	EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
139 	EXPECT_EQ(EINVAL, errno);
140 
141 	leak(fd);
142 }
143 
144 /*
145  * If no FUSE_LSEEK operation has been attempted since mount, try one as soon
146  * as a pathconf request comes in.  This is the typical pattern of bsdtar.  It
147  * will only try SEEK_HOLE/SEEK_DATA if fpathconf says they're supported.
148  */
149 TEST_F(LseekPathconf, seek_now)
150 {
151 	const char FULLPATH[] = "mountpoint/some_file.txt";
152 	const char RELPATH[] = "some_file.txt";
153 	const uint64_t ino = 42;
154 	off_t fsize = 1 << 30;	/* 1 GiB */
155 	off_t offset_initial = 1 << 27;
156 	off_t offset_out = 1 << 29;
157 	int fd;
158 
159 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
160 	expect_open(ino, 0, 1);
161 	EXPECT_CALL(*m_mock, process(
162 		ResultOf([=](auto in) {
163 			return (in.header.opcode == FUSE_LSEEK);
164 		}, Eq(true)),
165 		_)
166 	).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
167 		SET_OUT_HEADER_LEN(out, lseek);
168 		out.body.lseek.offset = offset_out;
169 	})));
170 
171 	fd = open(FULLPATH, O_RDONLY);
172 	EXPECT_EQ(offset_initial, lseek(fd, offset_initial, SEEK_SET));
173 	EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
174 	/* And check that the file pointer hasn't changed */
175 	EXPECT_EQ(offset_initial, lseek(fd, 0, SEEK_CUR));
176 
177 	leak(fd);
178 }
179 
180 /*
181  * For servers using older protocol versions, no FUSE_LSEEK should be attempted
182  */
183 TEST_F(LseekPathconf_7_23, already_enosys)
184 {
185 	const char FULLPATH[] = "mountpoint/some_file.txt";
186 	const char RELPATH[] = "some_file.txt";
187 	const uint64_t ino = 42;
188 	off_t fsize = 1 << 30;	/* 1 GiB */
189 	int fd;
190 
191 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
192 	expect_open(ino, 0, 1);
193 	EXPECT_CALL(*m_mock, process(
194 		ResultOf([=](auto in) {
195 			return (in.header.opcode == FUSE_LSEEK);
196 		}, Eq(true)),
197 		_)
198 	).Times(0);
199 
200 	fd = open(FULLPATH, O_RDONLY);
201 	EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
202 	EXPECT_EQ(EINVAL, errno);
203 
204 	leak(fd);
205 }
206 
207 TEST_F(LseekSeekData, ok)
208 {
209 	const char FULLPATH[] = "mountpoint/some_file.txt";
210 	const char RELPATH[] = "some_file.txt";
211 	const uint64_t ino = 42;
212 	off_t fsize = 1 << 30;	/* 1 GiB */
213 	off_t offset_in = 1 << 28;
214 	off_t offset_out = 1 << 29;
215 	int fd;
216 
217 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
218 	expect_open(ino, 0, 1);
219 	EXPECT_CALL(*m_mock, process(
220 		ResultOf([=](auto in) {
221 			return (in.header.opcode == FUSE_LSEEK &&
222 				in.header.nodeid == ino &&
223 				in.body.lseek.fh == FH &&
224 				(off_t)in.body.lseek.offset == offset_in &&
225 				in.body.lseek.whence == SEEK_DATA);
226 		}, Eq(true)),
227 		_)
228 	).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
229 		SET_OUT_HEADER_LEN(out, lseek);
230 		out.body.lseek.offset = offset_out;
231 	})));
232 	fd = open(FULLPATH, O_RDONLY);
233 	EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_DATA));
234 	EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR));
235 
236 	leak(fd);
237 }
238 
239 /*
240  * If the server returns ENOSYS, fusefs should fall back to the default
241  * behavior, and never query the server again.
242  */
243 TEST_F(LseekSeekData, enosys)
244 {
245 	const char FULLPATH[] = "mountpoint/some_file.txt";
246 	const char RELPATH[] = "some_file.txt";
247 	const uint64_t ino = 42;
248 	off_t fsize = 1 << 30;	/* 1 GiB */
249 	off_t offset_in = 1 << 28;
250 	int fd;
251 
252 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
253 	expect_open(ino, 0, 1);
254 	EXPECT_CALL(*m_mock, process(
255 		ResultOf([=](auto in) {
256 			return (in.header.opcode == FUSE_LSEEK &&
257 				in.header.nodeid == ino &&
258 				in.body.lseek.fh == FH &&
259 				(off_t)in.body.lseek.offset == offset_in &&
260 				in.body.lseek.whence == SEEK_DATA);
261 		}, Eq(true)),
262 		_)
263 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
264 	fd = open(FULLPATH, O_RDONLY);
265 
266 	/*
267 	 * Default behavior: ENXIO if offset is < 0 or >= fsize, offset
268 	 * otherwise.
269 	 */
270 	EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA));
271 	EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE));
272 	EXPECT_EQ(ENXIO, errno);
273 	EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE));
274 	EXPECT_EQ(ENXIO, errno);
275 
276 	leak(fd);
277 }
278 
279 TEST_F(LseekSeekHole, ok)
280 {
281 	const char FULLPATH[] = "mountpoint/some_file.txt";
282 	const char RELPATH[] = "some_file.txt";
283 	const uint64_t ino = 42;
284 	off_t fsize = 1 << 30;	/* 1 GiB */
285 	off_t offset_in = 1 << 28;
286 	off_t offset_out = 1 << 29;
287 	int fd;
288 
289 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
290 	expect_open(ino, 0, 1);
291 	EXPECT_CALL(*m_mock, process(
292 		ResultOf([=](auto in) {
293 			return (in.header.opcode == FUSE_LSEEK &&
294 				in.header.nodeid == ino &&
295 				in.body.lseek.fh == FH &&
296 				(off_t)in.body.lseek.offset == offset_in &&
297 				in.body.lseek.whence == SEEK_HOLE);
298 		}, Eq(true)),
299 		_)
300 	).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
301 		SET_OUT_HEADER_LEN(out, lseek);
302 		out.body.lseek.offset = offset_out;
303 	})));
304 	fd = open(FULLPATH, O_RDONLY);
305 	EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_HOLE));
306 	EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR));
307 
308 	leak(fd);
309 }
310 
311 /*
312  * If the server returns ENOSYS, fusefs should fall back to the default
313  * behavior, and never query the server again.
314  */
315 TEST_F(LseekSeekHole, enosys)
316 {
317 	const char FULLPATH[] = "mountpoint/some_file.txt";
318 	const char RELPATH[] = "some_file.txt";
319 	const uint64_t ino = 42;
320 	off_t fsize = 1 << 30;	/* 1 GiB */
321 	off_t offset_in = 1 << 28;
322 	int fd;
323 
324 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
325 	expect_open(ino, 0, 1);
326 	EXPECT_CALL(*m_mock, process(
327 		ResultOf([=](auto in) {
328 			return (in.header.opcode == FUSE_LSEEK &&
329 				in.header.nodeid == ino &&
330 				in.body.lseek.fh == FH &&
331 				(off_t)in.body.lseek.offset == offset_in &&
332 				in.body.lseek.whence == SEEK_HOLE);
333 		}, Eq(true)),
334 		_)
335 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
336 	fd = open(FULLPATH, O_RDONLY);
337 
338 	/*
339 	 * Default behavior: ENXIO if offset is < 0 or >= fsize, fsize
340 	 * otherwise.
341 	 */
342 	EXPECT_EQ(fsize, lseek(fd, offset_in, SEEK_HOLE));
343 	EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE));
344 	EXPECT_EQ(ENXIO, errno);
345 	EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE));
346 	EXPECT_EQ(ENXIO, errno);
347 
348 	leak(fd);
349 }
350 
351 /* lseek should return ENXIO when offset points to EOF */
352 TEST_F(LseekSeekHole, enxio)
353 {
354 	const char FULLPATH[] = "mountpoint/some_file.txt";
355 	const char RELPATH[] = "some_file.txt";
356 	const uint64_t ino = 42;
357 	off_t fsize = 1 << 30;	/* 1 GiB */
358 	off_t offset_in = fsize;
359 	int fd;
360 
361 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
362 	expect_open(ino, 0, 1);
363 	EXPECT_CALL(*m_mock, process(
364 		ResultOf([=](auto in) {
365 			return (in.header.opcode == FUSE_LSEEK &&
366 				in.header.nodeid == ino &&
367 				in.body.lseek.fh == FH &&
368 				(off_t)in.body.lseek.offset == offset_in &&
369 				in.body.lseek.whence == SEEK_HOLE);
370 		}, Eq(true)),
371 		_)
372 	).WillOnce(Invoke(ReturnErrno(ENXIO)));
373 	fd = open(FULLPATH, O_RDONLY);
374 	EXPECT_EQ(-1, lseek(fd, offset_in, SEEK_HOLE));
375 	EXPECT_EQ(ENXIO, errno);
376 
377 	leak(fd);
378 }
379