diff options
Diffstat (limited to 'dashboard/app')
| -rw-r--r-- | dashboard/app/ai.go | 87 | ||||
| -rw-r--r-- | dashboard/app/ai_test.go | 57 | ||||
| -rw-r--r-- | dashboard/app/entities_datastore.go | 13 | ||||
| -rw-r--r-- | dashboard/app/label.go | 21 | ||||
| -rw-r--r-- | dashboard/app/reporting_email.go | 2 | ||||
| -rw-r--r-- | dashboard/app/tree.go | 4 |
6 files changed, 173 insertions, 11 deletions
diff --git a/dashboard/app/ai.go b/dashboard/app/ai.go index 4ea52bd7e..29bfc38d1 100644 --- a/dashboard/app/ai.go +++ b/dashboard/app/ai.go @@ -4,6 +4,7 @@ package main import ( + "bytes" "context" "encoding/json" "fmt" @@ -118,7 +119,7 @@ func handleAIJobPage(ctx context.Context, w http.ResponseWriter, r *http.Request default: job.Correct = spanner.NullBool{} } - if err := aidb.UpdateJob(ctx, job); err != nil { + if err := aiJobUpdate(ctx, job); err != nil { return err } } @@ -284,10 +285,92 @@ func apiAIJobDone(ctx context.Context, req *dashapi.AIJobDoneReq) (any, error) { if len(req.Results) != 0 { job.Results = spanner.NullJSON{Value: req.Results, Valid: true} } - err = aidb.UpdateJob(ctx, job) + err = aiJobUpdate(ctx, job) return nil, err } +func aiJobUpdate(ctx context.Context, job *aidb.Job) error { + if err := aidb.UpdateJob(ctx, job); err != nil { + return err + } + if !job.BugID.Valid || !job.Finished.Valid || job.Error != "" { + return nil + } + bug, err := loadBug(ctx, job.BugID.StringVal) + if err != nil { + return err + } + labelType, labelValue, labelAdd, err := aiBugLabel(job) + if err != nil || labelType == EmptyLabel { + return err + } + label := BugLabel{ + Label: labelType, + Value: labelValue, + Link: job.ID, + } + labelSet := makeLabelSet(ctx, bug) + return updateSingleBug(ctx, bug.key(ctx), func(bug *Bug) error { + if bug.HasUserLabel(labelType) { + return nil + } + if labelAdd { + return bug.SetLabels(labelSet, []BugLabel{label}) + } + bug.UnsetLabels(labelType) + return nil + }) +} + +func aiBugLabel(job *aidb.Job) (typ BugLabelType, value string, set bool, err0 error) { + switch job.Type { + case ai.WorkflowAssessmentKCSAN: + // For now we require a manual correctness check, + // later we may apply some labels w/o the manual check. + if !job.Correct.Valid { + return + } + if !job.Correct.Bool { + return RaceLabel, "", false, nil + } + res, err := castJobResults[ai.AssessmentKCSANOutputs](job) + if err != nil { + err0 = err + return + } + if !res.Confident { + return + } + if res.Benign { + return RaceLabel, BenignRace, true, nil + } + return RaceLabel, HarmfulRace, true, nil + } + return +} + +func castJobResults[T any](job *aidb.Job) (T, error) { + var res T + raw, ok := job.Results.Value.(map[string]any) + if !ok || !job.Results.Valid { + return res, fmt.Errorf("finished job %v %v does not have results", job.Type, job.ID) + } + // Database may store older versions of the output structs. + // It's not possible to automatically handle all possible changes to the structs. + // For now we just parse in some way. Later when we start changing output structs, + // we may need to reconsider and use more careful parsing. + data, err := json.Marshal(raw) + if err != nil { + return res, err + } + dec := json.NewDecoder(bytes.NewReader(data)) + dec.DisallowUnknownFields() + if err := dec.Decode(&res); err != nil { + return res, fmt.Errorf("failed to unmarshal %T: %w", res, err) + } + return res, nil +} + func apiAITrajectoryLog(ctx context.Context, req *dashapi.AITrajectoryReq) (any, error) { err := aidb.StoreTrajectorySpan(ctx, req.JobID, req.Span) return nil, err diff --git a/dashboard/app/ai_test.go b/dashboard/app/ai_test.go index 86ede7901..bd3935b18 100644 --- a/dashboard/app/ai_test.go +++ b/dashboard/app/ai_test.go @@ -4,10 +4,12 @@ package main import ( + "fmt" "testing" "time" "github.com/google/syzkaller/dashboard/dashapi" + "github.com/google/syzkaller/pkg/aflow/ai" "github.com/google/syzkaller/pkg/aflow/trajectory" "github.com/google/syzkaller/prog" "github.com/stretchr/testify/require" @@ -198,3 +200,58 @@ func TestAIJob(t *testing.T) { }, })) } + +func TestAIAssessmentKCSAN(t *testing.T) { + c := NewSpannerCtx(t) + defer c.Close() + + build := testBuild(1) + c.aiClient.UploadBuild(build) + crash := testCrash(build, 1) + crash.Title = "KCSAN: data-race in foo / bar" + c.aiClient.ReportCrash(crash) + extID := c.aiClient.pollEmailExtID() + + resp, err := c.aiClient.AIJobPoll(&dashapi.AIJobPollReq{ + CodeRevision: prog.GitRevision, + LLMModel: "smarty", + Workflows: []dashapi.AIWorkflow{ + {Type: ai.WorkflowAssessmentKCSAN, Name: string(ai.WorkflowAssessmentKCSAN)}, + }, + }) + require.NoError(t, err) + require.Equal(t, resp.Workflow, string(ai.WorkflowAssessmentKCSAN)) + + _, err = c.GET(fmt.Sprintf("/ai_job?id=%v", resp.ID)) + require.NoError(t, err) + + // Since the job is not completed, setting correctness must fail. + _, err = c.GET(fmt.Sprintf("/ai_job?id=%v&correct=%v", resp.ID, aiCorrectnessCorrect)) + require.Error(t, err) + + require.NoError(t, c.aiClient.AIJobDone(&dashapi.AIJobDoneReq{ + ID: resp.ID, + Results: map[string]any{ + "Confident": true, + "Benign": true, + "Explanation": "I don't care about races.", + }, + })) + + // Now setting correctness must not fail. + _, err = c.GET(fmt.Sprintf("/ai_job?id=%v&correct=%v", resp.ID, aiCorrectnessCorrect)) + require.NoError(t, err) + + bug, _, _ := c.loadBug(extID) + labels := bug.LabelValues(RaceLabel) + require.Len(t, labels, 1) + require.Equal(t, labels[0].Value, BenignRace) + + // Re-mark the result as incorrect, this should remove the label. + _, err = c.GET(fmt.Sprintf("/ai_job?id=%v&correct=%v", resp.ID, aiCorrectnessIncorrect)) + require.NoError(t, err) + + bug, _, _ = c.loadBug(extID) + labels = bug.LabelValues(RaceLabel) + require.Len(t, labels, 0) +} diff --git a/dashboard/app/entities_datastore.go b/dashboard/app/entities_datastore.go index 370cf3ceb..5a204d5d2 100644 --- a/dashboard/app/entities_datastore.go +++ b/dashboard/app/entities_datastore.go @@ -168,7 +168,7 @@ type BugLabel struct { // The email of the user who manually set this subsystem tag. // If empty, the label was set automatically. SetBy string - // Link to the message. + // Link to the message, or AI job ID for automatic labels. Link string } @@ -193,7 +193,7 @@ func (bug *Bug) SetAutoSubsystems(c context.Context, list []*subsystem.Subsystem for _, item := range list { objects = append(objects, BugLabel{Label: SubsystemLabel, Value: item.Name}) } - bug.SetLabels(makeLabelSet(c, bug.Namespace), objects) + bug.SetLabels(makeLabelSet(c, bug), objects) } func updateSingleBug(c context.Context, bugKey *db.Key, transform func(*Bug) error) error { @@ -915,6 +915,15 @@ func bugKeyHash(c context.Context, ns, title string, seq int64) string { return hash.String([]byte(fmt.Sprintf("%v-%v-%v-%v", getNsConfig(c, ns).Key, ns, title, seq))) } +func loadBug(c context.Context, bugHash string) (*Bug, error) { + bug := new(Bug) + bugKey := db.NewKey(c, "Bug", bugHash, 0, nil) + if err := db.Get(c, bugKey, bug); err != nil { + return nil, fmt.Errorf("failed to load bug by hash %q: %w", bugHash, err) + } + return bug, nil +} + func loadSimilarBugs(c context.Context, bug *Bug) ([]*Bug, error) { domain := getNsConfig(c, bug.Namespace).SimilarityDomain dedup := make(map[string]bool) diff --git a/dashboard/app/label.go b/dashboard/app/label.go index 807951f48..de874cf50 100644 --- a/dashboard/app/label.go +++ b/dashboard/app/label.go @@ -8,6 +8,8 @@ import ( "fmt" "sort" "strings" + + "github.com/google/syzkaller/pkg/report/crash" ) const ( @@ -17,6 +19,7 @@ const ( NoRemindersLabel BugLabelType = "no-reminders" OriginLabel BugLabelType = "origin" MissingBackportLabel BugLabelType = "missing-backport" + RaceLabel BugLabelType = "race" ) type BugPrio string @@ -27,11 +30,16 @@ const ( HighPrioBug BugPrio = "high" ) +const ( + BenignRace = "benign" + HarmfulRace = "harmful" +) + type oneOf []string type subsetOf []string type trueFalse struct{} -func makeLabelSet(c context.Context, ns string) *labelSet { +func makeLabelSet(c context.Context, bug *Bug) *labelSet { ret := map[BugLabelType]any{ PriorityLabel: oneOf([]string{ string(LowPrioBug), @@ -41,7 +49,12 @@ func makeLabelSet(c context.Context, ns string) *labelSet { NoRemindersLabel: trueFalse{}, MissingBackportLabel: trueFalse{}, } - service := getNsConfig(c, ns).Subsystems.Service + typ := crash.TitleToType(bug.Title) + if typ == crash.KCSANDataRace { + ret[RaceLabel] = oneOf([]string{BenignRace, HarmfulRace}) + } + cfg := getNsConfig(c, bug.Namespace) + service := cfg.Subsystems.Service if service != nil { names := []string{} for _, item := range service.List() { @@ -51,7 +64,7 @@ func makeLabelSet(c context.Context, ns string) *labelSet { } originLabels := []string{} - for _, repo := range getNsConfig(c, ns).Repos { + for _, repo := range cfg.Repos { if repo.LabelIntroduced != "" { originLabels = append(originLabels, repo.LabelIntroduced) } @@ -66,7 +79,7 @@ func makeLabelSet(c context.Context, ns string) *labelSet { return &labelSet{ c: c, - ns: ns, + ns: bug.Namespace, labels: ret, } } diff --git a/dashboard/app/reporting_email.go b/dashboard/app/reporting_email.go index 236a95857..301da9ccc 100644 --- a/dashboard/app/reporting_email.go +++ b/dashboard/app/reporting_email.go @@ -973,7 +973,7 @@ Please contact the bot's maintainers.` func handleSetCommand(c context.Context, bug *Bug, msg *email.Email, command *email.SingleCommand) string { - labelSet := makeLabelSet(c, bug.Namespace) + labelSet := makeLabelSet(c, bug) match := setCmdRe.FindStringSubmatch(command.Args) if match == nil { diff --git a/dashboard/app/tree.go b/dashboard/app/tree.go index b6261e289..2103f78a1 100644 --- a/dashboard/app/tree.go +++ b/dashboard/app/tree.go @@ -223,7 +223,7 @@ func (ctx *bugTreeContext) setOriginLabels() pollTreeJobResult { for _, label := range allLabels { labels = append(labels, BugLabel{Label: OriginLabel, Value: label}) } - ctx.bug.SetLabels(makeLabelSet(ctx.c, ctx.bug.Namespace), labels) + ctx.bug.SetLabels(makeLabelSet(ctx.c, ctx.bug), labels) return pollResultSkip{} } @@ -330,7 +330,7 @@ func (ctx *bugTreeContext) missingBackports() pollTreeJobResult { } ctx.bug.UnsetLabels(MissingBackportLabel) if resultDone.Crashed { - ctx.bug.SetLabels(makeLabelSet(ctx.c, ctx.bug.Namespace), []BugLabel{ + ctx.bug.SetLabels(makeLabelSet(ctx.c, ctx.bug), []BugLabel{ {Label: MissingBackportLabel}, }) } |
