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:
parent
9821f1d323
commit
91ff3a0d3d
Notes:
svn2git
2020-12-20 02:59:44 +00:00
svn path=/projects/fuse2/; revision=345383
@ -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
|
||||
|
@ -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);
|
||||
|
191
tests/sys/fs/fusefs/allow_other.cc
Normal file
191
tests/sys/fs/fusefs/allow_other.cc
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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 */
|
||||
|
@ -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 */
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user