diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2026-01-12 12:37:14 +0100 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2026-01-12 17:34:20 +0000 |
| commit | d7bccd30b388650dc50996a1f81efb9698d0781b (patch) | |
| tree | 1d9f49a96f4b98a6d32ec7011bb068bf6ce50bb3 | |
| parent | ebb1853093b2f5c87126f8d2c5a9e17a94049246 (diff) | |
pkg/aflow/flow/assessment: add UAF moderation workflow
Add workflow that can be used for moderation of UAF bugs (consistent/actionable reports),
such UAF bugs can be upstreammed automatically, even if they happened only once
and don't have a reproducer.
| -rw-r--r-- | dashboard/app/ai.go | 14 | ||||
| -rw-r--r-- | dashboard/app/ai_test.go | 1 | ||||
| -rw-r--r-- | pkg/aflow/ai/ai.go | 3 | ||||
| -rw-r--r-- | pkg/aflow/flow/assessment/kcsan.go | 2 | ||||
| -rw-r--r-- | pkg/aflow/flow/assessment/moderation.go | 114 | ||||
| -rw-r--r-- | pkg/report/crash/types.go | 4 |
6 files changed, 136 insertions, 2 deletions
diff --git a/dashboard/app/ai.go b/dashboard/app/ai.go index e0847ea4a..9f620384f 100644 --- a/dashboard/app/ai.go +++ b/dashboard/app/ai.go @@ -16,6 +16,7 @@ import ( "github.com/google/syzkaller/dashboard/app/aidb" "github.com/google/syzkaller/dashboard/dashapi" "github.com/google/syzkaller/pkg/aflow/ai" + "github.com/google/syzkaller/pkg/report/crash" "github.com/google/syzkaller/pkg/vcs" db "google.golang.org/appengine/v2/datastore" ) @@ -322,6 +323,7 @@ func bugJobCreate(ctx context.Context, workflow string, typ ai.WorkflowType, bug Description: bug.displayTitle(), Link: fmt.Sprintf("/bug?id=%v", bug.keyHash(ctx)), Args: spanner.NullJSON{Valid: true, Value: map[string]any{ + "BugTitle": bug.Title, "ReproOpts": string(crash.ReproOpts), "ReproSyzID": crash.ReproSyz, "ReproCID": crash.ReproC, @@ -423,11 +425,21 @@ const currentAIJobCheckSeq = 1 func workflowsForBug(bug *Bug, manual bool) map[ai.WorkflowType]bool { workflows := make(map[ai.WorkflowType]bool) - if strings.HasPrefix(bug.Title, "KCSAN: data-race") { + typ := crash.TitleToType(bug.Title) + // UAF bugs stuck in last but one reporting. + if typ.IsUAF() && len(bug.Reporting) > 1 && + bug.Reporting[len(bug.Reporting)-1].Reported.IsZero() && + !bug.Reporting[len(bug.Reporting)-2].Reported.IsZero() { + workflows[ai.WorkflowModeration] = true + } + if typ == crash.KCSANDataRace { workflows[ai.WorkflowAssessmentKCSAN] = true } if manual { // Types we don't create automatically yet, but can be created manually. + if typ.IsUAF() { + workflows[ai.WorkflowModeration] = true + } if bug.HeadReproLevel > dashapi.ReproLevelNone { workflows[ai.WorkflowPatching] = true } diff --git a/dashboard/app/ai_test.go b/dashboard/app/ai_test.go index b5e686dfa..e378e77b4 100644 --- a/dashboard/app/ai_test.go +++ b/dashboard/app/ai_test.go @@ -124,6 +124,7 @@ func TestAIJob(t *testing.T) { require.NotEqual(t, resp.ID, "") require.Equal(t, resp.Workflow, "assessment-kcsan") require.Equal(t, resp.Args, map[string]any{ + "BugTitle": "KCSAN: data-race in foo / bar", "CrashReport": "report1", "KernelRepo": "repo1", "KernelCommit": "1111111111111111111111111111111111111111", diff --git a/pkg/aflow/ai/ai.go b/pkg/aflow/ai/ai.go index 0905fe5aa..a00380ae7 100644 --- a/pkg/aflow/ai/ai.go +++ b/pkg/aflow/ai/ai.go @@ -6,7 +6,10 @@ package ai type WorkflowType string +// Note: don't change string values of these types w/o a good reason. +// They are stored in the dashboard database as strings. const ( WorkflowPatching = WorkflowType("patching") + WorkflowModeration = WorkflowType("moderation") WorkflowAssessmentKCSAN = WorkflowType("assessment-kcsan") ) diff --git a/pkg/aflow/flow/assessment/kcsan.go b/pkg/aflow/flow/assessment/kcsan.go index e29ebd5fb..2521c0a41 100644 --- a/pkg/aflow/flow/assessment/kcsan.go +++ b/pkg/aflow/flow/assessment/kcsan.go @@ -72,7 +72,7 @@ by a mutual exclusion primitive. In the final reply explain why you think the given data race is benign or is harmful. -Use the provided tools to confirm any assumptions, what variables/fields being accessed, etc. +Use the provided tools to confirm any assumptions, variables/fields being accessed, etc. In particular, don't make assumptions about the kernel source code, use codesearch tools to read the actual source code. ` diff --git a/pkg/aflow/flow/assessment/moderation.go b/pkg/aflow/flow/assessment/moderation.go new file mode 100644 index 000000000..4b78f901c --- /dev/null +++ b/pkg/aflow/flow/assessment/moderation.go @@ -0,0 +1,114 @@ +// Copyright 2026 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 assessmenet + +import ( + "fmt" + + "github.com/google/syzkaller/pkg/aflow" + "github.com/google/syzkaller/pkg/aflow/action/kernel" + "github.com/google/syzkaller/pkg/aflow/ai" + "github.com/google/syzkaller/pkg/aflow/tool/codesearcher" + "github.com/google/syzkaller/pkg/report/crash" +) + +type moderationInputs struct { + BugTitle string + CrashReport string + KernelRepo string + KernelCommit string + KernelConfig string + CodesearchToolBin string +} + +type moderationOutputs struct { + Confident bool + Actionable bool + Explanation string +} + +func init() { + aflow.Register[moderationInputs, moderationOutputs]( + ai.WorkflowModeration, + "assess if a bug report is consistent and actionable or not", + &aflow.Flow{ + Root: &aflow.Pipeline{ + Actions: []aflow.Action{ + aflow.NewFuncAction("extract-crash-type", extractCrashType), + kernel.Checkout, + kernel.Build, + codesearcher.PrepareIndex, + &aflow.LLMAgent{ + Name: "expert", + Reply: "Explanation", + Outputs: aflow.LLMOutputs[struct { + Confident bool `jsonschema:"If you are confident in the verdict of the analysis or not."` + Actionable bool `jsonschema:"If the report is actionable or not."` + }](), + Temperature: 1, + Instruction: moderationInstruction, + Prompt: moderationPrompt, + Tools: codesearcher.Tools, + }, + }, + }, + }, + ) +} + +const moderationInstruction = ` +You are an experienced Linux kernel developer tasked with determining if the given kernel bug +report is actionable or not. Actionable means that it contains enough info to root cause +the underlying bug, and that the report is self-consistent and makes sense, rather than +e.g. a one-off nonsensical crash induced by a previous memory corruption. + +{{if .IsUAF}} +The bug report is about a use-after-free bug generated by KASAN tool. +It should contain 3 stack traces: the bad memory access stack, the heap block allocation stack, +and the heap block free stack. If the report does not contain 3 stacks, it's not actionable. + +All 3 stack traces should be related to the same object type, +and usually be in the same kernel subsystem (at least leaf stack frames). +An example of an actionable and consistent report would be: first access stack relates +to an access to a field of struct Foo, allocation/free stacks relate to allocation/free +of the struct Foo. +In inconsistent/nonsensical reports an access may be to a struct Foo, but allocation +stack allocates a different structure in a different subsystem. +Look for other suspicious signals/inconsistencies that can make this report hard to +debug/understand. +{{end}} + +In the final reply explain why you think the report is self-consistent and actionable, +or why it's inconsistent and/or not actionable. + +Use the provided tools to confirm any assumptions, variables/fields being accessed, etc. +In particular, don't make assumptions about the kernel source code, +use codesearch tools to read the actual source code. +` + +const moderationPrompt = ` +The bug report is: + +{{.CrashReport}} +` + +type extractArgs struct { + BugTitle string +} + +type extractResult struct { + IsUAF bool +} + +func extractCrashType(ctx *aflow.Context, args extractArgs) (extractResult, error) { + var res extractResult + typ := crash.TitleToType(args.BugTitle) + switch { + case typ.IsUAF(): + res.IsUAF = true + default: + return res, fmt.Errorf("unsupported bug type") + } + return res, nil +} diff --git a/pkg/report/crash/types.go b/pkg/report/crash/types.go index 915f3b4ae..2a5717d95 100644 --- a/pkg/report/crash/types.go +++ b/pkg/report/crash/types.go @@ -79,6 +79,10 @@ func (t Type) IsKASAN() bool { KASANUseAfterFreeRead, KASANUseAfterFreeWrite, KASANInvalidFree, KASANUnknown}, t) } +func (t Type) IsUAF() bool { + return slices.Contains([]Type{KASANUseAfterFreeRead, KASANUseAfterFreeWrite}, t) +} + func (t Type) IsKMSAN() bool { return slices.Contains([]Type{ KMSANUninitValue, KMSANInfoLeak, KMSANUseAfterFreeRead, KMSANUnknown}, t) |
