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 )