diff --git a/Makefile b/Makefile index cceee7275..5deddabbc 100644 --- a/Makefile +++ b/Makefile @@ -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. ## diff --git a/pkg/bpf/BUILD b/pkg/bpf/BUILD index 51b52752a..53862b80f 100644 --- a/pkg/bpf/BUILD +++ b/pkg/bpf/BUILD @@ -9,6 +9,7 @@ go_library( name = "bpf", srcs = [ "bpf.go", + "bpf_unsafe.go", "decoder.go", "input_bytes.go", "interpreter.go", diff --git a/pkg/bpf/bpf_unsafe.go b/pkg/bpf/bpf_unsafe.go new file mode 100644 index 000000000..418fe218c --- /dev/null +++ b/pkg/bpf/bpf_unsafe.go @@ -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 +} diff --git a/pkg/seccomp/seccomp.go b/pkg/seccomp/seccomp.go index 8b337f8ee..70d2571a0 100644 --- a/pkg/seccomp/seccomp.go +++ b/pkg/seccomp/seccomp.go @@ -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)) } diff --git a/runsc/boot/filter/BUILD b/runsc/boot/filter/BUILD index 194e20493..98cefc9e0 100644 --- a/runsc/boot/filter/BUILD +++ b/runsc/boot/filter/BUILD @@ -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", ], ) diff --git a/runsc/boot/filter/dumpfilter/dumpfilter.go b/runsc/boot/filter/dumpfilter/dumpfilter.go index 3c1459ee0..d15693dba 100644 --- a/runsc/boot/filter/dumpfilter/dumpfilter.go +++ b/runsc/boot/filter/dumpfilter/dumpfilter.go @@ -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) } diff --git a/runsc/boot/filter/filter_fuzz_golden.bpf b/runsc/boot/filter/filter_fuzz_golden.bpf new file mode 100644 index 000000000..488198f83 Binary files /dev/null and b/runsc/boot/filter/filter_fuzz_golden.bpf differ diff --git a/runsc/boot/filter/filter_fuzz_golden_test.go b/runsc/boot/filter/filter_fuzz_golden_test.go new file mode 100644 index 000000000..e9eab5c63 --- /dev/null +++ b/runsc/boot/filter/filter_fuzz_golden_test.go @@ -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, ¤tFuzzee) + if err != nil { + f.Fatalf("failed to create diff fuzzer: %v", err) + } + df.DeriveCorpusFromRuleSets(ruleSets) + df.Fuzz() +}