gvisor/runsc/specutils/seccomp/seccomp_test.go

415 lines
9.5 KiB
Go

// Copyright 2020 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 seccomp
import (
"fmt"
"syscall"
"testing"
specs "github.com/opencontainers/runtime-spec/specs-go"
"gvisor.dev/gvisor/pkg/binary"
"gvisor.dev/gvisor/pkg/bpf"
)
type seccompData struct {
nr uint32
arch uint32
instructionPointer uint64
args [6]uint64
}
// asInput converts a seccompData to a bpf.Input.
func asInput(d seccompData) bpf.Input {
return bpf.InputBytes{binary.Marshal(nil, binary.LittleEndian, d), binary.LittleEndian}
}
// testInput creates an Input struct with given seccomp input values.
func testInput(arch uint32, syscallName string, args *[6]uint64) bpf.Input {
syscallNo, err := lookupSyscallNo(arch, syscallName)
if err != nil {
// Assume tests set valid syscall names.
panic(err)
}
if args == nil {
argArray := [6]uint64{0, 0, 0, 0, 0, 0}
args = &argArray
}
data := seccompData{
nr: syscallNo,
arch: arch,
args: *args,
}
return asInput(data)
}
// testCase holds a seccomp test case.
type testCase struct {
name string
config specs.LinuxSeccomp
input bpf.Input
expected uint32
}
var (
// seccompTests is a list of speccomp test cases.
seccompTests = []testCase{
{
name: "default_allow",
config: specs.LinuxSeccomp{
DefaultAction: specs.ActAllow,
},
input: testInput(nativeArchAuditNo, "read", nil),
expected: uint32(allowAction),
},
{
name: "default_deny",
config: specs.LinuxSeccomp{
DefaultAction: specs.ActErrno,
},
input: testInput(nativeArchAuditNo, "read", nil),
expected: uint32(errnoAction),
},
{
name: "deny_arch",
config: specs.LinuxSeccomp{
DefaultAction: specs.ActAllow,
Syscalls: []specs.LinuxSyscall{
{
Names: []string{
"getcwd",
},
Action: specs.ActErrno,
},
},
},
// Syscall matches but the arch is AUDIT_ARCH_X86 so the return
// value is the bad arch action.
input: asInput(seccompData{nr: 183, arch: 0x40000003}), //
expected: uint32(killThreadAction),
},
{
name: "match_name_errno",
config: specs.LinuxSeccomp{
DefaultAction: specs.ActAllow,
Syscalls: []specs.LinuxSyscall{
{
Names: []string{
"getcwd",
"chmod",
},
Action: specs.ActErrno,
},
{
Names: []string{
"write",
},
Action: specs.ActTrace,
},
},
},
input: testInput(nativeArchAuditNo, "getcwd", nil),
expected: uint32(errnoAction),
},
{
name: "match_name_trace",
config: specs.LinuxSeccomp{
DefaultAction: specs.ActAllow,
Syscalls: []specs.LinuxSyscall{
{
Names: []string{
"getcwd",
"chmod",
},
Action: specs.ActErrno,
},
{
Names: []string{
"write",
},
Action: specs.ActTrace,
},
},
},
input: testInput(nativeArchAuditNo, "write", nil),
expected: uint32(traceAction),
},
{
name: "no_match_name_allow",
config: specs.LinuxSeccomp{
DefaultAction: specs.ActAllow,
Syscalls: []specs.LinuxSyscall{
{
Names: []string{
"getcwd",
"chmod",
},
Action: specs.ActErrno,
},
{
Names: []string{
"write",
},
Action: specs.ActTrace,
},
},
},
input: testInput(nativeArchAuditNo, "openat", nil),
expected: uint32(allowAction),
},
{
name: "simple_match_args",
config: specs.LinuxSeccomp{
DefaultAction: specs.ActAllow,
Syscalls: []specs.LinuxSyscall{
{
Names: []string{
"clone",
},
Args: []specs.LinuxSeccompArg{
{
Index: 0,
Value: syscall.CLONE_FS,
Op: specs.OpEqualTo,
},
},
Action: specs.ActErrno,
},
},
},
input: testInput(nativeArchAuditNo, "clone", &[6]uint64{syscall.CLONE_FS}),
expected: uint32(errnoAction),
},
{
name: "match_args_or",
config: specs.LinuxSeccomp{
DefaultAction: specs.ActAllow,
Syscalls: []specs.LinuxSyscall{
{
Names: []string{
"clone",
},
Args: []specs.LinuxSeccompArg{
{
Index: 0,
Value: syscall.CLONE_FS,
Op: specs.OpEqualTo,
},
{
Index: 0,
Value: syscall.CLONE_VM,
Op: specs.OpEqualTo,
},
},
Action: specs.ActErrno,
},
},
},
input: testInput(nativeArchAuditNo, "clone", &[6]uint64{syscall.CLONE_FS}),
expected: uint32(errnoAction),
},
{
name: "match_args_and",
config: specs.LinuxSeccomp{
DefaultAction: specs.ActAllow,
Syscalls: []specs.LinuxSyscall{
{
Names: []string{
"getsockopt",
},
Args: []specs.LinuxSeccompArg{
{
Index: 1,
Value: syscall.SOL_SOCKET,
Op: specs.OpEqualTo,
},
{
Index: 2,
Value: syscall.SO_PEERCRED,
Op: specs.OpEqualTo,
},
},
Action: specs.ActErrno,
},
},
},
input: testInput(nativeArchAuditNo, "getsockopt", &[6]uint64{0, syscall.SOL_SOCKET, syscall.SO_PEERCRED}),
expected: uint32(errnoAction),
},
{
name: "no_match_args_and",
config: specs.LinuxSeccomp{
DefaultAction: specs.ActAllow,
Syscalls: []specs.LinuxSyscall{
{
Names: []string{
"getsockopt",
},
Args: []specs.LinuxSeccompArg{
{
Index: 1,
Value: syscall.SOL_SOCKET,
Op: specs.OpEqualTo,
},
{
Index: 2,
Value: syscall.SO_PEERCRED,
Op: specs.OpEqualTo,
},
},
Action: specs.ActErrno,
},
},
},
input: testInput(nativeArchAuditNo, "getsockopt", &[6]uint64{0, syscall.SOL_SOCKET}),
expected: uint32(allowAction),
},
{
name: "Simple args (no match)",
config: specs.LinuxSeccomp{
DefaultAction: specs.ActAllow,
Syscalls: []specs.LinuxSyscall{
{
Names: []string{
"clone",
},
Args: []specs.LinuxSeccompArg{
{
Index: 0,
Value: syscall.CLONE_FS,
Op: specs.OpEqualTo,
},
},
Action: specs.ActErrno,
},
},
},
input: testInput(nativeArchAuditNo, "clone", &[6]uint64{syscall.CLONE_VM}),
expected: uint32(allowAction),
},
{
name: "OpMaskedEqual (match)",
config: specs.LinuxSeccomp{
DefaultAction: specs.ActAllow,
Syscalls: []specs.LinuxSyscall{
{
Names: []string{
"clone",
},
Args: []specs.LinuxSeccompArg{
{
Index: 0,
Value: syscall.CLONE_FS,
ValueTwo: syscall.CLONE_FS,
Op: specs.OpMaskedEqual,
},
},
Action: specs.ActErrno,
},
},
},
input: testInput(nativeArchAuditNo, "clone", &[6]uint64{syscall.CLONE_FS | syscall.CLONE_VM}),
expected: uint32(errnoAction),
},
{
name: "OpMaskedEqual (no match)",
config: specs.LinuxSeccomp{
DefaultAction: specs.ActAllow,
Syscalls: []specs.LinuxSyscall{
{
Names: []string{
"clone",
},
Args: []specs.LinuxSeccompArg{
{
Index: 0,
Value: syscall.CLONE_FS | syscall.CLONE_VM,
ValueTwo: syscall.CLONE_FS | syscall.CLONE_VM,
Op: specs.OpMaskedEqual,
},
},
Action: specs.ActErrno,
},
},
},
input: testInput(nativeArchAuditNo, "clone", &[6]uint64{syscall.CLONE_FS}),
expected: uint32(allowAction),
},
{
name: "OpMaskedEqual (clone)",
config: specs.LinuxSeccomp{
DefaultAction: specs.ActErrno,
Syscalls: []specs.LinuxSyscall{
{
Names: []string{
"clone",
},
// This comes from the Docker default seccomp
// profile for clone.
Args: []specs.LinuxSeccompArg{
{
Index: 0,
Value: 0x7e020000,
ValueTwo: 0x0,
Op: specs.OpMaskedEqual,
},
},
Action: specs.ActAllow,
},
},
},
input: testInput(nativeArchAuditNo, "clone", &[6]uint64{0x50f00}),
expected: uint32(allowAction),
},
}
)
// TestRunscSeccomp generates seccomp programs from OCI config and executes
// them using runsc's library, comparing against expected results.
func TestRunscSeccomp(t *testing.T) {
for _, tc := range seccompTests {
t.Run(tc.name, func(t *testing.T) {
runscProgram, err := BuildProgram(&tc.config)
if err != nil {
t.Fatalf("generating runsc BPF: %v", err)
}
if err := checkProgram(runscProgram, tc.input, tc.expected); err != nil {
t.Fatalf("running runsc BPF: %v", err)
}
})
}
}
// checkProgram runs the given program over the given input and checks the
// result against the expected output.
func checkProgram(p bpf.Program, in bpf.Input, expected uint32) error {
result, err := bpf.Exec(p, in)
if err != nil {
return err
}
if result != expected {
// Include a decoded version of the program in output for debugging purposes.
decoded, _ := bpf.DecodeProgram(p)
return fmt.Errorf("Unexpected result: got: %d, expected: %d\nBPF Program\n%s", result, expected, decoded)
}
return nil
}