1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
|
package bpf
import (
"sort"
"testing"
"golang.org/x/sys/unix"
)
// systems exercised by the property/fuzz tests. All target ABIs are covered
// because the interpreter is arch-agnostic (it just reads seccomp_data.arch).
var testSystems = []string{
"x86_64-linux",
"aarch64-linux",
"mips64-linux",
"mips64el-linux",
}
func compileOrFatal(t *testing.T, system string) []unix.SockFilter {
t.Helper()
prog, err := Compile(system)
if err != nil {
t.Fatalf("Compile(%q): %v", system, err)
}
return prog
}
// archRules returns this arch profile's resolved (nr, rule) entries, sorted and
// de-duplicated exactly as the compiler does.
func archRules(profile archProfile) []resolvedRule {
table := syscallTables[profile.name]
var rules []resolvedRule
for _, r := range lixRules {
if nr, ok := table[r.name]; ok {
rules = append(rules, resolvedRule{nr: nr, rule: r})
}
}
sort.SliceStable(rules, func(a, b int) bool { return rules[a].nr < rules[b].nr })
return dedupByNr(rules)
}
// TestAllowlistAllowed verifies every allowlisted syscall returns RET_ALLOW.
func TestAllowlistAllowed(t *testing.T) {
for _, system := range testSystems {
prog := compileOrFatal(t, system)
for _, ap := range targetArches[system] {
for _, rr := range archRules(ap) {
if rr.rule.kind != kindAllow {
continue
}
got, err := Run(prog, SeccompData{Arch: ap.auditArch, Nr: rr.nr})
if err != nil {
t.Fatalf("%s/%s nr=%d: Run: %v", system, ap.name, rr.nr, err)
}
if got != RetAllow {
t.Errorf("%s/%s %s (nr=%d): got 0x%08x, want RET_ALLOW", system, ap.name, rr.rule.name, rr.nr, got)
}
}
}
}
}
// TestSetxattrDenied verifies the setxattr family returns ENOTSUP.
func TestSetxattrDenied(t *testing.T) {
for _, system := range testSystems {
prog := compileOrFatal(t, system)
for _, ap := range targetArches[system] {
table := syscallTables[ap.name]
for _, name := range []string{"setxattr", "lsetxattr", "fsetxattr"} {
nr, ok := table[name]
if !ok {
continue
}
got, err := Run(prog, SeccompData{Arch: ap.auditArch, Nr: nr})
if err != nil {
t.Fatalf("%s/%s %s: Run: %v", system, ap.name, name, err)
}
if want := Errno(errENOTSUP); got != want {
t.Errorf("%s/%s %s: got 0x%08x, want ENOTSUP 0x%08x", system, ap.name, name, got, want)
}
}
}
}
}
// TestChmodArgCheck verifies chmod-family setuid/setgid handling.
func TestChmodArgCheck(t *testing.T) {
for _, system := range testSystems {
prog := compileOrFatal(t, system)
for _, ap := range targetArches[system] {
table := syscallTables[ap.name]
for _, tc := range []struct {
name string
argIdx int
}{
{"chmod", 1}, {"fchmod", 1}, {"fchmodat", 2}, {"fchmodat2", 2},
} {
nr, ok := table[tc.name]
if !ok {
continue
}
// Safe mode 0644 -> allow.
var safe [6]uint64
safe[tc.argIdx] = 0o644
if got, _ := Run(prog, SeccompData{Arch: ap.auditArch, Nr: nr, Args: safe}); got != RetAllow {
t.Errorf("%s/%s %s(0644): got 0x%08x, want ALLOW", system, ap.name, tc.name, got)
}
// setuid mode 04755 -> EPERM.
var suid [6]uint64
suid[tc.argIdx] = 0o4755
if got, _ := Run(prog, SeccompData{Arch: ap.auditArch, Nr: nr, Args: suid}); got != Errno(errEPERM) {
t.Errorf("%s/%s %s(04755): got 0x%08x, want EPERM", system, ap.name, tc.name, got)
}
// setgid mode 02755 -> EPERM.
var sgid [6]uint64
sgid[tc.argIdx] = 0o2755
if got, _ := Run(prog, SeccompData{Arch: ap.auditArch, Nr: nr, Args: sgid}); got != Errno(errEPERM) {
t.Errorf("%s/%s %s(02755): got 0x%08x, want EPERM", system, ap.name, tc.name, got)
}
}
}
}
}
// TestUnknownSyscallDenied verifies an unallocated nr returns ENOSYS.
func TestUnknownSyscallDenied(t *testing.T) {
for _, system := range testSystems {
prog := compileOrFatal(t, system)
for _, ap := range targetArches[system] {
// A very large nr that is not in any table.
got, err := Run(prog, SeccompData{Arch: ap.auditArch, Nr: 0x7ffffffe})
if err != nil {
t.Fatalf("%s/%s: Run: %v", system, ap.name, err)
}
if want := Errno(errENOSYS); got != want {
t.Errorf("%s/%s unknown nr: got 0x%08x, want ENOSYS", system, ap.name, got)
}
}
}
}
// TestUnknownArchKilled verifies a non-target arch is killed.
func TestUnknownArchKilled(t *testing.T) {
for _, system := range testSystems {
prog := compileOrFatal(t, system)
got, err := Run(prog, SeccompData{Arch: 0xdeadbeef, Nr: 0})
if err != nil {
t.Fatalf("%s: Run: %v", system, err)
}
if got != RetKillProcess {
t.Errorf("%s unknown arch: got 0x%08x, want KILL_PROCESS", system, got)
}
}
}
// TestDifferentialAllNumbers exhaustively checks every nr in a wide range
// against the reference evaluator for each target arch.
func TestDifferentialAllNumbers(t *testing.T) {
for _, system := range testSystems {
prog := compileOrFatal(t, system)
for _, ap := range targetArches[system] {
// Cover 0..maxNr+padding so both in-table and out-of-table numbers
// are exercised. x32 numbers carry the high bit, so test the masked
// range too.
maxNr := uint32(0)
for _, nr := range syscallTables[ap.name] {
if nr > maxNr {
maxNr = nr
}
}
_ = maxNr
rules := archRules(ap)
// Test each in-table number plus a non-member neighbour.
for _, rr := range rules {
for _, nr := range []uint32{rr.nr, rr.nr + 1} {
var args [6]uint64
want := referenceAction(system, ap.auditArch, nr, args)
got, err := Run(prog, SeccompData{Arch: ap.auditArch, Nr: nr, Args: args})
if err != nil {
t.Fatalf("%s/%s nr=%d: Run: %v", system, ap.name, nr, err)
}
if got != want {
t.Errorf("%s/%s nr=%d: got 0x%08x want 0x%08x", system, ap.name, nr, got, want)
}
}
}
}
}
}
|