1 /*-
2 * SPDX-License-Identifier: BSD-2-Clause
3 *
4 * Copyright (c) 2019 The FreeBSD Foundation
5 *
6 * This software was developed by BFF Storage Systems, LLC under sponsorship
7 * from the FreeBSD Foundation.
8 *
9 * Redistribution and use in source and binary forms, with or without
10 * modification, are permitted provided that the following conditions
11 * are met:
12 * 1. Redistributions of source code must retain the above copyright
13 * notice, this list of conditions and the following disclaimer.
14 * 2. Redistributions in binary form must reproduce the above copyright
15 * notice, this list of conditions and the following disclaimer in the
16 * documentation and/or other materials provided with the distribution.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
19 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
22 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
28 * SUCH DAMAGE.
29 */
30
31 extern "C" {
32 #include <sys/types.h>
33 #include <sys/resource.h>
34 #include <sys/stat.h>
35 #include <sys/time.h>
36
37 #include <fcntl.h>
38 #include <semaphore.h>
39 #include <signal.h>
40 }
41
42 #include "mockfs.hh"
43 #include "utils.hh"
44
45 using namespace testing;
46
47 class Setattr : public FuseTest {
48 public:
49 static sig_atomic_t s_sigxfsz;
50 };
51
52 class RofsSetattr: public Setattr {
53 public:
SetUp()54 virtual void SetUp() {
55 s_sigxfsz = 0;
56 m_ro = true;
57 Setattr::SetUp();
58 }
59 };
60
61 class Setattr_7_8: public Setattr {
62 public:
SetUp()63 virtual void SetUp() {
64 m_kernel_minor_version = 8;
65 Setattr::SetUp();
66 }
67 };
68
69
70 sig_atomic_t Setattr::s_sigxfsz = 0;
71
sigxfsz_handler(int __unused sig)72 void sigxfsz_handler(int __unused sig) {
73 Setattr::s_sigxfsz = 1;
74 }
75
76 /*
77 * If setattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs
78 * should use the cached attributes, rather than query the daemon
79 */
TEST_F(Setattr,attr_cache)80 TEST_F(Setattr, attr_cache)
81 {
82 const char FULLPATH[] = "mountpoint/some_file.txt";
83 const char RELPATH[] = "some_file.txt";
84 const uint64_t ino = 42;
85 struct stat sb;
86 const mode_t newmode = 0644;
87
88 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
89 .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
90 SET_OUT_HEADER_LEN(out, entry);
91 out.body.entry.attr.mode = S_IFREG | 0644;
92 out.body.entry.nodeid = ino;
93 out.body.entry.entry_valid = UINT64_MAX;
94 })));
95
96 EXPECT_CALL(*m_mock, process(
97 ResultOf([](auto in) {
98 return (in.header.opcode == FUSE_SETATTR &&
99 in.header.nodeid == ino);
100 }, Eq(true)),
101 _)
102 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
103 SET_OUT_HEADER_LEN(out, attr);
104 out.body.attr.attr.ino = ino; // Must match nodeid
105 out.body.attr.attr.mode = S_IFREG | newmode;
106 out.body.attr.attr_valid = UINT64_MAX;
107 })));
108 EXPECT_CALL(*m_mock, process(
109 ResultOf([](auto in) {
110 return (in.header.opcode == FUSE_GETATTR);
111 }, Eq(true)),
112 _)
113 ).Times(0);
114
115 /* Set an attribute with SETATTR */
116 ASSERT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
117
118 /* The stat(2) should use cached attributes */
119 ASSERT_EQ(0, stat(FULLPATH, &sb));
120 EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
121 }
122
123 /* Change the mode of a file */
TEST_F(Setattr,chmod)124 TEST_F(Setattr, chmod)
125 {
126 const char FULLPATH[] = "mountpoint/some_file.txt";
127 const char RELPATH[] = "some_file.txt";
128 const uint64_t ino = 42;
129 const mode_t oldmode = 0755;
130 const mode_t newmode = 0644;
131
132 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
133 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
134 SET_OUT_HEADER_LEN(out, entry);
135 out.body.entry.attr.mode = S_IFREG | oldmode;
136 out.body.entry.nodeid = ino;
137 })));
138
139 EXPECT_CALL(*m_mock, process(
140 ResultOf([](auto in) {
141 uint32_t valid = FATTR_MODE;
142 return (in.header.opcode == FUSE_SETATTR &&
143 in.header.nodeid == ino &&
144 in.body.setattr.valid == valid &&
145 in.body.setattr.mode == newmode);
146 }, Eq(true)),
147 _)
148 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
149 SET_OUT_HEADER_LEN(out, attr);
150 out.body.attr.attr.ino = ino; // Must match nodeid
151 out.body.attr.attr.mode = S_IFREG | newmode;
152 })));
153 EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
154 }
155
156 /*
157 * Chmod a multiply-linked file with cached attributes. Check that both files'
158 * attributes have changed.
159 */
TEST_F(Setattr,chmod_multiply_linked)160 TEST_F(Setattr, chmod_multiply_linked)
161 {
162 const char FULLPATH0[] = "mountpoint/some_file.txt";
163 const char RELPATH0[] = "some_file.txt";
164 const char FULLPATH1[] = "mountpoint/other_file.txt";
165 const char RELPATH1[] = "other_file.txt";
166 struct stat sb;
167 const uint64_t ino = 42;
168 const mode_t oldmode = 0777;
169 const mode_t newmode = 0666;
170
171 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH0)
172 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
173 SET_OUT_HEADER_LEN(out, entry);
174 out.body.entry.attr.mode = S_IFREG | oldmode;
175 out.body.entry.nodeid = ino;
176 out.body.entry.attr.nlink = 2;
177 out.body.entry.attr_valid = UINT64_MAX;
178 out.body.entry.entry_valid = UINT64_MAX;
179 })));
180
181 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH1)
182 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
183 SET_OUT_HEADER_LEN(out, entry);
184 out.body.entry.attr.mode = S_IFREG | oldmode;
185 out.body.entry.nodeid = ino;
186 out.body.entry.attr.nlink = 2;
187 out.body.entry.attr_valid = UINT64_MAX;
188 out.body.entry.entry_valid = UINT64_MAX;
189 })));
190
191 EXPECT_CALL(*m_mock, process(
192 ResultOf([](auto in) {
193 uint32_t valid = FATTR_MODE;
194 return (in.header.opcode == FUSE_SETATTR &&
195 in.header.nodeid == ino &&
196 in.body.setattr.valid == valid &&
197 in.body.setattr.mode == newmode);
198 }, Eq(true)),
199 _)
200 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
201 SET_OUT_HEADER_LEN(out, attr);
202 out.body.attr.attr.ino = ino;
203 out.body.attr.attr.mode = S_IFREG | newmode;
204 out.body.attr.attr.nlink = 2;
205 out.body.attr.attr_valid = UINT64_MAX;
206 })));
207
208 /* For a lookup of the 2nd file to get it into the cache*/
209 ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno);
210 EXPECT_EQ(S_IFREG | oldmode, sb.st_mode);
211
212 ASSERT_EQ(0, chmod(FULLPATH0, newmode)) << strerror(errno);
213 ASSERT_EQ(0, stat(FULLPATH0, &sb)) << strerror(errno);
214 EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
215 ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno);
216 EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
217 }
218
219
220 /* Change the owner and group of a file */
TEST_F(Setattr,chown)221 TEST_F(Setattr, chown)
222 {
223 const char FULLPATH[] = "mountpoint/some_file.txt";
224 const char RELPATH[] = "some_file.txt";
225 const uint64_t ino = 42;
226 const gid_t oldgroup = 66;
227 const gid_t newgroup = 99;
228 const uid_t olduser = 33;
229 const uid_t newuser = 44;
230
231 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
232 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
233 SET_OUT_HEADER_LEN(out, entry);
234 out.body.entry.attr.mode = S_IFREG | 0644;
235 out.body.entry.nodeid = ino;
236 out.body.entry.attr.gid = oldgroup;
237 out.body.entry.attr.uid = olduser;
238 })));
239
240 EXPECT_CALL(*m_mock, process(
241 ResultOf([](auto in) {
242 uint32_t valid = FATTR_GID | FATTR_UID;
243 return (in.header.opcode == FUSE_SETATTR &&
244 in.header.nodeid == ino &&
245 in.body.setattr.valid == valid &&
246 in.body.setattr.uid == newuser &&
247 in.body.setattr.gid == newgroup);
248 }, Eq(true)),
249 _)
250 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
251 SET_OUT_HEADER_LEN(out, attr);
252 out.body.attr.attr.ino = ino; // Must match nodeid
253 out.body.attr.attr.mode = S_IFREG | 0644;
254 out.body.attr.attr.uid = newuser;
255 out.body.attr.attr.gid = newgroup;
256 })));
257 EXPECT_EQ(0, chown(FULLPATH, newuser, newgroup)) << strerror(errno);
258 }
259
260
261
262 /*
263 * FUSE daemons are allowed to check permissions however they like. If the
264 * daemon returns EPERM, even if the file permissions "should" grant access,
265 * then fuse(4) should return EPERM too.
266 */
TEST_F(Setattr,eperm)267 TEST_F(Setattr, eperm)
268 {
269 const char FULLPATH[] = "mountpoint/some_file.txt";
270 const char RELPATH[] = "some_file.txt";
271 const uint64_t ino = 42;
272
273 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
274 .WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) {
275 SET_OUT_HEADER_LEN(out, entry);
276 out.body.entry.attr.mode = S_IFREG | 0777;
277 out.body.entry.nodeid = ino;
278 out.body.entry.attr.uid = in.header.uid;
279 out.body.entry.attr.gid = in.header.gid;
280 })));
281
282 EXPECT_CALL(*m_mock, process(
283 ResultOf([](auto in) {
284 return (in.header.opcode == FUSE_SETATTR &&
285 in.header.nodeid == ino);
286 }, Eq(true)),
287 _)
288 ).WillOnce(Invoke(ReturnErrno(EPERM)));
289 EXPECT_NE(0, truncate(FULLPATH, 10));
290 EXPECT_EQ(EPERM, errno);
291 }
292
293 /* Change the mode of an open file, by its file descriptor */
TEST_F(Setattr,fchmod)294 TEST_F(Setattr, fchmod)
295 {
296 const char FULLPATH[] = "mountpoint/some_file.txt";
297 const char RELPATH[] = "some_file.txt";
298 uint64_t ino = 42;
299 int fd;
300 const mode_t oldmode = 0755;
301 const mode_t newmode = 0644;
302
303 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
304 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
305 SET_OUT_HEADER_LEN(out, entry);
306 out.body.entry.attr.mode = S_IFREG | oldmode;
307 out.body.entry.nodeid = ino;
308 out.body.entry.attr_valid = UINT64_MAX;
309 })));
310
311 EXPECT_CALL(*m_mock, process(
312 ResultOf([=](auto in) {
313 return (in.header.opcode == FUSE_OPEN &&
314 in.header.nodeid == ino);
315 }, Eq(true)),
316 _)
317 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
318 out.header.len = sizeof(out.header);
319 SET_OUT_HEADER_LEN(out, open);
320 })));
321
322 EXPECT_CALL(*m_mock, process(
323 ResultOf([=](auto in) {
324 uint32_t valid = FATTR_MODE;
325 return (in.header.opcode == FUSE_SETATTR &&
326 in.header.nodeid == ino &&
327 in.body.setattr.valid == valid &&
328 in.body.setattr.mode == newmode);
329 }, Eq(true)),
330 _)
331 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
332 SET_OUT_HEADER_LEN(out, attr);
333 out.body.attr.attr.ino = ino; // Must match nodeid
334 out.body.attr.attr.mode = S_IFREG | newmode;
335 })));
336
337 fd = open(FULLPATH, O_RDONLY);
338 ASSERT_LE(0, fd) << strerror(errno);
339 ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno);
340 leak(fd);
341 }
342
343 /* Change the size of an open file, by its file descriptor */
TEST_F(Setattr,ftruncate)344 TEST_F(Setattr, ftruncate)
345 {
346 const char FULLPATH[] = "mountpoint/some_file.txt";
347 const char RELPATH[] = "some_file.txt";
348 uint64_t ino = 42;
349 int fd;
350 uint64_t fh = 0xdeadbeef1a7ebabe;
351 const off_t oldsize = 99;
352 const off_t newsize = 12345;
353
354 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
355 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
356 SET_OUT_HEADER_LEN(out, entry);
357 out.body.entry.attr.mode = S_IFREG | 0755;
358 out.body.entry.nodeid = ino;
359 out.body.entry.attr_valid = UINT64_MAX;
360 out.body.entry.attr.size = oldsize;
361 })));
362
363 EXPECT_CALL(*m_mock, process(
364 ResultOf([=](auto in) {
365 return (in.header.opcode == FUSE_OPEN &&
366 in.header.nodeid == ino);
367 }, Eq(true)),
368 _)
369 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
370 out.header.len = sizeof(out.header);
371 SET_OUT_HEADER_LEN(out, open);
372 out.body.open.fh = fh;
373 })));
374
375 EXPECT_CALL(*m_mock, process(
376 ResultOf([=](auto in) {
377 uint32_t valid = FATTR_SIZE | FATTR_FH;
378 return (in.header.opcode == FUSE_SETATTR &&
379 in.header.nodeid == ino &&
380 in.body.setattr.valid == valid &&
381 in.body.setattr.fh == fh);
382 }, Eq(true)),
383 _)
384 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
385 SET_OUT_HEADER_LEN(out, attr);
386 out.body.attr.attr.ino = ino; // Must match nodeid
387 out.body.attr.attr.mode = S_IFREG | 0755;
388 out.body.attr.attr.size = newsize;
389 })));
390
391 fd = open(FULLPATH, O_RDWR);
392 ASSERT_LE(0, fd) << strerror(errno);
393 ASSERT_EQ(0, ftruncate(fd, newsize)) << strerror(errno);
394 leak(fd);
395 }
396
397 /* Change the size of the file */
TEST_F(Setattr,truncate)398 TEST_F(Setattr, truncate) {
399 const char FULLPATH[] = "mountpoint/some_file.txt";
400 const char RELPATH[] = "some_file.txt";
401 const uint64_t ino = 42;
402 const uint64_t oldsize = 100'000'000;
403 const uint64_t newsize = 20'000'000;
404
405 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
406 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
407 SET_OUT_HEADER_LEN(out, entry);
408 out.body.entry.attr.mode = S_IFREG | 0644;
409 out.body.entry.nodeid = ino;
410 out.body.entry.attr.size = oldsize;
411 })));
412
413 EXPECT_CALL(*m_mock, process(
414 ResultOf([](auto in) {
415 uint32_t valid = FATTR_SIZE;
416 return (in.header.opcode == FUSE_SETATTR &&
417 in.header.nodeid == ino &&
418 in.body.setattr.valid == valid &&
419 in.body.setattr.size == newsize);
420 }, Eq(true)),
421 _)
422 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
423 SET_OUT_HEADER_LEN(out, attr);
424 out.body.attr.attr.ino = ino; // Must match nodeid
425 out.body.attr.attr.mode = S_IFREG | 0644;
426 out.body.attr.attr.size = newsize;
427 })));
428 EXPECT_EQ(0, truncate(FULLPATH, newsize)) << strerror(errno);
429 }
430
431 /*
432 * Truncating a file should discard cached data past the truncation point.
433 * This is a regression test for bug 233783.
434 *
435 * There are two distinct failure modes. The first one is a failure to zero
436 * the portion of the file's final buffer past EOF. It can be reproduced by
437 * fsx -WR -P /tmp -S10 fsx.bin
438 *
439 * The second is a failure to drop buffers beyond that. It can be reproduced by
440 * fsx -WR -P /tmp -S18 -n fsx.bin
441 * Also reproducible in sh with:
442 * $> /path/to/libfuse/build/example/passthrough -d /tmp/mnt
443 * $> cd /tmp/mnt/tmp
444 * $> dd if=/dev/random of=randfile bs=1k count=192
445 * $> truncate -s 1k randfile && truncate -s 192k randfile
446 * $> xxd randfile | less # xxd will wrongly show random data at offset 0x8000
447 */
TEST_F(Setattr,truncate_discards_cached_data)448 TEST_F(Setattr, truncate_discards_cached_data) {
449 const char FULLPATH[] = "mountpoint/some_file.txt";
450 const char RELPATH[] = "some_file.txt";
451 char *w0buf, *r0buf, *r1buf, *expected;
452 off_t w0_offset = 0;
453 size_t w0_size = 0x30000;
454 off_t r0_offset = 0;
455 off_t r0_size = w0_size;
456 size_t trunc0_size = 0x400;
457 size_t trunc1_size = w0_size;
458 off_t r1_offset = trunc0_size;
459 off_t r1_size = w0_size - trunc0_size;
460 size_t cur_size = 0;
461 const uint64_t ino = 42;
462 mode_t mode = S_IFREG | 0644;
463 int fd, r;
464 bool should_have_data = false;
465
466 w0buf = new char[w0_size];
467 memset(w0buf, 'X', w0_size);
468
469 r0buf = new char[r0_size];
470 r1buf = new char[r1_size];
471
472 expected = new char[r1_size]();
473
474 expect_lookup(RELPATH, ino, mode, 0, 1);
475 expect_open(ino, O_RDWR, 1);
476 EXPECT_CALL(*m_mock, process(
477 ResultOf([=](auto in) {
478 return (in.header.opcode == FUSE_GETATTR &&
479 in.header.nodeid == ino);
480 }, Eq(true)),
481 _)
482 ).WillRepeatedly(Invoke(ReturnImmediate([&](auto i __unused, auto& out) {
483 SET_OUT_HEADER_LEN(out, attr);
484 out.body.attr.attr.ino = ino;
485 out.body.attr.attr.mode = mode;
486 out.body.attr.attr.size = cur_size;
487 })));
488 EXPECT_CALL(*m_mock, process(
489 ResultOf([=](auto in) {
490 return (in.header.opcode == FUSE_WRITE);
491 }, Eq(true)),
492 _)
493 ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) {
494 SET_OUT_HEADER_LEN(out, write);
495 out.body.attr.attr.ino = ino;
496 out.body.write.size = in.body.write.size;
497 cur_size = std::max(static_cast<uint64_t>(cur_size),
498 in.body.write.size + in.body.write.offset);
499 })));
500
501 EXPECT_CALL(*m_mock, process(
502 ResultOf([=](auto in) {
503 return (in.header.opcode == FUSE_SETATTR &&
504 in.header.nodeid == ino &&
505 (in.body.setattr.valid & FATTR_SIZE));
506 }, Eq(true)),
507 _)
508 ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) {
509 auto trunc_size = in.body.setattr.size;
510 SET_OUT_HEADER_LEN(out, attr);
511 out.body.attr.attr.ino = ino;
512 out.body.attr.attr.mode = mode;
513 out.body.attr.attr.size = trunc_size;
514 cur_size = trunc_size;
515 })));
516
517 EXPECT_CALL(*m_mock, process(
518 ResultOf([=](auto in) {
519 return (in.header.opcode == FUSE_READ);
520 }, Eq(true)),
521 _)
522 ).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) {
523 auto osize = std::min(
524 static_cast<uint64_t>(cur_size) - in.body.read.offset,
525 static_cast<uint64_t>(in.body.read.size));
526 assert(osize <= sizeof(out.body.bytes));
527 out.header.len = sizeof(struct fuse_out_header) + osize;
528 if (should_have_data)
529 memset(out.body.bytes, 'X', osize);
530 else
531 bzero(out.body.bytes, osize);
532 })));
533
534 fd = open(FULLPATH, O_RDWR, 0644);
535 ASSERT_LE(0, fd) << strerror(errno);
536
537 /* Fill the file with Xs */
538 ASSERT_EQ(static_cast<ssize_t>(w0_size),
539 pwrite(fd, w0buf, w0_size, w0_offset));
540 should_have_data = true;
541 /* Fill the cache */
542 ASSERT_EQ(static_cast<ssize_t>(r0_size),
543 pread(fd, r0buf, r0_size, r0_offset));
544 /* 1st truncate should discard cached data */
545 EXPECT_EQ(0, ftruncate(fd, trunc0_size)) << strerror(errno);
546 should_have_data = false;
547 /* 2nd truncate extends file into previously cached data */
548 EXPECT_EQ(0, ftruncate(fd, trunc1_size)) << strerror(errno);
549 /* Read should return all zeros */
550 ASSERT_EQ(static_cast<ssize_t>(r1_size),
551 pread(fd, r1buf, r1_size, r1_offset));
552
553 r = memcmp(expected, r1buf, r1_size);
554 ASSERT_EQ(0, r);
555
556 delete[] expected;
557 delete[] r1buf;
558 delete[] r0buf;
559 delete[] w0buf;
560
561 leak(fd);
562 }
563
564 /* truncate should fail if it would cause the file to exceed RLIMIT_FSIZE */
TEST_F(Setattr,truncate_rlimit_rsize)565 TEST_F(Setattr, truncate_rlimit_rsize)
566 {
567 const char FULLPATH[] = "mountpoint/some_file.txt";
568 const char RELPATH[] = "some_file.txt";
569 struct rlimit rl;
570 const uint64_t ino = 42;
571 const uint64_t oldsize = 0;
572 const uint64_t newsize = 100'000'000;
573
574 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
575 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
576 SET_OUT_HEADER_LEN(out, entry);
577 out.body.entry.attr.mode = S_IFREG | 0644;
578 out.body.entry.nodeid = ino;
579 out.body.entry.attr.size = oldsize;
580 })));
581
582 rl.rlim_cur = newsize / 2;
583 rl.rlim_max = 10 * newsize;
584 ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno);
585 ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno);
586
587 EXPECT_EQ(-1, truncate(FULLPATH, newsize));
588 EXPECT_EQ(EFBIG, errno);
589 EXPECT_EQ(1, s_sigxfsz);
590 }
591
592 /* Change a file's timestamps */
TEST_F(Setattr,utimensat)593 TEST_F(Setattr, utimensat) {
594 const char FULLPATH[] = "mountpoint/some_file.txt";
595 const char RELPATH[] = "some_file.txt";
596 const uint64_t ino = 42;
597 const timespec oldtimes[2] = {
598 {.tv_sec = 1, .tv_nsec = 2},
599 {.tv_sec = 3, .tv_nsec = 4},
600 };
601 const timespec newtimes[2] = {
602 {.tv_sec = 5, .tv_nsec = 6},
603 {.tv_sec = 7, .tv_nsec = 8},
604 };
605
606 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
607 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
608 SET_OUT_HEADER_LEN(out, entry);
609 out.body.entry.attr.mode = S_IFREG | 0644;
610 out.body.entry.nodeid = ino;
611 out.body.entry.attr_valid = UINT64_MAX;
612 out.body.entry.attr.atime = oldtimes[0].tv_sec;
613 out.body.entry.attr.atimensec = oldtimes[0].tv_nsec;
614 out.body.entry.attr.mtime = oldtimes[1].tv_sec;
615 out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec;
616 })));
617
618 EXPECT_CALL(*m_mock, process(
619 ResultOf([=](auto in) {
620 uint32_t valid = FATTR_ATIME | FATTR_MTIME;
621 return (in.header.opcode == FUSE_SETATTR &&
622 in.header.nodeid == ino &&
623 in.body.setattr.valid == valid &&
624 (time_t)in.body.setattr.atime ==
625 newtimes[0].tv_sec &&
626 (long)in.body.setattr.atimensec ==
627 newtimes[0].tv_nsec &&
628 (time_t)in.body.setattr.mtime ==
629 newtimes[1].tv_sec &&
630 (long)in.body.setattr.mtimensec ==
631 newtimes[1].tv_nsec);
632 }, Eq(true)),
633 _)
634 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
635 SET_OUT_HEADER_LEN(out, attr);
636 out.body.attr.attr.ino = ino; // Must match nodeid
637 out.body.attr.attr.mode = S_IFREG | 0644;
638 out.body.attr.attr.atime = newtimes[0].tv_sec;
639 out.body.attr.attr.atimensec = newtimes[0].tv_nsec;
640 out.body.attr.attr.mtime = newtimes[1].tv_sec;
641 out.body.attr.attr.mtimensec = newtimes[1].tv_nsec;
642 })));
643 EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0))
644 << strerror(errno);
645 }
646
647 /* Change a file mtime but not its atime */
TEST_F(Setattr,utimensat_mtime_only)648 TEST_F(Setattr, utimensat_mtime_only) {
649 const char FULLPATH[] = "mountpoint/some_file.txt";
650 const char RELPATH[] = "some_file.txt";
651 const uint64_t ino = 42;
652 const timespec oldtimes[2] = {
653 {.tv_sec = 1, .tv_nsec = 2},
654 {.tv_sec = 3, .tv_nsec = 4},
655 };
656 const timespec newtimes[2] = {
657 {.tv_sec = 5, .tv_nsec = UTIME_OMIT},
658 {.tv_sec = 7, .tv_nsec = 8},
659 };
660
661 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
662 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
663 SET_OUT_HEADER_LEN(out, entry);
664 out.body.entry.attr.mode = S_IFREG | 0644;
665 out.body.entry.nodeid = ino;
666 out.body.entry.attr_valid = UINT64_MAX;
667 out.body.entry.attr.atime = oldtimes[0].tv_sec;
668 out.body.entry.attr.atimensec = oldtimes[0].tv_nsec;
669 out.body.entry.attr.mtime = oldtimes[1].tv_sec;
670 out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec;
671 })));
672
673 EXPECT_CALL(*m_mock, process(
674 ResultOf([=](auto in) {
675 uint32_t valid = FATTR_MTIME;
676 return (in.header.opcode == FUSE_SETATTR &&
677 in.header.nodeid == ino &&
678 in.body.setattr.valid == valid &&
679 (time_t)in.body.setattr.mtime ==
680 newtimes[1].tv_sec &&
681 (long)in.body.setattr.mtimensec ==
682 newtimes[1].tv_nsec);
683 }, Eq(true)),
684 _)
685 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
686 SET_OUT_HEADER_LEN(out, attr);
687 out.body.attr.attr.ino = ino; // Must match nodeid
688 out.body.attr.attr.mode = S_IFREG | 0644;
689 out.body.attr.attr.atime = oldtimes[0].tv_sec;
690 out.body.attr.attr.atimensec = oldtimes[0].tv_nsec;
691 out.body.attr.attr.mtime = newtimes[1].tv_sec;
692 out.body.attr.attr.mtimensec = newtimes[1].tv_nsec;
693 })));
694 EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0))
695 << strerror(errno);
696 }
697
698 /*
699 * Set a file's mtime and atime to now
700 *
701 * The design of FreeBSD's VFS does not allow fusefs to set just one of atime
702 * or mtime to UTIME_NOW; it's both or neither.
703 */
TEST_F(Setattr,utimensat_utime_now)704 TEST_F(Setattr, utimensat_utime_now) {
705 const char FULLPATH[] = "mountpoint/some_file.txt";
706 const char RELPATH[] = "some_file.txt";
707 const uint64_t ino = 42;
708 const timespec oldtimes[2] = {
709 {.tv_sec = 1, .tv_nsec = 2},
710 {.tv_sec = 3, .tv_nsec = 4},
711 };
712 const timespec newtimes[2] = {
713 {.tv_sec = 0, .tv_nsec = UTIME_NOW},
714 {.tv_sec = 0, .tv_nsec = UTIME_NOW},
715 };
716 /* "now" is whatever the server says it is */
717 const timespec now[2] = {
718 {.tv_sec = 5, .tv_nsec = 7},
719 {.tv_sec = 6, .tv_nsec = 8},
720 };
721 struct stat sb;
722
723 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
724 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
725 SET_OUT_HEADER_LEN(out, entry);
726 out.body.entry.attr.mode = S_IFREG | 0644;
727 out.body.entry.nodeid = ino;
728 out.body.entry.attr_valid = UINT64_MAX;
729 out.body.entry.entry_valid = UINT64_MAX;
730 out.body.entry.attr.atime = oldtimes[0].tv_sec;
731 out.body.entry.attr.atimensec = oldtimes[0].tv_nsec;
732 out.body.entry.attr.mtime = oldtimes[1].tv_sec;
733 out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec;
734 })));
735
736 EXPECT_CALL(*m_mock, process(
737 ResultOf([=](auto in) {
738 uint32_t valid = FATTR_ATIME | FATTR_ATIME_NOW |
739 FATTR_MTIME | FATTR_MTIME_NOW;
740 return (in.header.opcode == FUSE_SETATTR &&
741 in.header.nodeid == ino &&
742 in.body.setattr.valid == valid);
743 }, Eq(true)),
744 _)
745 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
746 SET_OUT_HEADER_LEN(out, attr);
747 out.body.attr.attr.ino = ino; // Must match nodeid
748 out.body.attr.attr.mode = S_IFREG | 0644;
749 out.body.attr.attr.atime = now[0].tv_sec;
750 out.body.attr.attr.atimensec = now[0].tv_nsec;
751 out.body.attr.attr.mtime = now[1].tv_sec;
752 out.body.attr.attr.mtimensec = now[1].tv_nsec;
753 out.body.attr.attr_valid = UINT64_MAX;
754 })));
755 ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0))
756 << strerror(errno);
757 ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
758 EXPECT_EQ(now[0].tv_sec, sb.st_atim.tv_sec);
759 EXPECT_EQ(now[0].tv_nsec, sb.st_atim.tv_nsec);
760 EXPECT_EQ(now[1].tv_sec, sb.st_mtim.tv_sec);
761 EXPECT_EQ(now[1].tv_nsec, sb.st_mtim.tv_nsec);
762 }
763
764 /*
765 * FUSE_SETATTR returns a different file type, even though the entry cache
766 * hasn't expired. This is a server bug! It probably means that the server
767 * removed the file and recreated it with the same inode but a different vtyp.
768 * The best thing fusefs can do is return ENOENT to the caller. After all, the
769 * entry must not have existed recently.
770 */
TEST_F(Setattr,vtyp_conflict)771 TEST_F(Setattr, vtyp_conflict)
772 {
773 const char FULLPATH[] = "mountpoint/some_file.txt";
774 const char RELPATH[] = "some_file.txt";
775 const uint64_t ino = 42;
776 uid_t newuser = 12345;
777 sem_t sem;
778
779 ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
780
781 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
782 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
783 SET_OUT_HEADER_LEN(out, entry);
784 out.body.entry.attr.mode = S_IFREG | 0777;
785 out.body.entry.nodeid = ino;
786 out.body.entry.entry_valid = UINT64_MAX;
787 })));
788
789 EXPECT_CALL(*m_mock, process(
790 ResultOf([](auto in) {
791 return (in.header.opcode == FUSE_SETATTR &&
792 in.header.nodeid == ino);
793 }, Eq(true)),
794 _)
795 ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
796 SET_OUT_HEADER_LEN(out, attr);
797 out.body.attr.attr.ino = ino;
798 out.body.attr.attr.mode = S_IFDIR | 0777; // Changed!
799 out.body.attr.attr.uid = newuser;
800 })));
801 // We should reclaim stale vnodes
802 expect_forget(ino, 1, &sem);
803
804 EXPECT_NE(0, chown(FULLPATH, newuser, -1));
805 EXPECT_EQ(ENOENT, errno);
806
807 sem_wait(&sem);
808 sem_destroy(&sem);
809 }
810
811 /* On a read-only mount, no attributes may be changed */
TEST_F(RofsSetattr,erofs)812 TEST_F(RofsSetattr, erofs)
813 {
814 const char FULLPATH[] = "mountpoint/some_file.txt";
815 const char RELPATH[] = "some_file.txt";
816 const uint64_t ino = 42;
817 const mode_t oldmode = 0755;
818 const mode_t newmode = 0644;
819
820 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
821 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
822 SET_OUT_HEADER_LEN(out, entry);
823 out.body.entry.attr.mode = S_IFREG | oldmode;
824 out.body.entry.nodeid = ino;
825 })));
826
827 ASSERT_EQ(-1, chmod(FULLPATH, newmode));
828 ASSERT_EQ(EROFS, errno);
829 }
830
831 /* Change the mode of a file */
TEST_F(Setattr_7_8,chmod)832 TEST_F(Setattr_7_8, chmod)
833 {
834 const char FULLPATH[] = "mountpoint/some_file.txt";
835 const char RELPATH[] = "some_file.txt";
836 const uint64_t ino = 42;
837 const mode_t oldmode = 0755;
838 const mode_t newmode = 0644;
839
840 EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
841 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
842 SET_OUT_HEADER_LEN(out, entry_7_8);
843 out.body.entry.attr.mode = S_IFREG | oldmode;
844 out.body.entry.nodeid = ino;
845 })));
846
847 EXPECT_CALL(*m_mock, process(
848 ResultOf([](auto in) {
849 uint32_t valid = FATTR_MODE;
850 return (in.header.opcode == FUSE_SETATTR &&
851 in.header.nodeid == ino &&
852 in.body.setattr.valid == valid &&
853 in.body.setattr.mode == newmode);
854 }, Eq(true)),
855 _)
856 ).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
857 SET_OUT_HEADER_LEN(out, attr_7_8);
858 out.body.attr.attr.ino = ino; // Must match nodeid
859 out.body.attr.attr.mode = S_IFREG | newmode;
860 })));
861 EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
862 }
863