Skip to content

Commit 60cf379

Browse files
committed
pkg/aflow: initial repro workflow implementation
1 parent fc6ad58 commit 60cf379

File tree

9 files changed

+318
-1
lines changed

9 files changed

+318
-1
lines changed

pkg/aflow/action/crash/compare.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2026 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package crash
5+
6+
import (
7+
"github.com/google/syzkaller/pkg/aflow"
8+
)
9+
10+
var CompareCrashSignature = aflow.NewFuncAction("compare-crash-signature", compareCrash)
11+
12+
type CompareCrashArgs struct {
13+
BugTitle string
14+
ProducedBugTitle string
15+
}
16+
17+
type CompareCrashResult struct {
18+
CrashSignatureMatches bool
19+
}
20+
21+
func compareCrash(ctx *aflow.Context, args CompareCrashArgs) (CompareCrashResult, error) {
22+
if args.BugTitle != args.ProducedBugTitle {
23+
return CompareCrashResult{CrashSignatureMatches: false}, nil
24+
}
25+
return CompareCrashResult{CrashSignatureMatches: true}, nil
26+
}

pkg/aflow/action/crash/reproduce.go

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func ReproduceCrash(args ReproduceArgs, workdir string) (*report.Report, string,
8686
return nil, "", err
8787
}
8888
// TODO: run multiple instances, handle TestError.Infra, and aggregate results.
89-
results, err := env.Test(1, nil, nil, []byte(args.ReproC))
89+
results, err := env.Test(1, []byte(args.ReproSyz), []byte(args.ReproOpts), []byte(args.ReproC))
9090
if err != nil {
9191
return nil, "", err
9292
}
@@ -160,3 +160,83 @@ func reproduce(ctx *aflow.Context, args ReproduceArgs) (reproduceResult, error)
160160
CrashReport: cached.Report,
161161
}, nil
162162
}
163+
164+
var ReproduceSyzlang = aflow.NewFuncAction("crash-reproducer", reproduceSyzlang)
165+
166+
type ReproduceSyzlangArgs struct {
167+
Syzkaller string
168+
Image string
169+
Type string
170+
VM json.RawMessage
171+
Syzlang string
172+
SyzkallerCommit string
173+
KernelSrc string
174+
KernelObj string
175+
KernelCommit string
176+
KernelConfig string
177+
}
178+
179+
type reproduceSyzlangResult struct {
180+
ProducedBugTitle string
181+
ReproduceErrors string
182+
}
183+
184+
func reproduceSyzlang(ctx *aflow.Context, args ReproduceSyzlangArgs) (reproduceSyzlangResult, error) {
185+
imageData, err := os.ReadFile(args.Image)
186+
if err != nil {
187+
return reproduceSyzlangResult{}, err
188+
}
189+
desc := fmt.Sprintf("kernel commit %v, kernel config hash %v, image hash %v,"+
190+
" vm %v, vm config hash %v, syz repro hash %v, version 4",
191+
args.KernelCommit, hash.String(args.KernelConfig), hash.String(imageData),
192+
args.Type, hash.String(args.VM), hash.String(args.Syzlang))
193+
194+
type Cached struct {
195+
BugTitle string
196+
Report string
197+
Error string
198+
}
199+
200+
cached, err := aflow.CacheObject(ctx, "repro", desc, func() (Cached, error) {
201+
var res Cached
202+
workdir, err := ctx.TempDir()
203+
if err != nil {
204+
return res, err
205+
}
206+
207+
reproArgs := ReproduceArgs{
208+
Syzkaller: args.Syzkaller,
209+
Image: args.Image,
210+
Type: args.Type,
211+
VM: args.VM,
212+
ReproOpts: "",
213+
ReproSyz: args.Syzlang,
214+
ReproC: "",
215+
SyzkallerCommit: args.SyzkallerCommit,
216+
KernelSrc: args.KernelSrc,
217+
KernelObj: args.KernelObj,
218+
KernelCommit: args.KernelCommit,
219+
KernelConfig: args.KernelConfig,
220+
}
221+
222+
rep, buildError, err := ReproduceCrash(reproArgs, workdir)
223+
if rep != nil {
224+
res.BugTitle = rep.Title
225+
res.Report = string(rep.Report)
226+
}
227+
res.Error = buildError
228+
return res, err
229+
})
230+
231+
if err != nil {
232+
return reproduceSyzlangResult{}, err
233+
}
234+
if cached.Error != "" {
235+
return reproduceSyzlangResult{ReproduceErrors: cached.Error}, nil
236+
} else if cached.Report == "" {
237+
return reproduceSyzlangResult{ReproduceErrors: "reproducer did not crash"}, nil
238+
}
239+
return reproduceSyzlangResult{
240+
ProducedBugTitle: cached.BugTitle,
241+
}, nil
242+
}

pkg/aflow/ai/ai.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,8 @@ type ModerationOutputs struct {
4545
Actionable bool
4646
Explanation string
4747
}
48+
49+
type ReproOutputs struct {
50+
Syzlang string
51+
Success bool
52+
}

pkg/aflow/flow/flows.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ package flow
66
import (
77
_ "github.com/google/syzkaller/pkg/aflow/flow/assessment"
88
_ "github.com/google/syzkaller/pkg/aflow/flow/patching"
9+
_ "github.com/google/syzkaller/pkg/aflow/flow/repro"
910
)

pkg/aflow/flow/repro/repro.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright 2026 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package repro
5+
6+
import (
7+
"encoding/json"
8+
9+
"github.com/google/syzkaller/pkg/aflow"
10+
"github.com/google/syzkaller/pkg/aflow/action/crash"
11+
"github.com/google/syzkaller/pkg/aflow/action/kernel"
12+
"github.com/google/syzkaller/pkg/aflow/ai"
13+
"github.com/google/syzkaller/pkg/aflow/tool/syzlang"
14+
)
15+
16+
type ReproInputs struct {
17+
BugTitle string
18+
CrashReport string
19+
KernelConfig string
20+
KernelRepo string
21+
KernelCommit string
22+
Syzkaller string
23+
SyzkallerCommit string
24+
Image string
25+
Type string
26+
VM json.RawMessage
27+
}
28+
29+
func init() {
30+
aflow.Register[ReproInputs, ai.ReproOutputs](
31+
ai.WorkflowRepro,
32+
"reproduce a kernel crash and generate a syzlang program",
33+
&aflow.Flow{
34+
Root: aflow.Pipeline(
35+
kernel.Checkout,
36+
kernel.Build,
37+
&aflow.DoWhile{
38+
Do: aflow.Pipeline(
39+
&aflow.LLMAgent{
40+
Name: "crash-reproducer",
41+
Model: aflow.BestExpensiveModel,
42+
Reply: "Syzlang",
43+
TaskType: aflow.FormalReasoningTask,
44+
Instruction: reproInstruction,
45+
Prompt: reproPrompt,
46+
Tools: syzlang.Tools,
47+
},
48+
crash.ReproduceSyzlang,
49+
crash.CompareCrashSignature,
50+
// If compiler succeeded, evaluate Reproduce and Compare outputs
51+
// In order to not fail early if reproduce failed or compile failed, we use an aggregator
52+
aflow.NewFuncAction("evaluate-iteration", func(ctx *aflow.Context, args struct {
53+
CrashSignatureMatches bool
54+
ReproduceErrors string
55+
}) (struct {
56+
TryHarder string
57+
Success bool
58+
}, error) {
59+
if !args.CrashSignatureMatches {
60+
return struct {
61+
TryHarder string
62+
Success bool
63+
}{TryHarder: "yes: " + args.ReproduceErrors, Success: false}, nil
64+
}
65+
return struct {
66+
TryHarder string
67+
Success bool
68+
}{Success: true}, nil
69+
}),
70+
),
71+
While: "TryHarder",
72+
MaxIterations: 10,
73+
},
74+
),
75+
},
76+
)
77+
}
78+
79+
const reproInstruction = `
80+
You are an expert in linux kernel fuzzing. Your goal is to write a syzkaller program to trigger
81+
a specific bug. Use syzlang syntax strictly.
82+
83+
First, search for the relevant syzlang definitions using the syzlang-search tool.
84+
Then, write a candidate .syz test program. Use the syz-compiler-check tool to validate your syntax.
85+
Once compilation passes, use the crash-reproducer tool to run it in the VM.
86+
After that, use compare-crash-signature to verify if the reproduced crash matches the target crash title.
87+
88+
If previous attempts failed, pay attention to the errors and fix them.
89+
Print only the syz program that could be executed directly.
90+
`
91+
92+
const reproPrompt = `
93+
Original Crash Report:
94+
{{.CrashReport}}
95+
`

pkg/aflow/tool/syzlang/compiler.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2026 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package syzlang
5+
6+
import (
7+
"github.com/google/syzkaller/pkg/aflow"
8+
"github.com/google/syzkaller/prog"
9+
_ "github.com/google/syzkaller/sys"
10+
)
11+
12+
var ToolSyzCompilerCheck = aflow.NewFuncTool("syz-compiler-check", compilerCheck, `
13+
Tool is used to verify the syz program correctness.
14+
`)
15+
16+
type CompilerCheckArgs struct {
17+
CandidateSyzlang string `jsonschema:"Syz program to verify."`
18+
}
19+
20+
type CompilerCheckResult struct {
21+
CompilerSuccess bool `jsonschema:"Success signal."`
22+
CompilerErrors string `jsonschema:"Error description on failure."`
23+
}
24+
25+
func compilerCheck(ctx *aflow.Context, state struct{}, args CompilerCheckArgs) (CompilerCheckResult, error) {
26+
pt, err := prog.GetTarget("linux", "amd64")
27+
if err != nil {
28+
return CompilerCheckResult{CompilerSuccess: false, CompilerErrors: err.Error()}, nil
29+
}
30+
_, err = pt.Deserialize([]byte(args.CandidateSyzlang), prog.Strict)
31+
if err != nil {
32+
return CompilerCheckResult{CompilerSuccess: false, CompilerErrors: err.Error()}, nil
33+
}
34+
return CompilerCheckResult{CompilerSuccess: true}, nil
35+
}

pkg/aflow/tool/syzlang/syzlang.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2026 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package syzlang
5+
6+
import (
7+
"io/fs"
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
12+
"github.com/google/syzkaller/pkg/aflow"
13+
)
14+
15+
var ToolSyzLangSearch = aflow.NewFuncTool("syzlang-search", search, `
16+
Tool provides syzlang definitions for a given subsystem (e.g., "bpf", "io_uring").`)
17+
18+
type searchArgs struct {
19+
Subsystem string `jsonschema:"Name of the subsystem to retrieve descriptions for (e.g. bpf, ext4)."`
20+
}
21+
22+
type searchResult struct {
23+
Definitions string `jsonschema:"Syzlang definitions."`
24+
Error string `jsonschema:"Error message, if any." json:",omitempty"`
25+
}
26+
27+
func search(ctx *aflow.Context, state struct{}, args searchArgs) (searchResult, error) {
28+
if args.Subsystem == "" || args.Subsystem == "all" {
29+
return searchResult{Error: "Subsystem name is required and cannot be 'all'. " +
30+
"Please specify a specific subsystem like 'bpf' or 'ext4'."}, nil
31+
}
32+
33+
// Let's assume the current working directory of the process is syzkaller root,
34+
// because aflow tools are executed by the syzkaller manager/tools.
35+
var out strings.Builder
36+
err := filepath.WalkDir("sys/linux", func(path string, d fs.DirEntry, err error) error {
37+
if err != nil {
38+
return err
39+
}
40+
if d.IsDir() || !strings.HasSuffix(d.Name(), ".txt") {
41+
return nil
42+
}
43+
44+
name := strings.TrimSuffix(d.Name(), ".txt")
45+
if name != "sys" && name != args.Subsystem && !strings.HasPrefix(name, args.Subsystem+"_") {
46+
return nil
47+
}
48+
49+
data, err := os.ReadFile(path)
50+
if err != nil {
51+
return err
52+
}
53+
out.Write(data)
54+
out.WriteString("\n")
55+
return nil
56+
})
57+
if err != nil {
58+
return searchResult{Error: "failed to access sys/linux: " + err.Error()}, nil
59+
}
60+
return searchResult{Definitions: out.String()}, nil
61+
}

pkg/aflow/tool/syzlang/tools.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Copyright 2026 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package syzlang
5+
6+
import "github.com/google/syzkaller/pkg/aflow"
7+
8+
var Tools = []aflow.Tool{ToolSyzLangSearch, ToolSyzCompilerCheck}

tools/syz-aflow/aflow.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ func downloadBug(id, inputFile, token string) error {
131131
"KernelCommit": crash["kernel-source-commit"],
132132
}
133133

134+
if title, ok := info["title"].(string); ok {
135+
inputs["BugTitle"] = title
136+
} else if title, ok := crash["title"].(string); ok {
137+
inputs["BugTitle"] = title
138+
}
139+
134140
fetchText := func(key string) (string, error) {
135141
path, ok := crash[key].(string)
136142
if !ok || path == "" {

0 commit comments

Comments
 (0)