mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Apply a image's file capabilities when creating a process from the image.
PiperOrigin-RevId: 616237209
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
FROM alpine
|
||||
|
||||
RUN apk add libcap
|
||||
|
||||
RUN cp /bin/busybox /mnt/cat && setcap cap_net_admin+ep /mnt/cat
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user