fusefs: add a test case for the allow_other mount option

Also, fix one of the default_permissions test cases.  I forgot the
expectation for FUSE_ACCESS, because that doesn't work right now.

Sponsored by:	The FreeBSD Foundation
This commit is contained in:
Alan Somers 2019-03-21 19:56:33 +00:00
parent 9821f1d323
commit 91ff3a0d3d
Notes: svn2git 2020-12-20 02:59:44 +00:00
svn path=/projects/fuse2/; revision=345383
8 changed files with 245 additions and 27 deletions

View File

@ -8,6 +8,7 @@ TESTSDIR= ${TESTSBASE}/sys/fs/fusefs
# Kyua treats googletest programs as plain tests, it's better to separate them
# out, so we get more granular reporting.
GTESTS+= access
GTESTS+= allow_other
GTESTS+= create
GTESTS+= default_permissions
GTESTS+= destroy
@ -42,6 +43,11 @@ SRCS.access+= getmntopts.c
SRCS.access+= mockfs.cc
SRCS.access+= utils.cc
SRCS.allow_other+= allow_other.cc
SRCS.allow_other+= getmntopts.c
SRCS.allow_other+= mockfs.cc
SRCS.allow_other+= utils.cc
SRCS.create+= create.cc
SRCS.create+= getmntopts.c
SRCS.create+= mockfs.cc

View File

@ -40,18 +40,6 @@ using namespace testing;
class Access: public FuseTest {
public:
void expect_access(uint64_t ino, mode_t access_mode, int error)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in->header.opcode == FUSE_ACCESS &&
in->header.nodeid == ino &&
in->body.access.mask == access_mode);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(error)));
}
void expect_lookup(const char *relpath, uint64_t ino)
{
FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1);

View File

@ -0,0 +1,191 @@
/*-
* 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.
*/
/*
* Tests for the "allow_other" mount option. They must be in their own
* file so they can be run as root
*/
extern "C" {
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <pwd.h>
#include <signal.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
void sighandler(int __unused sig) {}
static void
get_unprivileged_uid(int *uid)
{
struct passwd *pw;
/*
* First try "tests", Kyua's default unprivileged user. XXX after
* GoogleTest gains a proper Kyua wrapper, get this with the Kyua API
*/
pw = getpwnam("tests");
if (pw == NULL) {
/* Fall back to "nobody" */
pw = getpwnam("nobody");
}
if (pw == NULL)
GTEST_SKIP() << "Test requires an unprivileged user";
*uid = pw->pw_uid;
}
class NoAllowOther: public FuseTest {
public:
/* Unprivileged user id */
int m_uid;
virtual void SetUp() {
if (geteuid() != 0) {
GTEST_SKIP() << "This test must be run as root";
}
get_unprivileged_uid(&m_uid);
if (IsSkipped())
return;
FuseTest::SetUp();
}
};
class AllowOther: public NoAllowOther {
public:
virtual void SetUp() {
m_allow_other = true;
NoAllowOther::SetUp();
}
};
TEST_F(AllowOther, allowed)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
pid_t child;
signal(SIGUSR2, sighandler);
if ((child = fork()) == 0) {
/* In child */
pause();
/* Drop privileges before accessing */
if (0 != setreuid(-1, m_uid)) {
perror("setreuid");
_exit(1);
}
fd = open(FULLPATH, O_RDONLY);
if (fd < 0) {
perror("open");
_exit(1);
}
_exit(0);
/* Deliberately leak fd */
} else if (child > 0) {
/*
* In parent. Cleanup must happen here, because it's still
* privileged.
*/
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
expect_open(ino, 0, 1);
expect_release(ino, 1, 0, 0);
/* Until the attr cache is working, we may send an additional
* GETATTR */
expect_getattr(ino, 0);
m_mock->m_child_pid = child;
/* Signal the child process to go */
kill(child, SIGUSR2);
int child_status;
wait(&child_status);
ASSERT_EQ(0, WEXITSTATUS(child_status));
} else {
FAIL() << strerror(errno);
}
}
TEST_F(NoAllowOther, disallowed)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
int fd;
pid_t child;
signal(SIGUSR2, sighandler);
if ((child = fork()) == 0) {
/* In child */
pause();
/* Drop privileges before accessing */
if (0 != setreuid(-1, m_uid)) {
perror("setreuid");
_exit(1);
}
fd = open(FULLPATH, O_RDONLY);
if (fd >= 0) {
fprintf(stderr, "open should've failed\n");
_exit(1);
} else if (errno != EPERM) {
fprintf(stderr,
"Unexpected error: %s\n", strerror(errno));
_exit(1);
}
_exit(0);
/* Deliberately leak fd */
} else if (child > 0) {
/*
* In parent. Cleanup must happen here, because it's still
* privileged.
*/
m_mock->m_child_pid = child;
/* Signal the child process to go */
kill(child, SIGUSR2);
int child_status;
wait(&child_status);
ASSERT_EQ(0, WEXITSTATUS(child_status));
} else {
FAIL() << strerror(errno);
}
}

View File

@ -84,7 +84,8 @@ TEST_F(Access, DISABLED_eaccess)
ASSERT_EQ(EACCES, errno);
}
TEST_F(Access, ok)
/* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=236291 */
TEST_F(Access, DISABLED_ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
@ -92,10 +93,10 @@ TEST_F(Access, ok)
mode_t access_mode = R_OK;
expect_lookup(RELPATH, ino, S_IFREG | 0644);
expect_access(ino, access_mode, 0);
/*
* Once default_permissions is properly implemented, there might be
* another FUSE_GETATTR or something in here. But there should not be
* a FUSE_ACCESS
* another FUSE_GETATTR or something in here.
*/
ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);

View File

@ -243,12 +243,13 @@ void debug_fuseop(const mockfs_buf_in *in)
printf("\n");
}
MockFS::MockFS(int max_readahead, bool push_symlinks_in,
bool default_permissions, uint32_t flags)
MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions,
bool push_symlinks_in, uint32_t flags)
{
struct iovec *iov = NULL;
int iovlen = 0;
char fdstr[15];
const bool trueval = true;
m_daemon_id = NULL;
m_maxreadahead = max_readahead;
@ -262,33 +263,36 @@ MockFS::MockFS(int max_readahead, bool push_symlinks_in,
* googletest doesn't allow ASSERT_ in constructors, so we must throw
* instead.
*/
if (mkdir("mountpoint" , 0644) && errno != EEXIST)
if (mkdir("mountpoint" , 0755) && errno != EEXIST)
throw(std::system_error(errno, std::system_category(),
"Couldn't make mountpoint directory"));
m_fuse_fd = open("/dev/fuse", O_RDWR);
m_fuse_fd = open("/dev/fuse", O_CLOEXEC | O_RDWR);
if (m_fuse_fd < 0)
throw(std::system_error(errno, std::system_category(),
"Couldn't open /dev/fuse"));
sprintf(fdstr, "%d", m_fuse_fd);
m_pid = getpid();
m_child_pid = -1;
build_iovec(&iov, &iovlen, "fstype", __DECONST(void *, "fusefs"), -1);
build_iovec(&iov, &iovlen, "fspath",
__DECONST(void *, "mountpoint"), -1);
build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1);
build_iovec(&iov, &iovlen, "fd", fdstr, -1);
if (push_symlinks_in) {
const bool trueval = true;
build_iovec(&iov, &iovlen, "push_symlinks_in",
if (allow_other) {
build_iovec(&iov, &iovlen, "allow_other",
__DECONST(void*, &trueval), sizeof(bool));
}
if (default_permissions) {
const bool trueval = true;
build_iovec(&iov, &iovlen, "default_permissions",
__DECONST(void*, &trueval), sizeof(bool));
}
if (push_symlinks_in) {
build_iovec(&iov, &iovlen, "push_symlinks_in",
__DECONST(void*, &trueval), sizeof(bool));
}
if (nmount(iov, iovlen, 0))
throw(std::system_error(errno, std::system_category(),
"Couldn't mount filesystem"));
@ -397,6 +401,8 @@ void MockFS::loop() {
bool MockFS::pid_ok(pid_t pid) {
if (pid == m_pid) {
return (true);
} else if (pid == m_child_pid) {
return (true);
} else {
struct kinfo_proc *ki;
bool ok = false;

View File

@ -208,12 +208,16 @@ class MockFS {
void read_request(mockfs_buf_in*);
public:
/* pid of child process, for two-process test cases */
pid_t m_child_pid;
/* Maximum size of a FUSE_WRITE write */
uint32_t m_max_write;
/* Create a new mockfs and mount it to a tempdir */
MockFS(int max_readahead, bool push_symlinks_in,
bool default_permissions, uint32_t flags);
MockFS(int max_readahead, bool allow_other,
bool default_permissions, bool push_symlinks_in,
uint32_t flags);
virtual ~MockFS();
/* Kill the filesystem daemon without unmounting the filesystem */

View File

@ -91,13 +91,27 @@ void FuseTest::SetUp() {
m_maxbcachebuf = val;
try {
m_mock = new MockFS(m_maxreadahead, m_push_symlinks_in,
m_default_permissions, m_init_flags);
m_mock = new MockFS(m_maxreadahead, m_allow_other,
m_default_permissions, m_push_symlinks_in,
m_init_flags);
} catch (std::system_error err) {
FAIL() << err.what();
}
}
void
FuseTest::expect_access(uint64_t ino, mode_t access_mode, int error)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in->header.opcode == FUSE_ACCESS &&
in->header.nodeid == ino &&
in->body.access.mask == access_mode);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(error)));
}
void FuseTest::expect_getattr(uint64_t ino, uint64_t size)
{
/* Until the attr cache is working, we may send an additional GETATTR */

View File

@ -41,6 +41,7 @@ class FuseTest : public ::testing::Test {
protected:
uint32_t m_maxreadahead;
uint32_t m_init_flags;
bool m_allow_other;
bool m_default_permissions;
bool m_push_symlinks_in;
MockFS *m_mock = NULL;
@ -56,6 +57,7 @@ class FuseTest : public ::testing::Test {
*/
m_maxreadahead(UINT_MAX),
m_init_flags(0),
m_allow_other(false),
m_default_permissions(false),
m_push_symlinks_in(false)
{}
@ -67,6 +69,12 @@ class FuseTest : public ::testing::Test {
delete m_mock;
}
/*
* Create an expectation that FUSE_ACCESS will be called oncde for the
* given inode with the given access_mode, returning the given errno
*/
void expect_access(uint64_t ino, mode_t access_mode, int error);
/*
* Create an expectation that FUSE_GETATTR will be called for the given
* inode any number of times. It will respond with a few basic