Files
2026-08-11 09:56:10 -07:00

1320 lines
47 KiB
C++

// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <errno.h>
#include <fcntl.h>
#include <linux/capability.h>
#include <stdint.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/sysmacros.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <time.h>
#include <unistd.h>
#include <cerrno>
#include <string>
#include <vector>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "test/util/cleanup.h"
#include "test/util/file_descriptor.h"
#include "test/util/fs_util.h"
#include "test/util/linux_capability_util.h"
#include "test/util/mount_util.h"
#include "test/util/posix_error.h"
#include "test/util/temp_path.h"
#include "test/util/test_util.h"
#include "test/util/thread_util.h"
namespace gvisor {
namespace testing {
namespace {
// Extended attribute names for POSIX ACLs.
constexpr char kAccessACL[] = "system.posix_acl_access";
constexpr char kDefaultACL[] = "system.posix_acl_default";
// POSIX ACL constants.
constexpr uint32_t kACLVersion = 2;
constexpr uint16_t kUserObj = 0x01;
constexpr uint16_t kUser = 0x02;
constexpr uint16_t kGroupObj = 0x04;
constexpr uint16_t kGroup = 0x08;
constexpr uint16_t kMask = 0x10;
constexpr uint16_t kOther = 0x20;
constexpr uint32_t kUndef = 0xffffffff;
// Permission bits.
constexpr uint16_t kR = 0x04;
constexpr uint16_t kW = 0x02;
constexpr uint16_t kX = 0x01;
constexpr uid_t kNobody = 65534;
// ACLEntry mirrors struct posix_acl_xattr_entry.
struct ACLEntry {
uint16_t tag;
uint16_t perm;
uint32_t id;
};
static_assert(sizeof(ACLEntry) == 8, "unexpected ACLEntry size");
ACLEntry Ent(int tag, int perm, uint32_t id) {
return ACLEntry{static_cast<uint16_t>(tag), static_cast<uint16_t>(perm), id};
}
// BuildACL builds the raw xattr representation for a POSIX ACL.
std::string BuildACL(const std::vector<ACLEntry>& entries) {
uint32_t version = kACLVersion;
std::string buf(reinterpret_cast<const char*>(&version), sizeof(version));
for (const ACLEntry& e : entries) {
buf.append(reinterpret_cast<const char*>(&e), sizeof(e));
}
return buf;
}
// ParseACL parses a raw ACL value into its entries.
std::vector<ACLEntry> ParseACL(const std::string& blob) {
std::vector<ACLEntry> out;
for (size_t off = sizeof(uint32_t); off + sizeof(ACLEntry) <= blob.size();
off += sizeof(ACLEntry)) {
ACLEntry e;
memcpy(&e, blob.data() + off, sizeof(e));
out.push_back(e);
}
return out;
}
// FindEntryPerm returns the permission of the first entry matching tag (and id
// for named user/group tags), or -1 if not found.
int FindEntryPerm(const std::string& blob, uint16_t tag, uint32_t id) {
for (const ACLEntry& e : ParseACL(blob)) {
if (e.tag != tag) continue;
if ((tag == kUser || tag == kGroup) && e.id != id) continue;
return e.perm;
}
return -1;
}
// GetXattrString reads an xattr into a string.
PosixErrorOr<std::string> GetXattrString(const std::string& path,
const char* name) {
char buf[512];
int n = getxattr(path.c_str(), name, buf, sizeof(buf));
if (n < 0) {
return PosixError(errno, "getxattr");
}
return std::string(buf, n);
}
// ListContainsName reports whether a listxattr(2) result contains name.
bool ListContainsName(const char* list, int len, const char* name) {
for (int i = 0; i < len; i += strlen(list + i) + 1) {
if (strcmp(list + i, name) == 0) {
return true;
}
}
return false;
}
// Backend selects the filesystem the ACL tests run against.
//
// When support for POSIX ACLs is added to a new filesystem, it should be added
// here.
enum class Backend {
kTmpfs, // plain tmpfs
kOverlayUpper, // an overlay whose object is created in the upper layer
kOverlayLower, // an overlay whose object starts in the lower layer and is
// copied up on the first modifying operation
};
class PosixACLTest : public ::testing::TestWithParam<Backend> {
protected:
void SetUp() override {
// Use /dev/shm to allow tmpfs tests to run without privilege.
base_ = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn("/dev/shm"));
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(IsTmpfs(base_.path())));
const auto param = GetParam();
switch (param) {
case Backend::kTmpfs:
dir_ = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(base_.path()));
break;
case Backend::kOverlayUpper:
case Backend::kOverlayLower:
MountOverlay(param);
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(IsOverlayfs(dir_.path())));
}
file_ = JoinPath(dir_.path(), "posix_acl_test_file");
subdir_ = JoinPath(dir_.path(), "subdir");
sub_subdir_ = JoinPath(subdir_, "subsubdir");
// For the lower-layer case the objects were already seeded in the lower
// directory by MountOverlay(); the first modifying syscall in each test
// will copy them up. Otherwise create them now: directly on tmpfs, or
// through the merged mount (landing in the upper layer).
if (GetParam() != Backend::kOverlayLower) {
ASSERT_NO_ERRNO_AND_VALUE(Open(file_, O_CREAT | O_RDWR, 0644));
ASSERT_THAT(chmod(file_.c_str(), 0644), SyscallSucceeds());
ASSERT_THAT(mkdir(subdir_.c_str(), 0755), SyscallSucceeds());
ASSERT_THAT(mkdir(sub_subdir_.c_str(), 0755), SyscallSucceeds());
}
uid_ = getuid();
gid_ = getgid();
}
// MountOverlay mounts an overlay at dir_.
void MountOverlay(const ParamType param) {
// Mounting an overlay requires CAP_SYS_ADMIN
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
// gVisor doesn't accept /dev/shm as an overlayfs layer. We need
// CAP_SYS_ADMIN for this case anyways, so just mount a separate tmpfs.
layers_ = JoinPath(base_.path(), "layers");
ASSERT_THAT(mkdir(layers_.c_str(), 0755), SyscallSucceeds());
ASSERT_THAT(mount("tmpfs", layers_.c_str(), "tmpfs", 0, "mode=0755"),
SyscallSucceeds());
tmpfs_mounted_ = true;
const std::string lower = JoinPath(layers_, "lower");
const std::string upper = JoinPath(layers_, "upper");
const std::string work = JoinPath(layers_, "work");
for (const std::string& d : {lower, upper, work}) {
ASSERT_THAT(mkdir(d.c_str(), 0755), SyscallSucceeds());
}
// dir_ is the merged mount point.
dir_ = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(base_.path()));
if (param == Backend::kOverlayLower) {
const std::string lfile = JoinPath(lower, "posix_acl_test_file");
ASSERT_NO_ERRNO_AND_VALUE(Open(lfile, O_CREAT | O_RDWR, 0644));
ASSERT_THAT(chmod(lfile.c_str(), 0644), SyscallSucceeds());
ASSERT_THAT(mkdir(JoinPath(lower, "subdir").c_str(), 0755),
SyscallSucceeds());
ASSERT_THAT(mkdir(JoinPath(lower, "subdir/subsubdir").c_str(), 0755),
SyscallSucceeds());
}
const std::string opts =
"lowerdir=" + lower + ",upperdir=" + upper + ",workdir=" + work;
ASSERT_THAT(
mount("overlay", dir_.path().c_str(), "overlay", 0, opts.c_str()),
SyscallSucceeds());
overlay_mounted_ = true;
}
void TearDown() override {
// Unmount inner-to-outer, before the TempPath destructors remove the (now
// empty) mount points.
if (overlay_mounted_) {
EXPECT_THAT(umount2(dir_.path().c_str(), MNT_DETACH), SyscallSucceeds());
}
if (tmpfs_mounted_) {
EXPECT_THAT(umount2(layers_.c_str(), MNT_DETACH), SyscallSucceeds());
}
}
TempPath base_;
TempPath dir_;
std::string layers_;
bool tmpfs_mounted_ = false;
bool overlay_mounted_ = false;
std::string file_;
std::string subdir_;
std::string sub_subdir_;
uid_t uid_;
gid_t gid_;
};
// Setting an access ACL and reading it back returns an identical blob.
TEST_P(PosixACLTest, SetGetAccessACL) {
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kGroup, kR | kW, gid_),
Ent(kMask, kR | kW, kUndef),
Ent(kOther, kR, kUndef),
});
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
const std::string got =
ASSERT_NO_ERRNO_AND_VALUE(GetXattrString(file_, kAccessACL));
EXPECT_EQ(got, acl);
}
// getxattr reports the size when passed a zero-length buffer.
TEST_P(PosixACLTest, GetAccessACLSize) {
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR | kW, kUndef),
Ent(kOther, kR, kUndef),
});
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
EXPECT_THAT(getxattr(file_.c_str(), kAccessACL, nullptr, 0),
SyscallSucceedsWithValue(acl.size()));
}
// getxattr on a file with no ACL returns ENODATA.
TEST_P(PosixACLTest, GetAccessACLNoData) {
EXPECT_THAT(getxattr(file_.c_str(), kAccessACL, nullptr, 0),
SyscallFailsWithErrno(ENODATA));
}
// Setting an extended access ACL updates the file mode: the owner/other bits
// come from USER_OBJ/OTHER, and the group bits reflect the mask.
TEST_P(PosixACLTest, AccessACLUpdatesMode) {
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef), // owner rw-
Ent(kUser, kR, uid_), //
Ent(kGroupObj, kR, kUndef), // group_obj r--
Ent(kMask, kR | kW | kX, kUndef), // mask rwx (surfaces as group bits)
Ent(kOther, kR, kUndef), // other r--
});
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
struct stat st = {};
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
EXPECT_EQ(st.st_mode & 0777, 0674);
}
// A minimal ACL (only the three base entries, no mask/named entries) is
// equivalent to a mode: it is folded into the mode and not stored.
TEST_P(PosixACLTest, MinimalAccessACLFoldsToMode) {
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW | kX, kUndef),
Ent(kGroupObj, kR | kX, kUndef),
Ent(kOther, kR, kUndef),
});
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
struct stat st = {};
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
EXPECT_EQ(st.st_mode & 0777, 0754);
// No extended ACL is stored.
EXPECT_THAT(getxattr(file_.c_str(), kAccessACL, nullptr, 0),
SyscallFailsWithErrno(ENODATA));
}
// An extended access ACL appears in listxattr; removing it makes it disappear.
TEST_P(PosixACLTest, ListAndRemoveAccessACL) {
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
char list[512];
int n = listxattr(file_.c_str(), list, sizeof(list));
ASSERT_THAT(n, SyscallSucceeds());
EXPECT_TRUE(ListContainsName(list, n, kAccessACL));
ASSERT_THAT(removexattr(file_.c_str(), kAccessACL), SyscallSucceeds());
EXPECT_THAT(getxattr(file_.c_str(), kAccessACL, nullptr, 0),
SyscallFailsWithErrno(ENODATA));
n = listxattr(file_.c_str(), list, sizeof(list));
ASSERT_THAT(n, SyscallSucceeds());
EXPECT_FALSE(ListContainsName(list, n, kAccessACL));
}
// A default ACL cannot be set on a non-directory.
TEST_P(PosixACLTest, DefaultACLOnFileFails) {
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kGroupObj, kR, kUndef),
Ent(kOther, kR, kUndef),
});
EXPECT_THAT(setxattr(file_.c_str(), kDefaultACL, acl.data(), acl.size(), 0),
SyscallFailsWithErrno(EACCES));
}
// A default ACL can be set on and read back from a directory, and appears in
// listxattr.
TEST_P(PosixACLTest, SetGetDefaultACLOnDir) {
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW | kX, kUndef),
Ent(kUser, kR | kX, uid_),
Ent(kGroupObj, kR | kX, kUndef),
Ent(kMask, kR | kX, kUndef),
Ent(kOther, kR | kX, kUndef),
});
ASSERT_THAT(setxattr(subdir_.c_str(), kDefaultACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
const std::string got =
ASSERT_NO_ERRNO_AND_VALUE(GetXattrString(subdir_, kDefaultACL));
EXPECT_EQ(got, acl);
char list[512];
int n = listxattr(subdir_.c_str(), list, sizeof(list));
ASSERT_THAT(n, SyscallSucceeds());
EXPECT_TRUE(ListContainsName(list, n, kDefaultACL));
}
// A file created in a directory with a default ACL inherits an access ACL
// derived from that default ACL. A subdirectory also inherits the default ACL.
TEST_P(PosixACLTest, DefaultACLInheritance) {
const std::string dacl = BuildACL({
Ent(kUserObj, kR | kW | kX, kUndef),
Ent(kUser, kR | kX, uid_),
Ent(kGroupObj, kR | kX, kUndef),
Ent(kMask, kR | kW | kX, kUndef),
Ent(kOther, kR | kX, kUndef),
});
ASSERT_THAT(
setxattr(subdir_.c_str(), kDefaultACL, dacl.data(), dacl.size(), 0),
SyscallSucceeds());
// A regular child inherits an access ACL that names the user, but no default
// ACL.
const std::string child = JoinPath(subdir_, "child");
{
ASSERT_NO_ERRNO_AND_VALUE(Open(child, O_CREAT | O_RDWR, 0666));
}
const std::string cacl =
ASSERT_NO_ERRNO_AND_VALUE(GetXattrString(child, kAccessACL));
EXPECT_EQ(FindEntryPerm(cacl, kUser, uid_), kR | kX)
<< "child access ACL should inherit the named user from the default ACL";
EXPECT_THAT(getxattr(child.c_str(), kDefaultACL, nullptr, 0),
SyscallFailsWithErrno(ENODATA));
// A child directory inherits the default ACL verbatim as its own default ACL.
const std::string childdir = JoinPath(subdir_, "childdir");
ASSERT_THAT(mkdir(childdir.c_str(), 0777), SyscallSucceeds());
const std::string cdacl =
ASSERT_NO_ERRNO_AND_VALUE(GetXattrString(childdir, kDefaultACL));
EXPECT_EQ(cdacl, dacl);
}
// When a directory in lower with no default ACL, but whose parent has since
// been given a default ACL in upper, is copied up, it should *not* inherit
// the upper parent's default ACL.
//
// As a result, children created in the directory should not inherit the
// parent's default ACL.
TEST_P(PosixACLTest, CopyUpDirectoryDoesNotInheritDefaultACL) {
// Only valid for overlayfs-lower.
SKIP_IF(GetParam() != Backend::kOverlayLower);
// First, copy-up the dir's parent with a default ACL.
const std::string dacl = BuildACL({
Ent(kUserObj, kR | kW | kX, kUndef),
Ent(kUser, kR | kX, uid_),
Ent(kGroupObj, kR | kX, kUndef),
Ent(kMask, kR | kW | kX, kUndef),
Ent(kOther, kR | kX, kUndef),
});
ASSERT_THAT(
setxattr(subdir_.c_str(), kDefaultACL, dacl.data(), dacl.size(), 0),
SyscallSucceeds());
// Trigger copy-up of the directory by creating a file in it.
const std::string child_file = JoinPath(sub_subdir_, "child_file");
ASSERT_NO_ERRNO_AND_VALUE(Open(child_file, O_CREAT | O_RDWR, 0666));
// The directory should not get an ACL in upper after copy-up.
EXPECT_THAT(getxattr(sub_subdir_.c_str(), kAccessACL, nullptr, 0),
SyscallFailsWithErrno(ENODATA));
EXPECT_THAT(getxattr(sub_subdir_.c_str(), kDefaultACL, nullptr, 0),
SyscallFailsWithErrno(ENODATA));
// Existing file children should not get a spurious access ACL.
EXPECT_THAT(getxattr(child_file.c_str(), kAccessACL, nullptr, 0),
SyscallFailsWithErrno(ENODATA));
// A new subdirectory should not get a spurious ACL (access or default).
const std::string child_dir = JoinPath(sub_subdir_, "child_dir");
ASSERT_THAT(mkdir(child_dir.c_str(), 0777), SyscallSucceeds());
EXPECT_THAT(getxattr(child_dir.c_str(), kAccessACL, nullptr, 0),
SyscallFailsWithErrno(ENODATA));
EXPECT_THAT(getxattr(child_dir.c_str(), kDefaultACL, nullptr, 0),
SyscallFailsWithErrno(ENODATA));
}
// Removing a POSIX ACL on an overlay-lower file that has not been copied-up
// and has no POSIX ACL should not result in the file being copied up; instead,
// it should return ENODATA.
TEST_P(PosixACLTest, RemoveACLOnFileNotCopiedUp) {
// Only valid for overlayfs-lower.
SKIP_IF(GetParam() != Backend::kOverlayLower);
KernelVersion version = ASSERT_NO_ERRNO_AND_VALUE(GetKernelVersion());
if (version.major > 6 || (version.major == 6 && version.minor >= 2)) {
// Setting an empty string "" xattr for a POSIX ACL only clears the ACL on
// kernel >=6.2. Older kernels give EINVAL.
EXPECT_THAT(setxattr(file_.c_str(), kAccessACL, "", 0, 0),
SyscallFailsWithErrno(ENODATA));
}
EXPECT_THAT(removexattr(file_.c_str(), kAccessACL),
SyscallFailsWithErrno(ENODATA));
// Removing a *default* ACL should succeed (but still skip copy-up)
EXPECT_THAT(removexattr(file_.c_str(), kDefaultACL), SyscallSucceeds());
EXPECT_THAT(removexattr(file_.c_str(), kAccessACL),
SyscallFailsWithErrno(ENODATA));
}
// Regression test for overlayfs not setting the mode on copy-up.
//
// Lower has dir D (0755, no ACLs) containing char device N (0666, no ACL).
// Through the overlay, set a default ACL on D with a restrictive mask. Upper D
// now carries that default ACL.
//
// If N is copied-up for any reason, the default ACL should *not* affect its
// mode in the upper.
TEST_P(PosixACLTest, DeviceFileModeUnaffectedByPostCopyUpParentDefaultACL) {
// Only valid for overlayfs-lower.
SKIP_IF(GetParam() != Backend::kOverlayLower);
// CAP_MKNOD necessary to make a device file
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_MKNOD)));
const auto dirLower = JoinPath(layers_, "lower", "subdir");
ASSERT_THAT(chmod(dirLower.c_str(), 0755), SyscallSucceeds());
// Make a null S_IFCHR device file
const auto pathLower = JoinPath(dirLower, "null");
const dev_t dev = makedev(1, 3);
ASSERT_THAT(mknod(pathLower.c_str(), S_IFCHR | 0666, dev), SyscallSucceeds());
ASSERT_THAT(chmod(pathLower.c_str(), 0666),
SyscallSucceeds()); // adjust for possible umask
// Sanity check that the resulting device file mode was indeed 0666
const auto path = JoinPath(subdir_, "null");
struct stat st = {};
ASSERT_THAT(stat(path.c_str(), &st), SyscallSucceeds());
ASSERT_EQ(st.st_mode & 0777, 0666);
// Set a default ACL with a restrictive mask on dir, through the overlay
// (triggering copy-up)
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR | kW, uid_),
Ent(kGroupObj, 0, kUndef),
Ent(kMask, 0, kUndef),
Ent(kOther, kR, kUndef),
});
ASSERT_THAT(setxattr(subdir_.c_str(), kDefaultACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
// Trigger a copy-up of the device file
ASSERT_THAT(chown(path.c_str(), kNobody, kNobody), SyscallSucceeds());
// The device file's mode should be unchanged
ASSERT_THAT(stat(path.c_str(), &st), SyscallSucceeds());
ASSERT_EQ(st.st_mode & 0777, 0666);
}
// chmod on a file with an extended ACL updates USER_OBJ, the mask, and OTHER
// (not the GROUP_OBJ entry).
TEST_P(PosixACLTest, ChmodUpdate) {
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR | kW, uid_),
Ent(kGroupObj, kR, kUndef), // group_obj r--
Ent(kMask, kR, kUndef), // mask r--
Ent(kOther, kR, kUndef),
});
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
// Update u:r, g:rw, o:0.
ASSERT_THAT(chmod(file_.c_str(), 0460), SyscallSucceeds());
const std::string got =
ASSERT_NO_ERRNO_AND_VALUE(GetXattrString(file_, kAccessACL));
// USER_OBJ, MASK, and OTHER should have been updated.
EXPECT_EQ(FindEntryPerm(got, kUserObj, kUndef), kR);
EXPECT_EQ(FindEntryPerm(got, kMask, kUndef), kR | kW);
EXPECT_EQ(FindEntryPerm(got, kOther, kUndef), 0);
// GROUP_OBJ should have b leave GROUP_OBJ untouched.
EXPECT_EQ(FindEntryPerm(got, kGroupObj, kUndef), kR);
// The mode should be 460 (as we set).
struct stat st = {};
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
EXPECT_EQ(st.st_mode & 0777, 0460);
}
// A named-user ACL entry grants access that the mode bits alone would deny, and
// the mask caps that access.
TEST_P(PosixACLTest, NamedUserEnforcement) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETUID)));
// Without an ACL granting access, "nobody" cannot read a 0600 file.
ASSERT_THAT(chmod(file_.c_str(), 0600), SyscallSucceeds());
{
ScopedThread t([&] {
EXPECT_THAT(syscall(SYS_setuid, kNobody), SyscallSucceeds());
EXPECT_THAT(open(file_.c_str(), O_RDONLY), SyscallFailsWithErrno(EACCES));
});
}
// Grant "nobody" read via a named-user entry; the mask allows only read.
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR | kW | kX, kNobody), // capped by the mask
Ent(kGroupObj, 0, kUndef),
Ent(kMask, kR, kUndef), // mask r-- : nobody effectively gets r-- only
Ent(kOther, 0, kUndef),
});
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
ScopedThread([&] {
EXPECT_THAT(syscall(SYS_setuid, kNobody), SyscallSucceeds());
// Read is granted by the ACL.
int rfd = open(file_.c_str(), O_RDONLY);
EXPECT_THAT(rfd, SyscallSucceeds());
if (rfd >= 0) close(rfd);
// Write is denied because the mask limits nobody to read.
int wfd = open(file_.c_str(), O_WRONLY);
EXPECT_THAT(wfd, SyscallFailsWithErrno(EACCES));
if (wfd >= 0) close(wfd);
});
}
// A named-group ACL entry grants access to a process in that group that the
// mode bits alone would deny, capped by the mask.
TEST_P(PosixACLTest, NamedGroupEnforcement) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETUID)));
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETGID)));
// Grant group "nobody" read+write, but the mask caps the group class to read.
// Owner keeps rw; group_obj and other get nothing.
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kGroupObj, 0, kUndef),
Ent(kGroup, kR | kW, kNobody), // capped by the mask
Ent(kMask, kR, kUndef), // mask r-- : group class limited to read
Ent(kOther, 0, kUndef),
});
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
ScopedThread([&] {
// Become a member of only the named group and a non-owner user.
EXPECT_THAT(syscall(SYS_setgroups, 0, nullptr), SyscallSucceeds());
EXPECT_THAT(syscall(SYS_setgid, kNobody), SyscallSucceeds());
EXPECT_THAT(syscall(SYS_setuid, kNobody), SyscallSucceeds());
// Read is granted via the named group.
int rfd = open(file_.c_str(), O_RDONLY);
EXPECT_THAT(rfd, SyscallSucceeds());
if (rfd >= 0) close(rfd);
// Write is denied due to the mask.
int wfd = open(file_.c_str(), O_WRONLY);
EXPECT_THAT(wfd, SyscallFailsWithErrno(EACCES));
if (wfd >= 0) close(wfd);
});
}
// A restrictive USER_OBJ locks the user out of their own file, even
// if group/other would otherwise grant the access.
TEST_P(PosixACLTest, UserObjLocksSelfOutEnforcement) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETUID)));
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETGID)));
// Drop special capabilities if present
AutoCapability dacOverride(CAP_DAC_OVERRIDE, false);
AutoCapability dacReadSearch(CAP_DAC_READ_SEARCH, false);
// Read succeeds by default.
int rfd = open(file_.c_str(), O_RDONLY);
EXPECT_THAT(rfd, SyscallSucceeds());
// New ACL: everybody has read-only access, except USER_OBJ which has none.
const std::string acl = BuildACL({
Ent(kUserObj, 0, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
// Read now fails.
rfd = open(file_.c_str(), O_RDONLY);
EXPECT_THAT(rfd, SyscallFailsWithErrno(EACCES));
}
// A restrictive named USER locks the user out of their own file, even
// if group/other would otherwise grant the access.
TEST_P(PosixACLTest, NamedUserLocksSelfOutEnforcement) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETUID)));
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETGID)));
// Drop special capabilities if present
AutoCapability dacOverride(CAP_DAC_OVERRIDE, false);
AutoCapability dacReadSearch(CAP_DAC_READ_SEARCH, false);
// Read succeeds by default.
int rfd = open(file_.c_str(), O_RDONLY);
EXPECT_THAT(rfd, SyscallSucceeds());
// Make the file owned by uid kNobody.
ASSERT_THAT(chown(file_.c_str(), kNobody, gid_), SyscallSucceeds());
// New ACL: everybody has read-only access, except for USER uid_.
const std::string acl = BuildACL({
Ent(kUserObj, kR, kUndef),
Ent(kUser, 0, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
// Read now fails.
rfd = open(file_.c_str(), O_RDONLY);
EXPECT_THAT(rfd, SyscallFailsWithErrno(EACCES));
}
// Only the file owner (or a suitably privileged process) may set an ACL, even
// with write permission on the file.
TEST_P(PosixACLTest, SetACLRequiresOwnership) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETUID)));
// World-writable file owned by the (root) test process.
ASSERT_THAT(chmod(file_.c_str(), 0666), SyscallSucceeds());
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, kNobody),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
ScopedThread([&] {
EXPECT_THAT(syscall(SYS_setuid, kNobody), SyscallSucceeds());
EXPECT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallFailsWithErrno(EPERM));
});
}
// An exception to the above case: default ACLs on non-directories can be
// "cleared" by anyone.
TEST_P(PosixACLTest, ClearDefaultACLNonOwner) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETUID)));
// File owned by the test process with no permissions granted to
// anyone else.
ASSERT_THAT(chmod(file_.c_str(), 0600), SyscallSucceeds());
ScopedThread([&] {
EXPECT_THAT(syscall(SYS_setuid, kNobody), SyscallSucceeds());
EXPECT_THAT(removexattr(file_.c_str(), kDefaultACL), SyscallSucceeds());
});
}
// POSIX access ACLs should work properly with symlinks.
TEST_P(PosixACLTest, SetAccessACLSymlink) {
// Create a symlink to a file
auto sym = JoinPath(dir_.path(), "posix_acl_test_symlink");
ASSERT_THAT(symlink(file_.c_str(), sym.c_str()), SyscallSucceeds());
auto cleanup = Cleanup([&sym] { unlink(sym.c_str()); });
// Setting an ACL *through* the symlink should succeed.
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
EXPECT_THAT(setxattr(sym.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
// Fetching an ACL *through* the symlink should succeed.
EXPECT_THAT(getxattr(sym.c_str(), kAccessACL, nullptr, 0), SyscallSucceeds());
// Removing an ACL *through* the symlink should succeed.
EXPECT_THAT(removexattr(sym.c_str(), kAccessACL), SyscallSucceeds());
// Setting an ACL *on* the symlink itself should fail.
EXPECT_THAT(lsetxattr(sym.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallFailsWithErrno(EOPNOTSUPP));
// Fetching the ACL *of* the symlink itself should also fail.
EXPECT_THAT(lgetxattr(sym.c_str(), kAccessACL, nullptr, 0),
SyscallFailsWithErrno(EOPNOTSUPP));
// Removing an ACL *of* the symlink itself should also fail.
EXPECT_THAT(syscall(SYS_lremovexattr, sym.c_str(), kAccessACL, nullptr, 0),
SyscallFailsWithErrno(EOPNOTSUPP));
}
// POSIX default ACLs should work properly with symlinks.
TEST_P(PosixACLTest, SetDefaultACLSymlink) {
// Create a symlink to a directory
auto sym = JoinPath(dir_.path(), "posix_acl_test_symlink");
ASSERT_THAT(symlink(dir_.path().c_str(), sym.c_str()), SyscallSucceeds());
auto cleanup = Cleanup([&sym] { unlink(sym.c_str()); });
// Setting an ACL *through* the symlink should succeed.
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
EXPECT_THAT(setxattr(sym.c_str(), kDefaultACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
// Fetching an ACL *through* the symlink should succeed.
EXPECT_THAT(getxattr(sym.c_str(), kDefaultACL, nullptr, 0),
SyscallSucceeds());
// Removing an ACL *through* the symlink should succeed.
EXPECT_THAT(removexattr(sym.c_str(), kDefaultACL), SyscallSucceeds());
// Setting an ACL *on* the symlink itself should fail.
EXPECT_THAT(lsetxattr(sym.c_str(), kDefaultACL, acl.data(), acl.size(), 0),
SyscallFailsWithErrno(EOPNOTSUPP));
// Fetching the ACL *of* the symlink itself should also fail.
EXPECT_THAT(lgetxattr(sym.c_str(), kDefaultACL, nullptr, 0),
SyscallFailsWithErrno(EOPNOTSUPP));
// Removing an ACL *of* the symlink itself should also fail.
EXPECT_THAT(syscall(SYS_lremovexattr, sym.c_str(), kDefaultACL, nullptr, 0),
SyscallFailsWithErrno(EOPNOTSUPP));
}
static bool IsTimespecLater(struct timespec a, struct timespec b) {
if (a.tv_sec > b.tv_sec) {
return true;
}
if (a.tv_sec == b.tv_sec && a.tv_nsec > b.tv_nsec) {
return true;
}
return false;
}
TEST_P(PosixACLTest, SetACLUpdatesCTime) {
// Fetch the original ctime
struct stat st = {};
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
struct timespec old_ctime = st.st_ctim;
// Set an ACL on the file.
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
absl::SleepFor(absl::Milliseconds(10));
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
// Fetch the new ctime
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
EXPECT_TRUE(IsTimespecLater(st.st_ctim, old_ctime));
}
TEST_P(PosixACLTest, RemoveACLUpdatesCTime) {
// Set an ACL on the file.
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
// Fetch the original ctime
struct stat st = {};
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
struct timespec old_ctime = st.st_ctim;
// Remove the ACL
absl::SleepFor(absl::Milliseconds(10));
ASSERT_THAT(removexattr(file_.c_str(), kAccessACL), SyscallSucceeds());
// Fetch the new ctime
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
EXPECT_TRUE(IsTimespecLater(st.st_ctim, old_ctime));
}
TEST_P(PosixACLTest, SetACLClearsSGID) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_FSETID)));
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_CHOWN)));
// Set the setgid bit
ASSERT_THAT(chmod(file_.c_str(), S_ISGID | 0750), SyscallSucceeds());
struct stat st = {};
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
ASSERT_TRUE(st.st_mode & S_ISGID);
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
// Setting a POSIX ACL shouldn't clear the setgid bit if we have privilege
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
ASSERT_TRUE(st.st_mode & S_ISGID);
// If we drop privilege, setting a POSIX ACL *still* shouldn't clear the
// setgid bit since we're the owning group
{
AutoCapability fsetid(CAP_FSETID, false);
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
ASSERT_TRUE(st.st_mode & S_ISGID);
}
ASSERT_THAT(chown(file_.c_str(), kNobody, kNobody), SyscallSucceeds());
ASSERT_THAT(chmod(file_.c_str(), S_ISGID | 0750), SyscallSucceeds());
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
ASSERT_TRUE(st.st_mode & S_ISGID);
// If we change the owning user/group, setting a POSIX ACL should *now* clear
// the setgid bit
AutoCapability fsetid(CAP_FSETID, false);
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
ASSERT_FALSE(st.st_mode & S_ISGID);
}
TEST_P(PosixACLTest, RemoveACLClearsSGID) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_FSETID)));
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_CHOWN)));
// Set the setgid bit
ASSERT_THAT(chmod(file_.c_str(), S_ISGID | 0750), SyscallSucceeds());
struct stat st = {};
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
ASSERT_TRUE(st.st_mode & S_ISGID);
// Clearing a POSIX ACL shouldn't clear the setgid bit if we have privilege
ASSERT_THAT(removexattr(file_.c_str(), kAccessACL), SyscallSucceeds());
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
ASSERT_TRUE(st.st_mode & S_ISGID);
// If we drop privilege, clearing a POSIX ACL *still* shouldn't clear the
// setgid bit since we're the owning group
{
AutoCapability fsetid(CAP_FSETID, false);
ASSERT_THAT(removexattr(file_.c_str(), kAccessACL), SyscallSucceeds());
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
ASSERT_TRUE(st.st_mode & S_ISGID);
}
ASSERT_THAT(chown(file_.c_str(), kNobody, kNobody), SyscallSucceeds());
ASSERT_THAT(chmod(file_.c_str(), S_ISGID | 0750), SyscallSucceeds());
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
ASSERT_TRUE(st.st_mode & S_ISGID);
// If we change the owning user/group, clearing a POSIX ACL should *now* clear
// the setgid bit
AutoCapability fsetid(CAP_FSETID, false);
ASSERT_THAT(removexattr(file_.c_str(), kAccessACL), SyscallSucceeds());
ASSERT_THAT(stat(file_.c_str(), &st), SyscallSucceeds());
ASSERT_FALSE(st.st_mode & S_ISGID);
}
TEST_P(PosixACLTest, SetACLEmpty) {
if (!IsRunningOnGvisor()) {
// Setting an empty string "" xattr for a POSIX ACL only clears the ACL on
// kernel >=6.2. Older kernels give EINVAL.
KernelVersion version = ASSERT_NO_ERRNO_AND_VALUE(GetKernelVersion());
SKIP_IF(version.major < 6 || (version.major == 6 && version.minor < 2));
}
// Set an ACL on the file
std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
// Setting a zero-length xattr as the ACL should be equivalent to calling
// removexattr().
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, "", 0, 0), SyscallSucceeds());
EXPECT_THAT(getxattr(file_.c_str(), kAccessACL, nullptr, 0),
SyscallFailsWithErrno(ENODATA));
// Same for default ACLs.
acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
ASSERT_THAT(
setxattr(dir_.path().c_str(), kDefaultACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
ASSERT_THAT(setxattr(dir_.path().c_str(), kDefaultACL, "", 0, 0),
SyscallSucceeds());
EXPECT_THAT(getxattr(dir_.path().c_str(), kDefaultACL, nullptr, 0),
SyscallFailsWithErrno(ENODATA));
// Clearing default ACL on a file with empty setxattr() should work.
EXPECT_THAT(setxattr(file_.c_str(), kDefaultACL, "", 0, 0),
SyscallSucceeds());
}
TEST_P(PosixACLTest, SetACLEmptyHeaderOnly) {
std::string emptyACL = BuildACL({});
// Set an ACL on the file
std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
// Setting a zero-length xattr as the ACL should be equivalent to calling
// removexattr().
ASSERT_THAT(
setxattr(file_.c_str(), kAccessACL, emptyACL.data(), emptyACL.size(), 0),
SyscallSucceeds());
EXPECT_THAT(getxattr(file_.c_str(), kAccessACL, nullptr, 0),
SyscallFailsWithErrno(ENODATA));
// Same for default ACLs.
acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
ASSERT_THAT(
setxattr(dir_.path().c_str(), kDefaultACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
ASSERT_THAT(setxattr(dir_.path().c_str(), kDefaultACL, emptyACL.data(),
emptyACL.size(), 0),
SyscallSucceeds());
EXPECT_THAT(getxattr(dir_.path().c_str(), kDefaultACL, nullptr, 0),
SyscallFailsWithErrno(ENODATA));
// Clearing default ACL on a file with empty setxattr() should work.
EXPECT_THAT(
setxattr(file_.c_str(), kDefaultACL, emptyACL.data(), emptyACL.size(), 0),
SyscallSucceeds());
}
TEST_P(PosixACLTest, SetACLIncompleteHeader) {
// Set an ACL on the file
char xattr[1] = {};
EXPECT_THAT(setxattr(file_.c_str(), kAccessACL, xattr, 1, 0),
SyscallFailsWithErrno(EINVAL));
}
TEST_P(PosixACLTest, SetACLWrongVersion) {
std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
uint32_t version = 50000;
memcpy(acl.data(), &version, sizeof(version));
// Should fail with EOPNOTSUPP due to the incorrect version
EXPECT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallFailsWithErrno(EOPNOTSUPP));
}
TEST_P(PosixACLTest, SetACLNonWholeNumberEntries) {
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
// Should fail with EINVAL due to the odd size
EXPECT_THAT(
setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size() - 1, 0),
SyscallFailsWithErrno(EINVAL));
}
TEST_P(PosixACLTest, SetACLInvalidPermissionBits) {
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, 10, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
// Should fail with EINVAL due to the USER.Perm = 10
EXPECT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallFailsWithErrno(EINVAL));
}
TEST_P(PosixACLTest, SetACLMultipleObj) {
std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
// Should fail with EINVAL due to multiple USER_OBJ
EXPECT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallFailsWithErrno(EINVAL));
acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
// Should fail with EINVAL due to multiple GROUP_OBJ
EXPECT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallFailsWithErrno(EINVAL));
acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
Ent(kOther, kR, kUndef),
});
// Should fail with EINVAL due to multiple OTHER
EXPECT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallFailsWithErrno(EINVAL));
acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
// Should fail with EINVAL due to multiple MASK
EXPECT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallFailsWithErrno(EINVAL));
}
TEST_P(PosixACLTest, SetACLNonUniqueID) {
// acl(5) documents this as causing an ACL to be invalid, however
// Linux does not enforce this. So we won't either.
std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kUser, kR | kW, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
EXPECT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kGroupObj, kR, kUndef),
Ent(kGroup, kR, gid_),
Ent(kGroup, kR | kW, gid_),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
EXPECT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
}
TEST_P(PosixACLTest, SetACLNoObj) {
const std::string acl = BuildACL({
Ent(kUser, kR, uid_),
Ent(kUser, kR, uid_),
Ent(kMask, kR, kUndef),
Ent(kOther, kR, kUndef),
});
// Should fail with EINVAL due to no USER_OBJ or GROUP_OBJ
EXPECT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallFailsWithErrno(EINVAL));
}
TEST_P(PosixACLTest, SetACLNoMask) {
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kGroup, kR, gid_),
Ent(kOther, kR, kUndef),
});
// Should fail with EINVAL due to no mask despite presence of named user
EXPECT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallFailsWithErrno(EINVAL));
}
// Test reading a POSIX ACL from a lower overlayfs mount point through a stacked
// overlay.
TEST_P(PosixACLTest, GetACLThroughStackedOverlay) {
// TODO(gvisor.dev/issues/14066): fix nested-overlayfs locks
if (IsRunningOnGvisor()) {
GTEST_SKIP() << "Skipped due to #14066";
}
SKIP_IF(GetParam() != Backend::kOverlayLower);
// Need CAP_SYS_ADMIN to mount overlayfs.
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR | kW, uid_),
Ent(kGroupObj, 0, kUndef),
Ent(kMask, 0, kUndef),
Ent(kOther, kR, kUndef),
});
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
const auto upper2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(layers_));
const auto work2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(layers_));
const auto stacked_dir =
ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(base_.path()));
const std::string opts = "lowerdir=" + dir_.path() +
",upperdir=" + upper2.path() +
",workdir=" + work2.path();
const auto mount = ASSERT_NO_ERRNO_AND_VALUE(
Mount("overlay", stacked_dir.path(), "overlay", 0, opts, 0));
const auto stacked_file = JoinPath(stacked_dir.path(), "posix_acl_test_file");
const std::string got =
ASSERT_NO_ERRNO_AND_VALUE(GetXattrString(stacked_file, kAccessACL));
EXPECT_EQ(got, acl);
}
// Test setting a POSIX ACL on a file accessed through a stacked overlay.
TEST_P(PosixACLTest, SetACLThroughStackedOverlay) {
// TODO(gvisor.dev/issues/14066): fix nested-overlayfs locks
if (IsRunningOnGvisor()) {
GTEST_SKIP() << "Skipped due to #14066";
}
SKIP_IF(GetParam() != Backend::kOverlayLower);
// Need CAP_SYS_ADMIN to mount overlayfs.
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
const std::string acl = BuildACL({
Ent(kUserObj, kR | kW, kUndef),
Ent(kUser, kR, uid_),
Ent(kGroupObj, kR, kUndef),
Ent(kGroup, kR | kW, gid_),
Ent(kMask, kR | kW, kUndef),
Ent(kOther, kR, kUndef),
});
ASSERT_THAT(setxattr(file_.c_str(), kAccessACL, acl.data(), acl.size(), 0),
SyscallSucceeds());
const auto upper2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(layers_));
const auto work2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(layers_));
const auto stacked_dir =
ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(base_.path()));
const std::string opts = "lowerdir=" + dir_.path() +
",upperdir=" + upper2.path() +
",workdir=" + work2.path();
const auto mount = ASSERT_NO_ERRNO_AND_VALUE(
Mount("overlay", stacked_dir.path(), "overlay", 0, opts, 0));
// Trigger a copy-up (mode 664 matches u/mask/o from the ACL, so this chmod
// should not affect the ACL).
const auto stacked_file = JoinPath(stacked_dir.path(), "posix_acl_test_file");
ASSERT_THAT(chmod(stacked_file.c_str(), 0664), SyscallSucceeds());
const std::string got =
ASSERT_NO_ERRNO_AND_VALUE(GetXattrString(stacked_file, kAccessACL));
EXPECT_EQ(got, acl);
}
INSTANTIATE_TEST_SUITE_P(
All, PosixACLTest,
::testing::Values(Backend::kTmpfs, Backend::kOverlayUpper,
Backend::kOverlayLower),
[](const ::testing::TestParamInfo<Backend>& info) -> std::string {
switch (info.param) {
case Backend::kTmpfs:
return "Tmpfs";
case Backend::kOverlayUpper:
return "OverlayUpper";
case Backend::kOverlayLower:
return "OverlayLower";
}
return "Unknown";
});
} // namespace
} // namespace testing
} // namespace gvisor