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 }