aboutsummaryrefslogtreecommitdiffstats
path: root/dashboard
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2019-02-14 10:35:03 +0100
committerDmitry Vyukov <dvyukov@google.com>2019-02-17 15:08:45 +0100
commit3e98cc30803fb5e41504dd08b1325cb074a8a3f2 (patch)
tree5f0a7a4702bedbb9706c158e09e4be1875894625 /dashboard
parentf42dee6d5e501a061cdbb807672361369bf28492 (diff)
dashboard/app: poll commits info
This implements 2 features: - syz-ci polls a set of additional repos to discover fixing commits sooner (e.g. it can now discover a fixing commit in netfilter tree before it reaches any of the tested trees). - syz-ci uploads info about commits to dashboard. For example, a user marks a bug as fixed by commit "foo: bar". syz-ci will find this commit in the main namespace repo and upload commmit hash/date/author to dashboard. This in turn allows to show links to fixing commits. Fixes #691 Fixes #610
Diffstat (limited to 'dashboard')
-rw-r--r--dashboard/app/aetest.go8
-rw-r--r--dashboard/app/api.go234
-rw-r--r--dashboard/app/app_test.go49
-rw-r--r--dashboard/app/bug.html8
-rw-r--r--dashboard/app/commit_poll_test.go94
-rw-r--r--dashboard/app/config.go66
-rw-r--r--dashboard/app/email_test.go2
-rw-r--r--dashboard/app/entities.go45
-rw-r--r--dashboard/app/fix_test.go25
-rw-r--r--dashboard/app/index.yaml6
-rw-r--r--dashboard/app/main.go54
-rw-r--r--dashboard/app/noaetest.go8
-rw-r--r--dashboard/app/reporting.go4
-rw-r--r--dashboard/app/static/style.css13
-rw-r--r--dashboard/app/templates.html26
-rw-r--r--dashboard/dashapi/dashapi.go39
16 files changed, 549 insertions, 132 deletions
diff --git a/dashboard/app/aetest.go b/dashboard/app/aetest.go
new file mode 100644
index 000000000..c94379bc6
--- /dev/null
+++ b/dashboard/app/aetest.go
@@ -0,0 +1,8 @@
+// Copyright 2019 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.
+
+// +build aetest
+
+package dash
+
+const isAppEngineTest = true
diff --git a/dashboard/app/api.go b/dashboard/app/api.go
index 4f5b68be7..c37afb404 100644
--- a/dashboard/app/api.go
+++ b/dashboard/app/api.go
@@ -46,6 +46,8 @@ var apiNamespaceHandlers = map[string]APINamespaceHandler{
"report_failed_repro": apiReportFailedRepro,
"need_repro": apiNeedRepro,
"manager_stats": apiManagerStats,
+ "commit_poll": apiCommitPoll,
+ "upload_commits": apiUploadCommits,
}
type JSONHandler func(c context.Context, r *http.Request) (interface{}, error)
@@ -201,20 +203,153 @@ loop:
commits = append(commits, com)
}
sort.Strings(commits)
- reportEmail := ""
+ resp := &dashapi.BuilderPollResp{
+ PendingCommits: commits,
+ ReportEmail: reportEmail(c, ns),
+ }
+ return resp, nil
+}
+
+func reportEmail(c context.Context, ns string) string {
for _, reporting := range config.Namespaces[ns].Reporting {
if _, ok := reporting.Config.(*EmailConfig); ok {
- reportEmail = ownEmail(c)
- break
+ return ownEmail(c)
}
}
- resp := &dashapi.BuilderPollResp{
- PendingCommits: commits,
- ReportEmail: reportEmail,
+ return ""
+}
+
+func apiCommitPoll(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
+ resp := &dashapi.CommitPollResp{
+ ReportEmail: reportEmail(c, ns),
+ }
+ for _, repo := range config.Namespaces[ns].Repos {
+ resp.Repos = append(resp.Repos, dashapi.Repo{
+ URL: repo.URL,
+ Branch: repo.Branch,
+ })
+ }
+ var bugs []*Bug
+ _, err := datastore.NewQuery("Bug").
+ Filter("Namespace=", ns).
+ Filter("NeedCommitInfo=", true).
+ Project("Commits").
+ Limit(100).
+ GetAll(c, &bugs)
+ if err != nil {
+ return nil, fmt.Errorf("failed to query bugs: %v", err)
+ }
+ commits := make(map[string]bool)
+ for _, bug := range bugs {
+ for _, com := range bug.Commits {
+ commits[com] = true
+ }
+ }
+ for com := range commits {
+ resp.Commits = append(resp.Commits, com)
}
return resp, nil
}
+func apiUploadCommits(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
+ req := new(dashapi.CommitPollResultReq)
+ if err := json.Unmarshal(payload, req); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal request: %v", err)
+ }
+ // This adds fixing commits to bugs.
+ err := addCommitsToBugs(c, ns, "", nil, req.Commits)
+ if err != nil {
+ return nil, err
+ }
+ // Now add commit info to commits.
+ for _, com := range req.Commits {
+ if com.Hash == "" {
+ continue
+ }
+ if err := addCommitInfo(c, ns, com); err != nil {
+ return nil, err
+ }
+ }
+ return nil, nil
+}
+
+func addCommitInfo(c context.Context, ns string, com dashapi.Commit) error {
+ var bugs []*Bug
+ keys, err := datastore.NewQuery("Bug").
+ Filter("Namespace=", ns).
+ Filter("Commits=", com.Title).
+ GetAll(c, &bugs)
+ if err != nil {
+ return fmt.Errorf("failed to query bugs: %v", err)
+ }
+ for i, bug := range bugs {
+ if err := addCommitInfoToBug(c, bug, keys[i], com); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func addCommitInfoToBug(c context.Context, bug *Bug, bugKey *datastore.Key, com dashapi.Commit) error {
+ if needUpdate, err := addCommitInfoToBugImpl(c, bug, com); err != nil {
+ return err
+ } else if !needUpdate {
+ return nil
+ }
+ tx := func(c context.Context) error {
+ bug := new(Bug)
+ if err := datastore.Get(c, bugKey, bug); err != nil {
+ return fmt.Errorf("failed to get bug %v: %v", bugKey.StringID(), err)
+ }
+ if needUpdate, err := addCommitInfoToBugImpl(c, bug, com); err != nil {
+ return err
+ } else if !needUpdate {
+ return nil
+ }
+ if _, err := datastore.Put(c, bugKey, bug); err != nil {
+ return fmt.Errorf("failed to put bug: %v", err)
+ }
+ return nil
+ }
+ return datastore.RunInTransaction(c, tx, nil)
+}
+
+func addCommitInfoToBugImpl(c context.Context, bug *Bug, com dashapi.Commit) (bool, error) {
+ ci := -1
+ for i, title := range bug.Commits {
+ if title == com.Title {
+ ci = i
+ break
+ }
+ }
+ if ci < 0 {
+ return false, nil
+ }
+ for len(bug.CommitInfo) < len(bug.Commits) {
+ bug.CommitInfo = append(bug.CommitInfo, CommitInfo{})
+ }
+ hash0 := bug.CommitInfo[ci].Hash
+ date0 := bug.CommitInfo[ci].Date
+ author0 := bug.CommitInfo[ci].Author
+ needCommitInfo0 := bug.NeedCommitInfo
+
+ bug.CommitInfo[ci].Hash = com.Hash
+ bug.CommitInfo[ci].Date = com.Date
+ bug.CommitInfo[ci].Author = com.Author
+ bug.NeedCommitInfo = false
+ for i := range bug.CommitInfo {
+ if bug.CommitInfo[i].Hash == "" {
+ bug.NeedCommitInfo = true
+ break
+ }
+ }
+ changed := hash0 != bug.CommitInfo[ci].Hash ||
+ date0 != bug.CommitInfo[ci].Date ||
+ author0 != bug.CommitInfo[ci].Author ||
+ needCommitInfo0 != bug.NeedCommitInfo
+ return changed, nil
+}
+
func apiJobPoll(c context.Context, r *http.Request, payload []byte) (interface{}, error) {
req := new(dashapi.JobPollReq)
if err := json.Unmarshal(payload, req); err != nil {
@@ -255,6 +390,11 @@ func apiUploadBuild(c context.Context, ns string, r *http.Request, payload []byt
}
}
if len(req.Commits) != 0 || len(req.FixCommits) != 0 {
+ for i := range req.FixCommits {
+ // Reset hashes just to make sure,
+ // the build does not necessary come from the master repo, so we must not remember hashes.
+ req.FixCommits[i].Hash = ""
+ }
if err := addCommitsToBugs(c, ns, req.Manager, req.Commits, req.FixCommits); err != nil {
// We've already uploaded the build successfully and manager can use it.
// Moreover, addCommitsToBugs scans all bugs and can take long time.
@@ -329,8 +469,7 @@ func uploadBuild(c context.Context, now time.Time, ns string, req *dashapi.Build
return build, true, nil
}
-func addCommitsToBugs(c context.Context, ns, manager string,
- titles []string, fixCommits []dashapi.FixCommit) error {
+func addCommitsToBugs(c context.Context, ns, manager string, titles []string, fixCommits []dashapi.Commit) error {
presentCommits := make(map[string]bool)
bugFixedBy := make(map[string][]string)
for _, com := range titles {
@@ -338,7 +477,9 @@ func addCommitsToBugs(c context.Context, ns, manager string,
}
for _, com := range fixCommits {
presentCommits[com.Title] = true
- bugFixedBy[com.BugID] = append(bugFixedBy[com.BugID], com.Title)
+ for _, bugID := range com.BugIDs {
+ bugFixedBy[bugID] = append(bugFixedBy[bugID], com.Title)
+ }
}
managers, err := managerList(c, ns)
if err != nil {
@@ -347,10 +488,9 @@ func addCommitsToBugs(c context.Context, ns, manager string,
// Fetching all bugs in a namespace can be slow, and there is no way to filter only Open/Dup statuses.
// So we run a separate query for each status, this both avoids fetching unnecessary data
// and splits a long query into two (two smaller queries have lower chances of trigerring
- // timeouts that one huge).
+ // timeouts than one huge).
for _, status := range []int{BugStatusOpen, BugStatusDup} {
- err := addCommitsToBugsInStatus(c, status, ns, manager, managers,
- fixCommits, presentCommits, bugFixedBy)
+ err := addCommitsToBugsInStatus(c, status, ns, manager, managers, presentCommits, bugFixedBy)
if err != nil {
return err
}
@@ -359,7 +499,7 @@ func addCommitsToBugs(c context.Context, ns, manager string,
}
func addCommitsToBugsInStatus(c context.Context, status int, ns, manager string, managers []string,
- fixCommits []dashapi.FixCommit, presentCommits map[string]bool, bugFixedBy map[string][]string) error {
+ presentCommits map[string]bool, bugFixedBy map[string][]string) error {
var bugs []*Bug
_, err := datastore.NewQuery("Bug").
Filter("Namespace=", ns).
@@ -409,22 +549,22 @@ func addCommitsToBug(c context.Context, bug *Bug, manager string, managers []str
return nil
}
if len(fixCommits) != 0 && !reflect.DeepEqual(bug.Commits, fixCommits) {
- bug.Commits = fixCommits
- bug.FixTime = now
- bug.PatchedOn = nil
- }
- bug.PatchedOn = append(bug.PatchedOn, manager)
- if bug.Status == BugStatusOpen {
- fixed := true
- for _, mgr := range managers {
- if !stringInList(bug.PatchedOn, mgr) {
- fixed = false
- break
+ bug.updateCommits(fixCommits, now)
+ }
+ if manager != "" {
+ bug.PatchedOn = append(bug.PatchedOn, manager)
+ if bug.Status == BugStatusOpen {
+ fixed := true
+ for _, mgr := range managers {
+ if !stringInList(bug.PatchedOn, mgr) {
+ fixed = false
+ break
+ }
+ }
+ if fixed {
+ bug.Status = BugStatusFixed
+ bug.Closed = now
}
- }
- if fixed {
- bug.Status = BugStatusFixed
- bug.Closed = now
}
}
if _, err := datastore.Put(c, bugKey, bug); err != nil {
@@ -435,6 +575,25 @@ func addCommitsToBug(c context.Context, bug *Bug, manager string, managers []str
return datastore.RunInTransaction(c, tx, nil)
}
+func bugNeedsCommitUpdate(c context.Context, bug *Bug, manager string, fixCommits []string,
+ presentCommits map[string]bool, dolog bool) bool {
+ if len(fixCommits) != 0 && !reflect.DeepEqual(bug.Commits, fixCommits) {
+ if dolog {
+ log.Infof(c, "bug %q is fixed with %q", bug.Title, fixCommits)
+ }
+ return true
+ }
+ if len(bug.Commits) == 0 || manager == "" || stringInList(bug.PatchedOn, manager) {
+ return false
+ }
+ for _, com := range bug.Commits {
+ if !presentCommits[com] {
+ return false
+ }
+ }
+ return true
+}
+
func managerList(c context.Context, ns string) ([]string, error) {
var builds []*Build
_, err := datastore.NewQuery("Build").
@@ -456,25 +615,6 @@ func managerList(c context.Context, ns string) ([]string, error) {
return managers, nil
}
-func bugNeedsCommitUpdate(c context.Context, bug *Bug, manager string, fixCommits []string,
- presentCommits map[string]bool, dolog bool) bool {
- if len(fixCommits) != 0 && !reflect.DeepEqual(bug.Commits, fixCommits) {
- if dolog {
- log.Infof(c, "bug %q is fixed with %q", bug.Title, fixCommits)
- }
- return true
- }
- if len(bug.Commits) == 0 || stringInList(bug.PatchedOn, manager) {
- return false
- }
- for _, com := range bug.Commits {
- if !presentCommits[com] {
- return false
- }
- }
- return true
-}
-
func stringInList(list []string, str string) bool {
for _, s := range list {
if s == str {
diff --git a/dashboard/app/app_test.go b/dashboard/app/app_test.go
index e030ddaa4..a63f6e8d9 100644
--- a/dashboard/app/app_test.go
+++ b/dashboard/app/app_test.go
@@ -37,6 +37,20 @@ var testConfig = &GlobalConfig{
Clients: map[string]string{
client1: key1,
},
+ Repos: []KernelRepo{
+ {
+ URL: "git://syzkaller.org",
+ Branch: "branch10",
+ Alias: "repo10alias",
+ CC: []string{"maintainers@repo10.org", "bugs@repo10.org"},
+ },
+ {
+ URL: "git://github.com/google/syzkaller",
+ Branch: "master",
+ Alias: "repo10alias",
+ CC: []string{"maintainers@repo10.org", "bugs@repo10.org"},
+ },
+ },
Reporting: []Reporting{
{
Name: "reporting1",
@@ -67,6 +81,14 @@ var testConfig = &GlobalConfig{
Clients: map[string]string{
client2: key2,
},
+ Repos: []KernelRepo{
+ {
+ URL: "git://syzkaller.org",
+ Branch: "branch10",
+ Alias: "repo10alias",
+ CC: []string{"maintainers@repo10.org", "bugs@repo10.org"},
+ },
+ },
Managers: map[string]ConfigManager{
"restricted-manager": {
RestrictedTestingRepo: "git://restricted.git/restricted.git",
@@ -109,6 +131,13 @@ var testConfig = &GlobalConfig{
Clients: map[string]string{
clientAdmin: keyAdmin,
},
+ Repos: []KernelRepo{
+ {
+ URL: "git://syzkaller.org/access-admin.git",
+ Branch: "access-admin",
+ Alias: "access-admin",
+ },
+ },
Reporting: []Reporting{
{
Name: "access-admin-reporting1",
@@ -126,6 +155,13 @@ var testConfig = &GlobalConfig{
Clients: map[string]string{
clientUser: keyUser,
},
+ Repos: []KernelRepo{
+ {
+ URL: "git://syzkaller.org/access-user.git",
+ Branch: "access-user",
+ Alias: "access-user",
+ },
+ },
Reporting: []Reporting{
{
AccessLevel: AccessAdmin,
@@ -144,6 +180,13 @@ var testConfig = &GlobalConfig{
Clients: map[string]string{
clientPublic: keyPublic,
},
+ Repos: []KernelRepo{
+ {
+ URL: "git://syzkaller.org/access-public.git",
+ Branch: "access-public",
+ Alias: "access-public",
+ },
+ },
Reporting: []Reporting{
{
AccessLevel: AccessUser,
@@ -157,12 +200,6 @@ var testConfig = &GlobalConfig{
},
},
},
- KernelRepos: map[string]KernelRepo{
- "repo10/branch10": {
- Alias: "repo10alias",
- CC: []string{"maintainers@repo10.org", "bugs@repo10.org"},
- },
- },
}
const (
diff --git a/dashboard/app/bug.html b/dashboard/app/bug.html
index fa59f14bc..8b3afaf3d 100644
--- a/dashboard/app/bug.html
+++ b/dashboard/app/bug.html
@@ -18,12 +18,12 @@ Page with details about a single bug.
Status: {{if .Bug.ExternalLink}}<a href="{{.Bug.ExternalLink}}">{{.Bug.Status}}</a>{{else}}{{.Bug.Status}}{{end}}<br>
Reported-by: {{.Bug.CreditEmail}}<br>
{{if .Bug.Commits}}
- Commits: {{.Bug.Commits}}<br>
+ Fix commit: {{template "fix_commits" .Bug.Commits}}<br>
{{if .Bug.ClosedTime.IsZero}}
Patched on: {{.Bug.PatchedOn}}, missing on: {{.Bug.MissingOn}}<br>
{{end}}
{{end}}
- First: {{formatLateness $.Now $.Bug.FirstTime}}, last: {{formatLateness $.Now $.Bug.LastTime}}<br>
+ First crash: {{formatLateness $.Now $.Bug.FirstTime}}, last: {{formatLateness $.Now $.Bug.LastTime}}<br>
{{template "bug_list" .DupOf}}
{{template "bug_list" .Dups}}
@@ -57,8 +57,8 @@ Page with details about a single bug.
<td class="time">{{formatTime $c.Time}}</td>
<td class="kernel" title="{{$c.KernelAlias}}">{{$c.KernelAlias}}</td>
<td class="tag" title="{{$c.KernelCommit}}
-{{formatTime $c.KernelCommitDate}}">{{formatShortHash $c.KernelCommit}}</td>
- <td class="tag" title="{{$c.SyzkallerCommit}}">{{formatShortHash $c.SyzkallerCommit}}</td>
+{{formatTime $c.KernelCommitDate}}">{{link $c.KernelCommitLink (formatShortHash $c.KernelCommit)}}</td>
+ <td class="tag">{{link $c.SyzkallerCommitLink (formatShortHash $c.SyzkallerCommit)}}</td>
<td class="config">{{if $c.KernelConfigLink}}<a href="{{$c.KernelConfigLink}}">.config</a>{{end}}</td>
<td class="repro">{{if $c.LogLink}}<a href="{{$c.LogLink}}">log</a>{{end}}</td>
<td class="repro">{{if $c.ReportLink}}<a href="{{$c.ReportLink}}">report</a>{{end}}</td>
diff --git a/dashboard/app/commit_poll_test.go b/dashboard/app/commit_poll_test.go
new file mode 100644
index 000000000..48c86292f
--- /dev/null
+++ b/dashboard/app/commit_poll_test.go
@@ -0,0 +1,94 @@
+// Copyright 2019 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.
+
+// +build aetest
+
+package dash
+
+import (
+ "sort"
+ "testing"
+
+ "github.com/google/syzkaller/dashboard/dashapi"
+)
+
+func TestCommitPoll(t *testing.T) {
+ c := NewCtx(t)
+ defer c.Close()
+
+ build1 := testBuild(1)
+ c.client.UploadBuild(build1)
+
+ crash1 := testCrash(build1, 1)
+ c.client.ReportCrash(crash1)
+ rep1 := c.client.pollBug()
+
+ crash2 := testCrash(build1, 2)
+ c.client.ReportCrash(crash2)
+ rep2 := c.client.pollBug()
+
+ // No commits in commit poll.
+ commitPollResp, err := c.client.CommitPoll()
+ c.expectOK(err)
+ c.expectEQ(len(commitPollResp.Repos), 2)
+ c.expectEQ(commitPollResp.Repos[0].URL, testConfig.Namespaces["test1"].Repos[0].URL)
+ c.expectEQ(commitPollResp.Repos[0].Branch, testConfig.Namespaces["test1"].Repos[0].Branch)
+ c.expectEQ(commitPollResp.Repos[1].URL, testConfig.Namespaces["test1"].Repos[1].URL)
+ c.expectEQ(commitPollResp.Repos[1].Branch, testConfig.Namespaces["test1"].Repos[1].Branch)
+ c.expectEQ(len(commitPollResp.Commits), 0)
+
+ // Specify fixing commit for the bug.
+ reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{
+ ID: rep1.ID,
+ Status: dashapi.BugStatusOpen,
+ FixCommits: []string{"foo: fix1", "foo: fix2"},
+ })
+ c.expectEQ(reply.OK, true)
+
+ // The commit should appear in commit poll.
+ for i := 0; i < 2; i++ {
+ commitPollResp, err = c.client.CommitPoll()
+ c.expectOK(err)
+ c.expectEQ(len(commitPollResp.Commits), 2)
+ sort.Strings(commitPollResp.Commits)
+ c.expectEQ(commitPollResp.Commits[0], "foo: fix1")
+ c.expectEQ(commitPollResp.Commits[1], "foo: fix2")
+ }
+
+ // Upload hash for the first commit and fixing commit for the second bug.
+ c.expectOK(c.client.UploadCommits([]dashapi.Commit{
+ {Hash: "hash1", Title: "foo: fix1"},
+ {Hash: "hash2", Title: "bar: fix3", BugIDs: []string{rep2.ID}},
+ {Hash: "hash3", Title: "some unrelated commit", BugIDs: []string{"does not exist"}},
+ {Hash: "hash4", Title: "another unrelated commit"},
+ }))
+
+ commitPollResp, err = c.client.CommitPoll()
+ c.expectOK(err)
+ c.expectEQ(len(commitPollResp.Commits), 2)
+ sort.Strings(commitPollResp.Commits)
+ c.expectEQ(commitPollResp.Commits[0], "foo: fix1")
+ c.expectEQ(commitPollResp.Commits[1], "foo: fix2")
+
+ // Upload hash for the second commit and a new fixing commit for the second bug.
+ c.expectOK(c.client.UploadCommits([]dashapi.Commit{
+ {Hash: "hash5", Title: "foo: fix2"},
+ {Title: "bar: fix4", BugIDs: []string{rep2.ID}},
+ }))
+
+ commitPollResp, err = c.client.CommitPoll()
+ c.expectOK(err)
+ c.expectEQ(len(commitPollResp.Commits), 1)
+ c.expectEQ(commitPollResp.Commits[0], "bar: fix4")
+
+ // Upload hash for the second commit and a new fixing commit for the second bug.
+ c.expectOK(c.client.UploadCommits([]dashapi.Commit{
+ {Hash: "hash1", Title: "foo: fix1"},
+ {Hash: "hash5", Title: "foo: fix2"},
+ {Hash: "hash6", Title: "bar: fix4"},
+ }))
+
+ commitPollResp, err = c.client.CommitPoll()
+ c.expectOK(err)
+ c.expectEQ(len(commitPollResp.Commits), 0)
+}
diff --git a/dashboard/app/config.go b/dashboard/app/config.go
index 26487ae6b..f0f3d168d 100644
--- a/dashboard/app/config.go
+++ b/dashboard/app/config.go
@@ -12,6 +12,7 @@ import (
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/email"
+ "github.com/google/syzkaller/pkg/vcs"
)
// There are multiple configurable aspects of the app (namespaces, reporting, API clients, etc).
@@ -38,8 +39,6 @@ type GlobalConfig struct {
// Each namespace has own reporting config, own API clients
// and bugs are not merged across namespaces.
Namespaces map[string]*Config
- // Maps full repository address/branch to description of this repo.
- KernelRepos map[string]KernelRepo
}
// Per-namespace config.
@@ -70,6 +69,11 @@ type Config struct {
TransformCrash func(build *Build, crash *dashapi.Crash) bool
// NeedRepro hook can be used to prevent reproduction of some bugs.
NeedRepro func(bug *Bug) bool
+ // List of kernel repositories for this namespace.
+ // The first repo considered the "main" repo (e.g. fixing commit info is shown against this repo).
+ // Other repos are secondary repos, they may be tested or not.
+ // If not tested they are used to poll for fixing commits.
+ Repos []KernelRepo
}
// ConfigManager describes a single syz-manager instance.
@@ -114,6 +118,8 @@ type ReportingType interface {
}
type KernelRepo struct {
+ URL string
+ Branch string
// Alias is a short, readable name of a kernel repository.
Alias string
// ReportingPriority says if we need to prefer to report crashes in this
@@ -164,7 +170,14 @@ func installConfig(cfg *GlobalConfig) {
if config != nil {
panic("another config is already installed")
}
- // Validate the global cfg.
+ checkConfig(cfg)
+ config = cfg
+ initEmailReporting()
+ initHTTPHandlers()
+ initAPIHandlers()
+}
+
+func checkConfig(cfg *GlobalConfig) {
if len(cfg.Namespaces) == 0 {
panic("no namespaces found")
}
@@ -178,23 +191,6 @@ func installConfig(cfg *GlobalConfig) {
for ns, cfg := range cfg.Namespaces {
checkNamespace(ns, cfg, namespaces, clientNames)
}
- for repo, info := range cfg.KernelRepos {
- if info.Alias == "" {
- panic(fmt.Sprintf("empty kernel repo alias for %q", repo))
- }
- if prio := info.ReportingPriority; prio < 0 || prio > 9 {
- panic(fmt.Sprintf("bad kernel repo reporting priority %v for %q", prio, repo))
- }
- for _, email := range info.CC {
- if _, err := mail.ParseAddress(email); err != nil {
- panic(fmt.Sprintf("bad email address %q: %v", email, err))
- }
- }
- }
- config = cfg
- initEmailReporting()
- initHTTPHandlers()
- initAPIHandlers()
}
func checkNamespace(ns string, cfg *Config, namespaces, clientNames map[string]bool) {
@@ -231,6 +227,36 @@ func checkNamespace(ns string, cfg *Config, namespaces, clientNames map[string]b
return true
}
}
+ checkKernelRepos(ns, cfg)
+ checkNamespaceReporting(ns, cfg)
+}
+
+func checkKernelRepos(ns string, cfg *Config) {
+ if len(cfg.Repos) == 0 {
+ panic(fmt.Sprintf("no repos in namespace %q", ns))
+ }
+ for _, repo := range cfg.Repos {
+ if !vcs.CheckRepoAddress(repo.URL) {
+ panic(fmt.Sprintf("%v: bad repo URL %q", ns, repo.URL))
+ }
+ if !vcs.CheckBranch(repo.Branch) {
+ panic(fmt.Sprintf("%v: bad repo branch %q", ns, repo.Branch))
+ }
+ if repo.Alias == "" {
+ panic(fmt.Sprintf("%v: empty repo alias for %q", ns, repo.Alias))
+ }
+ if prio := repo.ReportingPriority; prio < 0 || prio > 9 {
+ panic(fmt.Sprintf("%v: bad kernel repo reporting priority %v for %q", ns, prio, repo.Alias))
+ }
+ for _, email := range repo.CC {
+ if _, err := mail.ParseAddress(email); err != nil {
+ panic(fmt.Sprintf("bad email address %q: %v", email, err))
+ }
+ }
+ }
+}
+
+func checkNamespaceReporting(ns string, cfg *Config) {
checkConfigAccessLevel(&cfg.AccessLevel, cfg.AccessLevel, fmt.Sprintf("namespace %q", ns))
parentAccessLevel := cfg.AccessLevel
reportingNames := make(map[string]bool)
diff --git a/dashboard/app/email_test.go b/dashboard/app/email_test.go
index 560dc54ca..557c4cccf 100644
--- a/dashboard/app/email_test.go
+++ b/dashboard/app/email_test.go
@@ -118,6 +118,8 @@ For more options, visit https://groups.google.com/d/optout.
// Now report syz reproducer and check updated email.
build2 := testBuild(10)
+ build2.KernelRepo = testConfig.Namespaces["test2"].Repos[0].URL
+ build2.KernelBranch = testConfig.Namespaces["test2"].Repos[0].Branch
build2.KernelCommitTitle = "a really long title, longer than 80 chars, really long-long-long-long-long-long title"
c.client2.UploadBuild(build2)
crash.BuildID = build2.ID
diff --git a/dashboard/app/entities.go b/dashboard/app/entities.go
index 3ffe09b7d..705f4a26d 100644
--- a/dashboard/app/entities.go
+++ b/dashboard/app/entities.go
@@ -74,6 +74,7 @@ type Bug struct {
NumRepro int64
ReproLevel dashapi.ReproLevel
HasReport bool
+ NeedCommitInfo bool
FirstTime time.Time
LastTime time.Time
LastSavedCrash time.Time
@@ -82,9 +83,16 @@ type Bug struct {
LastActivity time.Time // last time we observed any activity related to the bug
Closed time.Time
Reporting []BugReporting
- Commits []string
- HappenedOn []string `datastore:",noindex"` // list of managers
- PatchedOn []string `datastore:",noindex"` // list of managers
+ Commits []string // titles of fixing commmits
+ CommitInfo []CommitInfo // git hashes of fixing commits (parallel array to Commits)
+ HappenedOn []string `datastore:",noindex"` // list of managers
+ PatchedOn []string `datastore:",noindex"` // list of managers
+}
+
+type CommitInfo struct {
+ Hash string
+ Author string
+ Date time.Time
}
type BugReporting struct {
@@ -362,18 +370,35 @@ func bugReportingHash(bugHash, reporting string) string {
return hash.String([]byte(fmt.Sprintf("%v-%v", bugHash, reporting)))[:hashLen]
}
+func (bug *Bug) updateCommits(commits []string, now time.Time) {
+ bug.Commits = commits
+ bug.CommitInfo = nil
+ bug.NeedCommitInfo = true
+ bug.FixTime = now
+ bug.PatchedOn = nil
+}
+
+func (bug *Bug) getCommitInfo(i int) CommitInfo {
+ if i < len(bug.CommitInfo) {
+ return bug.CommitInfo[i]
+ }
+ return CommitInfo{}
+}
+
func kernelRepoInfo(build *Build) KernelRepo {
- return kernelRepoInfoRaw(build.KernelRepo, build.KernelBranch)
+ return kernelRepoInfoRaw(build.Namespace, build.KernelRepo, build.KernelBranch)
}
-func kernelRepoInfoRaw(repo, branch string) KernelRepo {
- repoID := repo
- if branch != "" {
- repoID += "/" + branch
+func kernelRepoInfoRaw(ns, url, branch string) KernelRepo {
+ var info KernelRepo
+ for _, repo := range config.Namespaces[ns].Repos {
+ if repo.URL == url && repo.Branch == branch {
+ info = repo
+ break
+ }
}
- info := config.KernelRepos[repoID]
if info.Alias == "" {
- info.Alias = repo
+ info.Alias = url
if branch != "" {
info.Alias += " " + branch
}
diff --git a/dashboard/app/fix_test.go b/dashboard/app/fix_test.go
index b0c7bee2b..448ee73a7 100644
--- a/dashboard/app/fix_test.go
+++ b/dashboard/app/fix_test.go
@@ -418,7 +418,10 @@ func TestFixedWithCommitTags(t *testing.T) {
rep := c.client.pollBug()
// Upload build with 2 fixing commits for this bug.
- build1.FixCommits = []dashapi.FixCommit{{"fix commit 1", rep.ID}, {"fix commit 2", rep.ID}}
+ build1.FixCommits = []dashapi.Commit{
+ {Title: "fix commit 1", BugIDs: []string{rep.ID}},
+ {Title: "fix commit 2", BugIDs: []string{rep.ID}},
+ }
c.client.UploadBuild(build1)
// Now the commits must be associated with the bug and the second
@@ -472,7 +475,9 @@ func TestFixedDup(t *testing.T) {
c.client.updateBug(rep2.ID, dashapi.BugStatusDup, rep1.ID)
// Upload build that fixes rep2.
- build.FixCommits = []dashapi.FixCommit{{"fix commit 1", rep2.ID}}
+ build.FixCommits = []dashapi.Commit{
+ {Title: "fix commit 1", BugIDs: []string{rep2.ID}},
+ }
c.client.UploadBuild(build)
// This must fix rep1.
@@ -505,16 +510,11 @@ func TestFixedDup2(t *testing.T) {
c.client.updateBug(rep2.ID, dashapi.BugStatusDup, rep1.ID)
// Upload build that fixes rep2.
- build1.FixCommits = []dashapi.FixCommit{{"fix commit 1", rep2.ID}}
+ build1.FixCommits = []dashapi.Commit{
+ {Title: "fix commit 1", BugIDs: []string{rep2.ID}},
+ }
c.client.UploadBuild(build1)
- /*
- dbBug1, _, _ := c.loadBug(rep1.ID)
- t.Logf("BUG1: status=%v, commits: %+v, patched: %+v", dbBug1.Status, dbBug1.Commits, dbBug1.PatchedOn)
- dbBug2, _, _ := c.loadBug(rep2.ID)
- t.Logf("BUG2: status=%v, commits: %+v, patched: %+v", dbBug2.Status, dbBug2.Commits, dbBug2.PatchedOn)
- */
-
// Now undup the bugs. They are still unfixed as only 1 manager uploaded the commit.
c.client.updateBug(rep2.ID, dashapi.BugStatusOpen, "")
@@ -557,7 +557,10 @@ func TestFixedDup3(t *testing.T) {
// Upload builds that fix rep1 and rep2 with different commits.
// This must fix rep1 eventually and we must not livelock in such scenario.
- build1.FixCommits = []dashapi.FixCommit{{"fix commit 1", rep1.ID}, {"fix commit 2", rep2.ID}}
+ build1.FixCommits = []dashapi.Commit{
+ {Title: "fix commit 1", BugIDs: []string{rep1.ID}},
+ {Title: "fix commit 2", BugIDs: []string{rep2.ID}},
+ }
build2.FixCommits = build1.FixCommits
c.client.UploadBuild(build1)
c.client.UploadBuild(build2)
diff --git a/dashboard/app/index.yaml b/dashboard/app/index.yaml
index 4adbf1527..56f82c937 100644
--- a/dashboard/app/index.yaml
+++ b/dashboard/app/index.yaml
@@ -22,6 +22,12 @@ indexes:
- name: Seq
direction: desc
+- kind: Bug
+ properties:
+ - name: Namespace
+ - name: NeedCommitInfo
+ - name: Commits
+
- kind: Build
properties:
- name: Namespace
diff --git a/dashboard/app/main.go b/dashboard/app/main.go
index 044b4f767..8c3f624c8 100644
--- a/dashboard/app/main.go
+++ b/dashboard/app/main.go
@@ -15,6 +15,7 @@ import (
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/email"
"github.com/google/syzkaller/pkg/html"
+ "github.com/google/syzkaller/pkg/vcs"
"golang.org/x/net/context"
"google.golang.org/appengine/datastore"
"google.golang.org/appengine/log"
@@ -62,12 +63,21 @@ type uiManager struct {
}
type uiBuild struct {
- Time time.Time
- SyzkallerCommit string
- KernelAlias string
- KernelCommit string
- KernelCommitDate time.Time
- KernelConfigLink string
+ Time time.Time
+ SyzkallerCommit string
+ SyzkallerCommitLink string
+ KernelAlias string
+ KernelCommit string
+ KernelCommitLink string
+ KernelCommitDate time.Time
+ KernelConfigLink string
+}
+
+type uiCommit struct {
+ Hash string
+ Title string
+ Link string
+ Date time.Time
}
type uiBugPage struct {
@@ -119,7 +129,7 @@ type uiBug struct {
Link string
ExternalLink string
CreditEmail string
- Commits string
+ Commits []uiCommit
PatchedOn []string
MissingOn []string
NumManagers int
@@ -165,6 +175,7 @@ func handleMain(c context.Context, w http.ResponseWriter, r *http.Request) error
var managers []*uiManager
var jobs []*uiJob
accessLevel := accessLevel(c, r)
+
if r.FormValue("fixed") == "" {
var err error
managers, err = loadManagers(c, accessLevel)
@@ -422,7 +433,7 @@ func fetchNamespaceBugs(c context.Context, accessLevel AccessLevel, ns string,
id := uiBug.ReportingIndex
if bug.Status == BugStatusFixed {
id = -1
- } else if uiBug.Commits != "" {
+ } else if len(uiBug.Commits) != 0 {
id = -2
}
groups[id] = append(groups[id], uiBug)
@@ -622,9 +633,14 @@ func createUIBug(c context.Context, bug *Bug, state *ReportingState, managers []
}
updateBugBadness(c, uiBug)
if len(bug.Commits) != 0 {
- uiBug.Commits = bug.Commits[0]
- if len(bug.Commits) > 1 {
- uiBug.Commits = fmt.Sprintf("%q", bug.Commits)
+ for i, com := range bug.Commits {
+ cfg := config.Namespaces[bug.Namespace]
+ info := bug.getCommitInfo(i)
+ uiBug.Commits = append(uiBug.Commits, uiCommit{
+ Hash: info.Hash,
+ Title: com,
+ Link: vcs.CommitLink(cfg.Repos[0].URL, info.Hash),
+ })
}
for _, mgr := range managers {
found := false
@@ -700,12 +716,14 @@ func loadCrashesForBug(c context.Context, bug *Bug) ([]*uiCrash, []byte, error)
func makeUIBuild(build *Build) *uiBuild {
return &uiBuild{
- Time: build.Time,
- SyzkallerCommit: build.SyzkallerCommit,
- KernelAlias: kernelRepoInfo(build).Alias,
- KernelCommit: build.KernelCommit,
- KernelCommitDate: build.KernelCommitDate,
- KernelConfigLink: textLink(textKernelConfig, build.KernelConfig),
+ Time: build.Time,
+ SyzkallerCommit: build.SyzkallerCommit,
+ SyzkallerCommitLink: vcs.LogLink(vcs.SyzkallerRepo, build.SyzkallerCommit),
+ KernelAlias: kernelRepoInfo(build).Alias,
+ KernelCommit: build.KernelCommit,
+ KernelCommitLink: vcs.LogLink(build.KernelRepo, build.KernelCommit),
+ KernelCommitDate: build.KernelCommitDate,
+ KernelConfigLink: textLink(textKernelConfig, build.KernelConfig),
}
}
@@ -813,7 +831,7 @@ func loadRecentJobs(c context.Context) ([]*uiJob, error) {
Namespace: job.Namespace,
Manager: job.Manager,
BugTitle: job.BugTitle,
- KernelAlias: kernelRepoInfoRaw(job.KernelRepo, job.KernelBranch).Alias,
+ KernelAlias: kernelRepoInfoRaw(job.Namespace, job.KernelRepo, job.KernelBranch).Alias,
PatchLink: textLink(textPatch, job.Patch),
Attempts: job.Attempts,
Started: job.Started,
diff --git a/dashboard/app/noaetest.go b/dashboard/app/noaetest.go
new file mode 100644
index 000000000..5c461b779
--- /dev/null
+++ b/dashboard/app/noaetest.go
@@ -0,0 +1,8 @@
+// Copyright 2019 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.
+
+// +build !aetest
+
+package dash
+
+const isAppEngineTest = false
diff --git a/dashboard/app/reporting.go b/dashboard/app/reporting.go
index 32d3fc8e6..c038c67cb 100644
--- a/dashboard/app/reporting.go
+++ b/dashboard/app/reporting.go
@@ -511,9 +511,7 @@ func incomingCommandTx(c context.Context, now time.Time, cmd *dashapi.BugUpdate,
if len(cmd.FixCommits) != 0 && (bug.Status == BugStatusOpen || bug.Status == BugStatusDup) {
sort.Strings(cmd.FixCommits)
if !reflect.DeepEqual(bug.Commits, cmd.FixCommits) {
- bug.Commits = cmd.FixCommits
- bug.FixTime = now
- bug.PatchedOn = nil
+ bug.updateCommits(cmd.FixCommits, now)
}
}
if cmd.CrashID != 0 {
diff --git a/dashboard/app/static/style.css b/dashboard/app/static/style.css
index 6ac90a5f9..3a42bf93e 100644
--- a/dashboard/app/static/style.css
+++ b/dashboard/app/static/style.css
@@ -77,6 +77,11 @@ table td, table th {
max-width: 350pt;
}
+.list_table .commit_list {
+ width: 500pt;
+ max-width: 500pt;
+}
+
.list_table .tag {
font-family: monospace;
font-size: 8pt;
@@ -101,8 +106,8 @@ table td, table th {
}
.list_table .kernel {
- width: 60pt;
- max-width: 60pt;
+ width: 80pt;
+ max-width: 80pt;
}
.list_table .maintainers {
@@ -151,3 +156,7 @@ textarea {
width:100%;
font-family: monospace;
}
+
+.mono {
+ font-family: monospace;
+}
diff --git a/dashboard/app/templates.html b/dashboard/app/templates.html
index 2047db570..4c81af27e 100644
--- a/dashboard/app/templates.html
+++ b/dashboard/app/templates.html
@@ -82,10 +82,10 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the
</td>
{{if $.ShowPatch}}
<td class="stat">{{formatLateness $.Now $b.ClosedTime}}</td>
- <td class="title" title="{{$b.Commits}}">{{$b.Commits}}</td>
+ <td class="commit_list">{{template "fix_commits" $b.Commits}}</td>
{{end}}
{{if $.ShowPatched}}
- <td class="patched" title="{{$b.Commits}}">{{if $b.Commits}}{{len $b.PatchedOn}}/{{$b.NumManagers}}{{end}}</td>
+ <td class="patched" {{if $b.Commits}}title="{{with $com := index $b.Commits 0}}{{$com.Title}}{{end}}"{{end}}>{{len $b.PatchedOn}}/{{$b.NumManagers}}</td>
{{end}}
{{if $.ShowStatus}}
<td class="status">
@@ -130,11 +130,11 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the
</td>
<td class="stat {{if $mgr.LastActiveBad}}bad{{end}}">{{formatLateness $mgr.Now $mgr.LastActive}}</td>
<td class="stat">{{formatDuration $mgr.CurrentUpTime}}</td>
- {{if $mgr.CurrentBuild}}
- <td class="stat">{{formatLateness $mgr.Now $mgr.CurrentBuild.Time}}</td>
- <td class="stat" title="{{$mgr.CurrentBuild.KernelAlias}} {{$mgr.CurrentBuild.KernelCommit}}
-{{formatTime $mgr.CurrentBuild.KernelCommitDate}}">{{formatShortHash $mgr.CurrentBuild.KernelCommit}}</td>
- <td class="stat" title="{{$mgr.CurrentBuild.SyzkallerCommit}}">{{formatShortHash $mgr.CurrentBuild.SyzkallerCommit}}</td>
+ {{with $build := $mgr.CurrentBuild}}
+ <td class="stat">{{formatLateness $mgr.Now $build.Time}}</td>
+ <td class="stat" title="{{$build.KernelAlias}} {{$build.KernelCommit}}
+{{formatTime $build.KernelCommitDate}}">{{link $build.KernelCommitLink (formatShortHash $mgr.CurrentBuild.KernelCommit)}}</td>
+ <td class="stat">{{link $mgr.CurrentBuild.SyzkallerCommitLink (formatShortHash $mgr.CurrentBuild.SyzkallerCommit)}}</td>
{{else}}
<td></td>
<td></td>
@@ -158,3 +158,15 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the
</table>
{{end}}
{{end}}
+
+{{/* List of fixing commits, invoked with []*uiCommit */}}
+{{define "fix_commits"}}
+{{range $com := .}}
+ <span class="mono">
+ {{if $com.Hash}}
+ {{formatShortHash $com.Hash}}
+ {{end}}
+ {{link $com.Link $com.Title}}
+ </span>
+{{end}}
+{{end}}
diff --git a/dashboard/dashapi/dashapi.go b/dashboard/dashapi/dashapi.go
index bec406d78..02d7ea132 100644
--- a/dashboard/dashapi/dashapi.go
+++ b/dashboard/dashapi/dashapi.go
@@ -68,12 +68,15 @@ type Build struct {
KernelCommitDate time.Time
KernelConfig []byte
Commits []string // see BuilderPoll
- FixCommits []FixCommit
+ FixCommits []Commit
}
-type FixCommit struct {
- Title string
- BugID string
+type Commit struct {
+ Hash string
+ Title string
+ Author string
+ BugIDs []string // ID's extracted from Reported-by tags
+ Date time.Time
}
func (dash *Dashboard) UploadBuild(build *Build) error {
@@ -161,6 +164,34 @@ func (dash *Dashboard) ReportBuildError(req *BuildErrorReq) error {
return dash.Query("report_build_error", req, nil)
}
+type CommitPollResp struct {
+ ReportEmail string
+ Repos []Repo
+ Commits []string
+}
+
+type CommitPollResultReq struct {
+ Commits []Commit
+}
+
+type Repo struct {
+ URL string
+ Branch string
+}
+
+func (dash *Dashboard) CommitPoll() (*CommitPollResp, error) {
+ resp := new(CommitPollResp)
+ err := dash.Query("commit_poll", nil, resp)
+ return resp, err
+}
+
+func (dash *Dashboard) UploadCommits(commits []Commit) error {
+ if len(commits) == 0 {
+ return nil
+ }
+ return dash.Query("upload_commits", &CommitPollResultReq{commits}, nil)
+}
+
// Crash describes a single kernel crash (potentially with repro).
type Crash struct {
BuildID string // refers to Build.ID