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  `)