Skip to content

Commit aa24b85

Browse files
committed
pkg/aflow: initial repro workflow implementation
1 parent 090b8b0 commit aa24b85

File tree

13 files changed

+490
-21
lines changed

13 files changed

+490
-21
lines changed

pkg/aflow/action/crash/reproduce.go

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/google/syzkaller/pkg/aflow"
1414
"github.com/google/syzkaller/pkg/build"
15+
"github.com/google/syzkaller/pkg/csource"
1516
"github.com/google/syzkaller/pkg/hash"
1617
"github.com/google/syzkaller/pkg/instance"
1718
"github.com/google/syzkaller/pkg/mgrconfig"
@@ -22,7 +23,7 @@ import (
2223
// Reproduce action tries to reproduce a crash with the given reproducer,
2324
// and outputs the resulting crash report.
2425
// If the reproducer does not trigger a crash, action fails.
25-
var Reproduce = aflow.NewFuncAction("crash-reproducer", reproduce)
26+
var Reproduce = aflow.NewFuncAction("crash-reproducer", ReproduceActionFunc)
2627

2728
type ReproduceArgs struct {
2829
Syzkaller string
@@ -38,9 +39,9 @@ type ReproduceArgs struct {
3839
KernelConfig string
3940
}
4041

41-
type reproduceResult struct {
42-
BugTitle string
43-
CrashReport string
42+
type ReproduceResult struct {
43+
ReproducedBugTitle string
44+
ReproducedCrashReport string
4445
}
4546

4647
// ReproduceCrash tests reproducer and returns:
@@ -84,8 +85,13 @@ func ReproduceCrash(args ReproduceArgs, workdir string) (*report.Report, string,
8485
if err != nil {
8586
return nil, "", err
8687
}
88+
reproOpts := csource.DefaultOpts(cfg).Serialize()
89+
if args.ReproOpts != "" {
90+
reproOpts = []byte(args.ReproOpts)
91+
}
92+
8793
// TODO: run multiple instances, handle TestError.Infra, and aggregate results.
88-
results, err := env.Test(1, nil, nil, []byte(args.ReproC))
94+
results, err := env.Test(1, []byte(args.ReproSyz), reproOpts, []byte(args.ReproC))
8995
if err != nil {
9096
return nil, "", err
9197
}
@@ -118,15 +124,16 @@ func parseTestError(err *instance.TestError) (*report.Report, string, error) {
118124
return nil, fmt.Sprintf("%v: %v\n%s", what, err.Title, extraInfo), nil
119125
}
120126

121-
func reproduce(ctx *aflow.Context, args ReproduceArgs) (reproduceResult, error) {
127+
func ReproduceActionFunc(ctx *aflow.Context, args ReproduceArgs) (ReproduceResult, error) {
122128
imageData, err := os.ReadFile(args.Image)
123129
if err != nil {
124-
return reproduceResult{}, err
130+
return ReproduceResult{}, err
125131
}
126132
desc := fmt.Sprintf("kernel commit %v, kernel config hash %v, image hash %v,"+
127-
" vm %v, vm config hash %v, C repro hash %v, version 3",
133+
" vm %v, vm config hash %v, C repro hash %v, syz repro hash %v, opts hash %v, version 4",
128134
args.KernelCommit, hash.String(args.KernelConfig), hash.String(imageData),
129-
args.Type, hash.String(args.VM), hash.String(args.ReproC))
135+
args.Type, hash.String(args.VM), hash.String(args.ReproC),
136+
hash.String([]byte(args.ReproSyz)), hash.String([]byte(args.ReproOpts)))
130137
type Cached struct {
131138
BugTitle string
132139
Report string
@@ -147,15 +154,15 @@ func reproduce(ctx *aflow.Context, args ReproduceArgs) (reproduceResult, error)
147154
return res, err
148155
})
149156
if err != nil {
150-
return reproduceResult{}, err
157+
return ReproduceResult{}, err
151158
}
152159
if cached.Error != "" {
153-
return reproduceResult{}, errors.New(cached.Error)
160+
return ReproduceResult{}, errors.New(cached.Error)
154161
} else if cached.Report == "" {
155-
return reproduceResult{}, aflow.FlowError(errors.New("reproducer did not crash"))
162+
return ReproduceResult{}, aflow.FlowError(errors.New("reproducer did not crash"))
156163
}
157-
return reproduceResult{
158-
BugTitle: cached.BugTitle,
159-
CrashReport: cached.Report,
164+
return ReproduceResult{
165+
ReproducedBugTitle: cached.BugTitle,
166+
ReproducedCrashReport: cached.Report,
160167
}, nil
161168
}

pkg/aflow/ai/ai.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,9 @@ type ModerationOutputs struct {
4545
Actionable bool
4646
Explanation string
4747
}
48+
49+
type ReproOutputs struct {
50+
ReproSyz string
51+
CrashSignatureMatches bool
52+
ReproducedCrashReport string
53+
}

pkg/aflow/compare.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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 aflow
5+
6+
import (
7+
"fmt"
8+
"reflect"
9+
10+
"github.com/google/syzkaller/pkg/aflow/trajectory"
11+
)
12+
13+
// Compare is a helper action that compares two string arguments for equality.
14+
// The result of the comparison is written to the state under resultVar.
15+
func Compare(arg1, arg2, resultVar string) Action {
16+
return &CompareAction{
17+
Arg1: arg1,
18+
Arg2: arg2,
19+
ResultVar: resultVar,
20+
}
21+
}
22+
23+
// CompareAction performs the comparison. It is exported so tools can use its Run method.
24+
type CompareAction struct {
25+
Arg1 string
26+
Arg2 string
27+
ResultVar string
28+
}
29+
30+
func (a *CompareAction) Run(ctx *Context, v1, v2 string) (bool, error) {
31+
span := &trajectory.Span{
32+
Type: trajectory.SpanAction,
33+
Name: "compare",
34+
}
35+
if err := ctx.startSpan(span); err != nil {
36+
return false, err
37+
}
38+
39+
res := v1 == v2
40+
41+
span.Results = map[string]any{"result": res}
42+
return res, ctx.finishSpan(span, nil)
43+
}
44+
45+
func (a *CompareAction) execute(ctx *Context) error {
46+
v1, ok := ctx.state[a.Arg1].(string)
47+
if !ok {
48+
return fmt.Errorf("compare missing string argument %q", a.Arg1)
49+
}
50+
v2, ok := ctx.state[a.Arg2].(string)
51+
if !ok {
52+
return fmt.Errorf("compare missing string argument %q", a.Arg2)
53+
}
54+
55+
res, err := a.Run(ctx, v1, v2)
56+
if err != nil {
57+
return err
58+
}
59+
60+
ctx.state[a.ResultVar] = res
61+
return nil
62+
}
63+
64+
func (a *CompareAction) verify(ctx *verifyContext) {
65+
ctx.requireNotEmpty("compare", "Arg1", a.Arg1)
66+
ctx.requireNotEmpty("compare", "Arg2", a.Arg2)
67+
ctx.requireNotEmpty("compare", "ResultVar", a.ResultVar)
68+
69+
ctx.requireInput("compare", a.Arg1, reflect.TypeFor[string]())
70+
ctx.requireInput("compare", a.Arg2, reflect.TypeFor[string]())
71+
ctx.provideOutput("compare", a.ResultVar, reflect.TypeFor[bool]())
72+
}

pkg/aflow/docs/crash-to-repro.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Proposal: AI-Driven Reproducer Generation Workflow
2+
3+
## 1. Objective
4+
To design a new AI agent workflow within the syzkaller `aflow` framework (`pkg/aflow/flow/repro/repro.go`) that
5+
automatically converts a kernel crash report (and associated execution logs) into a reliable
6+
syzkaller reproducer (syzlang).
7+
The agent will leverage existing syzlang descriptions (`sys/linux/*.txt`) to ensure the generated reproducers conform
8+
to syzkaller's type system and API constraints.
9+
10+
First step is to generate the MVP that works and allows to parallelize the work. If some tool or feature may be
11+
postponed it is better to postpone it.
12+
13+
## 2. High-Level Architecture & Agent Loop
14+
15+
The workflow will operate as an iterative feedback loop, utilizing the LLM's reasoning capabilities to bridge the gap
16+
between a crash signature and a functional syzlang program.
17+
18+
1. **Context Initialization:** Ingest the kernel crash log with stack trace,
19+
target kernel information (`.config`, `kernel repo`, `kernel commit`) and
20+
the raw execution log leading up to the crash (if available from the fuzzing instance).
21+
Core dump and kcov traces will be of great help when available.
22+
* MVP will get all the available information from syzbot dashboard. Bug ID is the input.
23+
2. **Subsystem Analysis:** Identify the vulnerable subsystem (e.g., `io_uring`, `bpf`, `ext4`) based on the stack trace.
24+
* It will be needed to support requests w/o subsustem name. It is not needed for syzbot.
25+
* MVP gets subsystem information from dashboard.
26+
3. **Syzlang Contextualization:** Query the syzlang descriptions to extract the relevant syscall signatures, structs,
27+
and valid flags for the identified subsystem.
28+
* MVP passes `syzlang-search` tools (`ListDescriptions`, `GetDescriptions`) to the LLM agent, allowing it to lookup exact descriptions dynamically.
29+
4. **Draft Generation:** The LLM generates an initial candidate `.syz` reproducer.
30+
5. **Execution & Verification:** Compile and run the candidate against an instrumented kernel VM.
31+
* MVP LLMAgent uses `syz-compiler-check` and `crash-reproducer` tools to verify and execute directly in a loop.
32+
* Note: The generated crash may be different from the original one (e.g. different stack trace, but same root cause).
33+
* We need to verify that the produced crash is "very close" to the original one (e.g. same function, same type).
34+
6. **Iterative Refinement:** If the crash does not reproduce, or if there is a syzlang compilation error, the agent
35+
analyzes the failure output, tweaks the arguments/syscall sequence, and tries again (up to a defined maximum
36+
iteration limit). This iteration logic is fully offloaded to the LLM agent instruction capabilities.
37+
38+
## 3. Required Framework Extensions
39+
40+
To achieve this, the `aflow` framework will need new tools and actions specifically tailored for syzlang manipulation
41+
and program execution.
42+
43+
### A. New Tools (`pkg/aflow/tool`)
44+
The MVP introduced several critical tools for the LLM agent to iterate effectively:
45+
* `syzlang-search` tools (`ListDescriptions`, `GetDescriptions`): Extracted `syzlang` definition lookup into exact, granular tools for the LLM.
46+
* `syz-compiler-check`: Validates the LLM-generated `.syz` program syntax relying on `prog.Target.Deserialize(...)`.
47+
* *Input:* Raw syzlang text.
48+
* *Output:* Success along with the strictly formatted canonical syzlang program serialized as a string, or a list of syntax/type errors.
49+
* `crash-reproducer`: Tool wrapper around `crash.Reproduce` allowing the LLM agent to trigger VM executions on the fly.
50+
* `compare-crash-signature`: Tool wrapper testing if the reproduced bug title matches the original.
51+
52+
### B. Actions (`pkg/aflow/action`)
53+
The core MVP workflow (`repro.go`) implements a linear pipeline combining kernel building actions with reasoning and testing modules:
54+
* `kernel.Checkout` and `kernel.Build`: Ensure the vulnerable kernel image is ready.
55+
* `aflow.LLMAgent`: Configured as `crash-repro-finder`, taking responsibility for the iterative *Generate -> Compile -> Execute -> Compare* loop using the newly defined `pkg/aflow/tool` set.
56+
* `crash.Reproduce`: Pipeline action running the ultimately verified `.syz` code inside the test VM.
57+
* `aflow.Compare`: A generic variable comparison helper (refactored from `CompareCrashSignature`) confirming if the produced crash directly matches the target bug title.
58+
59+
## 4. Implementation Plan
60+
61+
### Phase 1: Tooling & Infrastructure (Foundation)
62+
* **Implement `SyzlangSearch` tool:** Parse the AST of `sys/linux/` and expose a search interface to the agent.
63+
* **Reuse `crash.Reproduce` action:** Reuse the logic from `pkg/aflow/action/crash` so the agent can trigger executions inside the isolated
64+
test VMs already managed by `aflow`'s checkout/build actions. Modify it if necessary.
65+
66+
### Phase 2: Prompt Engineering & Context Management
67+
For MVP we don't care about the Context Window Optimization.
68+
* **System Prompt:** Define the persona.
69+
(e.g., *"You are an expert kernel security researcher. Your goal is to write a syzkaller program to trigger
70+
a specific bug. Use syzlang syntax strictly."*)
71+
* **Context Window Optimization:** Kernel logs and syzlang files can be large.
72+
Implement truncation and selective inclusion for dmesg and syzlang structs to avoid blowing out the token limit.
73+
74+
### Phase 3: Workflow Implementation (`pkg/aflow/flow/repro/repro.go`)
75+
* Setup is a linear pipeline orchestrating `Checkout` -> `Build` -> `LLMAgent` -> `Reproduce` -> `Compare`.
76+
* Implement the iterative loop: The loop structure (*Generate -> Compile -> Execute -> Evaluate -> Refine*) is abstracted and delegated entirely to the `LLMAgent`'s instructions, taking advantage of syzkaller tool execution.
77+
* Implement exit conditions: Success (matching crash signature produced or tool exit rules), handled seamlessly.
78+
79+
### Phase 4: Evaluation & Syzbot Integration
80+
* Test the workflow against historical syzbot bugs to measure the agent's success rate and iteration average.
81+
* Note: We don't need the known reproducers to measure success because the crash point is defined by the call stack.
82+
* Success is defined as triggering a crash "very close" to the original one (same root cause).
83+
* Deploy as an experimental job type on `syzbot.org/upstream/ai`.
84+
85+
## 5. Resolved Design Decisions
86+
* **`syz-manager` MCP mode vs separate runner:** Created a dedicated `syz-aflow` command-line tool (`tools/syz-aflow`) to invoke local workflows using JSON context inputs avoiding the complexity of modifying `syz-manager` directly.
87+
* **Program verification logic:** Reused existing parsing directly via `prog.GetTarget("linux", "amd64").Deserialize(...)` embedded inside the `syz-compiler-check` tool, keeping verification reliable and fast. The tool also serializes the parsed program to return its canonical representation for downstream analysis.

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: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ able to write a fix for the bug based on your explanation. Include all relevant
109109
into the response: function/struct/field/etc names, code snippets, line numbers,
110110
macro/enum values, etc.
111111
112-
{{if titleIsKASANNullDeref .BugTitle}}
112+
{{if titleIsKASANNullDeref .ReproducedBugTitle}}
113113
Note: under KASAN NULL-derefs on the source level don't happen around the actual 0 address,
114114
they happen on the KASAN shadow memory around address dfff800000000000 or dffffc0000000000.
115115
Don't be confused by that. Look for the like at the top of the report that tells
@@ -120,7 +120,7 @@ the access address and size.
120120
const debuggingPrompt = `
121121
The crash is:
122122
123-
{{.CrashReport}}
123+
{{.ReproducedCrashReport}}
124124
`
125125

126126
const patchInstruction = `
@@ -150,7 +150,7 @@ and if they need to be updated to handle new post-conditions. For example, if yo
150150
a function that previously never returned a NULL, return NULL, consider if callers
151151
need to be updated to handle NULL return value.
152152
153-
{{if titleIsWarning .BugTitle}}
153+
{{if titleIsWarning .ReproducedBugTitle}}
154154
If you will end up removing the WARN_ON macro because the condition can legitimately happen,
155155
add a pr_err call that logs that the unlikely condition has happened. The pr_err message
156156
must not include "WARNING" string.
@@ -160,7 +160,7 @@ must not include "WARNING" string.
160160
const patchPrompt = `
161161
The crash that corresponds to the bug is:
162162
163-
{{.CrashReport}}
163+
{{.ReproducedCrashReport}}
164164
165165
The explanation of the root cause of the bug is:
166166
@@ -208,7 +208,7 @@ The rest of the description must be word-wrapped at 72 characters.
208208
const descriptionPrompt = `
209209
The crash that corresponds to the bug is:
210210
211-
{{.CrashReport}}
211+
{{.ReproducedCrashReport}}
212212
213213
The explanation of the root cause of the bug is:
214214
@@ -228,7 +228,7 @@ are specified, letter capitalization, style, etc.
228228
229229
{{.RecentCommits}}
230230
231-
{{if titleIsWarning .BugTitle}}
231+
{{if titleIsWarning .ReproducedBugTitle}}
232232
If the patch removes the WARN_ON macro, refer to the fact that WARN_ON
233233
must not be used for conditions that can legitimately happen, and that pr_err
234234
should be used instead if necessary.

pkg/aflow/flow/repro/repro.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
toolcrash "github.com/google/syzkaller/pkg/aflow/tool/crash"
14+
"github.com/google/syzkaller/pkg/aflow/tool/syzlang"
15+
)
16+
17+
type ReproInputs struct {
18+
BugTitle string
19+
CrashReport string
20+
KernelConfig string
21+
KernelRepo string
22+
KernelCommit string
23+
Syzkaller string
24+
Image string
25+
Type string
26+
VM json.RawMessage
27+
28+
// We don't use them. Needed to use crash.Reproduce.
29+
ReproOpts string
30+
ReproC string
31+
}
32+
33+
func init() {
34+
aflow.Register[ReproInputs, ai.ReproOutputs](
35+
ai.WorkflowRepro,
36+
"reproduce a kernel crash and generate a syzlang program",
37+
&aflow.Flow{
38+
Root: aflow.Pipeline(
39+
kernel.Checkout,
40+
kernel.Build,
41+
&aflow.LLMAgent{
42+
Name: "crash-repro-finder",
43+
Model: aflow.BestExpensiveModel,
44+
Reply: "ReproSyz",
45+
TaskType: aflow.FormalReasoningTask,
46+
Instruction: reproInstruction,
47+
Prompt: reproPrompt,
48+
Tools: append(syzlang.Tools, toolcrash.ReproduceTool, toolcrash.CompareCrashTool),
49+
},
50+
crash.Reproduce,
51+
aflow.Compare("BugTitle", "ReproducedBugTitle", "CrashSignatureMatches"),
52+
),
53+
},
54+
)
55+
}
56+
57+
const reproInstruction = `
58+
You are an expert in linux kernel fuzzing. Your goal is to write a syzkaller program to trigger
59+
a specific bug.
60+
61+
First, search for the relevant syzlang definitions using the syzlang-search tool.
62+
Then, write a candidate .syz test program. Use the syz-compiler-check tool to validate your syntax.
63+
Once compilation passes, use the crash-reproducer tool to run it in the VM.
64+
After that, use compare-crash-signature to verify if the reproduced crash matches the target crash title.
65+
66+
If previous attempts failed, pay attention to the errors and fix them.
67+
Print only the syz program that could be executed directly, without backticks.
68+
`
69+
70+
const reproPrompt = `
71+
Original Crash Report:
72+
{{.CrashReport}}
73+
`

0 commit comments

Comments
 (0)