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 }