seccomp: Enforce that Sentry filters match against reference program.

This change adds a `filter_fuzz_golden.bpf` BPF program that was generated
manually prior to my recent set of changes to seccomp bytecode and rule
optimization changes. It represents the "reference logic"; the new test
verifies that the current seccomp-bpf library produces BPF bytecode that
has the same behavior, using fuzz testing with full line-based coverage.

PiperOrigin-RevId: 582914572
This commit is contained in:
Etienne Perot
2023-11-15 22:38:00 -08:00
committed by gVisor bot
parent 5c41509ff4
commit 201a046299
8 changed files with 193 additions and 10 deletions
+5
View File
@@ -458,6 +458,11 @@ run-benchmark: load-benchmarks ## Runs single benchmark and optionally sends dat
@$(call run_benchmark,$(RUNTIME))
.PHONY: run-benchmark
## Seccomp targets.
seccomp-sentry-filters: # Dumps seccomp-bpf program for the Sentry binary.
@$(call run,//runsc/boot/filter/dumpfilter,$(ARGS))
.PHONY: seccomp-sentry-filters
##
## Website & documentation helpers.
##
+1
View File
@@ -9,6 +9,7 @@ go_library(
name = "bpf",
srcs = [
"bpf.go",
"bpf_unsafe.go",
"decoder.go",
"input_bytes.go",
"interpreter.go",
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2023 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 bpf
import (
"fmt"
"unsafe"
)
// ParseBytecode converts raw BPF bytecode into BPF instructions.
// It verifies that the resulting set of instructions is a valid program.
func ParseBytecode(bytecode []byte) ([]Instruction, error) {
sizeOfInstruction := int(unsafe.Sizeof(Instruction{}))
if len(bytecode)%sizeOfInstruction != 0 {
return nil, fmt.Errorf("bytecode size (%d bytes) is not a multiple of BPF instruction size of %d bytes", len(bytecode), sizeOfInstruction)
}
insns := ([]Instruction)(unsafe.Slice((*Instruction)(unsafe.Pointer(&bytecode[0])), len(bytecode)/sizeOfInstruction))
if _, err := Compile(insns, false); err != nil {
return nil, fmt.Errorf("not a valid BPF program: %v", err)
}
return insns, nil
}
+4 -2
View File
@@ -92,7 +92,9 @@ func Install(rules SyscallRules, denyRules SyscallRules, options ProgramOptions)
return nil
}
func defaultAction() (linux.BPFAction, error) {
// DefaultAction returns a sane default for a failure to match
// a seccomp-bpf filter. Either kill the process, or trap.
func DefaultAction() (linux.BPFAction, error) {
available, err := isKillProcessAvailable()
if err != nil {
return 0, err
@@ -325,7 +327,7 @@ type ProgramOptions struct {
// DefaultProgramOptions returns the default program options.
func DefaultProgramOptions() ProgramOptions {
action, err := defaultAction()
action, err := DefaultAction()
if err != nil {
panic(fmt.Sprintf("cannot determine default seccomp action: %v", err))
}
+8 -1
View File
@@ -67,12 +67,19 @@ secbench_test(
go_test(
name = "filter_fuzz_test",
srcs = ["filter_fuzz_test.go"],
srcs = [
"filter_fuzz_golden_test.go",
"filter_fuzz_test.go",
],
data = ["filter_fuzz_golden.bpf"],
deps = [
":filter",
"//pkg/abi/linux",
"//pkg/bpf",
"//pkg/seccomp",
"//pkg/sentry/platform/systrap",
"//pkg/test/testutil",
"//test/secfuzz",
"@org_golang_x_sys//unix:go_default_library",
],
)
+43 -7
View File
@@ -30,10 +30,36 @@ import (
// Flags.
var (
output = flag.String("output", "fancy", "Output type: 'fancy' (human-readable with line numbers resolved), 'plain' (diffable but still human-readable output), 'bytecode' (dump raw bytecode)")
nvproxy = flag.Bool("nvproxy", false, "Enable nvproxy in filter configuration")
output = flag.String("output", "fancy", "Output type: 'fancy' (human-readable with line numbers resolved), 'plain' (diffable but still human-readable output), 'bytecode' (dump raw bytecode)")
nvproxy = flag.Bool("nvproxy", false, "Enable nvproxy in filter configuration")
denyAction = flag.String("deny-action", "default", "What to do if the syscall matches the 'deny' ruleset (one of: errno, kill_process, kill_thread)")
defaultAction = flag.String("default-action", "default", "What to do if all the syscall rules fail to match (one of: errno, kill_process, kill_thread)")
badArchAction = flag.String("bad-arch-action", "default", "What to do if all the architecture field mismatches (one of: errno, kill_process, kill_thread)")
out = flag.String("out", "/dev/stdout", "Where to write the filter output (defaults to standard output)")
)
func action(s string) linux.BPFAction {
switch s {
case "default":
def, err := seccomp.DefaultAction()
if err != nil {
log.Warningf("cannot determine default seccomp action: %v", err)
os.Exit(1)
}
return def
case "errno":
return linux.SECCOMP_RET_ERRNO
case "kill_process":
return linux.SECCOMP_RET_KILL_PROCESS
case "kill_thread":
return linux.SECCOMP_RET_KILL_THREAD
default:
log.Warningf("invalid action %q (want one of: errno, kill_process, kill_thread)", s)
os.Exit(1)
panic("unreachable")
}
}
func main() {
flag.Parse()
opt := filter.Options{
@@ -41,16 +67,20 @@ func main() {
NVProxy: *nvproxy,
}
rules, denyRules := filter.Rules(opt)
seccompOpts := filter.SeccompOptions(opt)
seccompOpts.DefaultAction = action(*defaultAction)
seccompOpts.BadArchAction = action(*badArchAction)
insns, stats, err := seccomp.BuildProgram([]seccomp.RuleSet{
{
Rules: denyRules,
Action: linux.SECCOMP_RET_ERRNO,
Action: action(*denyAction),
},
{
Rules: rules,
Action: linux.SECCOMP_RET_ALLOW,
},
}, filter.SeccompOptions(opt))
}, seccompOpts)
if err != nil {
log.Warningf("%v", err)
os.Exit(1)
@@ -61,6 +91,12 @@ func main() {
log.Infof("Rule optimization passes duration: %v", stats.RuleOptimizeDuration)
log.Infof("BPF optimization passes duration: %v", stats.BPFOptimizeDuration)
log.Infof("Total duration: %v", stats.BuildDuration+stats.RuleOptimizeDuration+stats.BPFOptimizeDuration)
outFile, err := os.Create(*out)
if err != nil {
log.Warningf("cannot open output file %q: %v", *out, err)
os.Exit(1)
}
defer outFile.Close()
switch *output {
case "fancy":
dump, err := bpf.DecodeInstructions(insns)
@@ -68,13 +104,13 @@ func main() {
log.Warningf("%v", err)
os.Exit(1)
}
fmt.Print(dump)
fmt.Fprint(outFile, dump)
case "plain":
for _, ins := range insns {
fmt.Println(ins.String())
fmt.Fprint(outFile, ins.String())
}
case "bytecode":
if _, err := os.Stdout.WriteString(InstructionsToBytecode(insns)); err != nil {
if _, err := outFile.WriteString(InstructionsToBytecode(insns)); err != nil {
log.Warningf("cannot write bytecode to stdout: %v", err)
os.Exit(1)
}
Binary file not shown.
@@ -0,0 +1,98 @@
// Copyright 2023 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.
//go:build !false
// +build !false
package filter_fuzz_test
import (
"os"
"testing"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/bpf"
"gvisor.dev/gvisor/pkg/seccomp"
"gvisor.dev/gvisor/pkg/sentry/platform/systrap"
"gvisor.dev/gvisor/pkg/test/testutil"
"gvisor.dev/gvisor/runsc/boot/filter"
"gvisor.dev/gvisor/test/secfuzz"
)
// FuzzFilterAgainstGolden tests that the behavior of the generated
// seccomp-bpf program has not changed.
// This is useful when modifying the way that the seccomp-bpf program
// is built, not when modifying what rules the program is meant to enforce.
// If you are modifying the seccomp-bpf rules in such a way that you
// are expecting the set of allowed/disallowed syscalls to change,
// you can update the reference program using:
//
// $ make seccomp-sentry-filters ARGS='--deny-action=errno --default-action=kill_thread --bad-arch-action=kill_process --output=bytecode --out=runsc/boot/filter/filter_fuzz_golden.bpf'
func FuzzFilterAgainstGolden(f *testing.F) {
goldenProgPath, err := testutil.FindFile("runsc/boot/filter/filter_fuzz_golden.bpf")
if err != nil {
f.Fatalf("failed to find golden program: %v", err)
}
goldenProg, err := os.ReadFile(goldenProgPath)
if err != nil {
f.Fatalf("failed to read golden program: %v", err)
}
goldenInstructions, err := bpf.ParseBytecode(goldenProg)
if err != nil {
f.Fatalf("failed to parse golden program bytecode: %v", err)
}
goldenFuzzee := secfuzz.Fuzzee{
Name: "golden",
Instructions: goldenInstructions,
// TODO(b/298726675): Enforce full coverage with the optimized program
// once confident that it works well.
// This will ensure that the generated fuzz corpus is sufficient to
// fully exhaust the golden program.
EnforceFullCoverage: false,
}
filterOpts := filter.Options{
Platform: &systrap.Systrap{},
}
rules, denyRules := filter.Rules(filterOpts)
ruleSets := []seccomp.RuleSet{
{
Rules: denyRules,
Action: linux.SECCOMP_RET_ERRNO,
},
{
Rules: rules,
Action: linux.SECCOMP_RET_ALLOW,
},
}
opts := filter.SeccompOptions(filterOpts)
// We use unique actions here to be able to tell them apart.
opts.DefaultAction = linux.SECCOMP_RET_KILL_THREAD
opts.BadArchAction = linux.SECCOMP_RET_KILL_PROCESS
current, _, err := seccomp.BuildProgram(ruleSets, opts)
if err != nil {
f.Fatalf("failed to build seccomp-bpf program: %v", err)
}
currentFuzzee := secfuzz.Fuzzee{
Name: "current",
Instructions: current,
EnforceFullCoverage: true,
}
df, err := secfuzz.NewDiffFuzzer(f, &goldenFuzzee, &currentFuzzee)
if err != nil {
f.Fatalf("failed to create diff fuzzer: %v", err)
}
df.DeriveCorpusFromRuleSets(ruleSets)
df.Fuzz()
}