aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2026-01-12 12:37:14 +0100
committerDmitry Vyukov <dvyukov@google.com>2026-01-12 17:34:20 +0000
commitd7bccd30b388650dc50996a1f81efb9698d0781b (patch)
tree1d9f49a96f4b98a6d32ec7011bb068bf6ce50bb3
parentebb1853093b2f5c87126f8d2c5a9e17a94049246 (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.go14
-rw-r--r--dashboard/app/ai_test.go1
-rw-r--r--pkg/aflow/ai/ai.go3
-rw-r--r--pkg/aflow/flow/assessment/kcsan.go2
-rw-r--r--pkg/aflow/flow/assessment/moderation.go114
-rw-r--r--pkg/report/crash/types.go4
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)