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 }