mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
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:
committed by
gVisor bot
parent
5c41509ff4
commit
201a046299
@@ -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.
|
||||
##
|
||||
|
||||
@@ -9,6 +9,7 @@ go_library(
|
||||
name = "bpf",
|
||||
srcs = [
|
||||
"bpf.go",
|
||||
"bpf_unsafe.go",
|
||||
"decoder.go",
|
||||
"input_bytes.go",
|
||||
"interpreter.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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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, ¤tFuzzee)
|
||||
if err != nil {
|
||||
f.Fatalf("failed to create diff fuzzer: %v", err)
|
||||
}
|
||||
df.DeriveCorpusFromRuleSets(ruleSets)
|
||||
df.Fuzz()
|
||||
}
|
||||
Reference in New Issue
Block a user