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