| // Copyright 2017 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 dash |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "strconv" |
| "strings" |
| "time" |
| |
| "github.com/google/syzkaller/dashboard/dashapi" |
| "github.com/google/syzkaller/pkg/email" |
| "github.com/google/syzkaller/pkg/git" |
| "golang.org/x/net/context" |
| "google.golang.org/appengine/datastore" |
| "google.golang.org/appengine/log" |
| ) |
| |
| // handleTestRequest added new job to datastore. |
| // Returns empty string if job added successfully, or reason why it wasn't added. |
| func handleTestRequest(c context.Context, bugID, user, extID, patch, repo, branch string) string { |
| log.Infof(c, "test request: bug=%q user=%q extID=%q patch=%v, repo=%q branch=%q", |
| bugID, user, extID, len(patch), repo, branch) |
| reply, err := addTestJob(c, bugID, user, extID, patch, repo, branch) |
| if err != nil { |
| log.Errorf(c, "test request failed: %v", err) |
| if reply == "" { |
| reply = internalError |
| } |
| } |
| return reply |
| } |
| |
| func addTestJob(c context.Context, bugID, user, extID, patch, repo, branch string) (string, error) { |
| bug, bugKey, err := findBugByReportingID(c, bugID) |
| if err != nil { |
| return "can't find associated bug", err |
| } |
| bugReporting, _ := bugReportingByID(bug, bugID) |
| |
| // TODO(dvyukov): find the exact crash that we reported. |
| crash, crashKey, err := findCrashForBug(c, bug) |
| if err != nil { |
| return "", err |
| } |
| if crash.ReproC == 0 && crash.ReproSyz == 0 { |
| return "This crash does not have a reproducer. I cannot test it.", nil |
| } |
| |
| switch { |
| case !git.CheckRepoAddress(repo): |
| return fmt.Sprintf("%q does not look like a valid git repo address.", repo), nil |
| case !git.CheckBranch(branch): |
| return fmt.Sprintf("%q does not look like a valid git branch name.", branch), nil |
| case len(patch) == 0: |
| return "I don't see any patch attached to the request.", nil |
| case crash.ReproC == 0 && crash.ReproSyz == 0: |
| return "This crash does not have a reproducer. I cannot test it.", nil |
| case bug.Status == BugStatusFixed: |
| return "This bug is already marked as fixed. No point in testing.", nil |
| case bug.Status == BugStatusInvalid: |
| return "This bug is already marked as invalid. No point in testing.", nil |
| // TODO(dvyukov): for BugStatusDup check status of the canonical bug. |
| case !bugReporting.Closed.IsZero(): |
| return "This bug is already upstreamed. Please test upstream.", nil |
| } |
| |
| patchID, err := putText(c, bug.Namespace, "Patch", []byte(patch), false) |
| if err != nil { |
| return "", err |
| } |
| |
| manager := crash.Manager |
| for _, ns := range config.Namespaces { |
| if delegated, ok := ns.DecommissionedManagers[manager]; ok { |
| manager = delegated |
| } |
| } |
| |
| job := &Job{ |
| Created: timeNow(c), |
| User: user, |
| Reporting: bugReporting.Name, |
| ExtID: extID, |
| Namespace: bug.Namespace, |
| Manager: manager, |
| BugTitle: bug.displayTitle(), |
| CrashID: crashKey.IntID(), |
| KernelRepo: repo, |
| KernelBranch: branch, |
| Patch: patchID, |
| } |
| jobKey, err := datastore.Put(c, datastore.NewIncompleteKey(c, "Job", bugKey), job) |
| if err != nil { |
| return "", fmt.Errorf("failed to put job: %v", err) |
| } |
| jobID := extJobID(jobKey) |
| |
| // Add user to bug reporting CC. |
| tx := func(c context.Context) error { |
| bug := new(Bug) |
| if err := datastore.Get(c, bugKey, bug); err != nil { |
| return err |
| } |
| bugReporting := bugReportingByName(bug, bugReporting.Name) |
| cc := strings.Split(bugReporting.CC, "|") |
| if stringInList(cc, user) { |
| return nil |
| } |
| merged := email.MergeEmailLists(cc, []string{user}) |
| bugReporting.CC = strings.Join(merged, "|") |
| if _, err := datastore.Put(c, bugKey, bug); err != nil { |
| return err |
| } |
| return nil |
| } |
| if err := datastore.RunInTransaction(c, tx, nil); err != nil { |
| // We've already stored the job, so just log the error. |
| log.Errorf(c, "job %v: failed to update bug: %v", jobID, err) |
| } |
| return "", nil |
| } |
| |
| func updateTestJob(c context.Context, extID, link string) error { |
| var jobs []*Job |
| keys, err := datastore.NewQuery("Job"). |
| Filter("ExtID=", extID). |
| GetAll(c, &jobs) |
| if len(jobs) != 1 || err != nil { |
| return fmt.Errorf("failed to query jobs: jobs=%v err=%v", len(jobs), err) |
| } |
| job, jobKey := jobs[0], keys[0] |
| if job.Link != "" { |
| return nil |
| } |
| tx := func(c context.Context) error { |
| job := new(Job) |
| if err := datastore.Get(c, jobKey, job); err != nil { |
| return err |
| } |
| job.Link = link |
| if _, err := datastore.Put(c, jobKey, job); err != nil { |
| return err |
| } |
| return nil |
| } |
| return datastore.RunInTransaction(c, tx, nil) |
| } |
| |
| // pollPendingJobs returns the next job to execute for the provided list of managers. |
| func pollPendingJobs(c context.Context, managers []string) (interface{}, error) { |
| retry: |
| job, jobKey, err := loadPendingJob(c, managers) |
| if job == nil || err != nil { |
| return job, err |
| } |
| jobID := extJobID(jobKey) |
| patch, err := getText(c, "Patch", job.Patch) |
| if err != nil { |
| return nil, err |
| } |
| bugKey := jobKey.Parent() |
| crashKey := datastore.NewKey(c, "Crash", "", job.CrashID, bugKey) |
| crash := new(Crash) |
| if err := datastore.Get(c, crashKey, crash); err != nil { |
| return nil, fmt.Errorf("job %v: failed to get crash: %v", jobID, err) |
| } |
| |
| build, err := loadBuild(c, job.Namespace, crash.BuildID) |
| if err != nil { |
| return nil, err |
| } |
| kernelConfig, err := getText(c, "KernelConfig", build.KernelConfig) |
| if err != nil { |
| return nil, err |
| } |
| |
| reproC, err := getText(c, "ReproC", crash.ReproC) |
| if err != nil { |
| return nil, err |
| } |
| reproSyz, err := getText(c, "ReproSyz", crash.ReproSyz) |
| if err != nil { |
| return nil, err |
| } |
| |
| now := timeNow(c) |
| stale := false |
| tx := func(c context.Context) error { |
| stale = false |
| job := new(Job) |
| if err := datastore.Get(c, jobKey, job); err != nil { |
| return fmt.Errorf("job %v: failed to get in tx: %v", jobID, err) |
| } |
| if !job.Finished.IsZero() { |
| // This happens sometimes due to inconsistent datastore. |
| stale = true |
| return nil |
| } |
| job.Attempts++ |
| job.Started = now |
| if _, err := datastore.Put(c, jobKey, job); err != nil { |
| return fmt.Errorf("job %v: failed to put: %v", jobID, err) |
| } |
| return nil |
| } |
| if err := datastore.RunInTransaction(c, tx, nil); err != nil { |
| return nil, err |
| } |
| if stale { |
| goto retry |
| } |
| resp := &dashapi.JobPollResp{ |
| ID: jobID, |
| Manager: job.Manager, |
| KernelRepo: job.KernelRepo, |
| KernelBranch: job.KernelBranch, |
| KernelConfig: kernelConfig, |
| SyzkallerCommit: build.SyzkallerCommit, |
| Patch: patch, |
| ReproOpts: crash.ReproOpts, |
| ReproSyz: reproSyz, |
| ReproC: reproC, |
| } |
| return resp, nil |
| } |
| |
| // doneJob is called by syz-ci to mark completion of a job. |
| func doneJob(c context.Context, req *dashapi.JobDoneReq) error { |
| jobID := req.ID |
| jobKey, err := jobID2Key(c, req.ID) |
| if err != nil { |
| return err |
| } |
| now := timeNow(c) |
| tx := func(c context.Context) error { |
| job := new(Job) |
| if err := datastore.Get(c, jobKey, job); err != nil { |
| return fmt.Errorf("job %v: failed to get job: %v", jobID, err) |
| } |
| if !job.Finished.IsZero() { |
| return fmt.Errorf("job %v: already finished", jobID) |
| } |
| ns := job.Namespace |
| if isNewBuild, err := uploadBuild(c, ns, &req.Build, BuildJob); err != nil { |
| return err |
| } else if !isNewBuild { |
| log.Warningf(c, "job %v: duplicate build %v", jobID, req.Build.ID) |
| } |
| if job.Error, err = putText(c, ns, "Error", req.Error, false); err != nil { |
| return err |
| } |
| if job.CrashLog, err = putText(c, ns, "CrashLog", req.CrashLog, false); err != nil { |
| return err |
| } |
| if job.CrashReport, err = putText(c, ns, "CrashReport", req.CrashReport, false); err != nil { |
| return err |
| } |
| job.BuildID = req.Build.ID |
| job.CrashTitle = req.CrashTitle |
| job.Finished = now |
| if _, err := datastore.Put(c, jobKey, job); err != nil { |
| return err |
| } |
| return nil |
| } |
| return datastore.RunInTransaction(c, tx, &datastore.TransactionOptions{XG: true, Attempts: 30}) |
| } |
| |
| func pollCompletedJobs(c context.Context, typ string) ([]*dashapi.BugReport, error) { |
| var jobs []*Job |
| keys, err := datastore.NewQuery("Job"). |
| Filter("Finished>", time.Time{}). |
| Filter("Reported=", false). |
| GetAll(c, &jobs) |
| if err != nil { |
| return nil, fmt.Errorf("failed to query jobs: %v", err) |
| } |
| var reports []*dashapi.BugReport |
| for i, job := range jobs { |
| reporting := config.Namespaces[job.Namespace].ReportingByName(job.Reporting) |
| if reporting.Config.Type() != typ { |
| continue |
| } |
| rep, err := createBugReportForJob(c, job, keys[i], reporting.Config) |
| if err != nil { |
| log.Errorf(c, "failed to create report for job: %v", err) |
| continue |
| } |
| reports = append(reports, rep) |
| } |
| return reports, nil |
| } |
| |
| func createBugReportForJob(c context.Context, job *Job, jobKey *datastore.Key, config interface{}) (*dashapi.BugReport, error) { |
| reportingConfig, err := json.Marshal(config) |
| if err != nil { |
| return nil, err |
| } |
| crashLog, err := getText(c, "CrashLog", job.CrashLog) |
| if err != nil { |
| return nil, err |
| } |
| if len(crashLog) > maxMailLogLen { |
| crashLog = crashLog[len(crashLog)-maxMailLogLen:] |
| } |
| report, err := getText(c, "CrashReport", job.CrashReport) |
| if err != nil { |
| return nil, err |
| } |
| if len(report) > maxMailReportLen { |
| report = report[:maxMailReportLen] |
| } |
| jobError, err := getText(c, "Error", job.Error) |
| if err != nil { |
| return nil, err |
| } |
| if len(jobError) > maxMailLogLen { |
| jobError = jobError[:maxMailLogLen] |
| } |
| patch, err := getText(c, "Patch", job.Patch) |
| if err != nil { |
| return nil, err |
| } |
| build, err := loadBuild(c, job.Namespace, job.BuildID) |
| if err != nil { |
| return nil, err |
| } |
| kernelConfig, err := getText(c, "KernelConfig", build.KernelConfig) |
| if err != nil { |
| return nil, err |
| } |
| bug := new(Bug) |
| if err := datastore.Get(c, jobKey.Parent(), bug); err != nil { |
| return nil, fmt.Errorf("failed to load job parent bug: %v", err) |
| } |
| bugReporting := bugReportingByName(bug, job.Reporting) |
| if bugReporting == nil { |
| return nil, fmt.Errorf("job bug has no reporting %q", job.Reporting) |
| } |
| rep := &dashapi.BugReport{ |
| Namespace: job.Namespace, |
| Config: reportingConfig, |
| ID: bugReporting.ID, |
| JobID: extJobID(jobKey), |
| ExtID: job.ExtID, |
| Title: bug.displayTitle(), |
| Log: crashLog, |
| Report: report, |
| OS: build.OS, |
| Arch: build.Arch, |
| VMArch: build.VMArch, |
| CompilerID: build.CompilerID, |
| KernelRepo: build.KernelRepo, |
| KernelBranch: build.KernelBranch, |
| KernelCommit: build.KernelCommit, |
| KernelConfig: kernelConfig, |
| CrashTitle: job.CrashTitle, |
| Error: jobError, |
| Patch: patch, |
| } |
| if bugReporting.CC != "" { |
| rep.CC = strings.Split(bugReporting.CC, "|") |
| } |
| rep.CC = append(rep.CC, job.User) |
| return rep, nil |
| } |
| |
| func jobReported(c context.Context, jobID string) error { |
| jobKey, err := jobID2Key(c, jobID) |
| if err != nil { |
| return err |
| } |
| tx := func(c context.Context) error { |
| job := new(Job) |
| if err := datastore.Get(c, jobKey, job); err != nil { |
| return fmt.Errorf("job %v: failed to get job: %v", jobID, err) |
| } |
| job.Reported = true |
| if _, err := datastore.Put(c, jobKey, job); err != nil { |
| return err |
| } |
| return nil |
| } |
| return datastore.RunInTransaction(c, tx, nil) |
| } |
| |
| func loadPendingJob(c context.Context, managers []string) (*Job, *datastore.Key, error) { |
| var jobs []*Job |
| keys, err := datastore.NewQuery("Job"). |
| Filter("Finished=", time.Time{}). |
| Order("Attempts"). |
| Order("Created"). |
| GetAll(c, &jobs) |
| if err != nil { |
| return nil, nil, fmt.Errorf("failed to query jobs: %v", err) |
| } |
| mgrs := make(map[string]bool) |
| for _, mgr := range managers { |
| mgrs[mgr] = true |
| } |
| for i, job := range jobs { |
| if !mgrs[job.Manager] { |
| continue |
| } |
| return job, keys[i], nil |
| } |
| return nil, nil, nil |
| } |
| |
| func extJobID(jobKey *datastore.Key) string { |
| return fmt.Sprintf("%v|%v", jobKey.Parent().StringID(), jobKey.IntID()) |
| } |
| |
| func jobID2Key(c context.Context, id string) (*datastore.Key, error) { |
| keyStr := strings.Split(id, "|") |
| if len(keyStr) != 2 { |
| return nil, fmt.Errorf("bad job id %q", id) |
| } |
| jobKeyID, err := strconv.ParseInt(keyStr[1], 10, 64) |
| if err != nil { |
| return nil, fmt.Errorf("bad job id %q", id) |
| } |
| bugKey := datastore.NewKey(c, "Bug", keyStr[0], 0, nil) |
| jobKey := datastore.NewKey(c, "Job", "", jobKeyID, bugKey) |
| return jobKey, nil |
| } |