Skip to content

Commit 4d636df

Browse files
committed
pkg/aflow: initial repro workflow implementation
1 parent cbc1a5e commit 4d636df

File tree

12 files changed

+404
-6
lines changed

12 files changed

+404
-6
lines changed

pkg/aflow/action/crash/compare.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2025 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+
Matches bool
19+
CompareErrors string
20+
}
21+
22+
func compareCrash(ctx *aflow.Context, args CompareCrashArgs) (CompareCrashResult, error) {
23+
if args.BugTitle != args.ProducedBugTitle {
24+
return CompareCrashResult{Matches: false, CompareErrors: "Crash signature did not match target"}, nil
25+
}
26+
return CompareCrashResult{Matches: true}, nil
27+
}

pkg/aflow/action/crash/compiler.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2025 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+
"regexp"
8+
"strings"
9+
10+
"github.com/google/syzkaller/pkg/aflow"
11+
"github.com/google/syzkaller/prog"
12+
_ "github.com/google/syzkaller/sys"
13+
)
14+
15+
var SyzCompilerCheck = aflow.NewFuncAction("syz-compiler-check", compilerCheck)
16+
17+
type CompilerCheckArgs struct {
18+
CandidateSyzlang string
19+
}
20+
21+
type CompilerCheckResult struct {
22+
CompilerSuccess bool
23+
CompilerErrors string
24+
}
25+
26+
func compilerCheck(ctx *aflow.Context, args CompilerCheckArgs) (CompilerCheckResult, error) {
27+
pt, err := prog.GetTarget("linux", "amd64")
28+
if err != nil {
29+
return CompilerCheckResult{CompilerSuccess: false, CompilerErrors: err.Error()}, nil
30+
}
31+
_, err = pt.Deserialize([]byte(args.CandidateSyzlang), prog.Strict)
32+
if err != nil {
33+
return CompilerCheckResult{CompilerSuccess: false, CompilerErrors: err.Error()}, nil
34+
}
35+
return CompilerCheckResult{CompilerSuccess: true}, nil
36+
}
37+
38+
var ExtractSyzCode = aflow.NewFuncAction("extract-syz-code", extractSyzCode)
39+
40+
type ExtractSyzCodeArgs struct {
41+
RawSyzlang string
42+
}
43+
44+
type ExtractSyzCodeResult struct {
45+
CandidateSyzlang string
46+
}
47+
48+
func extractSyzCode(ctx *aflow.Context, args ExtractSyzCodeArgs) (ExtractSyzCodeResult, error) {
49+
code := args.RawSyzlang
50+
// If the code is inside a markdown block, extract it.
51+
if match := reSyzBlock.FindStringSubmatch(code); match != nil {
52+
code = match[1]
53+
} else if match := reCodeBlock.FindStringSubmatch(code); match != nil {
54+
code = match[1]
55+
}
56+
code = strings.TrimSpace(code)
57+
return ExtractSyzCodeResult{CandidateSyzlang: code}, nil
58+
}
59+
60+
var (
61+
reSyzBlock = regexp.MustCompile("(?s)```(?:ex-?)?syz(?:kaller|lang)?\\n(.*?)```")
62+
reCodeBlock = regexp.MustCompile("(?s)```\\n(.*?)```")
63+
)

pkg/aflow/action/crash/reproduce.go

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,18 @@ func ReproduceCrash(args ReproduceArgs, workdir string) (*report.Report, string,
8585
if err != nil {
8686
return nil, "", err
8787
}
88+
var reproSyz, reproOpts, reproC []byte
89+
if args.ReproSyz != "" {
90+
reproSyz = []byte(args.ReproSyz)
91+
}
92+
if args.ReproOpts != "" {
93+
reproOpts = []byte(args.ReproOpts)
94+
}
95+
if args.ReproC != "" {
96+
reproC = []byte(args.ReproC)
97+
}
8898
// TODO: run multiple instances, handle TestError.Infra, and aggregate results.
89-
results, err := env.Test(1, nil, nil, []byte(args.ReproC))
99+
results, err := env.Test(1, reproSyz, reproOpts, reproC)
90100
if err != nil {
91101
return nil, "", err
92102
}
@@ -160,3 +170,85 @@ func reproduce(ctx *aflow.Context, args ReproduceArgs) (reproduceResult, error)
160170
CrashReport: cached.Report,
161171
}, nil
162172
}
173+
174+
var ReproduceSyzlang = aflow.NewFuncAction("crash-reproducer", reproduceSyzlang)
175+
176+
type ReproduceSyzlangArgs struct {
177+
Syzkaller string
178+
Image string
179+
Type string
180+
VM json.RawMessage
181+
CandidateSyzlang string
182+
SyzkallerCommit string
183+
KernelSrc string
184+
KernelObj string
185+
KernelCommit string
186+
KernelConfig string
187+
}
188+
189+
type reproduceSyzlangResult struct {
190+
ProducedBugTitle string
191+
ProducedCrashReport string
192+
ReproduceErrors string
193+
}
194+
195+
func reproduceSyzlang(ctx *aflow.Context, args ReproduceSyzlangArgs) (reproduceSyzlangResult, error) {
196+
imageData, err := os.ReadFile(args.Image)
197+
if err != nil {
198+
return reproduceSyzlangResult{}, err
199+
}
200+
desc := fmt.Sprintf("kernel commit %v, kernel config hash %v, image hash %v,"+
201+
" vm %v, vm config hash %v, syz repro hash %v, version 4",
202+
args.KernelCommit, hash.String(args.KernelConfig), hash.String(imageData),
203+
args.Type, hash.String(args.VM), hash.String(args.CandidateSyzlang))
204+
205+
type Cached struct {
206+
BugTitle string
207+
Report string
208+
Error string
209+
}
210+
211+
cached, err := aflow.CacheObject(ctx, "repro", desc, func() (Cached, error) {
212+
var res Cached
213+
workdir, err := ctx.TempDir()
214+
if err != nil {
215+
return res, err
216+
}
217+
218+
reproArgs := ReproduceArgs{
219+
Syzkaller: args.Syzkaller,
220+
Image: args.Image,
221+
Type: args.Type,
222+
VM: args.VM,
223+
ReproOpts: "",
224+
ReproSyz: args.CandidateSyzlang,
225+
ReproC: "",
226+
SyzkallerCommit: args.SyzkallerCommit,
227+
KernelSrc: args.KernelSrc,
228+
KernelObj: args.KernelObj,
229+
KernelCommit: args.KernelCommit,
230+
KernelConfig: args.KernelConfig,
231+
}
232+
233+
rep, buildError, err := ReproduceCrash(reproArgs, workdir)
234+
if rep != nil {
235+
res.BugTitle = rep.Title
236+
res.Report = string(rep.Report)
237+
}
238+
res.Error = buildError
239+
return res, err
240+
})
241+
242+
if err != nil {
243+
return reproduceSyzlangResult{}, err
244+
}
245+
if cached.Error != "" {
246+
return reproduceSyzlangResult{ReproduceErrors: cached.Error}, nil
247+
} else if cached.Report == "" {
248+
return reproduceSyzlangResult{ReproduceErrors: "reproducer did not crash"}, nil
249+
}
250+
return reproduceSyzlangResult{
251+
ProducedBugTitle: cached.BugTitle,
252+
ProducedCrashReport: cached.Report,
253+
}, nil
254+
}
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2025 syzkaller project authors. All rights reserved.
22
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
33

4-
package patching
4+
package kernel
55

66
import (
77
"errors"
@@ -14,11 +14,12 @@ import (
1414
"github.com/google/syzkaller/pkg/aflow"
1515
"github.com/google/syzkaller/pkg/aflow/action/kernel"
1616
"github.com/google/syzkaller/pkg/aflow/ai"
17+
"github.com/google/syzkaller/pkg/aflow/common"
1718
"github.com/google/syzkaller/pkg/osutil"
1819
"github.com/google/syzkaller/pkg/vcs"
1920
)
2021

21-
var baseCommitPicker = aflow.NewFuncAction("base-commit-picker", pickBaseCommit)
22+
var BaseCommitPicker = aflow.NewFuncAction("base-commit-picker", pickBaseCommit)
2223

2324
type baseCommitArgs struct {
2425
// Can be used to override the selected base commit (for manual testing).
@@ -60,7 +61,7 @@ func pickBaseCommit(ctx *aflow.Context, args baseCommitArgs) (baseCommitResult,
6061
return res, nil
6162
}
6263

63-
err := kernel.UseLinuxRepo(ctx, func(_ string, repo vcs.Repo) error {
64+
err := UseLinuxRepo(ctx, func(_ string, repo vcs.Repo) error {
6465
head, err := repo.Poll(baseRepo, baseBranch)
6566
if err != nil {
6667
return err
@@ -79,7 +80,7 @@ func pickBaseCommit(ctx *aflow.Context, args baseCommitArgs) (baseCommitResult,
7980
return res, err
8081
}
8182

82-
var getMaintainers = aflow.NewFuncAction("get-maintainers", maintainers)
83+
var GetMaintainers = aflow.NewFuncAction("get-maintainers", maintainers)
8384

8485
type maintainersArgs struct {
8586
KernelSrc string

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/common/recipient.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright 2025 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 common
5+
6+
type Recipient struct {
7+
Name string
8+
Email string
9+
To bool // whether the recipient should be on the To or Cc line
10+
}

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/patching/patching.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func createPatchingFlow(name string, summaryWindow int) *aflow.Flow {
4040
return &aflow.Flow{
4141
Name: name,
4242
Root: aflow.Pipeline(
43-
baseCommitPicker,
43+
kernel.BaseCommitPicker,
4444
kernel.Checkout,
4545
kernel.Build,
4646
// Ensure we can reproduce the crash (and the build boots).

pkg/aflow/flow/repro/repro.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright 2025 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+
Syzkaller string
21+
SyzkallerCommit string
22+
Image string
23+
Type string
24+
VM json.RawMessage
25+
26+
// Optional; used for testing.
27+
FixedBaseCommit string
28+
FixedRepository string
29+
}
30+
31+
func init() {
32+
aflow.Register[ReproInputs, ai.ReproOutputs](
33+
ai.WorkflowRepro,
34+
"reproduce a kernel crash and generate a syzlang program",
35+
&aflow.Flow{
36+
Root: aflow.Pipeline(
37+
kernel.BaseCommitPicker,
38+
kernel.Checkout,
39+
kernel.Build,
40+
&aflow.DoWhile{
41+
Do: aflow.Pipeline(
42+
&aflow.LLMAgent{
43+
Name: "expert",
44+
Model: aflow.BestExpensiveModel,
45+
Reply: "RawSyzlang",
46+
Temperature: 1,
47+
Instruction: reproInstruction,
48+
Prompt: reproPrompt,
49+
Tools: syzlang.Tools,
50+
},
51+
crash.ExtractSyzCode,
52+
crash.SyzCompilerCheck,
53+
// If compiler succeeded, evaluate Reproduce and Compare outputs
54+
// In order to not fail early if reproduce failed or compile failed, we use an aggregator
55+
aflow.NewFuncAction("evaluate-iteration", func(ctx *aflow.Context, args struct {
56+
CompilerSuccess bool
57+
CompilerErrors string
58+
ReproduceErrors string
59+
CompareErrors string
60+
ProducedCrashReport string
61+
}) (struct{ IterationErrors string }, error) {
62+
if !args.CompilerSuccess {
63+
return struct{ IterationErrors string }{IterationErrors: args.CompilerErrors}, nil
64+
}
65+
if args.ReproduceErrors != "" {
66+
return struct{ IterationErrors string }{IterationErrors: args.ReproduceErrors}, nil
67+
}
68+
if args.CompareErrors != "" {
69+
return struct{ IterationErrors string }{IterationErrors: args.CompareErrors}, nil
70+
}
71+
return struct{ IterationErrors string }{}, nil
72+
}),
73+
crash.ReproduceSyzlang,
74+
crash.CompareCrashSignature,
75+
),
76+
While: "IterationErrors",
77+
MaxIterations: 10,
78+
},
79+
aflow.NewFuncAction("emit-result", func(ctx *aflow.Context, args struct {
80+
CandidateSyzlang string
81+
Matches bool
82+
}) (ai.ReproOutputs, error) {
83+
return ai.ReproOutputs{
84+
Syzlang: args.CandidateSyzlang,
85+
Success: args.Matches,
86+
}, nil
87+
}),
88+
),
89+
},
90+
)
91+
}
92+
93+
const reproInstruction = `
94+
You are an expert kernel security researcher. Your goal is to write a syzkaller program to trigger
95+
a specific bug. Use syzlang syntax strictly.
96+
97+
First, search for the relevant syzlang definitions using the syzlang-search tool.
98+
Then, write a candidate .syz test program. Use the syz-compiler-check tool to validate your syntax.
99+
Once compilation passes, use the crash-reproducer tool to run it in the VM.
100+
After that, use compare-crash-signature to verify if the reproduced crash matches the target crash title.
101+
102+
If previous attempts failed, pay attention to the errors and fix them.
103+
104+
Iterate until you find a working reproducer or exhaust your attempts.
105+
`
106+
107+
const reproPrompt = `
108+
Bug Title: {{.BugTitle}}
109+
Original Crash Report:
110+
{{.CrashReport}}
111+
112+
{{if .IterationErrors}}
113+
Previous Attempt Errors:
114+
{{.IterationErrors}}
115+
{{end}}
116+
`

0 commit comments

Comments
 (0)