• 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	"compress/gzip"
9	"encoding/json"
10	"fmt"
11	"io/ioutil"
12	"net/http"
13	"reflect"
14	"sort"
15	"strings"
16	"time"
17	"unicode/utf8"
18
19	"github.com/google/syzkaller/dashboard/dashapi"
20	"github.com/google/syzkaller/pkg/email"
21	"github.com/google/syzkaller/pkg/hash"
22	"golang.org/x/net/context"
23	"google.golang.org/appengine"
24	"google.golang.org/appengine/datastore"
25	"google.golang.org/appengine/log"
26)
27
28func initAPIHandlers() {
29	http.Handle("/api", handleJSON(handleAPI))
30}
31
32var apiHandlers = map[string]APIHandler{
33	"log_error":             apiLogError,
34	"job_poll":              apiJobPoll,
35	"job_done":              apiJobDone,
36	"reporting_poll_bugs":   apiReportingPollBugs,
37	"reporting_poll_closed": apiReportingPollClosed,
38	"reporting_update":      apiReportingUpdate,
39}
40
41var apiNamespaceHandlers = map[string]APINamespaceHandler{
42	"upload_build":        apiUploadBuild,
43	"builder_poll":        apiBuilderPoll,
44	"report_build_error":  apiReportBuildError,
45	"report_crash":        apiReportCrash,
46	"report_failed_repro": apiReportFailedRepro,
47	"need_repro":          apiNeedRepro,
48	"manager_stats":       apiManagerStats,
49}
50
51type JSONHandler func(c context.Context, r *http.Request) (interface{}, error)
52type APIHandler func(c context.Context, r *http.Request, payload []byte) (interface{}, error)
53type APINamespaceHandler func(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error)
54
55const (
56	maxReproPerBug   = 10
57	reproRetryPeriod = 24 * time.Hour // try 1 repro per day until we have at least syz repro
58)
59
60// Overridable for testing.
61var timeNow = func(c context.Context) time.Time {
62	return time.Now()
63}
64
65func timeSince(c context.Context, t time.Time) time.Duration {
66	return timeNow(c).Sub(t)
67}
68
69func handleJSON(fn JSONHandler) http.Handler {
70	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
71		c := appengine.NewContext(r)
72		reply, err := fn(c, r)
73		if err != nil {
74			// ErrAccess is logged earlier.
75			if err != ErrAccess {
76				log.Errorf(c, "%v", err)
77			}
78			http.Error(w, err.Error(), http.StatusInternalServerError)
79			return
80		}
81		w.Header().Set("Content-Type", "application/json")
82		if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
83			w.Header().Set("Content-Encoding", "gzip")
84			gz := gzip.NewWriter(w)
85			if err := json.NewEncoder(gz).Encode(reply); err != nil {
86				log.Errorf(c, "failed to encode reply: %v", err)
87			}
88			gz.Close()
89		} else {
90			if err := json.NewEncoder(w).Encode(reply); err != nil {
91				log.Errorf(c, "failed to encode reply: %v", err)
92			}
93		}
94	})
95}
96
97func handleAPI(c context.Context, r *http.Request) (reply interface{}, err error) {
98	client := r.PostFormValue("client")
99	method := r.PostFormValue("method")
100	log.Infof(c, "api %q from %q", method, client)
101	ns, err := checkClient(c, client, r.PostFormValue("key"))
102	if err != nil {
103		if client != "" {
104			log.Errorf(c, "%v", err)
105		} else {
106			// Don't log as error if somebody just invokes /api.
107			log.Infof(c, "%v", err)
108		}
109		return nil, err
110	}
111	var payload []byte
112	if str := r.PostFormValue("payload"); str != "" {
113		gr, err := gzip.NewReader(strings.NewReader(str))
114		if err != nil {
115			return nil, fmt.Errorf("failed to ungzip payload: %v", err)
116		}
117		payload, err = ioutil.ReadAll(gr)
118		if err != nil {
119			return nil, fmt.Errorf("failed to ungzip payload: %v", err)
120		}
121		if err := gr.Close(); err != nil {
122			return nil, fmt.Errorf("failed to ungzip payload: %v", err)
123		}
124	}
125	handler := apiHandlers[method]
126	if handler != nil {
127		return handler(c, r, payload)
128	}
129	nsHandler := apiNamespaceHandlers[method]
130	if nsHandler == nil {
131		return nil, fmt.Errorf("unknown api method %q", method)
132	}
133	if ns == "" {
134		return nil, fmt.Errorf("method %q must be called within a namespace", method)
135	}
136	return nsHandler(c, ns, r, payload)
137}
138
139func checkClient(c context.Context, name0, key0 string) (string, error) {
140	for name, key := range config.Clients {
141		if name == name0 {
142			if key != key0 {
143				return "", ErrAccess
144			}
145			return "", nil
146		}
147	}
148	for ns, cfg := range config.Namespaces {
149		for name, key := range cfg.Clients {
150			if name == name0 {
151				if key != key0 {
152					return "", ErrAccess
153				}
154				return ns, nil
155			}
156		}
157	}
158	return "", ErrAccess
159}
160
161func apiLogError(c context.Context, r *http.Request, payload []byte) (interface{}, error) {
162	req := new(dashapi.LogEntry)
163	if err := json.Unmarshal(payload, req); err != nil {
164		return nil, fmt.Errorf("failed to unmarshal request: %v", err)
165	}
166	log.Errorf(c, "%v: %v", req.Name, req.Text)
167	return nil, nil
168}
169
170func apiBuilderPoll(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
171	req := new(dashapi.BuilderPollReq)
172	if err := json.Unmarshal(payload, req); err != nil {
173		return nil, fmt.Errorf("failed to unmarshal request: %v", err)
174	}
175	var bugs []*Bug
176	_, err := datastore.NewQuery("Bug").
177		Filter("Namespace=", ns).
178		Filter("Status<", BugStatusFixed).
179		GetAll(c, &bugs)
180	if err != nil {
181		return nil, fmt.Errorf("failed to query bugs: %v", err)
182	}
183	m := make(map[string]bool)
184loop:
185	for _, bug := range bugs {
186		// TODO(dvyukov): include this condition into the query if possible.
187		if len(bug.Commits) == 0 {
188			continue
189		}
190		for _, mgr := range bug.PatchedOn {
191			if mgr == req.Manager {
192				continue loop
193			}
194		}
195		for _, com := range bug.Commits {
196			m[com] = true
197		}
198	}
199	commits := make([]string, 0, len(m))
200	for com := range m {
201		commits = append(commits, com)
202	}
203	sort.Strings(commits)
204	reportEmail := ""
205	for _, reporting := range config.Namespaces[ns].Reporting {
206		if _, ok := reporting.Config.(*EmailConfig); ok {
207			reportEmail = ownEmail(c)
208			break
209		}
210	}
211	resp := &dashapi.BuilderPollResp{
212		PendingCommits: commits,
213		ReportEmail:    reportEmail,
214	}
215	return resp, nil
216}
217
218func apiJobPoll(c context.Context, r *http.Request, payload []byte) (interface{}, error) {
219	req := new(dashapi.JobPollReq)
220	if err := json.Unmarshal(payload, req); err != nil {
221		return nil, fmt.Errorf("failed to unmarshal request: %v", err)
222	}
223	if len(req.Managers) == 0 {
224		return nil, fmt.Errorf("no managers")
225	}
226	return pollPendingJobs(c, req.Managers)
227}
228
229func apiJobDone(c context.Context, r *http.Request, payload []byte) (interface{}, error) {
230	req := new(dashapi.JobDoneReq)
231	if err := json.Unmarshal(payload, req); err != nil {
232		return nil, fmt.Errorf("failed to unmarshal request: %v", err)
233	}
234	err := doneJob(c, req)
235	return nil, err
236}
237
238func apiUploadBuild(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
239	req := new(dashapi.Build)
240	if err := json.Unmarshal(payload, req); err != nil {
241		return nil, fmt.Errorf("failed to unmarshal request: %v", err)
242	}
243	now := timeNow(c)
244	isNewBuild, err := uploadBuild(c, now, ns, req, BuildNormal)
245	if err != nil {
246		return nil, err
247	}
248	if len(req.Commits) != 0 || len(req.FixCommits) != 0 {
249		if err := addCommitsToBugs(c, ns, req.Manager, req.Commits, req.FixCommits); err != nil {
250			return nil, err
251		}
252	}
253	if isNewBuild {
254		if err := updateManager(c, ns, req.Manager, func(mgr *Manager, stats *ManagerStats) {
255			mgr.CurrentBuild = req.ID
256			mgr.FailedBuildBug = ""
257		}); err != nil {
258			return nil, err
259		}
260	}
261	return nil, nil
262}
263
264func uploadBuild(c context.Context, now time.Time, ns string, req *dashapi.Build, typ BuildType) (bool, error) {
265	if _, err := loadBuild(c, ns, req.ID); err == nil {
266		return false, nil
267	}
268
269	checkStrLen := func(str, name string, maxLen int) error {
270		if str == "" {
271			return fmt.Errorf("%v is empty", name)
272		}
273		if len(str) > maxLen {
274			return fmt.Errorf("%v is too long (%v)", name, len(str))
275		}
276		return nil
277	}
278	if err := checkStrLen(req.Manager, "Build.Manager", MaxStringLen); err != nil {
279		return false, err
280	}
281	if err := checkStrLen(req.ID, "Build.ID", MaxStringLen); err != nil {
282		return false, err
283	}
284	if err := checkStrLen(req.KernelRepo, "Build.KernelRepo", MaxStringLen); err != nil {
285		return false, err
286	}
287	if len(req.KernelBranch) > MaxStringLen {
288		return false, fmt.Errorf("Build.KernelBranch is too long (%v)", len(req.KernelBranch))
289	}
290	if err := checkStrLen(req.SyzkallerCommit, "Build.SyzkallerCommit", MaxStringLen); err != nil {
291		return false, err
292	}
293	if len(req.CompilerID) > MaxStringLen {
294		return false, fmt.Errorf("Build.CompilerID is too long (%v)", len(req.CompilerID))
295	}
296	if err := checkStrLen(req.KernelCommit, "Build.KernelCommit", MaxStringLen); err != nil {
297		return false, err
298	}
299	configID, err := putText(c, ns, textKernelConfig, req.KernelConfig, true)
300	if err != nil {
301		return false, err
302	}
303	build := &Build{
304		Namespace:         ns,
305		Manager:           req.Manager,
306		ID:                req.ID,
307		Type:              typ,
308		Time:              now,
309		OS:                req.OS,
310		Arch:              req.Arch,
311		VMArch:            req.VMArch,
312		SyzkallerCommit:   req.SyzkallerCommit,
313		CompilerID:        req.CompilerID,
314		KernelRepo:        req.KernelRepo,
315		KernelBranch:      req.KernelBranch,
316		KernelCommit:      req.KernelCommit,
317		KernelCommitTitle: req.KernelCommitTitle,
318		KernelCommitDate:  req.KernelCommitDate,
319		KernelConfig:      configID,
320	}
321	if _, err := datastore.Put(c, buildKey(c, ns, req.ID), build); err != nil {
322		return false, err
323	}
324	return true, nil
325}
326
327func addCommitsToBugs(c context.Context, ns, manager string,
328	titles []string, fixCommits []dashapi.FixCommit) error {
329	presentCommits := make(map[string]bool)
330	bugFixedBy := make(map[string][]string)
331	for _, com := range titles {
332		presentCommits[com] = true
333	}
334	for _, com := range fixCommits {
335		presentCommits[com.Title] = true
336		bugFixedBy[com.BugID] = append(bugFixedBy[com.BugID], com.Title)
337	}
338	managers, err := managerList(c, ns)
339	if err != nil {
340		return err
341	}
342	var bugs []*Bug
343	_, err = datastore.NewQuery("Bug").
344		Filter("Namespace=", ns).
345		GetAll(c, &bugs)
346	if err != nil {
347		return fmt.Errorf("failed to query bugs: %v", err)
348	}
349nextBug:
350	for _, bug := range bugs {
351		switch bug.Status {
352		case BugStatusOpen, BugStatusDup:
353		case BugStatusFixed, BugStatusInvalid:
354			continue nextBug
355		default:
356			return fmt.Errorf("addCommitsToBugs: unknown bug status %v", bug.Status)
357		}
358		var fixCommits []string
359		for i := range bug.Reporting {
360			fixCommits = append(fixCommits, bugFixedBy[bug.Reporting[i].ID]...)
361		}
362		sort.Strings(fixCommits)
363		if err := addCommitsToBug(c, bug, manager, managers, fixCommits, presentCommits); err != nil {
364			return err
365		}
366		if bug.Status == BugStatusDup {
367			canon, err := canonicalBug(c, bug)
368			if err != nil {
369				return err
370			}
371			if canon.Status == BugStatusOpen && len(bug.Commits) == 0 {
372				if err := addCommitsToBug(c, canon, manager, managers,
373					fixCommits, presentCommits); err != nil {
374					return err
375				}
376			}
377		}
378	}
379	return nil
380}
381
382func addCommitsToBug(c context.Context, bug *Bug, manager string, managers []string,
383	fixCommits []string, presentCommits map[string]bool) error {
384	if !bugNeedsCommitUpdate(c, bug, manager, fixCommits, presentCommits, true) {
385		return nil
386	}
387	now := timeNow(c)
388	bugKey := datastore.NewKey(c, "Bug", bugKeyHash(bug.Namespace, bug.Title, bug.Seq), 0, nil)
389	tx := func(c context.Context) error {
390		bug := new(Bug)
391		if err := datastore.Get(c, bugKey, bug); err != nil {
392			return fmt.Errorf("failed to get bug %v: %v", bugKey.StringID(), err)
393		}
394		if !bugNeedsCommitUpdate(c, bug, manager, fixCommits, presentCommits, false) {
395			return nil
396		}
397		if len(fixCommits) != 0 && !reflect.DeepEqual(bug.Commits, fixCommits) {
398			bug.Commits = fixCommits
399			bug.PatchedOn = nil
400		}
401		bug.PatchedOn = append(bug.PatchedOn, manager)
402		if bug.Status == BugStatusOpen {
403			fixed := true
404			for _, mgr := range managers {
405				if !stringInList(bug.PatchedOn, mgr) {
406					fixed = false
407					break
408				}
409			}
410			if fixed {
411				bug.Status = BugStatusFixed
412				bug.Closed = now
413			}
414		}
415		if _, err := datastore.Put(c, bugKey, bug); err != nil {
416			return fmt.Errorf("failed to put bug: %v", err)
417		}
418		return nil
419	}
420	return datastore.RunInTransaction(c, tx, nil)
421}
422
423func managerList(c context.Context, ns string) ([]string, error) {
424	var builds []*Build
425	_, err := datastore.NewQuery("Build").
426		Filter("Namespace=", ns).
427		Project("Manager").
428		Distinct().
429		GetAll(c, &builds)
430	if err != nil {
431		return nil, fmt.Errorf("failed to query builds: %v", err)
432	}
433	configManagers := config.Namespaces[ns].Managers
434	var managers []string
435	for _, build := range builds {
436		if configManagers[build.Manager].Decommissioned {
437			continue
438		}
439		managers = append(managers, build.Manager)
440	}
441	return managers, nil
442}
443
444func bugNeedsCommitUpdate(c context.Context, bug *Bug, manager string, fixCommits []string,
445	presentCommits map[string]bool, dolog bool) bool {
446	if len(fixCommits) != 0 && !reflect.DeepEqual(bug.Commits, fixCommits) {
447		if dolog {
448			log.Infof(c, "bug %q is fixed with %q", bug.Title, fixCommits)
449		}
450		return true
451	}
452	if len(bug.Commits) == 0 || stringInList(bug.PatchedOn, manager) {
453		return false
454	}
455	for _, com := range bug.Commits {
456		if !presentCommits[com] {
457			return false
458		}
459	}
460	return true
461}
462
463func stringInList(list []string, str string) bool {
464	for _, s := range list {
465		if s == str {
466			return true
467		}
468	}
469	return false
470}
471
472func stringsInList(list, str []string) bool {
473	for _, s := range str {
474		if !stringInList(list, s) {
475			return false
476		}
477	}
478	return true
479}
480
481func apiReportBuildError(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
482	req := new(dashapi.BuildErrorReq)
483	if err := json.Unmarshal(payload, req); err != nil {
484		return nil, fmt.Errorf("failed to unmarshal request: %v", err)
485	}
486	now := timeNow(c)
487	if _, err := uploadBuild(c, now, ns, &req.Build, BuildFailed); err != nil {
488		return nil, err
489	}
490	req.Crash.BuildID = req.Build.ID
491	bug, err := reportCrash(c, ns, &req.Crash)
492	if err != nil {
493		return nil, err
494	}
495	if err := updateManager(c, ns, req.Build.Manager, func(mgr *Manager, stats *ManagerStats) {
496		mgr.FailedBuildBug = bugKeyHash(bug.Namespace, bug.Title, bug.Seq)
497	}); err != nil {
498		return nil, err
499	}
500	return nil, nil
501}
502
503const corruptedReportTitle = "corrupted report"
504
505func apiReportCrash(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
506	req := new(dashapi.Crash)
507	if err := json.Unmarshal(payload, req); err != nil {
508		return nil, fmt.Errorf("failed to unmarshal request: %v", err)
509	}
510	bug, err := reportCrash(c, ns, req)
511	if err != nil {
512		return nil, err
513	}
514	resp := &dashapi.ReportCrashResp{
515		NeedRepro: needRepro(c, bug),
516	}
517	return resp, nil
518}
519
520func reportCrash(c context.Context, ns string, req *dashapi.Crash) (*Bug, error) {
521	req.Title = limitLength(req.Title, maxTextLen)
522	req.Maintainers = email.MergeEmailLists(req.Maintainers)
523	if req.Corrupted {
524		// The report is corrupted and the title is most likely invalid.
525		// Such reports are usually unactionable and are discarded.
526		// Collect them into a single bin.
527		req.Title = corruptedReportTitle
528	}
529
530	bug, bugKey, err := findBugForCrash(c, ns, req.Title)
531	if err != nil {
532		return nil, err
533	}
534	if active, err := isActiveBug(c, bug); err != nil {
535		return nil, err
536	} else if !active {
537		bug, bugKey, err = createBugForCrash(c, ns, req)
538		if err != nil {
539			return nil, err
540		}
541	}
542	build, err := loadBuild(c, ns, req.BuildID)
543	if err != nil {
544		return nil, err
545	}
546
547	now := timeNow(c)
548	reproLevel := ReproLevelNone
549	if len(req.ReproC) != 0 {
550		reproLevel = ReproLevelC
551	} else if len(req.ReproSyz) != 0 {
552		reproLevel = ReproLevelSyz
553	}
554	save := reproLevel != ReproLevelNone ||
555		bug.NumCrashes < maxCrashes ||
556		now.Sub(bug.LastSavedCrash) > time.Hour ||
557		bug.NumCrashes%20 == 0
558	if save {
559		if err := saveCrash(c, ns, req, bugKey, build); err != nil {
560			return nil, err
561		}
562	} else {
563		log.Infof(c, "not saving crash for %q", bug.Title)
564	}
565
566	tx := func(c context.Context) error {
567		bug = new(Bug)
568		if err := datastore.Get(c, bugKey, bug); err != nil {
569			return fmt.Errorf("failed to get bug: %v", err)
570		}
571		bug.NumCrashes++
572		bug.LastTime = now
573		if save {
574			bug.LastSavedCrash = now
575		}
576		if reproLevel != ReproLevelNone {
577			bug.NumRepro++
578			bug.LastReproTime = now
579		}
580		if bug.ReproLevel < reproLevel {
581			bug.ReproLevel = reproLevel
582		}
583		if len(req.Report) != 0 {
584			bug.HasReport = true
585		}
586		if !stringInList(bug.HappenedOn, build.Manager) {
587			bug.HappenedOn = append(bug.HappenedOn, build.Manager)
588		}
589		if _, err = datastore.Put(c, bugKey, bug); err != nil {
590			return fmt.Errorf("failed to put bug: %v", err)
591		}
592		return nil
593	}
594	if err := datastore.RunInTransaction(c, tx, &datastore.TransactionOptions{XG: true}); err != nil {
595		return nil, err
596	}
597	if save {
598		purgeOldCrashes(c, bug, bugKey)
599	}
600	return bug, nil
601}
602
603func saveCrash(c context.Context, ns string, req *dashapi.Crash, bugKey *datastore.Key, build *Build) error {
604	// Reporting priority of this crash.
605	prio := int64(kernelRepoInfo(build).ReportingPriority) * 1e6
606	if len(req.ReproC) != 0 {
607		prio += 4e12
608	} else if len(req.ReproSyz) != 0 {
609		prio += 2e12
610	}
611	if build.Arch == "amd64" {
612		prio += 1e3
613	}
614	crash := &Crash{
615		Manager:     build.Manager,
616		BuildID:     req.BuildID,
617		Time:        timeNow(c),
618		Maintainers: req.Maintainers,
619		ReproOpts:   req.ReproOpts,
620		ReportLen:   prio,
621	}
622	var err error
623	if crash.Log, err = putText(c, ns, textCrashLog, req.Log, false); err != nil {
624		return err
625	}
626	if crash.Report, err = putText(c, ns, textCrashReport, req.Report, false); err != nil {
627		return err
628	}
629	if crash.ReproSyz, err = putText(c, ns, textReproSyz, req.ReproSyz, false); err != nil {
630		return err
631	}
632	if crash.ReproC, err = putText(c, ns, textReproC, req.ReproC, false); err != nil {
633		return err
634	}
635	crashKey := datastore.NewIncompleteKey(c, "Crash", bugKey)
636	if _, err = datastore.Put(c, crashKey, crash); err != nil {
637		return fmt.Errorf("failed to put crash: %v", err)
638	}
639	return nil
640}
641
642func purgeOldCrashes(c context.Context, bug *Bug, bugKey *datastore.Key) {
643	if bug.NumCrashes <= maxCrashes || (bug.NumCrashes-1)%10 != 0 {
644		return
645	}
646	var crashes []*Crash
647	keys, err := datastore.NewQuery("Crash").
648		Ancestor(bugKey).
649		Filter("ReproC=", 0).
650		Filter("ReproSyz=", 0).
651		Filter("Reported=", time.Time{}).
652		GetAll(c, &crashes)
653	if err != nil {
654		log.Errorf(c, "failed to fetch purge crashes: %v", err)
655		return
656	}
657	if len(keys) <= maxCrashes {
658		return
659	}
660	keyMap := make(map[*Crash]*datastore.Key)
661	for i, crash := range crashes {
662		keyMap[crash] = keys[i]
663	}
664	// Newest first.
665	sort.Slice(crashes, func(i, j int) bool {
666		return crashes[i].Time.After(crashes[j].Time)
667	})
668	// Find latest crash on each manager.
669	latestOnManager := make(map[string]*Crash)
670	for _, crash := range crashes {
671		if latestOnManager[crash.Manager] == nil {
672			latestOnManager[crash.Manager] = crash
673		}
674	}
675	// Oldest first but move latest crash on each manager to the end (preserve them).
676	sort.Slice(crashes, func(i, j int) bool {
677		latesti := latestOnManager[crashes[i].Manager] == crashes[i]
678		latestj := latestOnManager[crashes[j].Manager] == crashes[j]
679		if latesti != latestj {
680			return latestj
681		}
682		return crashes[i].Time.Before(crashes[j].Time)
683	})
684	crashes = crashes[:len(crashes)-maxCrashes]
685	var toDelete []*datastore.Key
686	for _, crash := range crashes {
687		if crash.ReproSyz != 0 || crash.ReproC != 0 || !crash.Reported.IsZero() {
688			log.Errorf(c, "purging reproducer?")
689			continue
690		}
691		toDelete = append(toDelete, keyMap[crash])
692		if crash.Log != 0 {
693			toDelete = append(toDelete, datastore.NewKey(c, textCrashLog, "", crash.Log, nil))
694		}
695		if crash.Report != 0 {
696			toDelete = append(toDelete, datastore.NewKey(c, textCrashReport, "", crash.Report, nil))
697		}
698	}
699	if err := datastore.DeleteMulti(c, toDelete); err != nil {
700		log.Errorf(c, "failed to delete old crashes: %v", err)
701		return
702	}
703	log.Infof(c, "deleted %v crashes for bug %q", len(crashes), bug.Title)
704}
705
706func apiReportFailedRepro(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
707	req := new(dashapi.CrashID)
708	if err := json.Unmarshal(payload, req); err != nil {
709		return nil, fmt.Errorf("failed to unmarshal request: %v", err)
710	}
711	req.Title = limitLength(req.Title, maxTextLen)
712
713	bug, bugKey, err := findBugForCrash(c, ns, req.Title)
714	if err != nil {
715		return nil, err
716	}
717	if bug == nil {
718		return nil, fmt.Errorf("%v: can't find bug for crash %q", ns, req.Title)
719	}
720	now := timeNow(c)
721	tx := func(c context.Context) error {
722		bug := new(Bug)
723		if err := datastore.Get(c, bugKey, bug); err != nil {
724			return fmt.Errorf("failed to get bug: %v", err)
725		}
726		bug.NumRepro++
727		bug.LastReproTime = now
728		if _, err := datastore.Put(c, bugKey, bug); err != nil {
729			return fmt.Errorf("failed to put bug: %v", err)
730		}
731		return nil
732	}
733	err = datastore.RunInTransaction(c, tx, &datastore.TransactionOptions{
734		XG:       true,
735		Attempts: 30,
736	})
737	return nil, err
738}
739
740func apiNeedRepro(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
741	req := new(dashapi.CrashID)
742	if err := json.Unmarshal(payload, req); err != nil {
743		return nil, fmt.Errorf("failed to unmarshal request: %v", err)
744	}
745	if req.Corrupted {
746		resp := &dashapi.NeedReproResp{
747			NeedRepro: false,
748		}
749		return resp, nil
750	}
751	req.Title = limitLength(req.Title, maxTextLen)
752
753	bug, _, err := findBugForCrash(c, ns, req.Title)
754	if err != nil {
755		return nil, err
756	}
757	if bug == nil {
758		return nil, fmt.Errorf("%v: can't find bug for crash %q", ns, req.Title)
759	}
760	resp := &dashapi.NeedReproResp{
761		NeedRepro: needRepro(c, bug),
762	}
763	return resp, nil
764}
765
766func apiManagerStats(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
767	req := new(dashapi.ManagerStatsReq)
768	if err := json.Unmarshal(payload, req); err != nil {
769		return nil, fmt.Errorf("failed to unmarshal request: %v", err)
770	}
771	now := timeNow(c)
772	err := updateManager(c, ns, req.Name, func(mgr *Manager, stats *ManagerStats) {
773		mgr.Link = req.Addr
774		mgr.LastAlive = now
775		mgr.CurrentUpTime = req.UpTime
776		if cur := int64(req.Corpus); cur > stats.MaxCorpus {
777			stats.MaxCorpus = cur
778		}
779		if cur := int64(req.Cover); cur > stats.MaxCover {
780			stats.MaxCover = cur
781		}
782		stats.TotalFuzzingTime += req.FuzzingTime
783		stats.TotalCrashes += int64(req.Crashes)
784		stats.TotalExecs += int64(req.Execs)
785	})
786	return nil, err
787}
788
789func findBugForCrash(c context.Context, ns, title string) (*Bug, *datastore.Key, error) {
790	var bugs []*Bug
791	keys, err := datastore.NewQuery("Bug").
792		Filter("Namespace=", ns).
793		Filter("Title=", title).
794		Order("-Seq").
795		Limit(1).
796		GetAll(c, &bugs)
797	if err != nil {
798		return nil, nil, fmt.Errorf("failed to query bugs: %v", err)
799	}
800	if len(bugs) == 0 {
801		return nil, nil, nil
802	}
803	return bugs[0], keys[0], nil
804}
805
806func createBugForCrash(c context.Context, ns string, req *dashapi.Crash) (*Bug, *datastore.Key, error) {
807	var bug *Bug
808	var bugKey *datastore.Key
809	now := timeNow(c)
810	tx := func(c context.Context) error {
811		for seq := int64(0); ; seq++ {
812			bug = new(Bug)
813			bugHash := bugKeyHash(ns, req.Title, seq)
814			bugKey = datastore.NewKey(c, "Bug", bugHash, 0, nil)
815			if err := datastore.Get(c, bugKey, bug); err != nil {
816				if err != datastore.ErrNoSuchEntity {
817					return fmt.Errorf("failed to get bug: %v", err)
818				}
819				bug = &Bug{
820					Namespace:  ns,
821					Seq:        seq,
822					Title:      req.Title,
823					Status:     BugStatusOpen,
824					NumCrashes: 0,
825					NumRepro:   0,
826					ReproLevel: ReproLevelNone,
827					HasReport:  false,
828					FirstTime:  now,
829					LastTime:   now,
830				}
831				for _, rep := range config.Namespaces[ns].Reporting {
832					bug.Reporting = append(bug.Reporting, BugReporting{
833						Name: rep.Name,
834						ID:   bugReportingHash(bugHash, rep.Name),
835					})
836				}
837				if bugKey, err = datastore.Put(c, bugKey, bug); err != nil {
838					return fmt.Errorf("failed to put new bug: %v", err)
839				}
840				return nil
841			}
842			canon, err := canonicalBug(c, bug)
843			if err != nil {
844				return err
845			}
846			if canon.Status != BugStatusOpen {
847				continue
848			}
849			return nil
850		}
851	}
852	if err := datastore.RunInTransaction(c, tx, &datastore.TransactionOptions{
853		XG:       true,
854		Attempts: 30,
855	}); err != nil {
856		return nil, nil, err
857	}
858	return bug, bugKey, nil
859}
860
861func isActiveBug(c context.Context, bug *Bug) (bool, error) {
862	if bug == nil {
863		return false, nil
864	}
865	canon, err := canonicalBug(c, bug)
866	if err != nil {
867		return false, err
868	}
869	return canon.Status == BugStatusOpen, nil
870}
871
872func needRepro(c context.Context, bug *Bug) bool {
873	if !needReproForBug(c, bug) {
874		return false
875	}
876	canon, err := canonicalBug(c, bug)
877	if err != nil {
878		log.Errorf(c, "failed to get canonical bug: %v", err)
879		return false
880	}
881	return needReproForBug(c, canon)
882}
883
884func needReproForBug(c context.Context, bug *Bug) bool {
885	return bug.ReproLevel < ReproLevelC &&
886		len(bug.Commits) == 0 &&
887		bug.Title != corruptedReportTitle &&
888		(bug.NumRepro < maxReproPerBug ||
889			bug.ReproLevel == ReproLevelNone && timeSince(c, bug.LastReproTime) > reproRetryPeriod)
890}
891
892func putText(c context.Context, ns, tag string, data []byte, dedup bool) (int64, error) {
893	if ns == "" {
894		return 0, fmt.Errorf("putting text outside of namespace")
895	}
896	if len(data) == 0 {
897		return 0, nil
898	}
899	const (
900		maxTextLen       = 2 << 20
901		maxCompressedLen = 1000 << 10 // datastore entity limit is 1MB
902	)
903	if len(data) > maxTextLen {
904		data = data[:maxTextLen]
905	}
906	b := new(bytes.Buffer)
907	for {
908		z, _ := gzip.NewWriterLevel(b, gzip.BestCompression)
909		z.Write(data)
910		z.Close()
911		if len(b.Bytes()) < maxCompressedLen {
912			break
913		}
914		data = data[:len(data)/10*9]
915		b.Reset()
916	}
917	var key *datastore.Key
918	if dedup {
919		h := hash.Hash([]byte(ns), b.Bytes())
920		key = datastore.NewKey(c, tag, "", h.Truncate64(), nil)
921	} else {
922		key = datastore.NewIncompleteKey(c, tag, nil)
923	}
924	text := &Text{
925		Namespace: ns,
926		Text:      b.Bytes(),
927	}
928	key, err := datastore.Put(c, key, text)
929	if err != nil {
930		return 0, err
931	}
932	return key.IntID(), nil
933}
934
935func getText(c context.Context, tag string, id int64) ([]byte, string, error) {
936	if id == 0 {
937		return nil, "", nil
938	}
939	text := new(Text)
940	if err := datastore.Get(c, datastore.NewKey(c, tag, "", id, nil), text); err != nil {
941		return nil, "", fmt.Errorf("failed to read text %v: %v", tag, err)
942	}
943	d, err := gzip.NewReader(bytes.NewBuffer(text.Text))
944	if err != nil {
945		return nil, "", fmt.Errorf("failed to read text %v: %v", tag, err)
946	}
947	data, err := ioutil.ReadAll(d)
948	if err != nil {
949		return nil, "", fmt.Errorf("failed to read text %v: %v", tag, err)
950	}
951	return data, text.Namespace, nil
952}
953
954// limitLength essentially does return s[:max],
955// but it ensures that we dot not split UTF-8 rune in half.
956// Otherwise appengine python scripts will break badly.
957func limitLength(s string, max int) string {
958	s = strings.TrimSpace(s)
959	if len(s) <= max {
960		return s
961	}
962	for {
963		s = s[:max]
964		r, size := utf8.DecodeLastRuneInString(s)
965		if r != utf8.RuneError || size != 1 {
966			return s
967		}
968		max--
969	}
970}
971