github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/tools/syz-fix-analyzer/fix-analyzer.go (about)

     1  // Copyright 2024 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-fix-analyzer analyzes fixed bugs on the dashboard for automatic fixability and prints statistics.
     5  // Fixability implies a known bug type + a simple fix of a particular form.
     6  // For example, for a NULL-deref bug it may be addition of a "if (ptr == NULL) return" check.
     7  package main
     8  
     9  import (
    10  	"flag"
    11  	"fmt"
    12  	"regexp"
    13  	"runtime"
    14  	"strings"
    15  
    16  	"github.com/google/syzkaller/dashboard/api"
    17  	"github.com/google/syzkaller/pkg/tool"
    18  	"github.com/google/syzkaller/pkg/vcs"
    19  	"github.com/google/syzkaller/sys/targets"
    20  	"github.com/speakeasy-api/git-diff-parser"
    21  )
    22  
    23  func main() {
    24  	var (
    25  		flagDashboard = flag.String("dashboard", "https://syzkaller.appspot.com", "dashboard address")
    26  		flagNamespace = flag.String("namespace", "upstream", "target namespace")
    27  		flagToken     = flag.String("token", "", "auth token from 'gcloud auth print-access-token'")
    28  		flagSourceDir = flag.String("sourcedir", "", "fresh linux kernel checkout")
    29  	)
    30  	defer tool.Init()()
    31  	for _, typ := range bugTypes {
    32  		typ.Re = regexp.MustCompile(typ.Pattern)
    33  	}
    34  	cli := api.NewClient(*flagDashboard, *flagToken)
    35  	patches, perType, err := run(cli, *flagNamespace, *flagSourceDir)
    36  	if err != nil {
    37  		tool.Fail(err)
    38  	}
    39  	for _, typ := range bugTypes {
    40  		fmt.Printf("fixable %v:\n", typ.Type)
    41  		for _, bug := range perType[typ.Type].Fixable {
    42  			fmt.Printf("%v\t%v\n", bug.Title, bug.FixCommits[0].Link)
    43  		}
    44  		fmt.Printf("\n")
    45  	}
    46  	total, fixable := 0, 0
    47  	fmt.Printf("%-22v %-8v %v\n", "Type", "Total", "Fixable")
    48  	for _, typ := range bugTypes {
    49  		ti := perType[typ.Type]
    50  		total += ti.Total
    51  		fixable += len(ti.Fixable)
    52  		fmt.Printf("%-22v %-8v %-4v (%.2f%%)\n",
    53  			typ.Type, ti.Total, len(ti.Fixable), percent(len(ti.Fixable), ti.Total))
    54  	}
    55  	fmt.Printf("---\n")
    56  	fmt.Printf("%-22v %-8v %-4v (%.2f%%)\n",
    57  		"classified", total, fixable, percent(fixable, total))
    58  	fmt.Printf("%-22v %-8v %-4v (%.2f%%)\n",
    59  		"total", patches, fixable, percent(fixable, patches))
    60  }
    61  
    62  type Job struct {
    63  	bug     api.BugSummary
    64  	repo    vcs.Repo
    65  	typ     BugType
    66  	fixable bool
    67  	err     error
    68  	done    chan struct{}
    69  }
    70  
    71  func run(cli *api.Client, ns, sourceDir string) (int, map[BugType]TypeStats, error) {
    72  	repo, err := vcs.NewRepo(targets.Linux, "", sourceDir, vcs.OptPrecious, vcs.OptDontSandbox)
    73  	if err != nil {
    74  		return 0, nil, err
    75  	}
    76  	bugs, err := cli.BugGroups(ns, api.BugGroupFixed)
    77  	if err != nil {
    78  		return 0, nil, err
    79  	}
    80  	jobs := runJobs(bugs, repo)
    81  	patches := make(map[string]bool)
    82  	perType := make(map[BugType]TypeStats)
    83  	for _, job := range jobs {
    84  		<-job.done
    85  		if job.err != nil {
    86  			return 0, nil, job.err
    87  		}
    88  		com := job.bug.FixCommits[0].Hash
    89  		// For now we consider only the first bug for this commit.
    90  		// Potentially we can consider all bugs for this commit,
    91  		// and check if at least one of them is fixable.
    92  		if com == "" || patches[com] {
    93  			continue
    94  		}
    95  		patches[com] = true
    96  		if job.typ == "" {
    97  			continue
    98  		}
    99  		ti := perType[job.typ]
   100  		ti.Total++
   101  		if job.fixable {
   102  			ti.Fixable = append(ti.Fixable, job.bug)
   103  		}
   104  		perType[job.typ] = ti
   105  	}
   106  	return len(patches), perType, nil
   107  }
   108  
   109  func runJobs(bugs []api.BugSummary, repo vcs.Repo) []*Job {
   110  	procs := runtime.GOMAXPROCS(0)
   111  	jobC := make(chan *Job, procs)
   112  	for p := 0; p < procs; p++ {
   113  		go func() {
   114  			for job := range jobC {
   115  				typ, fixable, err := isFixable(job.bug, job.repo)
   116  				job.typ, job.fixable, job.err = typ, fixable, err
   117  				close(job.done)
   118  			}
   119  		}()
   120  	}
   121  	var jobs []*Job
   122  	for _, bug := range bugs {
   123  		job := &Job{
   124  			bug:  bug,
   125  			repo: repo,
   126  			done: make(chan struct{}),
   127  		}
   128  		jobC <- job
   129  		jobs = append(jobs, job)
   130  	}
   131  	return jobs
   132  }
   133  
   134  func isFixable(bug api.BugSummary, repo vcs.Repo) (BugType, bool, error) {
   135  	// TODO: check that we can infer the file that needs to be fixed
   136  	// (matches the guilty frame in the bug report).
   137  
   138  	// TODO: For now we only look at one crash that the dashboard exports.
   139  	// There can be multiple (KASAN+KMSAN+paging fault),
   140  	// we could check if at least one of them is fixable.
   141  
   142  	if len(bug.FixCommits) == 0 {
   143  		return "", false, nil
   144  	}
   145  	var typ BugType
   146  	for _, t := range bugTypes {
   147  		if t.Re.MatchString(bug.Title) {
   148  			typ = t.Type
   149  			break
   150  		}
   151  	}
   152  	comHash := bug.FixCommits[0].Hash
   153  	if typ == "" || comHash == "" {
   154  		return "", false, nil
   155  	}
   156  	com, err := repo.Commit(comHash)
   157  	if err != nil {
   158  		return "", false, err
   159  	}
   160  	diff, errs := git_diff_parser.Parse(string(com.Patch))
   161  	if len(errs) != 0 {
   162  		return "", false, fmt.Errorf("parsing patch: %v", errs)
   163  	}
   164  	if len(diff.FileDiff) != 1 {
   165  		return typ, false, nil
   166  	}
   167  	file := diff.FileDiff[0]
   168  	if file.IsBinary || file.FromFile != file.ToFile ||
   169  		!strings.HasSuffix(file.FromFile, ".c") && !strings.HasSuffix(file.FromFile, ".h") {
   170  		return typ, false, nil
   171  	}
   172  	if len(file.Hunks) != 1 {
   173  		return typ, false, nil
   174  	}
   175  	// TODO: check that the patch matches our expected form for this bug type
   176  	// (e.g. adds if+return/continue, etc).
   177  	return typ, true, nil
   178  }
   179  
   180  type BugType string
   181  
   182  type BugMeta struct {
   183  	Type    BugType
   184  	Pattern string
   185  	Re      *regexp.Regexp
   186  }
   187  
   188  type TypeStats struct {
   189  	Total   int
   190  	Fixable []api.BugSummary
   191  }
   192  
   193  var bugTypes = []*BugMeta{
   194  	{
   195  		Type: "NULL deref",
   196  		// TODO: check that a GPF is in fact a NULL deref.
   197  		Pattern: `BUG: unable to handle kernel NULL pointer dereference|KASAN: null-ptr-deref|general protection fault`,
   198  	},
   199  	{
   200  		Type: "locking rules",
   201  		Pattern: `BUG: sleeping function called from invalid context|WARNING: suspicious RCU usage|` +
   202  			`suspicious RCU usage at|inconsistent lock state|INFO: trying to register non-static key`,
   203  	},
   204  	{
   205  		Type:    "double-free",
   206  		Pattern: `KASAN: double-free or invalid-free|KASAN: invalid-free`,
   207  	},
   208  	{
   209  		Type:    "out-of-bounds",
   210  		Pattern: `KASAN: .*out-of-bounds|UBSAN: array-index-out-of-bounds`,
   211  	},
   212  	{
   213  		Type:    "use-after-free",
   214  		Pattern: `(KASAN|KMSAN): .*use-after-free`,
   215  	},
   216  	{
   217  		Type:    "data-race",
   218  		Pattern: `KCSAN: data-race`,
   219  	},
   220  	{
   221  		Type:    "shift-out-of-bounds",
   222  		Pattern: `UBSAN: shift-out-of-bounds`,
   223  	},
   224  	{
   225  		Type:    "uninit",
   226  		Pattern: `KMSAN:`,
   227  	},
   228  	{
   229  		Type:    "deadlock",
   230  		Pattern: `deadlock`,
   231  	},
   232  	{
   233  		Type:    "memory leak",
   234  		Pattern: `memory leak in`,
   235  	},
   236  	{
   237  		Type:    "BUG/WARN",
   238  		Pattern: `BUG:|WARNING:`,
   239  	},
   240  }
   241  
   242  func percent(a, b int) float64 {
   243  	return float64(a) / float64(b) * 100
   244  }