• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 syzkaller project authors. All rights reserved.
2// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3
4package dash
5
6import (
7	"bytes"
8	"encoding/json"
9	"fmt"
10	"reflect"
11	"sort"
12	"strings"
13	"time"
14
15	"github.com/google/syzkaller/dashboard/dashapi"
16	"github.com/google/syzkaller/pkg/email"
17	"golang.org/x/net/context"
18	"google.golang.org/appengine/datastore"
19	"google.golang.org/appengine/log"
20)
21
22// Backend-independent reporting logic.
23// Two main entry points:
24//  - reportingPoll is called by backends to get list of bugs that need to be reported.
25//  - incomingCommand is called by backends to update bug statuses.
26
27const (
28	maxMailLogLen    = 1 << 20
29	maxMailReportLen = 64 << 10
30	maxInlineError   = 16 << 10
31	internalError    = "internal error"
32	// This is embedded as first line of syzkaller reproducer files.
33	syzReproPrefix = "# See https://goo.gl/kgGztJ for information about syzkaller reproducers.\n"
34)
35
36// reportingPoll is called by backends to get list of bugs that need to be reported.
37func reportingPollBugs(c context.Context, typ string) []*dashapi.BugReport {
38	state, err := loadReportingState(c)
39	if err != nil {
40		log.Errorf(c, "%v", err)
41		return nil
42	}
43	var bugs []*Bug
44	_, err = datastore.NewQuery("Bug").
45		Filter("Status<", BugStatusFixed).
46		GetAll(c, &bugs)
47	if err != nil {
48		log.Errorf(c, "%v", err)
49		return nil
50	}
51	log.Infof(c, "fetched %v bugs", len(bugs))
52	sort.Sort(bugReportSorter(bugs))
53	var reports []*dashapi.BugReport
54	for _, bug := range bugs {
55		rep, err := handleReportBug(c, typ, state, bug)
56		if err != nil {
57			log.Errorf(c, "%v: failed to report bug %v: %v", bug.Namespace, bug.Title, err)
58			continue
59		}
60		if rep == nil {
61			continue
62		}
63		reports = append(reports, rep)
64		if len(reports) > 50 {
65			break // temp measure during the jam
66		}
67	}
68	return reports
69}
70
71func handleReportBug(c context.Context, typ string, state *ReportingState, bug *Bug) (*dashapi.BugReport, error) {
72	reporting, bugReporting, crash, crashKey, _, _, _, err := needReport(c, typ, state, bug)
73	if err != nil || reporting == nil {
74		return nil, err
75	}
76	rep, err := createBugReport(c, bug, crash, crashKey, bugReporting, reporting.Config)
77	if err != nil {
78		return nil, err
79	}
80	log.Infof(c, "bug %q: reporting to %v", bug.Title, reporting.Name)
81	return rep, nil
82}
83
84func needReport(c context.Context, typ string, state *ReportingState, bug *Bug) (
85	reporting *Reporting, bugReporting *BugReporting, crash *Crash,
86	crashKey *datastore.Key, reportingIdx int, status, link string, err error) {
87	reporting, bugReporting, reportingIdx, status, err = currentReporting(c, bug)
88	if err != nil || reporting == nil {
89		return
90	}
91	if typ != "" && typ != reporting.Config.Type() {
92		status = "on a different reporting"
93		reporting, bugReporting = nil, nil
94		return
95	}
96	link = bugReporting.Link
97	if !bugReporting.Reported.IsZero() && bugReporting.ReproLevel >= bug.ReproLevel {
98		status = fmt.Sprintf("%v: reported%v on %v",
99			reporting.DisplayTitle, reproStr(bugReporting.ReproLevel),
100			formatTime(bugReporting.Reported))
101		reporting, bugReporting = nil, nil
102		return
103	}
104	ent := state.getEntry(timeNow(c), bug.Namespace, reporting.Name)
105	cfg := config.Namespaces[bug.Namespace]
106	if timeSince(c, bug.FirstTime) < cfg.ReportingDelay {
107		status = fmt.Sprintf("%v: initial reporting delay", reporting.DisplayTitle)
108		reporting, bugReporting = nil, nil
109		return
110	}
111	if bug.ReproLevel < ReproLevelC && timeSince(c, bug.FirstTime) < cfg.WaitForRepro {
112		status = fmt.Sprintf("%v: waiting for C repro", reporting.DisplayTitle)
113		reporting, bugReporting = nil, nil
114		return
115	}
116	if !cfg.MailWithoutReport && !bug.HasReport {
117		status = fmt.Sprintf("%v: no report", reporting.DisplayTitle)
118		reporting, bugReporting = nil, nil
119		return
120	}
121
122	crash, crashKey, err = findCrashForBug(c, bug)
123	if err != nil {
124		status = fmt.Sprintf("%v: no crashes!", reporting.DisplayTitle)
125		reporting, bugReporting = nil, nil
126		return
127	}
128	if reporting.Config.NeedMaintainers() && len(crash.Maintainers) == 0 {
129		status = fmt.Sprintf("%v: no maintainers", reporting.DisplayTitle)
130		reporting, bugReporting = nil, nil
131		return
132	}
133
134	// Limit number of reports sent per day,
135	// but don't limit sending repros to already reported bugs.
136	if bugReporting.Reported.IsZero() && reporting.DailyLimit != 0 &&
137		ent.Sent >= reporting.DailyLimit {
138		status = fmt.Sprintf("%v: out of quota for today", reporting.DisplayTitle)
139		reporting, bugReporting = nil, nil
140		return
141	}
142
143	// Ready to be reported.
144	if bugReporting.Reported.IsZero() {
145		// This update won't be committed, but it will prevent us from
146		// reporting too many bugs in a single poll.
147		ent.Sent++
148	}
149	status = fmt.Sprintf("%v: ready to report", reporting.DisplayTitle)
150	if !bugReporting.Reported.IsZero() {
151		status += fmt.Sprintf(" (reported%v on %v)",
152			reproStr(bugReporting.ReproLevel), formatTime(bugReporting.Reported))
153	}
154	return
155}
156
157func currentReporting(c context.Context, bug *Bug) (*Reporting, *BugReporting, int, string, error) {
158	for i := range bug.Reporting {
159		bugReporting := &bug.Reporting[i]
160		if !bugReporting.Closed.IsZero() {
161			continue
162		}
163		reporting := config.Namespaces[bug.Namespace].ReportingByName(bugReporting.Name)
164		if reporting == nil {
165			return nil, nil, 0, "", fmt.Errorf("%v: missing in config", bugReporting.Name)
166		}
167		switch reporting.Filter(bug) {
168		case FilterSkip:
169			if bugReporting.Reported.IsZero() {
170				continue
171			}
172			fallthrough
173		case FilterReport:
174			return reporting, bugReporting, i, "", nil
175		case FilterHold:
176			return nil, nil, 0, fmt.Sprintf("%v: reporting suspended", reporting.DisplayTitle), nil
177		}
178	}
179	return nil, nil, 0, "", fmt.Errorf("no reporting left")
180}
181
182func reproStr(level dashapi.ReproLevel) string {
183	switch level {
184	case ReproLevelSyz:
185		return " syz repro"
186	case ReproLevelC:
187		return " C repro"
188	default:
189		return ""
190	}
191}
192
193func createBugReport(c context.Context, bug *Bug, crash *Crash, crashKey *datastore.Key,
194	bugReporting *BugReporting, config interface{}) (*dashapi.BugReport, error) {
195	reportingConfig, err := json.Marshal(config)
196	if err != nil {
197		return nil, err
198	}
199	crashLog, _, err := getText(c, textCrashLog, crash.Log)
200	if err != nil {
201		return nil, err
202	}
203	if len(crashLog) > maxMailLogLen {
204		crashLog = crashLog[len(crashLog)-maxMailLogLen:]
205	}
206	report, _, err := getText(c, textCrashReport, crash.Report)
207	if err != nil {
208		return nil, err
209	}
210	if len(report) > maxMailReportLen {
211		report = report[:maxMailReportLen]
212	}
213	reproC, _, err := getText(c, textReproC, crash.ReproC)
214	if err != nil {
215		return nil, err
216	}
217	reproSyz, _, err := getText(c, textReproSyz, crash.ReproSyz)
218	if err != nil {
219		return nil, err
220	}
221	if len(reproSyz) != 0 {
222		buf := new(bytes.Buffer)
223		buf.WriteString(syzReproPrefix)
224		if len(crash.ReproOpts) != 0 {
225			fmt.Fprintf(buf, "#%s\n", crash.ReproOpts)
226		}
227		buf.Write(reproSyz)
228		reproSyz = buf.Bytes()
229	}
230	build, err := loadBuild(c, bug.Namespace, crash.BuildID)
231	if err != nil {
232		return nil, err
233	}
234	kernelConfig, _, err := getText(c, textKernelConfig, build.KernelConfig)
235	if err != nil {
236		return nil, err
237	}
238
239	rep := &dashapi.BugReport{
240		Namespace:         bug.Namespace,
241		Config:            reportingConfig,
242		ID:                bugReporting.ID,
243		ExtID:             bugReporting.ExtID,
244		First:             bugReporting.Reported.IsZero(),
245		Title:             bug.displayTitle(),
246		Log:               crashLog,
247		LogLink:           externalLink(c, textCrashLog, crash.Log),
248		Report:            report,
249		ReportLink:        externalLink(c, textCrashReport, crash.Report),
250		Maintainers:       crash.Maintainers,
251		OS:                build.OS,
252		Arch:              build.Arch,
253		VMArch:            build.VMArch,
254		CompilerID:        build.CompilerID,
255		KernelRepo:        build.KernelRepo,
256		KernelRepoAlias:   kernelRepoInfo(build).Alias,
257		KernelBranch:      build.KernelBranch,
258		KernelCommit:      build.KernelCommit,
259		KernelCommitTitle: build.KernelCommitTitle,
260		KernelCommitDate:  build.KernelCommitDate,
261		KernelConfig:      kernelConfig,
262		KernelConfigLink:  externalLink(c, textKernelConfig, build.KernelConfig),
263		ReproC:            reproC,
264		ReproCLink:        externalLink(c, textReproC, crash.ReproC),
265		ReproSyz:          reproSyz,
266		ReproSyzLink:      externalLink(c, textReproSyz, crash.ReproSyz),
267		CrashID:           crashKey.IntID(),
268		NumCrashes:        bug.NumCrashes,
269		HappenedOn:        managersToRepos(c, bug.Namespace, bug.HappenedOn),
270	}
271	if bugReporting.CC != "" {
272		rep.CC = strings.Split(bugReporting.CC, "|")
273	}
274	return rep, nil
275}
276
277func managersToRepos(c context.Context, ns string, managers []string) []string {
278	var repos []string
279	dedup := make(map[string]bool)
280	for _, manager := range managers {
281		build, err := lastManagerBuild(c, ns, manager)
282		if err != nil {
283			log.Errorf(c, "failed to get manager %q build: %v", manager, err)
284			continue
285		}
286		repo := kernelRepoInfo(build).Alias
287		if dedup[repo] {
288			continue
289		}
290		dedup[repo] = true
291		repos = append(repos, repo)
292	}
293	sort.Strings(repos)
294	return repos
295}
296
297// reportingPollClosed is called by backends to get list of closed bugs.
298func reportingPollClosed(c context.Context, ids []string) ([]string, error) {
299	var bugs []*Bug
300	_, err := datastore.NewQuery("Bug").
301		GetAll(c, &bugs)
302	if err != nil {
303		log.Errorf(c, "%v", err)
304		return nil, nil
305	}
306	bugMap := make(map[string]*Bug)
307	for _, bug := range bugs {
308		for i := range bug.Reporting {
309			bugMap[bug.Reporting[i].ID] = bug
310		}
311	}
312	var closed []string
313	for _, id := range ids {
314		bug := bugMap[id]
315		if bug == nil {
316			continue
317		}
318		bugReporting, _ := bugReportingByID(bug, id)
319		bug, err = canonicalBug(c, bug)
320		if err != nil {
321			log.Errorf(c, "%v", err)
322			continue
323		}
324		if bug.Status >= BugStatusFixed || !bugReporting.Closed.IsZero() {
325			closed = append(closed, id)
326		}
327	}
328	return closed, nil
329}
330
331// incomingCommand is entry point to bug status updates.
332func incomingCommand(c context.Context, cmd *dashapi.BugUpdate) (bool, string, error) {
333	log.Infof(c, "got command: %+v", cmd)
334	ok, reason, err := incomingCommandImpl(c, cmd)
335	if err != nil {
336		log.Errorf(c, "%v (%v)", reason, err)
337	} else if !ok && reason != "" {
338		log.Errorf(c, "invalid update: %v", reason)
339	}
340	return ok, reason, err
341}
342
343func incomingCommandImpl(c context.Context, cmd *dashapi.BugUpdate) (bool, string, error) {
344	for i, com := range cmd.FixCommits {
345		if len(com) >= 2 && com[0] == '"' && com[len(com)-1] == '"' {
346			com = com[1 : len(com)-1]
347			cmd.FixCommits[i] = com
348		}
349		if len(com) < 3 {
350			return false, fmt.Sprintf("bad commit title: %q", com), nil
351		}
352	}
353	bug, bugKey, err := findBugByReportingID(c, cmd.ID)
354	if err != nil {
355		return false, internalError, err
356	}
357	now := timeNow(c)
358	dupHash := ""
359	if cmd.Status == dashapi.BugStatusDup {
360		bugReporting, _ := bugReportingByID(bug, cmd.ID)
361		dup, dupKey, err := findBugByReportingID(c, cmd.DupOf)
362		if err != nil {
363			// Email reporting passes bug title in cmd.DupOf, try to find bug by title.
364			dup, dupKey, err = findDupByTitle(c, bug.Namespace, cmd.DupOf)
365			if err != nil {
366				return false, "can't find the dup bug", err
367			}
368			dupReporting := bugReportingByName(dup, bugReporting.Name)
369			if dupReporting == nil {
370				return false, "can't find the dup bug",
371					fmt.Errorf("dup does not have reporting %q", bugReporting.Name)
372			}
373			cmd.DupOf = dupReporting.ID
374		}
375		dupReporting, _ := bugReportingByID(dup, cmd.DupOf)
376		if bugReporting == nil || dupReporting == nil {
377			return false, internalError, fmt.Errorf("can't find bug reporting")
378		}
379		if bugKey.StringID() == dupKey.StringID() {
380			if bugReporting.Name == dupReporting.Name {
381				return false, "Can't dup bug to itself.", nil
382			}
383			return false, fmt.Sprintf("Can't dup bug to itself in different reporting (%v->%v).\n"+
384				"Please dup syzbot bugs only onto syzbot bugs for the same kernel/reporting.",
385				bugReporting.Name, dupReporting.Name), nil
386		}
387		if bug.Namespace != dup.Namespace {
388			return false, fmt.Sprintf("Duplicate bug corresponds to a different kernel (%v->%v).\n"+
389				"Please dup syzbot bugs only onto syzbot bugs for the same kernel.",
390				bug.Namespace, dup.Namespace), nil
391		}
392		if bugReporting.Name != dupReporting.Name {
393			return false, fmt.Sprintf("Can't dup bug to a bug in different reporting (%v->%v)."+
394				"Please dup syzbot bugs only onto syzbot bugs for the same kernel/reporting.",
395				bugReporting.Name, dupReporting.Name), nil
396		}
397		dupCanon, err := canonicalBug(c, dup)
398		if err != nil {
399			return false, internalError, fmt.Errorf("failed to get canonical bug for dup: %v", err)
400		}
401		if !dupReporting.Closed.IsZero() && dupCanon.Status == BugStatusOpen {
402			return false, "Dup bug is already upstreamed.", nil
403		}
404		dupHash = bugKeyHash(dup.Namespace, dup.Title, dup.Seq)
405	}
406
407	ok, reply := false, ""
408	tx := func(c context.Context) error {
409		var err error
410		ok, reply, err = incomingCommandTx(c, now, cmd, bugKey, dupHash)
411		return err
412	}
413	err = datastore.RunInTransaction(c, tx, &datastore.TransactionOptions{
414		XG: true,
415		// Default is 3 which fails sometimes.
416		// We don't want incoming bug updates to fail,
417		// because for e.g. email we won't have an external retry.
418		Attempts: 30,
419	})
420	if err != nil {
421		return false, internalError, err
422	}
423	return ok, reply, nil
424}
425
426func incomingCommandTx(c context.Context, now time.Time, cmd *dashapi.BugUpdate,
427	bugKey *datastore.Key, dupHash string) (bool, string, error) {
428	bug := new(Bug)
429	if err := datastore.Get(c, bugKey, bug); err != nil {
430		return false, internalError, fmt.Errorf("can't find the corresponding bug: %v", err)
431	}
432	bugReporting, final := bugReportingByID(bug, cmd.ID)
433	if bugReporting == nil {
434		return false, internalError, fmt.Errorf("can't find bug reporting")
435	}
436	if ok, reply, err := checkBugStatus(c, cmd, bug, bugReporting); !ok {
437		return false, reply, err
438	}
439	state, err := loadReportingState(c)
440	if err != nil {
441		return false, internalError, err
442	}
443	stateEnt := state.getEntry(now, bug.Namespace, bugReporting.Name)
444	if ok, reply, err := incomingCommandCmd(c, now, cmd, bug, bugReporting, final, dupHash, stateEnt); !ok {
445		return false, reply, err
446	}
447	if len(cmd.FixCommits) != 0 && (bug.Status == BugStatusOpen || bug.Status == BugStatusDup) {
448		sort.Strings(cmd.FixCommits)
449		if !reflect.DeepEqual(bug.Commits, cmd.FixCommits) {
450			bug.Commits = cmd.FixCommits
451			bug.PatchedOn = nil
452		}
453	}
454	if cmd.CrashID != 0 {
455		// Rememeber that we've reported this crash.
456		crash := new(Crash)
457		crashKey := datastore.NewKey(c, "Crash", "", cmd.CrashID, bugKey)
458		if err := datastore.Get(c, crashKey, crash); err != nil {
459			return false, internalError, fmt.Errorf("failed to get reported crash %v: %v",
460				cmd.CrashID, err)
461		}
462		crash.Reported = now
463		if _, err := datastore.Put(c, crashKey, crash); err != nil {
464			return false, internalError, fmt.Errorf("failed to put reported crash %v: %v",
465				cmd.CrashID, err)
466		}
467		bugReporting.CrashID = cmd.CrashID
468	}
469	if bugReporting.ExtID == "" {
470		bugReporting.ExtID = cmd.ExtID
471	}
472	if bugReporting.Link == "" {
473		bugReporting.Link = cmd.Link
474	}
475	if len(cmd.CC) != 0 {
476		merged := email.MergeEmailLists(strings.Split(bugReporting.CC, "|"), cmd.CC)
477		bugReporting.CC = strings.Join(merged, "|")
478	}
479	if bugReporting.ReproLevel < cmd.ReproLevel {
480		bugReporting.ReproLevel = cmd.ReproLevel
481	}
482	if bug.Status != BugStatusDup {
483		bug.DupOf = ""
484	}
485	if _, err := datastore.Put(c, bugKey, bug); err != nil {
486		return false, internalError, fmt.Errorf("failed to put bug: %v", err)
487	}
488	if err := saveReportingState(c, state); err != nil {
489		return false, internalError, err
490	}
491	return true, "", nil
492}
493
494func incomingCommandCmd(c context.Context, now time.Time, cmd *dashapi.BugUpdate,
495	bug *Bug, bugReporting *BugReporting, final bool, dupHash string,
496	stateEnt *ReportingStateEntry) (bool, string, error) {
497	switch cmd.Status {
498	case dashapi.BugStatusOpen:
499		bug.Status = BugStatusOpen
500		bug.Closed = time.Time{}
501		if bugReporting.Reported.IsZero() {
502			bugReporting.Reported = now
503			stateEnt.Sent++ // sending repro does not count against the quota
504		}
505		// Close all previous reporting if they are not closed yet
506		// (can happen due to Status == ReportingDisabled).
507		for i := range bug.Reporting {
508			if bugReporting == &bug.Reporting[i] {
509				break
510			}
511			if bug.Reporting[i].Closed.IsZero() {
512				bug.Reporting[i].Closed = now
513			}
514		}
515		if bug.ReproLevel < cmd.ReproLevel {
516			return false, internalError,
517				fmt.Errorf("bug update with invalid repro level: %v/%v",
518					bug.ReproLevel, cmd.ReproLevel)
519		}
520	case dashapi.BugStatusUpstream:
521		if final {
522			return false, "Can't upstream, this is final destination.", nil
523		}
524		if len(bug.Commits) != 0 {
525			// We could handle this case, but how/when it will occur
526			// in real life is unclear now.
527			return false, "Can't upstream this bug, the bug has fixing commits.", nil
528		}
529		bug.Status = BugStatusOpen
530		bug.Closed = time.Time{}
531		bugReporting.Closed = now
532	case dashapi.BugStatusInvalid:
533		bugReporting.Closed = now
534		bug.Closed = now
535		bug.Status = BugStatusInvalid
536	case dashapi.BugStatusDup:
537		bug.Status = BugStatusDup
538		bug.Closed = now
539		bug.DupOf = dupHash
540	case dashapi.BugStatusUpdate:
541		// Just update Link, Commits, etc below.
542	default:
543		return false, internalError, fmt.Errorf("unknown bug status %v", cmd.Status)
544	}
545	return true, "", nil
546}
547
548func checkBugStatus(c context.Context, cmd *dashapi.BugUpdate, bug *Bug, bugReporting *BugReporting) (
549	bool, string, error) {
550	switch bug.Status {
551	case BugStatusOpen:
552	case BugStatusDup:
553		canon, err := canonicalBug(c, bug)
554		if err != nil {
555			return false, internalError, err
556		}
557		if canon.Status != BugStatusOpen {
558			// We used to reject updates to closed bugs,
559			// but this is confusing and non-actionable for users.
560			// So now we fail the update, but give empty reason,
561			// which means "don't notify user".
562			if cmd.Status == dashapi.BugStatusUpdate {
563				// This happens when people discuss old bugs.
564				log.Infof(c, "Dup bug is already closed")
565			} else {
566				log.Errorf(c, "Dup bug is already closed")
567			}
568			return false, "", nil
569		}
570	case BugStatusFixed, BugStatusInvalid:
571		if cmd.Status != dashapi.BugStatusUpdate {
572			log.Errorf(c, "This bug is already closed")
573		}
574		return false, "", nil
575	default:
576		return false, internalError, fmt.Errorf("unknown bug status %v", bug.Status)
577	}
578	if !bugReporting.Closed.IsZero() {
579		if cmd.Status != dashapi.BugStatusUpdate {
580			log.Errorf(c, "This bug reporting is already closed")
581		}
582		return false, "", nil
583	}
584	return true, "", nil
585}
586
587func findBugByReportingID(c context.Context, id string) (*Bug, *datastore.Key, error) {
588	var bugs []*Bug
589	keys, err := datastore.NewQuery("Bug").
590		Filter("Reporting.ID=", id).
591		Limit(2).
592		GetAll(c, &bugs)
593	if err != nil {
594		return nil, nil, fmt.Errorf("failed to fetch bugs: %v", err)
595	}
596	if len(bugs) == 0 {
597		return nil, nil, fmt.Errorf("failed to find bug by reporting id %q", id)
598	}
599	if len(bugs) > 1 {
600		return nil, nil, fmt.Errorf("multiple bugs for reporting id %q", id)
601	}
602	return bugs[0], keys[0], nil
603}
604
605func findDupByTitle(c context.Context, ns, title string) (*Bug, *datastore.Key, error) {
606	title, seq, err := splitDisplayTitle(title)
607	if err != nil {
608		return nil, nil, err
609	}
610	bugHash := bugKeyHash(ns, title, seq)
611	bugKey := datastore.NewKey(c, "Bug", bugHash, 0, nil)
612	bug := new(Bug)
613	if err := datastore.Get(c, bugKey, bug); err != nil {
614		return nil, nil, fmt.Errorf("failed to get dup: %v", err)
615	}
616	return bug, bugKey, nil
617}
618
619func bugReportingByID(bug *Bug, id string) (*BugReporting, bool) {
620	for i := range bug.Reporting {
621		if bug.Reporting[i].ID == id {
622			return &bug.Reporting[i], i == len(bug.Reporting)-1
623		}
624	}
625	return nil, false
626}
627
628func bugReportingByName(bug *Bug, name string) *BugReporting {
629	for i := range bug.Reporting {
630		if bug.Reporting[i].Name == name {
631			return &bug.Reporting[i]
632		}
633	}
634	return nil
635}
636
637func queryCrashesForBug(c context.Context, bugKey *datastore.Key, limit int) (
638	[]*Crash, []*datastore.Key, error) {
639	var crashes []*Crash
640	keys, err := datastore.NewQuery("Crash").
641		Ancestor(bugKey).
642		Order("-ReportLen").
643		Order("-Reported").
644		Order("-Time").
645		Limit(limit).
646		GetAll(c, &crashes)
647	if err != nil {
648		return nil, nil, fmt.Errorf("failed to fetch crashes: %v", err)
649	}
650	return crashes, keys, nil
651}
652
653func findCrashForBug(c context.Context, bug *Bug) (*Crash, *datastore.Key, error) {
654	bugKey := datastore.NewKey(c, "Bug", bugKeyHash(bug.Namespace, bug.Title, bug.Seq), 0, nil)
655	crashes, keys, err := queryCrashesForBug(c, bugKey, 1)
656	if err != nil {
657		return nil, nil, err
658	}
659	if len(crashes) < 1 {
660		return nil, nil, fmt.Errorf("no crashes")
661	}
662	crash, key := crashes[0], keys[0]
663	if bug.ReproLevel == ReproLevelC {
664		if crash.ReproC == 0 {
665			log.Errorf(c, "bug '%v': has C repro, but crash without C repro", bug.Title)
666		}
667	} else if bug.ReproLevel == ReproLevelSyz {
668		if crash.ReproSyz == 0 {
669			log.Errorf(c, "bug '%v': has syz repro, but crash without syz repro", bug.Title)
670		}
671	} else if bug.HasReport {
672		if crash.Report == 0 {
673			log.Errorf(c, "bug '%v': has report, but crash without report", bug.Title)
674		}
675	}
676	return crash, key, nil
677}
678
679func loadReportingState(c context.Context) (*ReportingState, error) {
680	state := new(ReportingState)
681	key := datastore.NewKey(c, "ReportingState", "", 1, nil)
682	if err := datastore.Get(c, key, state); err != nil && err != datastore.ErrNoSuchEntity {
683		return nil, fmt.Errorf("failed to get reporting state: %v", err)
684	}
685	return state, nil
686}
687
688func saveReportingState(c context.Context, state *ReportingState) error {
689	key := datastore.NewKey(c, "ReportingState", "", 1, nil)
690	if _, err := datastore.Put(c, key, state); err != nil {
691		return fmt.Errorf("failed to put reporting state: %v", err)
692	}
693	return nil
694}
695
696func (state *ReportingState) getEntry(now time.Time, namespace, name string) *ReportingStateEntry {
697	if namespace == "" || name == "" {
698		panic(fmt.Sprintf("requesting reporting state for %v/%v", namespace, name))
699	}
700	// Convert time to date of the form 20170125.
701	date := timeDate(now)
702	for i := range state.Entries {
703		ent := &state.Entries[i]
704		if ent.Namespace == namespace && ent.Name == name {
705			if ent.Date != date {
706				ent.Date = date
707				ent.Sent = 0
708			}
709			return ent
710		}
711	}
712	state.Entries = append(state.Entries, ReportingStateEntry{
713		Namespace: namespace,
714		Name:      name,
715		Date:      date,
716		Sent:      0,
717	})
718	return &state.Entries[len(state.Entries)-1]
719}
720
721// bugReportSorter sorts bugs by priority we want to report them.
722// E.g. we want to report bugs with reproducers before bugs without reproducers.
723type bugReportSorter []*Bug
724
725func (a bugReportSorter) Len() int      { return len(a) }
726func (a bugReportSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
727func (a bugReportSorter) Less(i, j int) bool {
728	if a[i].ReproLevel != a[j].ReproLevel {
729		return a[i].ReproLevel > a[j].ReproLevel
730	}
731	if a[i].HasReport != a[j].HasReport {
732		return a[i].HasReport
733	}
734	if a[i].NumCrashes != a[j].NumCrashes {
735		return a[i].NumCrashes > a[j].NumCrashes
736	}
737	return a[i].FirstTime.Before(a[j].FirstTime)
738}
739