blob: e26e3a75d96cc31a7f6103f308243d7f83600245 [file] [log] [blame]
// 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
}