Files
Jing ChenandgVisor bot b3c3770164 Implement ambient capabilities.
Fix #3166

PiperOrigin-RevId: 929312330
2026-06-09 11:35:27 -07:00

340 lines
14 KiB
Go

// Copyright 2018 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 auth
import (
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/bits"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/log"
)
// A CapabilitySet is a set of capabilities implemented as a bitset. The zero
// value of CapabilitySet is a set containing no capabilities.
type CapabilitySet uint64
// AllCapabilities is a CapabilitySet containing all valid capabilities.
var AllCapabilities = CapabilitySetOf(linux.CAP_LAST_CAP+1) - 1
// CapabilitySetOf returns a CapabilitySet containing only the given
// capability.
func CapabilitySetOf(cp linux.Capability) CapabilitySet {
return bits.MaskOf[CapabilitySet](int(cp))
}
// CapabilitySetOfMany returns a CapabilitySet containing the given capabilities.
func CapabilitySetOfMany(cps []linux.Capability) CapabilitySet {
var cs CapabilitySet
for _, cp := range cps {
cs |= bits.MaskOf[CapabilitySet](int(cp))
}
return cs
}
// Add adds the given capability to the CapabilitySet.
func (cs *CapabilitySet) Add(cp linux.Capability) {
*cs |= CapabilitySetOf(cp)
}
// Clear removes the given capability from the CapabilitySet.
func (cs *CapabilitySet) Clear(cp linux.Capability) {
*cs &= ^CapabilitySetOf(cp)
}
// IsSubsetOf returns true if the given capability set is a subset of "super".
func (cs *CapabilitySet) IsSubsetOf(super CapabilitySet) bool {
return *cs&super == *cs
}
// VfsCapDataOf returns a VfsCapData containing the file capabilities for the given slice of bytes.
// For each field of the cap data, which are in the structure of either vfs_cap_data or vfs_ns_cap_data,
// the bytes are ordered in little endian.
func VfsCapDataOf(data []byte) (linux.VfsNsCapData, error) {
size := len(data)
if size != linux.XATTR_CAPS_SZ_2 && size != linux.XATTR_CAPS_SZ_3 {
log.Warningf("the size of security.capability is invalid: size=%d", size)
return linux.VfsNsCapData{}, linuxerr.EINVAL
}
var capData linux.VfsNsCapData
if size == linux.XATTR_CAPS_SZ_3 {
capData.UnmarshalUnsafe(data)
} else {
capData.VfsCapData.UnmarshalUnsafe(data)
// rootid = 0 is correct for version 2 file capabilities.
}
// See security/commoncap.c:validheader().
if sansflags := capData.MagicEtc & ^uint32(linux.VFS_CAP_FLAGS_EFFECTIVE); (size == linux.XATTR_CAPS_SZ_2 && sansflags != linux.VFS_CAP_REVISION_2) ||
(size == linux.XATTR_CAPS_SZ_3 && sansflags != linux.VFS_CAP_REVISION_3) {
log.Warningf("the magic header of security.capability is invalid: magic=%#x, size=%d", capData.MagicEtc, size)
return linux.VfsNsCapData{}, linuxerr.EINVAL
}
return capData, nil
}
// FixupVfsCapDataOnSet may convert the given value to v3 file capabilities. It
// is analogous to security/commoncap.c:cap_convert_nscap().
func FixupVfsCapDataOnSet(creds *Credentials, value string, kuid KUID, kgid KGID) (string, error) {
vfsCaps, err := VfsCapDataOf([]byte(value))
if err != nil {
return "", err
}
if !creds.HasCapabilityOnFile(linux.CAP_SETFCAP, kuid, kgid) {
return "", linuxerr.EPERM
}
if vfsCaps.IsRevision2() && creds.HasRootCapability(linux.CAP_SETFCAP) {
// The user is privileged, allow the v2 write.
return value, nil
}
// Linux does the following UID gymnastics:
// 1. The userspace-provided rootID is relative to the caller's user
// namespace. So vfsCaps.RootID is mapped down to KUID first.
// 2. If this is an ID-mapped mount, the result is mapped up using the
// ID-map and then down again using the filesystem's owning user
// namespace (inode->i_sb->s_user_ns). We again have a KUID result.
// 3. The result is mapped up using the filesystem's owning user namespace.
//
// The final result is saved in the xattr value at vfs_ns_cap_data->rootid.
// Since gVisor does not support ID-mapped mounts and all filesystems are
// owned by the initial user namespace, we can skip steps 2 and 3.
rootID := creds.UserNamespace.MapToKUID(UID(vfsCaps.RootID))
if !rootID.Ok() {
return "", linuxerr.EINVAL
}
vfsCaps.ConvertToV3(uint32(rootID))
return vfsCaps.ToString(), nil
}
// FixupVfsCapDataOnGet may convert the given value to v2 file capabilities. It
// is analogous to security/commoncap.c:cap_inode_getsecurity().
func FixupVfsCapDataOnGet(creds *Credentials, value string) (string, error) {
vfsCaps, err := VfsCapDataOf([]byte(value))
if err != nil {
return "", err
}
// Linux does the steps mentioned in FixupVfsCapDataOnSet in reverse. But
// since gVisor does not support ID-mapped mounts and all filesystems are
// owned by the initial user namespace, we only need to reverse step 1 here.
rootID := KUID(vfsCaps.RootID)
mappedRoot := creds.UserNamespace.MapFromKUID(rootID)
if mappedRoot.Ok() && mappedRoot != RootUID {
// Return this as v3.
vfsCaps.ConvertToV3(uint32(mappedRoot))
return vfsCaps.ToString(), nil
}
if !rootIDOwnsCurrentUserns(creds, rootID) {
return "", linuxerr.EOVERFLOW
}
// Return this as v2.
vfsCaps.ConvertToV2()
return vfsCaps.ToString(), nil
}
// Analogous to security/commoncap.c:rootid_owns_currentns().
func rootIDOwnsCurrentUserns(creds *Credentials, rootID KUID) bool {
if !rootID.Ok() {
return false
}
for ns := creds.UserNamespace; ns != nil; ns = ns.parent {
if ns.MapFromKUID(rootID) == RootUID {
return true
}
}
return false
}
// FilePrivileges contains the file privileges for a file.
type FilePrivileges struct {
// SetUserID, when not NoID, indicates that the file has the setuid bit set. It is the KUID of the
// owner of the file.
SetUserID KUID
// SetGroupID, when not NoID, indicates that the file has the setgid bit set. It is the KGID of
// the owning group of the file.
SetGroupID KGID
// HasCaps indicates whether the file has capabilities attached.
HasCaps bool
// CapRootID is the KUID of the namespace root of the Task that created the file caps.
CapRootID KUID
// "These capabilities are automatically permitted to the thread, regardless of the thread's
// inheritable capabilities." - capabilities(7).
PermittedCaps CapabilitySet
// "This set is ANDed with the thread's inheritable set to determine which inheritable capabilities
// are enabled in the permitted set of the thread after the execve(2)." - capabilities(7).
InheritableCaps CapabilitySet
// "Determines if all of the new permitted capabilities for the thread are also raised in the
// effective set." - capabilities(7).
Effective bool
}
// handlePrivilegedRoot updates creds for a privileged root user as per
// "Capabilities and execution of programs by root" in capabilities(7).
func handlePrivilegedRoot(c *Credentials, f *FilePrivileges, filename string) {
// gVisor currently does not support SECURE_NOROOT secure bit since
// PR_SET_SECUREBITS is not supported. So no need to check here.
root := c.UserNamespace.MapToKUID(RootUID)
// "If (a) the binary that is being executed has capabilities attached and (b) the real user ID of
// the process is not 0 (root) and (c) the effective user ID of the process is 0 (root), then the
// file capability bits are honored. (i.e., they are not notionally considered to be all ones)."
// - capabilities(7)
if f.HasCaps && c.RealKUID != root && c.EffectiveKUID == root {
log.Warningf("File %q has both SUID bit and file capabilities set, not raising all capabilities.", filename)
return
}
// "If the real or effective user ID of the process is 0 (root), then the file inheritable and
// permitted sets are ignored; instead they are notionally considered to be all ones (i.e., all
// capabilities enabled)." - capabilities(7)
if c.RealKUID == root || c.EffectiveKUID == root {
// P'(permitted) = P(inheritable) | P(bounding)
c.PermittedCaps = c.BoundingCaps | c.InheritableCaps
}
// "If the effective user ID of the process is 0 (root) or the file effective bit is in fact
// enabled, then the file effective bit is notionally defined to be one (enabled)." - capabilities(7)
f.Effective = c.EffectiveKUID == root || f.Effective
}
// ComputeCredsForExec computes the new credentials given the file privileges.
// It returns the new creds and a bool indicating if the task is executing with
// elevated privileges. A few words about the arguments:
// - c: The current credentials of the task.
// - f: The file privileges of the executable.
// - filename: The name of the executable, used for logging.
// - noNewPrivs: The current state of the prctl NO_NEW_PRIVS.
// - stopPrivGain: Determines if privilege gain should be stopped for reasons beyond NO_NEW_PRIVS.
// Both noNewPrivs and stopPrivGain prevent cap gain, but stopPrivGain does not by itself
// prevent ID gain.
// - allowSUID: If true, the task will be allowed to setuid.
// Both noNewPrivs and allowSUID prevent ID gain, but allowSUID does not by itself prevent cap
// gain. Note also that while noNewPrivs brings down the effective IDs down to the real IDs,
// allowSUID at most prevents further ID gain due the SUID/GID bits.
//
// Note that gVisor does not support Ambient capabilities.
func ComputeCredsForExec(c *Credentials, f FilePrivileges, filename string,
noNewPrivs bool, stopPrivGain bool, allowSUID bool) (*Credentials, bool, error) {
if noNewPrivs || !allowSUID {
f.SetUserID = NoID
f.SetGroupID = NoID
}
if noNewPrivs {
f.HasCaps = false
f.Effective = false
}
// "...if either the user or the group ID of the file has no mapping inside the namespace, the
// set-user-ID (set-group-ID) bit is silently ignored: the new program is executed, but the
// process's effective user (group) ID is left unchanged." - user_namespaces(7).
if !f.SetUserID.In(c.UserNamespace).Ok() {
f.SetUserID = NoID
}
if !f.SetGroupID.In(c.UserNamespace).Ok() {
f.SetGroupID = NoID
}
// "...capabilities are conferred only if the binary is executed by a process that resides in a
// user namespace whose UID 0 maps to the root user ID that is saved in the extended attribute,
// or when executed by a process that resides in a descendant of such a namespace."
// - capabilities(7).
if !rootIDOwnsCurrentUserns(c, f.CapRootID) {
f.HasCaps = false
f.Effective = false
}
newC := c.Fork()
if f.SetUserID.Ok() {
newC.EffectiveKUID = f.SetUserID
}
if f.SetGroupID.Ok() {
newC.EffectiveKGID = f.SetGroupID
}
// P'(ambient) = (file is privileged) ? 0 : P(ambient)
ambientCaps := c.AmbientCaps
fileIsPrivileged := f.SetUserID.Ok() || f.SetGroupID.Ok() || f.HasCaps
if fileIsPrivileged {
ambientCaps = CapabilitySet(0)
}
// P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & P(bounding)) | P'(ambient)
newC.PermittedCaps = CapabilitySet(0)
if !fileIsPrivileged {
newC.PermittedCaps = ambientCaps
}
if f.HasCaps {
newC.PermittedCaps |= (c.InheritableCaps & f.InheritableCaps) | (f.PermittedCaps & c.BoundingCaps)
// The "Safety checking for capability-dumb binaries" section of capabilities(7) says:
// "...For such applications, the effective capability bit is set on the file...
// ...If the process did not obtain the full set of file permitted capabilities,
// then execve(2) fails with the error EPERM."
if f.Effective && (newC.PermittedCaps&f.PermittedCaps != f.PermittedCaps) {
return nil, false, linuxerr.EPERM
}
}
// newC.PermittedCaps and f.Effective are set differently for namespace root.
handlePrivilegedRoot(newC, &f, filename)
// Deny privilege elevation if we have to, see commoncap.c:cap_bprm_creds_from_file().
gainedID := (newC.EffectiveKUID != c.RealKUID) || (newC.EffectiveKGID != c.RealKGID)
gainedCaps := !newC.PermittedCaps.IsSubsetOf(c.PermittedCaps)
if (gainedID || gainedCaps) && (noNewPrivs || stopPrivGain) {
if noNewPrivs || !c.HasSelfCapability(linux.CAP_SETUID) {
newC.EffectiveKUID = c.RealKUID
newC.EffectiveKGID = c.RealKGID
}
newC.PermittedCaps &= c.PermittedCaps
}
newC.SavedKUID = newC.EffectiveKUID
newC.SavedKGID = newC.EffectiveKGID
// P'(effective) = effective ? P'(permitted) : P'(ambient).
newC.EffectiveCaps = ambientCaps
if f.Effective {
newC.EffectiveCaps = newC.PermittedCaps
}
newC.AmbientCaps = ambientCaps
// prctl(2): The "keep capabilities" value will be reset to 0 on subsequent calls to execve(2).
newC.KeepCaps = false
root := c.UserNamespace.MapToKUID(RootUID)
// See commoncap.c:cap_bprm_secureexec() in Linux 4.3+.
secureExec := gainedID || (newC.RealKUID != root && (f.Effective || !newC.PermittedCaps.IsSubsetOf(ambientCaps)))
return newC, secureExec, nil
}
// TaskCapabilities represents all the capability sets for a task. Each of these
// sets is explained in greater detail in capabilities(7).
type TaskCapabilities struct {
// Permitted is a limiting superset for the effective capabilities that
// the thread may assume.
PermittedCaps CapabilitySet
// Inheritable is a set of capabilities preserved across an execve(2).
InheritableCaps CapabilitySet
// Effective is the set of capabilities used by the kernel to perform
// permission checks for the thread.
EffectiveCaps CapabilitySet
// Bounding is a limiting superset for the capabilities that a thread
// can add to its inheritable set using capset(2).
BoundingCaps CapabilitySet
// Ambient is a set of capabilities that are preserved across an
// execve(2) of a program that is not privileged.
AmbientCaps CapabilitySet
}