From 586c38d70081b13b2ed494cef48e99b93956843e Mon Sep 17 00:00:00 2001 From: Jing Chen Date: Fri, 15 Mar 2024 14:04:33 -0700 Subject: [PATCH] Apply a image's file capabilities when creating a process from the image. PiperOrigin-RevId: 616237209 --- images/basic/filecap/Dockerfile | 5 + pkg/abi/linux/capability.go | 2 + pkg/sentry/kernel/auth/BUILD | 5 +- pkg/sentry/kernel/auth/capability_set.go | 33 ++++- pkg/sentry/kernel/auth/capability_set_test.go | 125 ++++++++++++++++++ pkg/sentry/kernel/kernel.go | 17 ++- test/e2e/exec_test.go | 112 +++++++++++++--- 7 files changed, 275 insertions(+), 24 deletions(-) create mode 100644 images/basic/filecap/Dockerfile diff --git a/images/basic/filecap/Dockerfile b/images/basic/filecap/Dockerfile new file mode 100644 index 000000000..9aaf71d9f --- /dev/null +++ b/images/basic/filecap/Dockerfile @@ -0,0 +1,5 @@ +FROM alpine + +RUN apk add libcap + +RUN cp /bin/busybox /mnt/cat && setcap cap_net_admin+ep /mnt/cat diff --git a/pkg/abi/linux/capability.go b/pkg/abi/linux/capability.go index 02c0269fb..27f8aa288 100644 --- a/pkg/abi/linux/capability.go +++ b/pkg/abi/linux/capability.go @@ -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 diff --git a/pkg/sentry/kernel/auth/BUILD b/pkg/sentry/kernel/auth/BUILD index 3aeb779a3..c29186b79 100644 --- a/pkg/sentry/kernel/auth/BUILD +++ b/pkg/sentry/kernel/auth/BUILD @@ -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", + ], ) diff --git a/pkg/sentry/kernel/auth/capability_set.go b/pkg/sentry/kernel/auth/capability_set.go index 1ebfbe39b..d5f0ef450 100644 --- a/pkg/sentry/kernel/auth/capability_set.go +++ b/pkg/sentry/kernel/auth/capability_set.go @@ -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 { diff --git a/pkg/sentry/kernel/auth/capability_set_test.go b/pkg/sentry/kernel/auth/capability_set_test.go index 9bfa45858..35c3a514f 100644 --- a/pkg/sentry/kernel/auth/capability_set_test.go +++ b/pkg/sentry/kernel/auth/capability_set_test.go @@ -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 diff --git a/pkg/sentry/kernel/kernel.go b/pkg/sentry/kernel/kernel.go index 86f5c2a4e..73237adb7 100644 --- a/pkg/sentry/kernel/kernel.go +++ b/pkg/sentry/kernel/kernel.go @@ -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, diff --git a/test/e2e/exec_test.go b/test/e2e/exec_test.go index 39868375f..c93e02688 100644 --- a/test/e2e/exec_test.go +++ b/test/e2e/exec_test.go @@ -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))