xref: /freebsd/tests/sys/fs/fusefs/setattr.cc (revision 2eb4d8dc723da3cf7d735a3226ae49da4c8c5dbc)
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:
48 virtual void SetUp() {
49 	m_ro = true;
50 	Setattr::SetUp();
51 }
52 };
53 
54 class Setattr_7_8: public Setattr {
55 public:
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  */
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 */
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  */
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 */
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  */
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 */
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 */
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 */
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  */
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 */
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 				(long)in.body.setattr.atimensec ==
590 					newtimes[0].tv_nsec &&
591 				(time_t)in.body.setattr.mtime ==
592 					newtimes[1].tv_sec &&
593 				(long)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 */
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 				(long)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  */
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 */
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 */
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