github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/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 // TODO: reuse manager.CrashStore. 133 func readCrash(workdir, dir string) *UICrashType { 134 if len(dir) != 40 { 135 return nil 136 } 137 crashdir := filepath.Join(workdir, "crashes") 138 descFile, err := os.Open(filepath.Join(crashdir, dir, "description")) 139 if err != nil { 140 return nil 141 } 142 defer descFile.Close() 143 descBytes, err := io.ReadAll(descFile) 144 if err != nil || len(descBytes) == 0 { 145 return nil 146 } 147 desc := string(trimNewLines(descBytes)) 148 descFile.Close() 149 150 files, err := osutil.ListDir(filepath.Join(crashdir, dir)) 151 if err != nil { 152 return nil 153 } 154 var crashes []*UICrash 155 156 reproducers := make(map[string]string) 157 var causingCommits []string 158 var fixingCommits []string 159 var causingConfigs []string 160 for _, f := range files { 161 if strings.HasPrefix(f, "log") { 162 index, err := strconv.ParseUint(f[3:], 10, 64) 163 if err == nil { 164 crashes = append(crashes, &UICrash{ 165 Index: int(index), 166 }) 167 } 168 } 169 170 if strings.HasPrefix(f, "tag") { 171 tag, err := os.ReadFile(filepath.Join(crashdir, dir, f)) 172 if err == nil { 173 reproducers[string(tag)] = string(tag) 174 } 175 } 176 177 if strings.HasSuffix(f, "prog") { 178 reproducers[f] = f 179 } 180 181 if f == "cause.commit" { 182 commits, err := os.ReadFile(filepath.Join(crashdir, dir, f)) 183 if err == nil { 184 causingCommits = strings.Split(string(commits), "\n") 185 } 186 } 187 if f == "fix.commit" { 188 commits, err := os.ReadFile(filepath.Join(crashdir, dir, f)) 189 if err == nil { 190 fixingCommits = strings.Split(string(commits), "\n") 191 } 192 } 193 if f == kconfig.CauseConfigFile { 194 configs, err := os.ReadFile(filepath.Join(crashdir, dir, f)) 195 if err == nil { 196 configsList := strings.Split(string(configs), "\n") 197 // Ignore configuration list longer than 10 198 if len(configsList) <= 10 { 199 causingConfigs = configsList 200 } else { 201 causingConfigs = []string{"..."} 202 } 203 } 204 } 205 } 206 207 return &UICrashType{ 208 Description: desc, 209 ID: dir, 210 Count: len(crashes), 211 Reproducers: reproducers, 212 Crashes: crashes, 213 CausingCommits: causingCommits, 214 FixingCommits: fixingCommits, 215 CausingConfigs: causingConfigs, 216 } 217 } 218 219 func trimNewLines(data []byte) []byte { 220 for len(data) > 0 && data[len(data)-1] == '\n' { 221 data = data[:len(data)-1] 222 } 223 return data 224 } 225 226 var summaryTemplate = pages.Create(` 227 <!doctype html> 228 <html> 229 <head> 230 <title>{{.Name }} syzkaller</title> 231 {{HEAD}} 232 </head> 233 <body> 234 <b>{{.Name }} syzkaller</b> 235 <br> 236 <b>Workdir: {{.Workdir }}</b> 237 <br> 238 <b>Crash Count: {{ .CrashCount }}</b> 239 <br> 240 <b>Crash With Reproducers Count: {{ .CrashCountReproducers }}</b> 241 <br> 242 243 <table class="list_table"> 244 <caption>Crashes:</caption> 245 <tr> 246 <th><a onclick="return sortTable(this, 'Description', textSort)" href="#">Description</a></th> 247 <th><a onclick="return sortTable(this, 'Count', numSort)" href="#">Count</a></th> 248 <th><a onclick="return sortTable(this, 'Reproducers', lineSort)" href="#">Reproducers</a></th> 249 <th><a onclick="return sortTable(this, 'Causing_Commits', lineSort)" href="#">Causing_Commits</a></th> 250 <th><a onclick="return sortTable(this, 'Fixing_Commits', lineSort)" href="#">Fixing_Commits</a></th> 251 <th><a onclick="return sortTable(this, 'Causing_Configs', lineSort)" href="#">Causing_Configs</a></th> 252 </tr> 253 {{range $c := $.Crashes}} 254 <tr> 255 <td class="title">{{$c.Description}}</td> 256 <td class="stat">{{$c.Count}}</td> 257 <td class="reproducer"> 258 {{range $reproducer := $c.Reproducers}} 259 {{$reproducer}}</br> 260 {{end}} 261 </td> 262 <td class="Causing Commits"> 263 {{range $commit := $c.CausingCommits}} 264 {{$commit}}</br> 265 {{end}} 266 </td> 267 <td class="Fixing Commits"> 268 {{range $commit := $c.FixingCommits}} 269 {{$commit}}</br> 270 {{end}} 271 </td> 272 <td class="Causing Configs"> 273 {{range $config := $c.CausingConfigs}} 274 {{$config}}</br> 275 {{end}} 276 </td> 277 </tr> 278 {{end}} 279 </table> 280 281 </body></html> 282 `)