aboutsummaryrefslogtreecommitdiffstats
path: root/syz-cluster/pkg
diff options
context:
space:
mode:
authorAleksandr Nogikh <nogikh@google.com>2025-08-19 16:08:42 +0200
committerAleksandr Nogikh <nogikh@google.com>2025-08-21 12:29:47 +0000
commit55d1b48ed250d44ae3796f56178fd1c4729540c9 (patch)
tree293d6392a2b4265c63f0c28e90d45345588703eb /syz-cluster/pkg
parentd20da451d2ba749a51a7cbe5bacdd182095a3f0d (diff)
syz-cluster: collect information about base crashes
Track base crashes for (commit hash, config, arch) tuples.
Diffstat (limited to 'syz-cluster/pkg')
-rw-r--r--syz-cluster/pkg/api/client.go18
-rw-r--r--syz-cluster/pkg/controller/api.go53
-rw-r--r--syz-cluster/pkg/controller/api_test.go40
-rw-r--r--syz-cluster/pkg/db/base_finding_repo.go49
-rw-r--r--syz-cluster/pkg/db/base_finding_repo_test.go40
-rw-r--r--syz-cluster/pkg/db/entities.go9
-rw-r--r--syz-cluster/pkg/db/migrations/6_base_findings.down.sql1
-rw-r--r--syz-cluster/pkg/db/migrations/6_base_findings.up.sql6
-rw-r--r--syz-cluster/pkg/service/base_finding.go66
9 files changed, 272 insertions, 10 deletions
diff --git a/syz-cluster/pkg/api/client.go b/syz-cluster/pkg/api/client.go
index 1b571c628..bd9e4466e 100644
--- a/syz-cluster/pkg/api/client.go
+++ b/syz-cluster/pkg/api/client.go
@@ -114,6 +114,24 @@ func (client Client) UploadSession(ctx context.Context, req *NewSession) (*Uploa
return postJSON[NewSession, UploadSessionResp](ctx, client.baseURL+"/sessions/upload", req)
}
+type BaseFindingInfo struct {
+ BuildID string `json:"buildID"`
+ Title string `json:"title"`
+}
+
+func (client Client) UploadBaseFinding(ctx context.Context, req *BaseFindingInfo) error {
+ _, err := postJSON[BaseFindingInfo, any](ctx, client.baseURL+"/base_findings/upload", req)
+ return err
+}
+
+type BaseFindingStatus struct {
+ Observed bool `json:"observed"`
+}
+
+func (client Client) BaseFindingStatus(ctx context.Context, req *BaseFindingInfo) (*BaseFindingStatus, error) {
+ return postJSON[BaseFindingInfo, BaseFindingStatus](ctx, client.baseURL+"/base_findings/status", req)
+}
+
const requestTimeout = time.Minute
func finishRequest[Resp any](httpReq *http.Request) (*Resp, error) {
diff --git a/syz-cluster/pkg/controller/api.go b/syz-cluster/pkg/controller/api.go
index de8545319..4a230a35d 100644
--- a/syz-cluster/pkg/controller/api.go
+++ b/syz-cluster/pkg/controller/api.go
@@ -16,20 +16,22 @@ import (
)
type APIServer struct {
- seriesService *service.SeriesService
- sessionService *service.SessionService
- buildService *service.BuildService
- testService *service.SessionTestService
- findingService *service.FindingService
+ seriesService *service.SeriesService
+ sessionService *service.SessionService
+ buildService *service.BuildService
+ testService *service.SessionTestService
+ findingService *service.FindingService
+ baseFindingService *service.BaseFindingService
}
func NewAPIServer(env *app.AppEnvironment) *APIServer {
return &APIServer{
- seriesService: service.NewSeriesService(env),
- sessionService: service.NewSessionService(env),
- buildService: service.NewBuildService(env),
- testService: service.NewSessionTestService(env),
- findingService: service.NewFindingService(env),
+ seriesService: service.NewSeriesService(env),
+ sessionService: service.NewSessionService(env),
+ buildService: service.NewBuildService(env),
+ testService: service.NewSessionTestService(env),
+ findingService: service.NewFindingService(env),
+ baseFindingService: service.NewBaseFindingService(env),
}
}
@@ -46,6 +48,8 @@ func (c APIServer) Mux() *http.ServeMux {
mux.HandleFunc("/tests/upload_artifacts", c.uploadTestArtifact)
mux.HandleFunc("/tests/upload", c.uploadTest)
mux.HandleFunc("/trees", c.getTrees)
+ mux.HandleFunc("/base_findings/upload", c.uploadBaseFinding)
+ mux.HandleFunc("/base_findings/status", c.baseFindingStatus)
return mux
}
@@ -206,3 +210,32 @@ func (c APIServer) getTrees(w http.ResponseWriter, r *http.Request) {
FuzzConfigs: api.FuzzConfigs,
})
}
+
+func (c APIServer) uploadBaseFinding(w http.ResponseWriter, r *http.Request) {
+ req := api.ParseJSON[api.BaseFindingInfo](w, r)
+ if req == nil {
+ return
+ }
+ err := c.baseFindingService.Upload(r.Context(), req)
+ if errors.Is(err, service.ErrBuildNotFound) {
+ http.Error(w, fmt.Sprint(err), http.StatusNotFound)
+ return
+ } else if err != nil {
+ http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
+ return
+ }
+ api.ReplyJSON[interface{}](w, nil)
+}
+
+func (c APIServer) baseFindingStatus(w http.ResponseWriter, r *http.Request) {
+ req := api.ParseJSON[api.BaseFindingInfo](w, r)
+ if req == nil {
+ return
+ }
+ resp, err := c.baseFindingService.Status(r.Context(), req)
+ if err != nil {
+ http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
+ return
+ }
+ api.ReplyJSON[*api.BaseFindingStatus](w, resp)
+}
diff --git a/syz-cluster/pkg/controller/api_test.go b/syz-cluster/pkg/controller/api_test.go
index e64f3bc78..69dd02826 100644
--- a/syz-cluster/pkg/controller/api_test.go
+++ b/syz-cluster/pkg/controller/api_test.go
@@ -139,6 +139,46 @@ func TestAPIUploadTestArtifacts(t *testing.T) {
assert.NoError(t, err)
}
+func TestAPIBaseFindings(t *testing.T) {
+ env, ctx := app.TestEnvironment(t)
+ client := TestServer(t, env)
+ buildResp := UploadTestBuild(t, ctx, client, testBuild)
+
+ err := client.UploadBaseFinding(ctx, &api.BaseFindingInfo{
+ BuildID: buildResp.ID,
+ Title: "title 1",
+ })
+ assert.NoError(t, err)
+
+ // Let's upload a different build for the same revision.
+ buildResp2 := UploadTestBuild(t, ctx, client, testBuild)
+ assert.NotEqual(t, buildResp.ID, buildResp2.ID)
+
+ resp, err := client.BaseFindingStatus(ctx, &api.BaseFindingInfo{
+ BuildID: buildResp2.ID,
+ Title: "title 1",
+ })
+ assert.NoError(t, err)
+ assert.True(t, resp.Observed)
+
+ t.Run("unseen title", func(t *testing.T) {
+ resp, err := client.BaseFindingStatus(ctx, &api.BaseFindingInfo{
+ BuildID: buildResp2.ID,
+ Title: "title 2",
+ })
+ assert.NoError(t, err)
+ assert.False(t, resp.Observed)
+ })
+
+ t.Run("invalid build id", func(t *testing.T) {
+ _, err := client.BaseFindingStatus(ctx, &api.BaseFindingInfo{
+ BuildID: "unknown id",
+ Title: "title 1",
+ })
+ assert.Error(t, err)
+ })
+}
+
var testSeries = &api.Series{
ExtID: "ext-id",
AuthorEmail: "some@email.com",
diff --git a/syz-cluster/pkg/db/base_finding_repo.go b/syz-cluster/pkg/db/base_finding_repo.go
new file mode 100644
index 000000000..e963cb691
--- /dev/null
+++ b/syz-cluster/pkg/db/base_finding_repo.go
@@ -0,0 +1,49 @@
+// Copyright 2025 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package db
+
+import (
+ "context"
+
+ "cloud.google.com/go/spanner"
+)
+
+type BaseFindingRepository struct {
+ client *spanner.Client
+}
+
+func NewBaseFindingRepository(client *spanner.Client) *BaseFindingRepository {
+ return &BaseFindingRepository{
+ client: client,
+ }
+}
+
+func (repo *BaseFindingRepository) Save(ctx context.Context, info *BaseFinding) error {
+ _, err := repo.client.ReadWriteTransaction(ctx,
+ func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
+ m, err := spanner.InsertOrUpdateStruct("BaseFindings", info)
+ if err != nil {
+ return err
+ }
+ return txn.BufferWrite([]*spanner.Mutation{m})
+ })
+ return err
+}
+
+func (repo *BaseFindingRepository) Exists(ctx context.Context, info *BaseFinding) (bool, error) {
+ entity, err := readEntity[BaseFinding](ctx, repo.client.Single(), spanner.Statement{
+ SQL: `SELECT * FROM BaseFindings WHERE
+CommitHash = @commit AND
+Config = @config AND
+Arch = @arch AND
+Title = @title`,
+ Params: map[string]interface{}{
+ "commit": info.CommitHash,
+ "config": info.Config,
+ "arch": info.Arch,
+ "title": info.Title,
+ },
+ })
+ return entity != nil, err
+}
diff --git a/syz-cluster/pkg/db/base_finding_repo_test.go b/syz-cluster/pkg/db/base_finding_repo_test.go
new file mode 100644
index 000000000..de7eb07b6
--- /dev/null
+++ b/syz-cluster/pkg/db/base_finding_repo_test.go
@@ -0,0 +1,40 @@
+// Copyright 2025 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package db
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestBaseFindingRepository(t *testing.T) {
+ client, ctx := NewTransientDB(t)
+ repo := NewBaseFindingRepository(client)
+
+ // It works fine on unknown titles.
+ exists, err := repo.Exists(ctx, &BaseFinding{
+ CommitHash: "abcd",
+ Config: "cfg",
+ Arch: "x86",
+ })
+ require.NoError(t, err)
+ assert.False(t, exists)
+
+ // Add some new title.
+ finding := &BaseFinding{
+ CommitHash: "hash",
+ Config: "config",
+ Arch: "arch",
+ Title: "title",
+ }
+ err = repo.Save(ctx, finding)
+ require.NoError(t, err)
+
+ // Verify it exists.
+ exists, err = repo.Exists(ctx, finding)
+ require.NoError(t, err)
+ assert.True(t, exists)
+}
diff --git a/syz-cluster/pkg/db/entities.go b/syz-cluster/pkg/db/entities.go
index 53e52b4ea..13ba2fb85 100644
--- a/syz-cluster/pkg/db/entities.go
+++ b/syz-cluster/pkg/db/entities.go
@@ -161,3 +161,12 @@ type ReportReply struct {
ReportID string `spanner:"ReportID"`
Time time.Time `spanner:"Time"`
}
+
+// BaseFinding collects all crashes observed on the base kernel tree.
+// It will be used to avoid unnecessary bug reproduction attempts.
+type BaseFinding struct {
+ CommitHash string `spanner:"CommitHash"`
+ Config string `spanner:"Config"`
+ Arch string `spanner:"Arch"`
+ Title string `spanner:"Title"`
+}
diff --git a/syz-cluster/pkg/db/migrations/6_base_findings.down.sql b/syz-cluster/pkg/db/migrations/6_base_findings.down.sql
new file mode 100644
index 000000000..c30ac67a0
--- /dev/null
+++ b/syz-cluster/pkg/db/migrations/6_base_findings.down.sql
@@ -0,0 +1 @@
+DROP TABLE BaseFindings;
diff --git a/syz-cluster/pkg/db/migrations/6_base_findings.up.sql b/syz-cluster/pkg/db/migrations/6_base_findings.up.sql
new file mode 100644
index 000000000..8ec8f245a
--- /dev/null
+++ b/syz-cluster/pkg/db/migrations/6_base_findings.up.sql
@@ -0,0 +1,6 @@
+CREATE TABLE BaseFindings (
+ CommitHash STRING(64) NOT NULL,
+ Config STRING(256) NOT NULL,
+ Arch STRING(64) NOT NULL,
+ Title STRING(512) NOT NULL,
+) PRIMARY KEY (CommitHash, Config, Arch, Title);
diff --git a/syz-cluster/pkg/service/base_finding.go b/syz-cluster/pkg/service/base_finding.go
new file mode 100644
index 000000000..e7b59c18c
--- /dev/null
+++ b/syz-cluster/pkg/service/base_finding.go
@@ -0,0 +1,66 @@
+// Copyright 2025 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package service
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/google/syzkaller/syz-cluster/pkg/api"
+ "github.com/google/syzkaller/syz-cluster/pkg/app"
+ "github.com/google/syzkaller/syz-cluster/pkg/db"
+)
+
+type BaseFindingService struct {
+ baseFindingRepo *db.BaseFindingRepository
+ buildRepo *db.BuildRepository
+}
+
+func NewBaseFindingService(env *app.AppEnvironment) *BaseFindingService {
+ return &BaseFindingService{
+ baseFindingRepo: db.NewBaseFindingRepository(env.Spanner),
+ buildRepo: db.NewBuildRepository(env.Spanner),
+ }
+}
+
+var ErrBuildNotFound = errors.New("build not found")
+
+func (s *BaseFindingService) Upload(ctx context.Context, info *api.BaseFindingInfo) error {
+ finding, err := s.makeBaseFinding(ctx, info)
+ if err != nil {
+ return err
+ }
+ return s.baseFindingRepo.Save(ctx, finding)
+}
+
+func (s *BaseFindingService) Status(ctx context.Context, info *api.BaseFindingInfo) (
+ *api.BaseFindingStatus, error) {
+ finding, err := s.makeBaseFinding(ctx, info)
+ if err != nil {
+ return nil, err
+ }
+ exists, err := s.baseFindingRepo.Exists(ctx, finding)
+ if err != nil {
+ return nil, err
+ }
+ return &api.BaseFindingStatus{
+ Observed: exists,
+ }, nil
+}
+
+func (s *BaseFindingService) makeBaseFinding(ctx context.Context, info *api.BaseFindingInfo) (*db.BaseFinding, error) {
+ build, err := s.buildRepo.GetByID(ctx, info.BuildID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to query build: %w", err)
+ } else if build == nil {
+ return nil, ErrBuildNotFound
+ }
+ return &db.BaseFinding{
+ CommitHash: build.CommitHash,
+ Config: build.ConfigName,
+ Arch: build.Arch,
+ Title: info.Title,
+ }, nil
+}