github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/api.go (about)

     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 main
     5  
     6  import (
     7  	"bytes"
     8  	"compress/gzip"
     9  	"context"
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"math/rand"
    14  	"net/http"
    15  	"net/url"
    16  	"reflect"
    17  	"regexp"
    18  	"sort"
    19  	"strings"
    20  	"time"
    21  	"unicode/utf8"
    22  
    23  	"github.com/google/syzkaller/dashboard/dashapi"
    24  	"github.com/google/syzkaller/pkg/asset"
    25  	"github.com/google/syzkaller/pkg/auth"
    26  	"github.com/google/syzkaller/pkg/debugtracer"
    27  	"github.com/google/syzkaller/pkg/email"
    28  	"github.com/google/syzkaller/pkg/hash"
    29  	"github.com/google/syzkaller/pkg/subsystem"
    30  	"github.com/google/syzkaller/sys/targets"
    31  	"google.golang.org/appengine/v2"
    32  	db "google.golang.org/appengine/v2/datastore"
    33  	"google.golang.org/appengine/v2/log"
    34  	"google.golang.org/appengine/v2/user"
    35  )
    36  
    37  func initAPIHandlers() {
    38  	http.Handle("/api", handleJSON(handleAPI))
    39  }
    40  
    41  var apiHandlers = map[string]APIHandler{
    42  	"log_error":             apiLogError,
    43  	"job_poll":              apiJobPoll,
    44  	"job_reset":             apiJobReset,
    45  	"job_done":              apiJobDone,
    46  	"reporting_poll_bugs":   apiReportingPollBugs,
    47  	"reporting_poll_notifs": apiReportingPollNotifications,
    48  	"reporting_poll_closed": apiReportingPollClosed,
    49  	"reporting_update":      apiReportingUpdate,
    50  	"new_test_job":          apiNewTestJob,
    51  	"needed_assets":         apiNeededAssetsList,
    52  	"load_full_bug":         apiLoadFullBug,
    53  	"save_discussion":       apiSaveDiscussion,
    54  }
    55  
    56  var apiNamespaceHandlers = map[string]APINamespaceHandler{
    57  	"upload_build":        apiUploadBuild,
    58  	"builder_poll":        apiBuilderPoll,
    59  	"report_build_error":  apiReportBuildError,
    60  	"report_crash":        apiReportCrash,
    61  	"report_failed_repro": apiReportFailedRepro,
    62  	"need_repro":          apiNeedRepro,
    63  	"manager_stats":       apiManagerStats,
    64  	"commit_poll":         apiCommitPoll,
    65  	"upload_commits":      apiUploadCommits,
    66  	"bug_list":            apiBugList,
    67  	"load_bug":            apiLoadBug,
    68  	"update_report":       apiUpdateReport,
    69  	"add_build_assets":    apiAddBuildAssets,
    70  	"log_to_repro":        apiLogToReproduce,
    71  }
    72  
    73  type JSONHandler func(c context.Context, r *http.Request) (interface{}, error)
    74  type APIHandler func(c context.Context, r *http.Request, payload []byte) (interface{}, error)
    75  type APINamespaceHandler func(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error)
    76  
    77  const (
    78  	maxReproPerBug   = 10
    79  	reproRetryPeriod = 24 * time.Hour // try 1 repro per day until we have at least syz repro
    80  	// Attempt a new repro every ~ 3 months, even if we have already found it for the bug. This should:
    81  	// 1) Improve old repros over time (as we update descriptions / change syntax / repro algorithms).
    82  	// 2) Constrain the impact of bugs in syzkaller's backward compatibility. Fewer old repros, fewer problems.
    83  	reproStalePeriod = 100 * 24 * time.Hour
    84  )
    85  
    86  // Overridable for testing.
    87  var timeNow = func(c context.Context) time.Time {
    88  	return time.Now()
    89  }
    90  
    91  func timeSince(c context.Context, t time.Time) time.Duration {
    92  	return timeNow(c).Sub(t)
    93  }
    94  
    95  var maxCrashes = func() int {
    96  	const maxCrashesPerBug = 40
    97  	return maxCrashesPerBug
    98  }
    99  
   100  func handleJSON(fn JSONHandler) http.Handler {
   101  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   102  		c := appengine.NewContext(r)
   103  		reply, err := fn(c, r)
   104  		if err != nil {
   105  			// ErrAccess is logged earlier.
   106  			if err != ErrAccess {
   107  				log.Errorf(c, "%v", err)
   108  			}
   109  			http.Error(w, err.Error(), http.StatusInternalServerError)
   110  			return
   111  		}
   112  		w.Header().Set("Content-Type", "application/json")
   113  		if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
   114  			w.Header().Set("Content-Encoding", "gzip")
   115  			gz := gzip.NewWriter(w)
   116  			if err := json.NewEncoder(gz).Encode(reply); err != nil {
   117  				log.Errorf(c, "failed to encode reply: %v", err)
   118  			}
   119  			gz.Close()
   120  		} else {
   121  			if err := json.NewEncoder(w).Encode(reply); err != nil {
   122  				log.Errorf(c, "failed to encode reply: %v", err)
   123  			}
   124  		}
   125  	})
   126  }
   127  
   128  func handleAPI(c context.Context, r *http.Request) (reply interface{}, err error) {
   129  	client := r.PostFormValue("client")
   130  	method := r.PostFormValue("method")
   131  	log.Infof(c, "api %q from %q", method, client)
   132  	auth := auth.MakeEndpoint(auth.GoogleTokenInfoEndpoint)
   133  	subj, err := auth.DetermineAuthSubj(timeNow(c), r.Header["Authorization"])
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	// Somewhat confusingly the "key" parameter is the password.
   138  	ns, err := checkClient(getConfig(c), client, r.PostFormValue("key"), subj)
   139  	if err != nil {
   140  		if client != "" {
   141  			log.Errorf(c, "%v", err)
   142  		} else {
   143  			// Don't log as error if somebody just invokes /api.
   144  			log.Infof(c, "%v", err)
   145  		}
   146  		return nil, err
   147  	}
   148  	var payload []byte
   149  	if str := r.PostFormValue("payload"); str != "" {
   150  		gr, err := gzip.NewReader(strings.NewReader(str))
   151  		if err != nil {
   152  			return nil, fmt.Errorf("failed to ungzip payload: %w", err)
   153  		}
   154  		payload, err = io.ReadAll(gr)
   155  		if err != nil {
   156  			return nil, fmt.Errorf("failed to ungzip payload: %w", err)
   157  		}
   158  		if err := gr.Close(); err != nil {
   159  			return nil, fmt.Errorf("failed to ungzip payload: %w", err)
   160  		}
   161  	}
   162  	handler := apiHandlers[method]
   163  	if handler != nil {
   164  		return handler(c, r, payload)
   165  	}
   166  	nsHandler := apiNamespaceHandlers[method]
   167  	if nsHandler == nil {
   168  		return nil, fmt.Errorf("unknown api method %q", method)
   169  	}
   170  	if ns == "" {
   171  		return nil, fmt.Errorf("method %q must be called within a namespace", method)
   172  	}
   173  	return nsHandler(c, ns, r, payload)
   174  }
   175  
   176  func apiLogError(c context.Context, r *http.Request, payload []byte) (interface{}, error) {
   177  	req := new(dashapi.LogEntry)
   178  	if err := json.Unmarshal(payload, req); err != nil {
   179  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
   180  	}
   181  	log.Errorf(c, "%v: %v", req.Name, req.Text)
   182  	return nil, nil
   183  }
   184  
   185  func apiBuilderPoll(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
   186  	req := new(dashapi.BuilderPollReq)
   187  	if err := json.Unmarshal(payload, req); err != nil {
   188  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
   189  	}
   190  	bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query {
   191  		return query.Filter("Namespace=", ns).
   192  			Filter("Status<", BugStatusFixed)
   193  	})
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  	m := make(map[string]bool)
   198  loop:
   199  	for _, bug := range bugs {
   200  		// TODO(dvyukov): include this condition into the query if possible.
   201  		if len(bug.Commits) == 0 {
   202  			continue
   203  		}
   204  		for _, mgr := range bug.PatchedOn {
   205  			if mgr == req.Manager {
   206  				continue loop
   207  			}
   208  		}
   209  		for _, com := range bug.Commits {
   210  			m[com] = true
   211  		}
   212  	}
   213  	commits := make([]string, 0, len(m))
   214  	for com := range m {
   215  		commits = append(commits, com)
   216  	}
   217  	sort.Strings(commits)
   218  	resp := &dashapi.BuilderPollResp{
   219  		PendingCommits: commits,
   220  		ReportEmail:    reportEmail(c, ns),
   221  	}
   222  	return resp, nil
   223  }
   224  
   225  func reportEmail(c context.Context, ns string) string {
   226  	for _, reporting := range getNsConfig(c, ns).Reporting {
   227  		if _, ok := reporting.Config.(*EmailConfig); ok {
   228  			return ownEmail(c)
   229  		}
   230  	}
   231  	return ""
   232  }
   233  
   234  func apiCommitPoll(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
   235  	resp := &dashapi.CommitPollResp{
   236  		ReportEmail: reportEmail(c, ns),
   237  	}
   238  	for _, repo := range getNsConfig(c, ns).Repos {
   239  		if repo.NoPoll {
   240  			continue
   241  		}
   242  		resp.Repos = append(resp.Repos, dashapi.Repo{
   243  			URL:    repo.URL,
   244  			Branch: repo.Branch,
   245  		})
   246  	}
   247  	var bugs []*Bug
   248  	_, err := db.NewQuery("Bug").
   249  		Filter("Namespace=", ns).
   250  		Filter("NeedCommitInfo=", true).
   251  		Project("Commits").
   252  		Limit(100).
   253  		GetAll(c, &bugs)
   254  	if err != nil {
   255  		return nil, fmt.Errorf("failed to query bugs: %w", err)
   256  	}
   257  	commits := make(map[string]bool)
   258  	for _, bug := range bugs {
   259  		for _, com := range bug.Commits {
   260  			commits[com] = true
   261  		}
   262  	}
   263  	for com := range commits {
   264  		resp.Commits = append(resp.Commits, com)
   265  	}
   266  	if getNsConfig(c, ns).RetestMissingBackports {
   267  		const takeBackportTitles = 5
   268  		backportCommits, err := pollBackportCommits(c, ns, takeBackportTitles)
   269  		if err != nil {
   270  			return nil, err
   271  		}
   272  		resp.Commits = append(resp.Commits, backportCommits...)
   273  	}
   274  	return resp, nil
   275  }
   276  
   277  func pollBackportCommits(c context.Context, ns string, count int) ([]string, error) {
   278  	// Let's assume that there won't be too many pending backports.
   279  	bugs, jobs, _, err := relevantBackportJobs(c)
   280  	if err != nil {
   281  		return nil, fmt.Errorf("failed to query backport: %w", err)
   282  	}
   283  	var backportTitles []string
   284  	for i, bug := range bugs {
   285  		if bug.Namespace != ns {
   286  			continue
   287  		}
   288  		backportTitles = append(backportTitles, jobs[i].Commits[0].Title)
   289  	}
   290  	randomizer := rand.New(rand.NewSource(timeNow(c).UnixNano()))
   291  	randomizer.Shuffle(len(backportTitles), func(i, j int) {
   292  		backportTitles[i], backportTitles[j] = backportTitles[j], backportTitles[i]
   293  	})
   294  	if len(backportTitles) > count {
   295  		backportTitles = backportTitles[:count]
   296  	}
   297  	return backportTitles, nil
   298  }
   299  
   300  func apiUploadCommits(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
   301  	req := new(dashapi.CommitPollResultReq)
   302  	if err := json.Unmarshal(payload, req); err != nil {
   303  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
   304  	}
   305  	// This adds fixing commits to bugs.
   306  	err := addCommitsToBugs(c, ns, "", nil, req.Commits)
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  	// Now add commit info to commits.
   311  	for _, com := range req.Commits {
   312  		if com.Hash == "" {
   313  			continue
   314  		}
   315  		if err := addCommitInfo(c, ns, com); err != nil {
   316  			return nil, err
   317  		}
   318  	}
   319  	if getNsConfig(c, ns).RetestMissingBackports {
   320  		err = updateBackportCommits(c, ns, req.Commits)
   321  		if err != nil {
   322  			return nil, fmt.Errorf("failed to update backport commits: %w", err)
   323  		}
   324  	}
   325  	return nil, nil
   326  }
   327  
   328  func addCommitInfo(c context.Context, ns string, com dashapi.Commit) error {
   329  	var bugs []*Bug
   330  	keys, err := db.NewQuery("Bug").
   331  		Filter("Namespace=", ns).
   332  		Filter("Commits=", com.Title).
   333  		GetAll(c, &bugs)
   334  	if err != nil {
   335  		return fmt.Errorf("failed to query bugs: %w", err)
   336  	}
   337  	for i, bug := range bugs {
   338  		if err := addCommitInfoToBug(c, bug, keys[i], com); err != nil {
   339  			return err
   340  		}
   341  	}
   342  	return nil
   343  }
   344  
   345  func addCommitInfoToBug(c context.Context, bug *Bug, bugKey *db.Key, com dashapi.Commit) error {
   346  	if needUpdate, err := addCommitInfoToBugImpl(c, bug, com); err != nil {
   347  		return err
   348  	} else if !needUpdate {
   349  		return nil
   350  	}
   351  	tx := func(c context.Context) error {
   352  		bug := new(Bug)
   353  		if err := db.Get(c, bugKey, bug); err != nil {
   354  			return fmt.Errorf("failed to get bug %v: %w", bugKey.StringID(), err)
   355  		}
   356  		if needUpdate, err := addCommitInfoToBugImpl(c, bug, com); err != nil {
   357  			return err
   358  		} else if !needUpdate {
   359  			return nil
   360  		}
   361  		if _, err := db.Put(c, bugKey, bug); err != nil {
   362  			return fmt.Errorf("failed to put bug: %w", err)
   363  		}
   364  		return nil
   365  	}
   366  	return db.RunInTransaction(c, tx, nil)
   367  }
   368  
   369  func addCommitInfoToBugImpl(c context.Context, bug *Bug, com dashapi.Commit) (bool, error) {
   370  	ci := -1
   371  	for i, title := range bug.Commits {
   372  		if title == com.Title {
   373  			ci = i
   374  			break
   375  		}
   376  	}
   377  	if ci < 0 {
   378  		return false, nil
   379  	}
   380  	for len(bug.CommitInfo) < len(bug.Commits) {
   381  		bug.CommitInfo = append(bug.CommitInfo, Commit{})
   382  	}
   383  	hash0 := bug.CommitInfo[ci].Hash
   384  	date0 := bug.CommitInfo[ci].Date
   385  	author0 := bug.CommitInfo[ci].Author
   386  	needCommitInfo0 := bug.NeedCommitInfo
   387  
   388  	bug.CommitInfo[ci].Hash = com.Hash
   389  	bug.CommitInfo[ci].Date = com.Date
   390  	bug.CommitInfo[ci].Author = com.Author
   391  	bug.NeedCommitInfo = false
   392  	for i := range bug.CommitInfo {
   393  		if bug.CommitInfo[i].Hash == "" {
   394  			bug.NeedCommitInfo = true
   395  			break
   396  		}
   397  	}
   398  	changed := hash0 != bug.CommitInfo[ci].Hash ||
   399  		date0 != bug.CommitInfo[ci].Date ||
   400  		author0 != bug.CommitInfo[ci].Author ||
   401  		needCommitInfo0 != bug.NeedCommitInfo
   402  	return changed, nil
   403  }
   404  
   405  func apiJobPoll(c context.Context, r *http.Request, payload []byte) (interface{}, error) {
   406  	if stop, err := emergentlyStopped(c); err != nil || stop {
   407  		// The bot's operation was aborted. Don't accept new crash reports.
   408  		return &dashapi.JobPollResp{}, err
   409  	}
   410  	req := new(dashapi.JobPollReq)
   411  	if err := json.Unmarshal(payload, req); err != nil {
   412  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
   413  	}
   414  	if len(req.Managers) == 0 {
   415  		return nil, fmt.Errorf("no managers")
   416  	}
   417  	return pollPendingJobs(c, req.Managers)
   418  }
   419  
   420  // nolint: dupl
   421  func apiJobDone(c context.Context, r *http.Request, payload []byte) (interface{}, error) {
   422  	req := new(dashapi.JobDoneReq)
   423  	if err := json.Unmarshal(payload, req); err != nil {
   424  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
   425  	}
   426  	err := doneJob(c, req)
   427  	return nil, err
   428  }
   429  
   430  // nolint: dupl
   431  func apiJobReset(c context.Context, r *http.Request, payload []byte) (interface{}, error) {
   432  	req := new(dashapi.JobResetReq)
   433  	if err := json.Unmarshal(payload, req); err != nil {
   434  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
   435  	}
   436  	err := resetJobs(c, req)
   437  	return nil, err
   438  }
   439  
   440  func apiUploadBuild(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
   441  	req := new(dashapi.Build)
   442  	if err := json.Unmarshal(payload, req); err != nil {
   443  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
   444  	}
   445  	now := timeNow(c)
   446  	_, isNewBuild, err := uploadBuild(c, now, ns, req, BuildNormal)
   447  	if err != nil {
   448  		return nil, err
   449  	}
   450  	if isNewBuild {
   451  		err := updateManager(c, ns, req.Manager, func(mgr *Manager, stats *ManagerStats) error {
   452  			prevKernel, prevSyzkaller := "", ""
   453  			if mgr.CurrentBuild != "" {
   454  				prevBuild, err := loadBuild(c, ns, mgr.CurrentBuild)
   455  				if err != nil {
   456  					return err
   457  				}
   458  				prevKernel = prevBuild.KernelCommit
   459  				prevSyzkaller = prevBuild.SyzkallerCommit
   460  			}
   461  			log.Infof(c, "new build on %v: kernel %v->%v syzkaller %v->%v",
   462  				req.Manager, prevKernel, req.KernelCommit, prevSyzkaller, req.SyzkallerCommit)
   463  			mgr.CurrentBuild = req.ID
   464  			if req.KernelCommit != prevKernel {
   465  				mgr.FailedBuildBug = ""
   466  			}
   467  			if req.SyzkallerCommit != prevSyzkaller {
   468  				mgr.FailedSyzBuildBug = ""
   469  			}
   470  			return nil
   471  		})
   472  		if err != nil {
   473  			return nil, err
   474  		}
   475  	}
   476  	if len(req.Commits) != 0 || len(req.FixCommits) != 0 {
   477  		for i := range req.FixCommits {
   478  			// Reset hashes just to make sure,
   479  			// the build does not necessary come from the master repo, so we must not remember hashes.
   480  			req.FixCommits[i].Hash = ""
   481  		}
   482  		if err := addCommitsToBugs(c, ns, req.Manager, req.Commits, req.FixCommits); err != nil {
   483  			// We've already uploaded the build successfully and manager can use it.
   484  			// Moreover, addCommitsToBugs scans all bugs and can take long time.
   485  			// So just log the error.
   486  			log.Errorf(c, "failed to add commits to bugs: %v", err)
   487  		}
   488  	}
   489  	return nil, nil
   490  }
   491  
   492  func uploadBuild(c context.Context, now time.Time, ns string, req *dashapi.Build, typ BuildType) (
   493  	*Build, bool, error) {
   494  	newAssets := []Asset{}
   495  	for i, toAdd := range req.Assets {
   496  		newAsset, err := parseIncomingAsset(c, toAdd)
   497  		if err != nil {
   498  			return nil, false, fmt.Errorf("failed to parse asset #%d: %w", i, err)
   499  		}
   500  		newAssets = append(newAssets, newAsset)
   501  	}
   502  	if build, err := loadBuild(c, ns, req.ID); err == nil {
   503  		return build, false, nil
   504  	}
   505  	checkStrLen := func(str, name string, maxLen int) error {
   506  		if str == "" {
   507  			return fmt.Errorf("%v is empty", name)
   508  		}
   509  		if len(str) > maxLen {
   510  			return fmt.Errorf("%v is too long (%v)", name, len(str))
   511  		}
   512  		return nil
   513  	}
   514  	if err := checkStrLen(req.Manager, "Build.Manager", MaxStringLen); err != nil {
   515  		return nil, false, err
   516  	}
   517  	if err := checkStrLen(req.ID, "Build.ID", MaxStringLen); err != nil {
   518  		return nil, false, err
   519  	}
   520  	if err := checkStrLen(req.KernelRepo, "Build.KernelRepo", MaxStringLen); err != nil {
   521  		return nil, false, err
   522  	}
   523  	if len(req.KernelBranch) > MaxStringLen {
   524  		return nil, false, fmt.Errorf("Build.KernelBranch is too long (%v)", len(req.KernelBranch))
   525  	}
   526  	if err := checkStrLen(req.SyzkallerCommit, "Build.SyzkallerCommit", MaxStringLen); err != nil {
   527  		return nil, false, err
   528  	}
   529  	if len(req.CompilerID) > MaxStringLen {
   530  		return nil, false, fmt.Errorf("Build.CompilerID is too long (%v)", len(req.CompilerID))
   531  	}
   532  	if len(req.KernelCommit) > MaxStringLen {
   533  		return nil, false, fmt.Errorf("Build.KernelCommit is too long (%v)", len(req.KernelCommit))
   534  	}
   535  	configID, err := putText(c, ns, textKernelConfig, req.KernelConfig, true)
   536  	if err != nil {
   537  		return nil, false, err
   538  	}
   539  	build := &Build{
   540  		Namespace:           ns,
   541  		Manager:             req.Manager,
   542  		ID:                  req.ID,
   543  		Type:                typ,
   544  		Time:                now,
   545  		OS:                  req.OS,
   546  		Arch:                req.Arch,
   547  		VMArch:              req.VMArch,
   548  		SyzkallerCommit:     req.SyzkallerCommit,
   549  		SyzkallerCommitDate: req.SyzkallerCommitDate,
   550  		CompilerID:          req.CompilerID,
   551  		KernelRepo:          req.KernelRepo,
   552  		KernelBranch:        req.KernelBranch,
   553  		KernelCommit:        req.KernelCommit,
   554  		KernelCommitTitle:   req.KernelCommitTitle,
   555  		KernelCommitDate:    req.KernelCommitDate,
   556  		KernelConfig:        configID,
   557  		Assets:              newAssets,
   558  	}
   559  	if _, err := db.Put(c, buildKey(c, ns, req.ID), build); err != nil {
   560  		return nil, false, err
   561  	}
   562  	return build, true, nil
   563  }
   564  
   565  func addCommitsToBugs(c context.Context, ns, manager string, titles []string, fixCommits []dashapi.Commit) error {
   566  	presentCommits := make(map[string]bool)
   567  	bugFixedBy := make(map[string][]string)
   568  	for _, com := range titles {
   569  		presentCommits[com] = true
   570  	}
   571  	for _, com := range fixCommits {
   572  		presentCommits[com.Title] = true
   573  		for _, bugID := range com.BugIDs {
   574  			bugFixedBy[bugID] = append(bugFixedBy[bugID], com.Title)
   575  		}
   576  	}
   577  	managers, err := managerList(c, ns)
   578  	if err != nil {
   579  		return err
   580  	}
   581  	// Fetching all bugs in a namespace can be slow, and there is no way to filter only Open/Dup statuses.
   582  	// So we run a separate query for each status, this both avoids fetching unnecessary data
   583  	// and splits a long query into two (two smaller queries have lower chances of trigerring
   584  	// timeouts than one huge).
   585  	for _, status := range []int{BugStatusOpen, BugStatusDup} {
   586  		err := addCommitsToBugsInStatus(c, status, ns, manager, managers, presentCommits, bugFixedBy)
   587  		if err != nil {
   588  			return err
   589  		}
   590  	}
   591  	return nil
   592  }
   593  
   594  func addCommitsToBugsInStatus(c context.Context, status int, ns, manager string, managers []string,
   595  	presentCommits map[string]bool, bugFixedBy map[string][]string) error {
   596  	bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query {
   597  		return query.Filter("Namespace=", ns).
   598  			Filter("Status=", status)
   599  	})
   600  	if err != nil {
   601  		return err
   602  	}
   603  	for _, bug := range bugs {
   604  		var fixCommits []string
   605  		for i := range bug.Reporting {
   606  			fixCommits = append(fixCommits, bugFixedBy[bug.Reporting[i].ID]...)
   607  		}
   608  		sort.Strings(fixCommits)
   609  		if err := addCommitsToBug(c, bug, manager, managers, fixCommits, presentCommits); err != nil {
   610  			return err
   611  		}
   612  		if bug.Status == BugStatusDup {
   613  			canon, err := canonicalBug(c, bug)
   614  			if err != nil {
   615  				return err
   616  			}
   617  			if canon.Status == BugStatusOpen && len(bug.Commits) == 0 {
   618  				if err := addCommitsToBug(c, canon, manager, managers,
   619  					fixCommits, presentCommits); err != nil {
   620  					return err
   621  				}
   622  			}
   623  		}
   624  	}
   625  	return nil
   626  }
   627  
   628  func addCommitsToBug(c context.Context, bug *Bug, manager string, managers, fixCommits []string,
   629  	presentCommits map[string]bool) error {
   630  	if !bugNeedsCommitUpdate(c, bug, manager, fixCommits, presentCommits, true) {
   631  		return nil
   632  	}
   633  	now := timeNow(c)
   634  	bugKey := bug.key(c)
   635  	tx := func(c context.Context) error {
   636  		bug := new(Bug)
   637  		if err := db.Get(c, bugKey, bug); err != nil {
   638  			return fmt.Errorf("failed to get bug %v: %w", bugKey.StringID(), err)
   639  		}
   640  		if !bugNeedsCommitUpdate(c, bug, manager, fixCommits, presentCommits, false) {
   641  			return nil
   642  		}
   643  		if len(fixCommits) != 0 && !reflect.DeepEqual(bug.Commits, fixCommits) {
   644  			bug.updateCommits(fixCommits, now)
   645  		}
   646  		if manager != "" {
   647  			bug.PatchedOn = append(bug.PatchedOn, manager)
   648  			if bug.Status == BugStatusOpen {
   649  				fixed := true
   650  				for _, mgr := range managers {
   651  					if !stringInList(bug.PatchedOn, mgr) {
   652  						fixed = false
   653  						break
   654  					}
   655  				}
   656  				if fixed {
   657  					bug.Status = BugStatusFixed
   658  					bug.Closed = now
   659  				}
   660  			}
   661  		}
   662  		if _, err := db.Put(c, bugKey, bug); err != nil {
   663  			return fmt.Errorf("failed to put bug: %w", err)
   664  		}
   665  		return nil
   666  	}
   667  	return db.RunInTransaction(c, tx, nil)
   668  }
   669  
   670  func bugNeedsCommitUpdate(c context.Context, bug *Bug, manager string, fixCommits []string,
   671  	presentCommits map[string]bool, dolog bool) bool {
   672  	if len(fixCommits) != 0 && !reflect.DeepEqual(bug.Commits, fixCommits) {
   673  		if dolog {
   674  			log.Infof(c, "bug %q is fixed with %q", bug.Title, fixCommits)
   675  		}
   676  		return true
   677  	}
   678  	if len(bug.Commits) == 0 || manager == "" || stringInList(bug.PatchedOn, manager) {
   679  		return false
   680  	}
   681  	for _, com := range bug.Commits {
   682  		if !presentCommits[com] {
   683  			return false
   684  		}
   685  	}
   686  	return true
   687  }
   688  
   689  // Note: if you do not need the latest data, prefer CachedManagersList().
   690  func managerList(c context.Context, ns string) ([]string, error) {
   691  	var builds []*Build
   692  	_, err := db.NewQuery("Build").
   693  		Filter("Namespace=", ns).
   694  		Project("Manager").
   695  		Distinct().
   696  		GetAll(c, &builds)
   697  	if err != nil {
   698  		return nil, fmt.Errorf("failed to query builds: %w", err)
   699  	}
   700  	configManagers := getNsConfig(c, ns).Managers
   701  	var managers []string
   702  	for _, build := range builds {
   703  		if configManagers[build.Manager].Decommissioned {
   704  			continue
   705  		}
   706  		managers = append(managers, build.Manager)
   707  	}
   708  	return managers, nil
   709  }
   710  
   711  func apiReportBuildError(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
   712  	req := new(dashapi.BuildErrorReq)
   713  	if err := json.Unmarshal(payload, req); err != nil {
   714  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
   715  	}
   716  	now := timeNow(c)
   717  	build, _, err := uploadBuild(c, now, ns, &req.Build, BuildFailed)
   718  	if err != nil {
   719  		return nil, fmt.Errorf("failed to store build: %w", err)
   720  	}
   721  	req.Crash.BuildID = req.Build.ID
   722  	bug, err := reportCrash(c, build, &req.Crash)
   723  	if err != nil {
   724  		return nil, fmt.Errorf("failed to store crash: %w", err)
   725  	}
   726  	if err := updateManager(c, ns, req.Build.Manager, func(mgr *Manager, stats *ManagerStats) error {
   727  		log.Infof(c, "failed build on %v: kernel=%v", req.Build.Manager, req.Build.KernelCommit)
   728  		if req.Build.KernelCommit != "" {
   729  			mgr.FailedBuildBug = bug.keyHash(c)
   730  		} else {
   731  			mgr.FailedSyzBuildBug = bug.keyHash(c)
   732  		}
   733  		return nil
   734  	}); err != nil {
   735  		return nil, fmt.Errorf("failed to update manager: %w", err)
   736  	}
   737  	return nil, nil
   738  }
   739  
   740  const (
   741  	corruptedReportTitle  = "corrupted report"
   742  	suppressedReportTitle = "suppressed report"
   743  )
   744  
   745  func apiReportCrash(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
   746  	if stop, err := emergentlyStopped(c); err != nil || stop {
   747  		// The bot's operation was aborted. Don't accept new crash reports.
   748  		return &dashapi.ReportCrashResp{}, err
   749  	}
   750  	req := new(dashapi.Crash)
   751  	if err := json.Unmarshal(payload, req); err != nil {
   752  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
   753  	}
   754  	build, err := loadBuild(c, ns, req.BuildID)
   755  	if err != nil {
   756  		return nil, err
   757  	}
   758  	if !getNsConfig(c, ns).TransformCrash(build, req) {
   759  		return new(dashapi.ReportCrashResp), nil
   760  	}
   761  	var bug2 *Bug
   762  	if req.OriginalTitle != "" {
   763  		bug2, err = findExistingBugForCrash(c, ns, []string{req.OriginalTitle})
   764  		if err != nil {
   765  			return nil, fmt.Errorf("original bug query failed: %w", err)
   766  		}
   767  	}
   768  	bug, err := reportCrash(c, build, req)
   769  	if err != nil {
   770  		return nil, err
   771  	}
   772  	if bug2 != nil && bug2.Title != bug.Title && len(req.ReproLog) > 0 {
   773  		// During bug reproduction, we have diverted to another bug.
   774  		// Let's remember this.
   775  		err = saveFailedReproLog(c, bug2, build, req.ReproLog)
   776  		if err != nil {
   777  			return nil, fmt.Errorf("failed to save failed repro log: %w", err)
   778  		}
   779  	}
   780  	resp := &dashapi.ReportCrashResp{
   781  		NeedRepro: needRepro(c, bug),
   782  	}
   783  	return resp, nil
   784  }
   785  
   786  // nolint: gocyclo
   787  func reportCrash(c context.Context, build *Build, req *dashapi.Crash) (*Bug, error) {
   788  	assets, err := parseCrashAssets(c, req)
   789  	if err != nil {
   790  		return nil, err
   791  	}
   792  	req.Title = canonicalizeCrashTitle(req.Title, req.Corrupted, req.Suppressed)
   793  	if req.Corrupted || req.Suppressed {
   794  		req.AltTitles = []string{req.Title}
   795  	} else {
   796  		for i, t := range req.AltTitles {
   797  			req.AltTitles[i] = normalizeCrashTitle(t)
   798  		}
   799  		req.AltTitles = mergeStringList([]string{req.Title}, req.AltTitles) // dedup
   800  	}
   801  	req.Maintainers = email.MergeEmailLists(req.Maintainers)
   802  
   803  	ns := build.Namespace
   804  	bug, err := findBugForCrash(c, ns, req.AltTitles)
   805  	if err != nil {
   806  		return nil, fmt.Errorf("failed to find bug for the crash: %w", err)
   807  	}
   808  	if bug == nil {
   809  		bug, err = createBugForCrash(c, ns, req)
   810  		if err != nil {
   811  			return nil, fmt.Errorf("failed to create a bug: %w", err)
   812  		}
   813  	}
   814  
   815  	bugKey := bug.key(c)
   816  	now := timeNow(c)
   817  	reproLevel := ReproLevelNone
   818  	if len(req.ReproC) != 0 {
   819  		reproLevel = ReproLevelC
   820  	} else if len(req.ReproSyz) != 0 {
   821  		reproLevel = ReproLevelSyz
   822  	}
   823  	save := reproLevel != ReproLevelNone ||
   824  		bug.NumCrashes < int64(maxCrashes()) ||
   825  		now.Sub(bug.LastSavedCrash) > time.Hour ||
   826  		bug.NumCrashes%20 == 0 ||
   827  		!stringInList(bug.MergedTitles, req.Title)
   828  	if save {
   829  		if err := saveCrash(c, ns, req, bug, bugKey, build, assets); err != nil {
   830  			return nil, fmt.Errorf("failed to save the crash: %w", err)
   831  		}
   832  	} else {
   833  		log.Infof(c, "not saving crash for %q", bug.Title)
   834  	}
   835  
   836  	newSubsystems := []*subsystem.Subsystem{}
   837  	// Recalculate subsystems on the first saved crash and on the first saved repro,
   838  	// unless a user has already manually specified them.
   839  	calculateSubsystems := save &&
   840  		!bug.hasUserSubsystems() &&
   841  		(bug.NumCrashes == 0 ||
   842  			bug.ReproLevel == ReproLevelNone && reproLevel != ReproLevelNone)
   843  	if calculateSubsystems {
   844  		newSubsystems, err = inferSubsystems(c, bug, bugKey, &debugtracer.NullTracer{})
   845  		if err != nil {
   846  			log.Errorf(c, "%q: failed to extract subsystems: %s", bug.Title, err)
   847  			return nil, err
   848  		}
   849  	}
   850  
   851  	tx := func(c context.Context) error {
   852  		bug = new(Bug)
   853  		if err := db.Get(c, bugKey, bug); err != nil {
   854  			return fmt.Errorf("failed to get bug: %w", err)
   855  		}
   856  		bug.LastTime = now
   857  		if save {
   858  			bug.LastSavedCrash = now
   859  		}
   860  		if reproLevel != ReproLevelNone {
   861  			bug.NumRepro++
   862  			bug.LastReproTime = now
   863  		}
   864  		if bug.ReproLevel < reproLevel {
   865  			bug.ReproLevel = reproLevel
   866  		}
   867  		if bug.HeadReproLevel < reproLevel {
   868  			bug.HeadReproLevel = reproLevel
   869  		}
   870  		if len(req.Report) != 0 {
   871  			bug.HasReport = true
   872  		}
   873  		if calculateSubsystems {
   874  			bug.SetAutoSubsystems(c, newSubsystems, now, getNsConfig(c, ns).Subsystems.Revision)
   875  		}
   876  		bug.increaseCrashStats(now)
   877  		bug.HappenedOn = mergeString(bug.HappenedOn, build.Manager)
   878  		// Migration of older entities (for new bugs Title is always in MergedTitles).
   879  		bug.MergedTitles = mergeString(bug.MergedTitles, bug.Title)
   880  		bug.MergedTitles = mergeString(bug.MergedTitles, req.Title)
   881  		bug.AltTitles = mergeStringList(bug.AltTitles, req.AltTitles)
   882  		if _, err = db.Put(c, bugKey, bug); err != nil {
   883  			return fmt.Errorf("failed to put bug: %w", err)
   884  		}
   885  		return nil
   886  	}
   887  	if err := db.RunInTransaction(c, tx, &db.TransactionOptions{XG: true}); err != nil {
   888  		return nil, fmt.Errorf("bug updating failed: %w", err)
   889  	}
   890  	if save {
   891  		purgeOldCrashes(c, bug, bugKey)
   892  	}
   893  	return bug, nil
   894  }
   895  
   896  func parseCrashAssets(c context.Context, req *dashapi.Crash) ([]Asset, error) {
   897  	assets := []Asset{}
   898  	for i, toAdd := range req.Assets {
   899  		newAsset, err := parseIncomingAsset(c, toAdd)
   900  		if err != nil {
   901  			return nil, fmt.Errorf("failed to parse asset #%d: %w", i, err)
   902  		}
   903  		assets = append(assets, newAsset)
   904  	}
   905  	return assets, nil
   906  }
   907  
   908  func (crash *Crash) UpdateReportingPriority(c context.Context, build *Build, bug *Bug) {
   909  	prio := int64(kernelRepoInfo(c, build).ReportingPriority) * 1e6
   910  	divReproPrio := int64(1)
   911  	if crash.ReproIsRevoked {
   912  		divReproPrio = 10
   913  	}
   914  	if crash.ReproC > 0 {
   915  		prio += 4e12 / divReproPrio
   916  	} else if crash.ReproSyz > 0 {
   917  		prio += 2e12 / divReproPrio
   918  	}
   919  	if crash.Title == bug.Title {
   920  		prio += 1e8 // prefer reporting crash that matches bug title
   921  	}
   922  	managerPrio := 0
   923  	if _, mgrConfig := activeManager(c, crash.Manager, bug.Namespace); mgrConfig != nil {
   924  		managerPrio = mgrConfig.Priority
   925  	}
   926  	prio += int64((managerPrio - MinManagerPriority) * 1e5)
   927  	if build.Arch == targets.AMD64 {
   928  		prio += 1e3
   929  	}
   930  	crash.ReportLen = prio
   931  }
   932  
   933  func saveCrash(c context.Context, ns string, req *dashapi.Crash, bug *Bug, bugKey *db.Key,
   934  	build *Build, assets []Asset) error {
   935  	crash := &Crash{
   936  		Title:   req.Title,
   937  		Manager: build.Manager,
   938  		BuildID: req.BuildID,
   939  		Time:    timeNow(c),
   940  		Maintainers: email.MergeEmailLists(req.Maintainers,
   941  			GetEmails(req.Recipients, dashapi.To),
   942  			GetEmails(req.Recipients, dashapi.Cc)),
   943  		ReproOpts: req.ReproOpts,
   944  		Flags:     int64(req.Flags),
   945  		Assets:    assets,
   946  		ReportElements: CrashReportElements{
   947  			GuiltyFiles: req.GuiltyFiles,
   948  		},
   949  	}
   950  	var err error
   951  	if crash.Log, err = putText(c, ns, textCrashLog, req.Log, false); err != nil {
   952  		return err
   953  	}
   954  	if crash.Report, err = putText(c, ns, textCrashReport, req.Report, false); err != nil {
   955  		return err
   956  	}
   957  	if crash.ReproSyz, err = putText(c, ns, textReproSyz, req.ReproSyz, false); err != nil {
   958  		return err
   959  	}
   960  	if crash.ReproC, err = putText(c, ns, textReproC, req.ReproC, false); err != nil {
   961  		return err
   962  	}
   963  	if crash.MachineInfo, err = putText(c, ns, textMachineInfo, req.MachineInfo, true); err != nil {
   964  		return err
   965  	}
   966  	crash.UpdateReportingPriority(c, build, bug)
   967  	crashKey := db.NewIncompleteKey(c, "Crash", bugKey)
   968  	if _, err = db.Put(c, crashKey, crash); err != nil {
   969  		return fmt.Errorf("failed to put crash: %w", err)
   970  	}
   971  	return nil
   972  }
   973  
   974  func purgeOldCrashes(c context.Context, bug *Bug, bugKey *db.Key) {
   975  	const purgeEvery = 10
   976  	if bug.NumCrashes <= int64(2*maxCrashes()) || (bug.NumCrashes-1)%purgeEvery != 0 {
   977  		return
   978  	}
   979  	var crashes []*Crash
   980  	keys, err := db.NewQuery("Crash").
   981  		Ancestor(bugKey).
   982  		Filter("Reported=", time.Time{}).
   983  		GetAll(c, &crashes)
   984  	if err != nil {
   985  		log.Errorf(c, "failed to fetch purge crashes: %v", err)
   986  		return
   987  	}
   988  	keyMap := make(map[*Crash]*db.Key)
   989  	for i, crash := range crashes {
   990  		keyMap[crash] = keys[i]
   991  	}
   992  	// Newest first.
   993  	sort.Slice(crashes, func(i, j int) bool {
   994  		return crashes[i].Time.After(crashes[j].Time)
   995  	})
   996  	var toDelete []*db.Key
   997  	latestOnManager := make(map[string]bool)
   998  	uniqueTitle := make(map[string]bool)
   999  	deleted, reproCount, noreproCount := 0, 0, 0
  1000  	for _, crash := range crashes {
  1001  		if !crash.Reported.IsZero() {
  1002  			log.Errorf(c, "purging reported crash?")
  1003  			continue
  1004  		}
  1005  		// Preserve latest crash on each manager.
  1006  		if !latestOnManager[crash.Manager] {
  1007  			latestOnManager[crash.Manager] = true
  1008  			continue
  1009  		}
  1010  		// Preserve at least one crash with each title.
  1011  		if !uniqueTitle[crash.Title] {
  1012  			uniqueTitle[crash.Title] = true
  1013  			continue
  1014  		}
  1015  		// Preserve maxCrashes latest crashes with repro and without repro.
  1016  		count := &noreproCount
  1017  		if crash.ReproSyz != 0 || crash.ReproC != 0 {
  1018  			count = &reproCount
  1019  		}
  1020  		if *count < maxCrashes() {
  1021  			*count++
  1022  			continue
  1023  		}
  1024  		toDelete = append(toDelete, keyMap[crash])
  1025  		if crash.Log != 0 {
  1026  			toDelete = append(toDelete, db.NewKey(c, textCrashLog, "", crash.Log, nil))
  1027  		}
  1028  		if crash.Report != 0 {
  1029  			toDelete = append(toDelete, db.NewKey(c, textCrashReport, "", crash.Report, nil))
  1030  		}
  1031  		if crash.ReproSyz != 0 {
  1032  			toDelete = append(toDelete, db.NewKey(c, textReproSyz, "", crash.ReproSyz, nil))
  1033  		}
  1034  		if crash.ReproC != 0 {
  1035  			toDelete = append(toDelete, db.NewKey(c, textReproC, "", crash.ReproC, nil))
  1036  		}
  1037  		deleted++
  1038  		if deleted == 2*purgeEvery {
  1039  			break
  1040  		}
  1041  	}
  1042  	if len(toDelete) == 0 {
  1043  		return
  1044  	}
  1045  	if err := db.DeleteMulti(c, toDelete); err != nil {
  1046  		log.Errorf(c, "failed to delete old crashes: %v", err)
  1047  		return
  1048  	}
  1049  	log.Infof(c, "deleted %v crashes for bug %q", deleted, bug.Title)
  1050  }
  1051  
  1052  func apiReportFailedRepro(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
  1053  	req := new(dashapi.CrashID)
  1054  	if err := json.Unmarshal(payload, req); err != nil {
  1055  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
  1056  	}
  1057  	req.Title = canonicalizeCrashTitle(req.Title, req.Corrupted, req.Suppressed)
  1058  
  1059  	bug, err := findExistingBugForCrash(c, ns, []string{req.Title})
  1060  	if err != nil {
  1061  		return nil, err
  1062  	}
  1063  	if bug == nil {
  1064  		return nil, fmt.Errorf("%v: can't find bug for crash %q", ns, req.Title)
  1065  	}
  1066  	build, err := loadBuild(c, ns, req.BuildID)
  1067  	if err != nil {
  1068  		return nil, err
  1069  	}
  1070  	return nil, saveFailedReproLog(c, bug, build, req.ReproLog)
  1071  }
  1072  
  1073  func saveFailedReproLog(c context.Context, bug *Bug, build *Build, log []byte) error {
  1074  	now := timeNow(c)
  1075  	bugKey := bug.key(c)
  1076  	tx := func(c context.Context) error {
  1077  		bug := new(Bug)
  1078  		if err := db.Get(c, bugKey, bug); err != nil {
  1079  			return fmt.Errorf("failed to get bug: %w", err)
  1080  		}
  1081  		bug.NumRepro++
  1082  		bug.LastReproTime = now
  1083  		if len(log) > 0 {
  1084  			err := saveReproAttempt(c, bug, build, log)
  1085  			if err != nil {
  1086  				return fmt.Errorf("failed to save repro log: %w", err)
  1087  			}
  1088  		}
  1089  		if _, err := db.Put(c, bugKey, bug); err != nil {
  1090  			return fmt.Errorf("failed to put bug: %w", err)
  1091  		}
  1092  		return nil
  1093  	}
  1094  	return db.RunInTransaction(c, tx, &db.TransactionOptions{
  1095  		XG:       true,
  1096  		Attempts: 30,
  1097  	})
  1098  }
  1099  
  1100  const maxReproLogs = 5
  1101  
  1102  func saveReproAttempt(c context.Context, bug *Bug, build *Build, log []byte) error {
  1103  	var deleteKeys []*db.Key
  1104  	for len(bug.ReproAttempts)+1 > maxReproLogs {
  1105  		deleteKeys = append(deleteKeys,
  1106  			db.NewKey(c, textReproLog, "", bug.ReproAttempts[0].Log, nil))
  1107  		bug.ReproAttempts = bug.ReproAttempts[1:]
  1108  	}
  1109  	entry := BugReproAttempt{
  1110  		Time:    timeNow(c),
  1111  		Manager: build.Manager,
  1112  	}
  1113  	var err error
  1114  	if entry.Log, err = putText(c, bug.Namespace, textReproLog, log, false); err != nil {
  1115  		return err
  1116  	}
  1117  	if len(deleteKeys) > 0 {
  1118  		return db.DeleteMulti(c, deleteKeys)
  1119  	}
  1120  	bug.ReproAttempts = append(bug.ReproAttempts, entry)
  1121  	return nil
  1122  }
  1123  
  1124  func apiNeedRepro(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
  1125  	req := new(dashapi.CrashID)
  1126  	if err := json.Unmarshal(payload, req); err != nil {
  1127  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
  1128  	}
  1129  	if req.Corrupted {
  1130  		resp := &dashapi.NeedReproResp{
  1131  			NeedRepro: false,
  1132  		}
  1133  		return resp, nil
  1134  	}
  1135  	req.Title = canonicalizeCrashTitle(req.Title, req.Corrupted, req.Suppressed)
  1136  
  1137  	bug, err := findExistingBugForCrash(c, ns, []string{req.Title})
  1138  	if err != nil {
  1139  		return nil, err
  1140  	}
  1141  	if bug == nil {
  1142  		if req.MayBeMissing {
  1143  			// Manager does not send leak reports w/o repro to dashboard, we want to reproduce them.
  1144  			resp := &dashapi.NeedReproResp{
  1145  				NeedRepro: true,
  1146  			}
  1147  			return resp, nil
  1148  		}
  1149  		return nil, fmt.Errorf("%v: can't find bug for crash %q", ns, req.Title)
  1150  	}
  1151  	resp := &dashapi.NeedReproResp{
  1152  		NeedRepro: needRepro(c, bug),
  1153  	}
  1154  	return resp, nil
  1155  }
  1156  
  1157  func canonicalizeCrashTitle(title string, corrupted, suppressed bool) string {
  1158  	if corrupted {
  1159  		// The report is corrupted and the title is most likely invalid.
  1160  		// Such reports are usually unactionable and are discarded.
  1161  		// Collect them into a single bin.
  1162  		return corruptedReportTitle
  1163  	}
  1164  	if suppressed {
  1165  		// Collect all of them into a single bucket so that it's possible to control and assess them,
  1166  		// e.g. if there are some spikes in suppressed reports.
  1167  		return suppressedReportTitle
  1168  	}
  1169  	return normalizeCrashTitle(title)
  1170  }
  1171  
  1172  func normalizeCrashTitle(title string) string {
  1173  	return strings.TrimSpace(limitLength(title, maxTextLen))
  1174  }
  1175  
  1176  func apiManagerStats(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
  1177  	req := new(dashapi.ManagerStatsReq)
  1178  	if err := json.Unmarshal(payload, req); err != nil {
  1179  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
  1180  	}
  1181  	now := timeNow(c)
  1182  	err := updateManager(c, ns, req.Name, func(mgr *Manager, stats *ManagerStats) error {
  1183  		mgr.Link = req.Addr
  1184  		mgr.LastAlive = now
  1185  		mgr.CurrentUpTime = req.UpTime
  1186  		if cur := int64(req.Corpus); cur > stats.MaxCorpus {
  1187  			stats.MaxCorpus = cur
  1188  		}
  1189  		if cur := int64(req.PCs); cur > stats.MaxPCs {
  1190  			stats.MaxPCs = cur
  1191  		}
  1192  		if cur := int64(req.Cover); cur > stats.MaxCover {
  1193  			stats.MaxCover = cur
  1194  		}
  1195  		if cur := int64(req.CrashTypes); cur > stats.CrashTypes {
  1196  			stats.CrashTypes = cur
  1197  		}
  1198  		stats.TotalFuzzingTime += req.FuzzingTime
  1199  		stats.TotalCrashes += int64(req.Crashes)
  1200  		stats.SuppressedCrashes += int64(req.SuppressedCrashes)
  1201  		stats.TotalExecs += int64(req.Execs)
  1202  		if cur := int64(req.TriagedCoverage); cur > stats.TriagedCoverage {
  1203  			stats.TriagedCoverage = cur
  1204  		}
  1205  		if cur := int64(req.TriagedPCs); cur > stats.TriagedPCs {
  1206  			stats.TriagedPCs = cur
  1207  		}
  1208  		return nil
  1209  	})
  1210  	return nil, err
  1211  }
  1212  
  1213  func apiBugList(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
  1214  	keys, err := db.NewQuery("Bug").
  1215  		Filter("Namespace=", ns).
  1216  		KeysOnly().
  1217  		GetAll(c, nil)
  1218  	if err != nil {
  1219  		return nil, fmt.Errorf("failed to query bugs: %w", err)
  1220  	}
  1221  	resp := &dashapi.BugListResp{}
  1222  	for _, key := range keys {
  1223  		resp.List = append(resp.List, key.StringID())
  1224  	}
  1225  	return resp, nil
  1226  }
  1227  
  1228  func apiUpdateReport(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
  1229  	req := new(dashapi.UpdateReportReq)
  1230  	if err := json.Unmarshal(payload, req); err != nil {
  1231  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
  1232  	}
  1233  	bug := new(Bug)
  1234  	bugKey := db.NewKey(c, "Bug", req.BugID, 0, nil)
  1235  	if err := db.Get(c, bugKey, bug); err != nil {
  1236  		return nil, fmt.Errorf("failed to get bug: %w", err)
  1237  	}
  1238  	if bug.Namespace != ns {
  1239  		return nil, fmt.Errorf("no such bug")
  1240  	}
  1241  	tx := func(c context.Context) error {
  1242  		crash := new(Crash)
  1243  		crashKey := db.NewKey(c, "Crash", "", req.CrashID, bugKey)
  1244  		if err := db.Get(c, crashKey, crash); err != nil {
  1245  			return fmt.Errorf("failed to query the crash: %w", err)
  1246  		}
  1247  		if req.GuiltyFiles != nil {
  1248  			crash.ReportElements.GuiltyFiles = *req.GuiltyFiles
  1249  		}
  1250  		if _, err := db.Put(c, crashKey, crash); err != nil {
  1251  			return fmt.Errorf("failed to put reported crash: %w", err)
  1252  		}
  1253  		return nil
  1254  	}
  1255  	return nil, db.RunInTransaction(c, tx, &db.TransactionOptions{Attempts: 5})
  1256  }
  1257  
  1258  func apiLoadBug(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
  1259  	req := new(dashapi.LoadBugReq)
  1260  	if err := json.Unmarshal(payload, req); err != nil {
  1261  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
  1262  	}
  1263  	bug := new(Bug)
  1264  	bugKey := db.NewKey(c, "Bug", req.ID, 0, nil)
  1265  	if err := db.Get(c, bugKey, bug); err != nil {
  1266  		return nil, fmt.Errorf("failed to get bug: %w", err)
  1267  	}
  1268  	if bug.Namespace != ns {
  1269  		return nil, fmt.Errorf("no such bug")
  1270  	}
  1271  	return loadBugReport(c, bug)
  1272  }
  1273  
  1274  func apiLoadFullBug(c context.Context, r *http.Request, payload []byte) (interface{}, error) {
  1275  	req := new(dashapi.LoadFullBugReq)
  1276  	if err := json.Unmarshal(payload, req); err != nil {
  1277  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
  1278  	}
  1279  	bug, bugKey, err := findBugByReportingID(c, req.BugID)
  1280  	if err != nil {
  1281  		return nil, fmt.Errorf("failed to find the bug: %w", err)
  1282  	}
  1283  	bugReporting, _ := bugReportingByID(bug, req.BugID)
  1284  	if bugReporting == nil {
  1285  		return nil, fmt.Errorf("failed to find the bug reporting: %w", err)
  1286  	}
  1287  	return loadFullBugInfo(c, bug, bugKey, bugReporting)
  1288  }
  1289  
  1290  func loadBugReport(c context.Context, bug *Bug) (*dashapi.BugReport, error) {
  1291  	crash, crashKey, err := findCrashForBug(c, bug)
  1292  	if err != nil {
  1293  		return nil, err
  1294  	}
  1295  	// Create report for the last reporting so that it's stable and ExtID does not change over time.
  1296  	bugReporting := &bug.Reporting[len(bug.Reporting)-1]
  1297  	reporting := getNsConfig(c, bug.Namespace).ReportingByName(bugReporting.Name)
  1298  	if reporting == nil {
  1299  		return nil, fmt.Errorf("reporting %v is missing in config", bugReporting.Name)
  1300  	}
  1301  	return createBugReport(c, bug, crash, crashKey, bugReporting, reporting)
  1302  }
  1303  
  1304  func apiAddBuildAssets(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
  1305  	req := new(dashapi.AddBuildAssetsReq)
  1306  	if err := json.Unmarshal(payload, req); err != nil {
  1307  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
  1308  	}
  1309  	assets := []Asset{}
  1310  	for i, toAdd := range req.Assets {
  1311  		asset, err := parseIncomingAsset(c, toAdd)
  1312  		if err != nil {
  1313  			return nil, fmt.Errorf("failed to parse asset #%d: %w", i, err)
  1314  		}
  1315  		assets = append(assets, asset)
  1316  	}
  1317  	_, err := appendBuildAssets(c, ns, req.BuildID, assets)
  1318  	if err != nil {
  1319  		return nil, err
  1320  	}
  1321  	return nil, nil
  1322  }
  1323  
  1324  func parseIncomingAsset(c context.Context, newAsset dashapi.NewAsset) (Asset, error) {
  1325  	typeInfo := asset.GetTypeDescription(newAsset.Type)
  1326  	if typeInfo == nil {
  1327  		return Asset{}, fmt.Errorf("unknown asset type")
  1328  	}
  1329  	_, err := url.ParseRequestURI(newAsset.DownloadURL)
  1330  	if err != nil {
  1331  		return Asset{}, fmt.Errorf("invalid URL: %w", err)
  1332  	}
  1333  	return Asset{
  1334  		Type:        newAsset.Type,
  1335  		DownloadURL: newAsset.DownloadURL,
  1336  		CreateDate:  timeNow(c),
  1337  	}, nil
  1338  }
  1339  
  1340  func apiNeededAssetsList(c context.Context, r *http.Request, payload []byte) (interface{}, error) {
  1341  	return queryNeededAssets(c)
  1342  }
  1343  
  1344  func findExistingBugForCrash(c context.Context, ns string, titles []string) (*Bug, error) {
  1345  	// First, try to find an existing bug that we already used to report this crash title.
  1346  	var bugs []*Bug
  1347  	_, err := db.NewQuery("Bug").
  1348  		Filter("Namespace=", ns).
  1349  		Filter("MergedTitles=", titles[0]).
  1350  		GetAll(c, &bugs)
  1351  	if err != nil {
  1352  		return nil, fmt.Errorf("failed to query bugs: %w", err)
  1353  	}
  1354  	// We can find bugs with different bug.Title and uncomparable bug.Seq's.
  1355  	// But there should be only one active bug for each crash title,
  1356  	// so if we sort by Seq, the first active bug is our target bug.
  1357  	sort.Slice(bugs, func(i, j int) bool {
  1358  		return bugs[i].Seq > bugs[j].Seq
  1359  	})
  1360  	for _, bug := range bugs {
  1361  		if active, err := isActiveBug(c, bug); err != nil {
  1362  			return nil, err
  1363  		} else if active {
  1364  			return bug, nil
  1365  		}
  1366  	}
  1367  	// This is required for incremental migration.
  1368  	// Older bugs don't have MergedTitles, so we need to check Title as well
  1369  	// (reportCrash will set MergedTitles later).
  1370  	for _, title := range titles {
  1371  		_, err = db.NewQuery("Bug").
  1372  			Filter("Namespace=", ns).
  1373  			Filter("Title=", title).
  1374  			Order("-Seq").
  1375  			Limit(1).
  1376  			GetAll(c, &bugs)
  1377  		if err != nil {
  1378  			return nil, fmt.Errorf("failed to query bugs: %w", err)
  1379  		}
  1380  		if len(bugs) != 0 {
  1381  			bug := bugs[0]
  1382  			if active, err := isActiveBug(c, bug); err != nil {
  1383  				return nil, err
  1384  			} else if active {
  1385  				return bug, nil
  1386  			}
  1387  		}
  1388  	}
  1389  	return nil, nil
  1390  }
  1391  
  1392  func findBugForCrash(c context.Context, ns string, titles []string) (*Bug, error) {
  1393  	// First, try to find an existing bug that we already used to report this crash title.
  1394  	bug, err := findExistingBugForCrash(c, ns, titles)
  1395  	if bug != nil || err != nil {
  1396  		return bug, err
  1397  	}
  1398  	// If there is no active bug for this crash title, try to find an existing candidate based on AltTitles.
  1399  	var bugs []*Bug
  1400  	for _, title := range titles {
  1401  		var bugs1 []*Bug
  1402  		_, err := db.NewQuery("Bug").
  1403  			Filter("Namespace=", ns).
  1404  			Filter("AltTitles=", title).
  1405  			GetAll(c, &bugs1)
  1406  		if err != nil {
  1407  			return nil, fmt.Errorf("failed to query bugs: %w", err)
  1408  		}
  1409  		bugs = append(bugs, bugs1...)
  1410  	}
  1411  	// Sort to get determinism and skip inactive bugs.
  1412  	sort.Slice(bugs, func(i, j int) bool {
  1413  		if bugs[i].Title != bugs[j].Title {
  1414  			return bugs[i].Title < bugs[j].Title
  1415  		}
  1416  		return bugs[i].Seq > bugs[j].Seq
  1417  	})
  1418  	var best *Bug
  1419  	bestPrio := 0
  1420  	for i, bug := range bugs {
  1421  		if i != 0 && bugs[i-1].Title == bug.Title {
  1422  			continue // skip inactive bugs
  1423  		}
  1424  		if active, err := isActiveBug(c, bug); err != nil {
  1425  			return nil, err
  1426  		} else if !active {
  1427  			continue
  1428  		}
  1429  		// Generally we should have few candidates (one in most cases).
  1430  		// However, it's possible if e.g. we first get a data race between A<->B,
  1431  		// then a race between C<->D and now we handle a race between B<->D,
  1432  		// it can be merged into any of the previous ones.
  1433  		// The priority here is very basic. The only known case we want to handle is bug title renaming
  1434  		// where we have an active bug with title A, but then A is renamed to B and A is attached as alt title.
  1435  		// In such case we want to merge the new crash into the old one. However, it's also unlikely that
  1436  		// in this case we have any other candidates.
  1437  		// Overall selection algorithm can be arbitrary changed because the selection for existing crashes
  1438  		// is fixed with bug.MergedTitles (stable for existing bugs/crashes).
  1439  		prio := 0
  1440  		if stringInList(titles[1:], bug.Title) {
  1441  			prio = 2
  1442  		} else if stringInList(bug.AltTitles[1:], titles[0]) {
  1443  			prio = 1
  1444  		}
  1445  		if best == nil || prio > bestPrio {
  1446  			best, bestPrio = bug, prio
  1447  		}
  1448  	}
  1449  	return best, nil
  1450  }
  1451  
  1452  func createBugForCrash(c context.Context, ns string, req *dashapi.Crash) (*Bug, error) {
  1453  	var bug *Bug
  1454  	now := timeNow(c)
  1455  	tx := func(c context.Context) error {
  1456  		for seq := int64(0); ; seq++ {
  1457  			bug = new(Bug)
  1458  			bugHash := bugKeyHash(c, ns, req.Title, seq)
  1459  			bugKey := db.NewKey(c, "Bug", bugHash, 0, nil)
  1460  			if err := db.Get(c, bugKey, bug); err != nil {
  1461  				if err != db.ErrNoSuchEntity {
  1462  					return fmt.Errorf("failed to get bug: %w", err)
  1463  				}
  1464  				bug = &Bug{
  1465  					Namespace:      ns,
  1466  					Seq:            seq,
  1467  					Title:          req.Title,
  1468  					MergedTitles:   []string{req.Title},
  1469  					AltTitles:      req.AltTitles,
  1470  					Status:         BugStatusOpen,
  1471  					NumCrashes:     0,
  1472  					NumRepro:       0,
  1473  					ReproLevel:     ReproLevelNone,
  1474  					HasReport:      false,
  1475  					FirstTime:      now,
  1476  					LastTime:       now,
  1477  					SubsystemsTime: now,
  1478  				}
  1479  				err = bug.updateReportings(c, getNsConfig(c, ns), now)
  1480  				if err != nil {
  1481  					return err
  1482  				}
  1483  				if _, err = db.Put(c, bugKey, bug); err != nil {
  1484  					return fmt.Errorf("failed to put new bug: %w", err)
  1485  				}
  1486  				return nil
  1487  			}
  1488  			canon, err := canonicalBug(c, bug)
  1489  			if err != nil {
  1490  				return err
  1491  			}
  1492  			if canon.Status != BugStatusOpen {
  1493  				continue
  1494  			}
  1495  			return nil
  1496  		}
  1497  	}
  1498  	if err := db.RunInTransaction(c, tx, &db.TransactionOptions{
  1499  		XG:       true,
  1500  		Attempts: 30,
  1501  	}); err != nil {
  1502  		return nil, err
  1503  	}
  1504  	return bug, nil
  1505  }
  1506  
  1507  func isActiveBug(c context.Context, bug *Bug) (bool, error) {
  1508  	if bug == nil {
  1509  		return false, nil
  1510  	}
  1511  	canon, err := canonicalBug(c, bug)
  1512  	if err != nil {
  1513  		return false, err
  1514  	}
  1515  	return canon.Status == BugStatusOpen, nil
  1516  }
  1517  
  1518  func needRepro(c context.Context, bug *Bug) bool {
  1519  	if !needReproForBug(c, bug) {
  1520  		return false
  1521  	}
  1522  	canon, err := canonicalBug(c, bug)
  1523  	if err != nil {
  1524  		log.Errorf(c, "failed to get canonical bug: %v", err)
  1525  		return false
  1526  	}
  1527  	return needReproForBug(c, canon)
  1528  }
  1529  
  1530  var syzErrorTitleRe = regexp.MustCompile(`^SYZFAIL:|^SYZFATAL:`)
  1531  
  1532  func needReproForBug(c context.Context, bug *Bug) bool {
  1533  	// We already have fixing commits.
  1534  	if len(bug.Commits) > 0 {
  1535  		return false
  1536  	}
  1537  	if bug.Title == corruptedReportTitle ||
  1538  		bug.Title == suppressedReportTitle {
  1539  		return false
  1540  	}
  1541  	if !getNsConfig(c, bug.Namespace).NeedRepro(bug) {
  1542  		return false
  1543  	}
  1544  	bestReproLevel := ReproLevelC
  1545  	// For some bugs there's anyway no chance to find a C repro.
  1546  	if syzErrorTitleRe.MatchString(bug.Title) {
  1547  		bestReproLevel = ReproLevelSyz
  1548  	}
  1549  	if bug.HeadReproLevel < bestReproLevel {
  1550  		// We have not found a best-level repro yet, try until we do.
  1551  		return bug.NumRepro < maxReproPerBug || timeSince(c, bug.LastReproTime) >= reproRetryPeriod
  1552  	}
  1553  	// When the best repro is already found, still do a repro attempt once in a while.
  1554  	return timeSince(c, bug.LastReproTime) >= reproStalePeriod
  1555  }
  1556  
  1557  func putText(c context.Context, ns, tag string, data []byte, dedup bool) (int64, error) {
  1558  	if ns == "" {
  1559  		return 0, fmt.Errorf("putting text outside of namespace")
  1560  	}
  1561  	if len(data) == 0 {
  1562  		return 0, nil
  1563  	}
  1564  	const (
  1565  		// Kernel crash log is capped at ~1MB, but vm.Diagnose can add more.
  1566  		// These text files usually compress very well.
  1567  		maxTextLen       = 10 << 20
  1568  		maxCompressedLen = 1000 << 10 // datastore entity limit is 1MB
  1569  	)
  1570  	if len(data) > maxTextLen {
  1571  		data = data[:maxTextLen]
  1572  	}
  1573  	b := new(bytes.Buffer)
  1574  	for {
  1575  		z, _ := gzip.NewWriterLevel(b, gzip.BestCompression)
  1576  		z.Write(data)
  1577  		z.Close()
  1578  		if len(b.Bytes()) < maxCompressedLen {
  1579  			break
  1580  		}
  1581  		data = data[:len(data)/10*9]
  1582  		b.Reset()
  1583  	}
  1584  	var key *db.Key
  1585  	if dedup {
  1586  		h := hash.Hash([]byte(ns), b.Bytes())
  1587  		key = db.NewKey(c, tag, "", h.Truncate64(), nil)
  1588  	} else {
  1589  		key = db.NewIncompleteKey(c, tag, nil)
  1590  	}
  1591  	text := &Text{
  1592  		Namespace: ns,
  1593  		Text:      b.Bytes(),
  1594  	}
  1595  	key, err := db.Put(c, key, text)
  1596  	if err != nil {
  1597  		return 0, err
  1598  	}
  1599  	return key.IntID(), nil
  1600  }
  1601  
  1602  func getText(c context.Context, tag string, id int64) ([]byte, string, error) {
  1603  	if id == 0 {
  1604  		return nil, "", nil
  1605  	}
  1606  	text := new(Text)
  1607  	if err := db.Get(c, db.NewKey(c, tag, "", id, nil), text); err != nil {
  1608  		return nil, "", fmt.Errorf("failed to read text %v: %w", tag, err)
  1609  	}
  1610  	d, err := gzip.NewReader(bytes.NewBuffer(text.Text))
  1611  	if err != nil {
  1612  		return nil, "", fmt.Errorf("failed to read text %v: %w", tag, err)
  1613  	}
  1614  	data, err := io.ReadAll(d)
  1615  	if err != nil {
  1616  		return nil, "", fmt.Errorf("failed to read text %v: %w", tag, err)
  1617  	}
  1618  	return data, text.Namespace, nil
  1619  }
  1620  
  1621  // limitLength essentially does return s[:max],
  1622  // but it ensures that we dot not split UTF-8 rune in half.
  1623  // Otherwise appengine python scripts will break badly.
  1624  func limitLength(s string, max int) string {
  1625  	s = strings.TrimSpace(s)
  1626  	if len(s) <= max {
  1627  		return s
  1628  	}
  1629  	for {
  1630  		s = s[:max]
  1631  		r, size := utf8.DecodeLastRuneInString(s)
  1632  		if r != utf8.RuneError || size != 1 {
  1633  			return s
  1634  		}
  1635  		max--
  1636  	}
  1637  }
  1638  
  1639  func GetEmails(r dashapi.Recipients, filter dashapi.RecipientType) []string {
  1640  	emails := []string{}
  1641  	for _, user := range r {
  1642  		if user.Type == filter {
  1643  			emails = append(emails, user.Address.Address)
  1644  		}
  1645  	}
  1646  	sort.Strings(emails)
  1647  	return emails
  1648  }
  1649  
  1650  // Verifies that the given credentials are acceptable and returns the
  1651  // corresponding namespace.
  1652  func checkClient(conf *GlobalConfig, name0, secretPassword, oauthSubject string) (string, error) {
  1653  	checkAuth := func(ns, a string) (string, error) {
  1654  		if strings.HasPrefix(a, auth.OauthMagic) && a == oauthSubject {
  1655  			return ns, nil
  1656  		}
  1657  		if a != secretPassword {
  1658  			return ns, ErrAccess
  1659  		}
  1660  		return ns, nil
  1661  	}
  1662  	for name, authenticator := range conf.Clients {
  1663  		if name == name0 {
  1664  			return checkAuth("", authenticator)
  1665  		}
  1666  	}
  1667  	for ns, cfg := range conf.Namespaces {
  1668  		for name, authenticator := range cfg.Clients {
  1669  			if name == name0 {
  1670  				return checkAuth(ns, authenticator)
  1671  			}
  1672  		}
  1673  	}
  1674  	return "", ErrAccess
  1675  }
  1676  
  1677  func handleRefreshSubsystems(w http.ResponseWriter, r *http.Request) {
  1678  	c := appengine.NewContext(r)
  1679  	const updateBugsCount = 25
  1680  	for ns := range getConfig(c).Namespaces {
  1681  		err := reassignBugSubsystems(c, ns, updateBugsCount)
  1682  		if err != nil {
  1683  			log.Errorf(c, "failed to update subsystems for %s: %v", ns, err)
  1684  		}
  1685  	}
  1686  }
  1687  
  1688  func apiSaveDiscussion(c context.Context, r *http.Request, payload []byte) (interface{}, error) {
  1689  	req := new(dashapi.SaveDiscussionReq)
  1690  	if err := json.Unmarshal(payload, req); err != nil {
  1691  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
  1692  	}
  1693  	d := req.Discussion
  1694  	newBugIDs := []string{}
  1695  	for _, id := range d.BugIDs {
  1696  		_, _, err := findBugByReportingID(c, id)
  1697  		if err == nil {
  1698  			newBugIDs = append(newBugIDs, id)
  1699  		}
  1700  	}
  1701  	d.BugIDs = newBugIDs
  1702  	if len(d.BugIDs) == 0 {
  1703  		return nil, nil
  1704  	}
  1705  	return nil, mergeDiscussion(c, d)
  1706  }
  1707  
  1708  func emergentlyStopped(c context.Context) (bool, error) {
  1709  	keys, err := db.NewQuery("EmergencyStop").
  1710  		Limit(1).
  1711  		KeysOnly().
  1712  		GetAll(c, nil)
  1713  	if err != nil {
  1714  		return false, err
  1715  	}
  1716  	return len(keys) > 0, nil
  1717  }
  1718  
  1719  func recordEmergencyStop(c context.Context) error {
  1720  	key := db.NewKey(c, "EmergencyStop", "all", 0, nil)
  1721  	_, err := db.Put(c, key, &EmergencyStop{
  1722  		Time: timeNow(c),
  1723  		User: user.Current(c).Email,
  1724  	})
  1725  	return err
  1726  }
  1727  
  1728  // Share crash logs for non-reproduced bugs with syz-managers.
  1729  // In future, this can also take care of repro exchange between instances
  1730  // in the place of syz-hub.
  1731  func apiLogToReproduce(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) {
  1732  	req := new(dashapi.LogToReproReq)
  1733  	if err := json.Unmarshal(payload, req); err != nil {
  1734  		return nil, fmt.Errorf("failed to unmarshal request: %w", err)
  1735  	}
  1736  	build, err := loadBuild(c, ns, req.BuildID)
  1737  	if err != nil {
  1738  		return nil, err
  1739  	}
  1740  	bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query {
  1741  		return query.Filter("Namespace=", ns).
  1742  			Filter("HappenedOn=", build.Manager).
  1743  			Filter("Status=", BugStatusOpen)
  1744  	})
  1745  	if err != nil {
  1746  		return nil, fmt.Errorf("failed to query bugs: %w", err)
  1747  	}
  1748  	rand.New(rand.NewSource(timeNow(c).UnixNano())).Shuffle(len(bugs), func(i, j int) {
  1749  		bugs[i], bugs[j] = bugs[j], bugs[i]
  1750  	})
  1751  	// Let's limit the load on the DB.
  1752  	const bugsToConsider = 10
  1753  	checkedBugs := 0
  1754  	for _, bug := range bugs {
  1755  		if bug.ReproLevel != ReproLevelNone {
  1756  			continue
  1757  		}
  1758  		if len(bug.Commits) > 0 || len(bug.ReproAttempts) > 0 {
  1759  			// For now let's focus on all bugs where we have never ever
  1760  			// finished a bug reproduction process.
  1761  			continue
  1762  		}
  1763  		checkedBugs++
  1764  		if checkedBugs > bugsToConsider {
  1765  			break
  1766  		}
  1767  		resp, err := logToReproForBug(c, bug, build.Manager)
  1768  		if resp != nil || err != nil {
  1769  			return resp, err
  1770  		}
  1771  	}
  1772  	return nil, nil
  1773  }
  1774  
  1775  func logToReproForBug(c context.Context, bug *Bug, manager string) (*dashapi.LogToReproResp, error) {
  1776  	const considerCrashes = 10
  1777  	crashes, _, err := queryCrashesForBug(c, bug.key(c), considerCrashes)
  1778  	if err != nil {
  1779  		return nil, err
  1780  	}
  1781  	for _, crash := range crashes {
  1782  		if crash.Manager != manager {
  1783  			continue
  1784  		}
  1785  		crashLog, _, err := getText(c, textCrashLog, crash.Log)
  1786  		if err != nil {
  1787  			return nil, fmt.Errorf("failed to query a crash log: %w", err)
  1788  		}
  1789  		return &dashapi.LogToReproResp{
  1790  			Title:    bug.Title,
  1791  			CrashLog: crashLog,
  1792  		}, nil
  1793  	}
  1794  	return nil, nil
  1795  }