blob: 58f2e05b28b2e6149f7d4e89523edc8214666e80 [file] [log] [blame] [edit]
// 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 main
import (
"fmt"
"net/http"
"time"
"github.com/google/syzkaller/dashboard/dashapi"
"golang.org/x/net/context"
db "google.golang.org/appengine/v2/datastore"
"google.golang.org/appengine/v2/log"
aemail "google.golang.org/appengine/v2/mail"
)
func handleInvalidateBisection(c context.Context, w http.ResponseWriter, r *http.Request) error {
encodedKey := r.FormValue("key")
if encodedKey == "" {
return fmt.Errorf("mandatory parameter key is missing")
}
jobKey, err := db.DecodeKey(encodedKey)
if err != nil {
return fmt.Errorf("failed to decode job key %v: %w", encodedKey, err)
}
err = invalidateBisection(c, jobKey, r.FormValue("restart") == "1")
if err != nil {
return fmt.Errorf("failed to invalidate job %v: %w", jobKey, err)
}
// Sending back to bug page after successful invalidation.
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusFound)
return nil
}
// dropNamespace drops all entities related to a single namespace.
// Use with care. There is no undo.
// This functionality is intentionally not connected to any handler.
// To use it, first make a backup of the db. Then, specify the target
// namespace in the ns variable, connect the function to a handler, invoke it
// and double check the output. Finally, set dryRun to false and invoke again.
func dropNamespace(c context.Context, w http.ResponseWriter, r *http.Request) error {
ns := "non-existent"
dryRun := true
if !dryRun {
log.Criticalf(c, "dropping namespace %v", ns)
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "dropping namespace %v\n", ns)
if err := dropNamespaceReportingState(c, w, ns, dryRun); err != nil {
return err
}
type Entity struct {
name string
child string
}
entities := []Entity{
{textPatch, ""},
{textReproC, ""},
{textReproSyz, ""},
{textKernelConfig, ""},
{"Job", ""},
{textLog, ""},
{textError, ""},
{textCrashLog, ""},
{textCrashReport, ""},
{"Build", ""},
{"Manager", "ManagerStats"},
{"Bug", "Crash"},
}
for _, entity := range entities {
keys, err := db.NewQuery(entity.name).
Filter("Namespace=", ns).
KeysOnly().
GetAll(c, nil)
if err != nil {
return err
}
fmt.Fprintf(w, "%v: %v\n", entity.name, len(keys))
if entity.child != "" {
var childKeys []*db.Key
for _, key := range keys {
keys1, err := db.NewQuery(entity.child).
Ancestor(key).
KeysOnly().
GetAll(c, nil)
if err != nil {
return err
}
childKeys = append(childKeys, keys1...)
}
fmt.Fprintf(w, " %v: %v\n", entity.child, len(childKeys))
if err := dropEntities(c, childKeys, dryRun); err != nil {
return err
}
}
if err := dropEntities(c, keys, dryRun); err != nil {
return err
}
}
return nil
}
func dropNamespaceReportingState(c context.Context, w http.ResponseWriter, ns string, dryRun bool) error {
tx := func(c context.Context) error {
state, err := loadReportingState(c)
if err != nil {
return err
}
newState := new(ReportingState)
for _, ent := range state.Entries {
if ent.Namespace != ns {
newState.Entries = append(newState.Entries, ent)
}
}
if !dryRun {
if err := saveReportingState(c, newState); err != nil {
return err
}
}
fmt.Fprintf(w, "ReportingState: %v\n", len(state.Entries)-len(newState.Entries))
return nil
}
return db.RunInTransaction(c, tx, nil)
}
func dropEntities(c context.Context, keys []*db.Key, dryRun bool) error {
if dryRun {
return nil
}
for len(keys) != 0 {
batch := 100
if batch > len(keys) {
batch = len(keys)
}
if err := db.DeleteMulti(c, keys[:batch]); err != nil {
return err
}
keys = keys[batch:]
}
return nil
}
func restartFailedBisections(c context.Context, w http.ResponseWriter, r *http.Request) error {
if accessLevel(c, r) != AccessAdmin {
return fmt.Errorf("admin only")
}
ns := r.FormValue("ns")
if ns == "" {
return fmt.Errorf("no ns parameter")
}
var jobs []*Job
var jobKeys []*db.Key
jobKeys, err := db.NewQuery("Job").
Filter("Finished>", time.Time{}).
GetAll(c, &jobs)
if err != nil {
return fmt.Errorf("failed to query jobs: %w", err)
}
toReset := []*db.Key{}
for i, job := range jobs {
if job.Namespace != ns {
continue
}
if job.Type != JobBisectCause && job.Type != JobBisectFix {
continue
}
if job.Error == 0 {
continue
}
errorTextBytes, _, err := getText(c, textError, job.Error)
if err != nil {
return fmt.Errorf("failed to query error text: %w", err)
}
fmt.Fprintf(w, "job type %v, ns %s, finished at %s, error:%s\n========\n",
job.Type, job.Namespace, job.Finished, string(errorTextBytes))
toReset = append(toReset, jobKeys[i])
}
if r.FormValue("apply") != "yes" {
return nil
}
for idx, jobKey := range toReset {
err = invalidateBisection(c, jobKey, true)
if err != nil {
fmt.Fprintf(w, "job %v update failed: %s", idx, err)
}
}
fmt.Fprintf(w, "Done!\n")
return nil
}
// updateBugReporting adds missing reporting stages to bugs in a single namespace.
// Use with care. There is no undo.
// This can be used to migrate datastore to a new config with more reporting stages.
// This functionality is intentionally not connected to any handler.
// Before invoking it is recommended to stop all connected instances just in case.
func updateBugReporting(c context.Context, w http.ResponseWriter, r *http.Request) error {
if accessLevel(c, r) != AccessAdmin {
return fmt.Errorf("admin only")
}
ns := r.FormValue("ns")
if ns == "" {
return fmt.Errorf("no ns parameter")
}
var bugs []*Bug
keys, err := db.NewQuery("Bug").
Filter("Namespace=", ns).
GetAll(c, &bugs)
if err != nil {
return err
}
log.Warningf(c, "fetched %v bugs for namespce %v", len(bugs), ns)
cfg := getNsConfig(c, ns)
var update []*db.Key
for i, bug := range bugs {
if len(bug.Reporting) >= len(cfg.Reporting) {
continue
}
update = append(update, keys[i])
}
return updateBatch(c, update, func(_ *db.Key, bug *Bug) {
err := bug.updateReportings(c, cfg, timeNow(c))
if err != nil {
panic(err)
}
})
}
// updateBugTitles adds missing MergedTitles/AltTitles to bugs.
// This can be used to migrate datastore to the new scheme introduced:
// by commit fd1036219797 ("dashboard/app: merge duplicate crashes").
func updateBugTitles(c context.Context, w http.ResponseWriter, r *http.Request) error {
if accessLevel(c, r) != AccessAdmin {
return fmt.Errorf("admin only")
}
var keys []*db.Key
if err := foreachBug(c, nil, func(bug *Bug, key *db.Key) error {
if len(bug.MergedTitles) == 0 || len(bug.AltTitles) == 0 {
keys = append(keys, key)
}
return nil
}); err != nil {
return err
}
log.Warningf(c, "fetched %v bugs for update", len(keys))
return updateBatch(c, keys, func(_ *db.Key, bug *Bug) {
if len(bug.MergedTitles) == 0 {
bug.MergedTitles = []string{bug.Title}
}
if len(bug.AltTitles) == 0 {
bug.AltTitles = []string{bug.Title}
}
})
}
// updateCrashPriorities regenerates priorities for crashes.
// This has become necessary after the "dashboard: support per-Manager priority" commit.
// For now, the method only considers the crashes referenced from bug origin.
func updateCrashPriorities(c context.Context, w http.ResponseWriter, r *http.Request) error {
if accessLevel(c, r) != AccessAdmin {
return fmt.Errorf("admin only")
}
ns := r.FormValue("ns")
if ns == "" {
return fmt.Errorf("no ns parameter")
}
bugsCount := 0
bugPerKey := map[string]*Bug{}
var crashKeys []*db.Key
if err := foreachBug(c, func(query *db.Query) *db.Query {
return query.Filter("Status=", BugStatusOpen).Filter("Namespace=", ns)
}, func(bug *Bug, key *db.Key) error {
bugsCount++
// There'll be duplicate crash IDs.
crashIDs := map[int64]struct{}{}
for _, item := range bug.TreeTests.List {
crashIDs[item.CrashID] = struct{}{}
}
for crashID := range crashIDs {
crashKeys = append(crashKeys, db.NewKey(c, "Crash", "", crashID, key))
}
bugPerKey[key.String()] = bug
return nil
}); err != nil {
return err
}
log.Warningf(c, "fetched %d bugs and %v crash keys to update", bugsCount, len(crashKeys))
return updateBatch(c, crashKeys, func(key *db.Key, crash *Crash) {
bugKey := key.Parent()
bug := bugPerKey[bugKey.String()]
build, err := loadBuild(c, ns, crash.BuildID)
if build == nil || err != nil {
panic(fmt.Sprintf("err: %s, build: %v", err, build))
}
crash.UpdateReportingPriority(c, build, bug)
})
}
// setMissingBugFields makes sure all Bug entity fields are present in the database.
// The problem is that, in Datastore, sorting/filtering on a field will only return entries
// where that field is present.
func setMissingBugFields(c context.Context, w http.ResponseWriter, r *http.Request) error {
if accessLevel(c, r) != AccessAdmin {
return fmt.Errorf("admin only")
}
var keys []*db.Key
// Query everything.
err := foreachBug(c, nil, func(bug *Bug, key *db.Key) error {
keys = append(keys, key)
return nil
})
if err != nil {
return err
}
log.Warningf(c, "fetched %v bugs for update", len(keys))
// Save everything unchanged.
return updateBatch(c, keys, func(_ *db.Key, bug *Bug) {})
}
// adminSendEmail can be used to send an arbitrary message from the bot.
func adminSendEmail(c context.Context, w http.ResponseWriter, r *http.Request) error {
if accessLevel(c, r) != AccessAdmin {
return fmt.Errorf("admin only")
}
msg := &aemail.Message{
Sender: r.FormValue("from"),
To: []string{r.FormValue("to")},
Body: r.FormValue("body"),
}
return sendEmail(c, msg)
}
func updateHeadReproLevel(c context.Context, w http.ResponseWriter, r *http.Request) error {
if accessLevel(c, r) != AccessAdmin {
return fmt.Errorf("admin only")
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
var keys []*db.Key
newLevels := map[string]dashapi.ReproLevel{}
if err := foreachBug(c, func(query *db.Query) *db.Query {
return query.Filter("Status=", BugStatusOpen)
}, func(bug *Bug, key *db.Key) error {
if len(bug.Commits) > 0 {
return nil
}
actual := ReproLevelNone
reproCrashes, _, err := queryCrashesForBug(c, key, 2)
if err != nil {
return fmt.Errorf("failed to fetch crashes with repro: %w", err)
}
for _, crash := range reproCrashes {
if crash.ReproIsRevoked {
continue
}
if crash.ReproC > 0 {
actual = ReproLevelC
break
}
if crash.ReproSyz > 0 {
actual = ReproLevelSyz
}
}
if actual != bug.HeadReproLevel {
fmt.Fprintf(w, "%v: HeadReproLevel mismatch, actual=%d db=%d\n", bugLink(bug.keyHash(c)), actual, bug.HeadReproLevel)
newLevels[bug.keyHash(c)] = actual
keys = append(keys, key)
}
return nil
}); err != nil {
return err
}
return updateBatch(c, keys, func(_ *db.Key, bug *Bug) {
newLevel, ok := newLevels[bug.keyHash(c)]
if !ok {
panic("fetched unknown bug")
}
bug.HeadReproLevel = newLevel
})
}
func updateBatch[T any](c context.Context, keys []*db.Key, transform func(key *db.Key, item *T)) error {
for len(keys) != 0 {
batchSize := 20
if batchSize > len(keys) {
batchSize = len(keys)
}
batchKeys := keys[:batchSize]
keys = keys[batchSize:]
tx := func(c context.Context) error {
items := make([]*T, len(batchKeys))
if err := db.GetMulti(c, batchKeys, items); err != nil {
return err
}
for i, item := range items {
transform(batchKeys[i], item)
}
_, err := db.PutMulti(c, batchKeys, items)
return err
}
if err := db.RunInTransaction(c, tx, &db.TransactionOptions{XG: true}); err != nil {
return err
}
log.Warningf(c, "updated %v bugs", len(batchKeys))
}
return nil
}
// Prevent warnings about dead code.
var (
_ = dropNamespace
_ = updateBugReporting
_ = updateBugTitles
_ = restartFailedBisections
_ = setMissingBugFields
_ = adminSendEmail
_ = updateHeadReproLevel
_ = updateCrashPriorities
)