freebsd-dev/tests/sys/fs/fusefs/last_local_modify.cc
Alan Somers 4ac4b12699 fusefs: annotate more file descriptor leaks in the tests
The fusefs tests intentionally leak file descriptors.  Annotate all of
the leakages in order to hopefully pacify Coverity.

Reported by:	Coverity (20 different CIDs)
MFC after:	2 weeks
Sponsored by:	Axcient
2022-05-05 09:06:04 -06:00

517 lines
14 KiB
C++

/*-
* SPDX-License-Identifier: BSD-2-Clause-FreeBSD
*
* Copyright (c) 2021 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 <sys/mount.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>
#include <semaphore.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
/*
* "Last Local Modify" bugs
*
* This file tests a class of race conditions caused by one thread fetching a
* file's size with FUSE_LOOKUP while another thread simultaneously modifies it
* with FUSE_SETATTR, FUSE_WRITE, FUSE_COPY_FILE_RANGE or similar. It's
* possible for the second thread to start later yet finish first. If that
* happens, the first thread must not override the size set by the second
* thread.
*
* FUSE_GETATTR is not vulnerable to the same race, because it is always called
* with the vnode lock held.
*
* A few other operations like FUSE_LINK can also trigger the same race but
* with the file's ctime instead of size. However, the consequences of an
* incorrect ctime are much less disastrous than an incorrect size, so fusefs
* does not attempt to prevent such races.
*/
enum Mutator {
VOP_ALLOCATE,
VOP_COPY_FILE_RANGE,
VOP_SETATTR,
VOP_WRITE,
};
/*
* Translate a poll method's string representation to the enum value.
* Using strings with ::testing::Values gives better output with
* --gtest_list_tests
*/
enum Mutator writer_from_str(const char* s) {
if (0 == strcmp("VOP_ALLOCATE", s))
return VOP_ALLOCATE;
else if (0 == strcmp("VOP_COPY_FILE_RANGE", s))
return VOP_COPY_FILE_RANGE;
else if (0 == strcmp("VOP_SETATTR", s))
return VOP_SETATTR;
else
return VOP_WRITE;
}
uint32_t fuse_op_from_mutator(enum Mutator mutator) {
switch(mutator) {
case VOP_ALLOCATE:
return(FUSE_FALLOCATE);
case VOP_COPY_FILE_RANGE:
return(FUSE_COPY_FILE_RANGE);
case VOP_SETATTR:
return(FUSE_SETATTR);
case VOP_WRITE:
return(FUSE_WRITE);
}
}
class LastLocalModify: public FuseTest, public WithParamInterface<const char*> {
public:
virtual void SetUp() {
m_init_flags = FUSE_EXPORT_SUPPORT;
FuseTest::SetUp();
}
};
static void* allocate_th(void* arg) {
int fd;
ssize_t r;
sem_t *sem = (sem_t*) arg;
if (sem)
sem_wait(sem);
fd = open("mountpoint/some_file.txt", O_RDWR);
if (fd < 0)
return (void*)(intptr_t)errno;
r = posix_fallocate(fd, 0, 15);
LastLocalModify::leak(fd);
if (r >= 0)
return 0;
else
return (void*)(intptr_t)errno;
}
static void* copy_file_range_th(void* arg) {
ssize_t r;
int fd;
sem_t *sem = (sem_t*) arg;
off_t off_in = 0;
off_t off_out = 10;
ssize_t len = 5;
if (sem)
sem_wait(sem);
fd = open("mountpoint/some_file.txt", O_RDWR);
if (fd < 0)
return (void*)(intptr_t)errno;
r = copy_file_range(fd, &off_in, fd, &off_out, len, 0);
if (r >= 0) {
LastLocalModify::leak(fd);
return 0;
} else
return (void*)(intptr_t)errno;
}
static void* setattr_th(void* arg) {
int fd;
ssize_t r;
sem_t *sem = (sem_t*) arg;
if (sem)
sem_wait(sem);
fd = open("mountpoint/some_file.txt", O_RDWR);
if (fd < 0)
return (void*)(intptr_t)errno;
r = ftruncate(fd, 15);
LastLocalModify::leak(fd);
if (r >= 0)
return 0;
else
return (void*)(intptr_t)errno;
}
static void* write_th(void* arg) {
ssize_t r;
int fd;
sem_t *sem = (sem_t*) arg;
const char BUF[] = "abcdefghijklmn";
if (sem)
sem_wait(sem);
fd = open("mountpoint/some_file.txt", O_RDWR);
if (fd < 0)
return (void*)(intptr_t)errno;
r = write(fd, BUF, sizeof(BUF));
if (r >= 0) {
LastLocalModify::leak(fd);
return 0;
} else
return (void*)(intptr_t)errno;
}
/*
* VOP_LOOKUP should discard attributes returned by the server if they were
* modified by another VOP while the VOP_LOOKUP was in progress.
*
* Sequence of operations:
* * Thread 1 calls a mutator like ftruncate, which acquires the vnode lock
* exclusively.
* * Thread 2 calls stat, which does VOP_LOOKUP, which sends FUSE_LOOKUP to the
* server. The server replies with the old file length. Thread 2 blocks
* waiting for the vnode lock.
* * Thread 1 sends the mutator operation like FUSE_SETATTR that changes the
* file's size and updates the attribute cache. Then it releases the vnode
* lock.
* * Thread 2 acquires the vnode lock. At this point it must not add the
* now-stale file size to the attribute cache.
*
* Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071
*/
TEST_P(LastLocalModify, lookup)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
Sequence seq;
uint64_t ino = 3;
uint64_t mutator_unique;
const uint64_t oldsize = 10;
const uint64_t newsize = 15;
pthread_t th0;
void *thr0_value;
struct stat sb;
static sem_t sem;
Mutator mutator;
uint32_t mutator_op;
size_t mutator_size;
mutator = writer_from_str(GetParam());
mutator_op = fuse_op_from_mutator(mutator);
ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.InSequence(seq)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
/* Called by the mutator, caches attributes but not entries */
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.nodeid = ino;
out.body.entry.attr.size = oldsize;
out.body.entry.nodeid = ino;
out.body.entry.attr_valid_nsec = NAP_NS / 2;
out.body.entry.attr.ino = ino;
out.body.entry.attr.mode = S_IFREG | 0644;
})));
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == mutator_op &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).InSequence(seq)
.WillOnce(Invoke([&](auto in, auto &out __unused) {
/*
* The mutator changes the file size, but in order to simulate
* a race, don't reply. Instead, just save the unique for
* later.
*/
mutator_unique = in.header.unique;
switch(mutator) {
case VOP_WRITE:
mutator_size = in.body.write.size;
break;
case VOP_COPY_FILE_RANGE:
mutator_size = in.body.copy_file_range.len;
break;
default:
break;
}
/* Allow the lookup thread to proceed */
sem_post(&sem);
}));
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.InSequence(seq)
.WillOnce(Invoke([&](auto in __unused, auto& out) {
std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
/* First complete the lookup request, returning the old size */
out0->header.unique = in.header.unique;
SET_OUT_HEADER_LEN(*out0, entry);
out0->body.entry.attr.mode = S_IFREG | 0644;
out0->body.entry.nodeid = ino;
out0->body.entry.entry_valid = UINT64_MAX;
out0->body.entry.attr_valid = UINT64_MAX;
out0->body.entry.attr.size = oldsize;
out.push_back(std::move(out0));
/* Then, respond to the mutator request */
out1->header.unique = mutator_unique;
switch(mutator) {
case VOP_ALLOCATE:
out1->header.error = 0;
out1->header.len = sizeof(out1->header);
break;
case VOP_COPY_FILE_RANGE:
SET_OUT_HEADER_LEN(*out1, write);
out1->body.write.size = mutator_size;
break;
case VOP_SETATTR:
SET_OUT_HEADER_LEN(*out1, attr);
out1->body.attr.attr.ino = ino;
out1->body.attr.attr.mode = S_IFREG | 0644;
out1->body.attr.attr.size = newsize; // Changed size
out1->body.attr.attr_valid = UINT64_MAX;
break;
case VOP_WRITE:
SET_OUT_HEADER_LEN(*out1, write);
out1->body.write.size = mutator_size;
break;
}
out.push_back(std::move(out1));
}));
/* Start the mutator thread */
switch(mutator) {
case VOP_ALLOCATE:
ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th,
NULL)) << strerror(errno);
break;
case VOP_COPY_FILE_RANGE:
ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th,
NULL)) << strerror(errno);
break;
case VOP_SETATTR:
ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th, NULL))
<< strerror(errno);
break;
case VOP_WRITE:
ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, NULL))
<< strerror(errno);
break;
}
/* Wait for FUSE_SETATTR to be sent */
sem_wait(&sem);
/* Lookup again, which will race with setattr */
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
ASSERT_EQ((off_t)newsize, sb.st_size);
/* ftruncate should've completed without error */
pthread_join(th0, &thr0_value);
EXPECT_EQ(0, (intptr_t)thr0_value);
}
/*
* VFS_VGET should discard attributes returned by the server if they were
* modified by another VOP while the VFS_VGET was in progress.
*
* Sequence of operations:
* * Thread 1 calls fhstat, entering VFS_VGET, and issues FUSE_LOOKUP
* * Thread 2 calls a mutator like ftruncate, which acquires the vnode lock
* exclusively and issues a FUSE op like FUSE_SETATTR.
* * Thread 1's FUSE_LOOKUP returns with the old size, but the thread blocks
* waiting for the vnode lock.
* * Thread 2's FUSE op returns, and that thread sets the file's new size
* in the attribute cache. Finally it releases the vnode lock.
* * The vnode lock acquired, thread 1 must not overwrite the attr cache's size
* with the old value.
*
* Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071
*/
TEST_P(LastLocalModify, vfs_vget)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
Sequence seq;
uint64_t ino = 3;
uint64_t lookup_unique;
const uint64_t oldsize = 10;
const uint64_t newsize = 15;
pthread_t th0;
void *thr0_value;
struct stat sb;
static sem_t sem;
fhandle_t fhp;
Mutator mutator;
uint32_t mutator_op;
if (geteuid() != 0)
GTEST_SKIP() << "This test requires a privileged user";
mutator = writer_from_str(GetParam());
mutator_op = fuse_op_from_mutator(mutator);
ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.Times(1)
.InSequence(seq)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
{
/* Called by getfh, caches attributes but not entries */
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.nodeid = ino;
out.body.entry.attr.size = oldsize;
out.body.entry.nodeid = ino;
out.body.entry.attr_valid_nsec = NAP_NS / 2;
out.body.entry.attr.ino = ino;
out.body.entry.attr.mode = S_IFREG | 0644;
})));
EXPECT_LOOKUP(ino, ".")
.InSequence(seq)
.WillOnce(Invoke([&](auto in, auto &out __unused) {
/* Called by fhstat. Block to simulate a race */
lookup_unique = in.header.unique;
sem_post(&sem);
}));
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.Times(1)
.InSequence(seq)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
{
/* Called by VOP_SETATTR, caches attributes but not entries */
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.nodeid = ino;
out.body.entry.attr.size = oldsize;
out.body.entry.nodeid = ino;
out.body.entry.attr_valid_nsec = NAP_NS / 2;
out.body.entry.attr.ino = ino;
out.body.entry.attr.mode = S_IFREG | 0644;
})));
/* Called by the mutator thread */
expect_open(ino, 0, 1);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == mutator_op &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).InSequence(seq)
.WillOnce(Invoke([&](auto in __unused, auto& out) {
std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
/* First complete the lookup request, returning the old size */
out0->header.unique = lookup_unique;
SET_OUT_HEADER_LEN(*out0, entry);
out0->body.entry.attr.mode = S_IFREG | 0644;
out0->body.entry.nodeid = ino;
out0->body.entry.entry_valid = UINT64_MAX;
out0->body.entry.attr_valid = UINT64_MAX;
out0->body.entry.attr.size = oldsize;
out.push_back(std::move(out0));
/* Then, respond to the mutator request */
out1->header.unique = in.header.unique;
switch(mutator) {
case VOP_ALLOCATE:
out1->header.error = 0;
out1->header.len = sizeof(out1->header);
break;
case VOP_COPY_FILE_RANGE:
SET_OUT_HEADER_LEN(*out1, write);
out1->body.write.size = in.body.copy_file_range.len;
break;
case VOP_SETATTR:
SET_OUT_HEADER_LEN(*out1, attr);
out1->body.attr.attr.ino = ino;
out1->body.attr.attr.mode = S_IFREG | 0644;
out1->body.attr.attr.size = newsize; // Changed size
out1->body.attr.attr_valid = UINT64_MAX;
break;
case VOP_WRITE:
SET_OUT_HEADER_LEN(*out1, write);
out1->body.write.size = in.body.write.size;
break;
}
out.push_back(std::move(out1));
}));
/* First get a file handle */
ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
/* Start the mutator thread */
switch(mutator) {
case VOP_ALLOCATE:
ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th,
(void*)&sem)) << strerror(errno);
break;
case VOP_COPY_FILE_RANGE:
ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th,
(void*)&sem)) << strerror(errno);
break;
case VOP_SETATTR:
ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th,
(void*)&sem)) << strerror(errno);
break;
case VOP_WRITE:
ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, (void*)&sem))
<< strerror(errno);
break;
}
/* Lookup again, which will race with setattr */
ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
ASSERT_EQ((off_t)newsize, sb.st_size);
/* mutator should've completed without error */
pthread_join(th0, &thr0_value);
EXPECT_EQ(0, (intptr_t)thr0_value);
}
INSTANTIATE_TEST_CASE_P(LLM, LastLocalModify,
Values(
"VOP_ALLOCATE",
"VOP_COPY_FILE_RANGE",
"VOP_SETATTR",
"VOP_WRITE"
)
);