Alan Somers b0ecfb42d1 fusefs: avoid cache corruption with buggy fuse servers
The FUSE protocol allows the client (kernel) to cache a file's size, if the
server (userspace daemon) allows it. A well-behaved daemon obviously should
not change a file's size while a client has it cached. But a buggy daemon
might. If the kernel ever detects that that has happened, then it should
invalidate the entire cache for that file. Previously, we would not only
cache stale data, but in the case of a file extension while we had the size
cached, we accidentally extended the cache with zeros.

PR:		244178
Reported by:	Ben RUBSON <ben.rubson@gmx.com>
Reviewed by:	cem
MFC after:	2 weeks
Differential Revision:	https://reviews.freebsd.org/D24012
2020-03-11 04:29:45 +00:00

220 lines
6.4 KiB
C++

/*-
* SPDX-License-Identifier: BSD-2-Clause-FreeBSD
*
* Copyright (c) 2020 Alan Somers
*
* 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.
*
* $FreeBSD$
*/
extern "C" {
#include <sys/param.h>
#include <fcntl.h>
}
#include "mockfs.hh"
#include "utils.hh"
/*
* Tests for thorny cache problems not specific to any one opcode
*/
using namespace testing;
/*
* Parameters
* - reopen file - If true, close and reopen the file between reads
* - cache lookups - If true, allow lookups to be cached
* - cache attrs - If true, allow file attributes to be cached
* - cache_mode - uncached, writeback, or writethrough
* - initial size - File size before truncation
* - truncated size - File size after truncation
*/
typedef tuple<tuple<bool, bool, bool>, cache_mode, ssize_t, ssize_t> CacheParam;
class Cache: public FuseTest, public WithParamInterface<CacheParam> {
public:
bool m_direct_io;
Cache(): m_direct_io(false) {};
virtual void SetUp() {
int cache_mode = get<1>(GetParam());
switch (cache_mode) {
case Uncached:
m_direct_io = true;
break;
case WritebackAsync:
m_async = true;
/* FALLTHROUGH */
case Writeback:
m_init_flags |= FUSE_WRITEBACK_CACHE;
/* FALLTHROUGH */
case Writethrough:
break;
default:
FAIL() << "Unknown cache mode";
}
FuseTest::SetUp();
if (IsSkipped())
return;
}
void expect_getattr(uint64_t ino, int times, uint64_t size, uint64_t attr_valid)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).Times(times)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr_valid = attr_valid;
out.body.attr.attr.ino = ino;
out.body.attr.attr.mode = S_IFREG | 0644;
out.body.attr.attr.size = size;
})));
}
void expect_lookup(const char *relpath, uint64_t ino,
uint64_t size, uint64_t entry_valid, uint64_t attr_valid)
{
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.attr.nlink = 1;
out.body.entry.attr_valid = attr_valid;
out.body.entry.attr.size = size;
out.body.entry.entry_valid = entry_valid;
})));
}
void expect_open(uint64_t ino, int times)
{
FuseTest::expect_open(ino, m_direct_io ? FOPEN_DIRECT_IO: 0, times);
}
void expect_release(uint64_t ino, ProcessMockerT r)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_RELEASE &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(r));
}
};
// If the server truncates the file behind the kernel's back, the kernel should
// invalidate cached pages beyond the new EOF
TEST_P(Cache, truncate_by_surprise_invalidates_cache)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const char *CONTENTS = "abcdefghijklmnopqrstuvwxyz";
uint64_t ino = 42;
uint64_t attr_valid, entry_valid;
int fd;
ssize_t bufsize = strlen(CONTENTS);
uint8_t buf[bufsize];
bool reopen = get<0>(get<0>(GetParam()));
bool cache_lookups = get<1>(get<0>(GetParam()));
bool cache_attrs = get<2>(get<0>(GetParam()));
ssize_t osize = get<2>(GetParam());
ssize_t nsize = get<3>(GetParam());
ASSERT_LE(osize, bufsize);
ASSERT_LE(nsize, bufsize);
if (cache_attrs)
attr_valid = UINT64_MAX;
else
attr_valid = 0;
if (cache_lookups)
entry_valid = UINT64_MAX;
else
entry_valid = 0;
expect_lookup(RELPATH, ino, osize, entry_valid, attr_valid);
expect_open(ino, 1);
if (!cache_attrs)
expect_getattr(ino, 2, osize, attr_valid);
expect_read(ino, 0, osize, osize, CONTENTS);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(osize, read(fd, buf, bufsize)) << strerror(errno);
ASSERT_EQ(0, memcmp(buf, CONTENTS, osize));
// Now truncate the file behind the kernel's back. The next read
// should discard cache and fetch from disk again.
if (reopen) {
// Close and reopen the file
expect_flush(ino, 1, ReturnErrno(ENOSYS));
expect_release(ino, ReturnErrno(0));
ASSERT_EQ(0, close(fd));
expect_lookup(RELPATH, ino, nsize, entry_valid, attr_valid);
expect_open(ino, 1);
fd = open(FULLPATH, O_RDONLY);
ASSERT_LE(0, fd) << strerror(errno);
}
if (!cache_attrs)
expect_getattr(ino, 1, nsize, attr_valid);
expect_read(ino, 0, nsize, nsize, CONTENTS);
ASSERT_EQ(0, lseek(fd, 0, SEEK_SET));
ASSERT_EQ(nsize, read(fd, buf, bufsize)) << strerror(errno);
ASSERT_EQ(0, memcmp(buf, CONTENTS, nsize));
leak(fd);
}
INSTANTIATE_TEST_CASE_P(Cache, Cache,
Combine(
/* Test every combination that:
* - does not cache at least one of entries and attrs
* - either doesn't cache attrs, or reopens the file
* In the other combinations, the kernel will never learn that
* the file's size has changed.
*/
Values(
std::make_tuple(false, false, false),
std::make_tuple(false, true, false),
std::make_tuple(true, false, false),
std::make_tuple(true, false, true),
std::make_tuple(true, true, false)
),
Values(Writethrough, Writeback),
/* Test both reductions and extensions to file size */
Values(20),
Values(10, 25)
)
);