mirror of
https://github.com/reactos/syzkaller.git
synced 2025-02-23 13:10:44 +00:00

Check some errors where relevant. Unfortunately enabling errcheck does not look feasible, too many warnings. Update #538
512 lines
15 KiB
Go
512 lines
15 KiB
Go
// 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 (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/mail"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/google/syzkaller/dashboard/dashapi"
|
|
"github.com/google/syzkaller/pkg/email"
|
|
"golang.org/x/net/context"
|
|
"google.golang.org/appengine"
|
|
"google.golang.org/appengine/log"
|
|
aemail "google.golang.org/appengine/mail"
|
|
)
|
|
|
|
// Email reporting interface.
|
|
|
|
func initEmailReporting() {
|
|
http.HandleFunc("/email_poll", handleEmailPoll)
|
|
http.HandleFunc("/_ah/mail/", handleIncomingMail)
|
|
http.HandleFunc("/_ah/bounce", handleEmailBounce)
|
|
|
|
mailingLists = make(map[string]bool)
|
|
for _, cfg := range config.Namespaces {
|
|
for _, reporting := range cfg.Reporting {
|
|
if cfg, ok := reporting.Config.(*EmailConfig); ok {
|
|
mailingLists[email.CanonicalEmail(cfg.Email)] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const (
|
|
emailType = "email"
|
|
// This plays an important role at least for job replies.
|
|
// If we CC a kernel mailing list and it uses Patchwork,
|
|
// then any emails with a patch attached create a new patch
|
|
// entry pending for review. The prefix makes Patchwork
|
|
// treat it as a comment for a previous patch.
|
|
replySubjectPrefix = "Re: "
|
|
commitHashLen = 12
|
|
commitTitleLen = 47 // so that whole line fits into 78 chars
|
|
)
|
|
|
|
var mailingLists map[string]bool
|
|
|
|
type EmailConfig struct {
|
|
Email string
|
|
Moderation bool
|
|
MailMaintainers bool
|
|
DefaultMaintainers []string
|
|
}
|
|
|
|
func (cfg *EmailConfig) Type() string {
|
|
return emailType
|
|
}
|
|
|
|
func (cfg *EmailConfig) NeedMaintainers() bool {
|
|
return cfg.MailMaintainers && len(cfg.DefaultMaintainers) == 0
|
|
}
|
|
|
|
func (cfg *EmailConfig) Validate() error {
|
|
if _, err := mail.ParseAddress(cfg.Email); err != nil {
|
|
return fmt.Errorf("bad email address %q: %v", cfg.Email, err)
|
|
}
|
|
for _, email := range cfg.DefaultMaintainers {
|
|
if _, err := mail.ParseAddress(email); err != nil {
|
|
return fmt.Errorf("bad email address %q: %v", email, err)
|
|
}
|
|
}
|
|
if cfg.Moderation && cfg.MailMaintainers {
|
|
return fmt.Errorf("both Moderation and MailMaintainers set")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// handleEmailPoll is called by cron and sends emails for new bugs, if any.
|
|
func handleEmailPoll(w http.ResponseWriter, r *http.Request) {
|
|
c := appengine.NewContext(r)
|
|
if err := emailPollBugs(c); err != nil {
|
|
log.Errorf(c, "bug poll failed: %v", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if err := emailPollJobs(c); err != nil {
|
|
log.Errorf(c, "job poll failed: %v", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Write([]byte("OK"))
|
|
}
|
|
|
|
func emailPollBugs(c context.Context) error {
|
|
reports := reportingPollBugs(c, emailType)
|
|
for _, rep := range reports {
|
|
cfg := new(EmailConfig)
|
|
if err := json.Unmarshal(rep.Config, cfg); err != nil {
|
|
log.Errorf(c, "failed to unmarshal email config: %v", err)
|
|
continue
|
|
}
|
|
if cfg.MailMaintainers {
|
|
rep.CC = email.MergeEmailLists(rep.CC, rep.Maintainers, cfg.DefaultMaintainers)
|
|
}
|
|
if err := emailReport(c, rep, "mail_bug.txt"); err != nil {
|
|
log.Errorf(c, "failed to report bug: %v", err)
|
|
continue
|
|
}
|
|
cmd := &dashapi.BugUpdate{
|
|
ID: rep.ID,
|
|
Status: dashapi.BugStatusOpen,
|
|
ReproLevel: dashapi.ReproLevelNone,
|
|
CrashID: rep.CrashID,
|
|
}
|
|
if len(rep.ReproC) != 0 {
|
|
cmd.ReproLevel = dashapi.ReproLevelC
|
|
} else if len(rep.ReproSyz) != 0 {
|
|
cmd.ReproLevel = dashapi.ReproLevelSyz
|
|
}
|
|
ok, reason, err := incomingCommand(c, cmd)
|
|
if !ok || err != nil {
|
|
log.Errorf(c, "failed to update reported bug: ok=%v reason=%v err=%v", ok, reason, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func emailPollJobs(c context.Context) error {
|
|
jobs, err := pollCompletedJobs(c, emailType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, job := range jobs {
|
|
if err := emailReport(c, job, "mail_test_result.txt"); err != nil {
|
|
log.Errorf(c, "failed to report job: %v", err)
|
|
continue
|
|
}
|
|
if err := jobReported(c, job.JobID); err != nil {
|
|
log.Errorf(c, "failed to mark job reported: %v", err)
|
|
continue
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func emailReport(c context.Context, rep *dashapi.BugReport, templ string) error {
|
|
cfg := new(EmailConfig)
|
|
if err := json.Unmarshal(rep.Config, cfg); err != nil {
|
|
return fmt.Errorf("failed to unmarshal email config: %v", err)
|
|
}
|
|
to := email.MergeEmailLists([]string{cfg.Email}, rep.CC)
|
|
// Build error output and failing VM boot log can be way too long to inline.
|
|
if len(rep.Error) > maxInlineError {
|
|
rep.Error = rep.Error[len(rep.Error)-maxInlineError:]
|
|
} else {
|
|
rep.ErrorLink = ""
|
|
}
|
|
from, err := email.AddAddrContext(fromAddr(c), rep.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
creditEmail, err := email.AddAddrContext(ownEmail(c), rep.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
userspaceArch := ""
|
|
if rep.Arch == "386" {
|
|
userspaceArch = "i386"
|
|
}
|
|
link := fmt.Sprintf("%v/bug?extid=%v", appURL(c), rep.ID)
|
|
// Data passed to the template.
|
|
type BugReportData struct {
|
|
First bool
|
|
Link string
|
|
CreditEmail string
|
|
Moderation bool
|
|
Maintainers []string
|
|
CompilerID string
|
|
KernelRepo string
|
|
KernelCommit string
|
|
KernelCommitTitle string
|
|
KernelCommitDate string
|
|
UserSpaceArch string
|
|
CrashTitle string
|
|
Report []byte
|
|
Error []byte
|
|
ErrorLink string
|
|
LogLink string
|
|
KernelConfigLink string
|
|
ReproSyzLink string
|
|
ReproCLink string
|
|
NumCrashes int64
|
|
HappenedOn []string
|
|
PatchLink string
|
|
}
|
|
data := &BugReportData{
|
|
First: rep.First,
|
|
Link: link,
|
|
CreditEmail: creditEmail,
|
|
Moderation: cfg.Moderation,
|
|
Maintainers: rep.Maintainers,
|
|
CompilerID: rep.CompilerID,
|
|
KernelRepo: rep.KernelRepoAlias,
|
|
KernelCommit: rep.KernelCommit,
|
|
KernelCommitTitle: rep.KernelCommitTitle,
|
|
KernelCommitDate: formatKernelTime(rep.KernelCommitDate),
|
|
UserSpaceArch: userspaceArch,
|
|
CrashTitle: rep.CrashTitle,
|
|
Report: rep.Report,
|
|
Error: rep.Error,
|
|
ErrorLink: rep.ErrorLink,
|
|
LogLink: rep.LogLink,
|
|
KernelConfigLink: rep.KernelConfigLink,
|
|
ReproSyzLink: rep.ReproSyzLink,
|
|
ReproCLink: rep.ReproCLink,
|
|
NumCrashes: rep.NumCrashes,
|
|
HappenedOn: rep.HappenedOn,
|
|
PatchLink: rep.PatchLink,
|
|
}
|
|
if len(data.KernelCommit) > commitHashLen {
|
|
data.KernelCommit = data.KernelCommit[:commitHashLen]
|
|
}
|
|
if len(data.KernelCommitTitle) > commitTitleLen {
|
|
data.KernelCommitTitle = data.KernelCommitTitle[:commitTitleLen-2] + ".."
|
|
}
|
|
log.Infof(c, "sending email %q to %q", rep.Title, to)
|
|
return sendMailTemplate(c, rep.Title, from, to, rep.ExtID, nil, templ, data)
|
|
}
|
|
|
|
// handleIncomingMail is the entry point for incoming emails.
|
|
func handleIncomingMail(w http.ResponseWriter, r *http.Request) {
|
|
c := appengine.NewContext(r)
|
|
if err := incomingMail(c, r); err != nil {
|
|
log.Errorf(c, "%v", err)
|
|
}
|
|
}
|
|
|
|
func incomingMail(c context.Context, r *http.Request) error {
|
|
msg, err := email.Parse(r.Body, ownEmails(c))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Infof(c, "received email: subject %q, from %q, cc %q, msg %q, bug %q, cmd %q, link %q",
|
|
msg.Subject, msg.From, msg.Cc, msg.MessageID, msg.BugID, msg.Command, msg.Link)
|
|
if msg.Command == "fix:" && msg.CommandArgs == "exact-commit-title" {
|
|
// Sometimes it happens that somebody sends us our own text back, ignore it.
|
|
msg.Command, msg.CommandArgs = "", ""
|
|
}
|
|
bug, _, reporting := loadBugInfo(c, msg)
|
|
if bug == nil {
|
|
return nil // error was already logged
|
|
}
|
|
emailConfig := reporting.Config.(*EmailConfig)
|
|
// A mailing list can send us a duplicate email, to not process/reply
|
|
// to such duplicate emails, we ignore emails coming from our mailing lists.
|
|
mailingList := email.CanonicalEmail(emailConfig.Email)
|
|
fromMailingList := email.CanonicalEmail(msg.From) == mailingList
|
|
mailingListInCC := checkMailingListInCC(c, msg, mailingList)
|
|
log.Infof(c, "from/cc mailing list: %v/%v", fromMailingList, mailingListInCC)
|
|
if msg.Command == "test:" {
|
|
args := strings.Split(msg.CommandArgs, " ")
|
|
if len(args) != 2 {
|
|
return replyTo(c, msg, fmt.Sprintf("want 2 args (repo, branch), got %v",
|
|
len(args)), nil)
|
|
}
|
|
reply := handleTestRequest(c, msg.BugID, email.CanonicalEmail(msg.From),
|
|
msg.MessageID, msg.Link, msg.Patch, args[0], args[1], msg.Cc)
|
|
if reply != "" {
|
|
return replyTo(c, msg, reply, nil)
|
|
}
|
|
return nil
|
|
}
|
|
if fromMailingList && msg.Command != "" {
|
|
log.Infof(c, "duplicate email from mailing list, ignoring")
|
|
return nil
|
|
}
|
|
cmd := &dashapi.BugUpdate{
|
|
ID: msg.BugID,
|
|
ExtID: msg.MessageID,
|
|
Link: msg.Link,
|
|
CC: msg.Cc,
|
|
}
|
|
switch msg.Command {
|
|
case "":
|
|
cmd.Status = dashapi.BugStatusUpdate
|
|
case "upstream":
|
|
cmd.Status = dashapi.BugStatusUpstream
|
|
case "invalid":
|
|
cmd.Status = dashapi.BugStatusInvalid
|
|
case "undup":
|
|
cmd.Status = dashapi.BugStatusOpen
|
|
case "fix:":
|
|
if msg.CommandArgs == "" {
|
|
return replyTo(c, msg, fmt.Sprintf("no commit title"), nil)
|
|
}
|
|
cmd.Status = dashapi.BugStatusOpen
|
|
cmd.FixCommits = []string{msg.CommandArgs}
|
|
case "dup:":
|
|
if msg.CommandArgs == "" {
|
|
return replyTo(c, msg, fmt.Sprintf("no dup title"), nil)
|
|
}
|
|
cmd.Status = dashapi.BugStatusDup
|
|
cmd.DupOf = msg.CommandArgs
|
|
default:
|
|
return replyTo(c, msg, fmt.Sprintf("unknown command %q", msg.Command), nil)
|
|
}
|
|
ok, reply, err := incomingCommand(c, cmd)
|
|
if err != nil {
|
|
return nil // the error was already logged
|
|
}
|
|
if !ok && reply != "" {
|
|
return replyTo(c, msg, reply, nil)
|
|
}
|
|
if !mailingListInCC && msg.Command != "" {
|
|
warnMailingListInCC(c, msg, mailingList)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func handleEmailBounce(w http.ResponseWriter, r *http.Request) {
|
|
c := appengine.NewContext(r)
|
|
body, err := ioutil.ReadAll(r.Body)
|
|
if err != nil {
|
|
log.Errorf(c, "email bounced: failed to read body: %v", err)
|
|
return
|
|
}
|
|
if nonCriticalBounceRe.Match(body) {
|
|
log.Infof(c, "email bounced: address not found")
|
|
} else {
|
|
log.Errorf(c, "email bounced")
|
|
}
|
|
log.Infof(c, "%s", body)
|
|
}
|
|
|
|
// These are just stale emails in MAINTAINERS.
|
|
var nonCriticalBounceRe = regexp.MustCompile(`\*\* Address not found \*\*|550 #5\.1\.0 Address rejected`)
|
|
|
|
func loadBugInfo(c context.Context, msg *email.Email) (bug *Bug, bugReporting *BugReporting, reporting *Reporting) {
|
|
if msg.BugID == "" {
|
|
if msg.Command == "" {
|
|
// This happens when people CC syzbot on unrelated emails.
|
|
log.Infof(c, "no bug ID (%q)", msg.Subject)
|
|
} else {
|
|
log.Errorf(c, "no bug ID (%q)", msg.Subject)
|
|
if err := replyTo(c, msg, "Can't find the corresponding bug.", nil); err != nil {
|
|
log.Errorf(c, "failed to send reply: %v", err)
|
|
}
|
|
}
|
|
return nil, nil, nil
|
|
}
|
|
bug, _, err := findBugByReportingID(c, msg.BugID)
|
|
if err != nil {
|
|
log.Errorf(c, "can't find bug: %v", err)
|
|
if err := replyTo(c, msg, "Can't find the corresponding bug.", nil); err != nil {
|
|
log.Errorf(c, "failed to send reply: %v", err)
|
|
}
|
|
return nil, nil, nil
|
|
}
|
|
bugReporting, _ = bugReportingByID(bug, msg.BugID)
|
|
if bugReporting == nil {
|
|
log.Errorf(c, "can't find bug reporting: %v", err)
|
|
if err := replyTo(c, msg, "Can't find the corresponding bug.", nil); err != nil {
|
|
log.Errorf(c, "failed to send reply: %v", err)
|
|
}
|
|
return nil, nil, nil
|
|
}
|
|
reporting = config.Namespaces[bug.Namespace].ReportingByName(bugReporting.Name)
|
|
if reporting == nil {
|
|
log.Errorf(c, "can't find reporting for this bug: namespace=%q reporting=%q",
|
|
bug.Namespace, bugReporting.Name)
|
|
return nil, nil, nil
|
|
}
|
|
if reporting.Config.Type() != emailType {
|
|
log.Errorf(c, "reporting is not email: namespace=%q reporting=%q config=%q",
|
|
bug.Namespace, bugReporting.Name, reporting.Config.Type())
|
|
return nil, nil, nil
|
|
}
|
|
return bug, bugReporting, reporting
|
|
}
|
|
|
|
func checkMailingListInCC(c context.Context, msg *email.Email, mailingList string) bool {
|
|
if email.CanonicalEmail(msg.From) == mailingList {
|
|
return true
|
|
}
|
|
for _, cc := range msg.Cc {
|
|
if email.CanonicalEmail(cc) == mailingList {
|
|
return true
|
|
}
|
|
}
|
|
msg.Cc = append(msg.Cc, mailingList)
|
|
return false
|
|
}
|
|
|
|
func warnMailingListInCC(c context.Context, msg *email.Email, mailingList string) {
|
|
reply := fmt.Sprintf("Your '%v' command is accepted, but please keep %v mailing list"+
|
|
" in CC next time. It serves as a history of what happened with each bug report."+
|
|
" Thank you.",
|
|
msg.Command, mailingList)
|
|
if err := replyTo(c, msg, reply, nil); err != nil {
|
|
log.Errorf(c, "failed to send email reply: %v", err)
|
|
}
|
|
}
|
|
|
|
func sendMailTemplate(c context.Context, subject, from string, to []string, replyTo string,
|
|
attachments []aemail.Attachment, template string, data interface{}) error {
|
|
body := new(bytes.Buffer)
|
|
if err := mailTemplates.ExecuteTemplate(body, template, data); err != nil {
|
|
return fmt.Errorf("failed to execute %v template: %v", template, err)
|
|
}
|
|
msg := &aemail.Message{
|
|
Sender: from,
|
|
To: to,
|
|
Subject: subject,
|
|
Body: body.String(),
|
|
Attachments: attachments,
|
|
}
|
|
if replyTo != "" {
|
|
msg.Headers = mail.Header{"In-Reply-To": []string{replyTo}}
|
|
msg.Subject = replySubjectPrefix + msg.Subject
|
|
}
|
|
return sendEmail(c, msg)
|
|
}
|
|
|
|
func replyTo(c context.Context, msg *email.Email, reply string, attachment *aemail.Attachment) error {
|
|
var attachments []aemail.Attachment
|
|
if attachment != nil {
|
|
attachments = append(attachments, *attachment)
|
|
}
|
|
from, err := email.AddAddrContext(fromAddr(c), msg.BugID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Infof(c, "sending reply: to=%q cc=%q subject=%q reply=%q",
|
|
msg.From, msg.Cc, msg.Subject, reply)
|
|
replyMsg := &aemail.Message{
|
|
Sender: from,
|
|
To: []string{msg.From},
|
|
Cc: msg.Cc,
|
|
Subject: replySubjectPrefix + msg.Subject,
|
|
Body: email.FormReply(msg.Body, reply),
|
|
Attachments: attachments,
|
|
Headers: mail.Header{"In-Reply-To": []string{msg.MessageID}},
|
|
}
|
|
return sendEmail(c, replyMsg)
|
|
}
|
|
|
|
// Sends email, can be stubbed for testing.
|
|
var sendEmail = func(c context.Context, msg *aemail.Message) error {
|
|
if err := aemail.Send(c, msg); err != nil {
|
|
return fmt.Errorf("failed to send email: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ownEmail(c context.Context) string {
|
|
return fmt.Sprintf("syzbot@%v.appspotmail.com", appengine.AppID(c))
|
|
}
|
|
|
|
func fromAddr(c context.Context) string {
|
|
return fmt.Sprintf("\"syzbot\" <%v>", ownEmail(c))
|
|
}
|
|
|
|
func ownEmails(c context.Context) []string {
|
|
// Now we use syzbot@ but we used to use bot@, so we add them both.
|
|
return []string{
|
|
ownEmail(c),
|
|
fmt.Sprintf("bot@%v.appspotmail.com", appengine.AppID(c)),
|
|
}
|
|
}
|
|
|
|
func externalLink(c context.Context, tag string, id int64) string {
|
|
if id == 0 {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%v/x/%v?x=%v", appURL(c), textFilename(tag), strconv.FormatUint(uint64(id), 16))
|
|
}
|
|
|
|
func appURL(c context.Context) string {
|
|
return fmt.Sprintf("https://%v.appspot.com", appengine.AppID(c))
|
|
}
|
|
|
|
func formatKernelTime(t time.Time) string {
|
|
if t.IsZero() {
|
|
return ""
|
|
}
|
|
// This is how dates appear in git log.
|
|
return t.Format("Mon Jan 2 15:04:05 2006 -0700")
|
|
}
|
|
|
|
func formatStringList(list []string) string {
|
|
return strings.Join(list, ", ")
|
|
}
|
|
|
|
var (
|
|
mailTemplates = template.Must(template.New("").Funcs(mailFuncs).ParseGlob("mail_*.txt"))
|
|
|
|
mailFuncs = template.FuncMap{
|
|
"formatTime": formatKernelTime,
|
|
"formatList": formatStringList,
|
|
}
|
|
)
|