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