From 4722be1f747db278693dcc1960b63e400bd5a815 Mon Sep 17 00:00:00 2001 From: asomers Date: Thu, 21 Mar 2019 19:56:33 +0000 Subject: [PATCH] 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 --- tests/sys/fs/fusefs/Makefile | 6 + tests/sys/fs/fusefs/access.cc | 12 -- tests/sys/fs/fusefs/allow_other.cc | 191 +++++++++++++++++++++ tests/sys/fs/fusefs/default_permissions.cc | 7 +- tests/sys/fs/fusefs/mockfs.cc | 22 ++- tests/sys/fs/fusefs/mockfs.hh | 8 +- tests/sys/fs/fusefs/utils.cc | 18 +- tests/sys/fs/fusefs/utils.hh | 8 + 8 files changed, 245 insertions(+), 27 deletions(-) create mode 100644 tests/sys/fs/fusefs/allow_other.cc diff --git a/tests/sys/fs/fusefs/Makefile b/tests/sys/fs/fusefs/Makefile index d70e7a1a95a..cf258cd1b1f 100644 --- a/tests/sys/fs/fusefs/Makefile +++ b/tests/sys/fs/fusefs/Makefile @@ -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 diff --git a/tests/sys/fs/fusefs/access.cc b/tests/sys/fs/fusefs/access.cc index 094809be646..016d9bcd8d8 100644 --- a/tests/sys/fs/fusefs/access.cc +++ b/tests/sys/fs/fusefs/access.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); diff --git a/tests/sys/fs/fusefs/allow_other.cc b/tests/sys/fs/fusefs/allow_other.cc new file mode 100644 index 00000000000..c39cc2e4cb8 --- /dev/null +++ b/tests/sys/fs/fusefs/allow_other.cc @@ -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 +#include +#include +#include +#include +} + +#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); + } +} diff --git a/tests/sys/fs/fusefs/default_permissions.cc b/tests/sys/fs/fusefs/default_permissions.cc index 9790f75601b..c5b69632da0 100644 --- a/tests/sys/fs/fusefs/default_permissions.cc +++ b/tests/sys/fs/fusefs/default_permissions.cc @@ -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); diff --git a/tests/sys/fs/fusefs/mockfs.cc b/tests/sys/fs/fusefs/mockfs.cc index 7812c6dd225..d010f5a870b 100644 --- a/tests/sys/fs/fusefs/mockfs.cc +++ b/tests/sys/fs/fusefs/mockfs.cc @@ -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; diff --git a/tests/sys/fs/fusefs/mockfs.hh b/tests/sys/fs/fusefs/mockfs.hh index d9eb5888ae3..ac4c9a030e3 100644 --- a/tests/sys/fs/fusefs/mockfs.hh +++ b/tests/sys/fs/fusefs/mockfs.hh @@ -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 */ diff --git a/tests/sys/fs/fusefs/utils.cc b/tests/sys/fs/fusefs/utils.cc index 45756243d89..3ca9bd7d6e0 100644 --- a/tests/sys/fs/fusefs/utils.cc +++ b/tests/sys/fs/fusefs/utils.cc @@ -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 */ diff --git a/tests/sys/fs/fusefs/utils.hh b/tests/sys/fs/fusefs/utils.hh index 7791c4e0ef2..452c063e186 100644 --- a/tests/sys/fs/fusefs/utils.hh +++ b/tests/sys/fs/fusefs/utils.hh @@ -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 -- 2.45.0