blob: a7b52b6292dde51d3dd06cabae047a040804c929 [file] [log] [blame]
// Copyright 2017 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
package dash
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"sort"
"strings"
"time"
"unicode/utf8"
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/email"
"github.com/google/syzkaller/pkg/hash"
"golang.org/x/net/context"
"google.golang.org/appengine"
"google.golang.org/appengine/datastore"
"google.golang.org/appengine/log"
)
func init() {
http.Handle("/api", handleJSON(handleAPI))
}
var apiHandlers = map[string]APIHandler{
"log_error": apiLogError,
"job_poll": apiJobPoll,
"job_done": apiJobDone,
"reporting_poll": apiReportingPoll,
"reporting_update": apiReportingUpdate,
}
var apiNamespaceHandlers = map[string]APINamespaceHandler{
"upload_build": apiUploadBuild,
"builder_poll": apiBuilderPoll,
"report_build_error": apiReportBuildError,
"report_crash": apiReportCrash,
"report_failed_repro": apiReportFailedRepro,
"need_repro": apiNeedRepro,
"manager_stats": apiManagerStats,
}
type JSONHandler func(c context.Context, r *http.Request) (interface{}, error)
type APIHandler func(c context.Context, r *http.Request) (interface{}, error)
type APINamespaceHandler func(c context.Context, ns string, r *http.Request) (interface{}, error)
// Overridable for testing.
var timeNow = func(c context.Context) time.Time {
return time.Now()
}
func timeSince(c context.Context, t time.Time) time.Duration {
return timeNow(c).Sub(t)
}
func handleJSON(fn JSONHandler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
reply, err := fn(c, r)
if err != nil {
log.Errorf(c, "%v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
json.NewEncoder(gz).Encode(reply)
gz.Close()
} else {
json.NewEncoder(w).Encode(reply)
}
})
}
func handleAPI(c context.Context, r *http.Request) (reply interface{}, err error) {
ns, err := checkClient(c, r.FormValue("client"), r.FormValue("key"))
if err != nil {
log.Warningf(c, "%v", err)
return nil, fmt.Errorf("unauthorized request")
}
method := r.FormValue("method")
handler := apiHandlers[method]
if handler != nil {
return handler(c, r)
}
nsHandler := apiNamespaceHandlers[method]
if nsHandler == nil {
return nil, fmt.Errorf("unknown api method %q", method)
}
if ns == "" {
return nil, fmt.Errorf("method %q must be called within a namespace", method)
}
return nsHandler(c, ns, r)
}
func checkClient(c context.Context, name0, key0 string) (string, error) {
for name, key := range config.Clients {
if name == name0 {
if key != key0 {
return "", fmt.Errorf("wrong client %q key", name0)
}
return "", nil
}
}
for ns, cfg := range config.Namespaces {
for name, key := range cfg.Clients {
if name == name0 {
if key != key0 {
return "", fmt.Errorf("wrong client %q key", name0)
}
return ns, nil
}
}
}
return "", fmt.Errorf("unauthorized api request from %q", name0)
}
func apiLogError(c context.Context, r *http.Request) (interface{}, error) {
req := new(dashapi.LogEntry)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, fmt.Errorf("failed to unmarshal request: %v", err)
}
log.Errorf(c, "%v: %v", req.Name, req.Text)
return nil, nil
}
func apiBuilderPoll(c context.Context, ns string, r *http.Request) (interface{}, error) {
req := new(dashapi.BuilderPollReq)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, fmt.Errorf("failed to unmarshal request: %v", err)
}
var bugs []*Bug
_, err := datastore.NewQuery("Bug").
Filter("Namespace=", ns).
Filter("Status<", BugStatusFixed).
GetAll(c, &bugs)
if err != nil {
return nil, fmt.Errorf("failed to query bugs: %v", err)
}
m := make(map[string]bool)
loop:
for _, bug := range bugs {
// TODO(dvyukov): include this condition into the query if possible.
if len(bug.Commits) == 0 {
continue
}
for _, mgr := range bug.PatchedOn {
if mgr == req.Manager {
continue loop
}
}
for _, com := range bug.Commits {
m[com] = true
}
}
commits := make([]string, 0, len(m))
for com := range m {
commits = append(commits, com)
}
sort.Strings(commits)
resp := &dashapi.BuilderPollResp{
PendingCommits: commits,
}
return resp, nil
}
func apiJobPoll(c context.Context, r *http.Request) (interface{}, error) {
req := new(dashapi.JobPollReq)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, fmt.Errorf("failed to unmarshal request: %v", err)
}
if len(req.Managers) == 0 {
return nil, fmt.Errorf("no managers")
}
return pollPendingJobs(c, req.Managers)
}
func apiJobDone(c context.Context, r *http.Request) (interface{}, error) {
req := new(dashapi.JobDoneReq)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, fmt.Errorf("failed to unmarshal request: %v", err)
}
err := doneJob(c, req)
return nil, err
}
func apiUploadBuild(c context.Context, ns string, r *http.Request) (interface{}, error) {
req := new(dashapi.Build)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, fmt.Errorf("failed to unmarshal request: %v", err)
}
isNewBuild, err := uploadBuild(c, ns, req, BuildNormal)
if err != nil {
return nil, err
}
if len(req.Commits) != 0 {
if err := addCommitsToBugs(c, ns, req.Manager, req.Commits); err != nil {
return nil, err
}
}
if isNewBuild {
if err := updateManager(c, ns, req.Manager, func(mgr *Manager, stats *ManagerStats) {
mgr.CurrentBuild = req.ID
mgr.FailedBuildBug = ""
}); err != nil {
return nil, err
}
}
return nil, nil
}
func uploadBuild(c context.Context, ns string, req *dashapi.Build, typ BuildType) (bool, error) {
if _, err := loadBuild(c, ns, req.ID); err == nil {
return false, nil
}
checkStrLen := func(str, name string, maxLen int) error {
if str == "" {
return fmt.Errorf("%v is empty", name)
}
if len(str) > maxLen {
return fmt.Errorf("%v is too long (%v)", name, len(str))
}
return nil
}
if err := checkStrLen(req.Manager, "Build.Manager", MaxStringLen); err != nil {
return false, err
}
if err := checkStrLen(req.ID, "Build.ID", MaxStringLen); err != nil {
return false, err
}
if err := checkStrLen(req.KernelRepo, "Build.KernelRepo", MaxStringLen); err != nil {
return false, err
}
if err := checkStrLen(req.KernelBranch, "Build.KernelBranch", MaxStringLen); err != nil {
return false, err
}
if err := checkStrLen(req.SyzkallerCommit, "Build.SyzkallerCommit", MaxStringLen); err != nil {
return false, err
}
if err := checkStrLen(req.CompilerID, "Build.CompilerID", MaxStringLen); err != nil {
return false, err
}
if err := checkStrLen(req.KernelCommit, "Build.KernelCommit", MaxStringLen); err != nil {
return false, err
}
configID, err := putText(c, ns, "KernelConfig", req.KernelConfig, true)
if err != nil {
return false, err
}
build := &Build{
Namespace: ns,
Manager: req.Manager,
ID: req.ID,
Type: typ,
Time: timeNow(c),
OS: req.OS,
Arch: req.Arch,
VMArch: req.VMArch,
SyzkallerCommit: req.SyzkallerCommit,
CompilerID: req.CompilerID,
KernelRepo: req.KernelRepo,
KernelBranch: req.KernelBranch,
KernelCommit: req.KernelCommit,
KernelConfig: configID,
}
if _, err := datastore.Put(c, buildKey(c, ns, req.ID), build); err != nil {
return false, err
}
return true, nil
}
func addCommitsToBugs(c context.Context, ns, manager string, commits []string) error {
commitMap := make(map[string]bool)
for _, com := range commits {
commitMap[com] = true
}
managers, err := managerList(c, ns)
if err != nil {
return err
}
var bugs []*Bug
keys, err := datastore.NewQuery("Bug").
Filter("Namespace=", ns).
Filter("Status<", BugStatusFixed).
GetAll(c, &bugs)
if err != nil {
return fmt.Errorf("failed to query bugs: %v", err)
}
now := timeNow(c)
for i, bug := range bugs {
if !fixedWith(bug, manager, commitMap) {
continue
}
tx := func(c context.Context) error {
bug := new(Bug)
if err := datastore.Get(c, keys[i], bug); err != nil {
return fmt.Errorf("failed to get bug %v: %v", keys[i].StringID(), err)
}
if !fixedWith(bug, manager, commitMap) {
return nil
}
bug.PatchedOn = append(bug.PatchedOn, manager)
if bug.Status == BugStatusOpen {
fixed := true
for _, mgr := range managers {
if !stringInList(bug.PatchedOn, mgr) {
fixed = false
break
}
}
if fixed {
bug.Status = BugStatusFixed
bug.Closed = now
}
}
if _, err := datastore.Put(c, keys[i], bug); err != nil {
return fmt.Errorf("failed to put bug: %v", err)
}
return nil
}
if err := datastore.RunInTransaction(c, tx, nil); err != nil {
return err
}
}
return nil
}
func managerList(c context.Context, ns string) ([]string, error) {
var builds []*Build
_, err := datastore.NewQuery("Build").
Filter("Namespace=", ns).
Project("Manager").
Distinct().
GetAll(c, &builds)
if err != nil {
return nil, fmt.Errorf("failed to query builds: %v", err)
}
decommissioned := config.Namespaces[ns].DecommissionedManagers
var managers []string
for _, build := range builds {
if _, ok := decommissioned[build.Manager]; ok {
continue
}
managers = append(managers, build.Manager)
}
return managers, nil
}
func fixedWith(bug *Bug, manager string, commits map[string]bool) bool {
if stringInList(bug.PatchedOn, manager) {
return false
}
for _, com := range bug.Commits {
if !commits[com] {
return false
}
}
return len(bug.Commits) > 0
}
func stringInList(list []string, str string) bool {
for _, s := range list {
if s == str {
return true
}
}
return false
}
func apiReportBuildError(c context.Context, ns string, r *http.Request) (interface{}, error) {
req := new(dashapi.BuildErrorReq)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, fmt.Errorf("failed to unmarshal request: %v", err)
}
if _, err := uploadBuild(c, ns, &req.Build, BuildFailed); err != nil {
return nil, err
}
req.Crash.BuildID = req.Build.ID
bug, err := reportCrash(c, ns, &req.Crash)
if err != nil {
return nil, err
}
if err := updateManager(c, ns, req.Build.Manager, func(mgr *Manager, stats *ManagerStats) {
mgr.FailedBuildBug = bugKeyHash(bug.Namespace, bug.Title, bug.Seq)
}); err != nil {
return nil, err
}
return nil, nil
}
const corruptedReportTitle = "corrupted report"
func apiReportCrash(c context.Context, ns string, r *http.Request) (interface{}, error) {
req := new(dashapi.Crash)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, fmt.Errorf("failed to unmarshal request: %v", err)
}
bug, err := reportCrash(c, ns, req)
if err != nil {
return nil, err
}
resp := &dashapi.ReportCrashResp{
NeedRepro: needRepro(c, bug),
}
return resp, nil
}
func reportCrash(c context.Context, ns string, req *dashapi.Crash) (*Bug, error) {
req.Title = limitLength(req.Title, maxTextLen)
req.Maintainers = email.MergeEmailLists(req.Maintainers)
if req.Corrupted {
// The report is corrupted and the title is most likely invalid.
// Such reports are usually unactionable and are discarded.
// Collect them into a single bin.
req.Title = corruptedReportTitle
}
bug, bugKey, err := findBugForCrash(c, ns, req.Title)
if err != nil {
return nil, err
}
if active, err := isActiveBug(c, bug); err != nil {
return nil, err
} else if !active {
bug, bugKey, err = createBugForCrash(c, ns, req)
if err != nil {
return nil, err
}
}
now := timeNow(c)
reproLevel := ReproLevelNone
if len(req.ReproC) != 0 {
reproLevel = ReproLevelC
} else if len(req.ReproSyz) != 0 {
reproLevel = ReproLevelSyz
}
saveCrash := bug.NumCrashes < 1000 ||
now.Sub(bug.LastTime) > time.Hour ||
reproLevel != ReproLevelNone
if saveCrash {
build, err := loadBuild(c, ns, req.BuildID)
if err != nil {
return nil, err
}
crash := &Crash{
Manager: build.Manager,
BuildID: req.BuildID,
Time: now,
Maintainers: req.Maintainers,
ReproOpts: req.ReproOpts,
// We used to report crash with the longest report len to work around
// corrupted reports. Now that we explicitly detect corrupted reports,
// disable this sorting. When all old bugs are closed, we need to remove
// sorting by ReportLen from queryCrashesForBug.
ReportLen: 1e9,
}
if crash.Log, err = putText(c, ns, "CrashLog", req.Log, false); err != nil {
return nil, err
}
if crash.Report, err = putText(c, ns, "CrashReport", req.Report, false); err != nil {
return nil, err
}
if crash.ReproSyz, err = putText(c, ns, "ReproSyz", req.ReproSyz, false); err != nil {
return nil, err
}
if crash.ReproC, err = putText(c, ns, "ReproC", req.ReproC, false); err != nil {
return nil, err
}
crashKey := datastore.NewIncompleteKey(c, "Crash", bugKey)
if _, err = datastore.Put(c, crashKey, crash); err != nil {
return nil, fmt.Errorf("failed to put crash: %v", err)
}
}
tx := func(c context.Context) error {
bug = new(Bug)
if err := datastore.Get(c, bugKey, bug); err != nil {
return fmt.Errorf("failed to get bug: %v", err)
}
bug.NumCrashes++
bug.LastTime = now
if reproLevel != ReproLevelNone {
bug.NumRepro++
}
if bug.ReproLevel < reproLevel {
bug.ReproLevel = reproLevel
}
if len(req.Report) != 0 {
bug.HasReport = true
}
if _, err = datastore.Put(c, bugKey, bug); err != nil {
return fmt.Errorf("failed to put bug: %v", err)
}
return nil
}
if err := datastore.RunInTransaction(c, tx, &datastore.TransactionOptions{XG: true}); err != nil {
return nil, err
}
if saveCrash {
purgeOldCrashes(c, bug, bugKey)
}
return bug, nil
}
func purgeOldCrashes(c context.Context, bug *Bug, bugKey *datastore.Key) {
const batchSize = 10 // delete at most that many at once
if bug.NumCrashes <= maxCrashes {
return
}
var crashes []*Crash
keys, err := datastore.NewQuery("Crash").
Ancestor(bugKey).
Filter("ReproC=", 0).
Filter("ReproSyz=", 0).
Order("ReportLen").
Order("Time").
Limit(maxCrashes+batchSize).
GetAll(c, &crashes)
if err != nil {
log.Errorf(c, "failed to fetch purge crashes: %v", err)
return
}
if len(keys) <= maxCrashes {
return
}
keys = keys[:len(keys)-maxCrashes]
crashes = crashes[:len(crashes)-maxCrashes]
var texts []*datastore.Key
for _, crash := range crashes {
if crash.ReproSyz != 0 || crash.ReproC != 0 {
log.Errorf(c, "purging reproducer?")
continue
}
if crash.Log != 0 {
texts = append(texts, datastore.NewKey(c, "CrashLog", "", crash.Log, nil))
}
if crash.Report != 0 {
texts = append(texts, datastore.NewKey(c, "CrashReport", "", crash.Report, nil))
}
}
if len(texts) != 0 {
if err := datastore.DeleteMulti(c, texts); err != nil {
log.Errorf(c, "failed to delete old crash texts: %v", err)
return
}
}
if err := datastore.DeleteMulti(c, keys); err != nil {
log.Errorf(c, "failed to delete old crashes: %v", err)
return
}
log.Infof(c, "deleted %v crashes", len(keys))
}
func apiReportFailedRepro(c context.Context, ns string, r *http.Request) (interface{}, error) {
req := new(dashapi.CrashID)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, fmt.Errorf("failed to unmarshal request: %v", err)
}
req.Title = limitLength(req.Title, maxTextLen)
bug, bugKey, err := findBugForCrash(c, ns, req.Title)
if err != nil {
return nil, err
}
if bug == nil {
return nil, fmt.Errorf("%v: can't find bug for crash %q", ns, req.Title)
}
tx := func(c context.Context) error {
bug := new(Bug)
if err := datastore.Get(c, bugKey, bug); err != nil {
return fmt.Errorf("failed to get bug: %v", err)
}
bug.NumRepro++
if _, err := datastore.Put(c, bugKey, bug); err != nil {
return fmt.Errorf("failed to put bug: %v", err)
}
return nil
}
if err := datastore.RunInTransaction(c, tx, &datastore.TransactionOptions{
XG: true,
Attempts: 30,
}); err != nil {
return nil, err
}
return nil, nil
}
func apiNeedRepro(c context.Context, ns string, r *http.Request) (interface{}, error) {
req := new(dashapi.CrashID)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, fmt.Errorf("failed to unmarshal request: %v", err)
}
if req.Corrupted {
resp := &dashapi.NeedReproResp{
NeedRepro: false,
}
return resp, nil
}
req.Title = limitLength(req.Title, maxTextLen)
bug, _, err := findBugForCrash(c, ns, req.Title)
if err != nil {
return nil, err
}
if bug == nil {
return nil, fmt.Errorf("%v: can't find bug for crash %q", ns, req.Title)
}
resp := &dashapi.NeedReproResp{
NeedRepro: needRepro(c, bug),
}
return resp, nil
}
func apiManagerStats(c context.Context, ns string, r *http.Request) (interface{}, error) {
req := new(dashapi.ManagerStatsReq)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, fmt.Errorf("failed to unmarshal request: %v", err)
}
now := timeNow(c)
if err := updateManager(c, ns, req.Name, func(mgr *Manager, stats *ManagerStats) {
mgr.Link = req.Addr
mgr.LastAlive = now
mgr.CurrentUpTime = req.UpTime
if cur := int64(req.Corpus); cur > stats.MaxCorpus {
stats.MaxCorpus = cur
}
if cur := int64(req.Cover); cur > stats.MaxCover {
stats.MaxCover = cur
}
stats.TotalFuzzingTime += req.FuzzingTime
stats.TotalCrashes += int64(req.Crashes)
stats.TotalExecs += int64(req.Execs)
}); err != nil {
return nil, err
}
return nil, nil
}
func findBugForCrash(c context.Context, ns, title string) (*Bug, *datastore.Key, error) {
var bugs []*Bug
keys, err := datastore.NewQuery("Bug").
Filter("Namespace=", ns).
Filter("Title=", title).
Order("-Seq").
Limit(1).
GetAll(c, &bugs)
if err != nil {
return nil, nil, fmt.Errorf("failed to query bugs: %v", err)
}
if len(bugs) == 0 {
return nil, nil, nil
}
return bugs[0], keys[0], nil
}
func createBugForCrash(c context.Context, ns string, req *dashapi.Crash) (*Bug, *datastore.Key, error) {
var bug *Bug
var bugKey *datastore.Key
now := timeNow(c)
tx := func(c context.Context) error {
for seq := int64(0); ; seq++ {
bug = new(Bug)
bugHash := bugKeyHash(ns, req.Title, seq)
bugKey = datastore.NewKey(c, "Bug", bugHash, 0, nil)
if err := datastore.Get(c, bugKey, bug); err != nil {
if err != datastore.ErrNoSuchEntity {
return fmt.Errorf("failed to get bug: %v", err)
}
bug = &Bug{
Namespace: ns,
Seq: seq,
Title: req.Title,
Status: BugStatusOpen,
NumCrashes: 0,
NumRepro: 0,
ReproLevel: ReproLevelNone,
HasReport: false,
FirstTime: now,
LastTime: now,
}
for _, rep := range config.Namespaces[ns].Reporting {
bug.Reporting = append(bug.Reporting, BugReporting{
Name: rep.Name,
ID: bugReportingHash(bugHash, rep.Name),
})
}
if bugKey, err = datastore.Put(c, bugKey, bug); err != nil {
return fmt.Errorf("failed to put new bug: %v", err)
}
return nil
}
canon, err := canonicalBug(c, bug)
if err != nil {
return err
}
if canon.Status != BugStatusOpen {
continue
}
return nil
}
}
if err := datastore.RunInTransaction(c, tx, &datastore.TransactionOptions{
XG: true,
Attempts: 30,
}); err != nil {
return nil, nil, err
}
return bug, bugKey, nil
}
func isActiveBug(c context.Context, bug *Bug) (bool, error) {
if bug == nil {
return false, nil
}
canon, err := canonicalBug(c, bug)
if err != nil {
return false, err
}
return canon.Status == BugStatusOpen, nil
}
func needRepro(c context.Context, bug *Bug) bool {
if !needReproForBug(bug) {
return false
}
canon, err := canonicalBug(c, bug)
if err != nil {
log.Errorf(c, "failed to get canonical bug: %v", err)
return false
}
return needReproForBug(canon)
}
func needReproForBug(bug *Bug) bool {
return bug.ReproLevel < ReproLevelC &&
bug.NumRepro < 5 &&
len(bug.Commits) == 0 &&
bug.Title != corruptedReportTitle
}
func putText(c context.Context, ns, tag string, data []byte, dedup bool) (int64, error) {
if ns == "" {
return 0, fmt.Errorf("putting text outside of namespace")
}
if len(data) == 0 {
return 0, nil
}
const (
maxTextLen = 2 << 20
maxCompressedLen = 1000 << 10 // datastore entity limit is 1MB
)
if len(data) > maxTextLen {
data = data[:maxTextLen]
}
b := new(bytes.Buffer)
for {
z, _ := gzip.NewWriterLevel(b, gzip.BestCompression)
z.Write(data)
z.Close()
if len(b.Bytes()) < maxCompressedLen {
break
}
data = data[:len(data)/10*9]
b.Reset()
}
var key *datastore.Key
if dedup {
h := hash.Hash([]byte(ns), b.Bytes())
key = datastore.NewKey(c, tag, "", h.Truncate64(), nil)
} else {
key = datastore.NewIncompleteKey(c, tag, nil)
}
text := &Text{
Namespace: ns,
Text: b.Bytes(),
}
key, err := datastore.Put(c, key, text)
if err != nil {
return 0, err
}
return key.IntID(), nil
}
func getText(c context.Context, tag string, id int64) ([]byte, error) {
if id == 0 {
return nil, nil
}
text := new(Text)
if err := datastore.Get(c, datastore.NewKey(c, tag, "", id, nil), text); err != nil {
return nil, fmt.Errorf("failed to read text %v: %v", tag, err)
}
d, err := gzip.NewReader(bytes.NewBuffer(text.Text))
if err != nil {
return nil, fmt.Errorf("failed to read text %v: %v", tag, err)
}
data, err := ioutil.ReadAll(d)
if err != nil {
return nil, fmt.Errorf("failed to read text %v: %v", tag, err)
}
return data, nil
}
// limitLength essentially does return s[:max],
// but it ensures that we dot not split UTF-8 rune in half.
// Otherwise appengine python scripts will break badly.
func limitLength(s string, max int) string {
s = strings.TrimSpace(s)
if len(s) <= max {
return s
}
for {
s = s[:max]
r, size := utf8.DecodeLastRuneInString(s)
if r != utf8.RuneError || size != 1 {
return s
}
max--
}
}