Skip to content

Commit d246422

Browse files
committed
syz-agent: add binary
Add server for running agentic workflows as part of syzbot.
1 parent 3d1d5b6 commit d246422

File tree

3 files changed

+207
-2
lines changed

3 files changed

+207
-2
lines changed

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ ifeq ("$(TARGETOS)", "trusty")
104104
endif
105105

106106
.PHONY: all clean host target \
107-
manager executor kfuzztest ci hub \
107+
manager executor kfuzztest ci hub agent \
108108
execprog mutate prog2c trace2syz repro upgrade db \
109109
usbgen symbolize cover kconf syz-build crush \
110110
bin/syz-extract bin/syz-fmt \
@@ -172,6 +172,9 @@ ci: descriptions
172172
hub: descriptions
173173
GOOS=$(HOSTOS) GOARCH=$(HOSTARCH) $(HOSTGO) build $(GOHOSTFLAGS) -o ./bin/syz-hub github.com/google/syzkaller/syz-hub
174174

175+
agent: descriptions
176+
GOOS=$(HOSTOS) GOARCH=$(HOSTARCH) $(HOSTGO) build $(GOHOSTFLAGS) -o ./bin/syz-agent github.com/google/syzkaller/syz-agent
177+
175178
repro: descriptions
176179
GOOS=$(HOSTOS) GOARCH=$(HOSTARCH) $(HOSTGO) build $(GOHOSTFLAGS) -o ./bin/syz-repro github.com/google/syzkaller/tools/syz-repro
177180

pkg/updater/updater.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ func (upd *Updater) build(commit *vcs.Commit) error {
274274
}
275275
}
276276
// This will also generate descriptions and should go before the 'go test' below.
277-
cmd := osutil.Command(instance.MakeBin, "host", "ci")
277+
cmd := osutil.Command(instance.MakeBin, "host", "ci", "agent")
278278
cmd.Dir = upd.syzkallerDir
279279
cmd.Env = append([]string{"GOPATH=" + upd.gopathDir}, os.Environ()...)
280280
if _, err := osutil.Run(time.Hour, cmd); err != nil {

syz-agent/agent.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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 main
5+
6+
import (
7+
"context"
8+
"encoding/json"
9+
"flag"
10+
"fmt"
11+
"maps"
12+
"path/filepath"
13+
"slices"
14+
"strings"
15+
"sync"
16+
"time"
17+
18+
"github.com/google/syzkaller/dashboard/dashapi"
19+
"github.com/google/syzkaller/pkg/aflow"
20+
"github.com/google/syzkaller/pkg/aflow/flow/patching"
21+
"github.com/google/syzkaller/pkg/aflow/journal"
22+
"github.com/google/syzkaller/pkg/config"
23+
"github.com/google/syzkaller/pkg/log"
24+
"github.com/google/syzkaller/pkg/mgrconfig"
25+
"github.com/google/syzkaller/pkg/osutil"
26+
"github.com/google/syzkaller/pkg/tool"
27+
"github.com/google/syzkaller/pkg/updater"
28+
)
29+
30+
type Config struct {
31+
DashboardAddr string `json:"dashboard_addr"`
32+
DashboardClient string `json:"dashboard_client"` // Namespace-specific.
33+
DashboardKey string `json:"dashboard_key"`
34+
SyzkallerRepo string `json:"syzkaller_repo"`
35+
SyzkallerBranch string `json:"syzkaller_branch"`
36+
//Namespaces []string `json:"namespaces"`
37+
CodesearchToolBin string `json:"codesearch_tool_bin"`
38+
KernelConfig string `json:"kernel_config"`
39+
Target string `json:"target"`
40+
Image string `json:"image"`
41+
Type string `json:"type"`
42+
VM json.RawMessage `json:"vm"`
43+
}
44+
45+
func main() {
46+
var (
47+
flagConfig = flag.String("config", "", "config file")
48+
flagExitOnUpgrade = flag.Bool("exit-on-upgrade", false,
49+
"exit after a syz-ci upgrade is applied; otherwise syz-ci restarts")
50+
flagAutoUpdate = flag.Bool("autoupdate", true, "auto-update the binary (for testing)")
51+
)
52+
defer tool.Init()()
53+
log.SetName("syz-agent")
54+
if err := run(*flagConfig, *flagExitOnUpgrade, *flagAutoUpdate); err != nil {
55+
log.Fatal(err)
56+
}
57+
}
58+
59+
func run(configFile string, exitOnUpgrade, autoUpdate bool) error {
60+
cfg := &Config{
61+
SyzkallerRepo: "https://github.com/google/syzkaller.git",
62+
SyzkallerBranch: "master",
63+
}
64+
if err := config.LoadFile(configFile, cfg); err != nil {
65+
return fmt.Errorf("failed to load config: %w", err)
66+
}
67+
os, vmarch, arch, _, _, err := mgrconfig.SplitTarget(cfg.Target)
68+
if err != nil {
69+
return err
70+
}
71+
dash, err := dashapi.New(cfg.DashboardClient, cfg.DashboardAddr, cfg.DashboardKey)
72+
if err != nil {
73+
return err
74+
}
75+
buildSem := osutil.NewSemaphore(1)
76+
updater, err := updater.New(&updater.Config{
77+
ExitOnUpdate: exitOnUpgrade,
78+
BuildSem: buildSem,
79+
SyzkallerRepo: cfg.SyzkallerRepo,
80+
SyzkallerBranch: cfg.SyzkallerBranch,
81+
Targets: map[updater.Target]bool{
82+
updater.Target{
83+
OS: os,
84+
VMArch: vmarch,
85+
Arch: arch,
86+
}: true,
87+
},
88+
})
89+
if err != nil {
90+
return err
91+
}
92+
updatePending := make(chan struct{})
93+
shutdownPending := make(chan struct{})
94+
osutil.HandleInterrupts(shutdownPending)
95+
updater.UpdateOnStart(autoUpdate, updatePending, shutdownPending)
96+
97+
ctx, stop := context.WithCancel(context.Background())
98+
var wg sync.WaitGroup
99+
wg.Add(1)
100+
go func() {
101+
defer wg.Done()
102+
for {
103+
ok, err := poll(ctx, cfg, dash)
104+
if err != nil {
105+
log.Error(err)
106+
}
107+
var delay time.Duration
108+
if !ok {
109+
delay = 10 * time.Second
110+
}
111+
select {
112+
case <-ctx.Done():
113+
return
114+
case <-time.After(delay):
115+
}
116+
}
117+
}()
118+
119+
select {
120+
case <-shutdownPending:
121+
case <-updatePending:
122+
}
123+
stop()
124+
wg.Wait()
125+
126+
select {
127+
case <-shutdownPending:
128+
default:
129+
updater.UpdateAndRestart()
130+
}
131+
return nil
132+
}
133+
134+
func poll(ctx context.Context, cfg *Config, dash *dashapi.Dashboard) (bool, error) {
135+
req := &dashapi.AIJobPollReq{
136+
Workflows: slices.Collect(maps.Keys(aflow.Flows)),
137+
}
138+
resp, err := dash.AIJobPoll(req)
139+
if err != nil {
140+
return false, err
141+
}
142+
if resp.ID == "" {
143+
return false, nil
144+
}
145+
flow := aflow.Flows[resp.Workflow]
146+
if flow == nil {
147+
return false, fmt.Errorf("unsupported flow %q", resp.Workflow)
148+
}
149+
doneReq := &dashapi.AIJobDoneReq{
150+
ID: resp.ID,
151+
}
152+
switch flow.Type {
153+
case patching.Type:
154+
doneReq.Patching, err = patchingJob(ctx, cfg, dash, flow, resp)
155+
default:
156+
err = fmt.Errorf("unsupported flow type %q", flow.Type)
157+
}
158+
if err != nil {
159+
doneReq.Error = err.Error()
160+
}
161+
if err := dash.AIJobDone(doneReq); err != nil {
162+
return false, err
163+
}
164+
return true, nil
165+
}
166+
167+
func patchingJob(ctx context.Context, cfg *Config, dash *dashapi.Dashboard, flow *aflow.Flow, req *dashapi.AIJobPollResp) (
168+
*dashapi.AIPatchingResult, error) {
169+
inputs := patching.Inputs{
170+
ReproOpts: req.Patching.ReproOpts,
171+
ReproSyz: req.Patching.ReproSyz,
172+
ReproC: req.Patching.ReproC,
173+
KernelConfig: req.Patching.KernelConfig,
174+
SyzkallerCommit: req.Patching.SyzkallerCommit,
175+
CodesearchToolBin: cfg.CodesearchToolBin,
176+
Syzkaller: osutil.Abs(filepath.FromSlash("syzkaller/current")),
177+
Image: cfg.Image,
178+
Type: cfg.Type,
179+
VM: cfg.VM,
180+
}
181+
onEvent := func(ev *journal.Event) error {
182+
log.Logf(0, "%v%v", strings.Repeat(" ", ev.Nesting), ev.Description())
183+
dump, err := json.MarshalIndent(ev, "", "\t")
184+
if err != nil {
185+
panic(err)
186+
}
187+
fmt.Printf("%s\n\n", dump)
188+
return dash.AIJournal(&dashapi.AIJournalReq{
189+
JobID: req.ID,
190+
Event: ev,
191+
})
192+
}
193+
res, err := flow.Execute(ctx, false, osutil.Abs("workdir"), inputs, nil, onEvent)
194+
if err != nil {
195+
return nil, err
196+
}
197+
outputs := res.(patching.Outputs)
198+
return &dashapi.AIPatchingResult{
199+
PatchDescription: outputs.PatchDescription,
200+
PatchDiff: outputs.PatchDiff,
201+
}, nil
202+
}

0 commit comments

Comments
 (0)