From 679fd58e487f9616422799488fd8e71f2b7c7963 Mon Sep 17 00:00:00 2001 From: Etienne Perot Date: Fri, 2 Feb 2024 16:37:49 -0800 Subject: [PATCH] `secfuzz`: Add test that checks consistency of behavior after optimizations. PiperOrigin-RevId: 603818920 --- runsc/boot/filter/filter_fuzz_test.go | 15 ++++ test/secfuzz/secfuzz.go | 114 +++++++++++++++++++++++++- 2 files changed, 125 insertions(+), 4 deletions(-) diff --git a/runsc/boot/filter/filter_fuzz_test.go b/runsc/boot/filter/filter_fuzz_test.go index 8d5f23ee7..4ff8be24f 100644 --- a/runsc/boot/filter/filter_fuzz_test.go +++ b/runsc/boot/filter/filter_fuzz_test.go @@ -27,6 +27,21 @@ import ( // FuzzFilterOptimizationsResultInConsistentProgram tests that optimizations // do not affect the behavior of the generated seccomp-bpf program. func FuzzFilterOptimizationsResultInConsistentProgram(f *testing.F) { + fuzzFilterOptimizationsResultInConsistentProgram(f) +} + +// TestFilterOptimizationsResultInConsistentProgram tests that optimizations +// do not affect the behavior of the generated seccomp-bpf program, as a unit +// test. Unlike the fuzz-based test, it only operates on a static corpus. +// Still, it does enforce full branch coverage, so can be used as a quicker +// way to verify functionality without running a fuzz test for an interminate +// amount of time. +func TestFilterOptimizationsResultInConsistentProgram(t *testing.T) { + fuzzFilterOptimizationsResultInConsistentProgram(&secfuzz.StaticCorpus{T: t}) +} + +func fuzzFilterOptimizationsResultInConsistentProgram(f secfuzz.FuzzLike) { + f.Helper() filterOpts := config.Options{ Platform: (&systrap.Systrap{}).SeccompInfo(), } diff --git a/test/secfuzz/secfuzz.go b/test/secfuzz/secfuzz.go index 76e552644..c6ba38ec8 100644 --- a/test/secfuzz/secfuzz.go +++ b/test/secfuzz/secfuzz.go @@ -44,10 +44,116 @@ type Fuzzee struct { coverage [bpf.MaxInstructions]atomicbitops.Bool } +// FuzzLike represents a fuzzer. +// It is the subset of `testing.F` that secfuzz uses. +type FuzzLike interface { + Helper() + Add(seed ...any) + Errorf(message string, values ...any) + Fatalf(message string, values ...any) + Logf(message string, values ...any) + Fuzz(fn any) +} + +// Verify that `testing.F` implements `FuzzLike`. +var _ FuzzLike = (*testing.F)(nil) + +// StaticCorpus allows a unit test to use secfuzz by using a static corpus. +// This allows checking for coverage and consistency between programs, +// but no new inputs beyond those explicitly added will be tested. +type StaticCorpus struct { + T *testing.T + corpus []linux.SeccompData +} + +// Helper implements `FuzzLike.Helper`. +func (s *StaticCorpus) Helper() { + s.T.Helper() +} + +// unpack64Bits unpacks two 32-bit values into one unsigned 64-bit integer. +func unpack64Bits(high, low any) uint64 { + return uint64(high.(uint32))<<32 | uint64(low.(uint32)) +} + +// Add implements `FuzzLike.Add`. +func (s *StaticCorpus) Add(seed ...any) { + if len(seed) != 16 { + s.T.Fatalf("seed must have 16 components, got %d", len(seed)) + } + s.corpus = append(s.corpus, linux.SeccompData{ + Nr: seed[0].(int32), + Arch: seed[1].(uint32), + Args: [6]uint64{ + unpack64Bits(seed[2], seed[3]), + unpack64Bits(seed[4], seed[5]), + unpack64Bits(seed[6], seed[7]), + unpack64Bits(seed[8], seed[9]), + unpack64Bits(seed[10], seed[11]), + unpack64Bits(seed[12], seed[13]), + }, + InstructionPointer: unpack64Bits(seed[14], seed[15]), + }) +} + +// Errorf implements `FuzzLike.Errorf`. +func (s *StaticCorpus) Errorf(message string, values ...any) { + s.T.Helper() + s.T.Errorf(message, values...) +} + +// Fatalf implements `FuzzLike.Fatalf`. +func (s *StaticCorpus) Fatalf(message string, values ...any) { + s.T.Helper() + s.T.Fatalf(message, values...) +} + +// Logf implements `FuzzLike.Logf`. +func (s *StaticCorpus) Logf(message string, values ...any) { + s.T.Helper() + s.T.Logf(message, values...) +} + +// Fuzz implements `FuzzLike.Fuzz`. +func (s *StaticCorpus) Fuzz(fn any) { + s.T.Helper() + if len(s.corpus) == 0 { + s.T.Error("corpus is empty") + } + seccompFn := fn.(func( + t *testing.T, + nr int32, + arch uint32, + arg0High, arg0Low uint32, + arg1High, arg1Low uint32, + arg2High, arg2Low uint32, + arg3High, arg3Low uint32, + arg4High, arg4Low uint32, + arg5High, arg5Low uint32, + ripHigh, ripLow uint32)) + for _, scData := range s.corpus { + seccompFn( + s.T, + int32(scData.Nr), + uint32(scData.Arch), + uint32(scData.Args[0]>>32), uint32(scData.Args[0]), // arg0 + uint32(scData.Args[1]>>32), uint32(scData.Args[1]), // arg1 + uint32(scData.Args[2]>>32), uint32(scData.Args[2]), // arg2 + uint32(scData.Args[3]>>32), uint32(scData.Args[3]), // arg3 + uint32(scData.Args[4]>>32), uint32(scData.Args[4]), // arg4 + uint32(scData.Args[5]>>32), uint32(scData.Args[5]), // arg5 + uint32(scData.InstructionPointer>>32), uint32(scData.InstructionPointer), // rip + ) + } +} + +// Verify that StaticCorpus implements `FuzzLike`. +var _ FuzzLike = (*StaticCorpus)(nil) + // DiffFuzzer fuzzes two seccomp programs. type DiffFuzzer struct { // f is the Go fuzzer to use. - f *testing.F + f FuzzLike // The two programs being differentially fuzzed. fuzzee1, fuzzee2 *Fuzzee @@ -152,7 +258,7 @@ func (df *DiffFuzzer) DeriveCorpusFromRuleSets(ruleSets []seccomp.RuleSet) { // NewDiffFuzzer creates a fuzzer that verifies that two seccomp-bpf programs // are equivalent by fuzzing both of them with the same inputs and checking // that they output the same result. -func NewDiffFuzzer(f *testing.F, fuzzee1, fuzzee2 *Fuzzee) (*DiffFuzzer, error) { +func NewDiffFuzzer(f FuzzLike, fuzzee1, fuzzee2 *Fuzzee) (*DiffFuzzer, error) { f.Helper() if len(fuzzee1.Instructions) > bpf.MaxInstructions { return nil, fmt.Errorf("program %s has %d instructions, which exceeds the maximum of %d", fuzzee1.String(), len(fuzzee1.Instructions), bpf.MaxInstructions) @@ -263,7 +369,7 @@ func (df *DiffFuzzer) Fuzz() { df.f.Errorf("[NOT COVERED] % 4d: %s", pc, ins.String()) } } - df.f.Error("\n") + df.f.Errorf("\n") } else { df.f.Logf("Program %s not fully covered (but coverage not enforced).", df.fuzzee1.String()) } @@ -278,7 +384,7 @@ func (df *DiffFuzzer) Fuzz() { df.f.Errorf("[NOT COVERED] % 4d: %s", pc, ins.String()) } } - df.f.Error("\n") + df.f.Errorf("\n") } else { df.f.Logf("Program %s not fully covered (but coverage not enforced).", df.fuzzee2.String()) }