diff options
Diffstat (limited to 'tests/sys/fs/fusefs/setattr.cc')
| -rw-r--r-- | tests/sys/fs/fusefs/setattr.cc | 862 | 
1 files changed, 862 insertions, 0 deletions
| diff --git a/tests/sys/fs/fusefs/setattr.cc b/tests/sys/fs/fusefs/setattr.cc new file mode 100644 index 000000000000..79559db33b12 --- /dev/null +++ b/tests/sys/fs/fusefs/setattr.cc @@ -0,0 +1,862 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2019 The FreeBSD Foundation + * + * This software was developed by BFF Storage Systems, LLC under sponsorship + * from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + *    notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + *    notice, this list of conditions and the following disclaimer in the + *    documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +extern "C" { +#include <sys/types.h> +#include <sys/resource.h> +#include <sys/stat.h> +#include <sys/time.h> + +#include <fcntl.h> +#include <semaphore.h> +#include <signal.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +class Setattr : public FuseTest { +public: +static sig_atomic_t s_sigxfsz; +}; + +class RofsSetattr: public Setattr { +public: +virtual void SetUp() { +	s_sigxfsz = 0; +	m_ro = true; +	Setattr::SetUp(); +} +}; + +class Setattr_7_8: public Setattr { +public: +virtual void SetUp() { +	m_kernel_minor_version = 8; +	Setattr::SetUp(); +} +}; + + +sig_atomic_t Setattr::s_sigxfsz = 0; + +void sigxfsz_handler(int __unused sig) { +	Setattr::s_sigxfsz = 1; +} + +/* + * If setattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs + * should use the cached attributes, rather than query the daemon + */ +TEST_F(Setattr, attr_cache) +{ +	const char FULLPATH[] = "mountpoint/some_file.txt"; +	const char RELPATH[] = "some_file.txt"; +	const uint64_t ino = 42; +	struct stat sb; +	const mode_t newmode = 0644; + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) +	.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, entry); +		out.body.entry.attr.mode = S_IFREG | 0644; +		out.body.entry.nodeid = ino; +		out.body.entry.entry_valid = UINT64_MAX; +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([](auto in) { +			return (in.header.opcode == FUSE_SETATTR && +				in.header.nodeid == ino); +		}, Eq(true)), +		_) +	).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, attr); +		out.body.attr.attr.ino = ino;	// Must match nodeid +		out.body.attr.attr.mode = S_IFREG | newmode; +		out.body.attr.attr_valid = UINT64_MAX; +	}))); +	EXPECT_CALL(*m_mock, process( +		ResultOf([](auto in) { +			return (in.header.opcode == FUSE_GETATTR); +		}, Eq(true)), +		_) +	).Times(0); + +	/* Set an attribute with SETATTR */ +	ASSERT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); + +	/* The stat(2) should use cached attributes */ +	ASSERT_EQ(0, stat(FULLPATH, &sb)); +	EXPECT_EQ(S_IFREG | newmode, sb.st_mode); +} + +/* Change the mode of a file */ +TEST_F(Setattr, chmod) +{ +	const char FULLPATH[] = "mountpoint/some_file.txt"; +	const char RELPATH[] = "some_file.txt"; +	const uint64_t ino = 42; +	const mode_t oldmode = 0755; +	const mode_t newmode = 0644; + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) +	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, entry); +		out.body.entry.attr.mode = S_IFREG | oldmode; +		out.body.entry.nodeid = ino; +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([](auto in) { +			uint32_t valid = FATTR_MODE; +			return (in.header.opcode == FUSE_SETATTR && +				in.header.nodeid == ino && +				in.body.setattr.valid == valid && +				in.body.setattr.mode == newmode); +		}, Eq(true)), +		_) +	).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, attr); +		out.body.attr.attr.ino = ino;	// Must match nodeid +		out.body.attr.attr.mode = S_IFREG | newmode; +	}))); +	EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); +} + +/*  + * Chmod a multiply-linked file with cached attributes.  Check that both files' + * attributes have changed. + */ +TEST_F(Setattr, chmod_multiply_linked) +{ +	const char FULLPATH0[] = "mountpoint/some_file.txt"; +	const char RELPATH0[] = "some_file.txt"; +	const char FULLPATH1[] = "mountpoint/other_file.txt"; +	const char RELPATH1[] = "other_file.txt"; +	struct stat sb; +	const uint64_t ino = 42; +	const mode_t oldmode = 0777; +	const mode_t newmode = 0666; + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH0) +	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, entry); +		out.body.entry.attr.mode = S_IFREG | oldmode; +		out.body.entry.nodeid = ino; +		out.body.entry.attr.nlink = 2; +		out.body.entry.attr_valid = UINT64_MAX; +		out.body.entry.entry_valid = UINT64_MAX; +	}))); + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH1) +	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, entry); +		out.body.entry.attr.mode = S_IFREG | oldmode; +		out.body.entry.nodeid = ino; +		out.body.entry.attr.nlink = 2; +		out.body.entry.attr_valid = UINT64_MAX; +		out.body.entry.entry_valid = UINT64_MAX; +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([](auto in) { +			uint32_t valid = FATTR_MODE; +			return (in.header.opcode == FUSE_SETATTR && +				in.header.nodeid == ino && +				in.body.setattr.valid == valid && +				in.body.setattr.mode == newmode); +		}, Eq(true)), +		_) +	).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, attr); +		out.body.attr.attr.ino = ino; +		out.body.attr.attr.mode = S_IFREG | newmode; +		out.body.attr.attr.nlink = 2; +		out.body.attr.attr_valid = UINT64_MAX; +	}))); + +	/* For a lookup of the 2nd file to get it into the cache*/ +	ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno); +	EXPECT_EQ(S_IFREG | oldmode, sb.st_mode); + +	ASSERT_EQ(0, chmod(FULLPATH0, newmode)) << strerror(errno); +	ASSERT_EQ(0, stat(FULLPATH0, &sb)) << strerror(errno); +	EXPECT_EQ(S_IFREG | newmode, sb.st_mode); +	ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno); +	EXPECT_EQ(S_IFREG | newmode, sb.st_mode); +} + + +/* Change the owner and group of a file */ +TEST_F(Setattr, chown) +{ +	const char FULLPATH[] = "mountpoint/some_file.txt"; +	const char RELPATH[] = "some_file.txt"; +	const uint64_t ino = 42; +	const gid_t oldgroup = 66; +	const gid_t newgroup = 99; +	const uid_t olduser = 33; +	const uid_t newuser = 44; + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) +	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, entry); +		out.body.entry.attr.mode = S_IFREG | 0644; +		out.body.entry.nodeid = ino; +		out.body.entry.attr.gid = oldgroup; +		out.body.entry.attr.uid = olduser; +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([](auto in) { +			uint32_t valid = FATTR_GID | FATTR_UID; +			return (in.header.opcode == FUSE_SETATTR && +				in.header.nodeid == ino && +				in.body.setattr.valid == valid && +				in.body.setattr.uid == newuser && +				in.body.setattr.gid == newgroup); +		}, Eq(true)), +		_) +	).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, attr); +		out.body.attr.attr.ino = ino;	// Must match nodeid +		out.body.attr.attr.mode = S_IFREG | 0644; +		out.body.attr.attr.uid = newuser; +		out.body.attr.attr.gid = newgroup; +	}))); +	EXPECT_EQ(0, chown(FULLPATH, newuser, newgroup)) << strerror(errno); +} + + + +/*  + * FUSE daemons are allowed to check permissions however they like.  If the + * daemon returns EPERM, even if the file permissions "should" grant access, + * then fuse(4) should return EPERM too. + */ +TEST_F(Setattr, eperm) +{ +	const char FULLPATH[] = "mountpoint/some_file.txt"; +	const char RELPATH[] = "some_file.txt"; +	const uint64_t ino = 42; + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) +	.WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) { +		SET_OUT_HEADER_LEN(out, entry); +		out.body.entry.attr.mode = S_IFREG | 0777; +		out.body.entry.nodeid = ino; +		out.body.entry.attr.uid = in.header.uid; +		out.body.entry.attr.gid = in.header.gid; +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([](auto in) { +			return (in.header.opcode == FUSE_SETATTR && +				in.header.nodeid == ino); +		}, Eq(true)), +		_) +	).WillOnce(Invoke(ReturnErrno(EPERM))); +	EXPECT_NE(0, truncate(FULLPATH, 10)); +	EXPECT_EQ(EPERM, errno); +} + +/* Change the mode of an open file, by its file descriptor */ +TEST_F(Setattr, fchmod) +{ +	const char FULLPATH[] = "mountpoint/some_file.txt"; +	const char RELPATH[] = "some_file.txt"; +	uint64_t ino = 42; +	int fd; +	const mode_t oldmode = 0755; +	const mode_t newmode = 0644; + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) +	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, entry); +		out.body.entry.attr.mode = S_IFREG | oldmode; +		out.body.entry.nodeid = ino; +		out.body.entry.attr_valid = UINT64_MAX; +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([=](auto in) { +			return (in.header.opcode == FUSE_OPEN && +				in.header.nodeid == ino); +		}, Eq(true)), +		_) +	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		out.header.len = sizeof(out.header); +		SET_OUT_HEADER_LEN(out, open); +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([=](auto in) { +			uint32_t valid = FATTR_MODE; +			return (in.header.opcode == FUSE_SETATTR && +				in.header.nodeid == ino && +				in.body.setattr.valid == valid && +				in.body.setattr.mode == newmode); +		}, Eq(true)), +		_) +	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, attr); +		out.body.attr.attr.ino = ino;	// Must match nodeid +		out.body.attr.attr.mode = S_IFREG | newmode; +	}))); + +	fd = open(FULLPATH, O_RDONLY); +	ASSERT_LE(0, fd) << strerror(errno); +	ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno); +	leak(fd); +} + +/* Change the size of an open file, by its file descriptor */ +TEST_F(Setattr, ftruncate) +{ +	const char FULLPATH[] = "mountpoint/some_file.txt"; +	const char RELPATH[] = "some_file.txt"; +	uint64_t ino = 42; +	int fd; +	uint64_t fh = 0xdeadbeef1a7ebabe; +	const off_t oldsize = 99; +	const off_t newsize = 12345; + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) +	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, entry); +		out.body.entry.attr.mode = S_IFREG | 0755; +		out.body.entry.nodeid = ino; +		out.body.entry.attr_valid = UINT64_MAX; +		out.body.entry.attr.size = oldsize; +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([=](auto in) { +			return (in.header.opcode == FUSE_OPEN && +				in.header.nodeid == ino); +		}, Eq(true)), +		_) +	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		out.header.len = sizeof(out.header); +		SET_OUT_HEADER_LEN(out, open); +		out.body.open.fh = fh; +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([=](auto in) { +			uint32_t valid = FATTR_SIZE | FATTR_FH; +			return (in.header.opcode == FUSE_SETATTR && +				in.header.nodeid == ino && +				in.body.setattr.valid == valid && +				in.body.setattr.fh == fh); +		}, Eq(true)), +		_) +	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, attr); +		out.body.attr.attr.ino = ino;	// Must match nodeid +		out.body.attr.attr.mode = S_IFREG | 0755; +		out.body.attr.attr.size = newsize; +	}))); + +	fd = open(FULLPATH, O_RDWR); +	ASSERT_LE(0, fd) << strerror(errno); +	ASSERT_EQ(0, ftruncate(fd, newsize)) << strerror(errno); +	leak(fd); +} + +/* Change the size of the file */ +TEST_F(Setattr, truncate) { +	const char FULLPATH[] = "mountpoint/some_file.txt"; +	const char RELPATH[] = "some_file.txt"; +	const uint64_t ino = 42; +	const uint64_t oldsize = 100'000'000; +	const uint64_t newsize = 20'000'000; + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) +	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, entry); +		out.body.entry.attr.mode = S_IFREG | 0644; +		out.body.entry.nodeid = ino; +		out.body.entry.attr.size = oldsize; +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([](auto in) { +			uint32_t valid = FATTR_SIZE; +			return (in.header.opcode == FUSE_SETATTR && +				in.header.nodeid == ino && +				in.body.setattr.valid == valid && +				in.body.setattr.size == newsize); +		}, Eq(true)), +		_) +	).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, attr); +		out.body.attr.attr.ino = ino;	// Must match nodeid +		out.body.attr.attr.mode = S_IFREG | 0644; +		out.body.attr.attr.size = newsize; +	}))); +	EXPECT_EQ(0, truncate(FULLPATH, newsize)) << strerror(errno); +} + +/* + * Truncating a file should discard cached data past the truncation point. + * This is a regression test for bug 233783. + * + * There are two distinct failure modes.  The first one is a failure to zero + * the portion of the file's final buffer past EOF.  It can be reproduced by + * fsx -WR -P /tmp -S10 fsx.bin + * + * The second is a failure to drop buffers beyond that.  It can be reproduced by + * fsx -WR -P /tmp -S18 -n fsx.bin + * Also reproducible in sh with: + * $> /path/to/libfuse/build/example/passthrough -d /tmp/mnt + * $> cd /tmp/mnt/tmp + * $> dd if=/dev/random of=randfile bs=1k count=192 + * $> truncate -s 1k randfile && truncate -s 192k randfile + * $> xxd randfile | less # xxd will wrongly show random data at offset 0x8000 + */ +TEST_F(Setattr, truncate_discards_cached_data) { +	const char FULLPATH[] = "mountpoint/some_file.txt"; +	const char RELPATH[] = "some_file.txt"; +	char *w0buf, *r0buf, *r1buf, *expected; +	off_t w0_offset = 0; +	size_t w0_size = 0x30000; +	off_t r0_offset = 0; +	off_t r0_size = w0_size; +	size_t trunc0_size = 0x400; +	size_t trunc1_size = w0_size; +	off_t r1_offset = trunc0_size; +	off_t r1_size = w0_size - trunc0_size; +	size_t cur_size = 0; +	const uint64_t ino = 42; +	mode_t mode = S_IFREG | 0644; +	int fd, r; +	bool should_have_data = false; + +	w0buf = new char[w0_size]; +	memset(w0buf, 'X', w0_size); + +	r0buf = new char[r0_size]; +	r1buf = new char[r1_size]; + +	expected = new char[r1_size](); + +	expect_lookup(RELPATH, ino, mode, 0, 1); +	expect_open(ino, O_RDWR, 1); +	EXPECT_CALL(*m_mock, process( +		ResultOf([=](auto in) { +			return (in.header.opcode == FUSE_GETATTR && +				in.header.nodeid == ino); +		}, Eq(true)), +		_) +	).WillRepeatedly(Invoke(ReturnImmediate([&](auto i __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, attr); +		out.body.attr.attr.ino = ino; +		out.body.attr.attr.mode = mode; +		out.body.attr.attr.size = cur_size; +	}))); +	EXPECT_CALL(*m_mock, process( +		ResultOf([=](auto in) { +			return (in.header.opcode == FUSE_WRITE); +		}, Eq(true)), +		_) +	).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) { +		SET_OUT_HEADER_LEN(out, write); +		out.body.attr.attr.ino = ino; +		out.body.write.size = in.body.write.size; +		cur_size = std::max(static_cast<uint64_t>(cur_size), +			in.body.write.size + in.body.write.offset); +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([=](auto in) { +			return (in.header.opcode == FUSE_SETATTR && +				in.header.nodeid == ino && +				(in.body.setattr.valid & FATTR_SIZE)); +		}, Eq(true)), +		_) +	).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) { +		auto trunc_size = in.body.setattr.size; +		SET_OUT_HEADER_LEN(out, attr); +		out.body.attr.attr.ino = ino; +		out.body.attr.attr.mode = mode; +		out.body.attr.attr.size = trunc_size; +		cur_size = trunc_size; +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([=](auto in) { +			return (in.header.opcode == FUSE_READ); +		}, Eq(true)), +		_) +	).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) { +		auto osize = std::min( +			static_cast<uint64_t>(cur_size) - in.body.read.offset, +			static_cast<uint64_t>(in.body.read.size)); +		assert(osize <= sizeof(out.body.bytes)); +		out.header.len = sizeof(struct fuse_out_header) + osize; +		if (should_have_data) +			memset(out.body.bytes, 'X', osize); +		else +			bzero(out.body.bytes, osize); +	}))); + +	fd = open(FULLPATH, O_RDWR, 0644); +	ASSERT_LE(0, fd) << strerror(errno); + +	/* Fill the file with Xs */ +	ASSERT_EQ(static_cast<ssize_t>(w0_size), +		pwrite(fd, w0buf, w0_size, w0_offset)); +	should_have_data = true; +	/* Fill the cache */ +	ASSERT_EQ(static_cast<ssize_t>(r0_size), +		pread(fd, r0buf, r0_size, r0_offset)); +	/* 1st truncate should discard cached data */ +	EXPECT_EQ(0, ftruncate(fd, trunc0_size)) << strerror(errno); +	should_have_data = false; +	/* 2nd truncate extends file into previously cached data */ +	EXPECT_EQ(0, ftruncate(fd, trunc1_size)) << strerror(errno); +	/* Read should return all zeros */ +	ASSERT_EQ(static_cast<ssize_t>(r1_size), +		pread(fd, r1buf, r1_size, r1_offset)); + +	r = memcmp(expected, r1buf, r1_size); +	ASSERT_EQ(0, r); + +	delete[] expected; +	delete[] r1buf; +	delete[] r0buf; +	delete[] w0buf; + +	leak(fd); +} + +/* truncate should fail if it would cause the file to exceed RLIMIT_FSIZE */ +TEST_F(Setattr, truncate_rlimit_rsize) +{ +	const char FULLPATH[] = "mountpoint/some_file.txt"; +	const char RELPATH[] = "some_file.txt"; +	struct rlimit rl; +	const uint64_t ino = 42; +	const uint64_t oldsize = 0; +	const uint64_t newsize = 100'000'000; + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) +	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, entry); +		out.body.entry.attr.mode = S_IFREG | 0644; +		out.body.entry.nodeid = ino; +		out.body.entry.attr.size = oldsize; +	}))); + +	rl.rlim_cur = newsize / 2; +	rl.rlim_max = 10 * newsize; +	ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno); +	ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno); + +	EXPECT_EQ(-1, truncate(FULLPATH, newsize)); +	EXPECT_EQ(EFBIG, errno); +	EXPECT_EQ(1, s_sigxfsz); +} + +/* Change a file's timestamps */ +TEST_F(Setattr, utimensat) { +	const char FULLPATH[] = "mountpoint/some_file.txt"; +	const char RELPATH[] = "some_file.txt"; +	const uint64_t ino = 42; +	const timespec oldtimes[2] = { +		{.tv_sec = 1, .tv_nsec = 2}, +		{.tv_sec = 3, .tv_nsec = 4}, +	}; +	const timespec newtimes[2] = { +		{.tv_sec = 5, .tv_nsec = 6}, +		{.tv_sec = 7, .tv_nsec = 8}, +	}; + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) +	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, entry); +		out.body.entry.attr.mode = S_IFREG | 0644; +		out.body.entry.nodeid = ino; +		out.body.entry.attr_valid = UINT64_MAX; +		out.body.entry.attr.atime = oldtimes[0].tv_sec; +		out.body.entry.attr.atimensec = oldtimes[0].tv_nsec; +		out.body.entry.attr.mtime = oldtimes[1].tv_sec; +		out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec; +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([=](auto in) { +			uint32_t valid = FATTR_ATIME | FATTR_MTIME; +			return (in.header.opcode == FUSE_SETATTR && +				in.header.nodeid == ino && +				in.body.setattr.valid == valid && +				(time_t)in.body.setattr.atime == +					newtimes[0].tv_sec && +				(long)in.body.setattr.atimensec == +					newtimes[0].tv_nsec && +				(time_t)in.body.setattr.mtime == +					newtimes[1].tv_sec && +				(long)in.body.setattr.mtimensec == +					newtimes[1].tv_nsec); +		}, Eq(true)), +		_) +	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, attr); +		out.body.attr.attr.ino = ino;	// Must match nodeid +		out.body.attr.attr.mode = S_IFREG | 0644; +		out.body.attr.attr.atime = newtimes[0].tv_sec; +		out.body.attr.attr.atimensec = newtimes[0].tv_nsec; +		out.body.attr.attr.mtime = newtimes[1].tv_sec; +		out.body.attr.attr.mtimensec = newtimes[1].tv_nsec; +	}))); +	EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0)) +		<< strerror(errno); +} + +/* Change a file mtime but not its atime */ +TEST_F(Setattr, utimensat_mtime_only) { +	const char FULLPATH[] = "mountpoint/some_file.txt"; +	const char RELPATH[] = "some_file.txt"; +	const uint64_t ino = 42; +	const timespec oldtimes[2] = { +		{.tv_sec = 1, .tv_nsec = 2}, +		{.tv_sec = 3, .tv_nsec = 4}, +	}; +	const timespec newtimes[2] = { +		{.tv_sec = 5, .tv_nsec = UTIME_OMIT}, +		{.tv_sec = 7, .tv_nsec = 8}, +	}; + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) +	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, entry); +		out.body.entry.attr.mode = S_IFREG | 0644; +		out.body.entry.nodeid = ino; +		out.body.entry.attr_valid = UINT64_MAX; +		out.body.entry.attr.atime = oldtimes[0].tv_sec; +		out.body.entry.attr.atimensec = oldtimes[0].tv_nsec; +		out.body.entry.attr.mtime = oldtimes[1].tv_sec; +		out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec; +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([=](auto in) { +			uint32_t valid = FATTR_MTIME; +			return (in.header.opcode == FUSE_SETATTR && +				in.header.nodeid == ino && +				in.body.setattr.valid == valid && +				(time_t)in.body.setattr.mtime == +					newtimes[1].tv_sec && +				(long)in.body.setattr.mtimensec == +					newtimes[1].tv_nsec); +		}, Eq(true)), +		_) +	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, attr); +		out.body.attr.attr.ino = ino;	// Must match nodeid +		out.body.attr.attr.mode = S_IFREG | 0644; +		out.body.attr.attr.atime = oldtimes[0].tv_sec; +		out.body.attr.attr.atimensec = oldtimes[0].tv_nsec; +		out.body.attr.attr.mtime = newtimes[1].tv_sec; +		out.body.attr.attr.mtimensec = newtimes[1].tv_nsec; +	}))); +	EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0)) +		<< strerror(errno); +} + +/* + * Set a file's mtime and atime to now + * + * The design of FreeBSD's VFS does not allow fusefs to set just one of atime + * or mtime to UTIME_NOW; it's both or neither. + */ +TEST_F(Setattr, utimensat_utime_now) { +	const char FULLPATH[] = "mountpoint/some_file.txt"; +	const char RELPATH[] = "some_file.txt"; +	const uint64_t ino = 42; +	const timespec oldtimes[2] = { +		{.tv_sec = 1, .tv_nsec = 2}, +		{.tv_sec = 3, .tv_nsec = 4}, +	}; +	const timespec newtimes[2] = { +		{.tv_sec = 0, .tv_nsec = UTIME_NOW}, +		{.tv_sec = 0, .tv_nsec = UTIME_NOW}, +	}; +	/* "now" is whatever the server says it is */ +	const timespec now[2] = { +		{.tv_sec = 5, .tv_nsec = 7}, +		{.tv_sec = 6, .tv_nsec = 8}, +	}; +	struct stat sb; + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) +	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, entry); +		out.body.entry.attr.mode = S_IFREG | 0644; +		out.body.entry.nodeid = ino; +		out.body.entry.attr_valid = UINT64_MAX; +		out.body.entry.entry_valid = UINT64_MAX; +		out.body.entry.attr.atime = oldtimes[0].tv_sec; +		out.body.entry.attr.atimensec = oldtimes[0].tv_nsec; +		out.body.entry.attr.mtime = oldtimes[1].tv_sec; +		out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec; +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([=](auto in) { +			uint32_t valid = FATTR_ATIME | FATTR_ATIME_NOW | +				FATTR_MTIME | FATTR_MTIME_NOW; +			return (in.header.opcode == FUSE_SETATTR && +				in.header.nodeid == ino && +				in.body.setattr.valid == valid); +		}, Eq(true)), +		_) +	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, attr); +		out.body.attr.attr.ino = ino;	// Must match nodeid +		out.body.attr.attr.mode = S_IFREG | 0644; +		out.body.attr.attr.atime = now[0].tv_sec; +		out.body.attr.attr.atimensec = now[0].tv_nsec; +		out.body.attr.attr.mtime = now[1].tv_sec; +		out.body.attr.attr.mtimensec = now[1].tv_nsec; +		out.body.attr.attr_valid = UINT64_MAX; +	}))); +	ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0)) +		<< strerror(errno); +	ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno); +	EXPECT_EQ(now[0].tv_sec, sb.st_atim.tv_sec); +	EXPECT_EQ(now[0].tv_nsec, sb.st_atim.tv_nsec); +	EXPECT_EQ(now[1].tv_sec, sb.st_mtim.tv_sec); +	EXPECT_EQ(now[1].tv_nsec, sb.st_mtim.tv_nsec); +} + +/* + * FUSE_SETATTR returns a different file type, even though the entry cache + * hasn't expired.  This is a server bug!  It probably means that the server + * removed the file and recreated it with the same inode but a different vtyp. + * The best thing fusefs can do is return ENOENT to the caller.  After all, the + * entry must not have existed recently. + */ +TEST_F(Setattr, vtyp_conflict) +{ +	const char FULLPATH[] = "mountpoint/some_file.txt"; +	const char RELPATH[] = "some_file.txt"; +	const uint64_t ino = 42; +	uid_t newuser = 12345; +	sem_t sem; + +	ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno); + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) +	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, entry); +		out.body.entry.attr.mode = S_IFREG | 0777; +		out.body.entry.nodeid = ino; +		out.body.entry.entry_valid = UINT64_MAX; +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([](auto in) { +			return (in.header.opcode == FUSE_SETATTR && +				in.header.nodeid == ino); +		}, Eq(true)), +		_) +	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, attr); +		out.body.attr.attr.ino = ino; +		out.body.attr.attr.mode = S_IFDIR | 0777;	// Changed! +		out.body.attr.attr.uid = newuser; +	}))); +	// We should reclaim stale vnodes +	expect_forget(ino, 1, &sem); + +	EXPECT_NE(0, chown(FULLPATH, newuser, -1)); +	EXPECT_EQ(ENOENT, errno); + +	sem_wait(&sem); +	sem_destroy(&sem); +} + +/* On a read-only mount, no attributes may be changed */ +TEST_F(RofsSetattr, erofs) +{ +	const char FULLPATH[] = "mountpoint/some_file.txt"; +	const char RELPATH[] = "some_file.txt"; +	const uint64_t ino = 42; +	const mode_t oldmode = 0755; +	const mode_t newmode = 0644; + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) +	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, entry); +		out.body.entry.attr.mode = S_IFREG | oldmode; +		out.body.entry.nodeid = ino; +	}))); + +	ASSERT_EQ(-1, chmod(FULLPATH, newmode)); +	ASSERT_EQ(EROFS, errno); +} + +/* Change the mode of a file */ +TEST_F(Setattr_7_8, chmod) +{ +	const char FULLPATH[] = "mountpoint/some_file.txt"; +	const char RELPATH[] = "some_file.txt"; +	const uint64_t ino = 42; +	const mode_t oldmode = 0755; +	const mode_t newmode = 0644; + +	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) +	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, entry_7_8); +		out.body.entry.attr.mode = S_IFREG | oldmode; +		out.body.entry.nodeid = ino; +	}))); + +	EXPECT_CALL(*m_mock, process( +		ResultOf([](auto in) { +			uint32_t valid = FATTR_MODE; +			return (in.header.opcode == FUSE_SETATTR && +				in.header.nodeid == ino && +				in.body.setattr.valid == valid && +				in.body.setattr.mode == newmode); +		}, Eq(true)), +		_) +	).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) { +		SET_OUT_HEADER_LEN(out, attr_7_8); +		out.body.attr.attr.ino = ino;	// Must match nodeid +		out.body.attr.attr.mode = S_IFREG | newmode; +	}))); +	EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno); +} | 
