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