Skip to content

Commit 31e2117

Browse files
committed
syz-agent: add agentic server
Add server for running agentic workflows as part of syzbot. The architecture and use are similar to that of syz-ci.
1 parent 44dae31 commit 31e2117

File tree

3 files changed

+209
-2
lines changed

3 files changed

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

0 commit comments

Comments
 (0)