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