blob: 293f6748188adfaa31f658e82af333158f9da584 [file] [log] [blame]
// 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 := getNsConfig(c, ns).Subsystems.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 := getSubsystemRevision(c, ns)
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 interface{}) 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 getSubsystemRevision(c context.Context, ns string) int {
return getNsConfig(c, ns).Subsystems.Revision
}
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)
}