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 }