Skip to content

Commit f1386b5

Browse files
committed
syz-cluster: collect information about base crashes
Track base crashes for (commit hash, config, arch) tuples.
1 parent 0b9605c commit f1386b5

File tree

9 files changed

+272
-10
lines changed

9 files changed

+272
-10
lines changed

syz-cluster/pkg/api/client.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,24 @@ func (client Client) UploadSession(ctx context.Context, req *NewSession) (*Uploa
114114
return postJSON[NewSession, UploadSessionResp](ctx, client.baseURL+"/sessions/upload", req)
115115
}
116116

117+
type BaseFindingInfo struct {
118+
BuildID string `json:"buildID"`
119+
Title string `json:"title"`
120+
}
121+
122+
func (client Client) UploadBaseFinding(ctx context.Context, req *BaseFindingInfo) error {
123+
_, err := postJSON[BaseFindingInfo, any](ctx, client.baseURL+"/base_findings/upload", req)
124+
return err
125+
}
126+
127+
type BaseFindingStatus struct {
128+
Observed bool `json:"observed"`
129+
}
130+
131+
func (client Client) BaseFindingStatus(ctx context.Context, req *BaseFindingInfo) (*BaseFindingStatus, error) {
132+
return postJSON[BaseFindingInfo, BaseFindingStatus](ctx, client.baseURL+"/base_findings/status", req)
133+
}
134+
117135
const requestTimeout = time.Minute
118136

119137
func finishRequest[Resp any](httpReq *http.Request) (*Resp, error) {

syz-cluster/pkg/controller/api.go

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,22 @@ import (
1616
)
1717

1818
type APIServer struct {
19-
seriesService *service.SeriesService
20-
sessionService *service.SessionService
21-
buildService *service.BuildService
22-
testService *service.SessionTestService
23-
findingService *service.FindingService
19+
seriesService *service.SeriesService
20+
sessionService *service.SessionService
21+
buildService *service.BuildService
22+
testService *service.SessionTestService
23+
findingService *service.FindingService
24+
baseFindingService *service.BaseFindingService
2425
}
2526

2627
func NewAPIServer(env *app.AppEnvironment) *APIServer {
2728
return &APIServer{
28-
seriesService: service.NewSeriesService(env),
29-
sessionService: service.NewSessionService(env),
30-
buildService: service.NewBuildService(env),
31-
testService: service.NewSessionTestService(env),
32-
findingService: service.NewFindingService(env),
29+
seriesService: service.NewSeriesService(env),
30+
sessionService: service.NewSessionService(env),
31+
buildService: service.NewBuildService(env),
32+
testService: service.NewSessionTestService(env),
33+
findingService: service.NewFindingService(env),
34+
baseFindingService: service.NewBaseFindingService(env),
3335
}
3436
}
3537

@@ -46,6 +48,8 @@ func (c APIServer) Mux() *http.ServeMux {
4648
mux.HandleFunc("/tests/upload_artifacts", c.uploadTestArtifact)
4749
mux.HandleFunc("/tests/upload", c.uploadTest)
4850
mux.HandleFunc("/trees", c.getTrees)
51+
mux.HandleFunc("/base_findings/upload", c.uploadBaseFinding)
52+
mux.HandleFunc("/base_findings/status", c.baseFindingStatus)
4953
return mux
5054
}
5155

@@ -206,3 +210,32 @@ func (c APIServer) getTrees(w http.ResponseWriter, r *http.Request) {
206210
FuzzConfigs: api.FuzzConfigs,
207211
})
208212
}
213+
214+
func (c APIServer) uploadBaseFinding(w http.ResponseWriter, r *http.Request) {
215+
req := api.ParseJSON[api.BaseFindingInfo](w, r)
216+
if req == nil {
217+
return
218+
}
219+
err := c.baseFindingService.Upload(r.Context(), req)
220+
if errors.Is(err, service.ErrBuildNotFound) {
221+
http.Error(w, fmt.Sprint(err), http.StatusNotFound)
222+
return
223+
} else if err != nil {
224+
http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
225+
return
226+
}
227+
api.ReplyJSON[interface{}](w, nil)
228+
}
229+
230+
func (c APIServer) baseFindingStatus(w http.ResponseWriter, r *http.Request) {
231+
req := api.ParseJSON[api.BaseFindingInfo](w, r)
232+
if req == nil {
233+
return
234+
}
235+
resp, err := c.baseFindingService.Status(r.Context(), req)
236+
if err != nil {
237+
http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
238+
return
239+
}
240+
api.ReplyJSON[*api.BaseFindingStatus](w, resp)
241+
}

syz-cluster/pkg/controller/api_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,46 @@ func TestAPIUploadTestArtifacts(t *testing.T) {
139139
assert.NoError(t, err)
140140
}
141141

142+
func TestAPIBaseFindings(t *testing.T) {
143+
env, ctx := app.TestEnvironment(t)
144+
client := TestServer(t, env)
145+
buildResp := UploadTestBuild(t, ctx, client, testBuild)
146+
147+
err := client.UploadBaseFinding(ctx, &api.BaseFindingInfo{
148+
BuildID: buildResp.ID,
149+
Title: "title 1",
150+
})
151+
assert.NoError(t, err)
152+
153+
// Let's upload a different build for the same revision.
154+
buildResp2 := UploadTestBuild(t, ctx, client, testBuild)
155+
assert.NotEqual(t, buildResp.ID, buildResp2.ID)
156+
157+
resp, err := client.BaseFindingStatus(ctx, &api.BaseFindingInfo{
158+
BuildID: buildResp2.ID,
159+
Title: "title 1",
160+
})
161+
assert.NoError(t, err)
162+
assert.True(t, resp.Observed)
163+
164+
t.Run("unseen title", func(t *testing.T) {
165+
resp, err := client.BaseFindingStatus(ctx, &api.BaseFindingInfo{
166+
BuildID: buildResp2.ID,
167+
Title: "title 2",
168+
})
169+
assert.NoError(t, err)
170+
assert.False(t, resp.Observed)
171+
})
172+
173+
t.Run("invalid build id", func(t *testing.T) {
174+
_, err := client.BaseFindingStatus(ctx, &api.BaseFindingInfo{
175+
BuildID: "unknown id",
176+
Title: "title 1",
177+
})
178+
assert.Error(t, err)
179+
})
180+
}
181+
142182
var testSeries = &api.Series{
143183
ExtID: "ext-id",
144184
AuthorEmail: "some@email.com",
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 db
5+
6+
import (
7+
"context"
8+
9+
"cloud.google.com/go/spanner"
10+
)
11+
12+
type BaseFindingRepository struct {
13+
client *spanner.Client
14+
}
15+
16+
func NewBaseFindingRepository(client *spanner.Client) *BaseFindingRepository {
17+
return &BaseFindingRepository{
18+
client: client,
19+
}
20+
}
21+
22+
func (repo *BaseFindingRepository) Save(ctx context.Context, info *BaseFinding) error {
23+
_, err := repo.client.ReadWriteTransaction(ctx,
24+
func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
25+
m, err := spanner.InsertOrUpdateStruct("BaseFindings", info)
26+
if err != nil {
27+
return err
28+
}
29+
return txn.BufferWrite([]*spanner.Mutation{m})
30+
})
31+
return err
32+
}
33+
34+
func (repo *BaseFindingRepository) Exists(ctx context.Context, info *BaseFinding) (bool, error) {
35+
entity, err := readEntity[BaseFinding](ctx, repo.client.Single(), spanner.Statement{
36+
SQL: `SELECT * FROM BaseFindings WHERE
37+
CommitHash = @commit AND
38+
Config = @config AND
39+
Arch = @arch AND
40+
Title = @title`,
41+
Params: map[string]interface{}{
42+
"commit": info.CommitHash,
43+
"config": info.Config,
44+
"arch": info.Arch,
45+
"title": info.Title,
46+
},
47+
})
48+
return entity != nil, err
49+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 db
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestBaseFindingRepository(t *testing.T) {
14+
client, ctx := NewTransientDB(t)
15+
repo := NewBaseFindingRepository(client)
16+
17+
// It works fine on unknown titles.
18+
exists, err := repo.Exists(ctx, &BaseFinding{
19+
CommitHash: "abcd",
20+
Config: "cfg",
21+
Arch: "x86",
22+
})
23+
require.NoError(t, err)
24+
assert.False(t, exists)
25+
26+
// Add some new title.
27+
finding := &BaseFinding{
28+
CommitHash: "hash",
29+
Config: "config",
30+
Arch: "arch",
31+
Title: "title",
32+
}
33+
err = repo.Save(ctx, finding)
34+
require.NoError(t, err)
35+
36+
// Verify it exists.
37+
exists, err = repo.Exists(ctx, finding)
38+
require.NoError(t, err)
39+
assert.True(t, exists)
40+
}

syz-cluster/pkg/db/entities.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,12 @@ type ReportReply struct {
161161
ReportID string `spanner:"ReportID"`
162162
Time time.Time `spanner:"Time"`
163163
}
164+
165+
// BaseFinding collects all crashes observed on the base kernel tree.
166+
// It will be used to avoid unnecessary bug reproduction attempts.
167+
type BaseFinding struct {
168+
CommitHash string `spanner:"CommitHash"`
169+
Config string `spanner:"Config"`
170+
Arch string `spanner:"Arch"`
171+
Title string `spanner:"Title"`
172+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE BaseFindings;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CREATE TABLE BaseFindings (
2+
CommitHash STRING(64) NOT NULL,
3+
Config STRING(256) NOT NULL,
4+
Arch STRING(64) NOT NULL,
5+
Title STRING(512) NOT NULL,
6+
) PRIMARY KEY (CommitHash, Config, Arch, Title);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 service
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
11+
"github.com/google/syzkaller/syz-cluster/pkg/api"
12+
"github.com/google/syzkaller/syz-cluster/pkg/app"
13+
"github.com/google/syzkaller/syz-cluster/pkg/db"
14+
)
15+
16+
type BaseFindingService struct {
17+
baseFindingRepo *db.BaseFindingRepository
18+
buildRepo *db.BuildRepository
19+
}
20+
21+
func NewBaseFindingService(env *app.AppEnvironment) *BaseFindingService {
22+
return &BaseFindingService{
23+
baseFindingRepo: db.NewBaseFindingRepository(env.Spanner),
24+
buildRepo: db.NewBuildRepository(env.Spanner),
25+
}
26+
}
27+
28+
var ErrBuildNotFound = errors.New("build not found")
29+
30+
func (s *BaseFindingService) Upload(ctx context.Context, info *api.BaseFindingInfo) error {
31+
finding, err := s.makeBaseFinding(ctx, info)
32+
if err != nil {
33+
return err
34+
}
35+
return s.baseFindingRepo.Save(ctx, finding)
36+
}
37+
38+
func (s *BaseFindingService) Status(ctx context.Context, info *api.BaseFindingInfo) (
39+
*api.BaseFindingStatus, error) {
40+
finding, err := s.makeBaseFinding(ctx, info)
41+
if err != nil {
42+
return nil, err
43+
}
44+
exists, err := s.baseFindingRepo.Exists(ctx, finding)
45+
if err != nil {
46+
return nil, err
47+
}
48+
return &api.BaseFindingStatus{
49+
Observed: exists,
50+
}, nil
51+
}
52+
53+
func (s *BaseFindingService) makeBaseFinding(ctx context.Context, info *api.BaseFindingInfo) (*db.BaseFinding, error) {
54+
build, err := s.buildRepo.GetByID(ctx, info.BuildID)
55+
if err != nil {
56+
return nil, fmt.Errorf("failed to query build: %w", err)
57+
} else if build == nil {
58+
return nil, ErrBuildNotFound
59+
}
60+
return &db.BaseFinding{
61+
CommitHash: build.CommitHash,
62+
Config: build.ConfigName,
63+
Arch: build.Arch,
64+
Title: info.Title,
65+
}, nil
66+
}

0 commit comments

Comments
 (0)