From d3f4ee03a3936e1bcd62230273a2822f5d7d0bb7 Mon Sep 17 00:00:00 2001 From: Ryan El Kochta Date: Tue, 28 Jul 2026 16:16:23 -0700 Subject: [PATCH] tmpfs: implement POSIX ACLs Implements POSIX ACLs for gVisor's tmpfs. As a summary: - Adds `accessACL` and `defaultACL` fields to tmpfs's inode - Allows `system.posix_acl_access` and `system.posix_acl_default` xattrs to set and retrieve POSIX ACLs - Updates tmpfs's stat implementation to account for the ACL in setting/retrieving a file's mode - Updates `GenericCheckPermission` to take an optional ACL. All non-tmpfs filesystems pass the `nil` ACL. - Updates tmpfs's tar save-and-restore logic to store the base64-encoded POSIX ACL xattrs in the tar header - Adds unit tests for POSIX ACL parsing/serialization - Adds integration tests for POSIX ACLs - Adds a container test to verify the tmpfs tar save/restore logic with ACLs. This required a new subcommand for `test_app` to set and fetch xattrs in the container The motivation for POSIX ACLs is for journald (which uses POSIX ACLs to give non-root users access to their own `systemctl --user` services' logs). This commit is not actually sufficient to get this working -- we will need to plumb ACLs to overlayfs, too, which will be done in a follow-up commit. The implementation is very similar to the implementation in Linux, although it is filesystem-specific rather than at the VFS layer (much like we store `mode`, `uid`, `gid`, etc at the FS layer currently). An optional access and default ACL is stored in each inode. If present, it is used for permission checks and is kept in sync with the `mode`. The `mode` is computed from the ACL when permissions are updated. If the ACL is simple enough to be stored equivalently as a `mode`, an ACL is not stored. One limitation at the moment is that upon file creation, the process's umask is masked against the creation mode regardless of the presence of a default ACL, whereas Linux only masks against umask when *not* inheriting from a default ACL. Fixing this would require a fair bit of refactoring since umask isn't passed through to filesystems at all, and is instead masked with the creation mode at the syscall layer. This is not a security issue since the resulting permissions are always of equal-or-greater restrictiveness to those of Linux, but is tracked in #13688. FUTURE_COPYBARA_INTEGRATE_REVIEW=https://github.com/google/gvisor/pull/13693 from relkochta:relkochta/posix-acls-tmpfs 0df3de9a3feb7b0368f1791e10a8e5d0453306d2 PiperOrigin-RevId: 955529312 --- images/basic/libacl/Dockerfile | 6 + pkg/abi/linux/xattr.go | 114 +++ pkg/sentry/fsimpl/erofs/erofs.go | 2 +- pkg/sentry/fsimpl/erofs/filesystem.go | 16 + pkg/sentry/fsimpl/fuse/file.go | 2 +- pkg/sentry/fsimpl/fuse/inode.go | 6 +- pkg/sentry/fsimpl/gofer/filesystem.go | 25 +- pkg/sentry/fsimpl/gofer/gofer.go | 6 +- pkg/sentry/fsimpl/host/host.go | 4 +- pkg/sentry/fsimpl/kernfs/filesystem.go | 34 +- pkg/sentry/fsimpl/kernfs/inode_impl_util.go | 3 +- pkg/sentry/fsimpl/overlay/filesystem.go | 25 +- pkg/sentry/fsimpl/overlay/overlay.go | 16 +- pkg/sentry/fsimpl/overlay/regular_file.go | 2 +- pkg/sentry/fsimpl/pipefs/pipefs.go | 4 +- pkg/sentry/fsimpl/proc/task.go | 2 +- pkg/sentry/fsimpl/tmpfs/BUILD | 1 + pkg/sentry/fsimpl/tmpfs/device_file.go | 5 + pkg/sentry/fsimpl/tmpfs/directory.go | 4 + pkg/sentry/fsimpl/tmpfs/filesystem.go | 33 +- pkg/sentry/fsimpl/tmpfs/named_pipe.go | 5 + pkg/sentry/fsimpl/tmpfs/regular_file.go | 4 + pkg/sentry/fsimpl/tmpfs/save_restore.go | 20 + pkg/sentry/fsimpl/tmpfs/socket_file.go | 4 + pkg/sentry/fsimpl/tmpfs/symlink.go | 4 + pkg/sentry/fsimpl/tmpfs/tar.go | 97 +- pkg/sentry/fsimpl/tmpfs/tmpfs.go | 247 ++++- pkg/sentry/vfs/BUILD | 3 + pkg/sentry/vfs/anonfs.go | 22 +- pkg/sentry/vfs/file_description.go | 37 + pkg/sentry/vfs/file_description_impl_util.go | 12 + pkg/sentry/vfs/filesystem.go | 27 + pkg/sentry/vfs/opath.go | 5 + pkg/sentry/vfs/permissions.go | 59 +- pkg/sentry/vfs/posix_acl.go | 431 ++++++++ pkg/sentry/vfs/posix_acl_test.go | 362 +++++++ pkg/sentry/vfs/vfs.go | 40 + runsc/container/container_test.go | 158 +++ test/cmd/test_app/BUILD | 1 + test/cmd/test_app/main.go | 2 + test/cmd/test_app/xattr.go | 115 +++ test/e2e/integration_test.go | 51 + test/syscalls/BUILD | 4 + test/syscalls/linux/BUILD | 20 + test/syscalls/linux/posix_acl.cc | 990 +++++++++++++++++++ 45 files changed, 2957 insertions(+), 73 deletions(-) create mode 100644 images/basic/libacl/Dockerfile create mode 100644 pkg/sentry/vfs/posix_acl.go create mode 100644 pkg/sentry/vfs/posix_acl_test.go create mode 100644 test/cmd/test_app/xattr.go create mode 100644 test/syscalls/linux/posix_acl.cc diff --git a/images/basic/libacl/Dockerfile b/images/basic/libacl/Dockerfile new file mode 100644 index 00000000000..75dbc5172a5 --- /dev/null +++ b/images/basic/libacl/Dockerfile @@ -0,0 +1,6 @@ +FROM ubuntu:26.04@sha256:3131b4cc82a783df6c9df078f86e01819a13594b865c2cad47bd1bca2b7063bb + +RUN apt-get update && apt-get install -y acl + +RUN useradd alice +RUN groupadd employees \ No newline at end of file diff --git a/pkg/abi/linux/xattr.go b/pkg/abi/linux/xattr.go index 6d6606e6255..937c57c498e 100644 --- a/pkg/abi/linux/xattr.go +++ b/pkg/abi/linux/xattr.go @@ -14,6 +14,11 @@ package linux +import ( + "encoding/binary" + "structs" +) + // Constants for extended attributes. const ( XATTR_NAME_MAX = 255 @@ -37,3 +42,112 @@ const ( XATTR_USER_PREFIX = "user." XATTR_USER_PREFIX_LEN = len(XATTR_USER_PREFIX) ) + +// Constants for POSIX ACL extended attributes. +const ( + // Extended attribute names for POSIX ACLs. + XATTR_NAME_POSIX_ACL_ACCESS = XATTR_SYSTEM_PREFIX + "posix_acl_access" + XATTR_NAME_POSIX_ACL_DEFAULT = XATTR_SYSTEM_PREFIX + "posix_acl_default" + + POSIX_ACL_XATTR_VERSION = 2 + + // ACL_UNDEFINED_ID is the ID for entries that do not contain a + // named user or group. + ACL_UNDEFINED_ID = 0xffffffff + + // ACL entry tags. + ACL_USER_OBJ = 0x01 + ACL_USER = 0x02 + ACL_GROUP_OBJ = 0x04 + ACL_GROUP = 0x08 + ACL_MASK = 0x10 + ACL_OTHER = 0x20 + + // ACL entry permission bits. + ACL_READ = 0x04 + ACL_WRITE = 0x02 + ACL_EXECUTE = 0x01 +) + +// PosixACLXattrEntry is a single entry in the userspace representation +// of a POSIX ACL. It corresponds to Linux's struct posix_acl_xattr_entry. +// +// All fields in PosixACLXattrEntry are stored as little-endian. +// +// +marshal dynamic +type PosixACLXattrEntry struct { + _ structs.HostLayout + Tag uint16 + Perm uint16 + ID uint32 +} + +// SizeBytes implements marshal.Marshallable.SizeBytes. +func (a *PosixACLXattrEntry) SizeBytes() int { + return 8 +} + +// MarshalBytes implements marshal.Marshallable.MarshalBytes. +func (a *PosixACLXattrEntry) MarshalBytes(dst []byte) []byte { + binary.LittleEndian.PutUint16(dst[0:], a.Tag) + binary.LittleEndian.PutUint16(dst[2:], a.Perm) + binary.LittleEndian.PutUint32(dst[4:], a.ID) + + return dst[8:] +} + +// UnmarshalBytes implements marshal.Marshallable.UnmarshalBytes. +func (a *PosixACLXattrEntry) UnmarshalBytes(src []byte) []byte { + a.Tag = binary.LittleEndian.Uint16(src[0:]) + a.Perm = binary.LittleEndian.Uint16(src[2:]) + a.ID = binary.LittleEndian.Uint32(src[4:]) + + return src[8:] +} + +// PosixACLXattr is the userspace representation of a POSIX ACL. +// +// +marshal dynamic +type PosixACLXattr struct { + _ structs.HostLayout + + // Version is the POSIX ACL version, stored as little-endian. + Version uint32 + + // Entries contains the ACL entries. + Entries []PosixACLXattrEntry `hostlayout:"ignore"` +} + +// posixACLXattrHeaderSize is the size in bytes of the header. +const posixACLXattrHeaderSize = 4 + +// SizeBytes implements marshal.Marshallable.SizeBytes. +func (a *PosixACLXattr) SizeBytes() int { + return posixACLXattrHeaderSize + len(a.Entries)*(*PosixACLXattrEntry)(nil).SizeBytes() +} + +// MarshalBytes implements marshal.Marshallable.MarshalBytes. +func (a *PosixACLXattr) MarshalBytes(dst []byte) []byte { + binary.LittleEndian.PutUint32(dst, a.Version) + + dst = dst[posixACLXattrHeaderSize:] + for _, entry := range a.Entries { + dst = entry.MarshalBytes(dst) + } + + return dst +} + +// UnmarshalBytes implements marshal.Marshallable.UnmarshalBytes. +func (a *PosixACLXattr) UnmarshalBytes(src []byte) []byte { + a.Version = binary.LittleEndian.Uint32(src) + + src = src[posixACLXattrHeaderSize:] + for len(src) >= (*PosixACLXattrEntry)(nil).SizeBytes() { + var entry PosixACLXattrEntry + src = entry.UnmarshalBytes(src) + a.Entries = append(a.Entries, entry) + } + + return src +} diff --git a/pkg/sentry/fsimpl/erofs/erofs.go b/pkg/sentry/fsimpl/erofs/erofs.go index 7e1bff57b3e..4402072ad96 100644 --- a/pkg/sentry/fsimpl/erofs/erofs.go +++ b/pkg/sentry/fsimpl/erofs/erofs.go @@ -327,7 +327,7 @@ func (i *inode) DecRef(ctx context.Context) { } func (i *inode) checkPermissions(creds *auth.Credentials, ats vfs.AccessTypes) error { - return vfs.GenericCheckPermissions(creds, ats, linux.FileMode(i.Mode()), auth.KUID(i.UID()), auth.KGID(i.GID())) + return vfs.GenericCheckPermissions(creds, ats, linux.FileMode(i.Mode()), nil, auth.KUID(i.UID()), auth.KGID(i.GID())) } func (i *inode) statTo(stat *linux.Statx) { diff --git a/pkg/sentry/fsimpl/erofs/filesystem.go b/pkg/sentry/fsimpl/erofs/filesystem.go index 14b5ac5bf56..f52d02914b0 100644 --- a/pkg/sentry/fsimpl/erofs/filesystem.go +++ b/pkg/sentry/fsimpl/erofs/filesystem.go @@ -428,6 +428,22 @@ func (fs *filesystem) RemoveXattrAt(ctx context.Context, rp *vfs.ResolvingPath, return linuxerr.EROFS } +// GetPosixACLAt implements vfs.FilesystemImpl.GetPosixACLAt. +func (fs *filesystem) GetPosixACLAt(ctx context.Context, rp *vfs.ResolvingPath, t vfs.ACLType) (*vfs.PosixACL, error) { + _, err := resolve(ctx, rp) + // erofs does not support POSIX ACLs. + return nil, err +} + +// SetPosixACLAt implements vfs.FilesystemImpl.SetPosixACLAt. +func (fs *filesystem) SetPosixACLAt(ctx context.Context, rp *vfs.ResolvingPath, t vfs.ACLType, acl *vfs.PosixACL, clearSGID bool) (*vfs.PosixACL, linux.FileMode, error) { + if _, err := resolve(ctx, rp); err != nil { + return nil, 0, err + } + // erofs does not support POSIX ACLs. + return nil, 0, linuxerr.EOPNOTSUPP +} + // PrependPath implements vfs.FilesystemImpl.PrependPath. func (fs *filesystem) PrependPath(ctx context.Context, vfsroot, vd vfs.VirtualDentry, b *fspath.Builder) error { return genericPrependPath(fs, vfsroot, vd.Mount(), vd.Dentry().Impl().(*dentry), b) diff --git a/pkg/sentry/fsimpl/fuse/file.go b/pkg/sentry/fsimpl/fuse/file.go index 8a3c103b767..f304a1a827f 100644 --- a/pkg/sentry/fsimpl/fuse/file.go +++ b/pkg/sentry/fsimpl/fuse/file.go @@ -159,7 +159,7 @@ func (fd *fileDescription) SetStat(ctx context.Context, opts vfs.SetStatOptions) inode := fd.inode() inode.attrMu.Lock() defer inode.attrMu.Unlock() - if err := vfs.CheckSetStat(ctx, creds, &opts, inode.filemode(), auth.KUID(inode.uid.Load()), auth.KGID(inode.gid.Load())); err != nil { + if err := vfs.CheckSetStat(ctx, creds, &opts, inode.filemode(), nil, auth.KUID(inode.uid.Load()), auth.KGID(inode.gid.Load())); err != nil { return err } return inode.setAttr(ctx, fs, creds, opts, fhOptions{useFh: true, fh: fd.Fh}) diff --git a/pkg/sentry/fsimpl/fuse/inode.go b/pkg/sentry/fsimpl/fuse/inode.go index f3c233fa2aa..7646f272405 100644 --- a/pkg/sentry/fsimpl/fuse/inode.go +++ b/pkg/sentry/fsimpl/fuse/inode.go @@ -476,12 +476,12 @@ func (i *inode) CheckPermissions(ctx context.Context, creds *auth.Credentials, a } if i.fs.opts.defaultPermissions || (ats.MayExec() && i.filemode().FileType() == linux.S_IFREG) { - err := vfs.GenericCheckPermissions(creds, ats, linux.FileMode(i.mode.Load()), auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())) + err := vfs.GenericCheckPermissions(creds, ats, linux.FileMode(i.mode.Load()), nil, auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())) if linuxerr.Equals(linuxerr.EACCES, err) && !refreshed { if _, err := i.getAttr(ctx, creds, i.fs.VFSFilesystem(), opts, 0, 0); err != nil { return err } - return vfs.GenericCheckPermissions(creds, ats, linux.FileMode(i.mode.Load()), auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())) + return vfs.GenericCheckPermissions(creds, ats, linux.FileMode(i.mode.Load()), nil, auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())) } return err } @@ -854,7 +854,7 @@ func (i *inode) SetStat(ctx context.Context, fs *vfs.Filesystem, creds *auth.Cre i.attrMu.Lock() defer i.attrMu.Unlock() - if err := vfs.CheckSetStat(ctx, creds, &opts, i.filemode(), auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())); err != nil { + if err := vfs.CheckSetStat(ctx, creds, &opts, i.filemode(), nil, auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())); err != nil { return err } if opts.Stat.Mask == 0 { diff --git a/pkg/sentry/fsimpl/gofer/filesystem.go b/pkg/sentry/fsimpl/gofer/filesystem.go index f11c1fb58e5..44d2b447e88 100644 --- a/pkg/sentry/fsimpl/gofer/filesystem.go +++ b/pkg/sentry/fsimpl/gofer/filesystem.go @@ -828,7 +828,7 @@ func (fs *filesystem) LinkAt(ctx context.Context, rp *vfs.ResolvingPath, vd vfs. gid := auth.KGID(d.inode.gid.Load()) uid := auth.KUID(d.inode.uid.Load()) mode := linux.FileMode(d.inode.mode.Load()) - if err := vfs.MayLink(rp.Credentials(), mode, uid, gid); err != nil { + if err := vfs.MayLink(rp.Credentials(), mode, nil, uid, gid); err != nil { return nil, err } if d.inode.nlink.Load() == 0 { @@ -1839,6 +1839,29 @@ func (fs *filesystem) RemoveXattrAt(ctx context.Context, rp *vfs.ResolvingPath, return nil } +// GetPosixACLAt implements vfs.FilesystemImpl.GetPosixACLAt. +func (fs *filesystem) GetPosixACLAt(ctx context.Context, rp *vfs.ResolvingPath, t vfs.ACLType) (*vfs.PosixACL, error) { + var ds *[]*dentry + fs.renameMu.RLock() + defer fs.renameMuRUnlockAndCheckCaching(ctx, &ds) + // gofer does not currently support POSIX ACLs. + _, err := fs.resolveLocked(ctx, rp, &ds) + return nil, err +} + +// SetPosixACLAt implements vfs.FilesystemImpl.SetPosixACLAt. +func (fs *filesystem) SetPosixACLAt(ctx context.Context, rp *vfs.ResolvingPath, t vfs.ACLType, acl *vfs.PosixACL, clearSGID bool) (*vfs.PosixACL, linux.FileMode, error) { + var ds *[]*dentry + fs.renameMu.RLock() + defer fs.renameMuRUnlockAndCheckCaching(ctx, &ds) + _, err := fs.resolveLocked(ctx, rp, &ds) + if err != nil { + return nil, 0, err + } + // gofer does not currently support POSIX ACLs. + return nil, 0, linuxerr.EOPNOTSUPP +} + // PrependPath implements vfs.FilesystemImpl.PrependPath. func (fs *filesystem) PrependPath(ctx context.Context, vfsroot, vd vfs.VirtualDentry, b *fspath.Builder) error { return genericPrependPath(fs, vfsroot, vd.Mount(), vd.Dentry().Impl().(*dentry), b) diff --git a/pkg/sentry/fsimpl/gofer/gofer.go b/pkg/sentry/fsimpl/gofer/gofer.go index 8777d8aa560..58ca20ef2ea 100644 --- a/pkg/sentry/fsimpl/gofer/gofer.go +++ b/pkg/sentry/fsimpl/gofer/gofer.go @@ -1292,7 +1292,7 @@ func (d *dentry) setStat(ctx context.Context, creds *auth.Credentials, opts *vfs return linuxerr.EPERM } mode := linux.FileMode(d.inode.mode.Load()) - if err := vfs.CheckSetStat(ctx, creds, opts, mode, auth.KUID(d.inode.uid.Load()), auth.KGID(d.inode.gid.Load())); err != nil { + if err := vfs.CheckSetStat(ctx, creds, opts, mode, nil, auth.KUID(d.inode.uid.Load()), auth.KGID(d.inode.gid.Load())); err != nil { return err } if err := mnt.CheckBeginWrite(); err != nil { @@ -1505,7 +1505,7 @@ func (i *inode) updateSizeAndUnlockDataMuLocked(newSize uint64) { } func (d *dentry) checkPermissions(creds *auth.Credentials, ats vfs.AccessTypes) error { - return vfs.GenericCheckPermissions(creds, ats, linux.FileMode(d.inode.mode.Load()), auth.KUID(d.inode.uid.Load()), auth.KGID(d.inode.gid.Load())) + return vfs.GenericCheckPermissions(creds, ats, linux.FileMode(d.inode.mode.Load()), nil, auth.KUID(d.inode.uid.Load()), auth.KGID(d.inode.gid.Load())) } // Preconditions: d.inode.metadataMu must be locked. @@ -1529,7 +1529,7 @@ func (d *dentry) checkXattrPermissions(creds *auth.Credentials, name string, ats mode := linux.FileMode(d.inode.mode.RacyLoad()) kuid := auth.KUID(d.inode.uid.RacyLoad()) kgid := auth.KGID(d.inode.gid.RacyLoad()) - if err := vfs.GenericCheckPermissions(creds, ats, mode, kuid, kgid); err != nil { + if err := vfs.GenericCheckPermissions(creds, ats, mode, nil, kuid, kgid); err != nil { return err } return vfs.CheckXattrPermissions(creds, ats, mode, kuid, name) diff --git a/pkg/sentry/fsimpl/host/host.go b/pkg/sentry/fsimpl/host/host.go index a7d374ae865..b067ec37529 100644 --- a/pkg/sentry/fsimpl/host/host.go +++ b/pkg/sentry/fsimpl/host/host.go @@ -412,7 +412,7 @@ func (i *inode) CheckPermissions(ctx context.Context, creds *auth.Credentials, a if err := i.stat(&s); err != nil { return err } - return vfs.GenericCheckPermissions(creds, ats, linux.FileMode(s.Mode), auth.KUID(s.Uid), auth.KGID(s.Gid)) + return vfs.GenericCheckPermissions(creds, ats, linux.FileMode(s.Mode), nil, auth.KUID(s.Uid), auth.KGID(s.Gid)) } // Mode implements kernfs.Inode.Mode. @@ -607,7 +607,7 @@ func (i *inode) SetStat(ctx context.Context, fs *vfs.Filesystem, creds *auth.Cre if err := i.stat(&hostStat); err != nil { return err } - if err := vfs.CheckSetStat(ctx, creds, &opts, linux.FileMode(hostStat.Mode), auth.KUID(hostStat.Uid), auth.KGID(hostStat.Gid)); err != nil { + if err := vfs.CheckSetStat(ctx, creds, &opts, linux.FileMode(hostStat.Mode), nil, auth.KUID(hostStat.Uid), auth.KGID(hostStat.Gid)); err != nil { return err } diff --git a/pkg/sentry/fsimpl/kernfs/filesystem.go b/pkg/sentry/fsimpl/kernfs/filesystem.go index 20d351cb996..62d206fdb82 100644 --- a/pkg/sentry/fsimpl/kernfs/filesystem.go +++ b/pkg/sentry/fsimpl/kernfs/filesystem.go @@ -415,7 +415,7 @@ func (fs *Filesystem) LinkAt(ctx context.Context, rp *vfs.ResolvingPath, vd vfs. if inode.Mode().IsDir() { return linuxerr.EPERM } - if err := vfs.MayLink(rp.Credentials(), inode.Mode(), inode.UID(), inode.GID()); err != nil { + if err := vfs.MayLink(rp.Credentials(), inode.Mode(), nil, inode.UID(), inode.GID()); err != nil { return err } parent.dirMu.Lock() @@ -1129,7 +1129,7 @@ func (fs *Filesystem) GetXattrAt(ctx context.Context, rp *vfs.ResolvingPath, opt mode := d.inode.Mode() kuid := d.inode.UID() kgid := d.inode.GID() - if err := vfs.GenericCheckPermissions(creds, vfs.MayRead, mode, kuid, kgid); err != nil { + if err := vfs.GenericCheckPermissions(creds, vfs.MayRead, mode, nil, kuid, kgid); err != nil { return "", err } if err := vfs.CheckXattrPermissions(creds, vfs.MayRead, mode, kuid, opts.Name); err != nil { @@ -1155,7 +1155,7 @@ func (fs *Filesystem) SetXattrAt(ctx context.Context, rp *vfs.ResolvingPath, opt mode := d.inode.Mode() kuid := d.inode.UID() kgid := d.inode.GID() - if err := vfs.GenericCheckPermissions(creds, vfs.MayWrite, mode, kuid, kgid); err != nil { + if err := vfs.GenericCheckPermissions(creds, vfs.MayWrite, mode, nil, kuid, kgid); err != nil { return err } if err := vfs.CheckXattrPermissions(creds, vfs.MayWrite, mode, kuid, opts.Name); err != nil { @@ -1181,7 +1181,7 @@ func (fs *Filesystem) RemoveXattrAt(ctx context.Context, rp *vfs.ResolvingPath, mode := d.inode.Mode() kuid := d.inode.UID() kgid := d.inode.GID() - if err := vfs.GenericCheckPermissions(creds, vfs.MayWrite, mode, kuid, kgid); err != nil { + if err := vfs.GenericCheckPermissions(creds, vfs.MayWrite, mode, nil, kuid, kgid); err != nil { return err } if err := vfs.CheckXattrPermissions(creds, vfs.MayWrite, mode, kuid, name); err != nil { @@ -1193,6 +1193,32 @@ func (fs *Filesystem) RemoveXattrAt(ctx context.Context, rp *vfs.ResolvingPath, return linuxerr.ENOTSUP } +// GetPosixACLAt implements vfs.FilesystemImpl.GetPosixACLAt. +func (fs *Filesystem) GetPosixACLAt(ctx context.Context, rp *vfs.ResolvingPath, t vfs.ACLType) (*vfs.PosixACL, error) { + fs.mu.RLock() + defer fs.processDeferredDecRefs(ctx) + defer fs.mu.RUnlock() + _, err := fs.walkExistingLocked(ctx, rp) + if err != nil { + return nil, err + } + // kernfs currently does not support POSIX ACLs. + return nil, nil +} + +// SetPosixACLAt implements vfs.FilesystemImpl.SetPosixACLAt. +func (fs *Filesystem) SetPosixACLAt(ctx context.Context, rp *vfs.ResolvingPath, t vfs.ACLType, acl *vfs.PosixACL, clearSGID bool) (*vfs.PosixACL, linux.FileMode, error) { + fs.mu.RLock() + defer fs.processDeferredDecRefs(ctx) + defer fs.mu.RUnlock() + _, err := fs.walkExistingLocked(ctx, rp) + if err != nil { + return nil, 0, err + } + // kernfs currently does not support POSIX ACLs. + return nil, 0, linuxerr.EOPNOTSUPP +} + // PrependPath implements vfs.FilesystemImpl.PrependPath. func (fs *Filesystem) PrependPath(ctx context.Context, vfsroot, vd vfs.VirtualDentry, b *fspath.Builder) error { return genericPrependPath(fs, vfsroot, vd.Mount(), vd.Dentry().Impl().(*Dentry), b) diff --git a/pkg/sentry/fsimpl/kernfs/inode_impl_util.go b/pkg/sentry/fsimpl/kernfs/inode_impl_util.go index 9308cddae4d..5defec6ae32 100644 --- a/pkg/sentry/fsimpl/kernfs/inode_impl_util.go +++ b/pkg/sentry/fsimpl/kernfs/inode_impl_util.go @@ -310,7 +310,7 @@ func (a *InodeAttrs) SetStat(ctx context.Context, fs *vfs.Filesystem, creds *aut if opts.Stat.Mask&linux.STATX_SIZE != 0 && a.Mode().IsDir() { return linuxerr.EISDIR } - if err := vfs.CheckSetStat(ctx, creds, &opts, a.Mode(), auth.KUID(a.uid.Load()), auth.KGID(a.gid.Load())); err != nil { + if err := vfs.CheckSetStat(ctx, creds, &opts, a.Mode(), nil, auth.KUID(a.uid.Load()), auth.KGID(a.gid.Load())); err != nil { return err } @@ -373,6 +373,7 @@ func (a *InodeAttrs) CheckPermissions(_ context.Context, creds *auth.Credentials creds, ats, a.Mode(), + nil, auth.KUID(a.uid.Load()), auth.KGID(a.gid.Load()), ) diff --git a/pkg/sentry/fsimpl/overlay/filesystem.go b/pkg/sentry/fsimpl/overlay/filesystem.go index c3cd9711e17..13aab59237b 100644 --- a/pkg/sentry/fsimpl/overlay/filesystem.go +++ b/pkg/sentry/fsimpl/overlay/filesystem.go @@ -1500,7 +1500,7 @@ func (fs *filesystem) SetStatAt(ctx context.Context, rp *vfs.ResolvingPath, opts // Precondition: d.fs.renameMu must be held for reading. func (d *dentry) setStatLocked(ctx context.Context, rp *vfs.ResolvingPath, opts vfs.SetStatOptions) error { mode := linux.FileMode(d.mode.Load()) - if err := vfs.CheckSetStat(ctx, rp.Credentials(), &opts, mode, auth.KUID(d.uid.Load()), auth.KGID(d.gid.Load())); err != nil { + if err := vfs.CheckSetStat(ctx, rp.Credentials(), &opts, mode, nil, auth.KUID(d.uid.Load()), auth.KGID(d.gid.Load())); err != nil { return err } mnt := rp.Mount() @@ -1862,6 +1862,29 @@ func (fs *filesystem) removeXattrLocked(ctx context.Context, d *dentry, mnt *vfs return vfsObj.RemoveXattrAt(ctx, fs.creds, &vfs.PathOperation{Root: d.upperVD, Start: d.upperVD}, name) } +// GetPosixACLAt implements vfs.FilesystemImpl.GetPosixACLAt. +func (fs *filesystem) GetPosixACLAt(ctx context.Context, rp *vfs.ResolvingPath, t vfs.ACLType) (*vfs.PosixACL, error) { + var ds *[]*dentry + fs.renameMu.RLock() + defer fs.renameMuRUnlockAndCheckDrop(ctx, &ds) + // overlayfs does not currently support POSIX ACLs. + _, err := fs.resolveLocked(ctx, rp, &ds) + return nil, err +} + +// SetPosixACLAt implements vfs.FilesystemImpl.SetPosixACLAt. +func (fs *filesystem) SetPosixACLAt(ctx context.Context, rp *vfs.ResolvingPath, t vfs.ACLType, acl *vfs.PosixACL, clearSGID bool) (*vfs.PosixACL, linux.FileMode, error) { + var ds *[]*dentry + fs.renameMu.RLock() + defer fs.renameMuRUnlockAndCheckDrop(ctx, &ds) + _, err := fs.resolveLocked(ctx, rp, &ds) + if err != nil { + return nil, 0, err + } + // overlayfs does not currently support POSIX ACLs. + return nil, 0, linuxerr.EOPNOTSUPP +} + // PrependPath implements vfs.FilesystemImpl.PrependPath. func (fs *filesystem) PrependPath(ctx context.Context, vfsroot, vd vfs.VirtualDentry, b *fspath.Builder) error { return genericPrependPath(fs, vfsroot, vd.Mount(), vd.Dentry().Impl().(*dentry), b) diff --git a/pkg/sentry/fsimpl/overlay/overlay.go b/pkg/sentry/fsimpl/overlay/overlay.go index 0bc0c30b332..6c96834bee8 100644 --- a/pkg/sentry/fsimpl/overlay/overlay.go +++ b/pkg/sentry/fsimpl/overlay/overlay.go @@ -872,14 +872,14 @@ func (d *dentry) topLookupLayer() lookupLayer { } func (d *dentry) checkPermissions(creds *auth.Credentials, ats vfs.AccessTypes) error { - return vfs.GenericCheckPermissions(creds, ats, linux.FileMode(d.mode.Load()), auth.KUID(d.uid.Load()), auth.KGID(d.gid.Load())) + return vfs.GenericCheckPermissions(creds, ats, linux.FileMode(d.mode.Load()), nil, auth.KUID(d.uid.Load()), auth.KGID(d.gid.Load())) } func (d *dentry) checkXattrPermissions(creds *auth.Credentials, name string, ats vfs.AccessTypes) error { mode := linux.FileMode(d.mode.Load()) kuid := auth.KUID(d.uid.Load()) kgid := auth.KGID(d.gid.Load()) - if err := vfs.GenericCheckPermissions(creds, ats, mode, kuid, kgid); err != nil { + if err := vfs.GenericCheckPermissions(creds, ats, mode, nil, kuid, kgid); err != nil { return err } return vfs.CheckXattrPermissions(creds, ats, mode, kuid, name) @@ -1004,6 +1004,18 @@ func (fd *fileDescription) RemoveXattr(ctx context.Context, name string) error { return fs.removeXattrLocked(ctx, fd.dentry(), fd.vfsfd.Mount(), auth.CredentialsFromContext(ctx), name) } +// GetPosixACL implements vfs.FileDescriptionImpl.GetPosixACL. +func (fd *fileDescription) GetPosixACL(ctx context.Context, t vfs.ACLType) (*vfs.PosixACL, error) { + // overlayfs does not yet support POSIX ACLs. + return nil, nil +} + +// SetPosixACL implements vfs.FileDescriptionImpl.SetPosixACL. +func (fd *fileDescription) SetPosixACL(ctx context.Context, t vfs.ACLType, acl *vfs.PosixACL, clearSGID bool) (*vfs.PosixACL, linux.FileMode, error) { + // overlayfs does not yet support POSIX ACLs. + return nil, 0, linuxerr.EOPNOTSUPP +} + // IsCopiedUp returns true if the given vfs.Dentry is an overlayfs dentry that has // been copied up to the upper layer. func IsCopiedUp(d *vfs.Dentry) bool { diff --git a/pkg/sentry/fsimpl/overlay/regular_file.go b/pkg/sentry/fsimpl/overlay/regular_file.go index c752ca657b0..df5c0be7598 100644 --- a/pkg/sentry/fsimpl/overlay/regular_file.go +++ b/pkg/sentry/fsimpl/overlay/regular_file.go @@ -167,7 +167,7 @@ func (fd *regularFileFD) Allocate(ctx context.Context, mode, offset, length uint func (fd *regularFileFD) SetStat(ctx context.Context, opts vfs.SetStatOptions) error { d := fd.dentry() mode := linux.FileMode(d.mode.Load()) - if err := vfs.CheckSetStat(ctx, auth.CredentialsFromContext(ctx), &opts, mode, auth.KUID(d.uid.Load()), auth.KGID(d.gid.Load())); err != nil { + if err := vfs.CheckSetStat(ctx, auth.CredentialsFromContext(ctx), &opts, mode, nil, auth.KUID(d.uid.Load()), auth.KGID(d.gid.Load())); err != nil { return err } mnt := fd.vfsfd.Mount() diff --git a/pkg/sentry/fsimpl/pipefs/pipefs.go b/pkg/sentry/fsimpl/pipefs/pipefs.go index b332fdbfbbe..c1c7f4cd1d0 100644 --- a/pkg/sentry/fsimpl/pipefs/pipefs.go +++ b/pkg/sentry/fsimpl/pipefs/pipefs.go @@ -125,7 +125,7 @@ const pipeMode = 0600 | linux.S_IFIFO func (i *inode) CheckPermissions(ctx context.Context, creds *auth.Credentials, ats vfs.AccessTypes) error { i.attrMu.Lock() defer i.attrMu.Unlock() - return vfs.GenericCheckPermissions(creds, ats, pipeMode, i.uid, i.gid) + return vfs.GenericCheckPermissions(creds, ats, pipeMode, nil, i.uid, i.gid) } // Mode implements kernfs.Inode.Mode. @@ -177,7 +177,7 @@ func (i *inode) SetStat(ctx context.Context, vfsfs *vfs.Filesystem, creds *auth. } i.attrMu.Lock() defer i.attrMu.Unlock() - if err := vfs.CheckSetStat(ctx, creds, &opts, pipeMode, auth.KUID(i.uid), auth.KGID(i.gid)); err != nil { + if err := vfs.CheckSetStat(ctx, creds, &opts, pipeMode, nil, auth.KUID(i.uid), auth.KGID(i.gid)); err != nil { return err } if opts.Stat.Mask&linux.STATX_UID != 0 { diff --git a/pkg/sentry/fsimpl/proc/task.go b/pkg/sentry/fsimpl/proc/task.go index ad583dd2b78..69c94a61ffe 100644 --- a/pkg/sentry/fsimpl/proc/task.go +++ b/pkg/sentry/fsimpl/proc/task.go @@ -267,7 +267,7 @@ func (i *taskOwnedInode) Stat(ctx context.Context, fs *vfs.Filesystem, opts vfs. func (i *taskOwnedInode) CheckPermissions(ctx context.Context, creds *auth.Credentials, ats vfs.AccessTypes) error { mode := i.Mode() uid, gid := i.getOwner(mode) - return vfs.GenericCheckPermissions(creds, ats, mode, uid, gid) + return vfs.GenericCheckPermissions(creds, ats, mode, nil, uid, gid) } func (i *taskOwnedInode) getOwner(mode linux.FileMode) (auth.KUID, auth.KGID) { diff --git a/pkg/sentry/fsimpl/tmpfs/BUILD b/pkg/sentry/fsimpl/tmpfs/BUILD index a622d952488..3ccab4d8764 100644 --- a/pkg/sentry/fsimpl/tmpfs/BUILD +++ b/pkg/sentry/fsimpl/tmpfs/BUILD @@ -101,6 +101,7 @@ go_library( ], imports = [ "gvisor.dev/gvisor/pkg/sentry/checkpoint", + "gvisor.dev/gvisor/pkg/sentry/vfs", ], marshal = True, visibility = ["//pkg/sentry:internal"], diff --git a/pkg/sentry/fsimpl/tmpfs/device_file.go b/pkg/sentry/fsimpl/tmpfs/device_file.go index 0cae4ec4c14..056c4b05506 100644 --- a/pkg/sentry/fsimpl/tmpfs/device_file.go +++ b/pkg/sentry/fsimpl/tmpfs/device_file.go @@ -51,6 +51,11 @@ func isOvlWhiteoutDev(mode linux.FileMode, major, minor uint32) bool { linux.MakeDeviceID(uint16(major), minor) == linux.WHITEOUT_DEV } +// newDeviceFileLocked creates a new device file. +// +// If parentDir is not nil, certain fields (such as setgid and default ACL) will be inherited +// from parentDir. +// // Precondition: fs.mu must be locked for writing. func (fs *filesystem) newDeviceFileLocked(kuid auth.KUID, kgid auth.KGID, mode linux.FileMode, major, minor uint32, parentDir *directory) (*inode, error) { ovlWhiteout := isOvlWhiteoutDev(mode, major, minor) diff --git a/pkg/sentry/fsimpl/tmpfs/directory.go b/pkg/sentry/fsimpl/tmpfs/directory.go index 752c485439d..f9ceb420da1 100644 --- a/pkg/sentry/fsimpl/tmpfs/directory.go +++ b/pkg/sentry/fsimpl/tmpfs/directory.go @@ -47,6 +47,10 @@ type directory struct { childList dentryList } +// newDirectory creates a new directory. +// +// If parentDir is not nil, certain fields (such as setgid and default ACL) will be inherited +// from parentDir. func (fs *filesystem) newDirectory(kuid auth.KUID, kgid auth.KGID, mode linux.FileMode, parentDir *directory) (*directory, error) { dir := &directory{} err := dir.inode.init(dir, fs, kuid, kgid, linux.S_IFDIR|mode, parentDir) diff --git a/pkg/sentry/fsimpl/tmpfs/filesystem.go b/pkg/sentry/fsimpl/tmpfs/filesystem.go index beb297783c5..acfc4095b72 100644 --- a/pkg/sentry/fsimpl/tmpfs/filesystem.go +++ b/pkg/sentry/fsimpl/tmpfs/filesystem.go @@ -280,7 +280,7 @@ func (fs *filesystem) LinkAt(ctx context.Context, rp *vfs.ResolvingPath, vd vfs. if i.isDir() { return linuxerr.EPERM } - if err := vfs.MayLink(auth.CredentialsFromContext(ctx), linux.FileMode(i.mode.Load()), auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())); err != nil { + if err := vfs.MayLink(auth.CredentialsFromContext(ctx), linux.FileMode(i.mode.Load()), i.accessACL.Load(), auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())); err != nil { return err } if i.nlink.Load() == 0 { @@ -974,6 +974,37 @@ func (fs *filesystem) RemoveXattrAt(ctx context.Context, rp *vfs.ResolvingPath, return nil } +// GetPosixACLAt implements vfs.FilesystemImpl.GetPosixACLAt. +func (fs *filesystem) GetPosixACLAt(ctx context.Context, rp *vfs.ResolvingPath, t vfs.ACLType) (*vfs.PosixACL, error) { + fs.mu.RLock() + defer fs.mu.RUnlock() + d, err := resolveLocked(ctx, rp) + if err != nil { + return nil, err + } + + switch t { + case vfs.AccessACL: + return d.inode.accessACL.Load(), nil + case vfs.DefaultACL: + return d.inode.defaultACL.Load(), nil + } + + return nil, linuxerr.EINVAL +} + +// SetPosixACLAt implements vfs.FilesystemImpl.SetPosixACLAt. +func (fs *filesystem) SetPosixACLAt(ctx context.Context, rp *vfs.ResolvingPath, t vfs.ACLType, acl *vfs.PosixACL, clearSGID bool) (*vfs.PosixACL, linux.FileMode, error) { + fs.mu.RLock() + defer fs.mu.RUnlock() + d, err := resolveLocked(ctx, rp) + if err != nil { + return nil, 0, err + } + + return d.inode.setPosixACL(rp.Credentials(), t, acl, clearSGID) +} + // PrependPath implements vfs.FilesystemImpl.PrependPath. func (fs *filesystem) PrependPath(ctx context.Context, vfsroot, vd vfs.VirtualDentry, b *fspath.Builder) error { d := vd.Dentry().Impl().(*dentry) diff --git a/pkg/sentry/fsimpl/tmpfs/named_pipe.go b/pkg/sentry/fsimpl/tmpfs/named_pipe.go index b0198af782b..d7b5b6f56b8 100644 --- a/pkg/sentry/fsimpl/tmpfs/named_pipe.go +++ b/pkg/sentry/fsimpl/tmpfs/named_pipe.go @@ -28,6 +28,11 @@ type namedPipe struct { pipe *pipe.VFSPipe } +// newNamedPipe creates a new named pipe. +// +// If parentDir is not nil, certain fields (such as setgid and default ACL) will be inherited +// from parentDir. +// // Preconditions: // - fs.mu must be locked. // - rp.Mount().CheckBeginWrite() has been called successfully. diff --git a/pkg/sentry/fsimpl/tmpfs/regular_file.go b/pkg/sentry/fsimpl/tmpfs/regular_file.go index fa55922a6ed..4fec65831ed 100644 --- a/pkg/sentry/fsimpl/tmpfs/regular_file.go +++ b/pkg/sentry/fsimpl/tmpfs/regular_file.go @@ -112,6 +112,10 @@ type regularFile struct { size atomicbitops.Uint64 } +// newRegularFile creates a new regular file. +// +// If parentDir is not nil, certain fields (such as setgid and default ACL) will be inherited +// from parentDir. func (fs *filesystem) newRegularFile(kuid auth.KUID, kgid auth.KGID, mode linux.FileMode, parentDir *directory) (*inode, error) { file := ®ularFile{ memoryUsageKind: fs.usage, diff --git a/pkg/sentry/fsimpl/tmpfs/save_restore.go b/pkg/sentry/fsimpl/tmpfs/save_restore.go index f7763bfc966..a12095887e5 100644 --- a/pkg/sentry/fsimpl/tmpfs/save_restore.go +++ b/pkg/sentry/fsimpl/tmpfs/save_restore.go @@ -61,6 +61,26 @@ func (d *dentry) loadParent(_ goContext.Context, parent *dentry) { d.parent.Store(parent) } +// saveAccessACL is called by stateify. +func (i *inode) saveAccessACL() *vfs.PosixACL { + return i.accessACL.Load() +} + +// loadAccessACL is called by stateify. +func (i *inode) loadAccessACL(_ goContext.Context, acl *vfs.PosixACL) { + i.accessACL.Store(acl) +} + +// saveDefaultACL is called by stateify. +func (i *inode) saveDefaultACL() *vfs.PosixACL { + return i.defaultACL.Load() +} + +// loadDefaultACL is called by stateify. +func (i *inode) loadDefaultACL(_ goContext.Context, defaultACL *vfs.PosixACL) { + i.defaultACL.Store(defaultACL) +} + // PrepareSave implements vfs.FilesystemImplSaveRestoreExtension.PrepareSave. func (fs *filesystem) PrepareSave(ctx context.Context) error { resourceID := fs.mf.ResourceID() diff --git a/pkg/sentry/fsimpl/tmpfs/socket_file.go b/pkg/sentry/fsimpl/tmpfs/socket_file.go index 7a59f20394e..f7e4b67fbfa 100644 --- a/pkg/sentry/fsimpl/tmpfs/socket_file.go +++ b/pkg/sentry/fsimpl/tmpfs/socket_file.go @@ -29,6 +29,10 @@ type socketFile struct { ep transport.BoundEndpoint } +// newSocketFile creates a new socket file. +// +// If parentDir is not nil, certain fields (such as setgid and default ACL) will be inherited +// from parentDir. func (fs *filesystem) newSocketFile(kuid auth.KUID, kgid auth.KGID, mode linux.FileMode, ep transport.BoundEndpoint, parentDir *directory) (*inode, error) { file := &socketFile{ep: ep} err := file.inode.init(file, fs, kuid, kgid, mode, parentDir) diff --git a/pkg/sentry/fsimpl/tmpfs/symlink.go b/pkg/sentry/fsimpl/tmpfs/symlink.go index e82624f86c7..8e03d08de32 100644 --- a/pkg/sentry/fsimpl/tmpfs/symlink.go +++ b/pkg/sentry/fsimpl/tmpfs/symlink.go @@ -26,6 +26,10 @@ type symlink struct { target string // immutable } +// newSymlink creates a new symbolic link. +// +// If parentDir is not nil, certain fields (such as setgid) will be inherited +// from parentDir. func (fs *filesystem) newSymlink(kuid auth.KUID, kgid auth.KGID, mode linux.FileMode, target string, parentDir *directory) (*inode, error) { link := &symlink{ target: target, diff --git a/pkg/sentry/fsimpl/tmpfs/tar.go b/pkg/sentry/fsimpl/tmpfs/tar.go index 8ca79344bf9..54d92bccb11 100644 --- a/pkg/sentry/fsimpl/tmpfs/tar.go +++ b/pkg/sentry/fsimpl/tmpfs/tar.go @@ -17,6 +17,7 @@ package tmpfs import ( "archive/tar" "bytes" + "encoding/base64" "fmt" "io" "os" @@ -142,6 +143,12 @@ func (fs *filesystem) mkdirFromTar(hdr *tar.Header, pathToInode map[string]*inod ino := fs.root.inode ino.uid.Store(uint32(hdr.Uid)) ino.gid.Store(uint32(hdr.Gid)) + acl, defaultACL, err := getACLsFromHeader(hdr) + if err != nil { + return nil, err + } + ino.accessACL.Store(acl) + ino.defaultACL.Store(defaultACL) ino.mode.Store(uint32(hdr.Mode) | linux.S_IFDIR) ino.mtime.Store(hdr.ModTime.UnixNano()) ino.setXattrsFromPAXRecords(hdr) @@ -168,10 +175,16 @@ func (fs *filesystem) mkdirFromTar(hdr *tar.Header, pathToInode map[string]*inod if !ok { return nil, fmt.Errorf("parent inode at %v is not a directory", dir) } - childDir, err := fs.newDirectory(auth.KUID(hdr.Uid), auth.KGID(hdr.Gid), linux.FileMode(hdr.Mode), parentDir) + childDir, err := fs.newDirectory(auth.KUID(hdr.Uid), auth.KGID(hdr.Gid), linux.FileMode(hdr.Mode), nil /* parentDir */) if err != nil { return nil, fmt.Errorf("failed to create new directory inode: %v", err) } + acl, defaultACL, err := getACLsFromHeader(hdr) + if err != nil { + return nil, err + } + childDir.inode.accessACL.Store(acl) + childDir.inode.defaultACL.Store(defaultACL) parentDir.inode.incLinksLocked() // from child's ".." parentDir.inode.incRef() // child directory holds a reference to parent childDir.inode.mtime.Store(hdr.ModTime.UnixNano()) @@ -198,19 +211,25 @@ func (fs *filesystem) mknodFromTar(ctx context.Context, hdr *tar.Header, pathToI var err error switch hdr.Typeflag { case tar.TypeReg: - childInode, err = fs.newRegularFile(auth.KUID(hdr.Uid), auth.KGID(hdr.Gid), linux.FileMode(hdr.Mode), parentDir) + childInode, err = fs.newRegularFile(auth.KUID(hdr.Uid), auth.KGID(hdr.Gid), linux.FileMode(hdr.Mode), nil /* parentDir */) case tar.TypeFifo: - childInode, err = fs.newNamedPipe(auth.KUID(hdr.Uid), auth.KGID(hdr.Gid), linux.FileMode(hdr.Mode), parentDir) + childInode, err = fs.newNamedPipe(auth.KUID(hdr.Uid), auth.KGID(hdr.Gid), linux.FileMode(hdr.Mode), nil /* parentDir */) case tar.TypeBlock: - childInode, err = fs.newDeviceFileLocked(auth.KUID(hdr.Uid), auth.KGID(hdr.Gid), linux.FileMode(hdr.Mode|linux.S_IFBLK), uint32(hdr.Devmajor), uint32(hdr.Devminor), parentDir) + childInode, err = fs.newDeviceFileLocked(auth.KUID(hdr.Uid), auth.KGID(hdr.Gid), linux.FileMode(hdr.Mode|linux.S_IFBLK), uint32(hdr.Devmajor), uint32(hdr.Devminor), nil /* parentDir */) case tar.TypeChar: - childInode, err = fs.newDeviceFileLocked(auth.KUID(hdr.Uid), auth.KGID(hdr.Gid), linux.FileMode(hdr.Mode|linux.S_IFCHR), uint32(hdr.Devmajor), uint32(hdr.Devminor), parentDir) + childInode, err = fs.newDeviceFileLocked(auth.KUID(hdr.Uid), auth.KGID(hdr.Gid), linux.FileMode(hdr.Mode|linux.S_IFCHR), uint32(hdr.Devmajor), uint32(hdr.Devminor), nil /* parentDir */) default: return fmt.Errorf("mknod unsupported file type %v for %v", hdr.Typeflag, hdr.Name) } if err != nil { return err } + acl, defaultACL, err := getACLsFromHeader(hdr) + if err != nil { + return err + } + childInode.accessACL.Store(acl) + childInode.defaultACL.Store(defaultACL) childInode.mtime.Store(hdr.ModTime.UnixNano()) childInode.setXattrsFromPAXRecords(hdr) child := fs.newDentry(childInode) @@ -273,11 +292,17 @@ func (fs *filesystem) symlinkFromTar(hdr *tar.Header, pathToInode map[string]*in return fmt.Errorf("tmpfs: insufficient space to account for symlink target %q", hdr.Name) } } - childInode, err := fs.newSymlink(auth.KUID(hdr.Uid), auth.KGID(hdr.Gid), 0777, hdr.Linkname, parentDir) + childInode, err := fs.newSymlink(auth.KUID(hdr.Uid), auth.KGID(hdr.Gid), 0777, hdr.Linkname, nil /* parentDir */) if err != nil { return fmt.Errorf("failed to create inode from tar: %v", err) } child := fs.newDentry(childInode) + acl, defaultACL, err := getACLsFromHeader(hdr) + if err != nil { + return err + } + child.inode.accessACL.Store(acl) + child.inode.defaultACL.Store(defaultACL) child.inode.mtime.Store(hdr.ModTime.UnixNano()) child.inode.setXattrsFromPAXRecords(hdr) parentDir.insertChildLocked(child, name) @@ -469,6 +494,20 @@ func (d *dentry) createTarHeader(path string, inoToPath map[uint64]string, cb ta } } + // Serialize POSIX ACLs to PAXRecords. + if acl := d.inode.accessACL.Load(); acl != nil { + if header.PAXRecords == nil { + header.PAXRecords = make(map[string]string) + } + header.PAXRecords[paxXattrPrefix+linux.XATTR_NAME_POSIX_ACL_ACCESS] = aclToPAXRecord(acl) + } + if acl := d.inode.defaultACL.Load(); acl != nil { + if header.PAXRecords == nil { + header.PAXRecords = make(map[string]string) + } + header.PAXRecords[paxXattrPrefix+linux.XATTR_NAME_POSIX_ACL_DEFAULT] = aclToPAXRecord(acl) + } + inoToPath[d.inode.ino] = path return header, nil } @@ -511,3 +550,49 @@ func (tarDefaultWriterCallbacks) regularFileWrite(ctx context.Context, rf *regul } return nil } + +func aclToPAXRecord(acl *vfs.PosixACL) string { + // We can use a "fake" root userns for this serialization + // since the values are KUIDs anyway. + data := acl.Serialize(auth.NewRootUserNamespace()) + + // Base64 encode the ACLs since PaxRecords must be UTF-8 strings. + // Omit padding characters (RawStdEncoding) to avoid '=' chars + // as well (which tar PAXRecords disallows). + dataEncoded := base64.RawStdEncoding.EncodeToString(data) + + return dataEncoded +} + +func aclFromHeader(hdr *tar.Header, aclName string) (*vfs.PosixACL, error) { + record, ok := hdr.PAXRecords[paxXattrPrefix+aclName] + if !ok { + return nil, nil + } + + dataDecoded, err := base64.RawStdEncoding.DecodeString(record) + if err != nil { + return &vfs.PosixACL{}, err + } + + acl, err := vfs.ParsePosixACL(dataDecoded, auth.NewRootUserNamespace()) + if err != nil { + return &vfs.PosixACL{}, err + } + + return acl, nil +} + +func getACLsFromHeader(hdr *tar.Header) (*vfs.PosixACL, *vfs.PosixACL, error) { + acl, err := aclFromHeader(hdr, linux.XATTR_NAME_POSIX_ACL_ACCESS) + if err != nil { + return nil, nil, err + } + + defaultACL, err := aclFromHeader(hdr, linux.XATTR_NAME_POSIX_ACL_DEFAULT) + if err != nil { + return nil, nil, err + } + + return acl, defaultACL, nil +} diff --git a/pkg/sentry/fsimpl/tmpfs/tmpfs.go b/pkg/sentry/fsimpl/tmpfs/tmpfs.go index 184ad3511e7..adbc23aecfd 100644 --- a/pkg/sentry/fsimpl/tmpfs/tmpfs.go +++ b/pkg/sentry/fsimpl/tmpfs/tmpfs.go @@ -232,14 +232,16 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt disableDefaultSizeLimit := false newFSType := vfs.FilesystemType(&fstype) - // By default we support only "trusted" and "user" namespaces. Linux - // also supports "security" and (if configured) POSIX ACL namespaces - // "system.posix_acl_access" and "system.posix_acl_default". + // By default we support only "trusted", "user", and "system" namespaces. + // Linux also supports "security". allowXattrPrefix := map[string]struct{}{ linux.XATTR_TRUSTED_PREFIX: {}, linux.XATTR_USER_PREFIX: {}, // Only the "security.capability" xattr is supported. linux.XATTR_SECURITY_PREFIX: {}, + // Only the system.posix_acl_access and system.posix_acl_default + // xattrs are supported. + linux.XATTR_SYSTEM_PREFIX: {}, } tmpfsOpts, tmpfsOptsOk := opts.InternalData.(FilesystemOpts) @@ -631,12 +633,14 @@ type inode struct { // Inode metadata. Writing multiple fields atomically requires holding // mu, otherwise atomic operations can be used. - mu inodeMutex `state:"nosave"` - mode atomicbitops.Uint32 // file type and mode - nlink atomicbitops.Uint32 // protected by filesystem.mu instead of inode.mu - uid atomicbitops.Uint32 // auth.KUID, but stored as raw uint32 for sync/atomic - gid atomicbitops.Uint32 // auth.KGID, but ... - ino uint64 // immutable + mu inodeMutex `state:"nosave"` + mode atomicbitops.Uint32 // file type and mode + accessACL atomic.Pointer[vfs.PosixACL] `state:".(*vfs.PosixACL)"` + defaultACL atomic.Pointer[vfs.PosixACL] `state:".(*vfs.PosixACL)"` + nlink atomicbitops.Uint32 // protected by filesystem.mu instead of inode.mu + uid atomicbitops.Uint32 // auth.KUID, but stored as raw uint32 for sync/atomic + gid atomicbitops.Uint32 // auth.KGID, but ... + ino uint64 // immutable atime atomicbitops.Int64 // nanoseconds btime atomicbitops.Int64 // nanoseconds @@ -655,6 +659,8 @@ const maxLinks = math.MaxUint32 // init creates an inode and increments the total inode count associated with the filesystem. // Returns ENOSPC if the maximum inode count has been exhausted and no more inodes can be allocated. +// +// If parentDir is not nil, the setgid bit and default ACL will be inherited from parentDir. func (i *inode) init(impl any, fs *filesystem, kuid auth.KUID, kgid auth.KGID, mode linux.FileMode, parentDir *directory) error { if mode.FileType() == 0 { panic("file type is required in FileMode") @@ -673,8 +679,35 @@ func (i *inode) init(impl any, fs *filesystem, kuid auth.KUID, kgid auth.KGID, m } } - i.fs = fs + // Inherit the parent's default mode and ACL as appropriate i.mode = atomicbitops.FromUint32(uint32(mode)) + if parentDir != nil && mode.FileType() != linux.ModeSymlink { + parentDefaultACL := parentDir.inode.defaultACL.Load() + + if parentDefaultACL != nil { + // TODO(gvisor.dev/issues/13688): Linux only masks in the process's umask when creating + // in a directory with no default ACL. In gVisor, the umask is masked in by the syscall + // layer and not passed along to VFS and the filesystems, so skipping the umask in this + // case will require some refactoring. + // + // This will never lead to overly-permissive file permissions, only too-strict. + + newACL := parentDefaultACL.MaskNewFileMode(uint16(mode)) + equivMode, equiv := newACL.Mode() + if !equiv { + i.accessACL.Store(&newACL) + } + newMode := (uint16(equivMode) & linux.PermissionsMask) | (uint16(mode) &^ linux.PermissionsMask) + i.mode = atomicbitops.FromUint32(uint32(newMode)) + + if mode.IsDir() { + i.defaultACL.Store(parentDefaultACL) + } + + } + } + + i.fs = fs i.uid = atomicbitops.FromUint32(uint32(kuid)) i.gid = atomicbitops.FromUint32(uint32(kgid)) i.ino = fs.nextInoMinusOne.Add(1) @@ -759,7 +792,8 @@ func (i *inode) decRef(ctx context.Context) { func (i *inode) checkPermissions(creds *auth.Credentials, ats vfs.AccessTypes) error { mode := linux.FileMode(i.mode.Load()) - return vfs.GenericCheckPermissions(creds, ats, mode, auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())) + acl := i.accessACL.Load() + return vfs.GenericCheckPermissions(creds, ats, mode, acl, auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())) } // Go won't inline this function, and returning linux.Statx (which is quite @@ -817,7 +851,7 @@ func (i *inode) setStat(ctx context.Context, creds *auth.Credentials, opts *vfs. return linuxerr.EPERM } mode := linux.FileMode(i.mode.Load()) - if err := vfs.CheckSetStat(ctx, creds, opts, mode, auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())); err != nil { + if err := vfs.CheckSetStat(ctx, creds, opts, mode, i.accessACL.Load(), auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())); err != nil { return err } @@ -853,18 +887,34 @@ func (i *inode) setStat(ctx context.Context, creds *auth.Credentials, opts *vfs. clearSID := opts.ClearPrivs if mask&linux.STATX_MODE != 0 { + // Compute a new ACL (and equivalent mode) from the specified stat.Mode. + acl := i.accessACL.Load() + newPerm := stat.Mode & linux.PermissionsMask + if acl != nil { + newACL := acl.Chmod(uint16(stat.Mode & linux.PermissionsMask)) + var equiv bool + newPerm, equiv = newACL.Mode() + + if !equiv { + i.accessACL.Store(&newACL) + } else { + i.accessACL.Store(nil) + } + } + + // Swap in the new (or equivalent, if updating an ACL) mode. for { - old := i.mode.Load() - ft := old & linux.S_IFMT - newMode := ft | uint32(stat.Mode & ^uint16(linux.S_IFMT)) + oldMode := i.mode.Load() + newMode := uint32(oldMode&linux.FileTypeMask) | uint32(stat.Mode&^(linux.FileTypeMask|linux.PermissionsMask)) | uint32(newPerm&linux.PermissionsMask) if clearSID { newMode = vfs.ClearSUIDAndSGID(newMode) } - if swapped := i.mode.CompareAndSwap(old, newMode); swapped { + if swapped := i.mode.CompareAndSwap(oldMode, newMode); swapped { clearSID = false break } } + needsCtimeBump = true } now := i.fs.clock.Now().Nanoseconds() @@ -1012,7 +1062,19 @@ func (i *inode) checkXattrPrefix(name string) error { } func (i *inode) listXattr(creds *auth.Credentials, size uint64) ([]string, error) { - return i.xattrs.ListXattr(creds, size) + xattrs, err := i.xattrs.ListXattr(creds, size) + if err != nil { + return nil, err + } + + if i.accessACL.Load() != nil { + xattrs = append(xattrs, linux.XATTR_NAME_POSIX_ACL_ACCESS) + } + if i.defaultACL.Load() != nil { + xattrs = append(xattrs, linux.XATTR_NAME_POSIX_ACL_DEFAULT) + } + + return xattrs, nil } func (i *inode) getXattr(creds *auth.Credentials, opts *vfs.GetXattrOptions) (string, error) { @@ -1022,9 +1084,36 @@ func (i *inode) getXattr(creds *auth.Credentials, opts *vfs.GetXattrOptions) (st mode := linux.FileMode(i.mode.Load()) kuid := auth.KUID(i.uid.Load()) kgid := auth.KGID(i.gid.Load()) - if err := vfs.GenericCheckPermissions(creds, vfs.MayRead, mode, kuid, kgid); err != nil { + + // Handle POSIX ACL xattrs + if strings.HasPrefix(opts.Name, linux.XATTR_SYSTEM_PREFIX) { + var acl *vfs.PosixACL + switch opts.Name { + case linux.XATTR_NAME_POSIX_ACL_ACCESS: + acl = i.accessACL.Load() + case linux.XATTR_NAME_POSIX_ACL_DEFAULT: + acl = i.defaultACL.Load() + default: + return "", linuxerr.EOPNOTSUPP + } + + if mode.FileType() == linux.ModeSymlink { + return "", linuxerr.EOPNOTSUPP + } + + if acl == nil { + return "", linuxerr.ENODATA + } + + // Serialize the access ACL for userspace + return string(acl.Serialize(creds.UserNamespace)), nil + } + + acl := i.accessACL.Load() + if err := vfs.GenericCheckPermissions(creds, vfs.MayRead, mode, acl, kuid, kgid); err != nil { return "", err } + return i.xattrs.GetXattr(creds, mode, kuid, opts) } @@ -1033,9 +1122,52 @@ func (i *inode) setXattr(creds *auth.Credentials, opts *vfs.SetXattrOptions) err return err } mode := linux.FileMode(i.mode.Load()) + acl := i.accessACL.Load() kuid := auth.KUID(i.uid.Load()) kgid := auth.KGID(i.gid.Load()) - if err := vfs.GenericCheckPermissions(creds, vfs.MayWrite, mode, kuid, kgid); err != nil { + + if strings.HasPrefix(opts.Name, linux.XATTR_SYSTEM_PREFIX) { + // Handle POSIX ACLs + + var aclType vfs.ACLType + switch opts.Name { + case linux.XATTR_NAME_POSIX_ACL_ACCESS: + aclType = vfs.AccessACL + case linux.XATTR_NAME_POSIX_ACL_DEFAULT: + aclType = vfs.DefaultACL + default: + return linuxerr.EOPNOTSUPP + } + + if mode.FileType() == linux.ModeSymlink { + // ACLs cannot be set on symlinks + return linuxerr.EOPNOTSUPP + } + + // Parse the ACL from userspace + acl, err := vfs.ParsePosixACL([]byte(opts.Value), creds.UserNamespace) + if err != nil { + return err + } + + if aclType == vfs.DefaultACL && !mode.IsDir() { + if acl != nil { + // Default ACL can only be set on directories + return linuxerr.EACCES + } + return nil + } + + if !vfs.CanActAsOwner(creds, kuid) { + return linuxerr.EPERM + } + + // Set the ACL + _, _, err = i.setPosixACL(creds, aclType, acl, true /* clearSGID */) + return err + } + + if err := vfs.GenericCheckPermissions(creds, vfs.MayWrite, mode, acl, kuid, kgid); err != nil { return err } return i.xattrs.SetXattr(creds, mode, kuid, kgid, opts) @@ -1046,14 +1178,89 @@ func (i *inode) removeXattr(creds *auth.Credentials, name string) error { return err } mode := linux.FileMode(i.mode.Load()) + acl := i.accessACL.Load() kuid := auth.KUID(i.uid.Load()) kgid := auth.KGID(i.gid.Load()) - if err := vfs.GenericCheckPermissions(creds, vfs.MayWrite, mode, kuid, kgid); err != nil { + + if strings.HasPrefix(name, linux.XATTR_SYSTEM_PREFIX) { + var aclType vfs.ACLType + switch name { + case linux.XATTR_NAME_POSIX_ACL_ACCESS: + aclType = vfs.AccessACL + case linux.XATTR_NAME_POSIX_ACL_DEFAULT: + aclType = vfs.DefaultACL + default: + return linuxerr.EOPNOTSUPP + } + + if mode.FileType() == linux.ModeSymlink { + return linuxerr.EOPNOTSUPP + } + + if !vfs.CanActAsOwner(creds, kuid) { + return linuxerr.EPERM + } + + // Clear the ACL + _, _, err := i.setPosixACL(creds, aclType, nil /* acl */, true /* clearSGID */) + return err + } + + if err := vfs.GenericCheckPermissions(creds, vfs.MayWrite, mode, acl, kuid, kgid); err != nil { return err } + return i.xattrs.RemoveXattr(creds, mode, kuid, name) } +func (i *inode) setPosixACL(creds *auth.Credentials, t vfs.ACLType, acl *vfs.PosixACL, clearSGID bool) (*vfs.PosixACL, linux.FileMode, error) { + kuid := auth.KUID(i.uid.Load()) + kgid := auth.KGID(i.gid.Load()) + + // Update ctime + now := i.fs.clock.Now().Nanoseconds() + i.ctime.Store(now) + + switch t { + case vfs.AccessACL: + i.mu.Lock() + defer i.mu.Unlock() + + // If acl == nil, use the current mode. We still need to enter the loop to + // potentially clear the SGID bit. + mode, equiv := uint16(i.mode.Load()), true + if acl != nil { + // Otherwise if an ACL is provided, compute the equivalent mode from it. + mode, equiv = acl.Mode() + } + var newMode uint32 + for { + old := i.mode.Load() + newMode = (old &^ linux.PermissionsMask) | uint32(mode) + if clearSGID && vfs.ShouldClearSGID(creds, linux.FileMode(newMode), kuid, kgid) { + newMode &^= linux.S_ISGID + } + if swapped := i.mode.CompareAndSwap(old, newMode); swapped { + break + } + } + + if equiv { + // If the ACL can be represented simply as a mode, no need for the ACL. + i.accessACL.Store(nil) + return nil, linux.FileMode(newMode), nil + } + // Otherwise, store the ACL too for permission checking. + i.accessACL.Store(acl) + return acl, linux.FileMode(newMode), nil + case vfs.DefaultACL: + i.defaultACL.Store(acl) + return acl, linux.FileMode(i.mode.Load()), nil + } + + return nil, 0, linuxerr.EOPNOTSUPP +} + // fileDescription is embedded by tmpfs implementations of // vfs.FileDescriptionImpl. // diff --git a/pkg/sentry/vfs/BUILD b/pkg/sentry/vfs/BUILD index e46acc32229..207c4313c62 100644 --- a/pkg/sentry/vfs/BUILD +++ b/pkg/sentry/vfs/BUILD @@ -163,6 +163,7 @@ go_library( "options.go", "pathname.go", "permissions.go", + "posix_acl.go", "propagation.go", "resolving_path.go", "save_restore.go", @@ -212,12 +213,14 @@ go_test( srcs = [ "file_description_impl_util_test.go", "mount_test.go", + "posix_acl_test.go", ], library = ":vfs", deps = [ "//pkg/abi/linux", "//pkg/atomicbitops", "//pkg/context", + "//pkg/errors", "//pkg/errors/linuxerr", "//pkg/sentry/contexttest", "//pkg/sentry/kernel/auth", diff --git a/pkg/sentry/vfs/anonfs.go b/pkg/sentry/vfs/anonfs.go index f42b7f7464b..e2f9aa22320 100644 --- a/pkg/sentry/vfs/anonfs.go +++ b/pkg/sentry/vfs/anonfs.go @@ -108,7 +108,7 @@ func (fs *anonFilesystem) AccessAt(ctx context.Context, rp *ResolvingPath, creds if !rp.Done() || rp.MustBeDir() { return linuxerr.ENOTDIR } - return GenericCheckPermissions(creds, ats, anonFileMode, anonFileUID, anonFileGID) + return GenericCheckPermissions(creds, ats, anonFileMode, nil, anonFileUID, anonFileGID) } // GetDentryAt implements FilesystemImpl.GetDentryAt. @@ -252,7 +252,7 @@ func (fs *anonFilesystem) BoundEndpointAt(ctx context.Context, rp *ResolvingPath if !rp.Final() { return nil, linuxerr.ENOTDIR } - if err := GenericCheckPermissions(rp.Credentials(), MayWrite, anonFileMode, anonFileUID, anonFileGID); err != nil { + if err := GenericCheckPermissions(rp.Credentials(), MayWrite, anonFileMode, nil, anonFileUID, anonFileGID); err != nil { return nil, err } return nil, linuxerr.ECONNREFUSED @@ -290,6 +290,24 @@ func (fs *anonFilesystem) RemoveXattrAt(ctx context.Context, rp *ResolvingPath, return linuxerr.EPERM } +// GetPosixACLAt implements FilesystemImpl.GetPosixACLAt. +func (fs *anonFilesystem) GetPosixACLAt(ctx context.Context, rp *ResolvingPath, t ACLType) (*PosixACL, error) { + if !rp.Done() || rp.MustBeDir() { + return nil, linuxerr.ENOTDIR + } + // anonfs does not support POSIX ACLs. + return nil, nil +} + +// SetPosixACLAt implements FilesystemImpl.SetPosixACLAt. +func (fs *anonFilesystem) SetPosixACLAt(ctx context.Context, rp *ResolvingPath, t ACLType, acl *PosixACL, clearSGID bool) (*PosixACL, linux.FileMode, error) { + if !rp.Done() || rp.MustBeDir() { + return nil, 0, linuxerr.ENOTDIR + } + // anonfs does not support POSIX ACLs. + return nil, 0, linuxerr.EOPNOTSUPP +} + // IsDescendant implements FilesystemImpl.IsDescendant. func (fs *anonFilesystem) IsDescendant(vfsroot, vd VirtualDentry) bool { return vfsroot == vd diff --git a/pkg/sentry/vfs/file_description.go b/pkg/sentry/vfs/file_description.go index f60c17d64b1..cb9347a1e22 100644 --- a/pkg/sentry/vfs/file_description.go +++ b/pkg/sentry/vfs/file_description.go @@ -489,6 +489,12 @@ type FileDescriptionImpl interface { // RemoveXattr removes the given extended attribute from the file. RemoveXattr(ctx context.Context, name string) error + // GetPosixACL fetches the POSIX ACL from the file. + GetPosixACL(ctx context.Context, t ACLType) (*PosixACL, error) + + // SetPosixACL sets the POSIX ACL for the file, returning the resulting ACL and mode. + SetPosixACL(ctx context.Context, t ACLType, acl *PosixACL, clearSGID bool) (*PosixACL, linux.FileMode, error) + // SupportsLocks indicates whether file locks are supported. SupportsLocks() bool @@ -838,6 +844,37 @@ func (fd *FileDescription) RemoveXattr(ctx context.Context, name string) error { return nil } +// GetPosixACL gets the POSIX ACL from the file represented by fd. +func (fd *FileDescription) GetPosixACL(ctx context.Context, t ACLType) (*PosixACL, error) { + if fd.opts.UseDentryMetadata { + vfsObj := fd.vd.mount.vfs + rp := vfsObj.getResolvingPath(auth.CredentialsFromContext(ctx), &PathOperation{ + Root: fd.vd, + Start: fd.vd, + }) + acl, err := fd.vd.mount.fs.impl.GetPosixACLAt(ctx, rp, t) + rp.Release(ctx) + return acl, err + } + return fd.impl.GetPosixACL(ctx, t) +} + +// SetPosixACL sets the POSIX ACL for the file represented by fd. +// The resulting ACL and mode are returned. +func (fd *FileDescription) SetPosixACL(ctx context.Context, t ACLType, acl *PosixACL, clearSGID bool) (*PosixACL, linux.FileMode, error) { + if fd.opts.UseDentryMetadata { + vfsObj := fd.vd.mount.vfs + rp := vfsObj.getResolvingPath(auth.CredentialsFromContext(ctx), &PathOperation{ + Root: fd.vd, + Start: fd.vd, + }) + acl, mode, err := fd.vd.mount.fs.impl.SetPosixACLAt(ctx, rp, t, acl, clearSGID) + rp.Release(ctx) + return acl, mode, err + } + return fd.impl.SetPosixACL(ctx, t, acl, clearSGID) +} + // SyncFS instructs the filesystem containing fd to execute the semantics of // syncfs(2). func (fd *FileDescription) SyncFS(ctx context.Context) error { diff --git a/pkg/sentry/vfs/file_description_impl_util.go b/pkg/sentry/vfs/file_description_impl_util.go index 54ffb82ca38..295de2bb422 100644 --- a/pkg/sentry/vfs/file_description_impl_util.go +++ b/pkg/sentry/vfs/file_description_impl_util.go @@ -180,6 +180,18 @@ func (FileDescriptionDefaultImpl) RemoveXattr(ctx context.Context, name string) return linuxerr.ENOTSUP } +// GetPosixACL implements FileDescriptionImpl.GetPosixACL for filesystems that +// do not support POSIX ACLs. +func (FileDescriptionDefaultImpl) GetPosixACL(ctx context.Context, t ACLType) (*PosixACL, error) { + return nil, nil +} + +// SetPosixACL implements FileDescriptionImpl.SetPosixACL for filesystems that +// do not support POSIX ACLs. +func (FileDescriptionDefaultImpl) SetPosixACL(ctx context.Context, t ACLType, acl *PosixACL, clearSGID bool) (*PosixACL, linux.FileMode, error) { + return nil, 0, linuxerr.EOPNOTSUPP +} + // RegisterFileAsyncHandler implements FileDescriptionImpl.RegisterFileAsyncHandler. func (FileDescriptionDefaultImpl) RegisterFileAsyncHandler(fd *FileDescription) error { return fd.asyncHandler.Register(fd) diff --git a/pkg/sentry/vfs/filesystem.go b/pkg/sentry/vfs/filesystem.go index c5ec99ac27c..0c9fc9e3202 100644 --- a/pkg/sentry/vfs/filesystem.go +++ b/pkg/sentry/vfs/filesystem.go @@ -476,6 +476,33 @@ type FilesystemImpl interface { // - If name does not exist, ENODATA is returned. RemoveXattrAt(ctx context.Context, rp *ResolvingPath, name string) error + // GetPosixACLAt fetches the POSIX ACL from the file at rp. + // + // GetPosixACLAt does not correspond to a Linux syscall. It is used by internal + // sentry callers for two reasons: + // + // (1) to fetch an file's ACL without needing a serialization/deserialization + // round trip through GetXattrAt/ParsePosixACL; + // (2) to resolve KUIDs and KGIDs in an ACL without than needing to perform + // userns translation (e.g. when using the caller's userns would not be + // appropriate) + // + // GetPosixACLAt performs no permission checks. + GetPosixACLAt(ctx context.Context, rp *ResolvingPath, t ACLType) (*PosixACL, error) + + // SetPosixACLAt sets the POSIX ACL from the file at rp. + // For access ACLs, the ACL and mode are re-computed together, so the resulting + // ACL (which may be nil) and mode are returned together. + // + // SetPosixACLAt does not correspond to a Linux syscall. It is used by internal + // sentry callers to set the ACL without worrying about translating KUIDs/KGIDs. + // + // For access ACLs: if clearSGID is set, the filesystem may clear the setgid bit + // of the file depending on rp's creds, as described in chmod(2). + // + // SetPosixACLAt performs no permission checks. + SetPosixACLAt(ctx context.Context, rp *ResolvingPath, t ACLType, acl *PosixACL, clearSGID bool) (*PosixACL, linux.FileMode, error) + // BoundEndpointAt returns the Unix socket endpoint bound at the path rp. // // Errors: diff --git a/pkg/sentry/vfs/opath.go b/pkg/sentry/vfs/opath.go index eb5101f827b..5db92af850a 100644 --- a/pkg/sentry/vfs/opath.go +++ b/pkg/sentry/vfs/opath.go @@ -103,6 +103,11 @@ func (fd *opathFD) RemoveXattr(ctx context.Context, name string) error { return linuxerr.EBADF } +// GetPosixACL implements FileDescriptionImpl.GetPosixACL. +func (fd *opathFD) GetPosixACL(ctx context.Context, t ACLType) (*PosixACL, error) { + return nil, linuxerr.EBADF +} + // Sync implements FileDescriptionImpl.Sync. func (fd *opathFD) Sync(ctx context.Context) error { return linuxerr.EBADF diff --git a/pkg/sentry/vfs/permissions.go b/pkg/sentry/vfs/permissions.go index b69562d5a0d..57b93312d30 100644 --- a/pkg/sentry/vfs/permissions.go +++ b/pkg/sentry/vfs/permissions.go @@ -57,20 +57,31 @@ func (a AccessTypes) MayExec() bool { return a&MayExec != 0 } +func (a AccessTypes) checkPerms(perms AccessTypes) bool { + return uint16(a)&uint16(perms) == uint16(a) +} + // GenericCheckPermissions checks that creds has the given access rights on a // file with the given permissions, UID, and GID, subject to the rules of // fs/namei.c:generic_permission(). -func GenericCheckPermissions(creds *auth.Credentials, ats AccessTypes, mode linux.FileMode, kuid auth.KUID, kgid auth.KGID) error { - // Check permission bits. - perms := uint16(mode.Permissions()) - if creds.EffectiveKUID == kuid { - perms >>= 6 - } else if creds.InGroup(kgid) { - perms >>= 3 - } - if uint16(ats)&perms == uint16(ats) { - // All permission bits match, access granted. - return nil +func GenericCheckPermissions(creds *auth.Credentials, ats AccessTypes, mode linux.FileMode, acl *PosixACL, kuid auth.KUID, kgid auth.KGID) error { + if acl != nil { + // Check against the ACL (if present) + if acl.checkPermissions(creds, ats, kuid, kgid) { + return nil + } + } else { + // Fallback: check permission bits. + perms := uint16(mode.Permissions()) + if creds.EffectiveKUID == kuid { + perms >>= 6 + } else if creds.InGroup(kgid) { + perms >>= 3 + } + if uint16(ats)&perms == uint16(ats) { + // All permission bits match, access granted. + return nil + } } // CAP_DAC_READ_SEARCH allows the caller to read and search arbitrary @@ -95,7 +106,7 @@ func GenericCheckPermissions(creds *auth.Credentials, ats AccessTypes, mode linu // mode, kuid, and kgid is permitted. // // This corresponds to Linux's fs/namei.c:may_linkat. -func MayLink(creds *auth.Credentials, mode linux.FileMode, kuid auth.KUID, kgid auth.KGID) error { +func MayLink(creds *auth.Credentials, mode linux.FileMode, acl *PosixACL, kuid auth.KUID, kgid auth.KGID) error { // Source inode owner can hardlink all they like; otherwise, it must be a // safe source. if CanActAsOwner(creds, kuid) { @@ -116,7 +127,7 @@ func MayLink(creds *auth.Credentials, mode linux.FileMode, kuid auth.KUID, kgid // don't support S_IXGRP anyway. // Hardlinking to unreadable or unwritable sources is dangerous. - if err := GenericCheckPermissions(creds, MayRead|MayWrite, mode, kuid, kgid); err != nil { + if err := GenericCheckPermissions(creds, MayRead|MayWrite, mode, acl, kuid, kgid); err != nil { return linuxerr.EPERM } return nil @@ -177,10 +188,20 @@ func MayWriteFileWithOpenFlags(flags uint32) bool { } } +// ShouldClearSGID determines if a change to the file's mode by the given +// creds, kuid, and kgid should trigger clearing of the file's setgid bit. +func ShouldClearSGID(creds *auth.Credentials, mode linux.FileMode, kuid auth.KUID, kgid auth.KGID) bool { + // "If the calling process is not privileged (Linux: does not have the CAP_FSETID capability), + // and the group of the file does not match the effective group ID of the process or one of its + // supplementary group IDs, the S_ISGID bit will be turned off, but this will not cause an error + // to be returned." - chmod(2) + return mode&linux.S_ISGID != 0 && !creds.HasCapabilityOnFile(linux.CAP_FSETID, kuid, kgid) && !creds.InGroup(kgid) +} + // CheckSetStat checks that creds has permission to change the metadata of a // file with the given permissions, UID, and GID as specified by stat, subject // to the rules of Linux's fs/attr.c:setattr_prepare(). Might mutate `opts`. -func CheckSetStat(ctx context.Context, creds *auth.Credentials, opts *SetStatOptions, mode linux.FileMode, kuid auth.KUID, kgid auth.KGID) error { +func CheckSetStat(ctx context.Context, creds *auth.Credentials, opts *SetStatOptions, mode linux.FileMode, acl *PosixACL, kuid auth.KUID, kgid auth.KGID) error { stat := &opts.Stat if stat.Mask&linux.STATX_SIZE != 0 { limit, err := CheckLimit(ctx, 0, int64(stat.Size)) @@ -195,11 +216,7 @@ func CheckSetStat(ctx context.Context, creds *auth.Credentials, opts *SetStatOpt if !CanActAsOwner(creds, kuid) { return linuxerr.EPERM } - // "If the calling process is not privileged (Linux: does not have the CAP_FSETID capability), - // and the group of the file does not match the effective group ID of the process or one of its - // supplementary group IDs, the S_ISGID bit will be turned off, but this will not cause an error - // to be returned." - chmod(2) - if stat.Mode&linux.S_ISGID != 0 && !creds.HasCapabilityOnFile(linux.CAP_FSETID, kuid, kgid) && !creds.InGroup(kgid) { + if ShouldClearSGID(creds, linux.FileMode(stat.Mode), kuid, kgid) { stat.Mode &^= linux.S_ISGID } } @@ -216,7 +233,7 @@ func CheckSetStat(ctx context.Context, creds *auth.Credentials, opts *SetStatOpt } } if opts.NeedWritePerm { - if err := GenericCheckPermissions(creds, MayWrite, mode, kuid, kgid); err != nil { + if err := GenericCheckPermissions(creds, MayWrite, mode, acl, kuid, kgid); err != nil { return err } } @@ -227,7 +244,7 @@ func CheckSetStat(ctx context.Context, creds *auth.Credentials, opts *SetStatOpt (stat.Mask&linux.STATX_CTIME != 0 && stat.Ctime.Nsec != linux.UTIME_NOW) { return linuxerr.EPERM } - if err := GenericCheckPermissions(creds, MayWrite, mode, kuid, kgid); err != nil { + if err := GenericCheckPermissions(creds, MayWrite, mode, acl, kuid, kgid); err != nil { return err } } diff --git a/pkg/sentry/vfs/posix_acl.go b/pkg/sentry/vfs/posix_acl.go new file mode 100644 index 00000000000..9d4e5c10cac --- /dev/null +++ b/pkg/sentry/vfs/posix_acl.go @@ -0,0 +1,431 @@ +// 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. + +package vfs + +import ( + "fmt" + + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/errors/linuxerr" + "gvisor.dev/gvisor/pkg/sentry/kernel/auth" +) + +// ACLType is an ACL's type. +type ACLType uint + +const ( + // AccessACL is an ACL used to determine file permissions. + AccessACL ACLType = iota + + // DefaultACL is an ACL inherited by objects created in directories. + DefaultACL +) + +// ACLUser represents a named ACL user. +// +// +stateify savable +type ACLUser struct { + // UID is the named user. + UID auth.KUID + + // Perms is the permissions granted to the named user. + Perms AccessTypes +} + +// ACLGroup represents a named ACL group. +// +// +stateify savable +type ACLGroup struct { + // GID is the named group. + GID auth.KGID + + // Perms is the permissions granted to the named group. + Perms AccessTypes +} + +// PosixACL represents a POSIX ACL, analogous to Linux's struct posix_acl. +// +// *All* fields in PosixACL are immutable. +// +// +stateify savable +type PosixACL struct { + // UGOPerms represent the ACL_USER_OBJ, ACL_GROUP_OBJ, and ACL_OTHER permissions + // as specified in the ACL (all of which are required). + UGOPerms uint16 + + // Mask contains the ACL_MASK field as specified in the ACL (or nil if not present). + Mask *AccessTypes + + // Users is a list of named users in the ACL. + Users []ACLUser + + // Groups is a list of named groups in the ACL. + Groups []ACLGroup +} + +func (a *PosixACL) masked(perms AccessTypes) AccessTypes { + if a.Mask == nil { + return perms + } + return perms & *a.Mask +} + +// UserPerms returns the permissions granted to the owning user by the ACL. +func (a *PosixACL) UserPerms() AccessTypes { + return AccessTypes((a.UGOPerms & linux.ModeUserAll) >> 6) +} + +// GroupPerms returns the permissions granted to the owning group (without +// consideration for the mask) by the ACL. +func (a *PosixACL) GroupPerms() AccessTypes { + return AccessTypes((a.UGOPerms & linux.ModeGroupAll) >> 3) +} + +// OtherPerms returns the permissions granted to "other" by the ACL. +func (a *PosixACL) OtherPerms() AccessTypes { + return AccessTypes(a.UGOPerms & linux.ModeOtherAll) +} + +// Chmod returns a new PosixACL updated based on the new mode's permission bits. +// +// It is roughly analogous to Linux's fs/posix_acl.c:posix_acl_chmod(). +func (a *PosixACL) Chmod(mode uint16) PosixACL { + new := *a + + // User/Other come directly from the new mode + new.UGOPerms = (new.UGOPerms &^ linux.ModeUserAll) | (mode & linux.ModeUserAll) + new.UGOPerms = (new.UGOPerms &^ linux.ModeOtherAll) | (mode & linux.ModeOtherAll) + + // Group updates the mask if present + if new.Mask != nil { + newMask := AccessTypes((mode & linux.ModeGroupAll) >> 3) + new.Mask = &newMask + } else { + new.UGOPerms = (new.UGOPerms &^ linux.ModeGroupAll) | (mode & linux.ModeGroupAll) + } + + return new +} + +// MaskNewFileMode returns a PosixACL adjusted with the open mode when creating a new file. +// +// It is roughly analogous to Linux's fs/posix_acl.c:posix_acl_create(). +func (a *PosixACL) MaskNewFileMode(mode uint16) PosixACL { + newACL := *a + + // User + newACL.UGOPerms &= (mode & linux.ModeUserAll) | (newACL.UGOPerms &^ linux.ModeUserAll) + // Other + newACL.UGOPerms &= (mode & linux.ModeOtherAll) | (newACL.UGOPerms &^ linux.ModeOtherAll) + + if a.Mask != nil { + // Mask + newMask := *newACL.Mask + newMask &= AccessTypes((mode & linux.ModeGroupAll) >> 3) + newACL.Mask = &newMask + } else { + // Group + newACL.UGOPerms &= (mode & linux.ModeGroupAll) | (newACL.UGOPerms &^ linux.ModeGroupAll) + } + + return newACL +} + +// Mode returns the userspace-facing permission bits of the mode from the ACL, +// along with a bool indicating whether the ACL is fully equivalent to the mode +// (in other words, storing the ACL is not necessary). +// +// It is analogous to fs/posix_acl.c:posix_acl_equiv_mode() in Linux. +func (a *PosixACL) Mode() (uint16, bool) { + mode := a.UGOPerms + if a.Mask != nil { + // The mask, if present, appears to userspace as the group bits + mode = (mode &^ linux.ModeGroupAll) | (uint16(*a.Mask) << 3) + } + + equiv := len(a.Users) == 0 && len(a.Groups) == 0 && a.Mask == nil + + return mode, equiv +} + +// checkUserWithMask is used to check named users as part of the ACL access check algorithm. +// +// found indicates whether a named user determines the permissions for this check. +// passes indicates whether the permission check succeeds. +func (a *PosixACL) checkUserWithMask(creds *auth.Credentials, ats AccessTypes) (found bool, passes bool) { + for _, user := range a.Users { + if user.UID == creds.EffectiveKUID { + found = true + if ats.checkPerms(a.masked(user.Perms)) { + passes = true + } + break + } + } + return +} + +// checkGroupsWithMask is used to check named groups as part of the ACL access check algorithm. +// +// found indicates whether a named group determines the permissions for this check. +// passes indicates whether the permission check succeeds. +func (a *PosixACL) checkGroupsWithMask(creds *auth.Credentials, ats AccessTypes) (found bool, passes bool) { + for _, group := range a.Groups { + if creds.InGroup(group.GID) { + found = true + if ats.checkPerms(a.masked(group.Perms)) { + passes = true + break + } + } + } + return +} + +// checkPermissions implements the ACL portion of the access check algorithm as described in acl(5). +func (a *PosixACL) checkPermissions(creds *auth.Credentials, ats AccessTypes, kuid auth.KUID, kgid auth.KGID) bool { + // [From acl(5):] + if creds.EffectiveKUID == kuid { + // 1. if the effective user ID of the process matches + // the user ID of the file object owner, then + if ats.checkPerms(a.UserPerms()) { + // if the ACL_USER_OBJ entry contains the requested permissions, + // access is granted, + return true + } + // else access is denied. + } else if found, passes := a.checkUserWithMask(creds, ats); found { + // 2. else if the effective user ID of the process matches + // the qualifier of any entry of type ACL_USER, then + if passes { + // if the matching ACL_USER entry and the ACL_MASK entry + // contain the requested permissions, access is granted, + return true + } + // else access is denied. + } else if found, passes := a.checkGroupsWithMask(creds, ats); creds.InGroup(kgid) || found { + // 3. else if the effective group ID or any of the supplementary group IDs + // of the process match the file group or the qualifier of any entry of + // type ACL_GROUP, then + if creds.InGroup(kgid) && ats.checkPerms(a.masked(a.GroupPerms())) { + // if the ACL_MASK entry and any of the matching [...] ACL_GROUP + // entries contain the requested permissions, access is granted, + return true + } + if found && passes { + // [...] or ACL_GROUP entries + return true + } + + // [note that the above implicitly handles both present and missing ACL_MASK] + } else if ats.checkPerms(a.OtherPerms()) { + // 4. else if the ACL_OTHER entry contains the requested permissions, + // access is granted. + return true + } + + // 5. else access is denied. + return false +} + +// Serialize returns the userspace xattr representation of the specified PosixACL. +// Specifying a user namespace is required since UIDs and GIDs for named users in +// the ACL's xattr representation are transformed into the process's user namespace. +func (a *PosixACL) Serialize(userns *auth.UserNamespace) []byte { + // Version header + ret := linux.PosixACLXattr{ + Version: linux.POSIX_ACL_XATTR_VERSION, + Entries: make([]linux.PosixACLXattrEntry, 0), + } + + // Owning user's permissions + ret.Entries = append(ret.Entries, linux.PosixACLXattrEntry{ + Tag: linux.ACL_USER_OBJ, + Perm: uint16(a.UserPerms()), + ID: linux.ACL_UNDEFINED_ID, + }) + + // Named users + for _, user := range a.Users { + ret.Entries = append(ret.Entries, linux.PosixACLXattrEntry{ + Tag: linux.ACL_USER, + Perm: uint16(user.Perms), + ID: uint32(userns.MapFromKUID(user.UID)), + }) + } + + // Owning group's permissions + ret.Entries = append(ret.Entries, linux.PosixACLXattrEntry{ + Tag: linux.ACL_GROUP_OBJ, + Perm: uint16(a.GroupPerms()), + ID: linux.ACL_UNDEFINED_ID, + }) + + // Named groups + for _, group := range a.Groups { + ret.Entries = append(ret.Entries, linux.PosixACLXattrEntry{ + Tag: linux.ACL_GROUP, + Perm: uint16(group.Perms), + ID: uint32(userns.MapFromKGID(group.GID)), + }) + } + + // Mask + if a.Mask != nil { + ret.Entries = append(ret.Entries, linux.PosixACLXattrEntry{ + Tag: linux.ACL_MASK, + Perm: uint16(*a.Mask), + ID: linux.ACL_UNDEFINED_ID, + }) + } + + // Other permissions + ret.Entries = append(ret.Entries, linux.PosixACLXattrEntry{ + Tag: linux.ACL_OTHER, + Perm: uint16(a.OtherPerms()), + ID: linux.ACL_UNDEFINED_ID, + }) + + buf := make([]byte, ret.SizeBytes()) + ret.MarshalBytes(buf) + + return buf +} + +// ParsePosixACL parses a userspace-specified xattr into a Posix ACL. +func ParsePosixACL(src []byte, userns *auth.UserNamespace) (*PosixACL, error) { + if len(src) == 0 { + // Empty string counts as empty ACL. + return nil, nil + } + + // First, parse into a Linux.PosixACLXattr (the uabi type). + if len(src) < (&linux.PosixACLXattr{}).SizeBytes() { + // Header missing + return &PosixACL{}, linuxerr.EINVAL + } + acl := &linux.PosixACLXattr{} + if remaining := acl.UnmarshalBytes(src); len(remaining) != 0 { + // Should contain a whole number of entries + return &PosixACL{}, linuxerr.EINVAL + } + + if acl.Version != linux.POSIX_ACL_XATTR_VERSION { + // Version was incorrect + return &PosixACL{}, linuxerr.EOPNOTSUPP + } + + if len(acl.Entries) == 0 { + // ACL with no entries counts as empty ACL. + return nil, nil + } + + // Now, take a look at the entries. + var userObj, groupObj, other, mask *uint16 + users := make([]ACLUser, 0) + groups := make([]ACLGroup, 0) + for _, entry := range acl.Entries { + if entry.Perm&^(linux.ACL_READ|linux.ACL_WRITE|linux.ACL_EXECUTE) != 0 { + // Perm must always be a valid set of permission bits + return &PosixACL{}, linuxerr.EINVAL + } + + switch entry.Tag { + case linux.ACL_USER_OBJ: + if userObj != nil { + // Only one ACL_USER_OBJ allowed + return &PosixACL{}, linuxerr.EINVAL + } + userObj = &entry.Perm + case linux.ACL_GROUP_OBJ: + if groupObj != nil { + // Only one ACL_GROUP_OBJ allowed + return &PosixACL{}, linuxerr.EINVAL + } + groupObj = &entry.Perm + case linux.ACL_OTHER: + if other != nil { + // Only one ACL_OTHER allowed + return &PosixACL{}, linuxerr.EINVAL + } + other = &entry.Perm + case linux.ACL_USER: + kuid := userns.MapToKUID(auth.UID(entry.ID)) + if !kuid.Ok() { + return &PosixACL{}, linuxerr.EINVAL + } + + aclUser := ACLUser{ + UID: kuid, + Perms: AccessTypes(entry.Perm), + } + users = append(users, aclUser) + case linux.ACL_GROUP: + kgid := userns.MapToKGID(auth.GID(entry.ID)) + if !kgid.Ok() { + return &PosixACL{}, linuxerr.EINVAL + } + + aclGroup := ACLGroup{ + GID: kgid, + Perms: AccessTypes(entry.Perm), + } + groups = append(groups, aclGroup) + case linux.ACL_MASK: + if mask != nil { + // Only one ACL_MASK allowed + return &PosixACL{}, linuxerr.EINVAL + } + mask = &entry.Perm + default: + // Unknown tag + return &PosixACL{}, linuxerr.EINVAL + } + } + + // According to acl(5), a valid ACL: + // (a) must contain no *fewer* than one entry each of ACL_USER_OBJ, ACL_GROUP_OBJ, ACL_OTHER + if userObj == nil || groupObj == nil || other == nil { + return &PosixACL{}, linuxerr.EINVAL + } + // (b) must contain no *more* than one entry each of ACL_USER_OBJ, ACL_GROUP_OBJ, ACL_OTHER + // (checked in the loop) + // (c) if named users or groups are present, no *fewer* than one ACL_MASK must be present + if (len(users) > 0 || len(groups) > 0) && mask == nil { + return &PosixACL{}, linuxerr.EINVAL + } + // (d) no *more* than one ACL_MASK must be present + // (checked in the loop) + // (e) must have unique UIDs and GIDs for named users and groups + // (not enforced by Linux, so we ignore as well) + + ret := PosixACL{ + UGOPerms: (*userObj << 6) | (*groupObj << 3) | *other, + Mask: (*AccessTypes)(mask), + Users: users, + Groups: groups, + } + return &ret, nil +} + +// String represents a POSIX ACL as a human-readable string. +func (a PosixACL) String() string { + mask := "nil" + if a.Mask != nil { + mask = fmt.Sprintf("%O", *a.Mask) + } + + return fmt.Sprintf("PosixACL { UGOPerms: %O, Mask: %s, Users: %v, Groups: %v }", a.UGOPerms, mask, a.Users, a.Groups) +} diff --git a/pkg/sentry/vfs/posix_acl_test.go b/pkg/sentry/vfs/posix_acl_test.go new file mode 100644 index 00000000000..1d3235d6130 --- /dev/null +++ b/pkg/sentry/vfs/posix_acl_test.go @@ -0,0 +1,362 @@ +// 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. + +package vfs + +import ( + "bytes" + "testing" + + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/errors" + "gvisor.dev/gvisor/pkg/errors/linuxerr" + "gvisor.dev/gvisor/pkg/sentry/kernel/auth" +) + +// Permission bit shorthands. +const ( + permR = linux.ACL_READ + permW = linux.ACL_WRITE + permX = linux.ACL_EXECUTE + permRW = permR | permW + permRX = permR | permX + permRWX = permR | permW | permX +) + +const aclUndef = uint32(linux.ACL_UNDEFINED_ID) + +// aclEntry builds a single on-wire POSIX ACL entry. +func aclEntry(tag, perm uint16, id uint32) linux.PosixACLXattrEntry { + return linux.PosixACLXattrEntry{Tag: tag, Perm: perm, ID: id} +} + +// marshalACLXattr builds the userspace representation of a +// POSIX ACL. +func marshalACLXattr(version uint32, entries ...linux.PosixACLXattrEntry) []byte { + x := linux.PosixACLXattr{Version: version, Entries: entries} + buf := make([]byte, x.SizeBytes()) + x.MarshalBytes(buf) + return buf +} + +// aclMask returns p as a pointer. +func aclMask(p AccessTypes) *AccessTypes { return &p } + +// aclEqual reports whether two PosixACLs are equal. +func aclEqual(a, b PosixACL) bool { + if a.UGOPerms != b.UGOPerms { + return false + } + if (a.Mask == nil) != (b.Mask == nil) { + return false + } + if a.Mask != nil && *a.Mask != *b.Mask { + return false + } + if len(a.Users) != len(b.Users) { + return false + } + for i := range a.Users { + if a.Users[i] != b.Users[i] { + return false + } + } + if len(a.Groups) != len(b.Groups) { + return false + } + for i := range a.Groups { + if a.Groups[i] != b.Groups[i] { + return false + } + } + return true +} + +func TestParsePosixACL(t *testing.T) { + ns := auth.NewRootUserNamespace() + for _, tc := range []struct { + name string + src []byte + // wantErr is the expected error; nil means success, in which case want + // is compared against the parsed result. + wantErr *errors.Error + want *PosixACL + }{ + { + name: "minimal", + src: marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION, + aclEntry(linux.ACL_USER_OBJ, permRW, aclUndef), + aclEntry(linux.ACL_GROUP_OBJ, permR, aclUndef), + aclEntry(linux.ACL_OTHER, permR, aclUndef), + ), + want: &PosixACL{UGOPerms: 0o644}, + }, + { + name: "named user with mask", + src: marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION, + aclEntry(linux.ACL_USER_OBJ, permRW, aclUndef), + aclEntry(linux.ACL_USER, permR, 982), + aclEntry(linux.ACL_GROUP_OBJ, permR, aclUndef), + aclEntry(linux.ACL_MASK, permRW, aclUndef), + aclEntry(linux.ACL_OTHER, permR, aclUndef), + ), + want: &PosixACL{ + UGOPerms: 0o644, + Mask: aclMask(permRW), + Users: []ACLUser{{UID: 982, Perms: permR}}, + }, + }, + { + name: "named group with mask", + src: marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION, + aclEntry(linux.ACL_USER_OBJ, permRWX, aclUndef), + aclEntry(linux.ACL_GROUP_OBJ, permRX, aclUndef), + aclEntry(linux.ACL_GROUP, permRWX, 100), + aclEntry(linux.ACL_MASK, permRX, aclUndef), + aclEntry(linux.ACL_OTHER, 0, aclUndef), + ), + want: &PosixACL{ + UGOPerms: 0o750, + Mask: aclMask(permRX), + Groups: []ACLGroup{{GID: 100, Perms: permRWX}}, + }, + }, + { + name: "duplicate named user", + src: marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION, + aclEntry(linux.ACL_USER_OBJ, permRW, aclUndef), + aclEntry(linux.ACL_USER, permR, 982), + aclEntry(linux.ACL_USER, permR|permW, 982), + aclEntry(linux.ACL_GROUP_OBJ, permR, aclUndef), + aclEntry(linux.ACL_MASK, permRW, aclUndef), + aclEntry(linux.ACL_OTHER, permR, aclUndef), + ), + want: &PosixACL{ + UGOPerms: 0o644, + Mask: aclMask(permRW), + Users: []ACLUser{ + {UID: 982, Perms: permR}, + {UID: 982, Perms: permR | permW}, + }, + }, + }, + { + name: "too short for header", + src: []byte{0x02, 0x00, 0x00}, + wantErr: linuxerr.EINVAL, + }, + { + name: "unsupported version", + src: marshalACLXattr(1, + aclEntry(linux.ACL_USER_OBJ, permRW, aclUndef), + aclEntry(linux.ACL_GROUP_OBJ, permR, aclUndef), + aclEntry(linux.ACL_OTHER, permR, aclUndef), + ), + wantErr: linuxerr.EOPNOTSUPP, + }, + { + name: "trailing partial entry", + src: append(marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION, + aclEntry(linux.ACL_USER_OBJ, permRW, aclUndef), + aclEntry(linux.ACL_GROUP_OBJ, permR, aclUndef), + aclEntry(linux.ACL_OTHER, permR, aclUndef), + ), 0xff), + wantErr: linuxerr.EINVAL, + }, + { + name: "missing USER_OBJ", + src: marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION, + aclEntry(linux.ACL_GROUP_OBJ, permR, aclUndef), + aclEntry(linux.ACL_OTHER, permR, aclUndef), + ), + wantErr: linuxerr.EINVAL, + }, + { + name: "missing GROUP_OBJ", + src: marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION, + aclEntry(linux.ACL_USER_OBJ, permRW, aclUndef), + aclEntry(linux.ACL_OTHER, permR, aclUndef), + ), + wantErr: linuxerr.EINVAL, + }, + { + name: "missing OTHER", + src: marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION, + aclEntry(linux.ACL_USER_OBJ, permRW, aclUndef), + aclEntry(linux.ACL_GROUP_OBJ, permR, aclUndef), + ), + wantErr: linuxerr.EINVAL, + }, + { + name: "duplicate USER_OBJ", + src: marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION, + aclEntry(linux.ACL_USER_OBJ, permRW, aclUndef), + aclEntry(linux.ACL_USER_OBJ, permR, aclUndef), + aclEntry(linux.ACL_GROUP_OBJ, permR, aclUndef), + aclEntry(linux.ACL_OTHER, permR, aclUndef), + ), + wantErr: linuxerr.EINVAL, + }, + { + name: "duplicate MASK", + src: marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION, + aclEntry(linux.ACL_USER_OBJ, permRW, aclUndef), + aclEntry(linux.ACL_USER, permR, 982), + aclEntry(linux.ACL_GROUP_OBJ, permR, aclUndef), + aclEntry(linux.ACL_MASK, permRW, aclUndef), + aclEntry(linux.ACL_MASK, permR, aclUndef), + aclEntry(linux.ACL_OTHER, permR, aclUndef), + ), + wantErr: linuxerr.EINVAL, + }, + { + name: "named user without mask", + src: marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION, + aclEntry(linux.ACL_USER_OBJ, permRW, aclUndef), + aclEntry(linux.ACL_USER, permR, 982), + aclEntry(linux.ACL_GROUP_OBJ, permR, aclUndef), + aclEntry(linux.ACL_OTHER, permR, aclUndef), + ), + wantErr: linuxerr.EINVAL, + }, + { + name: "invalid permission bits", + src: marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION, + aclEntry(linux.ACL_USER_OBJ, 0x08, aclUndef), + aclEntry(linux.ACL_GROUP_OBJ, permR, aclUndef), + aclEntry(linux.ACL_OTHER, permR, aclUndef), + ), + wantErr: linuxerr.EINVAL, + }, + { + name: "unknown tag", + src: marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION, + aclEntry(linux.ACL_USER_OBJ, permRW, aclUndef), + aclEntry(linux.ACL_GROUP_OBJ, permR, aclUndef), + aclEntry(linux.ACL_OTHER, permR, aclUndef), + aclEntry(0x40, permR, aclUndef), + ), + wantErr: linuxerr.EINVAL, + }, + { + name: "empty string", + src: []byte{}, + want: nil, + }, + { + name: "header only", + src: marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION), + want: nil, + }, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := ParsePosixACL(tc.src, ns) + if tc.wantErr != nil { + if !linuxerr.Equals(tc.wantErr, err) { + t.Fatalf("ParsePosixACL: %v: got error %v, want %v", tc.name, err, tc.wantErr) + } + return + } + if err != nil { + t.Fatalf("ParsePosixACL: %v: unexpected error: %v", tc.name, err) + } + if ((got == nil) != (tc.want == nil)) || (got != nil && !aclEqual(*got, *tc.want)) { + t.Errorf("ParsePosixACL: %v: got %+v, want %+v", tc.name, got, tc.want) + } + }) + } +} + +func TestSerializePosixACL(t *testing.T) { + ns := auth.NewRootUserNamespace() + acl := PosixACL{ + UGOPerms: 0o644, + Mask: aclMask(permRW), + Users: []ACLUser{{UID: 982, Perms: permR}}, + } + // Serialize emits entries in canonical order: USER_OBJ, named users, + // GROUP_OBJ, named groups, MASK, OTHER; base entries carry + // ACL_UNDEFINED_ID, matching fs/posix_acl.c:posix_acl_to_xattr(). + want := marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION, + aclEntry(linux.ACL_USER_OBJ, permRW, aclUndef), + aclEntry(linux.ACL_USER, permR, 982), + aclEntry(linux.ACL_GROUP_OBJ, permR, aclUndef), + aclEntry(linux.ACL_MASK, permRW, aclUndef), + aclEntry(linux.ACL_OTHER, permR, aclUndef), + ) + if got := acl.Serialize(ns); !bytes.Equal(got, want) { + t.Errorf("Serialize = %x, want %x", got, want) + } +} + +func TestPosixACLRoundTrip(t *testing.T) { + ns := auth.NewRootUserNamespace() + for _, acl := range []PosixACL{ + {UGOPerms: 0o640}, + {UGOPerms: 0o644, Mask: aclMask(permRWX), Users: []ACLUser{{UID: 982, Perms: permR}}}, + {UGOPerms: 0o600, Mask: aclMask(permRX), Groups: []ACLGroup{{GID: 100, Perms: permRX}}}, + { + UGOPerms: 0o750, + Mask: aclMask(permRW), + Users: []ACLUser{{UID: 1000, Perms: permRWX}, {UID: 1001, Perms: permR}}, + Groups: []ACLGroup{{GID: 50, Perms: permR}}, + }, + } { + got, err := ParsePosixACL(acl.Serialize(ns), ns) + if err != nil { + t.Fatalf("round-trip ParsePosixACL(Serialize(%+v)): unexpected error: %v", acl, err) + } + if !aclEqual(*got, acl) { + t.Errorf("round-trip: got %+v, want %+v", got, acl) + } + } +} + +// TestPosixACLModeEquivalence checks Mode(), which reports the userspace-facing +// mode bits (mask surfaced as the group bits) and whether the ACL is fully +// representable by the mode alone. +func TestPosixACLModeEquivalence(t *testing.T) { + for _, tc := range []struct { + name string + acl PosixACL + wantMode uint16 + wantEquiv bool + }{ + { + name: "minimal is equivalent", + acl: PosixACL{UGOPerms: 0o751}, + wantMode: 0o751, + wantEquiv: true, + }, + { + name: "mask surfaces as group bits", + acl: PosixACL{UGOPerms: 0o644, Mask: aclMask(permRWX)}, + wantMode: 0o674, + wantEquiv: false, + }, + { + name: "named user is not equivalent", + acl: PosixACL{UGOPerms: 0o644, Mask: aclMask(permR), Users: []ACLUser{{UID: 1, Perms: permR}}}, + wantMode: 0o644, + wantEquiv: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + mode, equiv := tc.acl.Mode() + if mode != tc.wantMode || equiv != tc.wantEquiv { + t.Errorf("Mode() = (%#o, %t), want (%#o, %t)", mode, equiv, tc.wantMode, tc.wantEquiv) + } + }) + } +} diff --git a/pkg/sentry/vfs/vfs.go b/pkg/sentry/vfs/vfs.go index eeb8d426be7..cea91938fe4 100644 --- a/pkg/sentry/vfs/vfs.go +++ b/pkg/sentry/vfs/vfs.go @@ -861,6 +861,46 @@ func (vfs *VirtualFilesystem) RemoveXattrAt(ctx context.Context, creds *auth.Cre } } +// GetPosixACLAt fetches the POSIX ACL from the file at rp. +// No permission checks are performed. +func (vfs *VirtualFilesystem) GetPosixACLAt(ctx context.Context, creds *auth.Credentials, pop *PathOperation, t ACLType) (*PosixACL, error) { + rp := vfs.getResolvingPath(creds, pop) + defer rp.Release(ctx) + for { + if err := vfs.maybeBlockOnMountPromise(ctx, rp); err != nil { + return nil, err + } + acl, err := rp.mount.fs.impl.GetPosixACLAt(ctx, rp, t) + if err == nil { + return acl, nil + } + if !rp.handleError(ctx, err) { + return nil, err + } + } +} + +// SetPosixACLAt sets the POSIX ACL for the file at rp. +// No permission checks are performed. +// +// The resulting ACL (which may be nil) and mode are returned. +func (vfs *VirtualFilesystem) SetPosixACLAt(ctx context.Context, creds *auth.Credentials, pop *PathOperation, t ACLType, acl *PosixACL, clearSGID bool) (*PosixACL, linux.FileMode, error) { + rp := vfs.getResolvingPath(creds, pop) + defer rp.Release(ctx) + for { + if err := vfs.maybeBlockOnMountPromise(ctx, rp); err != nil { + return nil, 0, err + } + acl, mode, err := rp.mount.fs.impl.SetPosixACLAt(ctx, rp, t, acl, clearSGID) + if err == nil { + return acl, mode, nil + } + if !rp.handleError(ctx, err) { + return nil, 0, err + } + } +} + // SyncAllFilesystems has the semantics of Linux's sync(2). func (vfs *VirtualFilesystem) SyncAllFilesystems(ctx context.Context) error { var retErr error diff --git a/runsc/container/container_test.go b/runsc/container/container_test.go index d8ee004ff0c..76f45329b44 100644 --- a/runsc/container/container_test.go +++ b/runsc/container/container_test.go @@ -17,6 +17,7 @@ package container import ( "bufio" "bytes" + "encoding/base64" "fmt" "io" "math" @@ -37,6 +38,7 @@ import ( "github.com/cenkalti/backoff" specs "github.com/opencontainers/runtime-spec/specs-go" "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/cleanup" "gvisor.dev/gvisor/pkg/hostos" @@ -5680,6 +5682,162 @@ func TestTarRootfsUpperLayerOpaqueDir(t *testing.T) { t.Logf("/usr/share in restored container correctly contains only: %v", restoredFiles) } +// buildACLXattr serializes entries into the system.posix_acl_access / +// system.posix_acl_default xattr wire format (a little-endian version header +// followed by 8-byte entries). +func buildACLXattr(entries []linux.PosixACLXattrEntry) []byte { + x := linux.PosixACLXattr{Version: linux.POSIX_ACL_XATTR_VERSION, Entries: entries} + buf := make([]byte, x.SizeBytes()) + x.MarshalBytes(buf) + return buf +} + +// TestTarRootfsUpperLayerACL verifies that POSIX ACLs set on files in the overlay's +// tmpfs layer are preserved across tar serialization and restoration. +func TestTarRootfsUpperLayerACL(t *testing.T) { + conf := testutil.TestConfig(t) + conf.Overlay2.Set("root:memory") + + spec, _ := sleepSpecConf(t) + spec.Root.Readonly = false + + app, err := testutil.FindFile("test/cmd/test_app/test_app") + if err != nil { + t.Fatalf("error finding test_app: %v", err) + } + + _, bundleDir, cleanup, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up container: %v", err) + } + defer cleanup() + + // Create and start the container. + args := Args{ + ID: testutil.RandomContainerID(), + Spec: spec, + BundleDir: bundleDir, + } + cont, err := New(conf, args) + if err != nil { + t.Fatalf("error creating container: %v", err) + } + defer cont.Destroy() + if err := cont.Start(conf); err != nil { + t.Fatalf("error starting container: %v", err) + } + + // Paths in the writable (upper) layer that will receive ACLs. + const ( + aclFile = "/acltest-file" + aclDir = "/acltest-dir" + ) + + // Use ACLs with named users to ensure that the ACL is actually stored as an ACL + // rather than folded into the mode + accessACL := base64.StdEncoding.EncodeToString(buildACLXattr([]linux.PosixACLXattrEntry{ + {Tag: linux.ACL_USER_OBJ, Perm: linux.ACL_READ | linux.ACL_WRITE, ID: linux.ACL_UNDEFINED_ID}, + {Tag: linux.ACL_USER, Perm: linux.ACL_READ, ID: 1000}, + {Tag: linux.ACL_GROUP_OBJ, Perm: linux.ACL_READ, ID: linux.ACL_UNDEFINED_ID}, + {Tag: linux.ACL_MASK, Perm: linux.ACL_READ | linux.ACL_WRITE, ID: linux.ACL_UNDEFINED_ID}, + {Tag: linux.ACL_OTHER, Perm: linux.ACL_READ, ID: linux.ACL_UNDEFINED_ID}, + })) + defaultACL := base64.StdEncoding.EncodeToString(buildACLXattr([]linux.PosixACLXattrEntry{ + {Tag: linux.ACL_USER_OBJ, Perm: linux.ACL_READ | linux.ACL_WRITE | linux.ACL_EXECUTE, ID: linux.ACL_UNDEFINED_ID}, + {Tag: linux.ACL_USER, Perm: linux.ACL_READ | linux.ACL_EXECUTE, ID: 1001}, + {Tag: linux.ACL_GROUP_OBJ, Perm: linux.ACL_READ | linux.ACL_EXECUTE, ID: linux.ACL_UNDEFINED_ID}, + {Tag: linux.ACL_MASK, Perm: linux.ACL_READ | linux.ACL_WRITE | linux.ACL_EXECUTE, ID: linux.ACL_UNDEFINED_ID}, + {Tag: linux.ACL_OTHER, Perm: 0, ID: linux.ACL_UNDEFINED_ID}, + })) + + // Create the file and directory in the writable (upper) layer. + if out, err := executeCombinedOutput(conf, cont, nil, "/bin/touch", aclFile); err != nil { + t.Fatalf("error creating ACL test file: %v, output: %s", err, out) + } + if out, err := executeCombinedOutput(conf, cont, nil, "/bin/mkdir", aclDir); err != nil { + t.Fatalf("error creating ACL test directory: %v, output: %s", err, out) + } + + // setACL sets an ACL xattr on path via test_app. + setACL := func(path, name, b64 string) { + t.Helper() + if out, err := executeCombinedOutput(conf, cont, nil, app, "setxattr", + "--path="+path, "--name="+name, "--value="+b64); err != nil { + t.Fatalf("error setting %s on %s: %v, output: %s", name, path, err, out) + } + } + // getACL returns the base64-encoded ACL xattr on path in container c. + getACL := func(c *Container, path, name string) string { + t.Helper() + out, err := executeCombinedOutput(conf, c, nil, app, "getxattr", + "--path="+path, "--name="+name) + if err != nil { + t.Fatalf("error getting %s on %s: %v, output: %s", name, path, err, out) + } + return strings.TrimSpace(string(out)) + } + + setACL(aclFile, linux.XATTR_NAME_POSIX_ACL_ACCESS, accessACL) + wantFileAccess := getACL(cont, aclFile, linux.XATTR_NAME_POSIX_ACL_ACCESS) + + setACL(aclDir, linux.XATTR_NAME_POSIX_ACL_ACCESS, accessACL) + wantDirAccess := getACL(cont, aclDir, linux.XATTR_NAME_POSIX_ACL_ACCESS) + + setACL(aclDir, linux.XATTR_NAME_POSIX_ACL_DEFAULT, defaultACL) + wantDirDefault := getACL(cont, aclDir, linux.XATTR_NAME_POSIX_ACL_DEFAULT) + + for name, v := range map[string]string{"file access": wantFileAccess, "dir access": wantDirAccess, "dir default": wantDirDefault} { + if v == "" { + t.Fatalf("%s ACL was unexpectedly empty", name) + } + } + + // Tar the upper layer. + tarFile, err := os.CreateTemp(testutil.TmpDir(), "tarfile-acl-*.tar") + if err != nil { + t.Fatalf("error creating temp file: %v", err) + } + defer os.Remove(tarFile.Name()) + defer tarFile.Close() + if err := cont.TarRootfsUpperLayer(tarFile); err != nil { + t.Fatalf("error serializing rootfs upper layer to tar: %v", err) + } + + // Restore the tar into a new container. + spec.Annotations[specutils.AnnotationRootfsUpperTar] = tarFile.Name() + conf.AllowRootfsTarAnnotation = true + _, bundleDir2, cleanup2, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up restored container: %v", err) + } + defer cleanup2() + + args2 := Args{ + ID: testutil.RandomContainerID(), + Spec: spec, + BundleDir: bundleDir2, + } + newCont, err := New(conf, args2) + if err != nil { + t.Fatalf("error creating restored container: %v", err) + } + defer newCont.Destroy() + if err := newCont.Start(conf); err != nil { + t.Fatalf("error starting restored container: %v", err) + } + + // Verify the ACLs survived the round-trip. + if got := getACL(newCont, aclFile, linux.XATTR_NAME_POSIX_ACL_ACCESS); got != wantFileAccess { + t.Errorf("file access ACL mismatch after restore:\n got %q\nwant %q", got, wantFileAccess) + } + if got := getACL(newCont, aclDir, linux.XATTR_NAME_POSIX_ACL_ACCESS); got != wantDirAccess { + t.Errorf("dir access ACL mismatch after restore:\n got %q\nwant %q", got, wantDirAccess) + } + if got := getACL(newCont, aclDir, linux.XATTR_NAME_POSIX_ACL_DEFAULT); got != wantDirDefault { + t.Errorf("dir default ACL mismatch after restore:\n got %q\nwant %q", got, wantDirDefault) + } +} + func TestSpecValidationIgnore(t *testing.T) { conf := testutil.TestConfig(t) if err := conf.RestoreSpecValidation.Set("ignore"); err != nil { diff --git a/test/cmd/test_app/BUILD b/test/cmd/test_app/BUILD index 36fafdb2cf6..94126d045d2 100644 --- a/test/cmd/test_app/BUILD +++ b/test/cmd/test_app/BUILD @@ -12,6 +12,7 @@ go_binary( "fds.go", "hostinet_sr.go", "main.go", + "xattr.go", "zombies.go", ], features = ["fully_static_link"], diff --git a/test/cmd/test_app/main.go b/test/cmd/test_app/main.go index 702b75539a0..0bba0dbacff 100644 --- a/test/cmd/test_app/main.go +++ b/test/cmd/test_app/main.go @@ -61,6 +61,8 @@ func main() { subcommands.Register(new(uds), "") subcommands.Register(new(zombieTest), "") subcommands.Register(new(fsCheckpoint), "") + subcommands.Register(new(setXattr), "") + subcommands.Register(new(getXattr), "") flag.Parse() diff --git a/test/cmd/test_app/xattr.go b/test/cmd/test_app/xattr.go new file mode 100644 index 00000000000..33ea93d6941 --- /dev/null +++ b/test/cmd/test_app/xattr.go @@ -0,0 +1,115 @@ +// 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. + +package main + +import ( + "context" + "encoding/base64" + "fmt" + "log" + + "github.com/google/subcommands" + "golang.org/x/sys/unix" + + "gvisor.dev/gvisor/runsc/flag" +) + +const xattrSizeMax = 65536 + +// setXattr sets an extended attribute on a file. The value is passed as a +// base64-encoded string so that arbitrary binary blobs can be specified on +// the command line. +type setXattr struct { + path string + name string + value string +} + +// Name implements subcommands.Command.Name. +func (*setXattr) Name() string { return "setxattr" } + +// Synopsis implements subcommands.Command.Synopsis. +func (*setXattr) Synopsis() string { return "sets an extended attribute (value is base64-encoded)" } + +// Usage implements subcommands.Command.Usage. +func (*setXattr) Usage() string { + return "setxattr --path= --name= --value=\n" +} + +// SetFlags implements subcommands.Command.SetFlags. +func (c *setXattr) SetFlags(f *flag.FlagSet) { + f.StringVar(&c.path, "path", "", "path of the file to set the xattr on") + f.StringVar(&c.name, "name", "", "name of the xattr") + f.StringVar(&c.value, "value", "", "base64-encoded value of the xattr") +} + +// Execute implements subcommands.Command.Execute. +func (c *setXattr) Execute(ctx context.Context, f *flag.FlagSet, args ...any) subcommands.ExitStatus { + if c.path == "" || c.name == "" { + log.Print("--path and --name must be set") + return subcommands.ExitUsageError + } + value, err := base64.StdEncoding.DecodeString(c.value) + if err != nil { + log.Printf("failed to decode --value %q: %v", c.value, err) + return subcommands.ExitUsageError + } + if err := unix.Setxattr(c.path, c.name, value, 0); err != nil { + log.Printf("setxattr(%q, %q) failed: %v", c.path, c.name, err) + return subcommands.ExitFailure + } + return subcommands.ExitSuccess +} + +// getXattr prints the base64-encoded value of an extended attribute to stdout. +type getXattr struct { + path string + name string +} + +// Name implements subcommands.Command.Name. +func (*getXattr) Name() string { return "getxattr" } + +// Synopsis implements subcommands.Command.Synopsis. +func (*getXattr) Synopsis() string { + return "prints the base64-encoded value of an extended attribute to stdout" +} + +// Usage implements subcommands.Command.Usage. +func (*getXattr) Usage() string { + return "getxattr --path= --name=\n" +} + +// SetFlags implements subcommands.Command.SetFlags. +func (c *getXattr) SetFlags(f *flag.FlagSet) { + f.StringVar(&c.path, "path", "", "path of the file to get the xattr from") + f.StringVar(&c.name, "name", "", "name of the xattr") +} + +// Execute implements subcommands.Command.Execute. +func (c *getXattr) Execute(ctx context.Context, f *flag.FlagSet, args ...any) subcommands.ExitStatus { + if c.path == "" || c.name == "" { + log.Print("--path and --name must be set") + return subcommands.ExitUsageError + } + buf := make([]byte, xattrSizeMax) + n, err := unix.Getxattr(c.path, c.name, buf) + if err != nil { + log.Printf("getxattr(%q, %q) failed: %v", c.path, c.name, err) + return subcommands.ExitFailure + } + fmt.Println(base64.StdEncoding.EncodeToString(buf[:n])) + return subcommands.ExitSuccess +} diff --git a/test/e2e/integration_test.go b/test/e2e/integration_test.go index ec364162d7b..c4e7e39adab 100644 --- a/test/e2e/integration_test.go +++ b/test/e2e/integration_test.go @@ -1009,6 +1009,57 @@ func TestTmpMountWithSize(t *testing.T) { } } +func TestLibaclOnTmpMount(t *testing.T) { + ctx := context.Background() + d := dockerutil.MakeContainer(ctx, t) + defer d.CleanUp(ctx) + + opts := dockerutil.RunOpts{ + Image: "basic/libacl", + Mounts: []mount.Mount{ + { + Type: mount.TypeTmpfs, + Target: "/mymnt", + }, + }, + } + + if err := d.Create(ctx, opts, "sleep", "infinity"); err != nil { + t.Fatalf("docker create failed: %v", err) + } + if err := d.Start(ctx); err != nil { + t.Fatalf("docker start failed: %v", err) + } + + if _, err := d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", "mkdir /mymnt/acl_test"); err != nil { + t.Fatalf("docker exec failed: %v", err) + } + + // Try to add named users/groups to the directory as a default ACL. + cmd := "setfacl -d -m u:alice:rwx,g:employees:rwx /mymnt/acl_test" + if _, err := d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", cmd); err != nil { + t.Fatalf("docker exec failed: %v", err) + } + + // Create a new file in the directory. + if _, err := d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", "echo hello > /mymnt/acl_test/file"); err != nil { + t.Fatalf("docker exec failed: %v", err) + } + + // The default ACL is inherited as the file's access ACL. + cmd = "getfacl --access --no-effective --omit-header /mymnt/acl_test/file" + out, err := d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", cmd) + if err != nil { + t.Fatalf("docker exec failed: %v", err) + } + lines := strings.Fields(out) + for _, y := range []string{"user:alice:rwx", "group:employees:rwx"} { + if !slices.Contains(lines, y) { + t.Fatalf("getfacl output: expected %v, not found in: %v", y, lines) + } + } +} + // NOTE(b/236028361): Regression test. Check we can handle a working directory // without execute permissions. See comment in // pkg/sentry/kernel/kernel.go:CreateProcess() for more context. diff --git a/test/syscalls/BUILD b/test/syscalls/BUILD index 1a95b2851ba..87688119adc 100644 --- a/test/syscalls/BUILD +++ b/test/syscalls/BUILD @@ -1345,3 +1345,7 @@ syscall_test( add_overlay = True, test = "//test/syscalls/linux:xattr_test", ) + +syscall_test( + test = "//test/syscalls/linux:posix_acl_test", +) diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index e99544022d1..47e4852e0db 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -4964,6 +4964,26 @@ cc_binary( ], ) +cc_binary( + name = "posix_acl_test", + testonly = 1, + srcs = ["posix_acl.cc"], + linkstatic = 1, + malloc = "//test/util:errno_safe_allocator", + deps = select_gtest() + [ + "//test/util:capability_util", + "//test/util:cleanup", + "//test/util:file_descriptor", + "//test/util:fs_util", + "//test/util:posix_error", + "//test/util:temp_path", + "//test/util:test_main", + "//test/util:test_util", + "//test/util:thread_util", + "@com_google_absl//absl/time", + ], +) + cc_binary( name = "cgroup_test", testonly = 1, diff --git a/test/syscalls/linux/posix_acl.cc b/test/syscalls/linux/posix_acl.cc new file mode 100644 index 00000000000..addb5c1e958 --- /dev/null +++ b/test/syscalls/linux/posix_acl.cc @@ -0,0 +1,990 @@ +// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#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/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(tag), static_cast(perm), id}; +} + +// BuildACL builds the raw xattr representation for a POSIX ACL. +std::string BuildACL(const std::vector& entries) { + uint32_t version = kACLVersion; + std::string buf(reinterpret_cast(&version), sizeof(version)); + for (const ACLEntry& e : entries) { + buf.append(reinterpret_cast(&e), sizeof(e)); + } + return buf; +} + +// ParseACL parses a raw ACL value into its entries. +std::vector ParseACL(const std::string& blob) { + std::vector 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 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; +} + +class PosixACLTest : public ::testing::Test { + protected: + void SetUp() override { + // Use /dev/shm to allow the native tests to run without privilege + dir_ = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn("/dev/shm")); + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(IsTmpfs(dir_.path()))); + + file_ = JoinPath(dir_.path(), "posix_acl_test_file"); + ASSERT_NO_ERRNO_AND_VALUE(Open(file_, O_CREAT | O_RDWR, 0644)); + ASSERT_THAT(chmod(file_.c_str(), 0644), SyscallSucceeds()); + + subdir_ = JoinPath(dir_.path(), "subdir"); + ASSERT_THAT(mkdir(subdir_.c_str(), 0755), SyscallSucceeds()); + + uid_ = getuid(); + gid_ = getgid(); + } + + TempPath dir_; + std::string file_; + std::string subdir_; + uid_t uid_; + gid_t gid_; +}; + +// Setting an access ACL and reading it back returns an identical blob. +TEST_F(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_F(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_F(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_F(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_F(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_F(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_F(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_F(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_F(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); +} + +// chmod on a file with an extended ACL updates USER_OBJ, the mask, and OTHER +// (not the GROUP_OBJ entry). +TEST_F(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_F(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_F(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_F(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_F(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_F(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)); + }); +} + +// POSIX access ACLs should work properly with symlinks. +TEST_F(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_F(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_F(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_F(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_F(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_F(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_F(PosixACLTest, SetACLEmpty) { + // 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_F(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_F(PosixACLTest, SetACLIncompleteHeader) { + // Set an ACL on the file + char xattr[1] = {}; + EXPECT_THAT(setxattr(file_.c_str(), kAccessACL, xattr, 1, 0), + SyscallFailsWithErrno(EINVAL)); +} + +TEST_F(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_F(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_F(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_F(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_F(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_F(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_F(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)); +} + +} // namespace + +} // namespace testing +} // namespace gvisor