blob: 2b86ea7dbbdf04297485b34690270d93801e4a92 [file] [log] [blame]
// Copyright 2015 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package seccomp
import (
"flag"
"os"
"runtime"
"syscall"
"testing"
"unsafe"
)
var (
parseFilePath = flag.String("parse_file", "", "path for TestParseFile")
testInstall = flag.Bool("test_install", false, "enable TestInstall (use with -cpu=N)")
testEndian = flag.Bool("test_endian", false, "enable TestEndian")
)
func TestParseLine(t *testing.T) {
tests := []struct {
policy string
want string
err string
}{
{policy: "read: 1"},
{policy: "open: return 1"},
{policy: "prctl: arg0 == 0xf"},
{policy: "prctl: arg0 != 0xf"},
{policy: "ioctl: arg1 & 0x5401"},
{policy: "ioctl: arg1 == 0x4024700a || arg1 == 0x541b"},
{policy: "ioctl: arg1 == 0x4024700a && arg1 == 0x541b"},
{policy: "ioctl: arg1 == 0x5401 || arg1 == 0x700a || arg2 & 0x541b"},
{policy: "ioctl: arg1 == 0x5401 && arg1 == 0x700a && arg3 & 0x541b"},
{policy: "ioctl: arg1 == 0x5401 || arg1 == 0x700a && arg4 & 0x541b"},
{policy: "ioctl: arg1 == 0x5401 && arg1 == 0x700a || arg5 & 0x541b"},
{policy: "ioctl: arg1 == 0x5401 && arg1 == 0x700a || arg5 & 0x541b; return 1"},
{
// different spacing around colon.
policy: "read :1",
want: "read: 1",
},
{
// leading and trailing whitespace.
policy: " open : return 1 ",
want: "open: return 1",
},
{
// return hexadecimal errno.
policy: "open: return 0x10",
want: "open: return 16",
},
{
// return octal errno.
policy: "open: return 010",
want: "open: return 8",
},
{
// return highest errno.
policy: "open: return 0xffff",
want: "open: return 65535",
},
{
// expression with no spaces.
policy: "ioctl:arg1==0x5401&&arg1==0x700a||arg2&0x541b",
want: "ioctl: arg1 == 0x5401 && arg1 == 0x700a || arg2 & 0x541b",
},
{
// compare with decimal value.
policy: "ioctl: arg1 == 5401 && arg1 == 0x700a || arg2 & 0x541b",
want: "ioctl: arg1 == 0x1519 && arg1 == 0x700a || arg2 & 0x541b",
},
{
// compare with octal value.
policy: "ioctl: arg1 == 05401 && arg1 == 0x700a || arg2 & 0x541b",
want: "ioctl: arg1 == 0xb01 && arg1 == 0x700a || arg2 & 0x541b",
},
{
// all decimal comparisons.
policy: "clone: arg0 == 1 || arg0 == 2 || arg0 == 16",
want: "clone: arg0 == 0x1 || arg0 == 0x2 || arg0 == 0x10",
},
{
// missing syscall name.
policy: ": 1",
err: "invalid syntax",
},
{
// malformed syscall name.
policy: "two words: 1",
err: "invalid syntax",
},
{
// missing colon.
policy: "read = 1",
err: "invalid syntax",
},
{
// trailing semicolon after return.
policy: "open: return 1;",
err: "invalid syntax",
},
{
// trailing semicolon after expression.
policy: "prctl: arg0 == 0xf;",
err: "invalid syntax",
},
{
// missing return after semicolon.
policy: "prctl: arg0 == 0xf; 1",
err: "invalid syntax",
},
{
// bad syscall name.
policy: "bad: 1",
err: "unknown syscall: bad",
},
{
// symbolic errno is not supported.
policy: "open: return EPERM",
err: "invalid errno: EPERM",
},
{
// errno must fit in 16 bits.
policy: "open: return 0x10000",
err: "invalid errno: 0x10000",
},
{
// missing argument index.
policy: "prctl: arg == 0xf",
err: "invalid expression: arg == 0xf",
},
{
// arg index out of range.
policy: "prctl: arg6 == 0xf",
err: "invalid expression: arg6 == 0xf",
},
{
// bitwise and with argument not supported.
policy: "prctl: arg0 & 0xf == 0xf",
err: "invalid expression: arg0 & 0xf == 0xf",
},
{
// unknown operator.
policy: "prctl: arg0 !== 0xf",
err: "invalid expression: arg0 !== 0xf",
},
{
// invalid hexadecimal value.
policy: "prctl: arg0 == 0xfdx",
err: "invalid value: arg0 == 0xfdx",
},
{
// invalid decimal value.
policy: "prctl: arg0 == 123a",
err: "invalid value: arg0 == 123a",
},
{
// invalid octal value.
policy: "prctl: arg0 == 0129",
err: "invalid value: arg0 == 0129",
},
{
// invalid subexpression.
policy: "prctl: arg0 == 0x100 && arg1 = 0x101 || arg2 == 0x102",
err: "invalid expression: arg1 = 0x101",
},
}
for _, test := range tests {
var err string
p, e := parseLine(test.policy)
if e != nil {
err = e.Error()
}
if err != "" || test.err != "" {
if err != test.err {
t.Errorf("parseLine(%q): error = %q; want %q", test.policy, err, test.err)
}
continue
}
want := test.want
if want == "" {
want = test.policy
}
if got := p.String(); got != want {
t.Errorf("parseLine(%q) = %q; want %q", test.policy, got, test.want)
}
}
}
func TestParseLines(t *testing.T) {
tests := []struct {
file []string
err string
}{
{
// simple policy file.
file: []string{
"read: 1",
"write: 1",
"open: return 1",
},
},
{
// comment lines are ignored.
file: []string{
"read: 1",
"write: 1",
"# open: return EPERM",
"open: return 1",
},
},
{
// blank lines are ignored.
file: []string{
"read: 1",
"write: 1",
"",
"open: return 1",
},
},
{
// leading space on comment line.
file: []string{
"read: 1",
"write: 1",
" # open: return EPERM",
"open: return 1",
},
err: "line 3: invalid syntax",
},
{
// line consisting of whitespace only.
file: []string{
"read: 1",
"write: 1",
" ",
"open: return 1",
},
err: "line 3: invalid syntax",
},
{
// parse error on one line.
file: []string{
"read: 1",
"write: return 019",
"open: return 1",
},
err: "line 2: invalid errno: 019",
},
{
// multiple policies for a syscall.
file: []string{
"read: 1",
"write: 1",
"read: return 1",
"open: return 1",
},
err: "lines 1,3: multiple policies for read",
},
}
for _, test := range tests {
var err string
_, e := parseLines(test.file)
if e != nil {
err = e.Error()
}
if err != "" || test.err != "" {
if err != test.err {
t.Errorf("parseLines(%q): error = %q; want %q", test.file, err, test.err)
}
}
}
}
func TestParseFile(t *testing.T) {
if *parseFilePath == "" {
t.Skip("use -parse_file to enable.")
}
if _, err := parseFile(*parseFilePath); err != nil {
t.Errorf("parseFile(%q): %v", *parseFilePath, err)
}
}
func TestCompile(t *testing.T) {
syscallName := make(map[int32]string)
for name, nr := range syscallNum {
syscallName[int32(nr)] = name
}
call := func(name string, args ...uint64) SeccompData {
nr, ok := syscallNum[name]
if !ok {
t.Fatalf("unknown syscall: %s", name)
}
data := SeccompData{
NR: int32(nr),
Arch: auditArch,
}
copy(data.Args[:], args)
return data
}
eval := func(bpf []SockFilter, data SeccompData) uint32 {
var A uint32
IP := 0
for {
Insn := bpf[IP]
IP++
switch Insn.Code {
case opLOAD:
A = *(*uint32)(unsafe.Pointer(uintptr(unsafe.Pointer(&data)) + uintptr(Insn.K)))
case opJEQ:
if A == Insn.K {
IP += int(Insn.JT)
} else {
IP += int(Insn.JF)
}
case opJSET:
if A&Insn.K != 0 {
IP += int(Insn.JT)
} else {
IP += int(Insn.JF)
}
case opJUMP:
IP += int(Insn.K)
case opRET:
return Insn.K
default:
t.Fatalf("unsupported instruction: %v", Insn)
}
}
}
file := []string{
"read: 1",
"open: return 1",
"write: arg0 == 1",
"close: arg0 == 2; return 9",
"dup: arg0 == 1 || arg0 == 2",
"pipe: arg0 == 1 && arg1 == 2",
"link: arg0 != 1 && arg1 != 2 || arg2 == 3",
"unlink: arg0 != 1 || arg1 != 2 && arg2 == 3",
"creat: arg0 & 0xf00 && arg1 & 0x0f0 && arg2 & 0x00f",
"lseek: arg0 & 0x0f000f000f000f00 && arg1 & 0x00f000f000f000f0 && arg2 & 0x000f000f000f000f",
"stat: arg0 != 0x0123456789abcdef && arg1 != 0x123456789abcdef0 || arg2 == 0x00f000f000000000",
"fstat: arg0 != 0x0123456789abcdef || arg1 != 0x123456789abcdef0 && arg2 == 0x00f000f000000000",
}
tests := []struct {
data SeccompData
want uint32
}{
{call("fork"), retKill()},
{call("read"), retAllow()},
{call("open"), retErrno(1)},
{call("write", 0), retKill()},
{call("write", 1), retAllow()},
{call("close", 1), retErrno(9)},
{call("close", 2), retAllow()},
{call("dup", 0), retKill()},
{call("dup", 1), retAllow()},
{call("dup", 2), retAllow()},
{call("dup", 3), retKill()},
{call("pipe", 1, 1), retKill()},
{call("pipe", 1, 2), retAllow()},
{call("pipe", 2, 1), retKill()},
{call("pipe", 2, 2), retKill()},
{call("link", 1, 2, 3), retAllow()},
{call("link", 1, 2, 2), retKill()},
{call("link", 2, 2, 2), retKill()},
{call("link", 2, 1, 2), retAllow()},
{call("unlink", 2, 1, 2), retAllow()},
{call("unlink", 1, 1, 2), retKill()},
{call("unlink", 1, 1, 3), retAllow()},
{call("unlink", 1, 2, 3), retKill()},
{call("creat", 0x100, 0x100, 0x101), retKill()},
{call("creat", 0x200, 0x110, 0x101), retAllow()},
{call("creat", 0x400, 0x110, 0x110), retKill()},
{call("creat", 0x800, 0x110, 0x007), retAllow()},
{call("lseek", 0x0100, 0x0100, 0x0101), retKill()},
{call("lseek", 0x0200, 0x0110, 0x0101), retAllow()},
{call("lseek", 0x0400, 0x0110, 0x0110), retKill()},
{call("lseek", 0x0800, 0x0110, 0x0007), retAllow()},
{call("lseek", 0x0100000000000000, 0x0100000000000000, 0x0101000000000000), retKill()},
{call("lseek", 0x0200000000000000, 0x0110000000000000, 0x0101000000000000), retAllow()},
{call("lseek", 0x0400000000000000, 0x0110000000000000, 0x0110000000000000), retKill()},
{call("lseek", 0x0800000000000000, 0x0110000000000000, 0x0007000000000000), retAllow()},
{call("stat", 0x0123456789abcdef, 0x123456789abcdef0, 0x00f000f000000000), retAllow()},
{call("stat", 0x0123456789abcdef, 0x123456789abcdef0, 0x007000f000000000), retKill()},
{call("stat", 0x0133456789abcdef, 0x123457789abcdef0, 0x007000f000000000), retAllow()},
{call("stat", 0x0133456789abcdef, 0x123456789abcdef0, 0x007000f000000000), retKill()},
{call("fstat", 0x0123456789abcdef, 0x123456789abcdef0, 0x00f000f000000000), retKill()},
{call("fstat", 0x0133456789abcdef, 0x123456789abcdef0, 0x00f000f000000000), retAllow()},
{call("fstat", 0x0123456789abcdef, 0x123457789abcdef0, 0x00f000f000000000), retAllow()},
{call("fstat", 0x0123456789abcdef, 0x123457789abcdef0, 0x007000f000000000), retKill()},
}
ps, err := parseLines(file)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
bpf, err := compile(ps, true, bpfRet(retKill()))
if err != nil {
t.Fatalf("compile failed: %v", err)
}
t.Logf("len(bpf) = %d", len(bpf))
for _, test := range tests {
if got := eval(bpf, test.data); got != test.want {
t.Errorf("%s%#x = %#08x; want %#08x", syscallName[test.data.NR], test.data.Args, got, test.want)
}
}
}
func TestSupport(t *testing.T) {
if err := CheckSupport(); err != nil {
t.Error(err)
}
}
func TestInstall(t *testing.T) {
if !*testInstall {
t.Skip("use -test_install (with -cpu=N) to enable.")
}
file := []string{
"# open/openat: return EPERM",
"open: return 1",
"openat: return 1",
"# default: ALLOW",
}
ps, err := parseLines(file)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
bpf, err := compile(ps, nbits > 32, bpfRet(retAllow()))
if err != nil {
t.Fatalf("compile failed: %v", err)
}
if err = Install(bpf); err != nil {
t.Fatalf("install failed: %v", err)
}
N := runtime.GOMAXPROCS(0)
opened := make(chan bool)
for i := 0; i < N; i++ {
go func() {
if f, err := os.Open("/dev/null"); err != nil {
t.Logf("open() failed: %v", err)
opened <- false
} else {
t.Logf("open() succeeded")
f.Close()
opened <- true
}
}()
}
for i := 0; i < N; i++ {
if <-opened {
t.Fail()
}
}
}
func TestEndian(t *testing.T) {
if !*testEndian {
t.Skip("use -test_endian to enable.")
}
pass := syscall.EDOM
fail := syscall.ERANGE
name := map[error]string{
pass: "pass",
fail: "fail",
nil: "<nil>",
}
type seccomp [3]uintptr
ps := []policy{
{
name: "seccomp",
expr: orExpr{
andExpr{
argComp{0, "==", 0x0123456789abcdef},
},
andExpr{
argComp{1, "!=", 0x01234567},
argComp{2, "&", 0x01010101},
},
},
then: bpfRet(retErrno(pass)),
def: ptr(bpfRet(retErrno(fail))),
},
}
tests := []struct {
args seccomp
want error
}{
{seccomp{0x01234567, 0, 0}, fail},
{seccomp{0x89abcdef, 0, 0}, pass},
{seccomp{0, 0x01234567, 0}, fail},
{seccomp{0, 0x12345678, 0}, fail},
{seccomp{0, 0x01234567, 1}, fail},
{seccomp{0, 0x12345678, 1}, pass},
}
call := func(args seccomp) error {
if nr, ok := syscallNum["seccomp"]; ok {
if _, _, e := syscall.Syscall(uintptr(nr), args[0], args[1], args[2]); e != 0 {
return e
}
}
return nil
}
bpf, err := compile(ps, false, bpfRet(retAllow()))
if err != nil {
t.Fatalf("compile failed: %v", err)
}
if err = Install(bpf); err != nil {
t.Fatalf("install failed: %v", err)
}
for _, test := range tests {
if got := call(test.args); got != test.want {
t.Errorf("seccomp%#x = %v; want %v", test.args, name[got], name[test.want])
}
}
}