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