Path: blob/main/tests/sys/fs/fusefs/default_permissions.cc
39537 views
/*-1* SPDX-License-Identifier: BSD-2-Clause2*3* Copyright (c) 2019 The FreeBSD Foundation4*5* This software was developed by BFF Storage Systems, LLC under sponsorship6* from the FreeBSD Foundation.7*8* Redistribution and use in source and binary forms, with or without9* modification, are permitted provided that the following conditions10* are met:11* 1. Redistributions of source code must retain the above copyright12* notice, this list of conditions and the following disclaimer.13* 2. Redistributions in binary form must reproduce the above copyright14* notice, this list of conditions and the following disclaimer in the15* documentation and/or other materials provided with the distribution.16*17* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND18* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE19* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE20* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE21* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL22* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS23* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)24* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT25* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY26* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF27* SUCH DAMAGE.28*/2930/*31* Tests for the "default_permissions" mount option. They must be in their own32* file so they can be run as an unprivileged user.33*/3435extern "C" {36#include <sys/types.h>37#include <sys/extattr.h>3839#include <fcntl.h>40#include <semaphore.h>41#include <unistd.h>42}4344#include "mockfs.hh"45#include "utils.hh"4647using namespace testing;4849class DefaultPermissions: public FuseTest {5051virtual void SetUp() {52m_default_permissions = true;53FuseTest::SetUp();54if (HasFatalFailure() || IsSkipped())55return;5657if (geteuid() == 0) {58GTEST_SKIP() << "This test requires an unprivileged user";59}6061/* With -o default_permissions, FUSE_ACCESS should never be called */62EXPECT_CALL(*m_mock, process(63ResultOf([=](auto in) {64return (in.header.opcode == FUSE_ACCESS);65}, Eq(true)),66_)67).Times(0);68}6970public:71void expect_chmod(uint64_t ino, mode_t mode, uint64_t size = 0)72{73EXPECT_CALL(*m_mock, process(74ResultOf([=](auto in) {75return (in.header.opcode == FUSE_SETATTR &&76in.header.nodeid == ino &&77in.body.setattr.valid == FATTR_MODE &&78in.body.setattr.mode == mode);79}, Eq(true)),80_)81).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {82SET_OUT_HEADER_LEN(out, attr);83out.body.attr.attr.ino = ino; // Must match nodeid84out.body.attr.attr.mode = S_IFREG | mode;85out.body.attr.attr.size = size;86out.body.attr.attr_valid = UINT64_MAX;87})));88}8990void expect_create(const char *relpath, uint64_t ino)91{92EXPECT_CALL(*m_mock, process(93ResultOf([=](auto in) {94const char *name = (const char*)in.body.bytes +95sizeof(fuse_create_in);96return (in.header.opcode == FUSE_CREATE &&97(0 == strcmp(relpath, name)));98}, Eq(true)),99_)100).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {101SET_OUT_HEADER_LEN(out, create);102out.body.create.entry.attr.mode = S_IFREG | 0644;103out.body.create.entry.nodeid = ino;104out.body.create.entry.entry_valid = UINT64_MAX;105out.body.create.entry.attr_valid = UINT64_MAX;106})));107}108109void expect_copy_file_range(uint64_t ino_in, uint64_t off_in, uint64_t ino_out,110uint64_t off_out, uint64_t len)111{112EXPECT_CALL(*m_mock, process(113ResultOf([=](auto in) {114return (in.header.opcode == FUSE_COPY_FILE_RANGE &&115in.header.nodeid == ino_in &&116in.body.copy_file_range.off_in == off_in &&117in.body.copy_file_range.nodeid_out == ino_out &&118in.body.copy_file_range.off_out == off_out &&119in.body.copy_file_range.len == len);120}, Eq(true)),121_)122).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {123SET_OUT_HEADER_LEN(out, write);124out.body.write.size = len;125})));126}127128void expect_getattr(uint64_t ino, mode_t mode, uint64_t attr_valid, int times,129uid_t uid = 0, gid_t gid = 0)130{131EXPECT_CALL(*m_mock, process(132ResultOf([=](auto in) {133return (in.header.opcode == FUSE_GETATTR &&134in.header.nodeid == ino);135}, Eq(true)),136_)137).Times(times)138.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {139SET_OUT_HEADER_LEN(out, attr);140out.body.attr.attr.ino = ino; // Must match nodeid141out.body.attr.attr.mode = mode;142out.body.attr.attr.size = 0;143out.body.attr.attr.uid = uid;144out.body.attr.attr.gid = gid;145out.body.attr.attr_valid = attr_valid;146})));147}148149void expect_lookup(const char *relpath, uint64_t ino, mode_t mode,150uint64_t attr_valid, uid_t uid = 0, gid_t gid = 0)151{152FuseTest::expect_lookup(relpath, ino, mode, 0, 1, attr_valid, uid, gid);153}154155};156157class Access: public DefaultPermissions {};158class Chown: public DefaultPermissions {};159class Chgrp: public DefaultPermissions {};160class CopyFileRange: public DefaultPermissions {};161class Fspacectl: public DefaultPermissions {};162class Lookup: public DefaultPermissions {};163class Open: public DefaultPermissions {};164class PosixFallocate: public DefaultPermissions {};165class Read: public DefaultPermissions {};166class Setattr: public DefaultPermissions {};167class Unlink: public DefaultPermissions {};168class Utimensat: public DefaultPermissions {};169class Write: public DefaultPermissions {};170171/*172* Test permission handling during create, mkdir, mknod, link, symlink, and173* rename vops (they all share a common path for permission checks in174* VOP_LOOKUP)175*/176class Create: public DefaultPermissions {};177178class Deleteextattr: public DefaultPermissions {179public:180void expect_removexattr()181{182EXPECT_CALL(*m_mock, process(183ResultOf([=](auto in) {184return (in.header.opcode == FUSE_REMOVEXATTR);185}, Eq(true)),186_)187).WillOnce(Invoke(ReturnErrno(0)));188}189};190191class Getextattr: public DefaultPermissions {192public:193void expect_getxattr(ProcessMockerT r)194{195EXPECT_CALL(*m_mock, process(196ResultOf([=](auto in) {197return (in.header.opcode == FUSE_GETXATTR);198}, Eq(true)),199_)200).WillOnce(Invoke(r));201}202};203204class Listextattr: public DefaultPermissions {205public:206void expect_listxattr()207{208EXPECT_CALL(*m_mock, process(209ResultOf([=](auto in) {210return (in.header.opcode == FUSE_LISTXATTR);211}, Eq(true)),212_)213).WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {214out.body.listxattr.size = 0;215SET_OUT_HEADER_LEN(out, listxattr);216})));217}218};219220class Rename: public DefaultPermissions {221public:222/*223* Expect a rename and respond with the given error. Don't both to224* validate arguments; the tests in rename.cc do that.225*/226void expect_rename(int error)227{228EXPECT_CALL(*m_mock, process(229ResultOf([=](auto in) {230return (in.header.opcode == FUSE_RENAME);231}, Eq(true)),232_)233).WillOnce(Invoke(ReturnErrno(error)));234}235};236237class Setextattr: public DefaultPermissions {238public:239void expect_setxattr(int error)240{241EXPECT_CALL(*m_mock, process(242ResultOf([=](auto in) {243return (in.header.opcode == FUSE_SETXATTR);244}, Eq(true)),245_)246).WillOnce(Invoke(ReturnErrno(error)));247}248};249250/* Return a group to which this user does not belong */251static gid_t excluded_group()252{253int i, ngroups = 64;254gid_t newgid, groups[ngroups];255256getgrouplist(getlogin(), getegid(), groups, &ngroups);257for (newgid = 0; ; newgid++) {258bool belongs = false;259260for (i = 0; i < ngroups; i++) {261if (groups[i] == newgid)262belongs = true;263}264if (!belongs)265break;266}267/* newgid is now a group to which the current user does not belong */268return newgid;269}270271TEST_F(Access, eacces)272{273const char FULLPATH[] = "mountpoint/some_file.txt";274const char RELPATH[] = "some_file.txt";275uint64_t ino = 42;276mode_t access_mode = X_OK;277278expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);279expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);280281ASSERT_NE(0, access(FULLPATH, access_mode));282ASSERT_EQ(EACCES, errno);283}284285TEST_F(Access, eacces_no_cached_attrs)286{287const char FULLPATH[] = "mountpoint/some_file.txt";288const char RELPATH[] = "some_file.txt";289uint64_t ino = 42;290mode_t access_mode = X_OK;291292expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, 0, 1);293expect_lookup(RELPATH, ino, S_IFREG | 0644, 0);294expect_getattr(ino, S_IFREG | 0644, 0, 1);295/*296* Once default_permissions is properly implemented, there might be297* another FUSE_GETATTR or something in here. But there should not be298* a FUSE_ACCESS299*/300301ASSERT_NE(0, access(FULLPATH, access_mode));302ASSERT_EQ(EACCES, errno);303}304305TEST_F(Access, ok)306{307const char FULLPATH[] = "mountpoint/some_file.txt";308const char RELPATH[] = "some_file.txt";309uint64_t ino = 42;310mode_t access_mode = R_OK;311312expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);313expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);314/*315* Once default_permissions is properly implemented, there might be316* another FUSE_GETATTR or something in here.317*/318319ASSERT_EQ(0, access(FULLPATH, access_mode)) << strerror(errno);320}321322/* Unprivileged users may chown a file to their own uid */323TEST_F(Chown, chown_to_self)324{325const char FULLPATH[] = "mountpoint/some_file.txt";326const char RELPATH[] = "some_file.txt";327const uint64_t ino = 42;328const mode_t mode = 0755;329uid_t uid;330331uid = geteuid();332333expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid);334expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid);335/* The OS may optimize chown by omitting the redundant setattr */336EXPECT_CALL(*m_mock, process(337ResultOf([](auto in) {338return (in.header.opcode == FUSE_SETATTR);339}, Eq(true)),340_)341).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out){342SET_OUT_HEADER_LEN(out, attr);343out.body.attr.attr.mode = S_IFREG | mode;344out.body.attr.attr.uid = uid;345})));346347EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno);348}349350/*351* A successful chown by a non-privileged non-owner should clear a file's SUID352* bit353*/354TEST_F(Chown, clear_suid)355{356const char FULLPATH[] = "mountpoint/some_file.txt";357const char RELPATH[] = "some_file.txt";358uint64_t ino = 42;359const mode_t oldmode = 06755;360const mode_t newmode = 0755;361uid_t uid = geteuid();362uint32_t valid = FATTR_UID | FATTR_MODE;363364expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid);365expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid);366EXPECT_CALL(*m_mock, process(367ResultOf([=](auto in) {368return (in.header.opcode == FUSE_SETATTR &&369in.header.nodeid == ino &&370in.body.setattr.valid == valid &&371in.body.setattr.mode == newmode);372}, Eq(true)),373_)374).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {375SET_OUT_HEADER_LEN(out, attr);376out.body.attr.attr.ino = ino; // Must match nodeid377out.body.attr.attr.mode = S_IFREG | newmode;378out.body.attr.attr_valid = UINT64_MAX;379})));380381EXPECT_EQ(0, chown(FULLPATH, uid, -1)) << strerror(errno);382}383384385/* Only root may change a file's owner */386TEST_F(Chown, eperm)387{388const char FULLPATH[] = "mountpoint/some_file.txt";389const char RELPATH[] = "some_file.txt";390const uint64_t ino = 42;391const mode_t mode = 0755;392393expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, geteuid());394expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, geteuid());395EXPECT_CALL(*m_mock, process(396ResultOf([](auto in) {397return (in.header.opcode == FUSE_SETATTR);398}, Eq(true)),399_)400).Times(0);401402EXPECT_NE(0, chown(FULLPATH, 0, -1));403EXPECT_EQ(EPERM, errno);404}405406/*407* A successful chgrp by a non-privileged non-owner should clear a file's SUID408* bit409*/410TEST_F(Chgrp, clear_suid)411{412const char FULLPATH[] = "mountpoint/some_file.txt";413const char RELPATH[] = "some_file.txt";414uint64_t ino = 42;415const mode_t oldmode = 06755;416const mode_t newmode = 0755;417uid_t uid = geteuid();418gid_t gid = getegid();419uint32_t valid = FATTR_GID | FATTR_MODE;420421expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid);422expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid);423EXPECT_CALL(*m_mock, process(424ResultOf([=](auto in) {425return (in.header.opcode == FUSE_SETATTR &&426in.header.nodeid == ino &&427in.body.setattr.valid == valid &&428in.body.setattr.mode == newmode);429}, Eq(true)),430_)431).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {432SET_OUT_HEADER_LEN(out, attr);433out.body.attr.attr.ino = ino; // Must match nodeid434out.body.attr.attr.mode = S_IFREG | newmode;435out.body.attr.attr_valid = UINT64_MAX;436})));437438EXPECT_EQ(0, chown(FULLPATH, -1, gid)) << strerror(errno);439}440441/* non-root users may only chgrp a file to a group they belong to */442TEST_F(Chgrp, eperm)443{444const char FULLPATH[] = "mountpoint/some_file.txt";445const char RELPATH[] = "some_file.txt";446const uint64_t ino = 42;447const mode_t mode = 0755;448uid_t uid;449gid_t gid, newgid;450451uid = geteuid();452gid = getegid();453newgid = excluded_group();454455expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid);456expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid);457EXPECT_CALL(*m_mock, process(458ResultOf([](auto in) {459return (in.header.opcode == FUSE_SETATTR);460}, Eq(true)),461_)462).Times(0);463464EXPECT_NE(0, chown(FULLPATH, -1, newgid));465EXPECT_EQ(EPERM, errno);466}467468TEST_F(Chgrp, ok)469{470const char FULLPATH[] = "mountpoint/some_file.txt";471const char RELPATH[] = "some_file.txt";472const uint64_t ino = 42;473const mode_t mode = 0755;474uid_t uid;475gid_t gid, newgid;476477uid = geteuid();478gid = 0;479newgid = getegid();480481expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, uid, gid);482expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, uid, gid);483/* The OS may optimize chgrp by omitting the redundant setattr */484EXPECT_CALL(*m_mock, process(485ResultOf([](auto in) {486return (in.header.opcode == FUSE_SETATTR &&487in.header.nodeid == ino);488}, Eq(true)),489_)490).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out){491SET_OUT_HEADER_LEN(out, attr);492out.body.attr.attr.mode = S_IFREG | mode;493out.body.attr.attr.uid = uid;494out.body.attr.attr.gid = newgid;495})));496497EXPECT_EQ(0, chown(FULLPATH, -1, newgid)) << strerror(errno);498}499500/* A write by a non-owner should clear a file's SGID bit */501TEST_F(CopyFileRange, clear_sgid)502{503const char FULLPATH_IN[] = "mountpoint/in.txt";504const char RELPATH_IN[] = "in.txt";505const char FULLPATH_OUT[] = "mountpoint/out.txt";506const char RELPATH_OUT[] = "out.txt";507struct stat sb;508uint64_t ino_in = 42;509uint64_t ino_out = 43;510mode_t oldmode = 02777;511mode_t newmode = 0777;512off_t fsize = 16;513off_t off_in = 0;514off_t off_out = 8;515off_t len = 8;516int fd_in, fd_out;517518expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);519FuseTest::expect_lookup(RELPATH_IN, ino_in, S_IFREG | oldmode, fsize, 1,520UINT64_MAX, 0, 0);521expect_open(ino_in, 0, 1);522FuseTest::expect_lookup(RELPATH_OUT, ino_out, S_IFREG | oldmode, fsize,5231, UINT64_MAX, 0, 0);524expect_open(ino_out, 0, 1);525expect_copy_file_range(ino_in, off_in, ino_out, off_out, len);526expect_chmod(ino_out, newmode, fsize);527528fd_in = open(FULLPATH_IN, O_RDONLY);529ASSERT_LE(0, fd_in) << strerror(errno);530fd_out = open(FULLPATH_OUT, O_WRONLY);531ASSERT_LE(0, fd_out) << strerror(errno);532ASSERT_EQ(len,533copy_file_range(fd_in, &off_in, fd_out, &off_out, len, 0))534<< strerror(errno);535ASSERT_EQ(0, fstat(fd_out, &sb)) << strerror(errno);536EXPECT_EQ(S_IFREG | newmode, sb.st_mode);537ASSERT_EQ(0, fstat(fd_in, &sb)) << strerror(errno);538EXPECT_EQ(S_IFREG | oldmode, sb.st_mode);539540leak(fd_in);541leak(fd_out);542}543544/* A write by a non-owner should clear a file's SUID bit */545TEST_F(CopyFileRange, clear_suid)546{547const char FULLPATH_IN[] = "mountpoint/in.txt";548const char RELPATH_IN[] = "in.txt";549const char FULLPATH_OUT[] = "mountpoint/out.txt";550const char RELPATH_OUT[] = "out.txt";551struct stat sb;552uint64_t ino_in = 42;553uint64_t ino_out = 43;554mode_t oldmode = 04777;555mode_t newmode = 0777;556off_t fsize = 16;557off_t off_in = 0;558off_t off_out = 8;559off_t len = 8;560int fd_in, fd_out;561562expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);563FuseTest::expect_lookup(RELPATH_IN, ino_in, S_IFREG | oldmode, fsize, 1,564UINT64_MAX, 0, 0);565expect_open(ino_in, 0, 1);566FuseTest::expect_lookup(RELPATH_OUT, ino_out, S_IFREG | oldmode, fsize,5671, UINT64_MAX, 0, 0);568expect_open(ino_out, 0, 1);569expect_copy_file_range(ino_in, off_in, ino_out, off_out, len);570expect_chmod(ino_out, newmode, fsize);571572fd_in = open(FULLPATH_IN, O_RDONLY);573ASSERT_LE(0, fd_in) << strerror(errno);574fd_out = open(FULLPATH_OUT, O_WRONLY);575ASSERT_LE(0, fd_out) << strerror(errno);576ASSERT_EQ(len,577copy_file_range(fd_in, &off_in, fd_out, &off_out, len, 0))578<< strerror(errno);579ASSERT_EQ(0, fstat(fd_out, &sb)) << strerror(errno);580EXPECT_EQ(S_IFREG | newmode, sb.st_mode);581ASSERT_EQ(0, fstat(fd_in, &sb)) << strerror(errno);582EXPECT_EQ(S_IFREG | oldmode, sb.st_mode);583584leak(fd_in);585leak(fd_out);586}587588TEST_F(Create, ok)589{590const char FULLPATH[] = "mountpoint/some_file.txt";591const char RELPATH[] = "some_file.txt";592uint64_t ino = 42;593int fd;594595expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);596EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)597.WillOnce(Invoke(ReturnErrno(ENOENT)));598expect_create(RELPATH, ino);599600fd = open(FULLPATH, O_CREAT | O_EXCL, 0644);601ASSERT_LE(0, fd) << strerror(errno);602leak(fd);603}604605TEST_F(Create, eacces)606{607const char FULLPATH[] = "mountpoint/some_file.txt";608const char RELPATH[] = "some_file.txt";609610expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);611EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)612.WillOnce(Invoke(ReturnErrno(ENOENT)));613614ASSERT_EQ(-1, open(FULLPATH, O_CREAT | O_EXCL, 0644));615EXPECT_EQ(EACCES, errno);616}617618TEST_F(Deleteextattr, eacces)619{620const char FULLPATH[] = "mountpoint/some_file.txt";621const char RELPATH[] = "some_file.txt";622uint64_t ino = 42;623int ns = EXTATTR_NAMESPACE_USER;624625expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);626expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);627628ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));629ASSERT_EQ(EACCES, errno);630}631632TEST_F(Deleteextattr, ok)633{634const char FULLPATH[] = "mountpoint/some_file.txt";635const char RELPATH[] = "some_file.txt";636uint64_t ino = 42;637int ns = EXTATTR_NAMESPACE_USER;638639expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);640expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());641expect_removexattr();642643ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))644<< strerror(errno);645}646647/* Delete system attributes requires superuser privilege */648TEST_F(Deleteextattr, system)649{650const char FULLPATH[] = "mountpoint/some_file.txt";651const char RELPATH[] = "some_file.txt";652uint64_t ino = 42;653int ns = EXTATTR_NAMESPACE_SYSTEM;654655expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);656expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid());657658ASSERT_EQ(-1, extattr_delete_file(FULLPATH, ns, "foo"));659ASSERT_EQ(EPERM, errno);660}661662/* Anybody with write permission can set both timestamps to UTIME_NOW */663TEST_F(Utimensat, utime_now)664{665const char FULLPATH[] = "mountpoint/some_file.txt";666const char RELPATH[] = "some_file.txt";667const uint64_t ino = 42;668/* Write permissions for everybody */669const mode_t mode = 0666;670uid_t owner = 0;671const timespec times[2] = {672{.tv_sec = 0, .tv_nsec = UTIME_NOW},673{.tv_sec = 0, .tv_nsec = UTIME_NOW},674};675676expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);677expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, owner);678EXPECT_CALL(*m_mock, process(679ResultOf([](auto in) {680return (in.header.opcode == FUSE_SETATTR &&681in.header.nodeid == ino &&682in.body.setattr.valid & FATTR_ATIME &&683in.body.setattr.valid & FATTR_MTIME);684}, Eq(true)),685_)686).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {687SET_OUT_HEADER_LEN(out, attr);688out.body.attr.attr.mode = S_IFREG | mode;689})));690691ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, ×[0], 0))692<< strerror(errno);693}694695/* Anybody can set both timestamps to UTIME_OMIT */696TEST_F(Utimensat, utime_omit)697{698const char FULLPATH[] = "mountpoint/some_file.txt";699const char RELPATH[] = "some_file.txt";700const uint64_t ino = 42;701/* Write permissions for no one */702const mode_t mode = 0444;703uid_t owner = 0;704const timespec times[2] = {705{.tv_sec = 0, .tv_nsec = UTIME_OMIT},706{.tv_sec = 0, .tv_nsec = UTIME_OMIT},707};708709expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);710expect_lookup(RELPATH, ino, S_IFREG | mode, UINT64_MAX, owner);711712ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, ×[0], 0))713<< strerror(errno);714}715716/* Deleting user attributes merely requires WRITE privilege */717TEST_F(Deleteextattr, user)718{719const char FULLPATH[] = "mountpoint/some_file.txt";720const char RELPATH[] = "some_file.txt";721uint64_t ino = 42;722int ns = EXTATTR_NAMESPACE_USER;723724expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);725expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0);726expect_removexattr();727728ASSERT_EQ(0, extattr_delete_file(FULLPATH, ns, "foo"))729<< strerror(errno);730}731732TEST_F(Getextattr, eacces)733{734const char FULLPATH[] = "mountpoint/some_file.txt";735const char RELPATH[] = "some_file.txt";736uint64_t ino = 42;737char data[80];738int ns = EXTATTR_NAMESPACE_USER;739740expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);741expect_lookup(RELPATH, ino, S_IFREG | 0600, UINT64_MAX, 0);742743ASSERT_EQ(-1,744extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)));745ASSERT_EQ(EACCES, errno);746}747748TEST_F(Getextattr, ok)749{750const char FULLPATH[] = "mountpoint/some_file.txt";751const char RELPATH[] = "some_file.txt";752uint64_t ino = 42;753char data[80];754const char value[] = "whatever";755ssize_t value_len = strlen(value) + 1;756int ns = EXTATTR_NAMESPACE_USER;757ssize_t r;758759expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);760/* Getting user attributes only requires read access */761expect_lookup(RELPATH, ino, S_IFREG | 0444, UINT64_MAX, 0);762expect_getxattr(763ReturnImmediate([&](auto in __unused, auto& out) {764memcpy((void*)out.body.bytes, value, value_len);765out.header.len = sizeof(out.header) + value_len;766})767);768769r = extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data));770ASSERT_EQ(value_len, r) << strerror(errno);771EXPECT_STREQ(value, data);772}773774/* Getting system attributes requires superuser privileges */775TEST_F(Getextattr, system)776{777const char FULLPATH[] = "mountpoint/some_file.txt";778const char RELPATH[] = "some_file.txt";779uint64_t ino = 42;780char data[80];781int ns = EXTATTR_NAMESPACE_SYSTEM;782783expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);784expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid());785786ASSERT_EQ(-1,787extattr_get_file(FULLPATH, ns, "foo", data, sizeof(data)));788ASSERT_EQ(EPERM, errno);789}790791TEST_F(Listextattr, eacces)792{793const char FULLPATH[] = "mountpoint/some_file.txt";794const char RELPATH[] = "some_file.txt";795uint64_t ino = 42;796int ns = EXTATTR_NAMESPACE_USER;797798expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);799expect_lookup(RELPATH, ino, S_IFREG | 0600, UINT64_MAX, 0);800801ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));802ASSERT_EQ(EACCES, errno);803}804805TEST_F(Listextattr, ok)806{807const char FULLPATH[] = "mountpoint/some_file.txt";808const char RELPATH[] = "some_file.txt";809uint64_t ino = 42;810int ns = EXTATTR_NAMESPACE_USER;811812expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);813/* Listing user extended attributes merely requires read access */814expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);815expect_listxattr();816817ASSERT_EQ(0, extattr_list_file(FULLPATH, ns, NULL, 0))818<< strerror(errno);819}820821/* Listing system xattrs requires superuser privileges */822TEST_F(Listextattr, system)823{824const char FULLPATH[] = "mountpoint/some_file.txt";825const char RELPATH[] = "some_file.txt";826uint64_t ino = 42;827int ns = EXTATTR_NAMESPACE_SYSTEM;828829expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);830/* Listing user extended attributes merely requires read access */831expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());832833ASSERT_EQ(-1, extattr_list_file(FULLPATH, ns, NULL, 0));834ASSERT_EQ(EPERM, errno);835}836837/* A write by a non-owner should clear a file's SGID bit */838TEST_F(Fspacectl, clear_sgid)839{840const char FULLPATH[] = "mountpoint/file.txt";841const char RELPATH[] = "file.txt";842struct stat sb;843struct spacectl_range rqsr;844uint64_t ino = 42;845mode_t oldmode = 02777;846mode_t newmode = 0777;847off_t fsize = 16;848off_t off = 8;849off_t len = 8;850int fd;851852expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);853FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize,8541, UINT64_MAX, 0, 0);855expect_open(ino, 0, 1);856expect_fallocate(ino, off, len,857FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);858expect_chmod(ino, newmode, fsize);859860fd = open(FULLPATH, O_WRONLY);861ASSERT_LE(0, fd) << strerror(errno);862rqsr.r_len = len;863rqsr.r_offset = off;864EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));865ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);866EXPECT_EQ(S_IFREG | newmode, sb.st_mode);867868leak(fd);869}870871/* A write by a non-owner should clear a file's SUID bit */872TEST_F(Fspacectl, clear_suid)873{874const char FULLPATH[] = "mountpoint/file.txt";875const char RELPATH[] = "file.txt";876struct stat sb;877struct spacectl_range rqsr;878uint64_t ino = 42;879mode_t oldmode = 04777;880mode_t newmode = 0777;881off_t fsize = 16;882off_t off = 8;883off_t len = 8;884int fd;885886expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);887FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize,8881, UINT64_MAX, 0, 0);889expect_open(ino, 0, 1);890expect_fallocate(ino, off, len,891FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);892expect_chmod(ino, newmode, fsize);893894fd = open(FULLPATH, O_WRONLY);895ASSERT_LE(0, fd) << strerror(errno);896rqsr.r_len = len;897rqsr.r_offset = off;898EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));899ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);900EXPECT_EQ(S_IFREG | newmode, sb.st_mode);901902leak(fd);903}904905/*906* fspacectl() of a file without writable permissions should succeed as907* long as the file descriptor is writable. This is important when combined908* with O_CREAT909*/910TEST_F(Fspacectl, posix_fallocate_of_newly_created_file)911{912const char FULLPATH[] = "mountpoint/some_file.txt";913const char RELPATH[] = "some_file.txt";914struct spacectl_range rqsr;915const uint64_t ino = 42;916off_t off = 8;917off_t len = 8;918int fd;919920expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);921EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)922.WillOnce(Invoke(ReturnErrno(ENOENT)));923expect_create(RELPATH, ino);924expect_fallocate(ino, off, len,925FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);926927fd = open(FULLPATH, O_CREAT | O_RDWR, 0);928ASSERT_LE(0, fd) << strerror(errno);929rqsr.r_len = len;930rqsr.r_offset = off;931EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));932leak(fd);933}934935/* A component of the search path lacks execute permissions */936TEST_F(Lookup, eacces)937{938const char FULLPATH[] = "mountpoint/some_dir/some_file.txt";939const char RELDIRPATH[] = "some_dir";940uint64_t dir_ino = 42;941942expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);943expect_lookup(RELDIRPATH, dir_ino, S_IFDIR | 0700, UINT64_MAX, 0);944945EXPECT_EQ(-1, access(FULLPATH, F_OK));946EXPECT_EQ(EACCES, errno);947}948949TEST_F(Open, eacces)950{951const char FULLPATH[] = "mountpoint/some_file.txt";952const char RELPATH[] = "some_file.txt";953uint64_t ino = 42;954955expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);956expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);957958EXPECT_EQ(-1, open(FULLPATH, O_RDWR));959EXPECT_EQ(EACCES, errno);960}961962TEST_F(Open, ok)963{964const char FULLPATH[] = "mountpoint/some_file.txt";965const char RELPATH[] = "some_file.txt";966uint64_t ino = 42;967int fd;968969expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);970expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX);971expect_open(ino, 0, 1);972973fd = open(FULLPATH, O_RDONLY);974ASSERT_LE(0, fd) << strerror(errno);975leak(fd);976}977978/* A write by a non-owner should clear a file's SGID bit */979TEST_F(PosixFallocate, clear_sgid)980{981const char FULLPATH[] = "mountpoint/file.txt";982const char RELPATH[] = "file.txt";983struct stat sb;984uint64_t ino = 42;985mode_t oldmode = 02777;986mode_t newmode = 0777;987off_t fsize = 16;988off_t off = 8;989off_t len = 8;990int fd;991992expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);993FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize,9941, UINT64_MAX, 0, 0);995expect_open(ino, 0, 1);996expect_fallocate(ino, off, len, 0, 0);997expect_chmod(ino, newmode, fsize);998999fd = open(FULLPATH, O_WRONLY);1000ASSERT_LE(0, fd) << strerror(errno);1001EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno);1002ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);1003EXPECT_EQ(S_IFREG | newmode, sb.st_mode);10041005leak(fd);1006}10071008/* A write by a non-owner should clear a file's SUID bit */1009TEST_F(PosixFallocate, clear_suid)1010{1011const char FULLPATH[] = "mountpoint/file.txt";1012const char RELPATH[] = "file.txt";1013struct stat sb;1014uint64_t ino = 42;1015mode_t oldmode = 04777;1016mode_t newmode = 0777;1017off_t fsize = 16;1018off_t off = 8;1019off_t len = 8;1020int fd;10211022expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);1023FuseTest::expect_lookup(RELPATH, ino, S_IFREG | oldmode, fsize,10241, UINT64_MAX, 0, 0);1025expect_open(ino, 0, 1);1026expect_fallocate(ino, off, len, 0, 0);1027expect_chmod(ino, newmode, fsize);10281029fd = open(FULLPATH, O_WRONLY);1030ASSERT_LE(0, fd) << strerror(errno);1031EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno);1032ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);1033EXPECT_EQ(S_IFREG | newmode, sb.st_mode);10341035leak(fd);1036}10371038/*1039* posix_fallocate() of a file without writable permissions should succeed as1040* long as the file descriptor is writable. This is important when combined1041* with O_CREAT1042*/1043TEST_F(PosixFallocate, posix_fallocate_of_newly_created_file)1044{1045const char FULLPATH[] = "mountpoint/some_file.txt";1046const char RELPATH[] = "some_file.txt";1047const uint64_t ino = 42;1048off_t off = 8;1049off_t len = 8;1050int fd;10511052expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);1053EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)1054.WillOnce(Invoke(ReturnErrno(ENOENT)));1055expect_create(RELPATH, ino);1056expect_fallocate(ino, off, len, 0, 0);10571058fd = open(FULLPATH, O_CREAT | O_RDWR, 0);1059ASSERT_LE(0, fd) << strerror(errno);1060EXPECT_EQ(0, posix_fallocate(fd, off, len)) << strerror(errno);1061leak(fd);1062}10631064TEST_F(Rename, eacces_on_srcdir)1065{1066const char FULLDST[] = "mountpoint/d/dst";1067const char RELDST[] = "d/dst";1068const char FULLSRC[] = "mountpoint/src";1069const char RELSRC[] = "src";1070uint64_t ino = 42;10711072expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1, 0);1073expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX);1074EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)1075.Times(AnyNumber())1076.WillRepeatedly(Invoke(ReturnErrno(ENOENT)));10771078ASSERT_EQ(-1, rename(FULLSRC, FULLDST));1079ASSERT_EQ(EACCES, errno);1080}10811082TEST_F(Rename, eacces_on_dstdir_for_creating)1083{1084const char FULLDST[] = "mountpoint/d/dst";1085const char RELDSTDIR[] = "d";1086const char RELDST[] = "dst";1087const char FULLSRC[] = "mountpoint/src";1088const char RELSRC[] = "src";1089uint64_t src_ino = 42;1090uint64_t dstdir_ino = 43;10911092expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);1093expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX);1094expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX);1095EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));10961097ASSERT_EQ(-1, rename(FULLSRC, FULLDST));1098ASSERT_EQ(EACCES, errno);1099}11001101TEST_F(Rename, eacces_on_dstdir_for_removing)1102{1103const char FULLDST[] = "mountpoint/d/dst";1104const char RELDSTDIR[] = "d";1105const char RELDST[] = "dst";1106const char FULLSRC[] = "mountpoint/src";1107const char RELSRC[] = "src";1108uint64_t src_ino = 42;1109uint64_t dstdir_ino = 43;11101111expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);1112expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX);1113expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0755, UINT64_MAX);1114EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));11151116ASSERT_EQ(-1, rename(FULLSRC, FULLDST));1117ASSERT_EQ(EACCES, errno);1118}11191120TEST_F(Rename, eperm_on_sticky_srcdir)1121{1122const char FULLDST[] = "mountpoint/d/dst";1123const char FULLSRC[] = "mountpoint/src";1124const char RELSRC[] = "src";1125uint64_t ino = 42;11261127expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0);1128expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX);11291130ASSERT_EQ(-1, rename(FULLSRC, FULLDST));1131ASSERT_EQ(EPERM, errno);1132}11331134/*1135* A user cannot move out a subdirectory that he does not own, because that1136* would require changing the subdirectory's ".." dirent1137*/1138TEST_F(Rename, eperm_for_subdirectory)1139{1140const char FULLDST[] = "mountpoint/d/dst";1141const char FULLSRC[] = "mountpoint/src";1142const char RELDSTDIR[] = "d";1143const char RELDST[] = "dst";1144const char RELSRC[] = "src";1145uint64_t ino = 42;1146uint64_t dstdir_ino = 43;11471148expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);1149expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0);1150expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 0777, UINT64_MAX, 0);1151EXPECT_LOOKUP(dstdir_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));11521153ASSERT_EQ(-1, rename(FULLSRC, FULLDST));1154ASSERT_EQ(EACCES, errno);1155}11561157/*1158* A user _can_ rename a subdirectory to which he lacks write permissions, if1159* it will keep the same parent1160*/1161TEST_F(Rename, subdirectory_to_same_dir)1162{1163const char FULLDST[] = "mountpoint/dst";1164const char FULLSRC[] = "mountpoint/src";1165const char RELDST[] = "dst";1166const char RELSRC[] = "src";1167uint64_t ino = 42;11681169expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);1170expect_lookup(RELSRC, ino, S_IFDIR | 0755, UINT64_MAX, 0);1171EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)1172.WillOnce(Invoke(ReturnErrno(ENOENT)));1173expect_rename(0);11741175ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);1176}11771178TEST_F(Rename, eperm_on_sticky_dstdir)1179{1180const char FULLDST[] = "mountpoint/d/dst";1181const char RELDSTDIR[] = "d";1182const char RELDST[] = "dst";1183const char FULLSRC[] = "mountpoint/src";1184const char RELSRC[] = "src";1185uint64_t src_ino = 42;1186uint64_t dstdir_ino = 43;1187uint64_t dst_ino = 44;11881189expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, 0);1190expect_lookup(RELSRC, src_ino, S_IFREG | 0644, UINT64_MAX);1191expect_lookup(RELDSTDIR, dstdir_ino, S_IFDIR | 01777, UINT64_MAX);1192EXPECT_LOOKUP(dstdir_ino, RELDST)1193.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {1194SET_OUT_HEADER_LEN(out, entry);1195out.body.entry.attr.mode = S_IFREG | 0644;1196out.body.entry.nodeid = dst_ino;1197out.body.entry.attr_valid = UINT64_MAX;1198out.body.entry.entry_valid = UINT64_MAX;1199out.body.entry.attr.uid = 0;1200})));12011202ASSERT_EQ(-1, rename(FULLSRC, FULLDST));1203ASSERT_EQ(EPERM, errno);1204}12051206/* Successfully rename a file, overwriting the destination */1207TEST_F(Rename, ok)1208{1209const char FULLDST[] = "mountpoint/dst";1210const char RELDST[] = "dst";1211const char FULLSRC[] = "mountpoint/src";1212const char RELSRC[] = "src";1213// The inode of the already-existing destination file1214uint64_t dst_ino = 2;1215uint64_t ino = 42;12161217expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1, geteuid());1218expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX);1219expect_lookup(RELDST, dst_ino, S_IFREG | 0644, UINT64_MAX);1220expect_rename(0);12211222ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);1223}12241225TEST_F(Rename, ok_to_remove_src_because_of_stickiness)1226{1227const char FULLDST[] = "mountpoint/dst";1228const char RELDST[] = "dst";1229const char FULLSRC[] = "mountpoint/src";1230const char RELSRC[] = "src";1231uint64_t ino = 42;12321233expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1, 0);1234expect_lookup(RELSRC, ino, S_IFREG | 0644, UINT64_MAX, geteuid());1235EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)1236.WillOnce(Invoke(ReturnErrno(ENOENT)));1237expect_rename(0);12381239ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);1240}12411242// Don't update atime during close after read, if we lack permissions to write1243// that file.1244TEST_F(Read, atime_during_close)1245{1246const char FULLPATH[] = "mountpoint/some_file.txt";1247const char RELPATH[] = "some_file.txt";1248uint64_t ino = 42;1249int fd;1250ssize_t bufsize = 100;1251uint8_t buf[bufsize];1252const char *CONTENTS = "abcdefgh";1253ssize_t fsize = sizeof(CONTENTS);12541255expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);1256FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0755, fsize,12571, UINT64_MAX, 0, 0);1258expect_open(ino, 0, 1);1259expect_read(ino, 0, fsize, fsize, CONTENTS);1260EXPECT_CALL(*m_mock, process(1261ResultOf([&](auto in) {1262return (in.header.opcode == FUSE_SETATTR);1263}, Eq(true)),1264_)1265).Times(0);1266expect_flush(ino, 1, ReturnErrno(0));1267expect_release(ino, FuseTest::FH);12681269fd = open(FULLPATH, O_RDONLY);1270ASSERT_LE(0, fd) << strerror(errno);12711272/* Ensure atime will be different than during lookup */1273nap();12741275ASSERT_EQ(fsize, read(fd, buf, bufsize)) << strerror(errno);12761277close(fd);1278}12791280TEST_F(Setattr, ok)1281{1282const char FULLPATH[] = "mountpoint/some_file.txt";1283const char RELPATH[] = "some_file.txt";1284const uint64_t ino = 42;1285const mode_t oldmode = 0755;1286const mode_t newmode = 0644;12871288expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);1289expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid());1290EXPECT_CALL(*m_mock, process(1291ResultOf([](auto in) {1292return (in.header.opcode == FUSE_SETATTR &&1293in.header.nodeid == ino &&1294in.body.setattr.mode == newmode);1295}, Eq(true)),1296_)1297).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {1298SET_OUT_HEADER_LEN(out, attr);1299out.body.attr.attr.mode = S_IFREG | newmode;1300})));13011302EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);1303}13041305TEST_F(Setattr, eacces)1306{1307const char FULLPATH[] = "mountpoint/some_file.txt";1308const char RELPATH[] = "some_file.txt";1309const uint64_t ino = 42;1310const mode_t oldmode = 0755;1311const mode_t newmode = 0644;13121313expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);1314expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, 0);1315EXPECT_CALL(*m_mock, process(1316ResultOf([](auto in) {1317return (in.header.opcode == FUSE_SETATTR);1318}, Eq(true)),1319_)1320).Times(0);13211322EXPECT_NE(0, chmod(FULLPATH, newmode));1323EXPECT_EQ(EPERM, errno);1324}13251326/*1327* ftruncate() of a file without writable permissions should succeed as long as1328* the file descriptor is writable. This is important when combined with1329* O_CREAT1330*/1331TEST_F(Setattr, ftruncate_of_newly_created_file)1332{1333const char FULLPATH[] = "mountpoint/some_file.txt";1334const char RELPATH[] = "some_file.txt";1335const uint64_t ino = 42;1336const mode_t mode = 0000;1337int fd;13381339expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);1340EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)1341.WillOnce(Invoke(ReturnErrno(ENOENT)));1342expect_create(RELPATH, ino);1343EXPECT_CALL(*m_mock, process(1344ResultOf([](auto in) {1345return (in.header.opcode == FUSE_SETATTR &&1346in.header.nodeid == ino &&1347(in.body.setattr.valid & FATTR_SIZE));1348}, Eq(true)),1349_)1350).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {1351SET_OUT_HEADER_LEN(out, attr);1352out.body.attr.attr.ino = ino;1353out.body.attr.attr.mode = S_IFREG | mode;1354out.body.attr.attr_valid = UINT64_MAX;1355})));13561357fd = open(FULLPATH, O_CREAT | O_RDWR, 0);1358ASSERT_LE(0, fd) << strerror(errno);1359ASSERT_EQ(0, ftruncate(fd, 100)) << strerror(errno);1360leak(fd);1361}13621363/*1364* Setting the sgid bit should fail for an unprivileged user who doesn't belong1365* to the file's group1366*/1367TEST_F(Setattr, sgid_by_non_group_member)1368{1369const char FULLPATH[] = "mountpoint/some_file.txt";1370const char RELPATH[] = "some_file.txt";1371const uint64_t ino = 42;1372const mode_t oldmode = 0755;1373const mode_t newmode = 02755;1374uid_t uid = geteuid();1375gid_t gid = excluded_group();13761377expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);1378expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, uid, gid);1379EXPECT_CALL(*m_mock, process(1380ResultOf([](auto in) {1381return (in.header.opcode == FUSE_SETATTR);1382}, Eq(true)),1383_)1384).Times(0);13851386EXPECT_NE(0, chmod(FULLPATH, newmode));1387EXPECT_EQ(EPERM, errno);1388}13891390/* Only the superuser may set the sticky bit on a non-directory */1391TEST_F(Setattr, sticky_regular_file)1392{1393const char FULLPATH[] = "mountpoint/some_file.txt";1394const char RELPATH[] = "some_file.txt";1395const uint64_t ino = 42;1396const mode_t oldmode = 0644;1397const mode_t newmode = 01644;13981399expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);1400expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX, geteuid());1401EXPECT_CALL(*m_mock, process(1402ResultOf([](auto in) {1403return (in.header.opcode == FUSE_SETATTR);1404}, Eq(true)),1405_)1406).Times(0);14071408EXPECT_NE(0, chmod(FULLPATH, newmode));1409EXPECT_EQ(EFTYPE, errno);1410}14111412TEST_F(Setextattr, ok)1413{1414const char FULLPATH[] = "mountpoint/some_file.txt";1415const char RELPATH[] = "some_file.txt";1416uint64_t ino = 42;1417const char value[] = "whatever";1418ssize_t value_len = strlen(value) + 1;1419int ns = EXTATTR_NAMESPACE_USER;1420ssize_t r;14211422expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);1423expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());1424expect_setxattr(0);14251426r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,1427value_len);1428ASSERT_EQ(value_len, r) << strerror(errno);1429}14301431TEST_F(Setextattr, eacces)1432{1433const char FULLPATH[] = "mountpoint/some_file.txt";1434const char RELPATH[] = "some_file.txt";1435uint64_t ino = 42;1436const char value[] = "whatever";1437ssize_t value_len = strlen(value) + 1;1438int ns = EXTATTR_NAMESPACE_USER;14391440expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);1441expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);14421443ASSERT_EQ(-1, extattr_set_file(FULLPATH, ns, "foo", (const void*)value,1444value_len));1445ASSERT_EQ(EACCES, errno);1446}14471448// Setting system attributes requires superuser privileges1449TEST_F(Setextattr, system)1450{1451const char FULLPATH[] = "mountpoint/some_file.txt";1452const char RELPATH[] = "some_file.txt";1453uint64_t ino = 42;1454const char value[] = "whatever";1455ssize_t value_len = strlen(value) + 1;1456int ns = EXTATTR_NAMESPACE_SYSTEM;14571458expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);1459expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, geteuid());14601461ASSERT_EQ(-1, extattr_set_file(FULLPATH, ns, "foo", (const void*)value,1462value_len));1463ASSERT_EQ(EPERM, errno);1464}14651466// Setting user attributes merely requires write privileges1467TEST_F(Setextattr, user)1468{1469const char FULLPATH[] = "mountpoint/some_file.txt";1470const char RELPATH[] = "some_file.txt";1471uint64_t ino = 42;1472const char value[] = "whatever";1473ssize_t value_len = strlen(value) + 1;1474int ns = EXTATTR_NAMESPACE_USER;1475ssize_t r;14761477expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);1478expect_lookup(RELPATH, ino, S_IFREG | 0666, UINT64_MAX, 0);1479expect_setxattr(0);14801481r = extattr_set_file(FULLPATH, ns, "foo", (const void*)value,1482value_len);1483ASSERT_EQ(value_len, r) << strerror(errno);1484}14851486TEST_F(Unlink, ok)1487{1488const char FULLPATH[] = "mountpoint/some_file.txt";1489const char RELPATH[] = "some_file.txt";1490uint64_t ino = 42;1491sem_t sem;14921493ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);14941495expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0777, UINT64_MAX, 1);1496expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());1497expect_unlink(FUSE_ROOT_ID, RELPATH, 0);1498expect_forget(ino, 1, &sem);14991500ASSERT_EQ(0, unlink(FULLPATH)) << strerror(errno);15011502sem_wait(&sem);1503sem_destroy(&sem);1504}15051506/*1507* Ensure that a cached name doesn't cause unlink to bypass permission checks1508* in VOP_LOOKUP.1509*1510* This test should pass because lookup(9) purges the namecache entry by doing1511* a vfs_cache_lookup with ~MAKEENTRY when nameiop == DELETE.1512*/1513TEST_F(Unlink, cached_unwritable_directory)1514{1515const char FULLPATH[] = "mountpoint/some_file.txt";1516const char RELPATH[] = "some_file.txt";1517uint64_t ino = 42;15181519expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);1520EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)1521.Times(AnyNumber())1522.WillRepeatedly(Invoke(1523ReturnImmediate([=](auto i __unused, auto& out) {1524SET_OUT_HEADER_LEN(out, entry);1525out.body.entry.attr.mode = S_IFREG | 0644;1526out.body.entry.nodeid = ino;1527out.body.entry.entry_valid = UINT64_MAX;1528}))1529);15301531/* Fill name cache */1532ASSERT_EQ(0, access(FULLPATH, F_OK)) << strerror(errno);1533/* Despite cached name , unlink should fail */1534ASSERT_EQ(-1, unlink(FULLPATH));1535ASSERT_EQ(EACCES, errno);1536}15371538TEST_F(Unlink, unwritable_directory)1539{1540const char FULLPATH[] = "mountpoint/some_file.txt";1541const char RELPATH[] = "some_file.txt";1542uint64_t ino = 42;15431544expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);1545expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, geteuid());15461547ASSERT_EQ(-1, unlink(FULLPATH));1548ASSERT_EQ(EACCES, errno);1549}15501551TEST_F(Unlink, sticky_directory)1552{1553const char FULLPATH[] = "mountpoint/some_file.txt";1554const char RELPATH[] = "some_file.txt";1555uint64_t ino = 42;15561557expect_getattr(FUSE_ROOT_ID, S_IFDIR | 01777, UINT64_MAX, 1);1558expect_lookup(RELPATH, ino, S_IFREG | 0644, UINT64_MAX, 0);15591560ASSERT_EQ(-1, unlink(FULLPATH));1561ASSERT_EQ(EPERM, errno);1562}15631564/* A write by a non-owner should clear a file's SUID bit */1565TEST_F(Write, clear_suid)1566{1567const char FULLPATH[] = "mountpoint/some_file.txt";1568const char RELPATH[] = "some_file.txt";1569struct stat sb;1570uint64_t ino = 42;1571mode_t oldmode = 04777;1572mode_t newmode = 0777;1573char wbuf[1] = {'x'};1574int fd;15751576expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);1577expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX);1578expect_open(ino, 0, 1);1579expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf);1580expect_chmod(ino, newmode, sizeof(wbuf));15811582fd = open(FULLPATH, O_WRONLY);1583ASSERT_LE(0, fd) << strerror(errno);1584ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno);1585ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);1586EXPECT_EQ(S_IFREG | newmode, sb.st_mode);1587leak(fd);1588}15891590/* A write by a non-owner should clear a file's SGID bit */1591TEST_F(Write, clear_sgid)1592{1593const char FULLPATH[] = "mountpoint/some_file.txt";1594const char RELPATH[] = "some_file.txt";1595struct stat sb;1596uint64_t ino = 42;1597mode_t oldmode = 02777;1598mode_t newmode = 0777;1599char wbuf[1] = {'x'};1600int fd;16011602expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);1603expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX);1604expect_open(ino, 0, 1);1605expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf);1606expect_chmod(ino, newmode, sizeof(wbuf));16071608fd = open(FULLPATH, O_WRONLY);1609ASSERT_LE(0, fd) << strerror(errno);1610ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno);1611ASSERT_EQ(0, fstat(fd, &sb)) << strerror(errno);1612EXPECT_EQ(S_IFREG | newmode, sb.st_mode);1613leak(fd);1614}16151616/* Regression test for a specific recurse-of-nonrecursive-lock panic1617*1618* With writeback caching, we can't call vtruncbuf from fuse_io_strategy, or it1619* may panic. That happens if the FUSE_SETATTR response indicates that the1620* file's size has changed since the write.1621*/1622TEST_F(Write, recursion_panic_while_clearing_suid)1623{1624const char FULLPATH[] = "mountpoint/some_file.txt";1625const char RELPATH[] = "some_file.txt";1626uint64_t ino = 42;1627mode_t oldmode = 04777;1628mode_t newmode = 0777;1629char wbuf[1] = {'x'};1630int fd;16311632expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755, UINT64_MAX, 1);1633expect_lookup(RELPATH, ino, S_IFREG | oldmode, UINT64_MAX);1634expect_open(ino, 0, 1);1635expect_write(ino, 0, sizeof(wbuf), sizeof(wbuf), 0, 0, wbuf);1636/* XXX Return a smaller file size than what we just wrote! */1637expect_chmod(ino, newmode, 0);16381639fd = open(FULLPATH, O_WRONLY);1640ASSERT_LE(0, fd) << strerror(errno);1641ASSERT_EQ(1, write(fd, wbuf, sizeof(wbuf))) << strerror(errno);1642leak(fd);1643}164416451646