1 /*-
2 * SPDX-License-Identifier: BSD-2-Clause
3 *
4 * Copyright (c) 2021 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 #include <sys/mount.h>
31 #include <sys/resource.h>
32 #include <sys/time.h>
33
34 #include <fcntl.h>
35 #include <signal.h>
36 #include <unistd.h>
37
38 #include "mntopts.h" // for build_iovec
39 }
40
41 #include "mockfs.hh"
42 #include "utils.hh"
43
44 using namespace testing;
45
46 /* Is buf all zero? */
47 static bool
is_zero(const char * buf,uint64_t size)48 is_zero(const char *buf, uint64_t size)
49 {
50 return buf[0] == 0 && !memcmp(buf, buf + 1, size - 1);
51 }
52
53 class Fallocate: public FuseTest {
54 public:
55 /*
56 * expect VOP_DEALLOCATE to be implemented by vop_stddeallocate.
57 */
expect_vop_stddeallocate(uint64_t ino,uint64_t off,uint64_t length)58 void expect_vop_stddeallocate(uint64_t ino, uint64_t off, uint64_t length)
59 {
60 /* XXX read offset and size may depend on cache mode */
61 EXPECT_CALL(*m_mock, process(
62 ResultOf([=](auto in) {
63 return (in.header.opcode == FUSE_READ &&
64 in.header.nodeid == ino &&
65 in.body.read.offset <= off &&
66 in.body.read.offset + in.body.read.size >=
67 off + length);
68 }, Eq(true)),
69 _)
70 ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) {
71 assert(in.body.read.size <= sizeof(out.body.bytes));
72 out.header.len = sizeof(struct fuse_out_header) +
73 in.body.read.size;
74 memset(out.body.bytes, 'X', in.body.read.size);
75 }))).RetiresOnSaturation();
76 EXPECT_CALL(*m_mock, process(
77 ResultOf([=](auto in) {
78 const char *buf = (const char*)in.body.bytes +
79 sizeof(struct fuse_write_in);
80
81 assert(length <= sizeof(in.body.bytes) -
82 sizeof(struct fuse_write_in));
83 return (in.header.opcode == FUSE_WRITE &&
84 in.header.nodeid == ino &&
85 in.body.write.offset == off &&
86 in.body.write.size == length &&
87 is_zero(buf, length));
88 }, Eq(true)),
89 _)
90 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
91 SET_OUT_HEADER_LEN(out, write);
92 out.body.write.size = length;
93 })));
94 }
95 };
96
97 class Fspacectl: public Fallocate {};
98
99 class Fspacectl_7_18: public Fspacectl {
100 public:
SetUp()101 virtual void SetUp() {
102 m_kernel_minor_version = 18;
103 Fspacectl::SetUp();
104 }
105 };
106
107 class FspacectlCache: public Fspacectl, public WithParamInterface<cache_mode> {
108 public:
109 bool m_direct_io;
110
FspacectlCache()111 FspacectlCache(): m_direct_io(false) {};
112
SetUp()113 virtual void SetUp() {
114 int cache_mode = GetParam();
115 switch (cache_mode) {
116 case Uncached:
117 m_direct_io = true;
118 break;
119 case WritebackAsync:
120 m_async = true;
121 /* FALLTHROUGH */
122 case Writeback:
123 m_init_flags |= FUSE_WRITEBACK_CACHE;
124 /* FALLTHROUGH */
125 case Writethrough:
126 break;
127 default:
128 FAIL() << "Unknown cache mode";
129 }
130
131 FuseTest::SetUp();
132 if (IsSkipped())
133 return;
134 }
135 };
136
137 class PosixFallocate: public Fallocate {
138 public:
139 static sig_atomic_t s_sigxfsz;
140
SetUp()141 void SetUp() {
142 s_sigxfsz = 0;
143 FuseTest::SetUp();
144 }
145
TearDown()146 void TearDown() {
147 struct sigaction sa;
148
149 bzero(&sa, sizeof(sa));
150 sa.sa_handler = SIG_DFL;
151 sigaction(SIGXFSZ, &sa, NULL);
152
153 Fallocate::TearDown();
154 }
155
156 };
157
158 sig_atomic_t PosixFallocate::s_sigxfsz = 0;
159
sigxfsz_handler(int __unused sig)160 void sigxfsz_handler(int __unused sig) {
161 PosixFallocate::s_sigxfsz = 1;
162 }
163
164 class PosixFallocate_7_18: public PosixFallocate {
165 public:
SetUp()166 virtual void SetUp() {
167 m_kernel_minor_version = 18;
168 PosixFallocate::SetUp();
169 }
170 };
171
172
173 /*
174 * If the server returns ENOSYS, it indicates that the server does not support
175 * FUSE_FALLOCATE. This and future calls should fall back to vop_stddeallocate.
176 */
TEST_F(Fspacectl,enosys)177 TEST_F(Fspacectl, enosys)
178 {
179 const char FULLPATH[] = "mountpoint/some_file.txt";
180 const char RELPATH[] = "some_file.txt";
181 off_t fsize = 1 << 20;
182 off_t off0 = 100;
183 off_t len0 = 500;
184 struct spacectl_range rqsr = { .r_offset = off0, .r_len = len0 };
185 uint64_t ino = 42;
186 uint64_t off1 = fsize;
187 uint64_t len1 = 1000;
188 off_t off2 = fsize / 2;
189 off_t len2 = 500;
190 int fd;
191
192 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
193 expect_open(ino, 0, 1);
194 expect_fallocate(ino, off0, len0,
195 FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, ENOSYS);
196 expect_vop_stddeallocate(ino, off0, len0);
197 expect_vop_stddeallocate(ino, off2, len2);
198
199 fd = open(FULLPATH, O_RDWR);
200 ASSERT_LE(0, fd) << strerror(errno);
201 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
202
203 /* Subsequent calls shouldn't query the daemon either */
204 rqsr.r_offset = off2;
205 rqsr.r_len = len2;
206 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
207
208 /* Neither should posix_fallocate query the daemon */
209 EXPECT_EQ(EINVAL, posix_fallocate(fd, off1, len1));
210
211 leak(fd);
212 }
213
214 /*
215 * EOPNOTSUPP means "the file system does not support fallocate with the
216 * supplied mode on this particular file". So we should fallback, but not
217 * assume anything about whether the operation will fail on a different file or
218 * with a different mode.
219 */
TEST_F(Fspacectl,eopnotsupp)220 TEST_F(Fspacectl, eopnotsupp)
221 {
222 const char FULLPATH[] = "mountpoint/some_file.txt";
223 const char RELPATH[] = "some_file.txt";
224 struct spacectl_range rqsr;
225 uint64_t ino = 42;
226 uint64_t fsize = 1 << 20;
227 uint64_t off0 = 500;
228 uint64_t len = 1000;
229 uint64_t off1 = fsize / 2;
230 int fd;
231
232 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
233 expect_open(ino, 0, 1);
234 expect_fallocate(ino, off0, len,
235 FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE,
236 EOPNOTSUPP);
237 expect_vop_stddeallocate(ino, off0, len);
238 expect_fallocate(ino, off1, len,
239 FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE,
240 EOPNOTSUPP);
241 expect_vop_stddeallocate(ino, off1, len);
242 expect_fallocate(ino, fsize, len, 0, 0);
243
244 fd = open(FULLPATH, O_RDWR);
245 ASSERT_LE(0, fd) << strerror(errno);
246
247 /*
248 * Though the FUSE daemon will reject the call, the kernel should fall
249 * back to a read-modify-write approach.
250 */
251 rqsr.r_offset = off0;
252 rqsr.r_len = len;
253 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
254
255 /* Subsequent calls should still query the daemon */
256 rqsr.r_offset = off1;
257 rqsr.r_len = len;
258 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
259
260 /* But subsequent posix_fallocate calls _should_ query the daemon */
261 EXPECT_EQ(0, posix_fallocate(fd, fsize, len));
262
263 leak(fd);
264 }
265
TEST_F(Fspacectl,erofs)266 TEST_F(Fspacectl, erofs)
267 {
268 const char FULLPATH[] = "mountpoint/some_file.txt";
269 const char RELPATH[] = "some_file.txt";
270 struct statfs statbuf;
271 uint64_t fsize = 2000;
272 struct spacectl_range rqsr = { .r_offset = 0, .r_len = 1 };
273 struct iovec *iov = NULL;
274 int iovlen = 0;
275 uint64_t ino = 42;
276 int fd;
277 int newflags;
278
279 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
280 expect_open(ino, 0, 1);
281 EXPECT_CALL(*m_mock, process(
282 ResultOf([](auto in) {
283 return (in.header.opcode == FUSE_STATFS);
284 }, Eq(true)),
285 _)
286 ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
287 {
288 /*
289 * All of the fields except f_flags are don't care, and f_flags
290 * is set by the VFS
291 */
292 SET_OUT_HEADER_LEN(out, statfs);
293 })));
294
295 fd = open(FULLPATH, O_RDWR);
296 ASSERT_LE(0, fd) << strerror(errno);
297
298 /* Remount read-only */
299 ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
300 newflags = statbuf.f_flags | MNT_UPDATE | MNT_RDONLY;
301 build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1);
302 build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1);
303 build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1);
304 ASSERT_EQ(0, nmount(iov, iovlen, newflags)) << strerror(errno);
305 free_iovec(&iov, &iovlen);
306
307 EXPECT_EQ(-1, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
308 EXPECT_EQ(EROFS, errno);
309
310 leak(fd);
311 }
312
313 /*
314 * If FUSE_GETATTR fails when determining the size of the file, fspacectl
315 * should fail gracefully. This failure mode is easiest to trigger when
316 * attribute caching is disabled.
317 */
TEST_F(Fspacectl,getattr_fails)318 TEST_F(Fspacectl, getattr_fails)
319 {
320 const char FULLPATH[] = "mountpoint/some_file.txt";
321 const char RELPATH[] = "some_file.txt";
322 Sequence seq;
323 struct spacectl_range rqsr;
324 const uint64_t ino = 42;
325 const uint64_t fsize = 2000;
326 int fd;
327
328 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1, 0);
329 expect_open(ino, 0, 1);
330 EXPECT_CALL(*m_mock, process(
331 ResultOf([](auto in) {
332 return (in.header.opcode == FUSE_GETATTR &&
333 in.header.nodeid == ino);
334 }, Eq(true)),
335 _)
336 ).Times(1)
337 .InSequence(seq)
338 .WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
339 SET_OUT_HEADER_LEN(out, attr);
340 out.body.attr.attr.ino = ino;
341 out.body.attr.attr.mode = S_IFREG | 0644;
342 out.body.attr.attr.size = fsize;
343 out.body.attr.attr_valid = 0;
344 })));
345 EXPECT_CALL(*m_mock, process(
346 ResultOf([](auto in) {
347 return (in.header.opcode == FUSE_GETATTR &&
348 in.header.nodeid == ino);
349 }, Eq(true)),
350 _)
351 ).InSequence(seq)
352 .WillOnce(ReturnErrno(EIO));
353
354 fd = open(FULLPATH, O_RDWR);
355 ASSERT_LE(0, fd) << strerror(errno);
356 rqsr.r_offset = 500;
357 rqsr.r_len = 1000;
358 EXPECT_EQ(-1, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
359 EXPECT_EQ(EIO, errno);
360
361 leak(fd);
362 }
363
TEST_F(Fspacectl,ok)364 TEST_F(Fspacectl, ok)
365 {
366 const char FULLPATH[] = "mountpoint/some_file.txt";
367 const char RELPATH[] = "some_file.txt";
368 struct spacectl_range rqsr, rmsr;
369 struct stat sb0, sb1;
370 uint64_t ino = 42;
371 uint64_t fsize = 2000;
372 uint64_t offset = 500;
373 uint64_t length = 1000;
374 int fd;
375
376 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
377 expect_open(ino, 0, 1);
378 expect_fallocate(ino, offset, length,
379 FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);
380
381 fd = open(FULLPATH, O_RDWR);
382 ASSERT_LE(0, fd) << strerror(errno);
383 ASSERT_EQ(0, fstat(fd, &sb0)) << strerror(errno);
384 rqsr.r_offset = offset;
385 rqsr.r_len = length;
386 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr));
387 EXPECT_EQ(0, rmsr.r_len);
388 EXPECT_EQ((off_t)(offset + length), rmsr.r_offset);
389
390 /*
391 * The file's attributes should not have been invalidated, so this fstat
392 * will not requery the daemon.
393 */
394 EXPECT_EQ(0, fstat(fd, &sb1));
395 EXPECT_EQ(fsize, (uint64_t)sb1.st_size);
396
397 /* mtime and ctime should be updated */
398 EXPECT_EQ(sb0.st_atime, sb1.st_atime);
399 EXPECT_NE(sb0.st_mtime, sb1.st_mtime);
400 EXPECT_NE(sb0.st_ctime, sb1.st_ctime);
401
402 leak(fd);
403 }
404
405 /* The returned rqsr.r_off should be clipped at EoF */
TEST_F(Fspacectl,past_eof)406 TEST_F(Fspacectl, past_eof)
407 {
408 const char FULLPATH[] = "mountpoint/some_file.txt";
409 const char RELPATH[] = "some_file.txt";
410 struct spacectl_range rqsr, rmsr;
411 uint64_t ino = 42;
412 uint64_t fsize = 1000;
413 uint64_t offset = 1500;
414 uint64_t length = 1000;
415 int fd;
416
417 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
418 expect_open(ino, 0, 1);
419 expect_fallocate(ino, offset, length,
420 FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);
421
422 fd = open(FULLPATH, O_RDWR);
423 ASSERT_LE(0, fd) << strerror(errno);
424 rqsr.r_offset = offset;
425 rqsr.r_len = length;
426 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr));
427 EXPECT_EQ(0, rmsr.r_len);
428 EXPECT_EQ((off_t)fsize, rmsr.r_offset);
429
430 leak(fd);
431 }
432
433 /* The returned rqsr.r_off should be clipped at EoF */
TEST_F(Fspacectl,spans_eof)434 TEST_F(Fspacectl, spans_eof)
435 {
436 const char FULLPATH[] = "mountpoint/some_file.txt";
437 const char RELPATH[] = "some_file.txt";
438 struct spacectl_range rqsr, rmsr;
439 uint64_t ino = 42;
440 uint64_t fsize = 1000;
441 uint64_t offset = 500;
442 uint64_t length = 1000;
443 int fd;
444
445 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
446 expect_open(ino, 0, 1);
447 expect_fallocate(ino, offset, length,
448 FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);
449
450 fd = open(FULLPATH, O_RDWR);
451 ASSERT_LE(0, fd) << strerror(errno);
452 rqsr.r_offset = offset;
453 rqsr.r_len = length;
454 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr));
455 EXPECT_EQ(0, rmsr.r_len);
456 EXPECT_EQ((off_t)fsize, rmsr.r_offset);
457
458 leak(fd);
459 }
460
461 /*
462 * With older servers, no FUSE_FALLOCATE should be attempted. The kernel
463 * should fall back to vop_stddeallocate.
464 */
TEST_F(Fspacectl_7_18,ok)465 TEST_F(Fspacectl_7_18, ok)
466 {
467 const char FULLPATH[] = "mountpoint/some_file.txt";
468 const char RELPATH[] = "some_file.txt";
469 struct spacectl_range rqsr, rmsr;
470 char *buf;
471 uint64_t ino = 42;
472 uint64_t fsize = 2000;
473 uint64_t offset = 500;
474 uint64_t length = 1000;
475 int fd;
476
477 buf = new char[length];
478
479 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
480 expect_open(ino, 0, 1);
481 expect_vop_stddeallocate(ino, offset, length);
482
483 fd = open(FULLPATH, O_RDWR);
484 ASSERT_LE(0, fd) << strerror(errno);
485 rqsr.r_offset = offset;
486 rqsr.r_len = length;
487 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr));
488 EXPECT_EQ(0, rmsr.r_len);
489 EXPECT_EQ((off_t)(offset + length), rmsr.r_offset);
490
491 leak(fd);
492 delete[] buf;
493 }
494
495 /*
496 * A successful fspacectl should clear the zeroed data from the kernel cache.
497 */
TEST_P(FspacectlCache,clears_cache)498 TEST_P(FspacectlCache, clears_cache)
499 {
500 const char FULLPATH[] = "mountpoint/some_file.txt";
501 const char RELPATH[] = "some_file.txt";
502 const char *CONTENTS = "abcdefghijklmnopqrstuvwxyz";
503 struct spacectl_range rqsr, rmsr;
504 uint64_t ino = 42;
505 ssize_t bufsize = strlen(CONTENTS);
506 uint64_t fsize = bufsize;
507 uint8_t buf[bufsize];
508 char zbuf[bufsize];
509 uint64_t offset = 0;
510 uint64_t length = bufsize;
511 int fd;
512
513 bzero(zbuf, bufsize);
514
515 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
516 expect_open(ino, 0, 1);
517 /* NB: expectations are applied in LIFO order */
518 expect_read(ino, 0, fsize, fsize, zbuf);
519 expect_read(ino, 0, fsize, fsize, CONTENTS);
520 expect_fallocate(ino, offset, length,
521 FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);
522
523 fd = open(FULLPATH, O_RDWR);
524 ASSERT_LE(0, fd) << strerror(errno);
525
526 /* Populate the cache */
527 ASSERT_EQ(fsize, (uint64_t)pread(fd, buf, bufsize, 0))
528 << strerror(errno);
529 ASSERT_EQ(0, memcmp(buf, CONTENTS, fsize));
530
531 /* Zero the file */
532 rqsr.r_offset = offset;
533 rqsr.r_len = length;
534 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr));
535 EXPECT_EQ(0, rmsr.r_len);
536 EXPECT_EQ((off_t)(offset + length), rmsr.r_offset);
537
538 /* Read again. This should query the daemon */
539 ASSERT_EQ(fsize, (uint64_t)pread(fd, buf, bufsize, 0))
540 << strerror(errno);
541 ASSERT_EQ(0, memcmp(buf, zbuf, fsize));
542
543 leak(fd);
544 }
545
546 INSTANTIATE_TEST_SUITE_P(FspacectlCache, FspacectlCache,
547 Values(Uncached, Writethrough, Writeback)
548 );
549
550 /*
551 * If the server returns ENOSYS, it indicates that the server does not support
552 * FUSE_FALLOCATE. This and future calls should return EINVAL.
553 */
TEST_F(PosixFallocate,enosys)554 TEST_F(PosixFallocate, enosys)
555 {
556 const char FULLPATH[] = "mountpoint/some_file.txt";
557 const char RELPATH[] = "some_file.txt";
558 uint64_t ino = 42;
559 uint64_t off0 = 0;
560 uint64_t len0 = 1000;
561 off_t off1 = 100;
562 off_t len1 = 200;
563 uint64_t fsize = 500;
564 struct spacectl_range rqsr = { .r_offset = off1, .r_len = len1 };
565 int fd;
566
567 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
568 expect_open(ino, 0, 1);
569 expect_fallocate(ino, off0, len0, 0, ENOSYS);
570 expect_vop_stddeallocate(ino, off1, len1);
571
572 fd = open(FULLPATH, O_RDWR);
573 ASSERT_LE(0, fd) << strerror(errno);
574 EXPECT_EQ(EINVAL, posix_fallocate(fd, off0, len0));
575
576 /* Subsequent calls shouldn't query the daemon*/
577 EXPECT_EQ(EINVAL, posix_fallocate(fd, off0, len0));
578
579 /* Neither should VOP_DEALLOCATE query the daemon */
580 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
581
582 leak(fd);
583 }
584
585 /*
586 * EOPNOTSUPP means "the file system does not support fallocate with the
587 * supplied mode on this particular file". So we should fallback, but not
588 * assume anything about whether the operation will fail on a different file or
589 * with a different mode.
590 */
TEST_F(PosixFallocate,eopnotsupp)591 TEST_F(PosixFallocate, eopnotsupp)
592 {
593 const char FULLPATH[] = "mountpoint/some_file.txt";
594 const char RELPATH[] = "some_file.txt";
595 struct spacectl_range rqsr;
596 uint64_t ino = 42;
597 uint64_t fsize = 2000;
598 uint64_t offset = 0;
599 uint64_t length = 1000;
600 int fd;
601
602 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
603 expect_open(ino, 0, 1);
604 expect_fallocate(ino, fsize, length, 0, EOPNOTSUPP);
605 expect_fallocate(ino, offset, length, 0, EOPNOTSUPP);
606 expect_fallocate(ino, offset, length,
607 FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);
608
609 fd = open(FULLPATH, O_RDWR);
610 ASSERT_LE(0, fd) << strerror(errno);
611 EXPECT_EQ(EINVAL, posix_fallocate(fd, fsize, length));
612
613 /* Subsequent calls should still query the daemon*/
614 EXPECT_EQ(EINVAL, posix_fallocate(fd, offset, length));
615
616 /* And subsequent VOP_DEALLOCATE calls should also query the daemon */
617 rqsr.r_len = length;
618 rqsr.r_offset = offset;
619 EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
620
621 leak(fd);
622 }
623
624 /* EIO is not a permanent error, and may be retried */
TEST_F(PosixFallocate,eio)625 TEST_F(PosixFallocate, eio)
626 {
627 const char FULLPATH[] = "mountpoint/some_file.txt";
628 const char RELPATH[] = "some_file.txt";
629 uint64_t ino = 42;
630 uint64_t offset = 0;
631 uint64_t length = 1000;
632 int fd;
633
634 expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
635 expect_open(ino, 0, 1);
636 expect_fallocate(ino, offset, length, 0, EIO);
637
638 fd = open(FULLPATH, O_RDWR);
639 ASSERT_LE(0, fd) << strerror(errno);
640 EXPECT_EQ(EIO, posix_fallocate(fd, offset, length));
641
642 expect_fallocate(ino, offset, length, 0, 0);
643
644 EXPECT_EQ(0, posix_fallocate(fd, offset, length));
645
646 leak(fd);
647 }
648
TEST_F(PosixFallocate,erofs)649 TEST_F(PosixFallocate, erofs)
650 {
651 const char FULLPATH[] = "mountpoint/some_file.txt";
652 const char RELPATH[] = "some_file.txt";
653 struct statfs statbuf;
654 struct iovec *iov = NULL;
655 int iovlen = 0;
656 uint64_t ino = 42;
657 uint64_t offset = 0;
658 uint64_t length = 1000;
659 int fd;
660 int newflags;
661
662 expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
663 expect_open(ino, 0, 1);
664 EXPECT_CALL(*m_mock, process(
665 ResultOf([](auto in) {
666 return (in.header.opcode == FUSE_STATFS);
667 }, Eq(true)),
668 _)
669 ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
670 {
671 /*
672 * All of the fields except f_flags are don't care, and f_flags
673 * is set by the VFS
674 */
675 SET_OUT_HEADER_LEN(out, statfs);
676 })));
677
678 fd = open(FULLPATH, O_RDWR);
679 ASSERT_LE(0, fd) << strerror(errno);
680
681 /* Remount read-only */
682 ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
683 newflags = statbuf.f_flags | MNT_UPDATE | MNT_RDONLY;
684 build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1);
685 build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1);
686 build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1);
687 ASSERT_EQ(0, nmount(iov, iovlen, newflags)) << strerror(errno);
688 free_iovec(&iov, &iovlen);
689
690 EXPECT_EQ(EROFS, posix_fallocate(fd, offset, length));
691
692 leak(fd);
693 }
694
TEST_F(PosixFallocate,ok)695 TEST_F(PosixFallocate, ok)
696 {
697 const char FULLPATH[] = "mountpoint/some_file.txt";
698 const char RELPATH[] = "some_file.txt";
699 struct stat sb0, sb1;
700 uint64_t ino = 42;
701 uint64_t offset = 0;
702 uint64_t length = 1000;
703 int fd;
704
705 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
706 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
707 SET_OUT_HEADER_LEN(out, entry);
708 out.body.entry.attr.mode = S_IFREG | 0644;
709 out.body.entry.nodeid = ino;
710 out.body.entry.entry_valid = UINT64_MAX;
711 out.body.entry.attr_valid = UINT64_MAX;
712 })));
713 expect_open(ino, 0, 1);
714 expect_fallocate(ino, offset, length, 0, 0);
715
716 fd = open(FULLPATH, O_RDWR);
717 ASSERT_LE(0, fd) << strerror(errno);
718 ASSERT_EQ(0, fstat(fd, &sb0)) << strerror(errno);
719 EXPECT_EQ(0, posix_fallocate(fd, offset, length));
720 /*
721 * Despite the originally cached file size of zero, stat should now
722 * return either the new size or requery the daemon.
723 */
724 EXPECT_EQ(0, stat(FULLPATH, &sb1));
725 EXPECT_EQ(length, (uint64_t)sb1.st_size);
726
727 /* mtime and ctime should be updated */
728 EXPECT_EQ(sb0.st_atime, sb1.st_atime);
729 EXPECT_NE(sb0.st_mtime, sb1.st_mtime);
730 EXPECT_NE(sb0.st_ctime, sb1.st_ctime);
731
732 leak(fd);
733 }
734
735 /* fusefs should respect RLIMIT_FSIZE */
TEST_F(PosixFallocate,rlimit_fsize)736 TEST_F(PosixFallocate, rlimit_fsize)
737 {
738 const char FULLPATH[] = "mountpoint/some_file.txt";
739 const char RELPATH[] = "some_file.txt";
740 struct rlimit rl;
741 uint64_t ino = 42;
742 uint64_t offset = 0;
743 uint64_t length = 1'000'000;
744 int fd;
745
746 expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
747 expect_open(ino, 0, 1);
748
749 rl.rlim_cur = length / 2;
750 rl.rlim_max = 10 * length;
751 ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno);
752 ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno);
753
754 fd = open(FULLPATH, O_RDWR);
755 ASSERT_LE(0, fd) << strerror(errno);
756 EXPECT_EQ(EFBIG, posix_fallocate(fd, offset, length));
757 EXPECT_EQ(1, s_sigxfsz);
758
759 leak(fd);
760 }
761
762 /* With older servers, no FUSE_FALLOCATE should be attempted */
TEST_F(PosixFallocate_7_18,einval)763 TEST_F(PosixFallocate_7_18, einval)
764 {
765 const char FULLPATH[] = "mountpoint/some_file.txt";
766 const char RELPATH[] = "some_file.txt";
767 uint64_t ino = 42;
768 uint64_t offset = 0;
769 uint64_t length = 1000;
770 int fd;
771
772 expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
773 expect_open(ino, 0, 1);
774
775 fd = open(FULLPATH, O_RDWR);
776 ASSERT_LE(0, fd) << strerror(errno);
777 EXPECT_EQ(EINVAL, posix_fallocate(fd, offset, length));
778
779 leak(fd);
780 }
781