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:
SetUp()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 */
TEST_F(LseekPathconf,already_enosys)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 */
TEST_F(LseekPathconf,already_seeked)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 */
TEST_F(LseekPathconf,eacces)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 */
TEST_F(LseekPathconf,eio)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 */
TEST_F(LseekPathconf,enosys_now)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 */
TEST_F(LseekPathconf,pathconf)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 */
TEST_F(LseekPathconf,seek_now)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 */
TEST_F(LseekPathconf,zerolength)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 */
TEST_F(LseekPathconf_7_23,already_enosys)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
TEST_F(LseekSeekData,ok)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 */
TEST_F(LseekSeekData,enosys)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
TEST_F(LseekSeekHole,ok)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 */
TEST_F(LseekSeekHole,enosys)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 */
TEST_F(LseekSeekHole,enxio)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