Skip to content

Commit 34a2c16

Browse files
committed
pkg/aflow: initial repro workflow implementation
1 parent fc6ad58 commit 34a2c16

File tree

8 files changed

+329
-1
lines changed

8 files changed

+329
-1
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 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+
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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
"github.com/google/syzkaller/prog"
9+
_ "github.com/google/syzkaller/sys"
10+
)
11+
12+
var SyzCompilerCheck = aflow.NewFuncAction("syz-compiler-check", compilerCheck)
13+
14+
type CompilerCheckArgs struct {
15+
CandidateSyzlang string
16+
}
17+
18+
type CompilerCheckResult struct {
19+
CompilerSuccess bool
20+
CompilerErrors string
21+
}
22+
23+
func compilerCheck(ctx *aflow.Context, args CompilerCheckArgs) (CompilerCheckResult, error) {
24+
pt, err := prog.GetTarget("linux", "amd64")
25+
if err != nil {
26+
return CompilerCheckResult{CompilerSuccess: false, CompilerErrors: err.Error()}, nil
27+
}
28+
_, err = pt.Deserialize([]byte(args.CandidateSyzlang), prog.Strict)
29+
if err != nil {
30+
return CompilerCheckResult{CompilerSuccess: false, CompilerErrors: err.Error()}, nil
31+
}
32+
return CompilerCheckResult{CompilerSuccess: true}, nil
33+
}

pkg/aflow/action/crash/reproduce.go

Lines changed: 83 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,85 @@ 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+
CandidateSyzlang 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+
ProducedCrashReport string
182+
ReproduceErrors string
183+
}
184+
185+
func reproduceSyzlang(ctx *aflow.Context, args ReproduceSyzlangArgs) (reproduceSyzlangResult, error) {
186+
imageData, err := os.ReadFile(args.Image)
187+
if err != nil {
188+
return reproduceSyzlangResult{}, err
189+
}
190+
desc := fmt.Sprintf("kernel commit %v, kernel config hash %v, image hash %v,"+
191+
" vm %v, vm config hash %v, syz repro hash %v, version 4",
192+
args.KernelCommit, hash.String(args.KernelConfig), hash.String(imageData),
193+
args.Type, hash.String(args.VM), hash.String(args.CandidateSyzlang))
194+
195+
type Cached struct {
196+
BugTitle string
197+
Report string
198+
Error string
199+
}
200+
201+
cached, err := aflow.CacheObject(ctx, "repro", desc, func() (Cached, error) {
202+
var res Cached
203+
workdir, err := ctx.TempDir()
204+
if err != nil {
205+
return res, err
206+
}
207+
208+
reproArgs := ReproduceArgs{
209+
Syzkaller: args.Syzkaller,
210+
Image: args.Image,
211+
Type: args.Type,
212+
VM: args.VM,
213+
ReproOpts: "",
214+
ReproSyz: args.CandidateSyzlang,
215+
ReproC: "",
216+
SyzkallerCommit: args.SyzkallerCommit,
217+
KernelSrc: args.KernelSrc,
218+
KernelObj: args.KernelObj,
219+
KernelCommit: args.KernelCommit,
220+
KernelConfig: args.KernelConfig,
221+
}
222+
223+
rep, buildError, err := ReproduceCrash(reproArgs, workdir)
224+
if rep != nil {
225+
res.BugTitle = rep.Title
226+
res.Report = string(rep.Report)
227+
}
228+
res.Error = buildError
229+
return res, err
230+
})
231+
232+
if err != nil {
233+
return reproduceSyzlangResult{}, err
234+
}
235+
if cached.Error != "" {
236+
return reproduceSyzlangResult{ReproduceErrors: cached.Error}, nil
237+
} else if cached.Report == "" {
238+
return reproduceSyzlangResult{ReproduceErrors: "reproducer did not crash"}, nil
239+
}
240+
return reproduceSyzlangResult{
241+
ProducedBugTitle: cached.BugTitle,
242+
ProducedCrashReport: cached.Report,
243+
}, nil
244+
}

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: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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: "RawSyzlang",
43+
TaskType: aflow.FormalReasoningTask,
44+
Instruction: reproInstruction,
45+
Prompt: reproPrompt,
46+
Tools: syzlang.Tools,
47+
},
48+
crash.SyzCompilerCheck,
49+
// If compiler succeeded, evaluate Reproduce and Compare outputs
50+
// In order to not fail early if reproduce failed or compile failed, we use an aggregator
51+
aflow.NewFuncAction("evaluate-iteration", func(ctx *aflow.Context, args struct {
52+
CompilerSuccess bool
53+
CompilerErrors string
54+
ReproduceErrors string
55+
CompareErrors string
56+
ProducedCrashReport string
57+
}) (struct{ IterationErrors string }, error) {
58+
if !args.CompilerSuccess {
59+
return struct{ IterationErrors string }{IterationErrors: args.CompilerErrors}, nil
60+
}
61+
if args.ReproduceErrors != "" {
62+
return struct{ IterationErrors string }{IterationErrors: args.ReproduceErrors}, nil
63+
}
64+
if args.CompareErrors != "" {
65+
return struct{ IterationErrors string }{IterationErrors: args.CompareErrors}, nil
66+
}
67+
return struct{ IterationErrors string }{}, nil
68+
}),
69+
crash.ReproduceSyzlang,
70+
crash.CompareCrashSignature,
71+
),
72+
While: "IterationErrors",
73+
MaxIterations: 10,
74+
},
75+
aflow.NewFuncAction("emit-result", func(ctx *aflow.Context, args struct {
76+
CandidateSyzlang string
77+
Matches bool
78+
}) (ai.ReproOutputs, error) {
79+
return ai.ReproOutputs{
80+
Syzlang: args.CandidateSyzlang,
81+
Success: args.Matches,
82+
}, nil
83+
}),
84+
),
85+
},
86+
)
87+
}
88+
89+
const reproInstruction = `
90+
You are an expert in linux kernel fuzzing. Your goal is to write a syzkaller program to trigger
91+
a specific bug. Use syzlang syntax strictly.
92+
93+
First, search for the relevant syzlang definitions using the syzlang-search tool.
94+
Then, write a candidate .syz test program. Use the syz-compiler-check tool to validate your syntax.
95+
Once compilation passes, use the crash-reproducer tool to run it in the VM.
96+
After that, use compare-crash-signature to verify if the reproduced crash matches the target crash title.
97+
98+
If previous attempts failed, pay attention to the errors and fix them.
99+
Print only the syz program that could be executed directly.
100+
`
101+
102+
const reproPrompt = `
103+
Original Crash Report:
104+
{{.CrashReport}}
105+
106+
{{if .IterationErrors}}
107+
Previous Attempt Errors:
108+
{{.IterationErrors}}
109+
{{end}}
110+
`

pkg/aflow/tool/syzlang/syzlang.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 Tools = []aflow.Tool{
16+
aflow.NewFuncTool("syzlang-search", search, `
17+
Tool provides syzlang definitions for a given subsystem (e.g., "bpf", "io_uring").
18+
`),
19+
}
20+
21+
type searchArgs struct {
22+
Subsystem string `jsonschema:"Name of the subsystem to retrieve descriptions for (e.g. bpf, ext4)."`
23+
}
24+
25+
type searchResult struct {
26+
Definitions string `jsonschema:"Syzlang definitions."`
27+
Error string `jsonschema:"Error message, if any." json:",omitempty"`
28+
}
29+
30+
func search(ctx *aflow.Context, state struct{}, args searchArgs) (searchResult, error) {
31+
if args.Subsystem == "" || args.Subsystem == "all" {
32+
return searchResult{Error: "Subsystem name is required and cannot be 'all'. " +
33+
"Please specify a specific subsystem like 'bpf' or 'ext4'."}, nil
34+
}
35+
36+
// Let's assume the current working directory of the process is syzkaller root,
37+
// because aflow tools are executed by the syzkaller manager/tools.
38+
var out strings.Builder
39+
err := filepath.WalkDir("sys/linux", func(path string, d fs.DirEntry, err error) error {
40+
if err != nil {
41+
return err
42+
}
43+
if d.IsDir() || !strings.HasSuffix(d.Name(), ".txt") {
44+
return nil
45+
}
46+
47+
name := strings.TrimSuffix(d.Name(), ".txt")
48+
if name != "sys" && name != args.Subsystem && !strings.HasPrefix(name, args.Subsystem+"_") {
49+
return nil
50+
}
51+
52+
data, err := os.ReadFile(path)
53+
if err != nil {
54+
return err
55+
}
56+
out.Write(data)
57+
out.WriteString("\n")
58+
return nil
59+
})
60+
if err != nil {
61+
return searchResult{Error: "failed to access sys/linux: " + err.Error()}, nil
62+
}
63+
return searchResult{Definitions: out.String()}, nil
64+
}

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)