aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksandr Nogikh <nogikh@google.com>2024-10-03 17:43:17 +0200
committerAleksandr Nogikh <nogikh@google.com>2024-10-11 11:58:06 +0000
commitd041766f6c33a373e0064b832cc4e7a4401b3659 (patch)
treeba008da309760c5e88859ada245225d01769c15c
parent5e7b4bcaa61e8bb9b1d1fbca21684fe490f69133 (diff)
pkg/manager: factor out the crash storage functionality
It reduces the size of the syz-manager/ code and makes it testable. Use it in syz-testbed.
-rw-r--r--pkg/manager/crash.go304
-rw-r--r--pkg/manager/crash_test.go106
-rw-r--r--tools/syz-reporter/reporter.go1
-rw-r--r--tools/syz-testbed/stats.go32
4 files changed, 418 insertions, 25 deletions
diff --git a/pkg/manager/crash.go b/pkg/manager/crash.go
new file mode 100644
index 000000000..9be16e58e
--- /dev/null
+++ b/pkg/manager/crash.go
@@ -0,0 +1,304 @@
+// Copyright 2024 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package manager
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/google/syzkaller/pkg/hash"
+ "github.com/google/syzkaller/pkg/mgrconfig"
+ "github.com/google/syzkaller/pkg/osutil"
+ "github.com/google/syzkaller/prog"
+)
+
+type CrashStore struct {
+ Tag string
+ BaseDir string
+ MaxCrashLogs int
+ MaxReproLogs int
+}
+
+const reproFileName = "repro.prog"
+const cReproFileName = "repro.cprog"
+const straceFileName = "strace.log"
+
+const MaxReproAttempts = 3
+
+func NewCrashStore(cfg *mgrconfig.Config) *CrashStore {
+ return &CrashStore{
+ Tag: cfg.Tag,
+ BaseDir: cfg.Workdir,
+ MaxCrashLogs: cfg.MaxCrashLogs,
+ MaxReproLogs: MaxReproAttempts,
+ }
+}
+
+func ReadCrashStore(workdir string) *CrashStore {
+ return &CrashStore{
+ BaseDir: workdir,
+ }
+}
+
+// Returns whether it was the first crash of a kind.
+func (cs *CrashStore) SaveCrash(crash *Crash) (bool, error) {
+ dir := cs.path(crash.Title)
+ osutil.MkdirAll(dir)
+
+ err := osutil.WriteFile(filepath.Join(dir, "description"), []byte(crash.Title+"\n"))
+ if err != nil {
+ return false, fmt.Errorf("failed to write crash: %w", err)
+ }
+
+ // Save up to cs.cfg.MaxCrashLogs reports, overwrite the oldest once we've reached that number.
+ // Newer reports are generally more useful. Overwriting is also needed
+ // to be able to understand if a particular bug still happens or already fixed.
+ oldestI, first := 0, false
+ var oldestTime time.Time
+ for i := 0; i < cs.MaxCrashLogs; i++ {
+ info, err := os.Stat(filepath.Join(dir, fmt.Sprintf("log%v", i)))
+ if err != nil {
+ oldestI = i
+ if i == 0 {
+ first = true
+ }
+ break
+ }
+ if oldestTime.IsZero() || info.ModTime().Before(oldestTime) {
+ oldestI = i
+ oldestTime = info.ModTime()
+ }
+ }
+ writeOrRemove := func(name string, data []byte) {
+ filename := filepath.Join(dir, name+fmt.Sprint(oldestI))
+ if len(data) == 0 {
+ os.Remove(filename)
+ return
+ }
+ osutil.WriteFile(filename, data)
+ }
+ writeOrRemove("log", crash.Output)
+ writeOrRemove("tag", []byte(cs.Tag))
+ writeOrRemove("report", crash.Report.Report)
+ writeOrRemove("machineInfo", crash.MachineInfo)
+
+ return first, nil
+}
+
+func (cs *CrashStore) HasRepro(title string) bool {
+ return osutil.IsExist(filepath.Join(cs.path(title), reproFileName))
+}
+
+func (cs *CrashStore) MoreReproAttempts(title string) bool {
+ dir := cs.path(title)
+ for i := 0; i < cs.MaxReproLogs; i++ {
+ if !osutil.IsExist(filepath.Join(dir, fmt.Sprintf("repro%v", i))) {
+ return true
+ }
+ }
+ return false
+}
+
+func (cs *CrashStore) SaveFailedRepro(title string, log []byte) error {
+ dir := cs.path(title)
+ osutil.MkdirAll(dir)
+ for i := 0; i < cs.MaxReproLogs; i++ {
+ name := filepath.Join(dir, fmt.Sprintf("repro%v", i))
+ if !osutil.IsExist(name) && len(log) > 0 {
+ err := osutil.WriteFile(name, log)
+ if err != nil {
+ return err
+ }
+ break
+ }
+ }
+ return nil
+}
+
+func (cs *CrashStore) SaveRepro(res *ReproResult, progText, cProgText []byte) error {
+ repro := res.Repro
+ rep := repro.Report
+ dir := cs.path(rep.Title)
+ osutil.MkdirAll(dir)
+
+ err := osutil.WriteFile(filepath.Join(dir, "description"), []byte(rep.Title+"\n"))
+ if err != nil {
+ return fmt.Errorf("failed to write crash: %w", err)
+ }
+ // TODO: detect and handle errors below as well.
+ osutil.WriteFile(filepath.Join(dir, reproFileName), progText)
+ if cs.Tag != "" {
+ osutil.WriteFile(filepath.Join(dir, "repro.tag"), []byte(cs.Tag))
+ }
+ if len(rep.Output) > 0 {
+ osutil.WriteFile(filepath.Join(dir, "repro.log"), rep.Output)
+ }
+ if len(rep.Report) > 0 {
+ osutil.WriteFile(filepath.Join(dir, "repro.report"), rep.Report)
+ }
+ if len(cProgText) > 0 {
+ osutil.WriteFile(filepath.Join(dir, cReproFileName), cProgText)
+ }
+ var assetErr error
+ repro.Prog.ForEachAsset(func(name string, typ prog.AssetType, r io.Reader) {
+ fileName := filepath.Join(dir, name+".gz")
+ if err := osutil.WriteGzipStream(fileName, r); err != nil {
+ assetErr = fmt.Errorf("failed to write crash asset: type %d, %w", typ, err)
+ }
+ })
+ if assetErr != nil {
+ return assetErr
+ }
+ if res.Strace != nil {
+ // Unlike dashboard reporting, we save strace output separately from the original log.
+ if res.Strace.Error != nil {
+ osutil.WriteFile(filepath.Join(dir, "strace.error"),
+ []byte(fmt.Sprintf("%v", res.Strace.Error)))
+ }
+ if len(res.Strace.Output) > 0 {
+ osutil.WriteFile(filepath.Join(dir, straceFileName), res.Strace.Output)
+ }
+ }
+ if reproLog := res.Stats.FullLog(); len(reproLog) > 0 {
+ osutil.WriteFile(filepath.Join(dir, "repro.stats"), reproLog)
+ }
+ return nil
+}
+
+type BugReport struct {
+ Title string
+ Tag string
+ Prog []byte
+ CProg []byte
+ Report []byte
+}
+
+func (cs *CrashStore) Report(id string) (*BugReport, error) {
+ dir := filepath.Join(cs.BaseDir, "crashes", id)
+ desc, err := os.ReadFile(filepath.Join(dir, "description"))
+ if err != nil {
+ return nil, err
+ }
+ tag, _ := os.ReadFile(filepath.Join(dir, "repro.tag"))
+ ret := &BugReport{
+ Title: strings.TrimSpace(string(desc)),
+ Tag: strings.TrimSpace(string(tag)),
+ }
+ ret.Prog, _ = os.ReadFile(filepath.Join(dir, reproFileName))
+ ret.CProg, _ = os.ReadFile(filepath.Join(dir, cReproFileName))
+ ret.Report, _ = os.ReadFile(filepath.Join(dir, "repro.report"))
+ return ret, nil
+}
+
+type CrashInfo struct {
+ Index int
+ Log string // filename relative to the workdir
+
+ // These fields are only set if full=true.
+ Tag string
+ Report string // filename relative to workdir
+ Time time.Time
+}
+
+type BugInfo struct {
+ ID string
+ Title string
+ LastTime time.Time
+ HasRepro bool
+ HasCRepro bool
+ StraceFile string // relative to the workdir
+ ReproAttempts int
+ Crashes []*CrashInfo
+}
+
+func (cs *CrashStore) BugInfo(id string, full bool) (*BugInfo, error) {
+ dir := filepath.Join(cs.BaseDir, "crashes", id)
+
+ ret := &BugInfo{ID: id}
+ desc, err := os.ReadFile(filepath.Join(dir, "description"))
+ if err != nil {
+ return nil, err
+ }
+ stat, err := os.Stat(filepath.Join(dir, "description"))
+ if err != nil {
+ return nil, err
+ }
+ ret.Title = strings.TrimSpace(string(desc))
+ ret.LastTime = stat.ModTime()
+ files, err := osutil.ListDir(dir)
+ if err != nil {
+ return nil, err
+ }
+ for _, f := range files {
+ if strings.HasPrefix(f, "log") {
+ index, err := strconv.ParseUint(f[3:], 10, 64)
+ if err == nil {
+ ret.Crashes = append(ret.Crashes, &CrashInfo{
+ Index: int(index),
+ Log: filepath.Join("crashes", id, f),
+ })
+ }
+ } else if f == reproFileName {
+ ret.HasRepro = true
+ } else if f == cReproFileName {
+ ret.HasCRepro = true
+ } else if f == straceFileName {
+ ret.StraceFile = filepath.Join(dir, f)
+ } else if strings.HasPrefix(f, "repro") {
+ ret.ReproAttempts++
+ }
+ }
+ if !full {
+ return ret, nil
+ }
+ for _, crash := range ret.Crashes {
+ if stat, err := os.Stat(filepath.Join(cs.BaseDir, crash.Log)); err == nil {
+ crash.Time = stat.ModTime()
+ }
+ tag, _ := os.ReadFile(filepath.Join(dir, fmt.Sprintf("tag%d", crash.Index)))
+ crash.Tag = string(tag)
+ reportFile := filepath.Join("crashes", id, fmt.Sprintf("report%d", crash.Index))
+ if osutil.IsExist(filepath.Join(cs.BaseDir, reportFile)) {
+ crash.Report = reportFile
+ }
+ }
+ sort.Slice(ret.Crashes, func(i, j int) bool {
+ return ret.Crashes[i].Time.After(ret.Crashes[j].Time)
+ })
+ return ret, nil
+}
+
+func (cs *CrashStore) BugList() ([]*BugInfo, error) {
+ dirs, err := osutil.ListDir(filepath.Join(cs.BaseDir, "crashes"))
+ if err != nil {
+ return nil, err
+ }
+ var ret []*BugInfo
+ for _, dir := range dirs {
+ info, err := cs.BugInfo(dir, false)
+ if err != nil {
+ return nil, fmt.Errorf("failed to process crashes/%s", dir)
+ }
+ ret = append(ret, info)
+ }
+ sort.Slice(ret, func(i, j int) bool {
+ return strings.ToLower(ret[i].Title) < strings.ToLower(ret[j].Title)
+ })
+ return ret, nil
+}
+
+func (cs *CrashStore) titleToID(title string) string {
+ sig := hash.Hash([]byte(title))
+ return sig.String()
+}
+
+func (cs *CrashStore) path(title string) string {
+ return filepath.Join(cs.BaseDir, "crashes", cs.titleToID(title))
+}
diff --git a/pkg/manager/crash_test.go b/pkg/manager/crash_test.go
new file mode 100644
index 000000000..cce74284a
--- /dev/null
+++ b/pkg/manager/crash_test.go
@@ -0,0 +1,106 @@
+// Copyright 2024 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package manager
+
+import (
+ "testing"
+
+ "github.com/google/syzkaller/pkg/report"
+ "github.com/google/syzkaller/pkg/repro"
+ "github.com/google/syzkaller/prog"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCrashList(t *testing.T) {
+ crashStore := &CrashStore{
+ BaseDir: t.TempDir(),
+ MaxCrashLogs: 10,
+ }
+
+ first, err := crashStore.SaveCrash(&Crash{Report: &report.Report{
+ Title: "Title A",
+ Output: []byte("ABCD"),
+ }})
+ assert.NoError(t, err)
+ assert.True(t, first)
+ for i := 0; i < 2; i++ {
+ first, err := crashStore.SaveCrash(&Crash{Report: &report.Report{
+ Title: "Title B",
+ Output: []byte("ABCD"),
+ }})
+ assert.NoError(t, err)
+ assert.Equal(t, i == 0, first)
+ }
+ for i := 0; i < 3; i++ {
+ first, err := crashStore.SaveCrash(&Crash{Report: &report.Report{
+ Title: "Title C",
+ Output: []byte("ABCD"),
+ }})
+ assert.NoError(t, err)
+ assert.Equal(t, i == 0, first)
+ }
+
+ list, err := crashStore.BugList()
+ assert.NoError(t, err)
+ assert.Len(t, list, 3)
+
+ assert.Equal(t, "Title A", list[0].Title)
+ assert.Len(t, list[0].Crashes, 1)
+ assert.Equal(t, "Title B", list[1].Title)
+ assert.Len(t, list[1].Crashes, 2)
+ assert.Equal(t, "Title C", list[2].Title)
+ assert.Len(t, list[2].Crashes, 3)
+}
+
+func TestMaxCrashLogs(t *testing.T) {
+ crashStore := &CrashStore{
+ BaseDir: t.TempDir(),
+ MaxCrashLogs: 5,
+ }
+
+ for i := 0; i < 20; i++ {
+ _, err := crashStore.SaveCrash(&Crash{Report: &report.Report{
+ Title: "Title A",
+ Output: []byte("ABCD"),
+ }})
+ assert.NoError(t, err)
+ }
+
+ info, err := crashStore.BugInfo(crashStore.titleToID("Title A"), false)
+ assert.NoError(t, err)
+ assert.Len(t, info.Crashes, 5)
+}
+
+func TestCrashRepro(t *testing.T) {
+ crashStore := &CrashStore{
+ Tag: "abcd",
+ BaseDir: t.TempDir(),
+ MaxCrashLogs: 5,
+ }
+
+ _, err := crashStore.SaveCrash(&Crash{Report: &report.Report{
+ Title: "Some title",
+ Output: []byte("Some output"),
+ }})
+ assert.NoError(t, err)
+
+ err = crashStore.SaveRepro(&ReproResult{
+ Repro: &repro.Result{
+ Report: &report.Report{
+ Title: "Some title",
+ Report: []byte("Some report"),
+ },
+ Prog: &prog.Prog{},
+ },
+ }, []byte("prog text"), []byte("c prog text"))
+ assert.NoError(t, err)
+
+ report, err := crashStore.Report(crashStore.titleToID("Some title"))
+ assert.NoError(t, err)
+ assert.Equal(t, "Some title", report.Title)
+ assert.Equal(t, "abcd", report.Tag)
+ assert.Equal(t, []byte("prog text"), report.Prog)
+ assert.Equal(t, []byte("c prog text"), report.CProg)
+ assert.Equal(t, []byte("Some report"), report.Report)
+}
diff --git a/tools/syz-reporter/reporter.go b/tools/syz-reporter/reporter.go
index 6fa4adc47..53ccce4c3 100644
--- a/tools/syz-reporter/reporter.go
+++ b/tools/syz-reporter/reporter.go
@@ -129,6 +129,7 @@ func collectCrashes(workdir string) ([]*UICrashType, error) {
return crashTypes, nil
}
+// TODO: reuse manager.CrashStore.
func readCrash(workdir, dir string) *UICrashType {
if len(dir) != 40 {
return nil
diff --git a/tools/syz-testbed/stats.go b/tools/syz-testbed/stats.go
index 185156a5d..6044bfe3d 100644
--- a/tools/syz-testbed/stats.go
+++ b/tools/syz-testbed/stats.go
@@ -8,10 +8,9 @@ import (
"fmt"
"os"
"path/filepath"
- "strings"
"time"
- "github.com/google/syzkaller/pkg/osutil"
+ "github.com/google/syzkaller/pkg/manager"
"github.com/google/syzkaller/pkg/stat/sample"
)
@@ -51,33 +50,16 @@ type StatView struct {
Groups []RunResultGroup
}
-// TODO: we're implementing this functionaity at least the 3rd time (see syz-manager/html
-// and tools/reporter). Create a more generic implementation and put it into a globally
-// visible package.
func collectBugs(workdir string) ([]BugInfo, error) {
- crashdir := filepath.Join(workdir, "crashes")
- dirs, err := osutil.ListDir(crashdir)
+ list, err := manager.ReadCrashStore(workdir).BugList()
if err != nil {
return nil, err
}
- bugs := []BugInfo{}
- for _, dir := range dirs {
- bugFolder := filepath.Join(crashdir, dir)
- titleBytes, err := os.ReadFile(filepath.Join(bugFolder, "description"))
- if err != nil {
- return nil, err
- }
- bug := BugInfo{
- Title: strings.TrimSpace(string(titleBytes)),
- }
- files, err := os.ReadDir(bugFolder)
- if err != nil {
- return nil, err
- }
- for _, f := range files {
- if strings.HasPrefix(f.Name(), "log") {
- bug.Logs = append(bug.Logs, filepath.Join(bugFolder, f.Name()))
- }
+ var bugs []BugInfo
+ for _, info := range list {
+ bug := BugInfo{Title: info.Title}
+ for _, crash := range info.Crashes {
+ bug.Logs = append(bug.Logs, filepath.Join(workdir, crash.Log))
}
bugs = append(bugs, bug)
}