github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/manager/crash.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 package manager 5 6 import ( 7 "fmt" 8 "io" 9 "os" 10 "path/filepath" 11 "sort" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/google/syzkaller/pkg/hash" 17 "github.com/google/syzkaller/pkg/log" 18 "github.com/google/syzkaller/pkg/mgrconfig" 19 "github.com/google/syzkaller/pkg/osutil" 20 "github.com/google/syzkaller/pkg/report" 21 "github.com/google/syzkaller/prog" 22 ) 23 24 type CrashStore struct { 25 Tag string 26 BaseDir string 27 MaxCrashLogs int 28 MaxReproLogs int 29 } 30 31 const reproFileName = "repro.prog" 32 const cReproFileName = "repro.cprog" 33 const straceFileName = "strace.log" 34 35 const MaxReproAttempts = 3 36 37 func NewCrashStore(cfg *mgrconfig.Config) *CrashStore { 38 return &CrashStore{ 39 Tag: cfg.Tag, 40 BaseDir: cfg.Workdir, 41 MaxCrashLogs: cfg.MaxCrashLogs, 42 MaxReproLogs: MaxReproAttempts, 43 } 44 } 45 46 func ReadCrashStore(workdir string) *CrashStore { 47 return &CrashStore{ 48 BaseDir: workdir, 49 } 50 } 51 52 // Returns whether it was the first crash of a kind. 53 func (cs *CrashStore) SaveCrash(crash *Crash) (bool, error) { 54 dir := cs.path(crash.Title) 55 osutil.MkdirAll(dir) 56 57 err := osutil.WriteFile(filepath.Join(dir, "description"), []byte(crash.Title+"\n")) 58 if err != nil { 59 return false, fmt.Errorf("failed to write crash: %w", err) 60 } 61 62 // Save up to cs.cfg.MaxCrashLogs reports, overwrite the oldest once we've reached that number. 63 // Newer reports are generally more useful. Overwriting is also needed 64 // to be able to understand if a particular bug still happens or already fixed. 65 oldestI, first := 0, false 66 var oldestTime time.Time 67 for i := 0; i < cs.MaxCrashLogs; i++ { 68 info, err := os.Stat(filepath.Join(dir, fmt.Sprintf("log%v", i))) 69 if err != nil { 70 oldestI = i 71 if i == 0 { 72 first = true 73 } 74 break 75 } 76 if oldestTime.IsZero() || info.ModTime().Before(oldestTime) { 77 oldestI = i 78 oldestTime = info.ModTime() 79 } 80 } 81 writeOrRemove := func(name string, data []byte) { 82 filename := filepath.Join(dir, name+fmt.Sprint(oldestI)) 83 if len(data) == 0 { 84 os.Remove(filename) 85 return 86 } 87 osutil.WriteFile(filename, data) 88 } 89 reps := append([]*report.Report{crash.Report}, crash.TailReports...) 90 writeOrRemove("log", crash.Output) 91 writeOrRemove("tag", []byte(cs.Tag)) 92 writeOrRemove("report", report.MergeReportBytes(reps)) 93 writeOrRemove("machineInfo", crash.MachineInfo) 94 if err := report.AddTitleStat(filepath.Join(dir, "title-stat"), reps); err != nil { 95 return false, fmt.Errorf("report.AddTitleStat: %w", err) 96 } 97 98 return first, nil 99 } 100 101 func (cs *CrashStore) HasRepro(title string) bool { 102 return osutil.IsExist(filepath.Join(cs.path(title), reproFileName)) 103 } 104 105 func (cs *CrashStore) MoreReproAttempts(title string) bool { 106 dir := cs.path(title) 107 for i := 0; i < cs.MaxReproLogs; i++ { 108 if !osutil.IsExist(filepath.Join(dir, fmt.Sprintf("repro%v", i))) { 109 return true 110 } 111 } 112 return false 113 } 114 115 func (cs *CrashStore) SaveFailedRepro(title string, log []byte) error { 116 dir := cs.path(title) 117 osutil.MkdirAll(dir) 118 for i := 0; i < cs.MaxReproLogs; i++ { 119 name := filepath.Join(dir, fmt.Sprintf("repro%v", i)) 120 if !osutil.IsExist(name) && len(log) > 0 { 121 err := osutil.WriteFile(name, log) 122 if err != nil { 123 return err 124 } 125 break 126 } 127 } 128 return nil 129 } 130 131 func (cs *CrashStore) SaveRepro(res *ReproResult, progText, cProgText []byte) error { 132 repro := res.Repro 133 rep := repro.Report 134 dir := cs.path(rep.Title) 135 osutil.MkdirAll(dir) 136 137 err := osutil.WriteFile(filepath.Join(dir, "description"), []byte(rep.Title+"\n")) 138 if err != nil { 139 return fmt.Errorf("failed to write crash: %w", err) 140 } 141 // TODO: detect and handle errors below as well. 142 osutil.WriteFile(filepath.Join(dir, reproFileName), progText) 143 if cs.Tag != "" { 144 osutil.WriteFile(filepath.Join(dir, "repro.tag"), []byte(cs.Tag)) 145 } 146 if len(rep.Output) > 0 { 147 osutil.WriteFile(filepath.Join(dir, "repro.log"), rep.Output) 148 } 149 if len(rep.Report) > 0 { 150 osutil.WriteFile(filepath.Join(dir, "repro.report"), rep.Report) 151 } 152 if len(cProgText) > 0 { 153 osutil.WriteFile(filepath.Join(dir, cReproFileName), cProgText) 154 } 155 var assetErr error 156 repro.Prog.ForEachAsset(func(name string, typ prog.AssetType, r io.Reader, c *prog.Call) { 157 fileName := filepath.Join(dir, name+".gz") 158 if err := osutil.WriteGzipStream(fileName, r); err != nil { 159 assetErr = fmt.Errorf("failed to write crash asset: type %d, %w", typ, err) 160 } 161 }) 162 if assetErr != nil { 163 return assetErr 164 } 165 if res.Strace != nil { 166 // Unlike dashboard reporting, we save strace output separately from the original log. 167 if res.Strace.Error != nil { 168 osutil.WriteFile(filepath.Join(dir, "strace.error"), 169 []byte(fmt.Sprintf("%v", res.Strace.Error))) 170 } 171 if len(res.Strace.Output) > 0 { 172 osutil.WriteFile(filepath.Join(dir, straceFileName), res.Strace.Output) 173 } 174 } 175 if reproLog := res.Stats.FullLog(); len(reproLog) > 0 { 176 osutil.WriteFile(filepath.Join(dir, "repro.stats"), reproLog) 177 } 178 return nil 179 } 180 181 type BugReport struct { 182 Title string 183 Tag string 184 Prog []byte 185 CProg []byte 186 Report []byte 187 } 188 189 func (cs *CrashStore) Report(id string) (*BugReport, error) { 190 dir := filepath.Join(cs.BaseDir, "crashes", id) 191 desc, err := os.ReadFile(filepath.Join(dir, "description")) 192 if err != nil { 193 return nil, err 194 } 195 tag, _ := os.ReadFile(filepath.Join(dir, "repro.tag")) 196 ret := &BugReport{ 197 Title: strings.TrimSpace(string(desc)), 198 Tag: strings.TrimSpace(string(tag)), 199 } 200 ret.Prog, _ = os.ReadFile(filepath.Join(dir, reproFileName)) 201 ret.CProg, _ = os.ReadFile(filepath.Join(dir, cReproFileName)) 202 ret.Report, _ = os.ReadFile(filepath.Join(dir, "repro.report")) 203 return ret, nil 204 } 205 206 type CrashInfo struct { 207 Index int 208 Log string // filename relative to the workdir 209 210 // These fields are only set if full=true. 211 Tag string 212 Report string // filename relative to workdir 213 Time time.Time 214 } 215 216 type BugInfo struct { 217 ID string 218 Title string 219 TailTitles []*report.TitleFreqRank 220 FirstTime time.Time 221 LastTime time.Time 222 HasRepro bool 223 HasCRepro bool 224 StraceFile string // relative to the workdir 225 ReproAttempts int 226 Crashes []*CrashInfo 227 Rank int 228 } 229 230 func (cs *CrashStore) BugInfo(id string, full bool) (*BugInfo, error) { 231 dir := filepath.Join(cs.BaseDir, "crashes", id) 232 233 ret := &BugInfo{ID: id} 234 desc, err := os.ReadFile(filepath.Join(dir, "description")) 235 if err != nil { 236 return nil, err 237 } 238 stat, err := os.Stat(filepath.Join(dir, "description")) 239 if err != nil { 240 return nil, err 241 } 242 ret.Title = strings.TrimSpace(string(desc)) 243 244 // Bug rank may go up over time if we observe higher ranked bugs as a consequence of the first failure. 245 ret.Rank = report.TitlesToImpact(ret.Title) 246 if titleStat, err := report.ReadStatFile(filepath.Join(dir, "title-stat")); err == nil { 247 ret.TailTitles = report.ExplainTitleStat(titleStat) 248 for _, ti := range ret.TailTitles { 249 ret.Rank = max(ret.Rank, ti.Rank) 250 } 251 } 252 253 ret.FirstTime = osutil.CreationTime(stat) 254 ret.LastTime = stat.ModTime() 255 files, err := osutil.ListDir(dir) 256 if err != nil { 257 return nil, err 258 } 259 for _, f := range files { 260 if strings.HasPrefix(f, "log") { 261 index, err := strconv.ParseUint(f[3:], 10, 64) 262 if err == nil { 263 ret.Crashes = append(ret.Crashes, &CrashInfo{ 264 Index: int(index), 265 Log: filepath.Join("crashes", id, f), 266 }) 267 } 268 } else if f == reproFileName { 269 ret.HasRepro = true 270 } else if f == cReproFileName { 271 ret.HasCRepro = true 272 } else if f == straceFileName { 273 ret.StraceFile = filepath.Join(dir, f) 274 } else if strings.HasPrefix(f, "repro") { 275 ret.ReproAttempts++ 276 } 277 } 278 if !full { 279 return ret, nil 280 } 281 for _, crash := range ret.Crashes { 282 if stat, err := os.Stat(filepath.Join(cs.BaseDir, crash.Log)); err == nil { 283 crash.Time = stat.ModTime() 284 } 285 tag, _ := os.ReadFile(filepath.Join(dir, fmt.Sprintf("tag%d", crash.Index))) 286 crash.Tag = string(tag) 287 reportFile := filepath.Join("crashes", id, fmt.Sprintf("report%d", crash.Index)) 288 if osutil.IsExist(filepath.Join(cs.BaseDir, reportFile)) { 289 crash.Report = reportFile 290 } 291 } 292 sort.Slice(ret.Crashes, func(i, j int) bool { 293 return ret.Crashes[i].Time.After(ret.Crashes[j].Time) 294 }) 295 return ret, nil 296 } 297 298 func (cs *CrashStore) BugList() ([]*BugInfo, error) { 299 dirs, err := osutil.ListDir(filepath.Join(cs.BaseDir, "crashes")) 300 if err != nil { 301 if os.IsNotExist(err) { 302 // If there were no crashes, it's okay that there's no such folder. 303 return nil, nil 304 } 305 return nil, err 306 } 307 var ret []*BugInfo 308 var lastErr error 309 errCount := 0 310 for _, dir := range dirs { 311 info, err := cs.BugInfo(dir, false) 312 if err != nil { 313 errCount++ 314 lastErr = err 315 continue 316 } 317 ret = append(ret, info) 318 } 319 sort.Slice(ret, func(i, j int) bool { 320 return strings.ToLower(ret[i].Title) < strings.ToLower(ret[j].Title) 321 }) 322 if lastErr != nil { 323 log.Logf(0, "some stored crashes are inconsistent: %d skipped, last error %v", errCount, lastErr) 324 } 325 return ret, nil 326 } 327 328 func crashHash(title string) string { 329 sig := hash.Hash([]byte(title)) 330 return sig.String() 331 } 332 333 func (cs *CrashStore) path(title string) string { 334 return filepath.Join(cs.BaseDir, "crashes", crashHash(title)) 335 }