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  }