diff --git a/pkg/abi/linux/xattr.go b/pkg/abi/linux/xattr.go index 6d6606e625..937c57c498 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 7e1bff57b3..4402072ad9 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/fuse/file.go b/pkg/sentry/fsimpl/fuse/file.go index 8a3c103b76..f304a1a827 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 f3c233fa2a..7646f27240 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 f11c1fb58e..43a4491d14 100644 --- a/pkg/sentry/fsimpl/gofer/filesystem.go +++ b/pkg/sentry/fsimpl/gofer/filesystem.go @@ -21,6 +21,7 @@ import ( "sync" "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/atomicbitops" "gvisor.dev/gvisor/pkg/context" @@ -828,7 +829,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 { diff --git a/pkg/sentry/fsimpl/gofer/gofer.go b/pkg/sentry/fsimpl/gofer/gofer.go index 8777d8aa56..a4f5bd551b 100644 --- a/pkg/sentry/fsimpl/gofer/gofer.go +++ b/pkg/sentry/fsimpl/gofer/gofer.go @@ -49,6 +49,7 @@ import ( "sync/atomic" "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/atomicbitops" "gvisor.dev/gvisor/pkg/cleanup" @@ -1292,7 +1293,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 +1506,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 +1530,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 a7d374ae86..235128860e 100644 --- a/pkg/sentry/fsimpl/host/host.go +++ b/pkg/sentry/fsimpl/host/host.go @@ -21,6 +21,7 @@ import ( "math" "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/atomicbitops" "gvisor.dev/gvisor/pkg/context" @@ -412,7 +413,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 +608,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 20d351cb99..074f606d40 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 { diff --git a/pkg/sentry/fsimpl/kernfs/inode_impl_util.go b/pkg/sentry/fsimpl/kernfs/inode_impl_util.go index 9308cddae4..5defec6ae3 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 c3cd9711e1..17bdfc3717 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() diff --git a/pkg/sentry/fsimpl/overlay/overlay.go b/pkg/sentry/fsimpl/overlay/overlay.go index 4d20bded24..113617ad19 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) diff --git a/pkg/sentry/fsimpl/overlay/regular_file.go b/pkg/sentry/fsimpl/overlay/regular_file.go index c752ca657b..df5c0be759 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 b332fdbfbb..c1c7f4cd1d 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 1e16a2ff14..0aa7b5f2df 100644 --- a/pkg/sentry/fsimpl/proc/task.go +++ b/pkg/sentry/fsimpl/proc/task.go @@ -262,7 +262,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 9b35b60145..164d8f459f 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/filesystem.go b/pkg/sentry/fsimpl/tmpfs/filesystem.go index beb297783c..099aeee38f 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.acl.Load(), auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())); err != nil { return err } if i.nlink.Load() == 0 { diff --git a/pkg/sentry/fsimpl/tmpfs/save_restore.go b/pkg/sentry/fsimpl/tmpfs/save_restore.go index f7763bfc96..b50a435efc 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) } +// saveAcl is called by stateify. +func (i *inode) saveAcl() *vfs.PosixACL { + return i.acl.Load() +} + +// loadAcl is called by stateify. +func (i *inode) loadAcl(_ goContext.Context, acl *vfs.PosixACL) { + i.acl.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/tar.go b/pkg/sentry/fsimpl/tmpfs/tar.go index 8ca79344bf..4e98435dd3 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.acl.Store(acl) + ino.defaultACL.Store(defaultACL) ino.mode.Store(uint32(hdr.Mode) | linux.S_IFDIR) ino.mtime.Store(hdr.ModTime.UnixNano()) ino.setXattrsFromPAXRecords(hdr) @@ -172,6 +179,12 @@ func (fs *filesystem) mkdirFromTar(hdr *tar.Header, pathToInode map[string]*inod 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.acl.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()) @@ -211,6 +224,12 @@ func (fs *filesystem) mknodFromTar(ctx context.Context, hdr *tar.Header, pathToI if err != nil { return err } + acl, defaultACL, err := getACLsFromHeader(hdr) + if err != nil { + return err + } + childInode.acl.Store(acl) + childInode.defaultACL.Store(defaultACL) childInode.mtime.Store(hdr.ModTime.UnixNano()) childInode.setXattrsFromPAXRecords(hdr) child := fs.newDentry(childInode) @@ -278,6 +297,12 @@ func (fs *filesystem) symlinkFromTar(hdr *tar.Header, pathToInode map[string]*in 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.acl.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.acl.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 911463d570..821b06b2ec 100644 --- a/pkg/sentry/fsimpl/tmpfs/tmpfs.go +++ b/pkg/sentry/fsimpl/tmpfs/tmpfs.go @@ -221,14 +221,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) @@ -611,12 +613,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 + acl 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 @@ -653,8 +657,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.acl.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) @@ -739,7 +770,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.acl.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 @@ -797,7 +829,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.acl.Load(), auth.KUID(i.uid.Load()), auth.KGID(i.gid.Load())); err != nil { return err } @@ -833,18 +865,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.acl.Load() + newPerm := stat.Mode + if acl != nil { + newAcl := acl.Chmod(uint16(stat.Mode & linux.PermissionsMask)) + var equiv bool + newPerm, equiv = newAcl.Mode() + + if !equiv { + i.acl.Store(&newAcl) + } else { + i.acl.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.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() @@ -992,7 +1040,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.acl.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) { @@ -1000,11 +1060,37 @@ func (i *inode) getXattr(creds *auth.Credentials, opts *vfs.GetXattrOptions) (st return "", err } mode := linux.FileMode(i.mode.Load()) + acl := i.acl.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) { + switch opts.Name { + case linux.XATTR_NAME_POSIX_ACL_ACCESS: + if acl == nil { + return "", linuxerr.ENODATA + } + + // Serialize the access ACL for userspace + return string(acl.Serialize(creds.UserNamespace)), nil + case linux.XATTR_NAME_POSIX_ACL_DEFAULT: + defaultAcl := i.defaultACL.Load() + if defaultAcl == nil { + return "", linuxerr.ENODATA + } + + // Serialize the default ACL for userspace + return string(defaultAcl.Serialize(creds.UserNamespace)), nil + default: + return "", linuxerr.EOPNOTSUPP + } + } + + if err := vfs.GenericCheckPermissions(creds, vfs.MayRead, mode, acl, kuid, kgid); err != nil { return "", err } + return i.xattrs.GetXattr(creds, mode, kuid, opts) } @@ -1013,9 +1099,70 @@ func (i *inode) setXattr(creds *auth.Credentials, opts *vfs.SetXattrOptions) err return err } mode := linux.FileMode(i.mode.Load()) + acl := i.acl.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) { + switch opts.Name { + case linux.XATTR_NAME_POSIX_ACL_ACCESS: + // POSIX Access ACL + + if !vfs.CanActAsOwner(creds, kuid) { + return linuxerr.EPERM + } + + // POSIX ACL: parse from userspace + acl, err := vfs.ParsePosixACL([]byte(opts.Value), creds.UserNamespace) + if err != nil { + return err + } + + // Then, update the inode's mode + mode, equiv := acl.Mode() + i.mu.Lock() + defer i.mu.Unlock() + for { + old := i.mode.Load() + newMode := (old &^ linux.PermissionsMask) | uint32(mode) + 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.acl.Store(nil) + } else { + // Otherwise, store the ACL too for permission checking. + i.acl.Store(&acl) + } + case linux.XATTR_NAME_POSIX_ACL_DEFAULT: + // POSIX Default ACL + + if !mode.IsDir() { + // Default ACL can only be set on directories + return linuxerr.EACCES + } + + if !vfs.CanActAsOwner(creds, kuid) { + return linuxerr.EPERM + } + + // POSIX ACL: parse from userspace + defaultAcl, err := vfs.ParsePosixACL([]byte(opts.Value), creds.UserNamespace) + if err != nil { + return err + } + + i.defaultACL.Store(&defaultAcl) + default: + return linuxerr.EOPNOTSUPP + } + + return nil + } + + if err := vfs.GenericCheckPermissions(creds, vfs.MayWrite, mode, acl, kuid, kgid); err != nil { return err } return i.xattrs.SetXattr(creds, mode, kuid, kgid, opts) @@ -1026,11 +1173,37 @@ func (i *inode) removeXattr(creds *auth.Credentials, name string) error { return err } mode := linux.FileMode(i.mode.Load()) + acl := i.acl.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) { + switch name { + case linux.XATTR_NAME_POSIX_ACL_ACCESS: + if !vfs.CanActAsOwner(creds, kuid) { + return linuxerr.EPERM + } + + // Clear the access ACL. + i.acl.Store(nil) + case linux.XATTR_NAME_POSIX_ACL_DEFAULT: + if !vfs.CanActAsOwner(creds, kuid) { + return linuxerr.EPERM + } + + // Clear the default ACL. + i.defaultACL.Store(nil) + default: + return linuxerr.EOPNOTSUPP + } + + return nil + } + + if err := vfs.GenericCheckPermissions(creds, vfs.MayWrite, mode, acl, kuid, kgid); err != nil { return err } + return i.xattrs.RemoveXattr(creds, mode, kuid, name) } diff --git a/pkg/sentry/vfs/BUILD b/pkg/sentry/vfs/BUILD index e46acc3222..207c4313c6 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 f42b7f7464..b7d5e80a1e 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 diff --git a/pkg/sentry/vfs/permissions.go b/pkg/sentry/vfs/permissions.go index b69562d5a0..38df0e3558 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 @@ -180,7 +191,7 @@ func MayWriteFileWithOpenFlags(flags uint32) bool { // 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)) @@ -216,7 +227,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 +238,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 0000000000..1161d2bac8 --- /dev/null +++ b/pkg/sentry/vfs/posix_acl.go @@ -0,0 +1,390 @@ +// 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 ( + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/errors/linuxerr" + "gvisor.dev/gvisor/pkg/sentry/kernel/auth" +) + +// ACLUser represents a named ACL user. +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. +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. +// +// +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 an immutable list of named users in the ACL. + Users []ACLUser + + // Groups is an immutable 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) +} + +// UserPerms 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 { + if creds.EffectiveKUID == kuid { + // First, are we the owning user? + if ats.checkPerms(a.UserPerms()) { + return true + } + } else if found, passes := a.checkUserWithMask(creds, ats); found { + // If we're not the owning user, are we a named user? + // If so, check against the permissions specified there. + if passes { + return true + } + } else if found, passes := a.checkGroupsWithMask(creds, ats); creds.InGroup(kgid) || found { + // Otherwise, are we *either* in the owning group or a named group? + // If so, if any group grants access, allow the access. + if creds.InGroup(kgid) && ats.checkPerms(a.masked(a.GroupPerms())) { + return true + } + if found && passes { + return true + } + } else if ats.checkPerms(a.OtherPerms()) { + // Otherwise, use the permissions granted to other. + return true + } + + 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) { + // 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 + } + + // Now, take a look at the entries. + var userObj, groupObj, other, mask *uint16 + users := make([]ACLUser, 0) + groups := make([]ACLGroup, 0) + seenKUIDs := make(map[auth.KUID]struct{}) + seenKGIDs := make(map[auth.KGID]struct{}) + 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 + } + + if _, ok := seenKUIDs[kuid]; ok { + // UIDs must be unique + return PosixACL{}, linuxerr.EINVAL + } + seenKUIDs[kuid] = struct{}{} + + 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 + } + + if _, ok := seenKGIDs[kgid]; ok { + // GIDs must be unique + return PosixACL{}, linuxerr.EINVAL + } + seenKGIDs[kgid] = struct{}{} + + 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 + } + } + + // 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 + // (checked in the loop) + + ret := PosixACL{ + UGOPerms: (*userObj << 6) | (*groupObj << 3) | *other, + Mask: (*AccessTypes)(mask), + Users: users, + Groups: groups, + } + return ret, nil +} diff --git a/pkg/sentry/vfs/posix_acl_test.go b/pkg/sentry/vfs/posix_acl_test.go new file mode 100644 index 0000000000..6f7ebcf6ee --- /dev/null +++ b/pkg/sentry/vfs/posix_acl_test.go @@ -0,0 +1,350 @@ +// 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: "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: "header only, no entries", + src: marshalACLXattr(linux.POSIX_ACL_XATTR_VERSION), + 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: "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, permW, 982), + aclEntry(linux.ACL_GROUP_OBJ, permR, aclUndef), + aclEntry(linux.ACL_MASK, permRW, aclUndef), + aclEntry(linux.ACL_OTHER, permR, aclUndef), + ), + wantErr: linuxerr.EINVAL, + }, + } { + 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: got error %v, want %v", err, tc.wantErr) + } + return + } + if err != nil { + t.Fatalf("ParsePosixACL: unexpected error: %v", err) + } + if !aclEqual(got, tc.want) { + t.Errorf("ParsePosixACL = %+v, want %+v", 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/runsc/container/container_test.go b/runsc/container/container_test.go index 8b282c4fe0..fb74661615 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" @@ -36,6 +37,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" @@ -5283,6 +5285,160 @@ 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/sh", "-c", + fmt.Sprintf("touch %s && mkdir %s", aclFile, aclDir)); err != nil { + t.Fatalf("error creating ACL test files: %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 bfecf6c480..4b634565de 100644 --- a/test/cmd/test_app/BUILD +++ b/test/cmd/test_app/BUILD @@ -11,6 +11,7 @@ go_binary( srcs = [ "fds.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 c9cdd4fc68..d2dda5a17f 100644 --- a/test/cmd/test_app/main.go +++ b/test/cmd/test_app/main.go @@ -60,6 +60,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 0000000000..33ea93d694 --- /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/syscalls/BUILD b/test/syscalls/BUILD index c7c3628714..7dd394ff00 100644 --- a/test/syscalls/BUILD +++ b/test/syscalls/BUILD @@ -1338,3 +1338,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 37f8d56828..2a4bfae248 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -4936,6 +4936,24 @@ 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: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", + ], +) + 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 0000000000..8110c088a4 --- /dev/null +++ b/test/syscalls/linux/posix_acl.cc @@ -0,0 +1,464 @@ +// 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 "gmock/gmock.h" +#include "gtest/gtest.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(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); +} + +// Setting an access ACL with a named-group entry round-trips, and the stored +// entry is keyed by group ID. +TEST_F(PosixACLTest, SetGetNamedGroupACL) { + const std::string acl = BuildACL({ + Ent(kUserObj, kR | kW, kUndef), + 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); + EXPECT_EQ(FindEntryPerm(got, kGroup, gid_), kR | kW); +} + +// 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 the mask (the group bits), not +// the GROUP_OBJ entry. +TEST_F(PosixACLTest, ChmodUpdatesMask) { + 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()); + + // Give the group class read+write. This must move the mask to rw-, leaving + // GROUP_OBJ untouched. + ASSERT_THAT(chmod(file_.c_str(), 0664), SyscallSucceeds()); + + const std::string got = + ASSERT_NO_ERRNO_AND_VALUE(GetXattrString(file_, kAccessACL)); + EXPECT_EQ(FindEntryPerm(got, kMask, kUndef), kR | kW); + EXPECT_EQ(FindEntryPerm(got, kGroupObj, kUndef), kR); +} + +// 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([&] { + 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); + }); +} + +// 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)); + }); +} + +} // namespace + +} // namespace testing +} // namespace gvisor