mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
setgid directory support in overlayfs
PiperOrigin-RevId: 363276495
This commit is contained in:
committed by
gVisor bot
parent
05193de1cc
commit
607a1e481c
@@ -689,13 +689,9 @@ func (fs *filesystem) MkdirAt(ctx context.Context, rp *vfs.ResolvingPath, opts v
|
||||
}
|
||||
return err
|
||||
}
|
||||
creds := rp.Credentials()
|
||||
|
||||
if err := vfsObj.SetStatAt(ctx, fs.creds, &pop, &vfs.SetStatOptions{
|
||||
Stat: linux.Statx{
|
||||
Mask: linux.STATX_UID | linux.STATX_GID,
|
||||
UID: uint32(creds.EffectiveKUID),
|
||||
GID: uint32(creds.EffectiveKGID),
|
||||
},
|
||||
Stat: parent.newChildOwnerStat(opts.Mode, rp.Credentials()),
|
||||
}); err != nil {
|
||||
if cleanupErr := vfsObj.RmdirAt(ctx, fs.creds, &pop); cleanupErr != nil {
|
||||
panic(fmt.Sprintf("unrecoverable overlayfs inconsistency: failed to delete upper layer directory after MkdirAt metadata update failure: %v", cleanupErr))
|
||||
@@ -750,11 +746,7 @@ func (fs *filesystem) MknodAt(ctx context.Context, rp *vfs.ResolvingPath, opts v
|
||||
}
|
||||
creds := rp.Credentials()
|
||||
if err := vfsObj.SetStatAt(ctx, fs.creds, &pop, &vfs.SetStatOptions{
|
||||
Stat: linux.Statx{
|
||||
Mask: linux.STATX_UID | linux.STATX_GID,
|
||||
UID: uint32(creds.EffectiveKUID),
|
||||
GID: uint32(creds.EffectiveKGID),
|
||||
},
|
||||
Stat: parent.newChildOwnerStat(opts.Mode, creds),
|
||||
}); err != nil {
|
||||
if cleanupErr := vfsObj.UnlinkAt(ctx, fs.creds, &pop); cleanupErr != nil {
|
||||
panic(fmt.Sprintf("unrecoverable overlayfs inconsistency: failed to delete upper layer file after MknodAt metadata update failure: %v", cleanupErr))
|
||||
@@ -963,14 +955,11 @@ func (fs *filesystem) createAndOpenLocked(ctx context.Context, rp *vfs.Resolving
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Change the file's owner to the caller. We can't use upperFD.SetStat()
|
||||
// because it will pick up creds from ctx.
|
||||
if err := vfsObj.SetStatAt(ctx, fs.creds, &pop, &vfs.SetStatOptions{
|
||||
Stat: linux.Statx{
|
||||
Mask: linux.STATX_UID | linux.STATX_GID,
|
||||
UID: uint32(creds.EffectiveKUID),
|
||||
GID: uint32(creds.EffectiveKGID),
|
||||
},
|
||||
Stat: parent.newChildOwnerStat(opts.Mode, creds),
|
||||
}); err != nil {
|
||||
if cleanupErr := vfsObj.UnlinkAt(ctx, fs.creds, &pop); cleanupErr != nil {
|
||||
panic(fmt.Sprintf("unrecoverable overlayfs inconsistency: failed to delete upper layer file after OpenAt(O_CREAT) metadata update failure: %v", cleanupErr))
|
||||
|
||||
@@ -749,6 +749,27 @@ func (d *dentry) mayDelete(creds *auth.Credentials, child *dentry) error {
|
||||
)
|
||||
}
|
||||
|
||||
// newChildOwnerStat returns a Statx for configuring the UID, GID, and mode of
|
||||
// children.
|
||||
func (d *dentry) newChildOwnerStat(mode linux.FileMode, creds *auth.Credentials) linux.Statx {
|
||||
stat := linux.Statx{
|
||||
Mask: uint32(linux.STATX_UID | linux.STATX_GID),
|
||||
UID: uint32(creds.EffectiveKUID),
|
||||
GID: uint32(creds.EffectiveKGID),
|
||||
}
|
||||
// Set GID and possibly the SGID bit if the parent is an SGID directory.
|
||||
d.copyMu.RLock()
|
||||
defer d.copyMu.RUnlock()
|
||||
if atomic.LoadUint32(&d.mode)&linux.ModeSetGID == linux.ModeSetGID {
|
||||
stat.GID = atomic.LoadUint32(&d.gid)
|
||||
if stat.Mode&linux.ModeDirectory == linux.ModeDirectory {
|
||||
stat.Mode = uint16(mode) | linux.ModeSetGID
|
||||
stat.Mask |= linux.STATX_MODE
|
||||
}
|
||||
}
|
||||
return stat
|
||||
}
|
||||
|
||||
// fileDescription is embedded by overlay implementations of
|
||||
// vfs.FileDescriptionImpl.
|
||||
//
|
||||
|
||||
@@ -205,6 +205,20 @@ func (fd *regularFileFD) SetStat(ctx context.Context, opts vfs.SetStatOptions) e
|
||||
if err := wrappedFD.SetStat(ctx, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Changing owners may clear one or both of the setuid and setgid bits,
|
||||
// so we may have to update opts before setting d.mode.
|
||||
if opts.Stat.Mask&(linux.STATX_UID|linux.STATX_GID) != 0 {
|
||||
stat, err := wrappedFD.Stat(ctx, vfs.StatOptions{
|
||||
Mask: linux.STATX_MODE,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Stat.Mode = stat.Mode
|
||||
opts.Stat.Mask |= linux.STATX_MODE
|
||||
}
|
||||
|
||||
d.updateAfterSetStatLocked(&opts)
|
||||
if ev := vfs.InotifyEventFromStatMask(opts.Stat.Mask); ev != 0 {
|
||||
d.InotifyWithParent(ctx, ev, 0, vfs.InodeEvent)
|
||||
@@ -295,7 +309,11 @@ func (fd *regularFileFD) PWrite(ctx context.Context, src usermem.IOSequence, off
|
||||
return 0, err
|
||||
}
|
||||
defer wrappedFD.DecRef(ctx)
|
||||
return wrappedFD.PWrite(ctx, src, offset, opts)
|
||||
n, err := wrappedFD.PWrite(ctx, src, offset, opts)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
return fd.updateSetUserGroupIDs(ctx, wrappedFD, n)
|
||||
}
|
||||
|
||||
// Write implements vfs.FileDescriptionImpl.Write.
|
||||
@@ -307,7 +325,28 @@ func (fd *regularFileFD) Write(ctx context.Context, src usermem.IOSequence, opts
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return wrappedFD.Write(ctx, src, opts)
|
||||
n, err := wrappedFD.Write(ctx, src, opts)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
return fd.updateSetUserGroupIDs(ctx, wrappedFD, n)
|
||||
}
|
||||
|
||||
func (fd *regularFileFD) updateSetUserGroupIDs(ctx context.Context, wrappedFD *vfs.FileDescription, written int64) (int64, error) {
|
||||
// Writing can clear the setuid and/or setgid bits. We only have to
|
||||
// check this if something was written and one of those bits was set.
|
||||
dentry := fd.dentry()
|
||||
if written == 0 || atomic.LoadUint32(&dentry.mode)&(linux.S_ISUID|linux.S_ISGID) == 0 {
|
||||
return written, nil
|
||||
}
|
||||
stat, err := wrappedFD.Stat(ctx, vfs.StatOptions{Mask: linux.STATX_MODE})
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
dentry.copyMu.Lock()
|
||||
defer dentry.copyMu.Unlock()
|
||||
atomic.StoreUint32(&dentry.mode, uint32(stat.Mode))
|
||||
return written, nil
|
||||
}
|
||||
|
||||
// Seek implements vfs.FileDescriptionImpl.Seek.
|
||||
|
||||
@@ -2162,6 +2162,7 @@ cc_binary(
|
||||
"//test/util:temp_path",
|
||||
"//test/util:test_main",
|
||||
"//test/util:test_util",
|
||||
"@com_google_absl//absl/flags:flag",
|
||||
"@com_google_absl//absl/strings",
|
||||
gtest,
|
||||
],
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include "absl/flags/flag.h"
|
||||
#include "test/util/capability_util.h"
|
||||
#include "test/util/cleanup.h"
|
||||
#include "test/util/fs_util.h"
|
||||
@@ -24,6 +25,11 @@
|
||||
#include "test/util/temp_path.h"
|
||||
#include "test/util/test_util.h"
|
||||
|
||||
ABSL_FLAG(std::vector<std::string>, groups, std::vector<std::string>({}),
|
||||
"groups the test can use");
|
||||
|
||||
constexpr gid_t kNobody = 65534;
|
||||
|
||||
namespace gvisor {
|
||||
namespace testing {
|
||||
|
||||
@@ -46,6 +52,18 @@ PosixErrorOr<Cleanup> Setegid(gid_t egid) {
|
||||
|
||||
// Returns a pair of groups that the user is a member of.
|
||||
PosixErrorOr<std::pair<gid_t, gid_t>> Groups() {
|
||||
// Were we explicitly passed GIDs?
|
||||
std::vector<std::string> flagged_groups = absl::GetFlag(FLAGS_groups);
|
||||
if (flagged_groups.size() >= 2) {
|
||||
int group1;
|
||||
int group2;
|
||||
if (!absl::SimpleAtoi(flagged_groups[0], &group1) ||
|
||||
!absl::SimpleAtoi(flagged_groups[1], &group2)) {
|
||||
return PosixError(EINVAL, "failed converting group flags to ints");
|
||||
}
|
||||
return std::pair<gid_t, gid_t>(group1, group2);
|
||||
}
|
||||
|
||||
// See whether the user is a member of at least 2 groups.
|
||||
std::vector<gid_t> groups(64);
|
||||
for (; groups.size() <= NGROUPS_MAX; groups.resize(groups.size() * 2)) {
|
||||
@@ -58,26 +76,47 @@ PosixErrorOr<std::pair<gid_t, gid_t>> Groups() {
|
||||
return PosixError(errno, absl::StrFormat("getgroups(%d, %p)",
|
||||
groups.size(), groups.data()));
|
||||
}
|
||||
if (ngroups >= 2) {
|
||||
return std::pair<gid_t, gid_t>(groups[0], groups[1]);
|
||||
|
||||
if (ngroups < 2) {
|
||||
// There aren't enough groups.
|
||||
break;
|
||||
}
|
||||
// There aren't enough groups.
|
||||
break;
|
||||
|
||||
// TODO(b/181878080): Read /proc/sys/fs/overflowgid once it is supported in
|
||||
// gVisor.
|
||||
if (groups[0] == kNobody || groups[1] == kNobody) {
|
||||
// These groups aren't mapped into our user namespace, so we can't use
|
||||
// them.
|
||||
break;
|
||||
}
|
||||
return std::pair<gid_t, gid_t>(groups[0], groups[1]);
|
||||
}
|
||||
|
||||
// If we're root in the root user namespace, we can set our GID to whatever we
|
||||
// want. Try that before giving up.
|
||||
constexpr gid_t kGID1 = 1111;
|
||||
constexpr gid_t kGID2 = 2222;
|
||||
auto cleanup1 = Setegid(kGID1);
|
||||
// If we're running in gVisor and are root in the root user namespace, we can
|
||||
// set our GID to whatever we want. Try that before giving up.
|
||||
//
|
||||
// This won't work in native tests, as despite having CAP_SETGID, the gofer
|
||||
// process will be sandboxed and unable to change file GIDs.
|
||||
if (!IsRunningOnGvisor()) {
|
||||
return PosixError(EPERM, "no valid groups for native testing");
|
||||
}
|
||||
PosixErrorOr<bool> capable = HaveCapability(CAP_SETGID);
|
||||
if (!capable.ok()) {
|
||||
return capable.error();
|
||||
}
|
||||
if (!capable.ValueOrDie()) {
|
||||
return PosixError(EPERM, "missing CAP_SETGID");
|
||||
}
|
||||
gid_t gid = getegid();
|
||||
auto cleanup1 = Setegid(gid);
|
||||
if (!cleanup1.ok()) {
|
||||
return cleanup1.error();
|
||||
}
|
||||
auto cleanup2 = Setegid(kGID2);
|
||||
auto cleanup2 = Setegid(kNobody);
|
||||
if (!cleanup2.ok()) {
|
||||
return cleanup2.error();
|
||||
}
|
||||
return std::pair<gid_t, gid_t>(kGID1, kGID2);
|
||||
return std::pair<gid_t, gid_t>(gid, kNobody);
|
||||
}
|
||||
|
||||
class SetgidDirTest : public ::testing::Test {
|
||||
@@ -85,17 +124,20 @@ class SetgidDirTest : public ::testing::Test {
|
||||
void SetUp() override {
|
||||
original_gid_ = getegid();
|
||||
|
||||
// TODO(b/175325250): Enable when setgid directories are supported.
|
||||
SKIP_IF(IsRunningWithVFS1());
|
||||
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETGID)));
|
||||
|
||||
temp_dir_ = ASSERT_NO_ERRNO_AND_VALUE(
|
||||
TempPath::CreateDirWith(GetAbsoluteTestTmpdir(), 0777 /* mode */));
|
||||
groups_ = ASSERT_NO_ERRNO_AND_VALUE(Groups());
|
||||
|
||||
// If we can't find two usable groups, we're in an unsupporting environment.
|
||||
// Skip the test.
|
||||
PosixErrorOr<std::pair<gid_t, gid_t>> groups = Groups();
|
||||
SKIP_IF(!groups.ok());
|
||||
groups_ = groups.ValueOrDie();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
ASSERT_THAT(setegid(original_gid_), SyscallSucceeds());
|
||||
EXPECT_THAT(setegid(original_gid_), SyscallSucceeds());
|
||||
}
|
||||
|
||||
void MkdirAsGid(gid_t gid, const std::string& path, mode_t mode) {
|
||||
@@ -131,7 +173,7 @@ TEST_F(SetgidDirTest, Control) {
|
||||
ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, 0777));
|
||||
|
||||
// Set group to G2, create a file in g1owned, and confirm that G2 owns it.
|
||||
ASSERT_THAT(setegid(groups_.second), SyscallSucceeds());
|
||||
auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(Setegid(groups_.second));
|
||||
FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
|
||||
Open(JoinPath(g1owned, "g2owned").c_str(), O_CREAT | O_RDWR, 0777));
|
||||
struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
|
||||
@@ -146,7 +188,7 @@ TEST_F(SetgidDirTest, CreateFile) {
|
||||
ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeSgid), SyscallSucceeds());
|
||||
|
||||
// Set group to G2, create a file, and confirm that G1 owns it.
|
||||
ASSERT_THAT(setegid(groups_.second), SyscallSucceeds());
|
||||
auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(Setegid(groups_.second));
|
||||
FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
|
||||
Open(JoinPath(g1owned, "g2created").c_str(), O_CREAT | O_RDWR, 0666));
|
||||
struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
|
||||
@@ -194,7 +236,7 @@ TEST_F(SetgidDirTest, OldFile) {
|
||||
ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeNoSgid), SyscallSucceeds());
|
||||
|
||||
// Set group to G2, create a file, confirm that G2 owns it.
|
||||
ASSERT_THAT(setegid(groups_.second), SyscallSucceeds());
|
||||
auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(Setegid(groups_.second));
|
||||
FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
|
||||
Open(JoinPath(g1owned, "g2created").c_str(), O_CREAT | O_RDWR, 0666));
|
||||
struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
|
||||
@@ -217,7 +259,7 @@ TEST_F(SetgidDirTest, OldDir) {
|
||||
ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeNoSgid), SyscallSucceeds());
|
||||
|
||||
// Set group to G2, create a directory, confirm that G2 owns it.
|
||||
ASSERT_THAT(setegid(groups_.second), SyscallSucceeds());
|
||||
auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(Setegid(groups_.second));
|
||||
auto g2created = JoinPath(g1owned, "g2created");
|
||||
ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.second, g2created, 0666));
|
||||
struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g2created));
|
||||
|
||||
Reference in New Issue
Block a user