gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/test/secfuzz/secfuzz.go (about)

     1  // Copyright 2023 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package secfuzz allows fuzz-based testing of seccomp-bpf programs.
    16  package secfuzz
    17  
    18  import (
    19  	"fmt"
    20  	"testing"
    21  
    22  	"gvisor.dev/gvisor/pkg/abi/linux"
    23  	"gvisor.dev/gvisor/pkg/abi/sentry"
    24  	"gvisor.dev/gvisor/pkg/atomicbitops"
    25  	"gvisor.dev/gvisor/pkg/bpf"
    26  	"gvisor.dev/gvisor/pkg/seccomp"
    27  	"gvisor.dev/gvisor/pkg/sync"
    28  )
    29  
    30  // Fuzzee wraps a program for the purpose of fuzzing.
    31  type Fuzzee struct {
    32  	// Name is a human-friendly name for the program.
    33  	Name string
    34  
    35  	// If `EnforceFullCoverage` is set, the fuzz test will
    36  	// fail if any instruction in the program is not covered.
    37  	// The caller must ensure that the seed corpus is sufficient
    38  	// to fully cover the program.
    39  	EnforceFullCoverage bool
    40  
    41  	// Instructions is the set of instructions in the program.
    42  	Instructions []bpf.Instruction
    43  
    44  	coverage [bpf.MaxInstructions]atomicbitops.Bool
    45  }
    46  
    47  // FuzzLike represents a fuzzer.
    48  // It is the subset of `testing.F` that secfuzz uses.
    49  type FuzzLike interface {
    50  	Helper()
    51  	Add(seed ...any)
    52  	Errorf(message string, values ...any)
    53  	Fatalf(message string, values ...any)
    54  	Logf(message string, values ...any)
    55  	Fuzz(fn any)
    56  }
    57  
    58  // Verify that `testing.F` implements `FuzzLike`.
    59  var _ FuzzLike = (*testing.F)(nil)
    60  
    61  // StaticCorpus allows a unit test to use secfuzz by using a static corpus.
    62  // This allows checking for coverage and consistency between programs,
    63  // but no new inputs beyond those explicitly added will be tested.
    64  type StaticCorpus struct {
    65  	T      *testing.T
    66  	corpus []linux.SeccompData
    67  }
    68  
    69  // Helper implements `FuzzLike.Helper`.
    70  func (s *StaticCorpus) Helper() {
    71  	s.T.Helper()
    72  }
    73  
    74  // unpack64Bits unpacks two 32-bit values into one unsigned 64-bit integer.
    75  func unpack64Bits(high, low any) uint64 {
    76  	return uint64(high.(uint32))<<32 | uint64(low.(uint32))
    77  }
    78  
    79  // Add implements `FuzzLike.Add`.
    80  func (s *StaticCorpus) Add(seed ...any) {
    81  	if len(seed) != 16 {
    82  		s.T.Fatalf("seed must have 16 components, got %d", len(seed))
    83  	}
    84  	s.corpus = append(s.corpus, linux.SeccompData{
    85  		Nr:   seed[0].(int32),
    86  		Arch: seed[1].(uint32),
    87  		Args: [6]uint64{
    88  			unpack64Bits(seed[2], seed[3]),
    89  			unpack64Bits(seed[4], seed[5]),
    90  			unpack64Bits(seed[6], seed[7]),
    91  			unpack64Bits(seed[8], seed[9]),
    92  			unpack64Bits(seed[10], seed[11]),
    93  			unpack64Bits(seed[12], seed[13]),
    94  		},
    95  		InstructionPointer: unpack64Bits(seed[14], seed[15]),
    96  	})
    97  }
    98  
    99  // Errorf implements `FuzzLike.Errorf`.
   100  func (s *StaticCorpus) Errorf(message string, values ...any) {
   101  	s.T.Helper()
   102  	s.T.Errorf(message, values...)
   103  }
   104  
   105  // Fatalf implements `FuzzLike.Fatalf`.
   106  func (s *StaticCorpus) Fatalf(message string, values ...any) {
   107  	s.T.Helper()
   108  	s.T.Fatalf(message, values...)
   109  }
   110  
   111  // Logf implements `FuzzLike.Logf`.
   112  func (s *StaticCorpus) Logf(message string, values ...any) {
   113  	s.T.Helper()
   114  	s.T.Logf(message, values...)
   115  }
   116  
   117  // Fuzz implements `FuzzLike.Fuzz`.
   118  func (s *StaticCorpus) Fuzz(fn any) {
   119  	s.T.Helper()
   120  	if len(s.corpus) == 0 {
   121  		s.T.Error("corpus is empty")
   122  	}
   123  	seccompFn := fn.(func(
   124  		t *testing.T,
   125  		nr int32,
   126  		arch uint32,
   127  		arg0High, arg0Low uint32,
   128  		arg1High, arg1Low uint32,
   129  		arg2High, arg2Low uint32,
   130  		arg3High, arg3Low uint32,
   131  		arg4High, arg4Low uint32,
   132  		arg5High, arg5Low uint32,
   133  		ripHigh, ripLow uint32))
   134  	for _, scData := range s.corpus {
   135  		seccompFn(
   136  			s.T,
   137  			int32(scData.Nr),
   138  			uint32(scData.Arch),
   139  			uint32(scData.Args[0]>>32), uint32(scData.Args[0]), // arg0
   140  			uint32(scData.Args[1]>>32), uint32(scData.Args[1]), // arg1
   141  			uint32(scData.Args[2]>>32), uint32(scData.Args[2]), // arg2
   142  			uint32(scData.Args[3]>>32), uint32(scData.Args[3]), // arg3
   143  			uint32(scData.Args[4]>>32), uint32(scData.Args[4]), // arg4
   144  			uint32(scData.Args[5]>>32), uint32(scData.Args[5]), // arg5
   145  			uint32(scData.InstructionPointer>>32), uint32(scData.InstructionPointer), // rip
   146  		)
   147  	}
   148  }
   149  
   150  // Verify that StaticCorpus implements `FuzzLike`.
   151  var _ FuzzLike = (*StaticCorpus)(nil)
   152  
   153  // DiffFuzzer fuzzes two seccomp programs.
   154  type DiffFuzzer struct {
   155  	// f is the Go fuzzer to use.
   156  	f FuzzLike
   157  
   158  	// The two programs being differentially fuzzed.
   159  	fuzzee1, fuzzee2 *Fuzzee
   160  
   161  	compiled1, compiled2 bpf.Program
   162  }
   163  
   164  // String returns the program's name.
   165  func (f *Fuzzee) String() string {
   166  	return f.Name
   167  }
   168  
   169  // AddSeed adds the given syscall data to the fuzzer's seed corpus.
   170  func (df *DiffFuzzer) AddSeed(scData linux.SeccompData) {
   171  	df.f.Helper()
   172  
   173  	// We represent the syscall arguments as two uint32s so that the fuzzer
   174  	// can more easily notice that changing each half produces different
   175  	// coverage. This is due to the fact that BPF only supports 32-bit
   176  	// arithmetic, so it has to separately compare each 32-bit half of the
   177  	// 64-bit numbers.
   178  	df.f.Add(
   179  		int32(scData.Nr),
   180  		uint32(scData.Arch),
   181  		uint32(scData.Args[0]>>32), uint32(scData.Args[0]), // arg0
   182  		uint32(scData.Args[1]>>32), uint32(scData.Args[1]), // arg1
   183  		uint32(scData.Args[2]>>32), uint32(scData.Args[2]), // arg2
   184  		uint32(scData.Args[3]>>32), uint32(scData.Args[3]), // arg3
   185  		uint32(scData.Args[4]>>32), uint32(scData.Args[4]), // arg4
   186  		uint32(scData.Args[5]>>32), uint32(scData.Args[5]), // arg5
   187  		uint32(scData.InstructionPointer>>32), uint32(scData.InstructionPointer), // rip
   188  	)
   189  }
   190  
   191  // defaultSeedCorpus adds generally useful test cases to `f`'s seed corpus.
   192  func (df *DiffFuzzer) defaultSeedCorpus() {
   193  	df.f.Helper()
   194  
   195  	// Seed the fuzzer with each syscall number.
   196  	for sysno := 0; sysno <= sentry.MaxSyscallNum; sysno++ {
   197  		// Add a test case for each syscall argument half to have
   198  		// all bits set. This isn't perfect, but gives lots of
   199  		// coverage cheaply.
   200  		for i := -1; i < len(linux.SeccompData{}.Args); i++ {
   201  			for _, argValue := range []uint64{
   202  				0x0000000000000000,
   203  				0xffffffff00000000,
   204  				0x00000000ffffffff,
   205  			} {
   206  				data := linux.SeccompData{
   207  					Nr:   int32(sysno),
   208  					Arch: seccomp.LINUX_AUDIT_ARCH,
   209  				}
   210  				if i == -1 {
   211  					data.InstructionPointer = argValue
   212  				} else {
   213  					data.Args[i] = argValue
   214  				}
   215  				df.AddSeed(data)
   216  			}
   217  		}
   218  	}
   219  
   220  	// Add a case for the invalid arch case.
   221  	df.AddSeed(linux.SeccompData{
   222  		Nr:   0,
   223  		Arch: seccomp.LINUX_AUDIT_ARCH + 1,
   224  	})
   225  
   226  	// Add a case for an unknown syscall number.
   227  	df.AddSeed(linux.SeccompData{
   228  		Nr:   sentry.MaxSyscallNum + 1,
   229  		Arch: seccomp.LINUX_AUDIT_ARCH,
   230  	})
   231  
   232  	// ALL THE BITS.
   233  	df.AddSeed(linux.SeccompData{
   234  		Nr:                 -1,
   235  		Arch:               0xffffffff,
   236  		InstructionPointer: 0xffffffffffffffff,
   237  		Args: [6]uint64{
   238  			0xffffffffffffffff,
   239  			0xffffffffffffffff,
   240  			0xffffffffffffffff,
   241  			0xffffffffffffffff,
   242  			0xffffffffffffffff,
   243  			0xffffffffffffffff,
   244  		},
   245  	})
   246  }
   247  
   248  // DeriveCorpusFromRuleSets attempts to extract useful seed corpus rules
   249  // out of the given `RuleSet`s.
   250  func (df *DiffFuzzer) DeriveCorpusFromRuleSets(ruleSets []seccomp.RuleSet) {
   251  	for _, ruleSet := range ruleSets {
   252  		for _, tc := range ruleSet.Rules.UsefulTestCases() {
   253  			df.AddSeed(tc)
   254  		}
   255  	}
   256  }
   257  
   258  // NewDiffFuzzer creates a fuzzer that verifies that two seccomp-bpf programs
   259  // are equivalent by fuzzing both of them with the same inputs and checking
   260  // that they output the same result.
   261  func NewDiffFuzzer(f FuzzLike, fuzzee1, fuzzee2 *Fuzzee) (*DiffFuzzer, error) {
   262  	f.Helper()
   263  	if len(fuzzee1.Instructions) > bpf.MaxInstructions {
   264  		return nil, fmt.Errorf("program %s has %d instructions, which exceeds the maximum of %d", fuzzee1.String(), len(fuzzee1.Instructions), bpf.MaxInstructions)
   265  	}
   266  	if len(fuzzee2.Instructions) > bpf.MaxInstructions {
   267  		return nil, fmt.Errorf("program %s has %d instructions, which exceeds the maximum of %d", fuzzee2.String(), len(fuzzee2.Instructions), bpf.MaxInstructions)
   268  	}
   269  	compiled1, err := bpf.Compile(fuzzee1.Instructions, false)
   270  	if err != nil {
   271  		return nil, fmt.Errorf("failed to compile %s: %v", fuzzee1.String(), err)
   272  	}
   273  	compiled2, err := bpf.Compile(fuzzee2.Instructions, false)
   274  	if err != nil {
   275  		return nil, fmt.Errorf("failed to compile %s: %v", fuzzee2.String(), err)
   276  	}
   277  	df := &DiffFuzzer{
   278  		f:         f,
   279  		fuzzee1:   fuzzee1,
   280  		fuzzee2:   fuzzee2,
   281  		compiled1: compiled1,
   282  		compiled2: compiled2,
   283  	}
   284  	df.defaultSeedCorpus()
   285  	return df, nil
   286  }
   287  
   288  // Fuzz runs the fuzzer.
   289  func (df *DiffFuzzer) Fuzz() {
   290  	df.f.Helper()
   291  	pool := sync.Pool{
   292  		New: func() any {
   293  			buf := make([]byte, (&linux.SeccompData{}).SizeBytes())
   294  			return &buf
   295  		},
   296  	}
   297  	df.f.Fuzz(func(
   298  		t *testing.T,
   299  		sysno int32,
   300  		arch uint32,
   301  		arg0_high uint32, arg0_low uint32,
   302  		arg1_high uint32, arg1_low uint32,
   303  		arg2_high uint32, arg2_low uint32,
   304  		arg3_high uint32, arg3_low uint32,
   305  		arg4_high uint32, arg4_low uint32,
   306  		arg5_high uint32, arg5_low uint32,
   307  		rip_high uint32, rip_low uint32,
   308  	) {
   309  		// Reconstruct seccomp data from the fuzzed arguments.
   310  		scData := linux.SeccompData{
   311  			Nr:                 sysno,
   312  			Arch:               arch,
   313  			InstructionPointer: uint64(rip_high)<<32 | uint64(rip_low),
   314  			Args: [6]uint64{
   315  				uint64(arg0_high)<<32 | uint64(arg0_low),
   316  				uint64(arg1_high)<<32 | uint64(arg1_low),
   317  				uint64(arg2_high)<<32 | uint64(arg2_low),
   318  				uint64(arg3_high)<<32 | uint64(arg3_low),
   319  				uint64(arg4_high)<<32 | uint64(arg4_low),
   320  				uint64(arg5_high)<<32 | uint64(arg5_low),
   321  			},
   322  		}
   323  		// We can't allocate this buffer outside of the `fuzz` method because
   324  		// this inner function is called in multiple goroutines.
   325  		// We use a pool instead.
   326  		buf := pool.Get().(*[]byte)
   327  		exec1, err := bpf.InstrumentedExec[bpf.NativeEndian](df.compiled1, seccomp.DataAsBPFInput(&scData, *buf))
   328  		if err != nil {
   329  			t.Fatalf("Failed to execute %s with data %s: %v", df.fuzzee1.String(), scData.String(), err)
   330  		}
   331  		exec2, err := bpf.InstrumentedExec[bpf.NativeEndian](df.compiled2, seccomp.DataAsBPFInput(&scData, *buf))
   332  		if err != nil {
   333  			t.Fatalf("Failed to execute %s with data %s: %v", df.fuzzee2.String(), scData.String(), err)
   334  		}
   335  		pool.Put(buf)
   336  		if exec1.ReturnValue != exec2.ReturnValue {
   337  			t.Errorf(
   338  				"%s and %s return different results for %s: %s = %v, %s = %v",
   339  				df.fuzzee1.String(), df.fuzzee2.String(),
   340  				scData.String(),
   341  				df.fuzzee1.String(), linux.BPFAction(exec1.ReturnValue),
   342  				df.fuzzee2.String(), linux.BPFAction(exec2.ReturnValue),
   343  			)
   344  		}
   345  		countExecutedLinesProgram1(exec1, df.fuzzee1)
   346  		countExecutedLinesProgram2(exec2, df.fuzzee2)
   347  	})
   348  	notCovered1 := false
   349  	for i := 0; i < len(df.fuzzee1.Instructions); i++ {
   350  		if !df.fuzzee1.coverage[i].Load() {
   351  			notCovered1 = true
   352  			break
   353  		}
   354  	}
   355  	notCovered2 := false
   356  	for i := 0; i < len(df.fuzzee2.Instructions); i++ {
   357  		if !df.fuzzee2.coverage[i].Load() {
   358  			notCovered2 = true
   359  			break
   360  		}
   361  	}
   362  	if notCovered1 {
   363  		if df.fuzzee1.EnforceFullCoverage {
   364  			df.f.Errorf("Program %s not fully covered:", df.fuzzee1.String())
   365  			for pc, ins := range df.fuzzee1.Instructions {
   366  				if df.fuzzee1.coverage[pc].Load() {
   367  					df.f.Errorf("         [OK] % 4d: %s", pc, ins.String())
   368  				} else {
   369  					df.f.Errorf("[NOT COVERED] % 4d: %s", pc, ins.String())
   370  				}
   371  			}
   372  			df.f.Errorf("\n")
   373  		} else {
   374  			df.f.Logf("Program %s not fully covered (but coverage not enforced).", df.fuzzee1.String())
   375  		}
   376  	}
   377  	if notCovered2 {
   378  		if df.fuzzee2.EnforceFullCoverage {
   379  			df.f.Errorf("Program %s not fully covered:", df.fuzzee2.String())
   380  			for pc, ins := range df.fuzzee2.Instructions {
   381  				if df.fuzzee2.coverage[pc].Load() {
   382  					df.f.Errorf("         [OK] % 4d: %s", pc, ins.String())
   383  				} else {
   384  					df.f.Errorf("[NOT COVERED] % 4d: %s", pc, ins.String())
   385  				}
   386  			}
   387  			df.f.Errorf("\n")
   388  		} else {
   389  			df.f.Logf("Program %s not fully covered (but coverage not enforced).", df.fuzzee2.String())
   390  		}
   391  	}
   392  }