diff options
| author | Aleksandr Nogikh <nogikh@google.com> | 2025-08-19 16:08:42 +0200 |
|---|---|---|
| committer | Aleksandr Nogikh <nogikh@google.com> | 2025-08-21 12:29:47 +0000 |
| commit | 55d1b48ed250d44ae3796f56178fd1c4729540c9 (patch) | |
| tree | 293d6392a2b4265c63f0c28e90d45345588703eb /syz-cluster | |
| parent | d20da451d2ba749a51a7cbe5bacdd182095a3f0d (diff) | |
syz-cluster: collect information about base crashes
Track base crashes for (commit hash, config, arch) tuples.
Diffstat (limited to 'syz-cluster')
| -rw-r--r-- | syz-cluster/pkg/api/client.go | 18 | ||||
| -rw-r--r-- | syz-cluster/pkg/controller/api.go | 53 | ||||
| -rw-r--r-- | syz-cluster/pkg/controller/api_test.go | 40 | ||||
| -rw-r--r-- | syz-cluster/pkg/db/base_finding_repo.go | 49 | ||||
| -rw-r--r-- | syz-cluster/pkg/db/base_finding_repo_test.go | 40 | ||||
| -rw-r--r-- | syz-cluster/pkg/db/entities.go | 9 | ||||
| -rw-r--r-- | syz-cluster/pkg/db/migrations/6_base_findings.down.sql | 1 | ||||
| -rw-r--r-- | syz-cluster/pkg/db/migrations/6_base_findings.up.sql | 6 | ||||
| -rw-r--r-- | syz-cluster/pkg/service/base_finding.go | 66 |
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 +} |
