freebsd-skq/tests/sys/fs/fusefs/default_permissions.cc
asomers a04ba40349 fusefs: annotate deliberate file descriptor leaks in the tests
closing a file descriptor causes FUSE activity that is superfluous to the
purpose of most tests, but would nonetheless require matching expectations.
Rather than do that, most tests deliberately leak file descriptors instead.
This commit moves the leakage from each test into two trivial functions:
leak and leakdir.  Hopefully Coverity will only complain about those
functions and not all of their callers.

Sponsored by:	The FreeBSD Foundation
2019-06-26 20:25:57 +00:00

1304 lines
37 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.
*/
/*
* Tests for the "default_permissions" mount option. They must be in their own
* file so they can be run as an unprivileged user
*/
extern "C" {
#include <sys/types.h>
#include <sys/extattr.h>
#include <fcntl.h>
#include <unistd.h>
}
#include "mockfs.hh"
#include "utils.hh"
using namespace testing;
class DefaultPermissions: public FuseTest {
virtual void SetUp() {
m_default_permissions = true;
FuseTest::SetUp();
if (HasFatalFailure() || IsSkipped())
return;
if (geteuid() == 0) {
GTEST_SKIP() << "This test requires an unprivileged user";
}
/* With -o default_permissions, FUSE_ACCESS should never be called */
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_ACCESS);
}, Eq(true)),
_)
).Times(0);
}
public:
void expect_chmod(uint64_t ino, mode_t mode, uint64_t size = 0)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
in.body.setattr.valid == FATTR_MODE &&
in.body.setattr.mode == mode);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | mode;
out.body.attr.attr.size = size;
out.body.attr.attr_valid = UINT64_MAX;
})));
}
void expect_create(const char *relpath, uint64_t ino)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
const char *name = (const char*)in.body.bytes +
sizeof(fuse_create_in);
return (in.header.opcode == FUSE_CREATE &&
(0 == strcmp(relpath, name)));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, create);
out.body.create.entry.attr.mode = S_IFREG | 0644;
out.body.create.entry.nodeid = ino;
out.body.create.entry.entry_valid = UINT64_MAX;
out.body.create.entry.attr_valid = UINT64_MAX;
})));
}
void expect_getattr(uint64_t ino, mode_t mode, uint64_t attr_valid, int times,
uid_t uid = 0, gid_t gid = 0)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).Times(times)
.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = mode;
out.body.attr.attr.size = 0;
out.body.attr.attr.uid = uid;
out.body.attr.attr.uid = gid;
out.body.attr.attr_valid = attr_valid;
})));
}
void expect_lookup(const char *relpath, uint64_t ino, mode_t mode,
uint64_t attr_valid, uid_t uid = 0, gid_t gid = 0)
{
FuseTest::expect_lookup(relpath, ino, mode, 0, 1, attr_valid, uid, gid);
}
};
class Access: public DefaultPermissions {};
class Chown: public DefaultPermissions {};
class Chgrp: public DefaultPermissions {};
class Lookup: public DefaultPermissions {};
class Open: public DefaultPermissions {};
class Setattr: public DefaultPermissions {};
class Unlink: public DefaultPermissions {};
class Utimensat: public DefaultPermissions {};
class Write: public DefaultPermissions {};
/*
* Test permission handling during create, mkdir, mknod, link, symlink, and
* rename vops (they all share a common path for permission checks in
* VOP_LOOKUP)
*/
class Create: public DefaultPermissions {};
class Deleteextattr: public DefaultPermissions {
public:
void expect_removexattr()
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_REMOVEXATTR);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(0)));
}
};
class Getextattr: public DefaultPermissions {
public:
void expect_getxattr(ProcessMockerT r)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_GETXATTR);
}, Eq(true)),
_)
).WillOnce(Invoke(r));
}
};
class Listextattr: public DefaultPermissions {
public:
void expect_listxattr()
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_LISTXATTR);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
out.body.listxattr.size = 0;
SET_OUT_HEADER_LEN(out, listxattr);
})));
}
};
class Rename: public DefaultPermissions {
public:
/*
* Expect a rename and respond with the given error. Don't both to
* validate arguments; the tests in rename.cc do that.
*/
void expect_rename(int error)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_RENAME);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(error)));
}
};
class Setextattr: public DefaultPermissions {
public:
void expect_setxattr(int error)
{
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_SETXATTR);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnErrno(error)));
}
};
/* Return a group to which this user does not belong */
static gid_t excluded_group()
{
int i, ngroups = 64;
gid_t newgid, groups[ngroups];
getgrouplist(getlogin(), getegid(), groups, &ngroups);
for (newgid = 0; newgid >= 0; newgid++) {
bool belongs = false;
for (i = 0; i < ngroups; i++) {
if (groups[i] == newgid)
belongs = true;
}
if (!belongs)
break;
}
/* newgid is now a group to which the current user does not belong */
return newgid;
}
TEST_F(Access, eacces)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
mode_t access_mode = X_OK;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);
ASSERT_NE(0, access(FULLPATH, access_mode));
ASSERT_EQ(EACCES, errno);
}
TEST_F(Access, eacces_no_cached_attrs)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
mode_t access_mode = X_OK;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, 0, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0644, 0);
expect_getattr(ino, S_IFREG | 0644, 0, 1);
/*
* Once default_permissions is properly implemented, there might be
* another FUSE_GETATTR or something in here. But there should not be
* a FUSE_ACCESS
*/
ASSERT_NE(0, access(FULLPATH, access_mode));
ASSERT_EQ(EACCES, errno);
}
TEST_F(Access, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
mode_t access_mode = R_OK;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);
/*
* Once default_permissions is properly implemented, there might be
* another FUSE_GETATTR or something in here.
*/
ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);
}
/* Unprivileged users may chown a file to their own uid */
TEST_F(Chown, chown_to_self)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const mode_t mode = 0755;
uid_t uid;
uid = geteuid();
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid);
expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid);
/* The OS may optimize chown by omitting the redundant setattr */
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_SETATTR);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out){
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.mode = S_IFREG | mode;
out.body.attr.attr.uid = uid;
})));
EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno);
}
/*
* A successful chown by a non-privileged non-owner should clear a file's SUID
* bit
*/
TEST_F(Chown, clear_suid)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
const mode_t oldmode = 06755;
const mode_t newmode = 0755;
uid_t uid = geteuid();
uint32_t valid = FATTR_UID | FATTR_MODE;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid);
expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
in.body.setattr.valid == valid &&
in.body.setattr.mode == newmode);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | newmode;
out.body.attr.attr_valid = UINT64_MAX;
})));
EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno);
}
/* Only root may change a file's owner */
TEST_F(Chown, eperm)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const mode_t mode = 0755;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, geteuid());
expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, geteuid());
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_SETATTR);
}, Eq(true)),
_)
).Times(0);
EXPECT_NE(0, chown(FULLPATH, 0, -1));
EXPECT_EQ(EPERM, errno);
}
/*
* A successful chgrp by a non-privileged non-owner should clear a file's SUID
* bit
*/
TEST_F(Chgrp, clear_suid)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
const mode_t oldmode = 06755;
const mode_t newmode = 0755;
uid_t uid = geteuid();
gid_t gid = getegid();
uint32_t valid = FATTR_GID | FATTR_MODE;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid);
expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid);
EXPECT_CALL(*m_mock, process(
ResultOf([=](auto in) {
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
in.body.setattr.valid == valid &&
in.body.setattr.mode == newmode);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino; // Must match nodeid
out.body.attr.attr.mode = S_IFREG | newmode;
out.body.attr.attr_valid = UINT64_MAX;
})));
EXPECT_EQ(0, chown(FULLPATH, -1, gid)) << strerror(errno);
}
/* non-root users may only chgrp a file to a group they belong to */
TEST_F(Chgrp, eperm)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const mode_t mode = 0755;
uid_t uid;
gid_t gid, newgid;
uid = geteuid();
gid = getegid();
newgid = excluded_group();
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid);
expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid);
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_SETATTR);
}, Eq(true)),
_)
).Times(0);
EXPECT_NE(0, chown(FULLPATH, -1, newgid));
EXPECT_EQ(EPERM, errno);
}
TEST_F(Chgrp, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const mode_t mode = 0755;
uid_t uid;
gid_t gid, newgid;
uid = geteuid();
gid = 0;
newgid = getegid();
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid);
expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid);
/* The OS may optimize chgrp by omitting the redundant setattr */
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino);
}, Eq(true)),
_)
).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out){
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.mode = S_IFREG | mode;
out.body.attr.attr.uid = uid;
out.body.attr.attr.gid = newgid;
})));
EXPECT_EQ(0, chown(FULLPATH, -1, newgid)) << strerror(errno);
}
TEST_F(Create, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_create(RELPATH, ino);
fd = open(FULLPATH, O_CREAT | O_EXCL, 0644);
EXPECT_LE(0, fd) << strerror(errno);
leak(fd);
}
TEST_F(Create, eacces)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
EXPECT_EQ(-1, open(FULLPATH, O_CREAT | O_EXCL, 0644));
EXPECT_EQ(EACCES, errno);
}
TEST_F(Deleteextattr, eacces)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
ASSERT_EQ(EACCES, errno);
}
TEST_F(Deleteextattr, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
expect_removexattr();
ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))
<< strerror(errno);
}
/* Delete system attributes requires superuser privilege */
TEST_F(Deleteextattr, system)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_SYSTEM;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid());
ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));
ASSERT_EQ(EPERM, errno);
}
/* Anybody with write permission can set both timestamps to UTIME_NOW */
TEST_F(Utimensat, utime_now)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
/* Write permissions for everybody */
const mode_t mode = 0666;
uid_t owner = 0;
const timespec times[2] = {
{.tv_sec = 0, .tv_nsec = UTIME_NOW},
{.tv_sec = 0, .tv_nsec = UTIME_NOW},
};
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, owner);
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
in.body.setattr.valid & FATTR_ATIME &&
in.body.setattr.valid & FATTR_MTIME);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.mode = S_IFREG | mode;
})));
ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &times[0], 0))
<< strerror(errno);
}
/* Anybody can set both timestamps to UTIME_OMIT */
TEST_F(Utimensat, utime_omit)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
/* Write permissions for no one */
const mode_t mode = 0444;
uid_t owner = 0;
const timespec times[2] = {
{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
};
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, owner);
ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &times[0], 0))
<< strerror(errno);
}
/* Deleting user attributes merely requires WRITE privilege */
TEST_F(Deleteextattr, user)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0);
expect_removexattr();
ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))
<< strerror(errno);
}
TEST_F(Getextattr, eacces)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
char data[80];
int ns = EXTATTR_NAMESPACE_USER;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0600, UINT64_MAX, 0);
ASSERT_EQ(-1,
extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)));
ASSERT_EQ(EACCES, errno);
}
TEST_F(Getextattr, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
char data[80];
const char value[] = "whatever";
ssize_t value_len = strlen(value) + 1;
int ns = EXTATTR_NAMESPACE_USER;
ssize_t r;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
/* Getting user attributes only requires read access */
expect_lookup(RELPATH, ino, S_IFREG | 0444, UINT64_MAX, 0);
expect_getxattr(
ReturnImmediate([&](auto in __unused, auto& out) {
memcpy((void*)out.body.bytes, value, value_len);
out.header.len = sizeof(out.header) + value_len;
})
);
r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));
ASSERT_EQ(value_len, r) << strerror(errno);
EXPECT_STREQ(value, data);
}
/* Getting system attributes requires superuser privileges */
TEST_F(Getextattr, system)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
char data[80];
int ns = EXTATTR_NAMESPACE_SYSTEM;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid());
ASSERT_EQ(-1,
extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)));
ASSERT_EQ(EPERM, errno);
}
TEST_F(Listextattr, eacces)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0600, UINT64_MAX, 0);
ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
ASSERT_EQ(EACCES, errno);
}
TEST_F(Listextattr, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_USER;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
/* Listing user extended attributes merely requires read access */
expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
expect_listxattr();
ASSERT_EQ(0, extattr_list_file(FULLPATH, ns, NULL, 0))
<< strerror(errno);
}
/* Listing system xattrs requires superuser privileges */
TEST_F(Listextattr, system)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int ns = EXTATTR_NAMESPACE_SYSTEM;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
/* Listing user extended attributes merely requires read access */
expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));
ASSERT_EQ(EPERM, errno);
}
/* A component of the search path lacks execute permissions */
TEST_F(Lookup, eacces)
{
const char FULLPATH[] = "mountpoint/some_dir/some_file.txt";
const char RELDIRPATH[] = "some_dir";
uint64_t dir_ino = 42;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELDIRPATH, dir_ino, S_IFDIR | 0700, UINT64_MAX, 0);
EXPECT_EQ(-1, access(FULLPATH, F_OK));
EXPECT_EQ(EACCES, errno);
}
TEST_F(Open, eacces)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);
EXPECT_NE(0, open(FULLPATH, O_RDWR));
EXPECT_EQ(EACCES, errno);
}
TEST_F(Open, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
int fd;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);
expect_open(ino, 0, 1);
fd = open(FULLPATH, O_RDONLY);
EXPECT_LE(0, fd) << strerror(errno);
leak(fd);
}
TEST_F(Rename, eacces_on_srcdir)
{
const char FULLDST[] = "mountpoint/d/dst";
const char RELDST[] = "d/dst";
const char FULLSRC[] = "mountpoint/src";
const char RELSRC[] = "src";
uint64_t ino = 42;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, 0);
expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX);
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
.Times(AnyNumber())
.WillRepeatedly(Invoke(ReturnErrno(ENOENT)));
ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
ASSERT_EQ(EACCES, errno);
}
TEST_F(Rename, eacces_on_dstdir_for_creating)
{
const char FULLDST[] = "mountpoint/d/dst";
const char RELDSTDIR[] = "d";
const char RELDST[] = "dst";
const char FULLSRC[] = "mountpoint/src";
const char RELSRC[] = "src";
uint64_t src_ino = 42;
uint64_t dstdir_ino = 43;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);
expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX);
expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX);
EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
ASSERT_EQ(EACCES, errno);
}
TEST_F(Rename, eacces_on_dstdir_for_removing)
{
const char FULLDST[] = "mountpoint/d/dst";
const char RELDSTDIR[] = "d";
const char RELDST[] = "dst";
const char FULLSRC[] = "mountpoint/src";
const char RELSRC[] = "src";
uint64_t src_ino = 42;
uint64_t dstdir_ino = 43;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);
expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX);
expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX);
EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
ASSERT_EQ(EACCES, errno);
}
TEST_F(Rename, eperm_on_sticky_srcdir)
{
const char FULLDST[] = "mountpoint/d/dst";
const char FULLSRC[] = "mountpoint/src";
const char RELSRC[] = "src";
uint64_t ino = 42;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0);
expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX);
ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
ASSERT_EQ(EPERM, errno);
}
/*
* A user cannot move out a subdirectory that he does not own, because that
* would require changing the subdirectory's ".." dirent
*/
TEST_F(Rename, eperm_for_subdirectory)
{
const char FULLDST[] = "mountpoint/d/dst";
const char FULLSRC[] = "mountpoint/src";
const char RELDSTDIR[] = "d";
const char RELDST[] = "dst";
const char RELSRC[] = "src";
uint64_t ino = 42;
uint64_t dstdir_ino = 43;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);
expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0);
expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0777, UINT64_MAX, 0);
EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
ASSERT_EQ(EACCES, errno);
}
/*
* A user _can_ rename a subdirectory to which he lacks write permissions, if
* it will keep the same parent
*/
TEST_F(Rename, subdirectory_to_same_dir)
{
const char FULLDST[] = "mountpoint/dst";
const char FULLSRC[] = "mountpoint/src";
const char RELDST[] = "dst";
const char RELSRC[] = "src";
uint64_t ino = 42;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);
expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0);
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_rename(0);
ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
}
TEST_F(Rename, eperm_on_sticky_dstdir)
{
const char FULLDST[] = "mountpoint/d/dst";
const char RELDSTDIR[] = "d";
const char RELDST[] = "dst";
const char FULLSRC[] = "mountpoint/src";
const char RELSRC[] = "src";
uint64_t src_ino = 42;
uint64_t dstdir_ino = 43;
uint64_t dst_ino = 44;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);
expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX);
expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 01777, UINT64_MAX);
EXPECT_LOOKUP(dstdir_ino, RELDST)
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = dst_ino;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
out.body.entry.attr.uid = 0;
})));
ASSERT_EQ(-1, rename(FULLSRC, FULLDST));
ASSERT_EQ(EPERM, errno);
}
/* Successfully rename a file, overwriting the destination */
TEST_F(Rename, ok)
{
const char FULLDST[] = "mountpoint/dst";
const char RELDST[] = "dst";
const char FULLSRC[] = "mountpoint/src";
const char RELSRC[] = "src";
// The inode of the already-existing destination file
uint64_t dst_ino = 2;
uint64_t ino = 42;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, geteuid());
expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX);
expect_lookup(RELDST, dst_ino, S_IFREG | 0644, UINT64_MAX);
expect_rename(0);
ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
}
TEST_F(Rename, ok_to_remove_src_because_of_stickiness)
{
const char FULLDST[] = "mountpoint/dst";
const char RELDST[] = "dst";
const char FULLSRC[] = "mountpoint/src";
const char RELSRC[] = "src";
uint64_t ino = 42;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0);
expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_rename(0);
ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
}
TEST_F(Setattr, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const mode_t oldmode = 0755;
const mode_t newmode = 0644;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid());
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
in.body.setattr.mode == newmode);
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.mode = S_IFREG | newmode;
})));
EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
}
TEST_F(Setattr, eacces)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const mode_t oldmode = 0755;
const mode_t newmode = 0644;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, 0);
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_SETATTR);
}, Eq(true)),
_)
).Times(0);
EXPECT_NE(0, chmod(FULLPATH, newmode));
EXPECT_EQ(EPERM, errno);
}
/*
* ftruncate() of a file without writable permissions should succeed as long as
* the file descriptor is writable. This is important when combined with
* O_CREAT
*/
TEST_F(Setattr, ftruncate_of_newly_created_file)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const mode_t mode = 0000;
int fd;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.WillOnce(Invoke(ReturnErrno(ENOENT)));
expect_create(RELPATH, ino);
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_SETATTR &&
in.header.nodeid == ino &&
(in.body.setattr.valid & FATTR_SIZE));
}, Eq(true)),
_)
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
SET_OUT_HEADER_LEN(out, attr);
out.body.attr.attr.ino = ino;
out.body.attr.attr.mode = S_IFREG | mode;
out.body.attr.attr_valid = UINT64_MAX;
})));
fd = open(FULLPATH, O_CREAT | O_RDWR, 0);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(0, ftruncate(fd, 100)) << strerror(errno);
leak(fd);
}
/*
* Setting the sgid bit should fail for an unprivileged user who doesn't belong
* to the file's group
*/
TEST_F(Setattr, sgid_by_non_group_member)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const mode_t oldmode = 0755;
const mode_t newmode = 02755;
uid_t uid = geteuid();
gid_t gid = excluded_group();
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid);
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_SETATTR);
}, Eq(true)),
_)
).Times(0);
EXPECT_NE(0, chmod(FULLPATH, newmode));
EXPECT_EQ(EPERM, errno);
}
/* Only the superuser may set the sticky bit on a non-directory */
TEST_F(Setattr, sticky_regular_file)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
const uint64_t ino = 42;
const mode_t oldmode = 0644;
const mode_t newmode = 01644;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid());
EXPECT_CALL(*m_mock, process(
ResultOf([](auto in) {
return (in.header.opcode == FUSE_SETATTR);
}, Eq(true)),
_)
).Times(0);
EXPECT_NE(0, chmod(FULLPATH, newmode));
EXPECT_EQ(EFTYPE, errno);
}
TEST_F(Setextattr, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
const char value[] = "whatever";
ssize_t value_len = strlen(value) + 1;
int ns = EXTATTR_NAMESPACE_USER;
ssize_t r;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
expect_setxattr(0);
r = extattr_set_file(FULLPATH, ns, "foo", (void*)value, value_len);
ASSERT_EQ(value_len, r) << strerror(errno);
}
TEST_F(Setextattr, eacces)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
const char value[] = "whatever";
ssize_t value_len = strlen(value) + 1;
int ns = EXTATTR_NAMESPACE_USER;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
ASSERT_EQ(-1,
extattr_set_file(FULLPATH, ns, "foo", (void*)value, value_len));
ASSERT_EQ(EACCES, errno);
}
// Setting system attributes requires superuser privileges
TEST_F(Setextattr, system)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
const char value[] = "whatever";
ssize_t value_len = strlen(value) + 1;
int ns = EXTATTR_NAMESPACE_SYSTEM;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid());
ASSERT_EQ(-1,
extattr_set_file(FULLPATH, ns, "foo", (void*)value, value_len));
ASSERT_EQ(EPERM, errno);
}
// Setting user attributes merely requires write privileges
TEST_F(Setextattr, user)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
const char value[] = "whatever";
ssize_t value_len = strlen(value) + 1;
int ns = EXTATTR_NAMESPACE_USER;
ssize_t r;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0);
expect_setxattr(0);
r = extattr_set_file(FULLPATH, ns, "foo", (void*)value, value_len);
ASSERT_EQ(value_len, r) << strerror(errno);
}
TEST_F(Unlink, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
expect_unlink(FUSE_ROOT_ID, RELPATH, 0);
ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno);
}
/*
* Ensure that a cached name doesn't cause unlink to bypass permission checks
* in VOP_LOOKUP.
*
* This test should pass because lookup(9) purges the namecache entry by doing
* a vfs_cache_lookup with ~MAKEENTRY when nameiop == DELETE.
*/
TEST_F(Unlink, cached_unwritable_directory)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
.Times(AnyNumber())
.WillRepeatedly(Invoke(
ReturnImmediate([=](auto i __unused, auto& out) {
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFREG | 0644;
out.body.entry.nodeid = ino;
out.body.entry.entry_valid = UINT64_MAX;
}))
);
/* Fill name cache */
ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);
/* Despite cached name , unlink should fail */
ASSERT_EQ(-1, unlink(FULLPATH));
ASSERT_EQ(EACCES, errno);
}
TEST_F(Unlink, unwritable_directory)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());
ASSERT_EQ(-1, unlink(FULLPATH));
ASSERT_EQ(EACCES, errno);
}
TEST_F(Unlink, sticky_directory)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);
ASSERT_EQ(-1, unlink(FULLPATH));
ASSERT_EQ(EPERM, errno);
}
/* A write by a non-owner should clear a file's SUID bit */
TEST_F(Write, clear_suid)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
struct stat sb;
uint64_t ino = 42;
mode_t oldmode = 04777;
mode_t newmode = 0777;
char wbuf[1] = {'x'};
int fd;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX);
expect_open(ino, 0, 1);
expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf);
expect_chmod(ino, newmode, sizeof(wbuf));
fd = open(FULLPATH, O_WRONLY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno);
ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
leak(fd);
}
/* A write by a non-owner should clear a file's SGID bit */
TEST_F(Write, clear_sgid)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
struct stat sb;
uint64_t ino = 42;
mode_t oldmode = 02777;
mode_t newmode = 0777;
char wbuf[1] = {'x'};
int fd;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX);
expect_open(ino, 0, 1);
expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf);
expect_chmod(ino, newmode, sizeof(wbuf));
fd = open(FULLPATH, O_WRONLY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno);
ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);
EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
leak(fd);
}
/* Regression test for a specific recurse-of-nonrecursive-lock panic
*
* With writeback caching, we can't call vtruncbuf from fuse_io_strategy, or it
* may panic. That happens if the FUSE_SETATTR response indicates that the
* file's size has changed since the write.
*/
TEST_F(Write, recursion_panic_while_clearing_suid)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
uint64_t ino = 42;
mode_t oldmode = 04777;
mode_t newmode = 0777;
char wbuf[1] = {'x'};
int fd;
expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);
expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX);
expect_open(ino, 0, 1);
expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf);
/* XXX Return a smaller file size than what we just wrote! */
expect_chmod(ino, newmode, 0);
fd = open(FULLPATH, O_WRONLY);
ASSERT_LE(0, fd) << strerror(errno);
ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno);
leak(fd);
}