github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/tools/syz-reporter/reporter.go (about) 1 // Copyright 2020 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 // syz-reporter creates table information from crashes. 5 // Useful tool together with tools/syz-crush to collect 6 // results from the reproducer runs. 7 // 8 // Nice extension to this would be to accept multiple configurations and 9 // then collect table from all the different workdirectories. This would allow easy comparison 10 // if different kernel version have same BUGs. 11 package main 12 13 import ( 14 "bytes" 15 "flag" 16 "fmt" 17 "io" 18 "log" 19 "os" 20 "os/exec" 21 "path/filepath" 22 "sort" 23 "strconv" 24 "strings" 25 26 "github.com/google/syzkaller/pkg/html/pages" 27 "github.com/google/syzkaller/pkg/kconfig" 28 "github.com/google/syzkaller/pkg/mgrconfig" 29 "github.com/google/syzkaller/pkg/osutil" 30 ) 31 32 var ( 33 flagConfig = flag.String("config", "", "configuration file") 34 ) 35 36 type UISummaryData struct { 37 Name string 38 Crashes []*UICrashType 39 CrashCount int 40 CrashCountReproducers int 41 Workdir string 42 } 43 44 type UICrashType struct { 45 Description string 46 ID string 47 Count int 48 Reproducers map[string]string 49 Crashes []*UICrash 50 CausingCommits []string 51 FixingCommits []string 52 CausingConfigs []string 53 } 54 55 type UICrash struct { 56 Index int 57 Log string 58 Report string 59 } 60 61 func main() { 62 flag.Parse() 63 cfg, err := mgrconfig.LoadFile(*flagConfig) 64 if err != nil { 65 log.Fatalf("%v", err) 66 } 67 68 fn, err := osutil.TempFile("syz-reporter") 69 if err != nil { 70 log.Fatalf("%v", err) 71 } 72 fn += ".html" 73 74 buf := new(bytes.Buffer) 75 if httpSummary(buf, cfg) != nil { 76 log.Fatalf("%v", err) 77 } 78 79 if err := osutil.WriteFile(fn, buf.Bytes()); err != nil { 80 log.Fatalf("%v", err) 81 } 82 if err := exec.Command("xdg-open", fn).Start(); err != nil { 83 log.Fatalf("failed to start browser: %v", err) 84 } 85 } 86 87 func httpSummary(w io.Writer, cfg *mgrconfig.Config) error { 88 data := &UISummaryData{ 89 Name: cfg.Name, 90 Workdir: cfg.Workdir, 91 } 92 93 var err error 94 if data.Crashes, err = collectCrashes(cfg.Workdir); err != nil { 95 return fmt.Errorf("failed to collect crashes: %w", err) 96 } 97 98 data.CrashCount = len(data.Crashes) 99 100 for _, crash := range data.Crashes { 101 if len(crash.Reproducers) > 0 { 102 data.CrashCountReproducers = data.CrashCountReproducers + 1 103 } 104 } 105 106 if err = summaryTemplate.Execute(w, data); err != nil { 107 return fmt.Errorf("failed to execute template: %w", err) 108 } 109 110 return err 111 } 112 113 func collectCrashes(workdir string) ([]*UICrashType, error) { 114 crashdir := filepath.Join(workdir, "crashes") 115 dirs, err := osutil.ListDir(crashdir) 116 if err != nil { 117 return nil, err 118 } 119 var crashTypes []*UICrashType 120 for _, dir := range dirs { 121 crash := readCrash(workdir, dir) 122 if crash != nil { 123 crashTypes = append(crashTypes, crash) 124 } 125 } 126 sort.Slice(crashTypes, func(i, j int) bool { 127 return strings.ToLower(crashTypes[i].Description) < strings.ToLower(crashTypes[j].Description) 128 }) 129 return crashTypes, nil 130 } 131 132 func readCrash(workdir, dir string) *UICrashType { 133 if len(dir) != 40 { 134 return nil 135 } 136 crashdir := filepath.Join(workdir, "crashes") 137 descFile, err := os.Open(filepath.Join(crashdir, dir, "description")) 138 if err != nil { 139 return nil 140 } 141 defer descFile.Close() 142 descBytes, err := io.ReadAll(descFile) 143 if err != nil || len(descBytes) == 0 { 144 return nil 145 } 146 desc := string(trimNewLines(descBytes)) 147 descFile.Close() 148 149 files, err := osutil.ListDir(filepath.Join(crashdir, dir)) 150 if err != nil { 151 return nil 152 } 153 var crashes []*UICrash 154 155 reproducers := make(map[string]string) 156 var causingCommits []string 157 var fixingCommits []string 158 var causingConfigs []string 159 for _, f := range files { 160 if strings.HasPrefix(f, "log") { 161 index, err := strconv.ParseUint(f[3:], 10, 64) 162 if err == nil { 163 crashes = append(crashes, &UICrash{ 164 Index: int(index), 165 }) 166 } 167 } 168 169 if strings.HasPrefix(f, "tag") { 170 tag, err := os.ReadFile(filepath.Join(crashdir, dir, f)) 171 if err == nil { 172 reproducers[string(tag)] = string(tag) 173 } 174 } 175 176 if strings.HasSuffix(f, "prog") { 177 reproducers[f] = f 178 } 179 180 if f == "cause.commit" { 181 commits, err := os.ReadFile(filepath.Join(crashdir, dir, f)) 182 if err == nil { 183 causingCommits = strings.Split(string(commits), "\n") 184 } 185 } 186 if f == "fix.commit" { 187 commits, err := os.ReadFile(filepath.Join(crashdir, dir, f)) 188 if err == nil { 189 fixingCommits = strings.Split(string(commits), "\n") 190 } 191 } 192 if f == kconfig.CauseConfigFile { 193 configs, err := os.ReadFile(filepath.Join(crashdir, dir, f)) 194 if err == nil { 195 configsList := strings.Split(string(configs), "\n") 196 // Ignore configuration list longer than 10 197 if len(configsList) <= 10 { 198 causingConfigs = configsList 199 } else { 200 causingConfigs = []string{"..."} 201 } 202 } 203 } 204 } 205 206 return &UICrashType{ 207 Description: desc, 208 ID: dir, 209 Count: len(crashes), 210 Reproducers: reproducers, 211 Crashes: crashes, 212 CausingCommits: causingCommits, 213 FixingCommits: fixingCommits, 214 CausingConfigs: causingConfigs, 215 } 216 } 217 218 func trimNewLines(data []byte) []byte { 219 for len(data) > 0 && data[len(data)-1] == '\n' { 220 data = data[:len(data)-1] 221 } 222 return data 223 } 224 225 var summaryTemplate = pages.Create(` 226 <!doctype html> 227 <html> 228 <head> 229 <title>{{.Name }} syzkaller</title> 230 {{HEAD}} 231 </head> 232 <body> 233 <b>{{.Name }} syzkaller</b> 234 <br> 235 <b>Workdir: {{.Workdir }}</b> 236 <br> 237 <b>Crash Count: {{ .CrashCount }}</b> 238 <br> 239 <b>Crash With Reproducers Count: {{ .CrashCountReproducers }}</b> 240 <br> 241 242 <table class="list_table"> 243 <caption>Crashes:</caption> 244 <tr> 245 <th><a onclick="return sortTable(this, 'Description', textSort)" href="#">Description</a></th> 246 <th><a onclick="return sortTable(this, 'Count', numSort)" href="#">Count</a></th> 247 <th><a onclick="return sortTable(this, 'Reproducers', lineSort)" href="#">Reproducers</a></th> 248 <th><a onclick="return sortTable(this, 'Causing_Commits', lineSort)" href="#">Causing_Commits</a></th> 249 <th><a onclick="return sortTable(this, 'Fixing_Commits', lineSort)" href="#">Fixing_Commits</a></th> 250 <th><a onclick="return sortTable(this, 'Causing_Configs', lineSort)" href="#">Causing_Configs</a></th> 251 </tr> 252 {{range $c := $.Crashes}} 253 <tr> 254 <td class="title">{{$c.Description}}</td> 255 <td class="stat">{{$c.Count}}</td> 256 <td class="reproducer"> 257 {{range $reproducer := $c.Reproducers}} 258 {{$reproducer}}</br> 259 {{end}} 260 </td> 261 <td class="Causing Commits"> 262 {{range $commit := $c.CausingCommits}} 263 {{$commit}}</br> 264 {{end}} 265 </td> 266 <td class="Fixing Commits"> 267 {{range $commit := $c.FixingCommits}} 268 {{$commit}}</br> 269 {{end}} 270 </td> 271 <td class="Causing Configs"> 272 {{range $config := $c.CausingConfigs}} 273 {{$config}}</br> 274 {{end}} 275 </td> 276 </tr> 277 {{end}} 278 </table> 279 280 </body></html> 281 `)