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