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 )