// Copyright 2023 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 ( "context" "fmt" "reflect" "sort" "time" "github.com/google/syzkaller/pkg/debugtracer" "github.com/google/syzkaller/pkg/subsystem" db "google.golang.org/appengine/v2/datastore" "google.golang.org/appengine/v2/log" ) // reassignBugSubsystems is expected to be periodically called to refresh old automatic // subsystem assignments. func reassignBugSubsystems(c context.Context, ns string, count int) error { service := getNsConfig(c, ns).Subsystems.Service if service == nil { return nil } bugs, keys, err := bugsToUpdateSubsystems(c, ns, count) if err != nil { return err } log.Infof(c, "updating subsystems for %d bugs in %#v", len(keys), ns) rev := service.Revision for i, bugKey := range keys { if bugs[i].hasUserSubsystems() { // It might be that the user-set subsystem no longer exists. // For now let's just log an error in this case. checkOutdatedSubsystems(c, service, bugs[i]) // If we don't set the latst revision, we'll have to update this // bug over and over again. err = updateBugSubsystems(c, bugKey, nil, updateRevision(rev)) } else { var list []*subsystem.Subsystem list, err = inferSubsystems(c, bugs[i], bugKey, &debugtracer.NullTracer{}) if err != nil { return fmt.Errorf("failed to infer subsystems: %w", err) } err = updateBugSubsystems(c, bugKey, list, autoInference(rev)) } if err != nil { return fmt.Errorf("failed to save subsystems: %w", err) } } return nil } func bugsToUpdateSubsystems(c context.Context, ns string, count int) ([]*Bug, []*db.Key, error) { now := timeNow(c) rev := getSubsystemService(c, ns).Revision queries := []*db.Query{ // If revision has been updated, first update open bugs. db.NewQuery("Bug"). Filter("Namespace=", ns). Filter("Status=", BugStatusOpen). Filter("SubsystemsRev<", rev), // The next priority is the regular update of open bugs. db.NewQuery("Bug"). Filter("Namespace=", ns). Filter("Status=", BugStatusOpen). Filter("SubsystemsTime<", now.Add(-openBugsUpdateTime)), // Then let's consider the update of closed bugs. db.NewQuery("Bug"). Filter("Namespace=", ns). Filter("Status=", BugStatusFixed). Filter("SubsystemsRev<", rev), // And, at the end, everything else. db.NewQuery("Bug"). Filter("Namespace=", ns). Filter("SubsystemsRev<", rev), } var bugs []*Bug var keys []*db.Key for i, query := range queries { if count <= 0 { break } var tmpBugs []*Bug tmpKeys, err := query.Limit(count).GetAll(c, &tmpBugs) if err != nil { return nil, nil, fmt.Errorf("query %d failed: %w", i, err) } bugs = append(bugs, tmpBugs...) keys = append(keys, tmpKeys...) count -= len(tmpKeys) } return bugs, keys, nil } func checkOutdatedSubsystems(c context.Context, service *subsystem.Service, bug *Bug) { for _, item := range bug.LabelValues(SubsystemLabel) { if service.ByName(item.Value) == nil { log.Errorf(c, "ns=%s bug=%s subsystem %s no longer exists", bug.Namespace, bug.Title, item.Value) } } } type ( autoInference int updateRevision int ) func updateBugSubsystems(c context.Context, bugKey *db.Key, list []*subsystem.Subsystem, info any) error { now := timeNow(c) return updateSingleBug(c, bugKey, func(bug *Bug) error { switch v := info.(type) { case autoInference: logSubsystemChange(c, bug, list) bug.SetAutoSubsystems(c, list, now, int(v)) case updateRevision: bug.SubsystemsRev = int(v) bug.SubsystemsTime = now } return nil }) } func logSubsystemChange(c context.Context, bug *Bug, new []*subsystem.Subsystem) { var oldNames, newNames []string for _, item := range bug.LabelValues(SubsystemLabel) { oldNames = append(oldNames, item.Value) } for _, item := range new { newNames = append(newNames, item.Name) } sort.Strings(oldNames) sort.Strings(newNames) if !reflect.DeepEqual(oldNames, newNames) { log.Infof(c, "bug %s: subsystems set from %v to %v", bug.keyHash(c), oldNames, newNames) } } const ( // We load the top crashesForInference crashes to determine the bug subsystem(s). crashesForInference = 7 // How often we update open bugs. openBugsUpdateTime = time.Hour * 24 * 30 ) // inferSubsystems determines the best yet possible estimate of the bug's subsystems. func inferSubsystems(c context.Context, bug *Bug, bugKey *db.Key, tracer debugtracer.DebugTracer) ([]*subsystem.Subsystem, error) { service := getSubsystemService(c, bug.Namespace) if service == nil { // There's nothing we can do. return nil, nil } dbCrashes, dbCrashKeys, err := queryCrashesForBug(c, bugKey, crashesForInference) if err != nil { return nil, err } crashes := []*subsystem.Crash{} for i, dbCrash := range dbCrashes { crash := &subsystem.Crash{} if len(dbCrash.ReportElements.GuiltyFiles) > 0 { // For now we anyway only store one. crash.GuiltyPath = dbCrash.ReportElements.GuiltyFiles[0] } if dbCrash.ReproSyz != 0 { crash.SyzRepro, _, err = getText(c, textReproSyz, dbCrash.ReproSyz) if err != nil { return nil, fmt.Errorf("failed to load syz repro for %s: %w", dbCrashKeys[i], err) } } crashes = append(crashes, crash) } return service.TracedExtract(crashes, tracer), nil } // subsystemMaintainers queries the list of emails to send the bug to. func subsystemMaintainers(c context.Context, ns, subsystemName string) []string { service := getNsConfig(c, ns).Subsystems.Service if service == nil { return nil } item := service.ByName(subsystemName) if item == nil { return nil } return item.Emails() } func getSubsystemService(c context.Context, ns string) *subsystem.Service { return getNsConfig(c, ns).Subsystems.Service } func subsystemListURL(c context.Context, ns string) string { if getNsConfig(c, ns).Subsystems.Service == nil { return "" } return fmt.Sprintf("%v/%v/subsystems?all=true", appURL(c), ns) }