Apply a image's file capabilities when creating a process from the image.

PiperOrigin-RevId: 616237209
This commit is contained in:
Jing Chen
2024-03-15 14:08:39 -07:00
committed by gVisor bot
parent d20b115010
commit 586c38d700
7 changed files with 275 additions and 24 deletions
+5
View File
@@ -0,0 +1,5 @@
FROM alpine
RUN apk add libcap
RUN cp /bin/busybox /mnt/cat && setcap cap_net_admin+ep /mnt/cat
+2
View File
@@ -230,6 +230,8 @@ const (
// Constants that are used by file capability extended attributes, defined
// in Linux's include/uapi/linux/capability.h.
const (
// The flag decides the value of effective file capabilit
VFS_CAP_FLAGS_EFFECTIVE = 0x000001
// VFS_CAP_REVISION_1 was the original file capability implementation,
// which supported 32-bit masks for file capabilities.
VFS_CAP_REVISION_1 = 0x01000000
+4 -1
View File
@@ -107,5 +107,8 @@ go_test(
name = "auth_test",
srcs = ["capability_set_test.go"],
library = ":auth",
deps = ["//pkg/abi/linux"],
deps = [
"//pkg/abi/linux",
"//pkg/errors/linuxerr",
],
)
+31 -2
View File
@@ -20,6 +20,7 @@ import (
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/bits"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
)
// A CapabilitySet is a set of capabilities implemented as a bitset. The zero
@@ -69,14 +70,14 @@ func VfsCapDataOf(data []byte) (VfsCapData, error) {
// slice.
version := capData.MagicEtc & linux.VFS_CAP_REVISION_MASK
switch {
case version == linux.VFS_CAP_REVISION_3 && size == linux.XATTR_CAPS_SZ_3:
case version == linux.VFS_CAP_REVISION_3 && size >= linux.XATTR_CAPS_SZ_3:
// Like version 2 file capabilities, version 3 capability
// masks are 64 bits in size. In addition, version 3 has
// the root user ID of namespace, which is encoded in the
// security.capability extended attribute.
capData.RootID = binary.LittleEndian.Uint32(data[20:24])
fallthrough
case version == linux.VFS_CAP_REVISION_2 && size == linux.XATTR_CAPS_SZ_2:
case version == linux.VFS_CAP_REVISION_2 && size >= linux.XATTR_CAPS_SZ_2:
capData.Permitted += CapabilitySet(binary.LittleEndian.Uint32(data[12:16])) << 32
capData.Inheritable += CapabilitySet(binary.LittleEndian.Uint32(data[16:20])) << 32
default:
@@ -85,6 +86,34 @@ func VfsCapDataOf(data []byte) (VfsCapData, error) {
return capData, nil
}
// CapsFromVfsCaps returns a copy of the given creds with new capability sets
// by applying the file capability that is specified by capData.
func CapsFromVfsCaps(capData VfsCapData, creds *Credentials) (*Credentials, error) {
// If the real or effective user ID of the process is root,
// the file inheritable and permitted sets are ignored from
// `Capabilities and execution of programs by root` at capabilities(7).
if root := creds.UserNamespace.MapToKUID(RootUID); creds.EffectiveKUID == root || creds.RealKUID == root {
return creds, nil
}
// The credentials object is immutable.
newCreds := creds.Fork()
effective := (capData.MagicEtc & linux.VFS_CAP_FLAGS_EFFECTIVE) > 0
newCreds.PermittedCaps = (capData.Permitted & creds.BoundingCaps) |
(capData.Inheritable & creds.InheritableCaps)
// P'(effective) = effective ? P'(permitted) : P'(ambient).
// The ambient capabilities has not supported yet in gVisor,
// set effective capabilities to 0 when effective bit is false.
newCreds.EffectiveCaps = 0
if effective {
newCreds.EffectiveCaps = newCreds.PermittedCaps
}
// Insufficient to execute correctly.
if (capData.Permitted & ^newCreds.PermittedCaps) != 0 {
return nil, linuxerr.EPERM
}
return newCreds, 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 {
@@ -19,8 +19,133 @@ import (
"testing"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
)
// capsEquals returns trun when the given creds' capabilities match the given caps.
func capsEquals(creds *Credentials, caps TaskCapabilities) bool {
return creds.PermittedCaps == caps.PermittedCaps &&
creds.InheritableCaps == caps.InheritableCaps &&
creds.EffectiveCaps == caps.EffectiveCaps &&
creds.BoundingCaps == caps.BoundingCaps
}
// credentialsWithCaps returns a copy of creds with the given capabilities.
func credentialsWithCaps(creds *Credentials, permittedCaps, inheritableCaps, effectiveCaps, boundingCaps CapabilitySet) *Credentials {
newCreds := creds.Fork()
newCreds.PermittedCaps = permittedCaps
newCreds.InheritableCaps = inheritableCaps
newCreds.EffectiveCaps = effectiveCaps
newCreds.BoundingCaps = boundingCaps
return newCreds
}
func TestCapsFromVfsCaps(t *testing.T) {
for _, tst := range []struct {
name string
capData VfsCapData
creds *Credentials
wantCaps TaskCapabilities
wantErr error
}{
{
name: "TestRootCredential",
capData: VfsCapData{
MagicEtc: 0x2000001,
Permitted: CapabilitySetOf(linux.CAP_NET_ADMIN),
Inheritable: CapabilitySetOf(linux.CAP_NET_ADMIN),
},
creds: credentialsWithCaps(NewRootCredentials(NewRootUserNamespace()), AllCapabilities, CapabilitySetOf(linux.CAP_NET_RAW), AllCapabilities, CapabilitySetOf(linux.CAP_SYSLOG)),
wantCaps: TaskCapabilities{
PermittedCaps: AllCapabilities,
InheritableCaps: CapabilitySetOf(linux.CAP_NET_RAW),
EffectiveCaps: AllCapabilities,
BoundingCaps: CapabilitySetOf(linux.CAP_SYSLOG),
},
wantErr: nil,
},
{
name: "TestPermittedAndInheritableCaps",
capData: VfsCapData{
MagicEtc: 0x2000001,
Permitted: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETUID}),
Inheritable: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETGID}),
},
creds: credentialsWithCaps(
NewUserCredentials(123, 321, nil, nil, NewRootUserNamespace()),
AllCapabilities,
AllCapabilities,
AllCapabilities,
AllCapabilities),
wantCaps: TaskCapabilities{
PermittedCaps: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETUID, linux.CAP_SETGID}),
InheritableCaps: AllCapabilities,
EffectiveCaps: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETUID, linux.CAP_SETGID}),
BoundingCaps: AllCapabilities,
},
wantErr: nil,
},
{
name: "TestEffectiveBitOff",
capData: VfsCapData{
MagicEtc: 0x2000000,
Permitted: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETUID}),
Inheritable: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETGID}),
},
creds: credentialsWithCaps(
NewUserCredentials(123, 321, nil, nil, NewRootUserNamespace()),
AllCapabilities,
AllCapabilities,
AllCapabilities,
AllCapabilities),
wantCaps: TaskCapabilities{
PermittedCaps: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETUID, linux.CAP_SETGID}),
InheritableCaps: AllCapabilities,
EffectiveCaps: 0,
BoundingCaps: AllCapabilities,
},
wantErr: nil,
},
{
name: "TestInsufficientCaps",
capData: VfsCapData{
MagicEtc: 0x2000001,
Permitted: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETUID}),
Inheritable: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN}),
},
creds: credentialsWithCaps(
NewUserCredentials(123, 321, nil, nil, NewRootUserNamespace()),
AllCapabilities,
AllCapabilities,
AllCapabilities,
CapabilitySetOf(linux.CAP_CHOWN)),
wantCaps: TaskCapabilities{},
wantErr: linuxerr.EPERM,
},
} {
t.Run(tst.name, func(t *testing.T) {
newCreds, err := CapsFromVfsCaps(tst.capData, tst.creds)
if err == nil {
if tst.wantErr != nil {
t.Errorf("CapsFromVfsCaps(%v, %v) returned unexpected error %v", tst.capData, tst.creds, tst.wantErr)
}
if !capsEquals(newCreds, tst.wantCaps) {
t.Errorf("CapsFromVfsCaps(%v, %v) returned capabilities: %v, want capabilities: %v",
tst.capData, tst.creds,
TaskCapabilities{
PermittedCaps: newCreds.PermittedCaps,
InheritableCaps: newCreds.InheritableCaps,
EffectiveCaps: newCreds.EffectiveCaps,
BoundingCaps: newCreds.BoundingCaps,
}, tst.wantCaps)
}
} else if tst.wantErr == nil || tst.wantErr.Error() != err.Error() {
t.Errorf("CapsFromVfsCaps(%v, %v) returned error %v, wantErr: %v", tst.capData, tst.creds, err, tst.wantErr)
}
})
}
}
func TestVfsCapData(t *testing.T) {
for _, tst := range []struct {
name string
+13 -4
View File
@@ -1019,9 +1019,18 @@ func (k *Kernel) CreateProcess(args CreateProcessArgs) (*ThreadGroup, ThreadID,
if se != nil {
return nil, 0, errors.New(se.String())
}
// Take a reference on the FDTable, which will be transferred to
// TaskSet.NewTask().
var capData auth.VfsCapData
if len(image.FileCaps()) != 0 {
var err error
capData, err = auth.VfsCapDataOf([]byte(image.FileCaps()))
if err != nil {
return nil, 0, err
}
}
creds, err := auth.CapsFromVfsCaps(capData, args.Credentials)
if err != nil {
return nil, 0, err
}
args.FDTable.IncRef()
// Create the task.
@@ -1031,7 +1040,7 @@ func (k *Kernel) CreateProcess(args CreateProcessArgs) (*ThreadGroup, ThreadID,
TaskImage: image,
FSContext: fsContext,
FDTable: args.FDTable,
Credentials: args.Credentials,
Credentials: creds,
NetworkNamespace: k.RootNetworkNamespace(),
AllowedCPUMask: sched.NewFullCPUSet(k.applicationCores),
UTSNamespace: args.UTSNamespace,
+95 -17
View File
@@ -36,6 +36,11 @@ import (
"gvisor.dev/gvisor/runsc/specutils"
)
const (
noCap = "0000000000000000"
netAdminOnlyCap = "0000000000001000"
)
// Test that exec uses the exact same capability set as the container.
func TestExecCapabilities(t *testing.T) {
ctx := context.Background()
@@ -45,29 +50,104 @@ func TestExecCapabilities(t *testing.T) {
// Start the container.
if err := d.Spawn(ctx, dockerutil.RunOpts{
Image: "basic/alpine",
}, "sh", "-c", "cat /proc/self/status; sleep 100"); err != nil {
}, "sh", "-c", "cat /proc/self/status; sleep 200"); err != nil {
t.Fatalf("docker run failed: %v", err)
}
// Check that capability.
matches, err := d.WaitForOutputSubmatch(ctx, "CapEff:\t([0-9a-f]+)\n", 5*time.Second)
if err != nil {
t.Fatalf("WaitForOutputSubmatch() timeout: %v", err)
}
if len(matches) != 2 {
t.Fatalf("There should be a match for the whole line and the capability bitmask")
}
want := fmt.Sprintf("CapEff:\t%s\n", matches[1])
t.Log("Root capabilities:", want)
caps := []string{"CapInh", "CapPrm", "CapEff", "CapBnd"}
// Expected capabilities for non-root usres.
wantCaps := map[string]string{}
// For the root user.
for _, cap := range caps {
pattern := fmt.Sprintf("%s:\t([0-9a-f]+)\n", cap)
matches, err := d.WaitForOutputSubmatch(ctx, pattern, 5*time.Second)
if err != nil {
t.Fatalf("WaitForOutputSubmatch() timeout: %v", err)
}
if len(matches) != 2 {
t.Fatalf("There should be a match for the whole line and the capability bitmask")
}
want := fmt.Sprintf("%s:\t%s\n", cap, matches[1])
t.Log("root capabilities:", want)
// Now check that exec'd process capabilities match the root.
got, err := d.Exec(ctx, dockerutil.ExecOpts{}, "grep", "CapEff:", "/proc/self/status")
// Now check that exec'd process capabilities match the root.
got, err := d.Exec(ctx, dockerutil.ExecOpts{}, "grep", fmt.Sprintf("%s:", cap), "/proc/self/status")
if err != nil {
t.Fatalf("docker exec failed: %v", err)
}
if got != want {
t.Errorf("wrong %s, got: %q, want: %q", cap, got, want)
}
// CapBnd and CpaInh are unchanged, other capabilities will
// be tranformed for non-root users.
wantCaps[cap] = fmt.Sprintf("%s:\t%s\n", cap, noCap)
if cap == "CapBnd" || cap == "CapInh" {
wantCaps[cap] = got
}
}
gid, uid, groupname, username := "1001", "1002", "gvisor-test", "gvisor-test"
// Add a new group.
if _, err := d.Exec(ctx, dockerutil.ExecOpts{}, "addgroup", groupname, "--gid", gid); err != nil {
t.Fatalf("failed to create a new group: %v", err)
}
// Add a new user.
if _, err := d.Exec(ctx, dockerutil.ExecOpts{}, "adduser", "--no-create-home", "--disabled-password", "--gecos", "", "--ingroup", groupname, username); err != nil {
t.Fatalf("failed to create a new user: %v", err)
}
for cap, want := range wantCaps {
got, err := d.Exec(ctx, dockerutil.ExecOpts{User: uid}, "grep", fmt.Sprintf("%s:", cap), "/proc/self/status")
if err != nil {
t.Fatalf("docker exec failed: %v", err)
}
t.Logf("%s: %v", cap, got)
// Format the matched capability.
if got != want {
t.Errorf("wrong %s, got: %q, want: %q", cap, got, want)
}
}
}
func TestFileCap(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
// Start the container.
if err := d.Spawn(ctx, dockerutil.RunOpts{
Image: "basic/filecap",
CapAdd: []string{"NET_ADMIN"},
}, "sh", "-c", "cat /proc/self/status; sleep 100"); err != nil {
t.Fatalf("docker run failed: %v", err)
}
output, err := d.Exec(ctx, dockerutil.ExecOpts{User: "1001"}, "/mnt/cat", "/proc/self/status")
if err != nil {
t.Fatalf("docker exec failed: %v", err)
}
t.Logf("CapEff: %v", got)
if got != want {
t.Errorf("wrong capabilities, got: %q, want: %q", got, want)
expectedCaps := fmt.Sprintf("CapInh:\t%s\nCapPrm:\t%s\nCapEff:\t%s\n", noCap, netAdminOnlyCap, netAdminOnlyCap)
if !strings.Contains(output, expectedCaps) {
t.Fatalf("can't find expected caps:\n %v, output: %v", expectedCaps, output)
}
}
func TestNoExpectedFileCap(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
// Start the container.
if err := d.Spawn(ctx, dockerutil.RunOpts{
Image: "basic/filecap",
CapAdd: []string{"NET_RAW"},
}, "sh", "-c", "cat /proc/self/status; sleep 100"); err != nil {
t.Fatalf("docker run failed: %v", err)
}
output, err := d.Exec(ctx, dockerutil.ExecOpts{User: "1001"}, "/mnt/cat", "/proc/self/status")
if err == nil {
t.Fatalf("error not present")
}
if !strings.Contains(output, "operation not permitted") {
t.Fatalf("expected error: operation not permitted, got: %v", err)
}
}
@@ -107,7 +187,6 @@ func TestExecPrivileged(t *testing.T) {
if err != nil {
t.Fatalf("failed to convert capabilities %q: %v", matches[1], err)
}
t.Logf("Container capabilities: %#x", containerCaps)
// Expect no capabilities, unless raw sockets configured.
var wantContainerCaps uint64
@@ -126,7 +205,6 @@ func TestExecPrivileged(t *testing.T) {
if err != nil {
t.Fatalf("docker exec failed: %v", err)
}
t.Logf("Exec CapEff: %v", got)
wantCaps := specutils.AllCapabilitiesUint64()
if !testutil.IsRunningWithNetRaw() {
wantCaps &= ^bits.MaskOf64(int(linux.CAP_NET_RAW))