github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/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 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 := min(len(keys), 100)
   135  		if err := db.DeleteMulti(c, keys[:batch]); err != nil {
   136  			return err
   137  		}
   138  		keys = keys[batch:]
   139  	}
   140  	return nil
   141  }
   142  
   143  func restartFailedBisections(c context.Context, w http.ResponseWriter, r *http.Request) error {
   144  	if accessLevel(c, r) != AccessAdmin {
   145  		return fmt.Errorf("admin only")
   146  	}
   147  	ns := r.FormValue("ns")
   148  	if ns == "" {
   149  		return fmt.Errorf("no ns parameter")
   150  	}
   151  	var jobs []*Job
   152  	var jobKeys []*db.Key
   153  	jobKeys, err := db.NewQuery("Job").
   154  		Filter("Finished>", time.Time{}).
   155  		GetAll(c, &jobs)
   156  	if err != nil {
   157  		return fmt.Errorf("failed to query jobs: %w", err)
   158  	}
   159  	toReset := []*db.Key{}
   160  	for i, job := range jobs {
   161  		if job.Namespace != ns {
   162  			continue
   163  		}
   164  		if job.Type != JobBisectCause && job.Type != JobBisectFix {
   165  			continue
   166  		}
   167  		if job.Error == 0 {
   168  			continue
   169  		}
   170  		errorTextBytes, _, err := getText(c, textError, job.Error)
   171  		if err != nil {
   172  			return fmt.Errorf("failed to query error text: %w", err)
   173  		}
   174  		fmt.Fprintf(w, "job type %v, ns %s, finished at %s, error:%s\n========\n",
   175  			job.Type, job.Namespace, job.Finished, string(errorTextBytes))
   176  		toReset = append(toReset, jobKeys[i])
   177  	}
   178  	if r.FormValue("apply") != "yes" {
   179  		return nil
   180  	}
   181  	for idx, jobKey := range toReset {
   182  		err = invalidateBisection(c, jobKey, true)
   183  		if err != nil {
   184  			fmt.Fprintf(w, "job %v update failed: %s", idx, err)
   185  		}
   186  	}
   187  
   188  	fmt.Fprintf(w, "Done!\n")
   189  	return nil
   190  }
   191  
   192  // updateBugReporting adds missing reporting stages to bugs in a single namespace.
   193  // Use with care. There is no undo.
   194  // This can be used to migrate datastore to a new config with more reporting stages.
   195  // This functionality is intentionally not connected to any handler.
   196  // Before invoking it is recommended to stop all connected instances just in case.
   197  func updateBugReporting(c context.Context, w http.ResponseWriter, r *http.Request) error {
   198  	if accessLevel(c, r) != AccessAdmin {
   199  		return fmt.Errorf("admin only")
   200  	}
   201  	ns := r.FormValue("ns")
   202  	if ns == "" {
   203  		return fmt.Errorf("no ns parameter")
   204  	}
   205  	var bugs []*Bug
   206  	keys, err := db.NewQuery("Bug").
   207  		Filter("Namespace=", ns).
   208  		GetAll(c, &bugs)
   209  	if err != nil {
   210  		return err
   211  	}
   212  	log.Warningf(c, "fetched %v bugs for namespce %v", len(bugs), ns)
   213  	cfg := getNsConfig(c, ns)
   214  	var update []*db.Key
   215  	for i, bug := range bugs {
   216  		if len(bug.Reporting) >= len(cfg.Reporting) {
   217  			continue
   218  		}
   219  		update = append(update, keys[i])
   220  	}
   221  	return updateBatch(c, update, func(_ *db.Key, bug *Bug) {
   222  		err := bug.updateReportings(c, cfg, timeNow(c))
   223  		if err != nil {
   224  			panic(err)
   225  		}
   226  	})
   227  }
   228  
   229  // updateBugTitles adds missing MergedTitles/AltTitles to bugs.
   230  // This can be used to migrate datastore to the new scheme introduced:
   231  // by commit fd1036219797 ("dashboard/app: merge duplicate crashes").
   232  func updateBugTitles(c context.Context, w http.ResponseWriter, r *http.Request) error {
   233  	if accessLevel(c, r) != AccessAdmin {
   234  		return fmt.Errorf("admin only")
   235  	}
   236  	var keys []*db.Key
   237  	if err := foreachBug(c, nil, func(bug *Bug, key *db.Key) error {
   238  		if len(bug.MergedTitles) == 0 || len(bug.AltTitles) == 0 {
   239  			keys = append(keys, key)
   240  		}
   241  		return nil
   242  	}); err != nil {
   243  		return err
   244  	}
   245  	log.Warningf(c, "fetched %v bugs for update", len(keys))
   246  	return updateBatch(c, keys, func(_ *db.Key, bug *Bug) {
   247  		if len(bug.MergedTitles) == 0 {
   248  			bug.MergedTitles = []string{bug.Title}
   249  		}
   250  		if len(bug.AltTitles) == 0 {
   251  			bug.AltTitles = []string{bug.Title}
   252  		}
   253  	})
   254  }
   255  
   256  // updateCrashPriorities regenerates priorities for crashes.
   257  // This has become necessary after the "dashboard: support per-Manager priority" commit.
   258  // For now, the method only considers the crashes referenced from bug origin.
   259  func updateCrashPriorities(c context.Context, w http.ResponseWriter, r *http.Request) error {
   260  	if accessLevel(c, r) != AccessAdmin {
   261  		return fmt.Errorf("admin only")
   262  	}
   263  	ns := r.FormValue("ns")
   264  	if ns == "" {
   265  		return fmt.Errorf("no ns parameter")
   266  	}
   267  	bugsCount := 0
   268  	bugPerKey := map[string]*Bug{}
   269  	var crashKeys []*db.Key
   270  	if err := foreachBug(c, func(query *db.Query) *db.Query {
   271  		return query.Filter("Status=", BugStatusOpen).Filter("Namespace=", ns)
   272  	}, func(bug *Bug, key *db.Key) error {
   273  		bugsCount++
   274  		// There'll be duplicate crash IDs.
   275  		crashIDs := map[int64]struct{}{}
   276  		for _, item := range bug.TreeTests.List {
   277  			crashIDs[item.CrashID] = struct{}{}
   278  		}
   279  		for crashID := range crashIDs {
   280  			crashKeys = append(crashKeys, db.NewKey(c, "Crash", "", crashID, key))
   281  		}
   282  		bugPerKey[key.String()] = bug
   283  		return nil
   284  	}); err != nil {
   285  		return err
   286  	}
   287  	log.Warningf(c, "fetched %d bugs and %v crash keys to update", bugsCount, len(crashKeys))
   288  	return updateBatch(c, crashKeys, func(key *db.Key, crash *Crash) {
   289  		bugKey := key.Parent()
   290  		bug := bugPerKey[bugKey.String()]
   291  		build, err := loadBuild(c, ns, crash.BuildID)
   292  		if build == nil || err != nil {
   293  			panic(fmt.Sprintf("err: %s, build: %v", err, build))
   294  		}
   295  		crash.UpdateReportingPriority(c, build, bug)
   296  	})
   297  }
   298  
   299  // setMissingBugFields makes sure all Bug entity fields are present in the database.
   300  // The problem is that, in Datastore, sorting/filtering on a field will only return entries
   301  // where that field is present.
   302  func setMissingBugFields(c context.Context, w http.ResponseWriter, r *http.Request) error {
   303  	if accessLevel(c, r) != AccessAdmin {
   304  		return fmt.Errorf("admin only")
   305  	}
   306  	var keys []*db.Key
   307  	// Query everything.
   308  	err := foreachBug(c, nil, func(bug *Bug, key *db.Key) error {
   309  		keys = append(keys, key)
   310  		return nil
   311  	})
   312  	if err != nil {
   313  		return err
   314  	}
   315  	log.Warningf(c, "fetched %v bugs for update", len(keys))
   316  	// Save everything unchanged.
   317  	return updateBatch(c, keys, func(_ *db.Key, bug *Bug) {})
   318  }
   319  
   320  // adminSendEmail can be used to send an arbitrary message from the bot.
   321  func adminSendEmail(c context.Context, w http.ResponseWriter, r *http.Request) error {
   322  	if accessLevel(c, r) != AccessAdmin {
   323  		return fmt.Errorf("admin only")
   324  	}
   325  	msg := &aemail.Message{
   326  		Sender: r.FormValue("from"),
   327  		To:     []string{r.FormValue("to")},
   328  		Body:   r.FormValue("body"),
   329  	}
   330  	return sendEmail(c, msg)
   331  }
   332  
   333  func updateHeadReproLevel(c context.Context, w http.ResponseWriter, r *http.Request) error {
   334  	if accessLevel(c, r) != AccessAdmin {
   335  		return fmt.Errorf("admin only")
   336  	}
   337  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   338  	var keys []*db.Key
   339  	newLevels := map[string]dashapi.ReproLevel{}
   340  	if err := foreachBug(c, func(query *db.Query) *db.Query {
   341  		return query.Filter("Status=", BugStatusOpen)
   342  	}, func(bug *Bug, key *db.Key) error {
   343  		if len(bug.Commits) > 0 {
   344  			return nil
   345  		}
   346  		actual := ReproLevelNone
   347  		reproCrashes, _, err := queryCrashesForBug(c, key, 2)
   348  		if err != nil {
   349  			return fmt.Errorf("failed to fetch crashes with repro: %w", err)
   350  		}
   351  		for _, crash := range reproCrashes {
   352  			if crash.ReproIsRevoked {
   353  				continue
   354  			}
   355  			if crash.ReproC > 0 {
   356  				actual = ReproLevelC
   357  				break
   358  			}
   359  			if crash.ReproSyz > 0 {
   360  				actual = ReproLevelSyz
   361  			}
   362  		}
   363  		if actual != bug.HeadReproLevel {
   364  			fmt.Fprintf(w, "%v: HeadReproLevel mismatch, actual=%d db=%d\n", bugLink(bug.keyHash(c)), actual, bug.HeadReproLevel)
   365  			newLevels[bug.keyHash(c)] = actual
   366  			keys = append(keys, key)
   367  		}
   368  		return nil
   369  	}); err != nil {
   370  		return err
   371  	}
   372  	return updateBatch(c, keys, func(_ *db.Key, bug *Bug) {
   373  		newLevel, ok := newLevels[bug.keyHash(c)]
   374  		if !ok {
   375  			panic("fetched unknown bug")
   376  		}
   377  		bug.HeadReproLevel = newLevel
   378  	})
   379  }
   380  
   381  func updateBatch[T any](c context.Context, keys []*db.Key, transform func(key *db.Key, item *T)) error {
   382  	for len(keys) != 0 {
   383  		batchSize := min(len(keys), 20)
   384  		batchKeys := keys[:batchSize]
   385  		keys = keys[batchSize:]
   386  
   387  		tx := func(c context.Context) error {
   388  			items := make([]*T, len(batchKeys))
   389  			if err := db.GetMulti(c, batchKeys, items); err != nil {
   390  				return err
   391  			}
   392  			for i, item := range items {
   393  				transform(batchKeys[i], item)
   394  			}
   395  			_, err := db.PutMulti(c, batchKeys, items)
   396  			return err
   397  		}
   398  		if err := runInTransaction(c, tx, &db.TransactionOptions{XG: true}); err != nil {
   399  			return err
   400  		}
   401  		log.Warningf(c, "updated %v bugs", len(batchKeys))
   402  	}
   403  	return nil
   404  }
   405  
   406  // Prevent warnings about dead code.
   407  var (
   408  	_ = dropNamespace
   409  	_ = updateBugReporting
   410  	_ = updateBugTitles
   411  	_ = restartFailedBisections
   412  	_ = setMissingBugFields
   413  	_ = adminSendEmail
   414  	_ = updateHeadReproLevel
   415  	_ = updateCrashPriorities
   416  )