github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/workflow/triage-step/main.go (about)

     1  // Copyright 2024 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 main
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"errors"
    10  	"flag"
    11  	"fmt"
    12  
    13  	"github.com/google/syzkaller/pkg/debugtracer"
    14  	"github.com/google/syzkaller/pkg/osutil"
    15  	"github.com/google/syzkaller/syz-cluster/pkg/api"
    16  	"github.com/google/syzkaller/syz-cluster/pkg/app"
    17  	"github.com/google/syzkaller/syz-cluster/pkg/triage"
    18  )
    19  
    20  var (
    21  	flagSession = flag.String("session", "", "session ID")
    22  	flagRepo    = flag.String("repository", "", "path to a kernel checkout")
    23  	flagVerdict = flag.String("verdict", "", "where to save the verdict")
    24  )
    25  
    26  func main() {
    27  	flag.Parse()
    28  	if *flagSession == "" || *flagRepo == "" {
    29  		// TODO: abort the whole workflow, no sense to retry. Alert the error.
    30  		app.Fatalf("--session and --repo must be set")
    31  	}
    32  	client := app.DefaultClient()
    33  	repo, err := triage.NewGitTreeOps(*flagRepo, true)
    34  	if err != nil {
    35  		app.Fatalf("failed to initialize the repository: %v", err)
    36  	}
    37  	ctx := context.Background()
    38  	output := new(bytes.Buffer)
    39  	tracer := &debugtracer.GenericTracer{WithTime: true, TraceWriter: output}
    40  
    41  	triager := &seriesTriager{
    42  		DebugTracer: tracer,
    43  		client:      client,
    44  		ops:         repo,
    45  	}
    46  	verdict, err := triager.GetVerdict(ctx, *flagSession)
    47  	if err != nil {
    48  		app.Fatalf("failed to get the verdict: %v", err)
    49  	}
    50  	err = client.UploadTriageResult(ctx, *flagSession, &api.UploadTriageResultReq{
    51  		SkipReason: verdict.SkipReason,
    52  		Log:        output.Bytes(),
    53  	})
    54  	if err != nil {
    55  		app.Fatalf("failed to upload triage results: %v", err)
    56  	}
    57  	if *flagVerdict != "" {
    58  		osutil.WriteJSON(*flagVerdict, verdict)
    59  	}
    60  
    61  	// TODO:
    62  	// 1. It might be that the kernel builds/boots for one arch and does not build for another.
    63  	// 2. What if controller does not reply? Let Argo just restart the step.
    64  }
    65  
    66  type seriesTriager struct {
    67  	debugtracer.DebugTracer
    68  	client *api.Client
    69  	ops    triage.TreeOps
    70  }
    71  
    72  func (triager *seriesTriager) GetVerdict(ctx context.Context, sessionID string) (*api.TriageResult, error) {
    73  	series, err := triager.client.GetSessionSeries(ctx, sessionID)
    74  	if err != nil {
    75  		// TODO: the workflow step must be retried.
    76  		return nil, fmt.Errorf("failed to query series: %w", err)
    77  	}
    78  	treesResp, err := triager.client.GetTrees(ctx)
    79  	if err != nil {
    80  		return nil, fmt.Errorf("failed to query trees: %w", err)
    81  	}
    82  	selectedTrees := triage.SelectTrees(series, treesResp.Trees)
    83  	if len(selectedTrees) == 0 {
    84  		return &api.TriageResult{
    85  			SkipReason: "no suitable base kernel trees found",
    86  		}, nil
    87  	}
    88  	fuzzConfigs := triage.MergeKernelFuzzConfigs(triage.SelectFuzzConfigs(series, treesResp.FuzzTargets))
    89  	if len(fuzzConfigs) == 0 {
    90  		return &api.TriageResult{
    91  			SkipReason: "no suitable fuzz configs found",
    92  		}, nil
    93  	}
    94  	ret := &api.TriageResult{}
    95  	for _, campaign := range fuzzConfigs {
    96  		fuzzTask, err := triager.prepareFuzzingTask(ctx, series, selectedTrees, campaign)
    97  		var skipErr *SkipTriageError
    98  		if errors.As(err, &skipErr) {
    99  			ret.SkipReason = skipErr.Reason.Error()
   100  			continue
   101  		} else if err != nil {
   102  			return nil, err
   103  		}
   104  		ret.Fuzz = append(ret.Fuzz, fuzzTask)
   105  	}
   106  	if len(ret.Fuzz) > 0 {
   107  		// If we have prepared at least one fuzzing task, the series was not skipped.
   108  		ret.SkipReason = ""
   109  	}
   110  	return ret, nil
   111  }
   112  
   113  func (triager *seriesTriager) prepareFuzzingTask(ctx context.Context, series *api.Series, trees []*api.Tree,
   114  	target *triage.MergedFuzzConfig) (*api.FuzzTask, error) {
   115  	var skipErr error
   116  	for _, tree := range trees {
   117  		triager.Log("considering tree %q", tree.Name)
   118  		arch := "amd64"
   119  		lastBuild, err := triager.client.LastBuild(ctx, &api.LastBuildReq{
   120  			Arch:       arch,
   121  			ConfigName: target.KernelConfig,
   122  			TreeName:   tree.Name,
   123  			Status:     api.BuildSuccess,
   124  		})
   125  		if err != nil {
   126  			// TODO: the workflow step must be retried.
   127  			return nil, fmt.Errorf("failed to query the last build for %q: %w", tree.Name, err)
   128  		}
   129  		triager.Log("%q's last build: %q", tree.Name, lastBuild)
   130  		selector := triage.NewCommitSelector(triager.ops, triager.DebugTracer)
   131  		result, err := selector.Select(series, tree, lastBuild)
   132  		if err != nil {
   133  			// TODO: the workflow step must be retried.
   134  			return nil, fmt.Errorf("failed to run the commit selector for %q: %w", tree.Name, err)
   135  		} else if result.Commit == "" {
   136  			// If we fail to find a suitable commit for all the trees, return an error just about the first one.
   137  			if skipErr == nil {
   138  				skipErr = SkipError("failed to find a base commit: " + result.Reason)
   139  			}
   140  			triager.Log("failed to find a base commit for %q", tree.Name)
   141  			continue
   142  		}
   143  		triager.Log("selected base commit: %s", result.Commit)
   144  		base := api.BuildRequest{
   145  			TreeName:   tree.Name,
   146  			TreeURL:    tree.URL,
   147  			ConfigName: target.KernelConfig,
   148  			CommitHash: result.Commit,
   149  			Arch:       arch,
   150  		}
   151  		fuzz := &api.FuzzTask{
   152  			Base:       base,
   153  			Patched:    base,
   154  			FuzzConfig: *target.FuzzConfig,
   155  		}
   156  		fuzz.Patched.SeriesID = series.ID
   157  		return fuzz, nil
   158  	}
   159  	return nil, skipErr
   160  }
   161  
   162  type SkipTriageError struct {
   163  	Reason error
   164  }
   165  
   166  func SkipError(reason string) *SkipTriageError {
   167  	return &SkipTriageError{Reason: errors.New(reason)}
   168  }
   169  
   170  func (e *SkipTriageError) Error() string {
   171  	return fmt.Sprintf("series must be skipped: %s", e.Reason)
   172  }
   173  
   174  func (e *SkipTriageError) Unwrap() error {
   175  	return e.Reason
   176  }