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