github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/admin.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  	"context"
     8  	"fmt"
     9  	"net/http"
    10  	"time"
    11  
    12  	"github.com/google/syzkaller/dashboard/dashapi"
    13  	db "google.golang.org/appengine/v2/datastore"
    14  	"google.golang.org/appengine/v2/log"
    15  	aemail "google.golang.org/appengine/v2/mail"
    16  )
    17  
    18  func handleInvalidateBisection(c context.Context, w http.ResponseWriter, r *http.Request) error {
    19  	encodedKey := r.FormValue("key")
    20  	if encodedKey == "" {
    21  		return fmt.Errorf("mandatory parameter key is missing")
    22  	}
    23  	jobKey, err := db.DecodeKey(encodedKey)
    24  	if err != nil {
    25  		return fmt.Errorf("failed to decode job key %v: %w", encodedKey, err)
    26  	}
    27  
    28  	err = invalidateBisection(c, jobKey, r.FormValue("restart") == "1")
    29  	if err != nil {
    30  		return fmt.Errorf("failed to invalidate job %v: %w", jobKey, err)
    31  	}
    32  
    33  	// Sending back to bug page after successful invalidation.
    34  	http.Redirect(w, r, r.Header.Get("Referer"), http.StatusFound)
    35  	return nil
    36  }
    37  
    38  // dropNamespace drops all entities related to a single namespace.
    39  // Use with care. There is no undo.
    40  // This functionality is intentionally not connected to any handler.
    41  // To use it, first make a backup of the db. Then, specify the target
    42  // namespace in the ns variable, connect the function to a handler, invoke it
    43  // and double check the output. Finally, set dryRun to false and invoke again.
    44  func dropNamespace(c context.Context, w http.ResponseWriter, r *http.Request) error {
    45  	ns := "non-existent"
    46  	dryRun := true
    47  	if !dryRun {
    48  		log.Criticalf(c, "dropping namespace %v", ns)
    49  	}
    50  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    51  	fmt.Fprintf(w, "dropping namespace %v\n", ns)
    52  	if err := dropNamespaceReportingState(c, w, ns, dryRun); err != nil {
    53  		return err
    54  	}
    55  	type Entity struct {
    56  		name  string
    57  		child string
    58  	}
    59  	entities := []Entity{
    60  		{textPatch, ""},
    61  		{textReproC, ""},
    62  		{textReproSyz, ""},
    63  		{textKernelConfig, ""},
    64  		{"Job", ""},
    65  		{textLog, ""},
    66  		{textError, ""},
    67  		{textCrashLog, ""},
    68  		{textCrashReport, ""},
    69  		{"Build", ""},
    70  		{"Manager", "ManagerStats"},
    71  		{"Bug", "Crash"},
    72  	}
    73  	for _, entity := range entities {
    74  		keys, err := db.NewQuery(entity.name).
    75  			Filter("Namespace=", ns).
    76  			KeysOnly().
    77  			GetAll(c, nil)
    78  		if err != nil {
    79  			return err
    80  		}
    81  		fmt.Fprintf(w, "%v: %v\n", entity.name, len(keys))
    82  		if entity.child != "" {
    83  			var childKeys []*db.Key
    84  			for _, key := range keys {
    85  				keys1, err := db.NewQuery(entity.child).
    86  					Ancestor(key).
    87  					KeysOnly().
    88  					GetAll(c, nil)
    89  				if err != nil {
    90  					return err
    91  				}
    92  				childKeys = append(childKeys, keys1...)
    93  			}
    94  			fmt.Fprintf(w, "  %v: %v\n", entity.child, len(childKeys))
    95  			if err := dropEntities(c, childKeys, dryRun); err != nil {
    96  				return err
    97  			}
    98  		}
    99  		if err := dropEntities(c, keys, dryRun); err != nil {
   100  			return err
   101  		}
   102  	}
   103  	return nil
   104  }
   105  
   106  func dropNamespaceReportingState(c context.Context, w http.ResponseWriter, ns string, dryRun bool) error {
   107  	tx := func(c context.Context) error {
   108  		state, err := loadReportingState(c)
   109  		if err != nil {
   110  			return err
   111  		}
   112  		newState := new(ReportingState)
   113  		for _, ent := range state.Entries {
   114  			if ent.Namespace != ns {
   115  				newState.Entries = append(newState.Entries, ent)
   116  			}
   117  		}
   118  		if !dryRun {
   119  			if err := saveReportingState(c, newState); err != nil {
   120  				return err
   121  			}
   122  		}
   123  		fmt.Fprintf(w, "ReportingState: %v\n", len(state.Entries)-len(newState.Entries))
   124  		return nil
   125  	}
   126  	return db.RunInTransaction(c, tx, nil)
   127  }
   128  
   129  func dropEntities(c context.Context, keys []*db.Key, dryRun bool) error {
   130  	if dryRun {
   131  		return nil
   132  	}
   133  	for len(keys) != 0 {
   134  		batch := 100
   135  		if batch > len(keys) {
   136  			batch = len(keys)
   137  		}
   138  		if err := db.DeleteMulti(c, keys[:batch]); err != nil {
   139  			return err
   140  		}
   141  		keys = keys[batch:]
   142  	}
   143  	return nil
   144  }
   145  
   146  func restartFailedBisections(c context.Context, w http.ResponseWriter, r *http.Request) error {
   147  	if accessLevel(c, r) != AccessAdmin {
   148  		return fmt.Errorf("admin only")
   149  	}
   150  	ns := r.FormValue("ns")
   151  	if ns == "" {
   152  		return fmt.Errorf("no ns parameter")
   153  	}
   154  	var jobs []*Job
   155  	var jobKeys []*db.Key
   156  	jobKeys, err := db.NewQuery("Job").
   157  		Filter("Finished>", time.Time{}).
   158  		GetAll(c, &jobs)
   159  	if err != nil {
   160  		return fmt.Errorf("failed to query jobs: %w", err)
   161  	}
   162  	toReset := []*db.Key{}
   163  	for i, job := range jobs {
   164  		if job.Namespace != ns {
   165  			continue
   166  		}
   167  		if job.Type != JobBisectCause && job.Type != JobBisectFix {
   168  			continue
   169  		}
   170  		if job.Error == 0 {
   171  			continue
   172  		}
   173  		errorTextBytes, _, err := getText(c, textError, job.Error)
   174  		if err != nil {
   175  			return fmt.Errorf("failed to query error text: %w", err)
   176  		}
   177  		fmt.Fprintf(w, "job type %v, ns %s, finished at %s, error:%s\n========\n",
   178  			job.Type, job.Namespace, job.Finished, string(errorTextBytes))
   179  		toReset = append(toReset, jobKeys[i])
   180  	}
   181  	if r.FormValue("apply") != "yes" {
   182  		return nil
   183  	}
   184  	for idx, jobKey := range toReset {
   185  		err = invalidateBisection(c, jobKey, true)
   186  		if err != nil {
   187  			fmt.Fprintf(w, "job %v update failed: %s", idx, err)
   188  		}
   189  	}
   190  
   191  	fmt.Fprintf(w, "Done!\n")
   192  	return nil
   193  }
   194  
   195  // updateBugReporting adds missing reporting stages to bugs in a single namespace.
   196  // Use with care. There is no undo.
   197  // This can be used to migrate datastore to a new config with more reporting stages.
   198  // This functionality is intentionally not connected to any handler.
   199  // Before invoking it is recommended to stop all connected instances just in case.
   200  func updateBugReporting(c context.Context, w http.ResponseWriter, r *http.Request) error {
   201  	if accessLevel(c, r) != AccessAdmin {
   202  		return fmt.Errorf("admin only")
   203  	}
   204  	ns := r.FormValue("ns")
   205  	if ns == "" {
   206  		return fmt.Errorf("no ns parameter")
   207  	}
   208  	var bugs []*Bug
   209  	keys, err := db.NewQuery("Bug").
   210  		Filter("Namespace=", ns).
   211  		GetAll(c, &bugs)
   212  	if err != nil {
   213  		return err
   214  	}
   215  	log.Warningf(c, "fetched %v bugs for namespce %v", len(bugs), ns)
   216  	cfg := getNsConfig(c, ns)
   217  	var update []*db.Key
   218  	for i, bug := range bugs {
   219  		if len(bug.Reporting) >= len(cfg.Reporting) {
   220  			continue
   221  		}
   222  		update = append(update, keys[i])
   223  	}
   224  	return updateBatch(c, update, func(_ *db.Key, bug *Bug) {
   225  		err := bug.updateReportings(c, cfg, timeNow(c))
   226  		if err != nil {
   227  			panic(err)
   228  		}
   229  	})
   230  }
   231  
   232  // updateBugTitles adds missing MergedTitles/AltTitles to bugs.
   233  // This can be used to migrate datastore to the new scheme introduced:
   234  // by commit fd1036219797 ("dashboard/app: merge duplicate crashes").
   235  func updateBugTitles(c context.Context, w http.ResponseWriter, r *http.Request) error {
   236  	if accessLevel(c, r) != AccessAdmin {
   237  		return fmt.Errorf("admin only")
   238  	}
   239  	var keys []*db.Key
   240  	if err := foreachBug(c, nil, func(bug *Bug, key *db.Key) error {
   241  		if len(bug.MergedTitles) == 0 || len(bug.AltTitles) == 0 {
   242  			keys = append(keys, key)
   243  		}
   244  		return nil
   245  	}); err != nil {
   246  		return err
   247  	}
   248  	log.Warningf(c, "fetched %v bugs for update", len(keys))
   249  	return updateBatch(c, keys, func(_ *db.Key, bug *Bug) {
   250  		if len(bug.MergedTitles) == 0 {
   251  			bug.MergedTitles = []string{bug.Title}
   252  		}
   253  		if len(bug.AltTitles) == 0 {
   254  			bug.AltTitles = []string{bug.Title}
   255  		}
   256  	})
   257  }
   258  
   259  // updateCrashPriorities regenerates priorities for crashes.
   260  // This has become necessary after the "dashboard: support per-Manager priority" commit.
   261  // For now, the method only considers the crashes referenced from bug origin.
   262  func updateCrashPriorities(c context.Context, w http.ResponseWriter, r *http.Request) error {
   263  	if accessLevel(c, r) != AccessAdmin {
   264  		return fmt.Errorf("admin only")
   265  	}
   266  	ns := r.FormValue("ns")
   267  	if ns == "" {
   268  		return fmt.Errorf("no ns parameter")
   269  	}
   270  	bugsCount := 0
   271  	bugPerKey := map[string]*Bug{}
   272  	var crashKeys []*db.Key
   273  	if err := foreachBug(c, func(query *db.Query) *db.Query {
   274  		return query.Filter("Status=", BugStatusOpen).Filter("Namespace=", ns)
   275  	}, func(bug *Bug, key *db.Key) error {
   276  		bugsCount++
   277  		// There'll be duplicate crash IDs.
   278  		crashIDs := map[int64]struct{}{}
   279  		for _, item := range bug.TreeTests.List {
   280  			crashIDs[item.CrashID] = struct{}{}
   281  		}
   282  		for crashID := range crashIDs {
   283  			crashKeys = append(crashKeys, db.NewKey(c, "Crash", "", crashID, key))
   284  		}
   285  		bugPerKey[key.String()] = bug
   286  		return nil
   287  	}); err != nil {
   288  		return err
   289  	}
   290  	log.Warningf(c, "fetched %d bugs and %v crash keys to update", bugsCount, len(crashKeys))
   291  	return updateBatch(c, crashKeys, func(key *db.Key, crash *Crash) {
   292  		bugKey := key.Parent()
   293  		bug := bugPerKey[bugKey.String()]
   294  		build, err := loadBuild(c, ns, crash.BuildID)
   295  		if build == nil || err != nil {
   296  			panic(fmt.Sprintf("err: %s, build: %v", err, build))
   297  		}
   298  		crash.UpdateReportingPriority(c, build, bug)
   299  	})
   300  }
   301  
   302  // setMissingBugFields makes sure all Bug entity fields are present in the database.
   303  // The problem is that, in Datastore, sorting/filtering on a field will only return entries
   304  // where that field is present.
   305  func setMissingBugFields(c context.Context, w http.ResponseWriter, r *http.Request) error {
   306  	if accessLevel(c, r) != AccessAdmin {
   307  		return fmt.Errorf("admin only")
   308  	}
   309  	var keys []*db.Key
   310  	// Query everything.
   311  	err := foreachBug(c, nil, func(bug *Bug, key *db.Key) error {
   312  		keys = append(keys, key)
   313  		return nil
   314  	})
   315  	if err != nil {
   316  		return err
   317  	}
   318  	log.Warningf(c, "fetched %v bugs for update", len(keys))
   319  	// Save everything unchanged.
   320  	return updateBatch(c, keys, func(_ *db.Key, bug *Bug) {})
   321  }
   322  
   323  // adminSendEmail can be used to send an arbitrary message from the bot.
   324  func adminSendEmail(c context.Context, w http.ResponseWriter, r *http.Request) error {
   325  	if accessLevel(c, r) != AccessAdmin {
   326  		return fmt.Errorf("admin only")
   327  	}
   328  	msg := &aemail.Message{
   329  		Sender: r.FormValue("from"),
   330  		To:     []string{r.FormValue("to")},
   331  		Body:   r.FormValue("body"),
   332  	}
   333  	return sendEmail(c, msg)
   334  }
   335  
   336  func updateHeadReproLevel(c context.Context, w http.ResponseWriter, r *http.Request) error {
   337  	if accessLevel(c, r) != AccessAdmin {
   338  		return fmt.Errorf("admin only")
   339  	}
   340  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   341  	var keys []*db.Key
   342  	newLevels := map[string]dashapi.ReproLevel{}
   343  	if err := foreachBug(c, func(query *db.Query) *db.Query {
   344  		return query.Filter("Status=", BugStatusOpen)
   345  	}, func(bug *Bug, key *db.Key) error {
   346  		if len(bug.Commits) > 0 {
   347  			return nil
   348  		}
   349  		actual := ReproLevelNone
   350  		reproCrashes, _, err := queryCrashesForBug(c, key, 2)
   351  		if err != nil {
   352  			return fmt.Errorf("failed to fetch crashes with repro: %w", err)
   353  		}
   354  		for _, crash := range reproCrashes {
   355  			if crash.ReproIsRevoked {
   356  				continue
   357  			}
   358  			if crash.ReproC > 0 {
   359  				actual = ReproLevelC
   360  				break
   361  			}
   362  			if crash.ReproSyz > 0 {
   363  				actual = ReproLevelSyz
   364  			}
   365  		}
   366  		if actual != bug.HeadReproLevel {
   367  			fmt.Fprintf(w, "%v: HeadReproLevel mismatch, actual=%d db=%d\n", bugLink(bug.keyHash(c)), actual, bug.HeadReproLevel)
   368  			newLevels[bug.keyHash(c)] = actual
   369  			keys = append(keys, key)
   370  		}
   371  		return nil
   372  	}); err != nil {
   373  		return err
   374  	}
   375  	return updateBatch(c, keys, func(_ *db.Key, bug *Bug) {
   376  		newLevel, ok := newLevels[bug.keyHash(c)]
   377  		if !ok {
   378  			panic("fetched unknown bug")
   379  		}
   380  		bug.HeadReproLevel = newLevel
   381  	})
   382  }
   383  
   384  func updateBatch[T any](c context.Context, keys []*db.Key, transform func(key *db.Key, item *T)) error {
   385  	for len(keys) != 0 {
   386  		batchSize := 20
   387  		if batchSize > len(keys) {
   388  			batchSize = len(keys)
   389  		}
   390  		batchKeys := keys[:batchSize]
   391  		keys = keys[batchSize:]
   392  
   393  		tx := func(c context.Context) error {
   394  			items := make([]*T, len(batchKeys))
   395  			if err := db.GetMulti(c, batchKeys, items); err != nil {
   396  				return err
   397  			}
   398  			for i, item := range items {
   399  				transform(batchKeys[i], item)
   400  			}
   401  			_, err := db.PutMulti(c, batchKeys, items)
   402  			return err
   403  		}
   404  		if err := db.RunInTransaction(c, tx, &db.TransactionOptions{XG: true}); err != nil {
   405  			return err
   406  		}
   407  		log.Warningf(c, "updated %v bugs", len(batchKeys))
   408  	}
   409  	return nil
   410  }
   411  
   412  // Prevent warnings about dead code.
   413  var (
   414  	_ = dropNamespace
   415  	_ = updateBugReporting
   416  	_ = updateBugTitles
   417  	_ = restartFailedBisections
   418  	_ = setMissingBugFields
   419  	_ = adminSendEmail
   420  	_ = updateHeadReproLevel
   421  	_ = updateCrashPriorities
   422  )