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