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