github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/workflow/build-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  	"encoding/json"
    10  	"errors"
    11  	"flag"
    12  	"fmt"
    13  	"log"
    14  	"os"
    15  	"path/filepath"
    16  
    17  	"github.com/google/syzkaller/pkg/build"
    18  	"github.com/google/syzkaller/pkg/debugtracer"
    19  	"github.com/google/syzkaller/pkg/osutil"
    20  	"github.com/google/syzkaller/pkg/vcs"
    21  	"github.com/google/syzkaller/sys/targets"
    22  	"github.com/google/syzkaller/syz-cluster/pkg/api"
    23  	"github.com/google/syzkaller/syz-cluster/pkg/app"
    24  	"github.com/google/syzkaller/syz-cluster/pkg/triage"
    25  )
    26  
    27  var (
    28  	flagRequest    = flag.String("request", "", "path to a build request description")
    29  	flagRepository = flag.String("repository", "", "path to a kernel checkout")
    30  	flagOutput     = flag.String("output", "", "path to save kernel build artifacts")
    31  	flagTestName   = flag.String("test_name", "", "test name")
    32  	flagSession    = flag.String("session", "", "session ID")
    33  	flagFindings   = flag.Bool("findings", false, "report build failures as findings")
    34  	flagSmokeBuild = flag.Bool("smoke_build", false, "build only if new, don't report findings")
    35  )
    36  
    37  func main() {
    38  	flag.Parse()
    39  	ensureFlags(*flagRequest, "--request",
    40  		*flagRepository, "--repository",
    41  		*flagOutput, "--output")
    42  	if !*flagSmokeBuild {
    43  		ensureFlags(
    44  			*flagTestName, "--test_name",
    45  			*flagSession, "--session",
    46  		)
    47  	}
    48  
    49  	req := readRequest()
    50  	ctx := context.Background()
    51  	client := app.DefaultClient()
    52  	// TODO: (optimization) query whether the same BuildRequest has already been completed.
    53  	var series *api.Series
    54  	if req.SeriesID != "" {
    55  		var err error
    56  		series, err = client.GetSeries(ctx, req.SeriesID)
    57  		if err != nil {
    58  			app.Fatalf("failed to query the series info: %v", err)
    59  		}
    60  	}
    61  	uploadReq := &api.UploadBuildReq{
    62  		Build: api.Build{
    63  			Arch:       req.Arch,
    64  			ConfigName: req.ConfigName,
    65  			TreeName:   req.TreeName,
    66  			TreeURL:    req.TreeURL,
    67  			SeriesID:   req.SeriesID,
    68  		},
    69  	}
    70  	output := new(bytes.Buffer)
    71  	tracer := &debugtracer.GenericTracer{
    72  		WithTime:    false,
    73  		TraceWriter: output,
    74  		OutDir:      "",
    75  	}
    76  	commit, err := checkoutKernel(tracer, req, series)
    77  	if commit != nil {
    78  		uploadReq.CommitHash = commit.Hash
    79  		uploadReq.CommitDate = commit.CommitDate
    80  	}
    81  	ret := &BuildResult{}
    82  	if err != nil {
    83  		log.Printf("failed to checkout: %v", err)
    84  		reportResults(ctx, client, nil, nil, []byte(err.Error()))
    85  		return
    86  	} else {
    87  		if *flagSmokeBuild {
    88  			skip, err := alreadyBuilt(ctx, client, uploadReq)
    89  			if err != nil {
    90  				app.Fatalf("failed to query known builds: %v", err)
    91  			} else if skip {
    92  				log.Printf("%s already built, skipping", uploadReq.CommitHash)
    93  				return
    94  			}
    95  		}
    96  		ret, err = buildKernel(tracer, req)
    97  		if err != nil {
    98  			log.Printf("build process failed: %v", err)
    99  			reportResults(ctx, client, nil, nil, []byte(err.Error()))
   100  			return
   101  		} else {
   102  			uploadReq.Compiler = ret.Compiler
   103  			uploadReq.Config = ret.Config
   104  			if ret.Finding == nil {
   105  				uploadReq.BuildSuccess = true
   106  			} else {
   107  				log.Printf("%s", output.Bytes())
   108  				log.Printf("failed: %s\n%s", ret.Finding.Title, ret.Finding.Report)
   109  				uploadReq.Log = ret.Finding.Log
   110  			}
   111  		}
   112  	}
   113  	reportResults(ctx, client, uploadReq, ret.Finding, output.Bytes())
   114  }
   115  
   116  func reportResults(ctx context.Context, client *api.Client,
   117  	uploadReq *api.UploadBuildReq, finding *api.NewFinding, output []byte) {
   118  	var buildID string
   119  	status := api.TestPassed
   120  	if uploadReq != nil {
   121  		if !uploadReq.BuildSuccess {
   122  			status = api.TestFailed
   123  		}
   124  		buildInfo, err := client.UploadBuild(ctx, uploadReq)
   125  		if err != nil {
   126  			app.Fatalf("failed to upload build: %v", err)
   127  		}
   128  		log.Printf("uploaded build, reply: %q", buildInfo)
   129  		buildID = buildInfo.ID
   130  	} else {
   131  		status = api.TestError
   132  	}
   133  	osutil.WriteJSON(filepath.Join(*flagOutput, "result.json"), &api.BuildResult{
   134  		BuildID: buildID,
   135  		Success: status == api.TestPassed,
   136  	})
   137  	if *flagSmokeBuild {
   138  		return
   139  	}
   140  	testResult := &api.TestResult{
   141  		SessionID: *flagSession,
   142  		TestName:  *flagTestName,
   143  		Result:    status,
   144  		Log:       output,
   145  	}
   146  	if uploadReq != nil {
   147  		if uploadReq.SeriesID != "" {
   148  			testResult.PatchedBuildID = buildID
   149  		} else {
   150  			testResult.BaseBuildID = buildID
   151  		}
   152  	}
   153  	err := client.UploadTestResult(ctx, testResult)
   154  	if err != nil {
   155  		app.Fatalf("failed to report the test result: %v", err)
   156  	}
   157  	if *flagFindings && finding != nil {
   158  		err = client.UploadFinding(ctx, finding)
   159  		if err != nil {
   160  			app.Fatalf("failed to report the finding: %v", err)
   161  		}
   162  	}
   163  }
   164  
   165  func alreadyBuilt(ctx context.Context, client *api.Client,
   166  	req *api.UploadBuildReq) (bool, error) {
   167  	build, err := client.LastBuild(ctx, &api.LastBuildReq{
   168  		Arch:       req.Build.Arch,
   169  		ConfigName: req.Build.ConfigName,
   170  		TreeName:   req.Build.TreeName,
   171  		Commit:     req.CommitHash,
   172  	})
   173  	if err != nil {
   174  		return false, err
   175  	}
   176  	return build != nil, nil
   177  }
   178  
   179  func readRequest() *api.BuildRequest {
   180  	raw, err := os.ReadFile(*flagRequest)
   181  	if err != nil {
   182  		app.Fatalf("failed to read request: %v", err)
   183  		return nil
   184  	}
   185  	var req api.BuildRequest
   186  	err = json.Unmarshal(raw, &req)
   187  	if err != nil {
   188  		app.Fatalf("failed to unmarshal request: %v, %s", err, raw)
   189  		return nil
   190  	}
   191  	return &req
   192  }
   193  
   194  func checkoutKernel(tracer debugtracer.DebugTracer, req *api.BuildRequest, series *api.Series) (*vcs.Commit, error) {
   195  	tracer.Log("checking out %q", req.CommitHash)
   196  	ops, err := triage.NewGitTreeOps(*flagRepository, true)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	commit, err := ops.Commit(req.TreeName, req.CommitHash)
   201  	if err != nil {
   202  		return nil, fmt.Errorf("failed to get commit info: %w", err)
   203  	}
   204  	var patches [][]byte
   205  	if series != nil {
   206  		patches = series.PatchBodies()
   207  	}
   208  	if len(patches) > 0 {
   209  		tracer.Log("applying %d patches", len(patches))
   210  	}
   211  	err = ops.ApplySeries(commit.Hash, patches)
   212  	return commit, err
   213  }
   214  
   215  type BuildResult struct {
   216  	Config   []byte
   217  	Compiler string
   218  	Finding  *api.NewFinding
   219  }
   220  
   221  func buildKernel(tracer debugtracer.DebugTracer, req *api.BuildRequest) (*BuildResult, error) {
   222  	kernelConfig, err := os.ReadFile(filepath.Join("/kernel-configs", req.ConfigName))
   223  	if err != nil {
   224  		return nil, fmt.Errorf("failed to read the kernel config: %w", err)
   225  	}
   226  	if req.Arch != "amd64" {
   227  		// TODO: lift this restriction.
   228  		return nil, fmt.Errorf("only amd64 builds are supported now")
   229  	}
   230  	params := build.Params{
   231  		TargetOS:     targets.Linux,
   232  		TargetArch:   req.Arch,
   233  		VMType:       "qemu", // TODO: support others.
   234  		KernelDir:    *flagRepository,
   235  		OutputDir:    *flagOutput,
   236  		Compiler:     "clang",
   237  		Linker:       "ld.lld",
   238  		UserspaceDir: "/disk-images/buildroot_amd64_2024.09", // See the Dockerfile.
   239  		Config:       kernelConfig,
   240  		Tracer:       tracer,
   241  	}
   242  	tracer.Log("started build: %q", req)
   243  	info, err := build.Image(params)
   244  	tracer.Log("compiler: %q", info.CompilerID)
   245  	tracer.Log("signature: %q", info.Signature)
   246  	// We can fill this regardless of whether it succeeded.
   247  	ret := &BuildResult{
   248  		Compiler: info.CompilerID,
   249  	}
   250  	ret.Config, _ = os.ReadFile(filepath.Join(*flagOutput, "kernel.config"))
   251  	if err != nil {
   252  		ret.Finding = &api.NewFinding{
   253  			SessionID: *flagSession,
   254  			TestName:  *flagTestName,
   255  			Title:     "kernel build error",
   256  		}
   257  		var kernelError *build.KernelError
   258  		var verboseError *osutil.VerboseError
   259  		switch {
   260  		case errors.As(err, &kernelError):
   261  			tracer.Log("kernel error: %q / %s", kernelError.Report, kernelError.Output)
   262  			ret.Finding.Report = kernelError.Report
   263  			ret.Finding.Log = kernelError.Output
   264  			return ret, nil
   265  		case errors.As(err, &verboseError):
   266  			tracer.Log("verbose error: %s / %s", verboseError, verboseError.Output)
   267  			ret.Finding.Report = []byte(verboseError.Error())
   268  			ret.Finding.Log = verboseError.Output
   269  			return ret, nil
   270  		default:
   271  			tracer.Log("other error: %v", err)
   272  		}
   273  		return nil, err
   274  	}
   275  	tracer.Log("build finished successfully")
   276  
   277  	err = saveSymbolHashes(tracer)
   278  	if err != nil {
   279  		tracer.Log("failed to save symbol hashes: %s", err)
   280  	}
   281  	// Note: Output directory has the following structure:
   282  	//   |-- image
   283  	//   |-- symbol_hashes.json
   284  	//   |-- kernel
   285  	//   |-- kernel.config
   286  	//   `-- obj
   287  	//      `-- vmlinux
   288  	return ret, nil
   289  }
   290  
   291  func saveSymbolHashes(tracer debugtracer.DebugTracer) error {
   292  	hashes, err := build.ElfSymbolHashes(filepath.Join(*flagRepository, "vmlinux.o"))
   293  	if err != nil {
   294  		return fmt.Errorf("failed to query symbol hashes: %w", err)
   295  	}
   296  	tracer.Log("extracted hashes for %d text symbols and %d data symbols",
   297  		len(hashes.Text), len(hashes.Data))
   298  	file, err := os.Create(filepath.Join(*flagOutput, "symbol_hashes.json"))
   299  	if err != nil {
   300  		return fmt.Errorf("failed to open symbol_hashes.json: %w", err)
   301  	}
   302  	defer file.Close()
   303  	err = json.NewEncoder(file).Encode(hashes)
   304  	if err != nil {
   305  		return fmt.Errorf("failed to serialize: %w", err)
   306  	}
   307  	return nil
   308  }
   309  
   310  func ensureFlags(args ...string) {
   311  	for i := 0; i+1 < len(args); i += 2 {
   312  		if args[i] == "" {
   313  			app.Fatalf("%s must be set", args[i+1])
   314  		}
   315  	}
   316  }