github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/pkg/fuzzer/fuzzer_test.go (about)

     1  // Copyright 2024 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  package fuzzer
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"hash/crc32"
    11  	"math/rand"
    12  	"regexp"
    13  	"runtime"
    14  	"strings"
    15  	"sync"
    16  	"testing"
    17  	"time"
    18  
    19  	"github.com/google/syzkaller/pkg/corpus"
    20  	"github.com/google/syzkaller/pkg/csource"
    21  	"github.com/google/syzkaller/pkg/flatrpc"
    22  	"github.com/google/syzkaller/pkg/fuzzer/queue"
    23  	"github.com/google/syzkaller/pkg/ipc"
    24  	"github.com/google/syzkaller/pkg/ipc/ipcconfig"
    25  	"github.com/google/syzkaller/pkg/signal"
    26  	"github.com/google/syzkaller/pkg/testutil"
    27  	"github.com/google/syzkaller/prog"
    28  	"github.com/google/syzkaller/sys/targets"
    29  	"github.com/stretchr/testify/assert"
    30  	"golang.org/x/sync/errgroup"
    31  )
    32  
    33  func TestFuzz(t *testing.T) {
    34  	defer checkGoroutineLeaks()
    35  
    36  	target, err := prog.GetTarget(targets.TestOS, targets.TestArch64Fuzz)
    37  	if err != nil {
    38  		t.Fatal(err)
    39  	}
    40  	sysTarget := targets.Get(target.OS, target.Arch)
    41  	if sysTarget.BrokenCompiler != "" {
    42  		t.Skipf("skipping, broken cross-compiler: %v", sysTarget.BrokenCompiler)
    43  	}
    44  	executor := csource.BuildExecutor(t, target, "../..", "-fsanitize-coverage=trace-pc", "-g")
    45  	ctx, cancel := context.WithCancel(context.Background())
    46  	defer cancel()
    47  
    48  	_, opts, _ := ipcconfig.Default(target)
    49  	corpusUpdates := make(chan corpus.NewItemEvent)
    50  	fuzzer := NewFuzzer(ctx, &Config{
    51  		Debug:    true,
    52  		BaseOpts: *opts,
    53  		Corpus:   corpus.NewMonitoredCorpus(ctx, corpusUpdates),
    54  		Logf: func(level int, msg string, args ...interface{}) {
    55  			if level > 1 {
    56  				return
    57  			}
    58  			t.Logf(msg, args...)
    59  		},
    60  		Coverage: true,
    61  		EnabledCalls: map[*prog.Syscall]bool{
    62  			target.SyscallMap["syz_test_fuzzer1"]: true,
    63  		},
    64  	}, rand.New(testutil.RandSource(t)), target)
    65  
    66  	go func() {
    67  		for {
    68  			select {
    69  			case <-ctx.Done():
    70  				return
    71  			case u := <-corpusUpdates:
    72  				t.Logf("new prog:\n%s", u.ProgData)
    73  			}
    74  		}
    75  	}()
    76  
    77  	tf := newTestFuzzer(t, fuzzer, map[string]bool{
    78  		"first bug":  true,
    79  		"second bug": true,
    80  	}, 10000)
    81  
    82  	for i := 0; i < 2; i++ {
    83  		tf.registerExecutor(newProc(t, target, executor))
    84  	}
    85  	tf.wait()
    86  
    87  	t.Logf("resulting corpus:")
    88  	for _, p := range fuzzer.Config.Corpus.Programs() {
    89  		t.Logf("-----")
    90  		t.Logf("%s", p.Serialize())
    91  	}
    92  
    93  	assert.Equal(t, len(tf.expectedCrashes), len(tf.crashes),
    94  		"not all expected crashes were found")
    95  }
    96  
    97  func BenchmarkFuzzer(b *testing.B) {
    98  	b.ReportAllocs()
    99  	target, err := prog.GetTarget(targets.TestOS, targets.TestArch64Fuzz)
   100  	if err != nil {
   101  		b.Fatal(err)
   102  	}
   103  	ctx, cancel := context.WithCancel(context.Background())
   104  	defer cancel()
   105  	calls := map[*prog.Syscall]bool{}
   106  	for _, c := range target.Syscalls {
   107  		calls[c] = true
   108  	}
   109  	fuzzer := NewFuzzer(ctx, &Config{
   110  		Corpus:       corpus.NewCorpus(ctx),
   111  		Coverage:     true,
   112  		EnabledCalls: calls,
   113  	}, rand.New(rand.NewSource(time.Now().UnixNano())), target)
   114  
   115  	b.ResetTimer()
   116  	b.RunParallel(func(pb *testing.PB) {
   117  		for pb.Next() {
   118  			req := fuzzer.Next()
   119  			res, _, _ := emulateExec(req)
   120  			req.Done(res)
   121  		}
   122  	})
   123  }
   124  
   125  const anyTestProg = `syz_compare(&AUTO="00000000", 0x4, &AUTO=@conditional={0x0, @void, @void}, AUTO)`
   126  
   127  func TestRotate(t *testing.T) {
   128  	target, err := prog.GetTarget(targets.TestOS, targets.TestArch64Fuzz)
   129  	if err != nil {
   130  		t.Fatal(err)
   131  	}
   132  
   133  	ctx, cancel := context.WithCancel(context.Background())
   134  	defer cancel()
   135  
   136  	corpusObj := corpus.NewCorpus(ctx)
   137  	fuzzer := NewFuzzer(ctx, &Config{
   138  		Debug:    true,
   139  		Corpus:   corpusObj,
   140  		Coverage: true,
   141  		EnabledCalls: map[*prog.Syscall]bool{
   142  			target.SyscallMap["syz_compare"]: true,
   143  		},
   144  	}, rand.New(testutil.RandSource(t)), target)
   145  
   146  	fakeSignal := func(size int) signal.Signal {
   147  		var pc []uint32
   148  		for i := 0; i < size; i++ {
   149  			pc = append(pc, uint32(i))
   150  		}
   151  		return signal.FromRaw(pc, 0)
   152  	}
   153  
   154  	prog, err := target.Deserialize([]byte(anyTestProg), prog.NonStrict)
   155  	assert.NoError(t, err)
   156  	corpusObj.Save(corpus.NewInput{
   157  		Prog:   prog,
   158  		Call:   0,
   159  		Signal: fakeSignal(100),
   160  	})
   161  	fuzzer.Cover.AddMaxSignal(fakeSignal(1000))
   162  
   163  	assert.Equal(t, 1000, len(fuzzer.Cover.maxSignal))
   164  	assert.Equal(t, 100, corpusObj.StatSignal.Val())
   165  
   166  	// Rotate some of the signal.
   167  	fuzzer.RotateMaxSignal(200)
   168  	assert.Equal(t, 800, len(fuzzer.Cover.maxSignal))
   169  	assert.Equal(t, 100, corpusObj.StatSignal.Val())
   170  
   171  	plus, minus := fuzzer.Cover.GrabSignalDelta()
   172  	assert.Equal(t, 0, plus.Len())
   173  	assert.Equal(t, 200, minus.Len())
   174  
   175  	// Rotate the rest.
   176  	fuzzer.RotateMaxSignal(1000)
   177  	assert.Equal(t, 100, len(fuzzer.Cover.maxSignal))
   178  	assert.Equal(t, 100, corpusObj.StatSignal.Val())
   179  	plus, minus = fuzzer.Cover.GrabSignalDelta()
   180  	assert.Equal(t, 0, plus.Len())
   181  	assert.Equal(t, 700, minus.Len())
   182  }
   183  
   184  // Based on the example from Go documentation.
   185  var crc32q = crc32.MakeTable(0xD5828281)
   186  
   187  func emulateExec(req *queue.Request) (*queue.Result, string, error) {
   188  	serializedLines := bytes.Split(req.Prog.Serialize(), []byte("\n"))
   189  	var info ipc.ProgInfo
   190  	for i, call := range req.Prog.Calls {
   191  		cover := uint32(call.Meta.ID*1024) +
   192  			crc32.Checksum(serializedLines[i], crc32q)%4
   193  		callInfo := ipc.CallInfo{}
   194  		if req.ExecOpts.ExecFlags&flatrpc.ExecFlagCollectCover > 0 {
   195  			callInfo.Cover = []uint32{cover}
   196  		}
   197  		if req.ExecOpts.ExecFlags&flatrpc.ExecFlagCollectSignal > 0 {
   198  			callInfo.Signal = []uint32{cover}
   199  		}
   200  		info.Calls = append(info.Calls, callInfo)
   201  	}
   202  	return &queue.Result{Info: &info}, "", nil
   203  }
   204  
   205  type testFuzzer struct {
   206  	t               testing.TB
   207  	eg              errgroup.Group
   208  	fuzzer          *Fuzzer
   209  	mu              sync.Mutex
   210  	crashes         map[string]int
   211  	expectedCrashes map[string]bool
   212  	iter            int
   213  	iterLimit       int
   214  }
   215  
   216  func newTestFuzzer(t testing.TB, fuzzer *Fuzzer, expectedCrashes map[string]bool, iterLimit int) *testFuzzer {
   217  	return &testFuzzer{
   218  		t:               t,
   219  		fuzzer:          fuzzer,
   220  		expectedCrashes: expectedCrashes,
   221  		crashes:         map[string]int{},
   222  		iterLimit:       iterLimit,
   223  	}
   224  }
   225  
   226  func (f *testFuzzer) oneMore() bool {
   227  	f.mu.Lock()
   228  	defer f.mu.Unlock()
   229  	f.iter++
   230  	if f.iter%100 == 0 {
   231  		f.t.Logf("<iter %d>: corpus %d, signal %d, max signal %d, crash types %d, running jobs %d",
   232  			f.iter, f.fuzzer.Config.Corpus.StatProgs.Val(), f.fuzzer.Config.Corpus.StatSignal.Val(),
   233  			len(f.fuzzer.Cover.maxSignal), len(f.crashes), f.fuzzer.statJobs.Val())
   234  	}
   235  	return f.iter < f.iterLimit &&
   236  		(f.expectedCrashes == nil || len(f.crashes) != len(f.expectedCrashes))
   237  }
   238  
   239  func (f *testFuzzer) registerExecutor(proc *executorProc) {
   240  	f.eg.Go(func() error {
   241  		for f.oneMore() {
   242  			req := f.fuzzer.Next()
   243  			res, crash, err := proc.execute(req)
   244  			if err != nil {
   245  				return err
   246  			}
   247  			if crash != "" {
   248  				res = &queue.Result{Status: queue.Crashed}
   249  				if !f.expectedCrashes[crash] {
   250  					return fmt.Errorf("unexpected crash: %q", crash)
   251  				}
   252  				f.mu.Lock()
   253  				f.t.Logf("CRASH: %s", crash)
   254  				f.crashes[crash]++
   255  				f.mu.Unlock()
   256  			}
   257  			req.Done(res)
   258  		}
   259  		return nil
   260  	})
   261  }
   262  
   263  func (f *testFuzzer) wait() {
   264  	t := f.t
   265  	err := f.eg.Wait()
   266  	if err != nil {
   267  		t.Fatal(err)
   268  	}
   269  	t.Logf("crashes:")
   270  	for title, cnt := range f.crashes {
   271  		t.Logf("%s: %d", title, cnt)
   272  	}
   273  }
   274  
   275  // TODO: it's already implemented in syz-fuzzer/proc.go,
   276  // pkg/runtest and tools/syz-execprog.
   277  // Looks like it's time to factor out this functionality.
   278  type executorProc struct {
   279  	env      *ipc.Env
   280  	execOpts ipc.ExecOpts
   281  }
   282  
   283  func newProc(t *testing.T, target *prog.Target, executor string) *executorProc {
   284  	config, execOpts, err := ipcconfig.Default(target)
   285  	if err != nil {
   286  		t.Fatal(err)
   287  	}
   288  	config.Executor = executor
   289  	execOpts.EnvFlags |= flatrpc.ExecEnvSignal
   290  	env, err := ipc.MakeEnv(config, 0)
   291  	if err != nil {
   292  		t.Fatal(err)
   293  	}
   294  	t.Cleanup(func() { env.Close() })
   295  	return &executorProc{
   296  		env:      env,
   297  		execOpts: *execOpts,
   298  	}
   299  }
   300  
   301  var crashRe = regexp.MustCompile(`{{CRASH: (.*?)}}`)
   302  
   303  func (proc *executorProc) execute(req *queue.Request) (*queue.Result, string, error) {
   304  	// TODO: support hints emulation.
   305  	output, info, _, err := proc.env.Exec(&req.ExecOpts, req.Prog)
   306  	ret := crashRe.FindStringSubmatch(string(output))
   307  	if ret != nil {
   308  		return nil, ret[1], nil
   309  	} else if err != nil {
   310  		return nil, "", err
   311  	}
   312  	return &queue.Result{Info: info}, "", nil
   313  }
   314  
   315  func checkGoroutineLeaks() {
   316  	// Inspired by src/net/http/main_test.go.
   317  	buf := make([]byte, 2<<20)
   318  	err := ""
   319  	for i := 0; i < 3; i++ {
   320  		buf = buf[:runtime.Stack(buf, true)]
   321  		err = ""
   322  		for _, g := range strings.Split(string(buf), "\n\n") {
   323  			if !strings.Contains(g, "pkg/fuzzer/fuzzer.go") {
   324  				continue
   325  			}
   326  			err = fmt.Sprintf("%sLeaked goroutine:\n%s", err, g)
   327  		}
   328  		if err == "" {
   329  			return
   330  		}
   331  		// Give ctx.Done() a chance to propagate to all goroutines.
   332  		time.Sleep(100 * time.Millisecond)
   333  	}
   334  	if err != "" {
   335  		panic(err)
   336  	}
   337  }