Home | History | Annotate | Download | only in app
      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 
      4 package dash
      5 
      6 import (
      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 
     28 func initAPIHandlers() {
     29 	http.Handle("/api", handleJSON(handleAPI))
     30 }
     31 
     32 var 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 
     41 var 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 
     51 type JSONHandler func(c context.Context, r *http.Request) (interface{}, error)
     52 type APIHandler func(c context.Context, r *http.Request, payload []byte) (interface{}, error)
     53 type APINamespaceHandler func(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error)
     54 
     55 const (
     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.
     61 var timeNow = func(c context.Context) time.Time {
     62 	return time.Now()
     63 }
     64 
     65 func timeSince(c context.Context, t time.Time) time.Duration {
     66 	return timeNow(c).Sub(t)
     67 }
     68 
     69 func 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 
     97 func 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 
    139 func 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 
    161 func 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 
    170 func 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)
    184 loop:
    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 
    218 func 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 
    229 func 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 
    238 func 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 
    264 func 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 
    327 func 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 	}
    349 nextBug:
    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 
    382 func 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 
    423 func 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 
    444 func 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 
    463 func 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 
    472 func 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 
    481 func 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 
    503 const corruptedReportTitle = "corrupted report"
    504 
    505 func 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 
    520 func 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 
    603 func 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 
    642 func 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 
    706 func 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 
    740 func 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 
    766 func 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 
    789 func 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 
    806 func 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 
    861 func 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 
    872 func 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 
    884 func 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 
    892 func 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 
    935 func 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.
    957 func 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