github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/tools/syz-testbed/targets.go (about)

     1  // Copyright 2022 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 main
     5  
     6  import (
     7  	"fmt"
     8  	"log"
     9  	"math/rand"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/google/syzkaller/pkg/osutil"
    17  	"github.com/google/syzkaller/pkg/tool"
    18  )
    19  
    20  // TestbedTarget represents all behavioral differences between specific testbed targets.
    21  type TestbedTarget interface {
    22  	NewJob(slotName string, checkouts []*Checkout) (*Checkout, Instance, error)
    23  	SaveStatView(view StatView, dir string) error
    24  	SupportsHTMLView(key string) bool
    25  }
    26  
    27  type SyzManagerTarget struct {
    28  	config         *TestbedConfig
    29  	nextCheckoutID int
    30  	nextInstanceID int
    31  	mu             sync.Mutex
    32  }
    33  
    34  var targetConstructors = map[string]func(cfg *TestbedConfig) TestbedTarget{
    35  	"syz-manager": func(cfg *TestbedConfig) TestbedTarget {
    36  		return &SyzManagerTarget{
    37  			config: cfg,
    38  		}
    39  	},
    40  	"syz-repro": func(cfg *TestbedConfig) TestbedTarget {
    41  		inputFiles := []string{}
    42  		reproConfig := cfg.ReproConfig
    43  		if reproConfig.InputLogs != "" {
    44  			err := filepath.Walk(reproConfig.InputLogs, func(path string, info os.FileInfo, err error) error {
    45  				if err != nil {
    46  					return err
    47  				}
    48  				if !info.IsDir() {
    49  					inputFiles = append(inputFiles, path)
    50  				}
    51  				return nil
    52  			})
    53  			if err != nil {
    54  				tool.Failf("failed to read logs file directory: %s", err)
    55  			}
    56  		} else if reproConfig.InputWorkdir != "" {
    57  			skipRegexps := []*regexp.Regexp{}
    58  			for _, reStr := range reproConfig.SkipBugs {
    59  				skipRegexps = append(skipRegexps, regexp.MustCompile(reStr))
    60  			}
    61  			bugs, err := collectBugs(reproConfig.InputWorkdir)
    62  			if err != nil {
    63  				tool.Failf("failed to read workdir: %s", err)
    64  			}
    65  			r := rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
    66  			for _, bug := range bugs {
    67  				skip := false
    68  				for _, re := range skipRegexps {
    69  					if re.MatchString(bug.Title) {
    70  						skip = true
    71  						break
    72  					}
    73  				}
    74  				if skip {
    75  					continue
    76  				}
    77  				logs := append([]string{}, bug.Logs...)
    78  				for i := 0; i < reproConfig.CrashesPerBug && len(logs) > 0; i++ {
    79  					randID := r.Intn(len(logs))
    80  					logs[len(logs)-1], logs[randID] = logs[randID], logs[len(logs)-1]
    81  					inputFiles = append(inputFiles, logs[len(logs)-1])
    82  					logs = logs[:len(logs)-1]
    83  				}
    84  			}
    85  		}
    86  		inputs := []*SyzReproInput{}
    87  		log.Printf("picked up crash files:")
    88  		for _, path := range inputFiles {
    89  			log.Printf("- %s", path)
    90  			inputs = append(inputs, &SyzReproInput{
    91  				Path:  path,
    92  				runBy: make(map[*Checkout]int),
    93  			})
    94  		}
    95  		if len(inputs) == 0 {
    96  			tool.Failf("no inputs given")
    97  		}
    98  		// TODO: shuffle?
    99  		return &SyzReproTarget{
   100  			config:     cfg,
   101  			dedupTitle: make(map[string]int),
   102  			inputs:     inputs,
   103  		}
   104  	},
   105  }
   106  
   107  func (t *SyzManagerTarget) NewJob(slotName string, checkouts []*Checkout) (*Checkout, Instance, error) {
   108  	// Round-robin strategy should suffice.
   109  	t.mu.Lock()
   110  	checkout := checkouts[t.nextCheckoutID%len(checkouts)]
   111  	instanceID := t.nextInstanceID
   112  	t.nextCheckoutID++
   113  	t.nextInstanceID++
   114  	t.mu.Unlock()
   115  	uniqName := fmt.Sprintf("%s-%d", checkout.Name, instanceID)
   116  	instance, err := t.newSyzManagerInstance(slotName, uniqName, checkout)
   117  	if err != nil {
   118  		return nil, nil, err
   119  	}
   120  	return checkout, instance, nil
   121  }
   122  
   123  func (t *SyzManagerTarget) SupportsHTMLView(key string) bool {
   124  	supported := map[string]bool{
   125  		HTMLBugsTable:      true,
   126  		HTMLBugCountsTable: true,
   127  		HTMLStatsTable:     true,
   128  	}
   129  	return supported[key]
   130  }
   131  
   132  func (t *SyzManagerTarget) SaveStatView(view StatView, dir string) error {
   133  	benchDir := filepath.Join(dir, "benches")
   134  	err := osutil.MkdirAll(benchDir)
   135  	if err != nil {
   136  		return fmt.Errorf("failed to create %s: %w", benchDir, err)
   137  	}
   138  	tableStats := map[string]func(view StatView) (*Table, error){
   139  		"bugs.csv":           (StatView).GenerateBugTable,
   140  		"checkout_stats.csv": (StatView).StatsTable,
   141  		"instance_stats.csv": (StatView).InstanceStatsTable,
   142  	}
   143  	for fileName, genFunc := range tableStats {
   144  		table, err := genFunc(view)
   145  		if err == nil {
   146  			table.SaveAsCsv(filepath.Join(dir, fileName))
   147  		} else {
   148  			log.Printf("stat generation error: %s", err)
   149  		}
   150  	}
   151  	_, err = view.SaveAvgBenches(benchDir)
   152  	return err
   153  }
   154  
   155  // TODO: consider other repro testing modes.
   156  // E.g. group different logs by title. Then we could also set different sets of inputs
   157  // for each checkout. It can be important if we improve executor logging.
   158  type SyzReproTarget struct {
   159  	config     *TestbedConfig
   160  	inputs     []*SyzReproInput
   161  	seqID      int
   162  	dedupTitle map[string]int
   163  	mu         sync.Mutex
   164  }
   165  
   166  type SyzReproInput struct {
   167  	Path      string
   168  	Title     string
   169  	Skip      bool
   170  	origTitle string
   171  	runBy     map[*Checkout]int
   172  }
   173  
   174  func (inp *SyzReproInput) QueryTitle(checkout *Checkout, dupsMap map[string]int) error {
   175  	data, err := os.ReadFile(inp.Path)
   176  	if err != nil {
   177  		return fmt.Errorf("failed to read: %w", err)
   178  	}
   179  	report := checkout.GetReporter().Parse(data)
   180  	if report == nil {
   181  		return fmt.Errorf("found no crash")
   182  	}
   183  	if inp.Title == "" {
   184  		inp.origTitle = report.Title
   185  		inp.Title = report.Title
   186  		// Some bug titles may be present in multiple log files.
   187  		// Ensure they are all distict to the user.
   188  		dupsMap[inp.origTitle]++
   189  		if dupsMap[inp.Title] > 1 {
   190  			inp.Title += fmt.Sprintf(" (%d)", dupsMap[inp.origTitle])
   191  		}
   192  	}
   193  	return nil
   194  }
   195  
   196  func (t *SyzReproTarget) NewJob(slotName string, checkouts []*Checkout) (*Checkout, Instance, error) {
   197  	t.mu.Lock()
   198  	seqID := t.seqID
   199  	checkout := checkouts[t.seqID%len(checkouts)]
   200  	t.seqID++
   201  	// This may be not the most efficient algorithm, but it guarantees even distribution of
   202  	// resources and CPU time is negligible in comparison with the amount of time each instance runs.
   203  	var input *SyzReproInput
   204  	for _, candidate := range t.inputs {
   205  		if candidate.Skip {
   206  			continue
   207  		}
   208  		if candidate.runBy[checkout] == 0 {
   209  			// This is the first time we'll attempt to give this log to the checkout.
   210  			// Check if it can handle it.
   211  			err := candidate.QueryTitle(checkout, t.dedupTitle)
   212  			if err != nil {
   213  				log.Printf("[log %s]: %s, skipping", candidate.Path, err)
   214  				candidate.Skip = true
   215  				continue
   216  			}
   217  		}
   218  		if input == nil || input.runBy[checkout] > candidate.runBy[checkout] {
   219  			// Pick the least executed one.
   220  			input = candidate
   221  		}
   222  	}
   223  
   224  	if input == nil {
   225  		t.mu.Unlock()
   226  		return nil, nil, fmt.Errorf("no available inputs")
   227  	}
   228  	input.runBy[checkout]++
   229  	t.mu.Unlock()
   230  
   231  	uniqName := fmt.Sprintf("%s-%d", checkout.Name, seqID)
   232  	instance, err := t.newSyzReproInstance(slotName, uniqName, input, checkout)
   233  	if err != nil {
   234  		return nil, nil, err
   235  	}
   236  	return checkout, instance, nil
   237  }
   238  
   239  func (t *SyzReproTarget) SupportsHTMLView(key string) bool {
   240  	supported := map[string]bool{
   241  		HTMLReprosTable:        true,
   242  		HTMLCReprosTable:       true,
   243  		HTMLReproAttemptsTable: true,
   244  		HTMLReproDurationTable: true,
   245  	}
   246  	return supported[key]
   247  }
   248  
   249  func (t *SyzReproTarget) SaveStatView(view StatView, dir string) error {
   250  	tableStats := map[string]func(view StatView) (*Table, error){
   251  		"repro_success.csv":   (StatView).GenerateReproSuccessTable,
   252  		"crepros_success.csv": (StatView).GenerateCReproSuccessTable,
   253  		"repro_attempts.csv":  (StatView).GenerateReproAttemptsTable,
   254  		"repro_duration.csv":  (StatView).GenerateReproDurationTable,
   255  	}
   256  	for fileName, genFunc := range tableStats {
   257  		table, err := genFunc(view)
   258  		if err == nil {
   259  			table.SaveAsCsv(filepath.Join(dir, fileName))
   260  		} else {
   261  			log.Printf("stat generation error: %s", err)
   262  		}
   263  	}
   264  	return nil
   265  }