a81776c270
* In the fatal_signal test, wait for the daemon to receive FUSE_INTERRUPT before exiting. * Explicitly disable restarting syscalls after SIGUSR2. This fixes intermittency in the priority test. I don't know why, but sometimes that test's mkdir would be restarted, and sometimes it would return EINTR. ERESTART should be the default. * Remove a useless copy/pasted sleep in the priority test. Sponsored by: The FreeBSD Foundation
783 lines
21 KiB
C++
783 lines
21 KiB
C++
/*-
|
|
* SPDX-License-Identifier: BSD-2-Clause-FreeBSD
|
|
*
|
|
* 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/extattr.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/wait.h>
|
|
#include <fcntl.h>
|
|
#include <pthread.h>
|
|
#include <semaphore.h>
|
|
#include <signal.h>
|
|
}
|
|
|
|
#include "mockfs.hh"
|
|
#include "utils.hh"
|
|
|
|
using namespace testing;
|
|
|
|
/* Initial size of files used by these tests */
|
|
const off_t FILESIZE = 1000;
|
|
/* Access mode used by all directories in these tests */
|
|
const mode_t MODE = 0755;
|
|
const char FULLDIRPATH0[] = "mountpoint/some_dir";
|
|
const char RELDIRPATH0[] = "some_dir";
|
|
const char FULLDIRPATH1[] = "mountpoint/other_dir";
|
|
const char RELDIRPATH1[] = "other_dir";
|
|
|
|
static sem_t *blocked_semaphore;
|
|
static sem_t *signaled_semaphore;
|
|
|
|
static bool killer_should_sleep = false;
|
|
|
|
/* Don't do anything; all we care about is that the syscall gets interrupted */
|
|
void sigusr2_handler(int __unused sig) {
|
|
if (verbosity > 1) {
|
|
printf("Signaled! thread %p\n", pthread_self());
|
|
}
|
|
|
|
}
|
|
|
|
void* killer(void* target) {
|
|
/* Wait until the main thread is blocked in fdisp_wait_answ */
|
|
if (killer_should_sleep)
|
|
nap();
|
|
else
|
|
sem_wait(blocked_semaphore);
|
|
if (verbosity > 1)
|
|
printf("Signalling! thread %p\n", target);
|
|
pthread_kill((pthread_t)target, SIGUSR2);
|
|
if (signaled_semaphore != NULL)
|
|
sem_post(signaled_semaphore);
|
|
|
|
return(NULL);
|
|
}
|
|
|
|
class Interrupt: public FuseTest {
|
|
public:
|
|
pthread_t m_child;
|
|
|
|
Interrupt(): m_child(NULL) {};
|
|
|
|
void expect_lookup(const char *relpath, uint64_t ino)
|
|
{
|
|
FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, FILESIZE, 1);
|
|
}
|
|
|
|
/*
|
|
* Expect a FUSE_MKDIR but don't reply. Instead, just record the unique value
|
|
* to the provided pointer
|
|
*/
|
|
void expect_mkdir(uint64_t *mkdir_unique)
|
|
{
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([=](auto in) {
|
|
return (in->header.opcode == FUSE_MKDIR);
|
|
}, Eq(true)),
|
|
_)
|
|
).WillOnce(Invoke([=](auto in, auto &out __unused) {
|
|
*mkdir_unique = in->header.unique;
|
|
sem_post(blocked_semaphore);
|
|
}));
|
|
}
|
|
|
|
/*
|
|
* Expect a FUSE_READ but don't reply. Instead, just record the unique value
|
|
* to the provided pointer
|
|
*/
|
|
void expect_read(uint64_t ino, uint64_t *read_unique)
|
|
{
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([=](auto in) {
|
|
return (in->header.opcode == FUSE_READ &&
|
|
in->header.nodeid == ino);
|
|
}, Eq(true)),
|
|
_)
|
|
).WillOnce(Invoke([=](auto in, auto &out __unused) {
|
|
*read_unique = in->header.unique;
|
|
sem_post(blocked_semaphore);
|
|
}));
|
|
}
|
|
|
|
/*
|
|
* Expect a FUSE_WRITE but don't reply. Instead, just record the unique value
|
|
* to the provided pointer
|
|
*/
|
|
void expect_write(uint64_t ino, uint64_t *write_unique)
|
|
{
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([=](auto in) {
|
|
return (in->header.opcode == FUSE_WRITE &&
|
|
in->header.nodeid == ino);
|
|
}, Eq(true)),
|
|
_)
|
|
).WillOnce(Invoke([=](auto in, auto &out __unused) {
|
|
*write_unique = in->header.unique;
|
|
sem_post(blocked_semaphore);
|
|
}));
|
|
}
|
|
|
|
void setup_interruptor(pthread_t target, bool sleep = false)
|
|
{
|
|
ASSERT_NE(SIG_ERR, signal(SIGUSR2, sigusr2_handler)) << strerror(errno);
|
|
killer_should_sleep = sleep;
|
|
ASSERT_EQ(0, pthread_create(&m_child, NULL, killer, (void*)target))
|
|
<< strerror(errno);
|
|
}
|
|
|
|
void SetUp() {
|
|
const int mprot = PROT_READ | PROT_WRITE;
|
|
const int mflags = MAP_ANON | MAP_SHARED;
|
|
|
|
signaled_semaphore = NULL;
|
|
|
|
blocked_semaphore = (sem_t*)mmap(NULL, sizeof(*blocked_semaphore),
|
|
mprot, mflags, -1, 0);
|
|
ASSERT_NE(MAP_FAILED, blocked_semaphore) << strerror(errno);
|
|
ASSERT_EQ(0, sem_init(blocked_semaphore, 1, 0)) << strerror(errno);
|
|
ASSERT_EQ(0, siginterrupt(SIGUSR2, 1));
|
|
|
|
FuseTest::SetUp();
|
|
}
|
|
|
|
void TearDown() {
|
|
struct sigaction sa;
|
|
|
|
if (m_child != NULL) {
|
|
pthread_join(m_child, NULL);
|
|
}
|
|
bzero(&sa, sizeof(sa));
|
|
sa.sa_handler = SIG_DFL;
|
|
sigaction(SIGUSR2, &sa, NULL);
|
|
|
|
sem_destroy(blocked_semaphore);
|
|
munmap(blocked_semaphore, sizeof(*blocked_semaphore));
|
|
|
|
FuseTest::TearDown();
|
|
}
|
|
};
|
|
|
|
static void* mkdir0(void* arg __unused) {
|
|
ssize_t r;
|
|
|
|
r = mkdir(FULLDIRPATH0, MODE);
|
|
if (r >= 0)
|
|
return 0;
|
|
else
|
|
return (void*)(intptr_t)errno;
|
|
}
|
|
|
|
static void* read1(void* arg) {
|
|
const size_t bufsize = FILESIZE;
|
|
char buf[bufsize];
|
|
int fd = (int)(intptr_t)arg;
|
|
ssize_t r;
|
|
|
|
r = read(fd, buf, bufsize);
|
|
if (r >= 0)
|
|
return 0;
|
|
else
|
|
return (void*)(intptr_t)errno;
|
|
}
|
|
|
|
/*
|
|
* An interrupt operation that gets received after the original command is
|
|
* complete should generate an EAGAIN response.
|
|
*/
|
|
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */
|
|
TEST_F(Interrupt, already_complete)
|
|
{
|
|
uint64_t ino = 42;
|
|
pthread_t self;
|
|
uint64_t mkdir_unique = 0;
|
|
Sequence seq;
|
|
|
|
self = pthread_self();
|
|
|
|
EXPECT_LOOKUP(1, RELDIRPATH0)
|
|
.InSequence(seq)
|
|
.WillOnce(Invoke(ReturnErrno(ENOENT)));
|
|
expect_mkdir(&mkdir_unique);
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([&](auto in) {
|
|
return (in->header.opcode == FUSE_INTERRUPT &&
|
|
in->body.interrupt.unique == mkdir_unique);
|
|
}, Eq(true)),
|
|
_)
|
|
).WillOnce(Invoke([&](auto in, auto &out) {
|
|
// First complete the mkdir request
|
|
auto out0 = new mockfs_buf_out;
|
|
out0->header.unique = mkdir_unique;
|
|
SET_OUT_HEADER_LEN(out0, entry);
|
|
out0->body.create.entry.attr.mode = S_IFDIR | MODE;
|
|
out0->body.create.entry.nodeid = ino;
|
|
out.push_back(out0);
|
|
|
|
// Then, respond EAGAIN to the interrupt request
|
|
auto out1 = new mockfs_buf_out;
|
|
out1->header.unique = in->header.unique;
|
|
out1->header.error = -EAGAIN;
|
|
out1->header.len = sizeof(out1->header);
|
|
out.push_back(out1);
|
|
}));
|
|
EXPECT_LOOKUP(1, RELDIRPATH0)
|
|
.InSequence(seq)
|
|
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) {
|
|
SET_OUT_HEADER_LEN(out, entry);
|
|
out->body.entry.attr.mode = S_IFDIR | MODE;
|
|
out->body.entry.nodeid = ino;
|
|
out->body.entry.attr.nlink = 2;
|
|
})));
|
|
|
|
setup_interruptor(self);
|
|
EXPECT_EQ(0, mkdir(FULLDIRPATH0, MODE)) << strerror(errno);
|
|
/*
|
|
* The final syscall simply ensures that the test's main thread doesn't
|
|
* end before the daemon finishes responding to the FUSE_INTERRUPT.
|
|
*/
|
|
EXPECT_EQ(0, access(FULLDIRPATH0, F_OK)) << strerror(errno);
|
|
}
|
|
|
|
/*
|
|
* If a FUSE file system returns ENOSYS for a FUSE_INTERRUPT operation, the
|
|
* kernel should not attempt to interrupt any other operations on that mount
|
|
* point.
|
|
*/
|
|
TEST_F(Interrupt, enosys)
|
|
{
|
|
uint64_t ino0 = 42, ino1 = 43;;
|
|
uint64_t mkdir_unique;
|
|
pthread_t self, th0;
|
|
sem_t sem0, sem1;
|
|
void *thr0_value;
|
|
Sequence seq;
|
|
|
|
self = pthread_self();
|
|
ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
|
|
ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno);
|
|
|
|
EXPECT_LOOKUP(1, RELDIRPATH1).WillOnce(Invoke(ReturnErrno(ENOENT)));
|
|
EXPECT_LOOKUP(1, RELDIRPATH0).WillOnce(Invoke(ReturnErrno(ENOENT)));
|
|
expect_mkdir(&mkdir_unique);
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([&](auto in) {
|
|
return (in->header.opcode == FUSE_INTERRUPT &&
|
|
in->body.interrupt.unique == mkdir_unique);
|
|
}, Eq(true)),
|
|
_)
|
|
).InSequence(seq)
|
|
.WillOnce(Invoke([&](auto in, auto &out) {
|
|
// reject FUSE_INTERRUPT and respond to the FUSE_MKDIR
|
|
auto out0 = new mockfs_buf_out;
|
|
auto out1 = new mockfs_buf_out;
|
|
|
|
out0->header.unique = in->header.unique;
|
|
out0->header.error = -ENOSYS;
|
|
out0->header.len = sizeof(out0->header);
|
|
out.push_back(out0);
|
|
|
|
SET_OUT_HEADER_LEN(out1, entry);
|
|
out1->body.create.entry.attr.mode = S_IFDIR | MODE;
|
|
out1->body.create.entry.nodeid = ino1;
|
|
out1->header.unique = mkdir_unique;
|
|
out.push_back(out1);
|
|
}));
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([&](auto in) {
|
|
return (in->header.opcode == FUSE_MKDIR);
|
|
}, Eq(true)),
|
|
_)
|
|
).InSequence(seq)
|
|
.WillOnce(Invoke([&](auto in, auto &out) {
|
|
auto out0 = new mockfs_buf_out;
|
|
|
|
sem_post(&sem0);
|
|
sem_wait(&sem1);
|
|
|
|
SET_OUT_HEADER_LEN(out0, entry);
|
|
out0->body.create.entry.attr.mode = S_IFDIR | MODE;
|
|
out0->body.create.entry.nodeid = ino0;
|
|
out0->header.unique = in->header.unique;
|
|
out.push_back(out0);
|
|
}));
|
|
|
|
setup_interruptor(self);
|
|
/* First mkdir operation should finish synchronously */
|
|
ASSERT_EQ(0, mkdir(FULLDIRPATH1, MODE)) << strerror(errno);
|
|
|
|
ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL))
|
|
<< strerror(errno);
|
|
|
|
sem_wait(&sem0);
|
|
/*
|
|
* th0 should be blocked waiting for the fuse daemon thread.
|
|
* Signal it. No FUSE_INTERRUPT should result
|
|
*/
|
|
pthread_kill(th0, SIGUSR1);
|
|
/* Allow the daemon thread to proceed */
|
|
sem_post(&sem1);
|
|
pthread_join(th0, &thr0_value);
|
|
/* Second mkdir should've finished without error */
|
|
EXPECT_EQ(0, (intptr_t)thr0_value);
|
|
}
|
|
|
|
/*
|
|
* Upon receipt of a fatal signal, fusefs should return ASAP after sending
|
|
* FUSE_INTERRUPT.
|
|
*/
|
|
TEST_F(Interrupt, fatal_signal)
|
|
{
|
|
int status;
|
|
pthread_t self;
|
|
uint64_t mkdir_unique;
|
|
sem_t sem;
|
|
|
|
ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
|
|
self = pthread_self();
|
|
|
|
EXPECT_LOOKUP(1, RELDIRPATH0).WillOnce(Invoke(ReturnErrno(ENOENT)));
|
|
expect_mkdir(&mkdir_unique);
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([&](auto in) {
|
|
return (in->header.opcode == FUSE_INTERRUPT &&
|
|
in->body.interrupt.unique == mkdir_unique);
|
|
}, Eq(true)),
|
|
_)
|
|
).WillOnce(Invoke([&](auto in __unused, auto &out __unused) {
|
|
sem_post(&sem);
|
|
/* Don't respond. The process should exit anyway */
|
|
}));
|
|
|
|
fork(false, &status, [&] {
|
|
}, [&]() {
|
|
struct sigaction sa;
|
|
int r;
|
|
pthread_t killer_th;
|
|
pthread_t self;
|
|
|
|
/* SIGUSR2 terminates the process by default */
|
|
bzero(&sa, sizeof(sa));
|
|
sa.sa_handler = SIG_DFL;
|
|
r = sigaction(SIGUSR2, &sa, NULL);
|
|
if (r != 0) {
|
|
perror("sigaction");
|
|
return 1;
|
|
}
|
|
self = pthread_self();
|
|
r = pthread_create(&killer_th, NULL, killer, (void*)self);
|
|
if (r != 0) {
|
|
perror("pthread_create");
|
|
return 1;
|
|
}
|
|
|
|
mkdir(FULLDIRPATH0, MODE);
|
|
return 1;
|
|
});
|
|
ASSERT_EQ(SIGUSR2, WTERMSIG(status));
|
|
|
|
EXPECT_EQ(0, sem_wait(&sem)) << strerror(errno);
|
|
sem_destroy(&sem);
|
|
}
|
|
|
|
/*
|
|
* A FUSE filesystem is legally allowed to ignore INTERRUPT operations, and
|
|
* complete the original operation whenever it damn well pleases.
|
|
*/
|
|
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */
|
|
TEST_F(Interrupt, ignore)
|
|
{
|
|
uint64_t ino = 42;
|
|
pthread_t self;
|
|
uint64_t mkdir_unique;
|
|
|
|
self = pthread_self();
|
|
|
|
EXPECT_LOOKUP(1, RELDIRPATH0).WillOnce(Invoke(ReturnErrno(ENOENT)));
|
|
expect_mkdir(&mkdir_unique);
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([&](auto in) {
|
|
return (in->header.opcode == FUSE_INTERRUPT &&
|
|
in->body.interrupt.unique == mkdir_unique);
|
|
}, Eq(true)),
|
|
_)
|
|
).WillOnce(Invoke([&](auto in __unused, auto &out) {
|
|
// Ignore FUSE_INTERRUPT; respond to the FUSE_MKDIR
|
|
auto out0 = new mockfs_buf_out;
|
|
out0->header.unique = mkdir_unique;
|
|
SET_OUT_HEADER_LEN(out0, entry);
|
|
out0->body.create.entry.attr.mode = S_IFDIR | MODE;
|
|
out0->body.create.entry.nodeid = ino;
|
|
out.push_back(out0);
|
|
}));
|
|
|
|
setup_interruptor(self);
|
|
ASSERT_EQ(0, mkdir(FULLDIRPATH0, MODE)) << strerror(errno);
|
|
}
|
|
|
|
/*
|
|
* A restartable operation (basically, anything except write or setextattr)
|
|
* that hasn't yet been sent to userland can be interrupted without sending
|
|
* FUSE_INTERRUPT, and will be automatically restarted.
|
|
*/
|
|
TEST_F(Interrupt, in_kernel_restartable)
|
|
{
|
|
const char FULLPATH1[] = "mountpoint/other_file.txt";
|
|
const char RELPATH1[] = "other_file.txt";
|
|
uint64_t ino0 = 42, ino1 = 43;
|
|
int fd1;
|
|
pthread_t self, th0, th1;
|
|
sem_t sem0, sem1;
|
|
void *thr0_value, *thr1_value;
|
|
|
|
ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
|
|
ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno);
|
|
self = pthread_self();
|
|
|
|
EXPECT_LOOKUP(1, RELDIRPATH0).WillOnce(Invoke(ReturnErrno(ENOENT)));
|
|
expect_lookup(RELPATH1, ino1);
|
|
expect_open(ino1, 0, 1);
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([=](auto in) {
|
|
return (in->header.opcode == FUSE_MKDIR);
|
|
}, Eq(true)),
|
|
_)
|
|
).WillOnce(Invoke(ReturnImmediate([&](auto in __unused, auto out) {
|
|
/* Let the next write proceed */
|
|
sem_post(&sem1);
|
|
/* Pause the daemon thread so it won't read the next op */
|
|
sem_wait(&sem0);
|
|
|
|
SET_OUT_HEADER_LEN(out, entry);
|
|
out->body.create.entry.attr.mode = S_IFDIR | MODE;
|
|
out->body.create.entry.nodeid = ino0;
|
|
})));
|
|
FuseTest::expect_read(ino1, 0, FILESIZE, 0, NULL);
|
|
|
|
fd1 = open(FULLPATH1, O_RDONLY);
|
|
ASSERT_LE(0, fd1) << strerror(errno);
|
|
|
|
/* Use a separate thread for each operation */
|
|
ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL))
|
|
<< strerror(errno);
|
|
|
|
sem_wait(&sem1); /* Sequence the two operations */
|
|
|
|
ASSERT_EQ(0, pthread_create(&th1, NULL, read1, (void*)(intptr_t)fd1))
|
|
<< strerror(errno);
|
|
|
|
setup_interruptor(self, true);
|
|
|
|
pause(); /* Wait for signal */
|
|
|
|
/* Unstick the daemon */
|
|
ASSERT_EQ(0, sem_post(&sem0)) << strerror(errno);
|
|
|
|
/* Wait awhile to make sure the signal generates no FUSE_INTERRUPT */
|
|
nap();
|
|
|
|
pthread_join(th1, &thr1_value);
|
|
pthread_join(th0, &thr0_value);
|
|
EXPECT_EQ(0, (intptr_t)thr1_value);
|
|
EXPECT_EQ(0, (intptr_t)thr0_value);
|
|
sem_destroy(&sem1);
|
|
sem_destroy(&sem0);
|
|
}
|
|
|
|
/*
|
|
* An operation that hasn't yet been sent to userland can be interrupted
|
|
* without sending FUSE_INTERRUPT. If it's a non-restartable operation (write
|
|
* or setextattr) it will return EINTR.
|
|
*/
|
|
TEST_F(Interrupt, in_kernel_nonrestartable)
|
|
{
|
|
const char FULLPATH1[] = "mountpoint/other_file.txt";
|
|
const char RELPATH1[] = "other_file.txt";
|
|
const char value[] = "whatever";
|
|
ssize_t value_len = strlen(value) + 1;
|
|
uint64_t ino0 = 42, ino1 = 43;
|
|
int ns = EXTATTR_NAMESPACE_USER;
|
|
int fd1;
|
|
pthread_t self, th0;
|
|
sem_t sem0, sem1;
|
|
void *thr0_value;
|
|
ssize_t r;
|
|
|
|
ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
|
|
ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno);
|
|
self = pthread_self();
|
|
|
|
EXPECT_LOOKUP(1, RELDIRPATH0).WillOnce(Invoke(ReturnErrno(ENOENT)));
|
|
expect_lookup(RELPATH1, ino1);
|
|
expect_open(ino1, 0, 1);
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([=](auto in) {
|
|
return (in->header.opcode == FUSE_MKDIR);
|
|
}, Eq(true)),
|
|
_)
|
|
).WillOnce(Invoke(ReturnImmediate([&](auto in __unused, auto out) {
|
|
/* Let the next write proceed */
|
|
sem_post(&sem1);
|
|
/* Pause the daemon thread so it won't read the next op */
|
|
sem_wait(&sem0);
|
|
|
|
SET_OUT_HEADER_LEN(out, entry);
|
|
out->body.create.entry.attr.mode = S_IFDIR | MODE;
|
|
out->body.create.entry.nodeid = ino0;
|
|
})));
|
|
|
|
fd1 = open(FULLPATH1, O_WRONLY);
|
|
ASSERT_LE(0, fd1) << strerror(errno);
|
|
|
|
/* Use a separate thread for the first write */
|
|
ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL))
|
|
<< strerror(errno);
|
|
|
|
sem_wait(&sem1); /* Sequence the two operations */
|
|
|
|
setup_interruptor(self, true);
|
|
|
|
r = extattr_set_fd(fd1, ns, "foo", (void*)value, value_len);
|
|
EXPECT_EQ(EINTR, errno);
|
|
|
|
/* Unstick the daemon */
|
|
ASSERT_EQ(0, sem_post(&sem0)) << strerror(errno);
|
|
|
|
/* Wait awhile to make sure the signal generates no FUSE_INTERRUPT */
|
|
nap();
|
|
|
|
pthread_join(th0, &thr0_value);
|
|
EXPECT_EQ(0, (intptr_t)thr0_value);
|
|
sem_destroy(&sem1);
|
|
sem_destroy(&sem0);
|
|
}
|
|
|
|
/*
|
|
* A syscall that gets interrupted while blocking on FUSE I/O should send a
|
|
* FUSE_INTERRUPT command to the fuse filesystem, which should then send EINTR
|
|
* in response to the _original_ operation. The kernel should ultimately
|
|
* return EINTR to userspace
|
|
*/
|
|
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */
|
|
TEST_F(Interrupt, in_progress)
|
|
{
|
|
pthread_t self;
|
|
uint64_t mkdir_unique;
|
|
|
|
self = pthread_self();
|
|
|
|
EXPECT_LOOKUP(1, RELDIRPATH0).WillOnce(Invoke(ReturnErrno(ENOENT)));
|
|
expect_mkdir(&mkdir_unique);
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([&](auto in) {
|
|
return (in->header.opcode == FUSE_INTERRUPT &&
|
|
in->body.interrupt.unique == mkdir_unique);
|
|
}, Eq(true)),
|
|
_)
|
|
).WillOnce(Invoke([&](auto in __unused, auto &out) {
|
|
auto out0 = new mockfs_buf_out;
|
|
out0->header.error = -EINTR;
|
|
out0->header.unique = mkdir_unique;
|
|
out0->header.len = sizeof(out0->header);
|
|
out.push_back(out0);
|
|
}));
|
|
|
|
setup_interruptor(self);
|
|
ASSERT_EQ(-1, mkdir(FULLDIRPATH0, MODE));
|
|
EXPECT_EQ(EINTR, errno);
|
|
}
|
|
|
|
/* Reads should also be interruptible */
|
|
TEST_F(Interrupt, in_progress_read)
|
|
{
|
|
const char FULLPATH[] = "mountpoint/some_file.txt";
|
|
const char RELPATH[] = "some_file.txt";
|
|
const size_t bufsize = 80;
|
|
char buf[bufsize];
|
|
uint64_t ino = 42;
|
|
int fd;
|
|
pthread_t self;
|
|
uint64_t read_unique;
|
|
|
|
self = pthread_self();
|
|
|
|
expect_lookup(RELPATH, ino);
|
|
expect_open(ino, 0, 1);
|
|
expect_read(ino, &read_unique);
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([&](auto in) {
|
|
return (in->header.opcode == FUSE_INTERRUPT &&
|
|
in->body.interrupt.unique == read_unique);
|
|
}, Eq(true)),
|
|
_)
|
|
).WillOnce(Invoke([&](auto in __unused, auto &out) {
|
|
auto out0 = new mockfs_buf_out;
|
|
out0->header.error = -EINTR;
|
|
out0->header.unique = read_unique;
|
|
out0->header.len = sizeof(out0->header);
|
|
out.push_back(out0);
|
|
}));
|
|
|
|
fd = open(FULLPATH, O_RDONLY);
|
|
ASSERT_LE(0, fd) << strerror(errno);
|
|
|
|
setup_interruptor(self);
|
|
ASSERT_EQ(-1, read(fd, buf, bufsize));
|
|
EXPECT_EQ(EINTR, errno);
|
|
}
|
|
|
|
/* FUSE_INTERRUPT operations should take priority over other pending ops */
|
|
TEST_F(Interrupt, priority)
|
|
{
|
|
Sequence seq;
|
|
uint64_t ino1 = 43;
|
|
uint64_t mkdir_unique;
|
|
pthread_t self, th0;
|
|
sem_t sem0, sem1;
|
|
|
|
ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
|
|
ASSERT_EQ(0, sem_init(&sem1, 0, 0)) << strerror(errno);
|
|
self = pthread_self();
|
|
|
|
EXPECT_LOOKUP(1, RELDIRPATH0).WillOnce(Invoke(ReturnErrno(ENOENT)));
|
|
EXPECT_LOOKUP(1, RELDIRPATH1).WillOnce(Invoke(ReturnErrno(ENOENT)));
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([=](auto in) {
|
|
return (in->header.opcode == FUSE_MKDIR);
|
|
}, Eq(true)),
|
|
_)
|
|
).InSequence(seq)
|
|
.WillOnce(Invoke(ReturnImmediate([&](auto in, auto out) {
|
|
mkdir_unique = in->header.unique;
|
|
|
|
/* Let the next mkdir proceed */
|
|
sem_post(&sem1);
|
|
|
|
/* Pause the daemon thread so it won't read the next op */
|
|
sem_wait(&sem0);
|
|
|
|
/* Finally, interrupt the original op */
|
|
out->header.error = -EINTR;
|
|
out->header.unique = mkdir_unique;
|
|
out->header.len = sizeof(out->header);
|
|
})));
|
|
/*
|
|
* FUSE_INTERRUPT should be received before the second FUSE_MKDIR,
|
|
* even though it was generated later
|
|
*/
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([&](auto in) {
|
|
return (in->header.opcode == FUSE_INTERRUPT &&
|
|
in->body.interrupt.unique == mkdir_unique);
|
|
}, Eq(true)),
|
|
_)
|
|
).InSequence(seq)
|
|
.WillOnce(Invoke(ReturnErrno(EAGAIN)));
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([&](auto in) {
|
|
return (in->header.opcode == FUSE_MKDIR);
|
|
}, Eq(true)),
|
|
_)
|
|
).InSequence(seq)
|
|
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) {
|
|
SET_OUT_HEADER_LEN(out, entry);
|
|
out->body.create.entry.attr.mode = S_IFDIR | MODE;
|
|
out->body.create.entry.nodeid = ino1;
|
|
})));
|
|
|
|
/* Use a separate thread for the first mkdir */
|
|
ASSERT_EQ(0, pthread_create(&th0, NULL, mkdir0, NULL))
|
|
<< strerror(errno);
|
|
|
|
signaled_semaphore = &sem0;
|
|
|
|
sem_wait(&sem1); /* Sequence the two mkdirs */
|
|
setup_interruptor(th0, true);
|
|
ASSERT_EQ(0, mkdir(FULLDIRPATH1, MODE)) << strerror(errno);
|
|
|
|
pthread_join(th0, NULL);
|
|
sem_destroy(&sem1);
|
|
sem_destroy(&sem0);
|
|
}
|
|
|
|
/*
|
|
* If the FUSE filesystem receives the FUSE_INTERRUPT operation before
|
|
* processing the original, then it should wait for "some timeout" for the
|
|
* original operation to arrive. If not, it should send EAGAIN to the
|
|
* INTERRUPT operation, and the kernel should requeue the INTERRUPT.
|
|
*
|
|
* In this test, we'll pretend that the INTERRUPT arrives too soon, gets
|
|
* EAGAINed, then the kernel requeues it, and the second time around it
|
|
* successfully interrupts the original
|
|
*/
|
|
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236530 */
|
|
TEST_F(Interrupt, too_soon)
|
|
{
|
|
Sequence seq;
|
|
pthread_t self;
|
|
uint64_t mkdir_unique;
|
|
|
|
self = pthread_self();
|
|
|
|
EXPECT_LOOKUP(1, RELDIRPATH0).WillOnce(Invoke(ReturnErrno(ENOENT)));
|
|
expect_mkdir(&mkdir_unique);
|
|
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([&](auto in) {
|
|
return (in->header.opcode == FUSE_INTERRUPT &&
|
|
in->body.interrupt.unique == mkdir_unique);
|
|
}, Eq(true)),
|
|
_)
|
|
).InSequence(seq)
|
|
.WillOnce(Invoke(ReturnErrno(EAGAIN)));
|
|
|
|
EXPECT_CALL(*m_mock, process(
|
|
ResultOf([&](auto in) {
|
|
return (in->header.opcode == FUSE_INTERRUPT &&
|
|
in->body.interrupt.unique == mkdir_unique);
|
|
}, Eq(true)),
|
|
_)
|
|
).InSequence(seq)
|
|
.WillOnce(Invoke([&](auto in __unused, auto &out __unused) {
|
|
auto out0 = new mockfs_buf_out;
|
|
out0->header.error = -EINTR;
|
|
out0->header.unique = mkdir_unique;
|
|
out0->header.len = sizeof(out0->header);
|
|
out.push_back(out0);
|
|
}));
|
|
|
|
setup_interruptor(self);
|
|
ASSERT_EQ(-1, mkdir(FULLDIRPATH0, MODE));
|
|
EXPECT_EQ(EINTR, errno);
|
|
}
|
|
|
|
|
|
// TODO: add a test where write returns EWOULDBLOCK
|