aboutsummaryrefslogtreecommitdiffstats
path: root/syz-cluster/controller
diff options
context:
space:
mode:
authorAleksandr Nogikh <nogikh@google.com>2025-01-15 17:41:31 +0100
committerAleksandr Nogikh <nogikh@google.com>2025-01-22 13:17:53 +0000
commitcc143e38041972ad4dbaff9cfbfd416c29d581b5 (patch)
tree31e07572a22bebdc20c1fc73a6ef9140c03eb3e5 /syz-cluster/controller
parent6bab9518e47d67b3c9bba00f213aa3e1637063e1 (diff)
syz-cluster: add support for findings
Findings are crashes and build/boot/test errors that happened during the patch series processing.
Diffstat (limited to 'syz-cluster/controller')
-rw-r--r--syz-cluster/controller/api.go40
-rw-r--r--syz-cluster/controller/api_test.go66
-rw-r--r--syz-cluster/controller/services.go69
3 files changed, 151 insertions, 24 deletions
diff --git a/syz-cluster/controller/api.go b/syz-cluster/controller/api.go
index 6f1089bd2..9db06c230 100644
--- a/syz-cluster/controller/api.go
+++ b/syz-cluster/controller/api.go
@@ -1,6 +1,7 @@
// Copyright 2024 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.
+// nolint: dupl // The methods look similar, but extracting the common parts will only make the code worse.
package main
import (
@@ -12,20 +13,21 @@ import (
"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 ControllerAPI struct {
- seriesService *SeriesService
- buildService *BuildService
- testRepo *db.SessionTestRepository
+ seriesService *SeriesService
+ buildService *BuildService
+ testService *SessionTestService
+ findingService *FindingService
}
func NewControllerAPI(env *app.AppEnvironment) *ControllerAPI {
return &ControllerAPI{
- seriesService: NewSeriesService(env),
- buildService: NewBuildService(env),
- testRepo: db.NewSessionTestRepository(env.Spanner),
+ seriesService: NewSeriesService(env),
+ buildService: NewBuildService(env),
+ testService: NewSessionTestService(env),
+ findingService: NewFindingService(env),
}
}
@@ -36,6 +38,7 @@ func (c ControllerAPI) Mux() *http.ServeMux {
mux.HandleFunc("/builds/last", c.getLastBuild)
mux.HandleFunc("/builds/upload", c.uploadBuild)
mux.HandleFunc("/tests/upload", c.uploadTest)
+ mux.HandleFunc("/findings/upload", c.uploadFinding)
return mux
}
@@ -85,15 +88,22 @@ func (c ControllerAPI) uploadTest(w http.ResponseWriter, r *http.Request) {
return
}
// TODO: add parameters validation.
- err := c.testRepo.Insert(context.Background(), &db.SessionTest{
- SessionID: req.SessionID,
- BaseBuildID: req.BaseBuildID,
- PatchedBuildID: req.PatchedBuildID,
- TestName: req.TestName,
- Result: req.Result,
- })
+ err := c.testService.Save(context.Background(), req)
+ if err != nil {
+ http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
+ return
+ }
+ reply[interface{}](w, nil)
+}
+
+func (c ControllerAPI) uploadFinding(w http.ResponseWriter, r *http.Request) {
+ req := parseBody[api.Finding](w, r)
+ if req == nil {
+ return
+ }
+ // TODO: add parameters validation.
+ err := c.findingService.Save(context.Background(), req)
if err != nil {
- // TODO: sometimes it's not StatusInternalServerError.
http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
return
}
diff --git a/syz-cluster/controller/api_test.go b/syz-cluster/controller/api_test.go
index 6076ddc63..bc205392b 100644
--- a/syz-cluster/controller/api_test.go
+++ b/syz-cluster/controller/api_test.go
@@ -4,6 +4,7 @@
package main
import (
+ "context"
"net/http/httptest"
"testing"
"time"
@@ -39,6 +40,60 @@ func TestAPISuccessfulBuild(t *testing.T) {
server := httptest.NewServer(apiServer.Mux())
defer server.Close()
+ client := api.NewClient(server.URL)
+ buildInfo, _ := uploadTestBuild(t, client)
+ info, err := client.LastSuccessfulBuild(ctx, &api.LastBuildReq{
+ Arch: buildInfo.Arch,
+ TreeName: buildInfo.TreeName,
+ ConfigName: buildInfo.ConfigName,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, buildInfo, info)
+}
+
+func TestAPISaveFinding(t *testing.T) {
+ env, ctx := app.TestEnvironment(t)
+ apiServer := NewControllerAPI(env)
+ server := httptest.NewServer(apiServer.Mux())
+ defer server.Close()
+
+ series := addTestSeries(t, db.NewSeriesRepository(env.Spanner), env.BlobStorage)
+ session := addTestSession(t, db.NewSessionRepository(env.Spanner), series)
+
+ client := api.NewClient(server.URL)
+ _, buildResp := uploadTestBuild(t, client)
+ err := client.UploadTestResult(ctx, &api.TestResult{
+ SessionID: session.ID,
+ BaseBuildID: buildResp.ID,
+ TestName: "test",
+ Result: api.TestRunning,
+ })
+ assert.NoError(t, err)
+
+ t.Run("not existing test", func(t *testing.T) {
+ err = client.UploadFinding(ctx, &api.Finding{
+ SessionID: session.ID,
+ TestName: "unknown test",
+ })
+ assert.Error(t, err)
+ })
+
+ t.Run("must succeed", func(t *testing.T) {
+ finding := &api.Finding{
+ SessionID: session.ID,
+ TestName: "test",
+ Report: []byte("report"),
+ Log: []byte("log"),
+ }
+ err = client.UploadFinding(ctx, finding)
+ assert.NoError(t, err)
+ // Even if the finding is reported the second time, it must still not fail.
+ err = client.UploadFinding(ctx, finding)
+ assert.NoError(t, err)
+ })
+}
+
+func uploadTestBuild(t *testing.T, client *api.Client) (*api.Build, *api.UploadBuildResp) {
buildInfo := &api.Build{
Arch: "amd64",
TreeName: "mainline",
@@ -47,17 +102,10 @@ func TestAPISuccessfulBuild(t *testing.T) {
CommitDate: time.Date(2020, time.January, 1, 3, 0, 0, 0, time.UTC),
BuildSuccess: true,
}
- client := api.NewClient(server.URL)
- ret, err := client.UploadBuild(ctx, &api.UploadBuildReq{
+ ret, err := client.UploadBuild(context.Background(), &api.UploadBuildReq{
Build: *buildInfo,
})
assert.NoError(t, err)
assert.NotEmpty(t, ret.ID)
- info, err := client.LastSuccessfulBuild(ctx, &api.LastBuildReq{
- Arch: buildInfo.Arch,
- TreeName: buildInfo.TreeName,
- ConfigName: buildInfo.ConfigName,
- })
- assert.NoError(t, err)
- assert.Equal(t, buildInfo, info)
+ return buildInfo, ret
}
diff --git a/syz-cluster/controller/services.go b/syz-cluster/controller/services.go
index c97ef4f4f..6906f6ccd 100644
--- a/syz-cluster/controller/services.go
+++ b/syz-cluster/controller/services.go
@@ -4,11 +4,13 @@
package main
import (
+ "bytes"
"context"
"errors"
"fmt"
"io"
+ "cloud.google.com/go/spanner"
"github.com/google/syzkaller/syz-cluster/pkg/api"
"github.com/google/syzkaller/syz-cluster/pkg/app"
"github.com/google/syzkaller/syz-cluster/pkg/blob"
@@ -127,3 +129,70 @@ func (s *BuildService) LastBuild(ctx context.Context, req *api.LastBuildReq) (*a
}
return resp, nil
}
+
+type SessionTestService struct {
+ testRepo *db.SessionTestRepository
+}
+
+func NewSessionTestService(env *app.AppEnvironment) *SessionTestService {
+ return &SessionTestService{
+ testRepo: db.NewSessionTestRepository(env.Spanner),
+ }
+}
+
+func (s *SessionTestService) Save(ctx context.Context, req *api.TestResult) error {
+ entity := &db.SessionTest{
+ SessionID: req.SessionID,
+ TestName: req.TestName,
+ Result: req.Result,
+ }
+ if req.BaseBuildID != "" {
+ entity.BaseBuildID = spanner.NullString{StringVal: req.BaseBuildID, Valid: true}
+ }
+ if req.PatchedBuildID != "" {
+ entity.PatchedBuildID = spanner.NullString{StringVal: req.PatchedBuildID, Valid: true}
+ }
+ return s.testRepo.InsertOrUpdate(context.Background(), entity)
+}
+
+type FindingService struct {
+ findingRepo *db.FindingRepository
+ blobStorage blob.Storage
+}
+
+func NewFindingService(env *app.AppEnvironment) *FindingService {
+ return &FindingService{
+ findingRepo: db.NewFindingRepository(env.Spanner),
+ blobStorage: env.BlobStorage,
+ }
+}
+
+func (s *FindingService) Save(ctx context.Context, req *api.Finding) error {
+ var reportURI, logURI string
+ var err error
+ if len(req.Log) > 0 {
+ logURI, err = s.blobStorage.Store(bytes.NewReader(req.Log))
+ if err != nil {
+ return fmt.Errorf("failed to save the log: %w", err)
+ }
+ }
+ if len(req.Report) > 0 {
+ reportURI, err = s.blobStorage.Store(bytes.NewReader(req.Report))
+ if err != nil {
+ return fmt.Errorf("failed to save the report: %w", err)
+ }
+ }
+ // TODO: if it's not actually addded, the blob records will be orphaned.
+ err = s.findingRepo.Save(ctx, &db.Finding{
+ SessionID: req.SessionID,
+ TestName: req.TestName,
+ Title: req.Title,
+ ReportURI: reportURI,
+ LogURI: logURI,
+ })
+ if err == db.ErrFindingExists {
+ // It's ok, just ignore.
+ return nil
+ }
+ return err
+}