diff --git a/dashboard/app/api.go b/dashboard/app/api.go
index 49c524c1464b..3bffab48dfff 100644
--- a/dashboard/app/api.go
+++ b/dashboard/app/api.go
@@ -916,6 +916,9 @@ func reportCrash(c context.Context, build *Build, req *dashapi.Crash) (*Bug, err
bug.SetAutoSubsystems(c, newSubsystems, now, subsystemService.Revision)
}
bug.increaseCrashStats(now)
+ if err = bug.addTitleStat(req.TailTitles); err != nil {
+ return fmt.Errorf("failed to add title stat: %w", err)
+ }
bug.HappenedOn = mergeString(bug.HappenedOn, build.Manager)
// Migration of older entities (for new bugs Title is always in MergedTitles).
bug.MergedTitles = mergeString(bug.MergedTitles, bug.Title)
@@ -975,10 +978,11 @@ func (crash *Crash) UpdateReportingPriority(c context.Context, build *Build, bug
func saveCrash(c context.Context, ns string, req *dashapi.Crash, bug *Bug, bugKey *db.Key,
build *Build, assets []Asset) error {
crash := &Crash{
- Title: req.Title,
- Manager: build.Manager,
- BuildID: req.BuildID,
- Time: timeNow(c),
+ Title: req.Title,
+ TailTitles: req.TailTitles,
+ Manager: build.Manager,
+ BuildID: req.BuildID,
+ Time: timeNow(c),
Maintainers: email.MergeEmailLists(req.Maintainers,
GetEmails(req.Recipients, dashapi.To),
GetEmails(req.Recipients, dashapi.Cc)),
@@ -996,6 +1000,13 @@ func saveCrash(c context.Context, ns string, req *dashapi.Crash, bug *Bug, bugKe
if crash.Report, err = putText(c, ns, textCrashReport, req.Report); err != nil {
return err
}
+ for _, tailReport := range req.TailReports {
+ tailReportID, err := putText(c, ns, textCrashReport, tailReport)
+ if err != nil {
+ return err
+ }
+ crash.TailReports = append(crash.TailReports, tailReportID)
+ }
if crash.ReproSyz, err = putText(c, ns, textReproSyz, req.ReproSyz); err != nil {
return err
}
@@ -1073,6 +1084,11 @@ func purgeOldCrashes(c context.Context, bug *Bug, bugKey *db.Key) {
if crash.Report != 0 {
toDelete = append(toDelete, db.NewKey(c, textCrashReport, "", crash.Report, nil))
}
+ if len(crash.TailReports) != 0 {
+ for _, tailReport := range crash.TailReports {
+ toDelete = append(toDelete, db.NewKey(c, textCrashReport, "", tailReport, nil))
+ }
+ }
if crash.ReproSyz != 0 {
toDelete = append(toDelete, db.NewKey(c, textReproSyz, "", crash.ReproSyz, nil))
}
diff --git a/dashboard/app/app_test.go b/dashboard/app/app_test.go
index 693ca61ee9ad..9370fc8de0d3 100644
--- a/dashboard/app/app_test.go
+++ b/dashboard/app/app_test.go
@@ -16,9 +16,11 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/auth"
+ "github.com/google/syzkaller/pkg/report"
"github.com/google/syzkaller/pkg/subsystem"
_ "github.com/google/syzkaller/pkg/subsystem/lists"
"github.com/google/syzkaller/sys/targets"
+ "github.com/stretchr/testify/assert"
"google.golang.org/appengine/v2/user"
)
@@ -1139,3 +1141,43 @@ kernel BUG at %s
%s<", crash.Title, crash.TailTitles[0])
+ if !strings.Contains(string(res), wantTitles) {
+ t.Logf("%s", res)
+ t.Errorf("can't find titles string %q", wantTitles)
+ }
+ if !strings.Contains(string(res), ">tail report 1<") {
+ t.Logf("%s", res)
+ t.Error("can't find tail report string")
+ }
+}
diff --git a/dashboard/app/entities_datastore.go b/dashboard/app/entities_datastore.go
index e424172f4cce..32ca92b26d01 100644
--- a/dashboard/app/entities_datastore.go
+++ b/dashboard/app/entities_datastore.go
@@ -13,6 +13,7 @@ import (
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/hash"
+ "github.com/google/syzkaller/pkg/report"
"github.com/google/syzkaller/pkg/subsystem"
db "google.golang.org/appengine/v2/datastore"
)
@@ -91,6 +92,7 @@ type Bug struct {
Title string
MergedTitles []string // crash titles that we already merged into this bug
AltTitles []string // alternative crash titles that we may merge into this bug
+ TitleStat string `datastore:",noindex"` // serialized report.TitleStat
Status int
StatusReason dashapi.BugStatusReason // e.g. if the bug status is "invalid", here's the reason why
DupOf string
@@ -348,6 +350,7 @@ type Crash struct {
// May be different from bug.Title due to AltTitles.
// May be empty for old bugs, in such case bug.Title is the right title.
Title string
+ TailTitles []string
Manager string
BuildID string
Time time.Time
@@ -357,6 +360,7 @@ type Crash struct {
Log int64 // reference to CrashLog text entity
Flags int64 // properties of the Crash
Report int64 // reference to CrashReport text entity
+ TailReports []int64 // references to CrashReport text entity
ReportElements CrashReportElements // parsed parts of the crash report
ReproOpts []byte `datastore:",noindex"`
ReproSyz int64 // reference to ReproSyz text entity
@@ -985,6 +989,20 @@ func (bug *Bug) increaseCrashStats(now time.Time) {
}
}
+func (bug *Bug) addTitleStat(titles []string) error {
+ ts, err := report.TitleStatFromBytes([]byte(bug.TitleStat))
+ if err != nil {
+ return err
+ }
+ ts.Add(append([]string{bug.Title}, titles...))
+ bytes, err := ts.ToBytes()
+ if err != nil {
+ return err
+ }
+ bug.TitleStat = string(bytes)
+ return nil
+}
+
func (bug *Bug) dailyStatsTail(from time.Time) []BugDailyStats {
startDate := timeDate(from)
startPos := len(bug.DailyStats)
diff --git a/dashboard/app/main.go b/dashboard/app/main.go
index 0d2e588b1902..7f28eb8e4935 100644
--- a/dashboard/app/main.go
+++ b/dashboard/app/main.go
@@ -368,6 +368,7 @@ type uiBug struct {
Namespace string
Title string
ImpactScore int
+ RankTooltip string
NumCrashes int64
NumCrashesBad bool
BisectCause BisectStatus
@@ -398,19 +399,21 @@ type uiBugLabel struct {
}
type uiCrash struct {
- Title string
- Manager string
- Time time.Time
- Maintainers string
- LogLink string
- LogHasStrace bool
- ReportLink string
- ReproSyzLink string
- ReproCLink string
- ReproIsRevoked bool
- ReproLogLink string
- MachineInfoLink string
- Assets []*uiAsset
+ Title string
+ TailTitles []string
+ Manager string
+ Time time.Time
+ Maintainers string
+ LogLink string
+ LogHasStrace bool
+ ReportLink string
+ TailReportsLinks []string
+ ReproSyzLink string
+ ReproCLink string
+ ReproIsRevoked bool
+ ReproLogLink string
+ MachineInfoLink string
+ Assets []*uiAsset
*uiBuild
}
@@ -1938,10 +1941,16 @@ func createUIBug(c context.Context, bug *Bug, state *ReportingState, managers []
log.Errorf(c, "failed to generate credit email: %v", err)
}
}
+
+ titleStat, err := report.TitleStatFromBytes([]byte(bug.TitleStat))
+ if err != nil {
+ log.Errorf(c, "report.TitleStatFromBytes: %v", err)
+ }
uiBug := &uiBug{
Namespace: bug.Namespace,
Title: bug.displayTitle(),
ImpactScore: report.TitlesToImpact(bug.Title, bug.AltTitles...),
+ RankTooltip: report.HigherRankTooltip(bug.Title, titleStat.Explain()),
BisectCause: bug.BisectCause,
BisectFix: bug.BisectFix,
NumCrashes: bug.NumCrashes,
@@ -2076,20 +2085,26 @@ func makeUIAssets(c context.Context, build *Build, crash *Crash, forReport bool)
}
func makeUICrash(c context.Context, crash *Crash, build *Build) *uiCrash {
+ var tailReportsLinks []string
+ for _, tailReportID := range crash.TailReports {
+ tailReportsLinks = append(tailReportsLinks, textLink(textCrashReport, tailReportID))
+ }
ui := &uiCrash{
- Title: crash.Title,
- Manager: crash.Manager,
- Time: crash.Time,
- Maintainers: strings.Join(crash.Maintainers, ", "),
- LogLink: textLink(textCrashLog, crash.Log),
- LogHasStrace: dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0,
- ReportLink: textLink(textCrashReport, crash.Report),
- ReproSyzLink: textLink(textReproSyz, crash.ReproSyz),
- ReproCLink: textLink(textReproC, crash.ReproC),
- ReproLogLink: textLink(textReproLog, crash.ReproLog),
- ReproIsRevoked: crash.ReproIsRevoked,
- MachineInfoLink: textLink(textMachineInfo, crash.MachineInfo),
- Assets: makeUIAssets(c, build, crash, true),
+ Title: crash.Title,
+ TailTitles: crash.TailTitles,
+ Manager: crash.Manager,
+ Time: crash.Time,
+ Maintainers: strings.Join(crash.Maintainers, ", "),
+ LogLink: textLink(textCrashLog, crash.Log),
+ LogHasStrace: dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0,
+ ReportLink: textLink(textCrashReport, crash.Report),
+ TailReportsLinks: tailReportsLinks,
+ ReproSyzLink: textLink(textReproSyz, crash.ReproSyz),
+ ReproCLink: textLink(textReproC, crash.ReproC),
+ ReproLogLink: textLink(textReproLog, crash.ReproLog),
+ ReproIsRevoked: crash.ReproIsRevoked,
+ MachineInfoLink: textLink(textMachineInfo, crash.MachineInfo),
+ Assets: makeUIAssets(c, build, crash, true),
}
if build != nil {
ui.uiBuild = makeUIBuild(c, build, true)
diff --git a/dashboard/app/templates/templates.html b/dashboard/app/templates/templates.html
index af54dab6f4d3..5491904c7a50 100644
--- a/dashboard/app/templates/templates.html
+++ b/dashboard/app/templates/templates.html
@@ -206,7 +206,14 @@
{{link .Link .Name}}
{{- end}}
-
{{$b.ImpactScore}} |
+
+ {{if $b.RankTooltip}}
+ {{$b.ImpactScore}}
+ {{$b.RankTooltip}}
+ {{else}}
+ {{$b.ImpactScore}}
+ {{end}}
+ |
{{formatReproLevel $b.ReproLevel}} |
{{print $b.BisectCause}} |
{{print $b.BisectFix}} |
@@ -456,7 +463,14 @@
{{link $b.SyzkallerCommitLink (formatShortHash $b.SyzkallerCommit)}} |
{{if $b.KernelConfigLink}}.config{{end}} |
{{if $b.LogLink}}{{if $b.LogHasStrace}}strace{{else}}console{{end}} log{{end}} |
- {{if $b.ReportLink}}report{{end}} |
+
+ {{- if $b.ReportLink -}}
+ report
+ {{- end -}}
+ {{- range $i, $trl := $b.TailReportsLinks -}}
+ tail report {{add $i 1}}
+ {{- end -}}
+ |
{{if $b.ReproSyzLink}}syz{{end}}{{if $b.ReproLogLink}} / log{{end}} |
{{if $b.ReproCLink}}C{{end}} |
{{if $b.MachineInfoLink}}info{{end}} |
@@ -464,7 +478,12 @@
[{{$asset.Title}}{{if $asset.FsckLogURL}} ({{if $asset.FsIsClean}}clean{{else}}corrupt{{end}} fs){{end}}]
{{end}}
{{$b.Manager}} |
- {{$b.Title}} |
+
+ {{- $b.Title}}
+ {{- range $tt := $b.TailTitles -}}
+ {{$tt}}
+ {{- end -}}
+ |
{{end}}
diff --git a/dashboard/dashapi/dashapi.go b/dashboard/dashapi/dashapi.go
index 1f9dec94dcfa..5594a4ac7c7f 100644
--- a/dashboard/dashapi/dashapi.go
+++ b/dashboard/dashapi/dashapi.go
@@ -322,6 +322,7 @@ type Crash struct {
BuildID string // refers to Build.ID
Title string
AltTitles []string // alternative titles, used for better deduplication
+ TailTitles []string // titles of the tail reports, see TailReports field
Corrupted bool // report is corrupted (corrupted title, no stacks, etc)
Suppressed bool
Maintainers []string // deprecated in favor of Recipients
@@ -329,6 +330,9 @@ type Crash struct {
Log []byte
Flags CrashFlags
Report []byte
+ // Crashing machine may generate report chain like WARNING -> WARNING -> KASAN -> GPF.
+ // These additional reports are used to better understand the bug nature and impact.
+ TailReports [][]byte
MachineInfo []byte
Assets []NewAsset
GuiltyFiles []string
diff --git a/pkg/html/html.go b/pkg/html/html.go
index e077153d1856..9e75d830a34a 100644
--- a/pkg/html/html.go
+++ b/pkg/html/html.go
@@ -52,24 +52,27 @@ func CreateTextGlob(glob string) *texttemplate.Template {
}
var Funcs = template.FuncMap{
- "link": link,
- "optlink": optlink,
- "formatTime": FormatTime,
- "formatDate": FormatDate,
- "formatKernelTime": formatKernelTime,
- "formatJSTime": formatJSTime,
+ // keep-sorted start
+ "add": func(a, b int) int { return a + b },
+ "commitLink": commitLink,
+ "dereference": dereferencePointer,
"formatClock": formatClock,
+ "formatCommitTableTitle": formatCommitTableTitle,
+ "formatDate": FormatDate,
"formatDuration": formatDuration,
+ "formatJSTime": formatJSTime,
+ "formatKernelTime": formatKernelTime,
"formatLateness": formatLateness,
+ "formatList": formatStringList,
"formatReproLevel": formatReproLevel,
- "formatStat": formatStat,
"formatShortHash": formatShortHash,
+ "formatStat": formatStat,
"formatTagHash": formatTagHash,
- "formatCommitTableTitle": formatCommitTableTitle,
- "formatList": formatStringList,
+ "formatTime": FormatTime,
+ "link": link,
+ "optlink": optlink,
"selectBisect": selectBisect,
- "dereference": dereferencePointer,
- "commitLink": commitLink,
+ // keep-sorted end
}
func selectBisect(rep *dashapi.BugReport) *dashapi.BisectResult {
diff --git a/pkg/manager/crash.go b/pkg/manager/crash.go
index a62ec8280125..b3ecd20f2a2d 100644
--- a/pkg/manager/crash.go
+++ b/pkg/manager/crash.go
@@ -91,8 +91,10 @@ func (cs *CrashStore) SaveCrash(crash *Crash) (bool, error) {
writeOrRemove("tag", []byte(cs.Tag))
writeOrRemove("report", report.MergeReportBytes(reps))
writeOrRemove("machineInfo", crash.MachineInfo)
- if err := report.AddTitleStat(filepath.Join(dir, "title-stat"), reps); err != nil {
- return false, fmt.Errorf("report.AddTitleStat: %w", err)
+ titleStatPath := filepath.Join(dir, "title-stat")
+ titles := append([]string{crash.Title}, crash.TailTitles()...)
+ if err := report.AddTitlesToStatFile(titleStatPath, titles); err != nil {
+ return false, fmt.Errorf("report.AddTitlesToStatFile: %w", err)
}
return first, nil
@@ -243,11 +245,13 @@ func (cs *CrashStore) BugInfo(id string, full bool) (*BugInfo, error) {
// Bug rank may go up over time if we observe higher ranked bugs as a consequence of the first failure.
ret.Rank = report.TitlesToImpact(ret.Title)
- if titleStat, err := report.ReadStatFile(filepath.Join(dir, "title-stat")); err == nil {
- ret.TailTitles = report.ExplainTitleStat(titleStat)
- for _, ti := range ret.TailTitles {
- ret.Rank = max(ret.Rank, ti.Rank)
- }
+ titleStat, err := report.ReadStatFile(filepath.Join(dir, "title-stat"))
+ if err != nil {
+ return nil, err
+ }
+ ret.TailTitles = titleStat.Explain()
+ for _, ti := range ret.TailTitles {
+ ret.Rank = max(ret.Rank, ti.Rank)
}
ret.FirstTime = osutil.CreationTime(stat)
diff --git a/pkg/manager/http.go b/pkg/manager/http.go
index 61d208f410cc..07042a5cd6ae 100644
--- a/pkg/manager/http.go
+++ b/pkg/manager/http.go
@@ -357,7 +357,7 @@ func makeUICrashType(info *BugInfo, startTime time.Time, repros map[string]bool)
info.ReproAttempts >= MaxReproAttempts)
return UICrashType{
BugInfo: *info,
- RankTooltip: higherRankTooltip(info.Title, info.TailTitles),
+ RankTooltip: report.HigherRankTooltip(info.Title, info.TailTitles),
New: info.FirstTime.After(startTime),
Active: info.LastTime.After(startTime),
Triaged: triaged,
@@ -365,26 +365,6 @@ func makeUICrashType(info *BugInfo, startTime time.Time, repros map[string]bool)
}
}
-// higherRankTooltip generates the prioritized list of the titles with higher Rank
-// than the firstTitle has.
-func higherRankTooltip(firstTitle string, titlesInfo []*report.TitleFreqRank) string {
- baseRank := report.TitlesToImpact(firstTitle)
- res := ""
- for _, ti := range titlesInfo {
- if ti.Rank <= baseRank {
- continue
- }
- res += fmt.Sprintf("[rank %2v, freq %5.1f%%] %s\n",
- ti.Rank,
- 100*float32(ti.Count)/float32(ti.Total),
- ti.Title)
- }
- if res != "" {
- return fmt.Sprintf("[rank %2v, originally] %s\n%s", baseRank, firstTitle, res)
- }
- return res
-}
-
var crashIDRe = regexp.MustCompile(`^\w+$`)
func (serv *HTTPServer) httpCrash(w http.ResponseWriter, r *http.Request) {
diff --git a/pkg/manager/repro.go b/pkg/manager/repro.go
index 184945c5ce9b..a3bc90569b3d 100644
--- a/pkg/manager/repro.go
+++ b/pkg/manager/repro.go
@@ -51,6 +51,22 @@ func (c *Crash) FullTitle() string {
panic("the crash is expected to have a report")
}
+func (c *Crash) TailTitles() []string {
+ res := make([]string, len(c.TailReports))
+ for i, r := range c.TailReports {
+ res[i] = r.Title
+ }
+ return res
+}
+
+func (c *Crash) TailReportsBytes() [][]byte {
+ res := make([][]byte, len(c.TailReports))
+ for i, r := range c.TailReports {
+ res[i] = r.Report
+ }
+ return res
+}
+
type ReproManagerView interface {
RunRepro(ctx context.Context, crash *Crash) *ReproResult
NeedRepro(crash *Crash) bool
diff --git a/pkg/report/impact_score.go b/pkg/report/impact_score.go
index 8139644d2a6a..63adb652ad41 100644
--- a/pkg/report/impact_score.go
+++ b/pkg/report/impact_score.go
@@ -4,8 +4,6 @@
package report
import (
- "sort"
-
"github.com/google/syzkaller/pkg/report/crash"
)
@@ -64,45 +62,3 @@ func TitlesToImpact(title string, otherTitles ...string) int {
}
return maxImpact
}
-
-type TitleFreqRank struct {
- Title string
- Count int
- Total int
- Rank int
-}
-
-func ExplainTitleStat(ts *titleStat) []*TitleFreqRank {
- titleCount := map[string]int{}
- var totalCount int
- ts.visit(func(count int, titles ...string) {
- uniq := map[string]bool{}
- for _, title := range titles {
- uniq[title] = true
- }
- for title := range uniq {
- titleCount[title] += count
- }
- totalCount += count
- })
- var res []*TitleFreqRank
- for title, count := range titleCount {
- res = append(res, &TitleFreqRank{
- Title: title,
- Count: count,
- Total: totalCount,
- Rank: TitlesToImpact(title),
- })
- }
- sort.Slice(res, func(l, r int) bool {
- if res[l].Rank != res[r].Rank {
- return res[l].Rank > res[r].Rank
- }
- lTitle, rTitle := res[l].Title, res[r].Title
- if titleCount[lTitle] != titleCount[rTitle] {
- return titleCount[lTitle] > titleCount[rTitle]
- }
- return lTitle < rTitle
- })
- return res
-}
diff --git a/pkg/report/report.go b/pkg/report/report.go
index b4f93e3ac0bd..ab7c6b7e7b5f 100644
--- a/pkg/report/report.go
+++ b/pkg/report/report.go
@@ -955,6 +955,10 @@ func MergeReportBytes(reps []*Report) []byte {
return res
}
-func SplitReportBytes(data []byte) [][]byte {
- return bytes.Split(data, []byte(reportSeparator))
+func firstReportBytes(data []byte) []byte {
+ reps := bytes.Split(data, []byte(reportSeparator))
+ if len(reps) == 0 {
+ return nil
+ }
+ return reps[0]
}
diff --git a/pkg/report/report_test.go b/pkg/report/report_test.go
index 8c863fe8daff..45533d5f2a3b 100644
--- a/pkg/report/report_test.go
+++ b/pkg/report/report_test.go
@@ -511,37 +511,37 @@ BCDEF`), Truncate([]byte(`0123456789ABCDEF`), 0, 5))
DEF`), Truncate([]byte(`0123456789ABCDEF`), 4, 3))
}
-func TestSplitReportBytes(t *testing.T) {
+func TestFirstReportBytes(t *testing.T) {
tests := []struct {
- name string
- input []byte
- wantFirst string
+ name string
+ input []byte
+ want string
}{
{
- name: "empty",
- input: nil,
- wantFirst: "",
+ name: "empty",
+ input: nil,
+ want: "",
},
{
- name: "single",
- input: []byte("report1"),
- wantFirst: "report1",
+ name: "single",
+ input: []byte("report1"),
+ want: "report1",
},
{
- name: "split in the middle",
- input: []byte("report1" + reportSeparator + "report2"),
- wantFirst: "report1",
+ name: "split in the middle",
+ input: []byte("report1" + reportSeparator + "report2"),
+ want: "report1",
},
{
- name: "split in the middle, save new line",
- input: []byte("report1\n" + reportSeparator + "report2"),
- wantFirst: "report1\n",
+ name: "split in the middle, save new line",
+ input: []byte("report1\n" + reportSeparator + "report2"),
+ want: "report1\n",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
- splitted := SplitReportBytes(test.input)
- assert.Equal(t, test.wantFirst, string(splitted[0]))
+ first := firstReportBytes(test.input)
+ assert.Equal(t, test.want, string(first))
})
}
}
diff --git a/pkg/report/title_stat.go b/pkg/report/title_stat.go
index 5b3cd3fb791c..7c43cf032d14 100644
--- a/pkg/report/title_stat.go
+++ b/pkg/report/title_stat.go
@@ -9,61 +9,54 @@ import (
"fmt"
"maps"
"os"
+ "sort"
)
-func AddTitleStat(file string, reps []*Report) error {
- var titles []string
- for _, rep := range reps {
- titles = append(titles, rep.Title)
- }
+func AddTitlesToStatFile(file string, titles []string) error {
stat, err := ReadStatFile(file)
if err != nil {
- return fmt.Errorf("report.ReadStatFile: %w", err)
+ return fmt.Errorf("readStatFile: %w", err)
}
- stat.add(titles)
- if err := writeStatFile(file, stat); err != nil {
- return fmt.Errorf("writeStatFile: %w", err)
+ stat.Add(titles)
+ bytes, err := stat.ToBytes()
+ if err != nil {
+ return err
}
- return nil
+ return os.WriteFile(file, bytes, 0644)
}
-func ReadStatFile(file string) (*titleStat, error) {
- stat := &titleStat{}
+func ReadStatFile(file string) (*TitleStat, error) {
+ res := &TitleStat{}
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
- return stat, nil
+ return res, nil
}
data, err := os.ReadFile(file)
if err != nil {
return nil, err
}
- if len(data) == 0 {
- return stat, nil
- }
- if err := json.Unmarshal(data, stat); err != nil {
- return nil, err
- }
- return stat, nil
+ return TitleStatFromBytes(data)
}
-func writeStatFile(file string, stat *titleStat) error {
- data, err := json.MarshalIndent(stat, "", "\t")
- if err != nil {
- return err
+func TitleStatFromBytes(data []byte) (*TitleStat, error) {
+ var ts TitleStat
+ if len(data) == 0 {
+ return &ts, nil
}
- if err := os.WriteFile(file, data, 0644); err != nil {
- return err
+ if err := json.Unmarshal(data, &ts); err != nil {
+ return nil, err
}
- return nil
+ return &ts, nil
}
-type titleStatNodes map[string]*titleStat
+type titleStatNodes map[string]*TitleStat
-type titleStat struct {
+type TitleStat struct {
Count int
Nodes titleStatNodes
}
-func (ts *titleStat) add(reps []string) {
+func (ts *TitleStat) Add(reps []string) {
+ ts.Count++
if len(reps) == 0 {
return
}
@@ -71,13 +64,12 @@ func (ts *titleStat) add(reps []string) {
ts.Nodes = make(titleStatNodes)
}
if ts.Nodes[reps[0]] == nil {
- ts.Nodes[reps[0]] = &titleStat{}
+ ts.Nodes[reps[0]] = &TitleStat{}
}
- ts.Nodes[reps[0]].Count++
- ts.Nodes[reps[0]].add(reps[1:])
+ ts.Nodes[reps[0]].Add(reps[1:])
}
-func (ts *titleStat) visit(cb func(int, ...string), titles ...string) {
+func (ts *TitleStat) visit(cb func(int, ...string), titles ...string) {
if len(ts.Nodes) == 0 {
cb(ts.Count, titles...)
return
@@ -86,3 +78,67 @@ func (ts *titleStat) visit(cb func(int, ...string), titles ...string) {
ts.Nodes[title].visit(cb, append(titles, title)...)
}
}
+
+func (ts *TitleStat) ToBytes() ([]byte, error) {
+ return json.MarshalIndent(ts, "", "\t")
+}
+
+type TitleFreqRank struct {
+ Title string
+ Count int
+ Total int
+ Rank int
+}
+
+func (ts *TitleStat) Explain() []*TitleFreqRank {
+ titleCount := map[string]int{}
+
+ ts.visit(func(count int, titles ...string) {
+ uniq := map[string]bool{}
+ for _, title := range titles {
+ uniq[title] = true
+ }
+ for title := range uniq {
+ titleCount[title] += count
+ }
+ })
+ var res []*TitleFreqRank
+ for title, count := range titleCount {
+ res = append(res, &TitleFreqRank{
+ Title: title,
+ Count: count,
+ Total: ts.Count,
+ Rank: TitlesToImpact(title),
+ })
+ }
+ sort.Slice(res, func(l, r int) bool {
+ if res[l].Rank != res[r].Rank {
+ return res[l].Rank > res[r].Rank
+ }
+ lTitle, rTitle := res[l].Title, res[r].Title
+ if titleCount[lTitle] != titleCount[rTitle] {
+ return titleCount[lTitle] > titleCount[rTitle]
+ }
+ return lTitle < rTitle
+ })
+ return res
+}
+
+// HigherRankTooltip generates a prioritized list of titles with a rank higher than firstTitle.
+func HigherRankTooltip(firstTitle string, titlesInfo []*TitleFreqRank) string {
+ baseRank := TitlesToImpact(firstTitle)
+ res := ""
+ for _, ti := range titlesInfo {
+ if ti.Rank <= baseRank {
+ continue
+ }
+ res += fmt.Sprintf("[rank %2v, freq %5.1f%%] %s\n",
+ ti.Rank,
+ 100*float32(ti.Count)/float32(ti.Total),
+ ti.Title)
+ }
+ if res != "" {
+ return fmt.Sprintf("[rank %2v, originally] %s\n%s", baseRank, firstTitle, res)
+ }
+ return res
+}
diff --git a/pkg/report/title_stat_test.go b/pkg/report/title_stat_test.go
index 216af456ca99..c1de3b72c598 100644
--- a/pkg/report/title_stat_test.go
+++ b/pkg/report/title_stat_test.go
@@ -4,36 +4,36 @@
package report
import (
- "os"
"testing"
"github.com/stretchr/testify/assert"
)
-func TestAddTitleStat(t *testing.T) {
+func TestAddTitlesToStatFile(t *testing.T) {
tests := []struct {
- name string
- base string
- reps [][]*Report
- want *titleStat
+ name string
+ titleChains [][]string
+ want *TitleStat
}{
{
name: "read empty",
- want: &titleStat{},
+ want: &TitleStat{},
},
{
- name: "add single",
- reps: [][]*Report{{{Title: "warning 1"}}},
- want: &titleStat{
+ name: "add single",
+ titleChains: [][]string{{"warning 1"}},
+ want: &TitleStat{
+ Count: 1,
Nodes: titleStatNodes{
"warning 1": {Count: 1},
},
},
},
{
- name: "add chain",
- reps: [][]*Report{{{Title: "warning 1"}, {Title: "warning 2"}}},
- want: &titleStat{
+ name: "add chain",
+ titleChains: [][]string{{"warning 1", "warning 2"}},
+ want: &TitleStat{
+ Count: 1,
Nodes: titleStatNodes{
"warning 1": {Count: 1,
Nodes: titleStatNodes{
@@ -44,9 +44,10 @@ func TestAddTitleStat(t *testing.T) {
},
},
{
- name: "add multi chains",
- reps: [][]*Report{{{Title: "warning 1"}, {Title: "warning 2"}}, {{Title: "warning 1"}, {Title: "warning 3"}}},
- want: &titleStat{
+ name: "add multi chains",
+ titleChains: [][]string{{"warning 1", "warning 2"}, {"warning 1", "warning 3"}},
+ want: &TitleStat{
+ Count: 2,
Nodes: titleStatNodes{
"warning 1": {Count: 2,
Nodes: titleStatNodes{
@@ -62,15 +63,66 @@ func TestAddTitleStat(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
tmpFile := t.TempDir() + "/test.input"
- err := os.WriteFile(tmpFile, []byte(test.base), 0644)
- assert.NoError(t, err)
- for _, reps := range test.reps {
- err = AddTitleStat(tmpFile, reps)
+ for _, titles := range test.titleChains {
+ err := AddTitlesToStatFile(tmpFile, titles)
assert.NoError(t, err)
}
- got, err := ReadStatFile(tmpFile)
+ statData, err := ReadStatFile(tmpFile)
assert.NoError(t, err)
- assert.Equal(t, test.want, got)
+ assert.Equal(t, test.want, statData)
+ })
+ }
+}
+
+func TestTitleStat_Explain(t *testing.T) {
+ tests := []struct {
+ name string
+ input [][]string
+ want []*TitleFreqRank
+ }{
+ {
+ name: "empty",
+ want: nil,
+ },
+ {
+ name: "single input",
+ input: [][]string{{"info"}},
+ want: []*TitleFreqRank{
+ {
+ Title: "info",
+ Count: 1,
+ Total: 1,
+ Rank: -1,
+ },
+ },
+ },
+ {
+ name: "single nested input",
+ input: [][]string{{"info"}, {"info", "warning"}},
+ want: []*TitleFreqRank{
+ {
+ Title: "info",
+ Count: 1,
+ Total: 2,
+ Rank: -1,
+ },
+ {
+ Title: "warning",
+ Count: 1,
+ Total: 2,
+ Rank: -1,
+ },
+ },
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
+ ts := &TitleStat{}
+ for _, input := range test.input {
+ ts.Add(input)
+ }
+ assert.Equal(t, test.want, ts.Explain())
})
}
}
diff --git a/syz-manager/manager.go b/syz-manager/manager.go
index 3f94bd23a714..bf5aca3956dc 100644
--- a/syz-manager/manager.go
+++ b/syz-manager/manager.go
@@ -714,8 +714,8 @@ func (mgr *Manager) saveCrash(crash *manager.Crash) bool {
flags += " [suppressed]"
}
log.Logf(0, "VM %v: crash: %v%v", crash.InstanceIndex, crash.Report.Title, flags)
- for i, report := range crash.TailReports {
- log.Logf(0, "VM %v: crash(tail%d): %v%v", crash.InstanceIndex, i, report.Title, flags)
+ for i, t := range crash.TailTitles() {
+ log.Logf(0, "VM %v: crash(tail%d): %v%v", crash.InstanceIndex, i, t, flags)
}
if mgr.mode.FailOnCrashes {
@@ -748,12 +748,14 @@ func (mgr *Manager) saveCrash(crash *manager.Crash) bool {
dc := &dashapi.Crash{
BuildID: mgr.cfg.Tag,
Title: crash.Title,
+ TailTitles: crash.TailTitles(),
AltTitles: crash.AltTitles,
Corrupted: crash.Corrupted,
Suppressed: crash.Suppressed,
Recipients: crash.Recipients.ToDash(),
Log: crash.Output,
- Report: report.SplitReportBytes(crash.Report.Report)[0],
+ Report: crash.Report.Report,
+ TailReports: crash.TailReportsBytes(),
MachineInfo: crash.MachineInfo,
}
setGuiltyFiles(dc, crash.Report)
@@ -900,12 +902,14 @@ func (mgr *Manager) saveRepro(res *manager.ReproResult) {
dc := &dashapi.Crash{
BuildID: mgr.cfg.Tag,
Title: reproReport.Title,
+ TailTitles: res.Crash.TailTitles(),
AltTitles: reproReport.AltTitles,
Suppressed: reproReport.Suppressed,
Recipients: reproReport.Recipients.ToDash(),
Log: output,
Flags: crashFlags,
- Report: report.SplitReportBytes(reproReport.Report)[0],
+ Report: reproReport.Report,
+ TailReports: res.Crash.TailReportsBytes(),
ReproOpts: repro.Opts.Serialize(),
ReproSyz: progText,
ReproC: cprogText,