github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/pkg/service/finding.go (about) 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 "bytes" 8 "context" 9 "fmt" 10 "time" 11 12 "github.com/google/syzkaller/syz-cluster/pkg/api" 13 "github.com/google/syzkaller/syz-cluster/pkg/app" 14 "github.com/google/syzkaller/syz-cluster/pkg/blob" 15 "github.com/google/syzkaller/syz-cluster/pkg/db" 16 "github.com/google/uuid" 17 ) 18 19 type FindingService struct { 20 findingRepo *db.FindingRepository 21 sessionTestRepo *db.SessionTestRepository 22 buildRepo *db.BuildRepository 23 urls *api.URLGenerator 24 blobStorage blob.Storage 25 } 26 27 func NewFindingService(env *app.AppEnvironment) *FindingService { 28 return &FindingService{ 29 findingRepo: db.NewFindingRepository(env.Spanner), 30 blobStorage: env.BlobStorage, 31 urls: env.URLs, 32 buildRepo: db.NewBuildRepository(env.Spanner), 33 sessionTestRepo: db.NewSessionTestRepository(env.Spanner), 34 } 35 } 36 37 func (s *FindingService) Save(ctx context.Context, req *api.NewFinding) error { 38 return s.findingRepo.Store(ctx, &db.FindingID{ 39 SessionID: req.SessionID, 40 TestName: req.TestName, 41 Title: req.Title, 42 }, func(session *db.Session, old *db.Finding) (*db.Finding, error) { 43 if !session.FinishedAt.IsNull() { 44 // We may have already sent a report, so the findings must stay as they are. 45 return nil, fmt.Errorf("session is already finished") 46 } 47 if old != nil && (old.CReproURI != "" || len(req.CRepro) == 0) { 48 // The existing finding already has a C reproducer, no reason to update. 49 return nil, nil 50 } 51 finding := &db.Finding{ 52 ID: uuid.NewString(), 53 SessionID: req.SessionID, 54 TestName: req.TestName, 55 Title: req.Title, 56 } 57 // TODO: if it's not actually addded, these blobs will be orphaned. 58 err := s.saveAssets(finding, req) 59 if err != nil { 60 return nil, err 61 } 62 return finding, nil 63 }) 64 } 65 66 func (s *FindingService) saveAssets(finding *db.Finding, req *api.NewFinding) error { 67 type saveAsset struct { 68 saveTo *string 69 value []byte 70 name string 71 } 72 for _, asset := range []saveAsset{ 73 {&finding.LogURI, req.Log, "log"}, 74 {&finding.ReportURI, req.Report, "report"}, 75 {&finding.SyzReproURI, req.SyzRepro, "syz_repro"}, 76 {&finding.SyzReproOptsURI, req.SyzReproOpts, "syz_repro_opts"}, 77 {&finding.CReproURI, req.CRepro, "c_repro"}, 78 } { 79 if len(asset.value) == 0 { 80 continue 81 } 82 var err error 83 *asset.saveTo, err = s.blobStorage.Write(bytes.NewReader(asset.value), "Finding", finding.ID, asset.name) 84 if err != nil { 85 return fmt.Errorf("failed to save %s: %w", asset.name, err) 86 } 87 } 88 return nil 89 } 90 91 func (s *FindingService) InvalidateSession(ctx context.Context, sessionID string) error { 92 findings, err := s.findingRepo.ListForSession(ctx, sessionID, 0) 93 if err != nil { 94 return err 95 } 96 for _, finding := range findings { 97 err := s.findingRepo.Update(ctx, finding.ID, func(finding *db.Finding) error { 98 finding.SetInvalidatedAt(time.Now()) 99 return nil 100 }) 101 if err != nil { 102 return fmt.Errorf("failed to update finding %s: %w", finding.ID, err) 103 } 104 } 105 return nil 106 } 107 108 func (s *FindingService) List(ctx context.Context, sessionID string, limit int) ([]*api.Finding, error) { 109 list, err := s.findingRepo.ListForSession(ctx, sessionID, limit) 110 if err != nil { 111 return nil, fmt.Errorf("failed to query the list: %w", err) 112 } 113 tests, err := s.sessionTestRepo.BySession(ctx, sessionID) 114 if err != nil { 115 return nil, fmt.Errorf("failed to query session tests: %w", err) 116 } 117 testPerName := map[string]*db.FullSessionTest{} 118 for _, test := range tests { 119 testPerName[test.TestName] = test 120 } 121 var ret []*api.Finding 122 for _, item := range list { 123 finding := &api.Finding{ 124 Title: item.Title, 125 LogURL: s.urls.FindingLog(item.ID), 126 } 127 if item.SyzReproURI != "" { 128 finding.LinkSyzRepro = s.urls.FindingSyzRepro(item.ID) 129 } 130 if item.CReproURI != "" { 131 finding.LinkCRepro = s.urls.FindingCRepro(item.ID) 132 } 133 if !item.InvalidatedAt.IsNull() { 134 finding.Invalidated = true 135 } 136 build := testPerName[item.TestName].PatchedBuild 137 if build != nil { 138 finding.Build = makeBuildInfo(s.urls, build) 139 } 140 bytes, err := blob.ReadAllBytes(s.blobStorage, item.ReportURI) 141 if err != nil { 142 return nil, fmt.Errorf("failed to read the report: %w", err) 143 } 144 finding.Report = string(bytes) 145 ret = append(ret, finding) 146 } 147 return ret, nil 148 }