github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/workflow/fuzz-step/main.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 main
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/json"
    10  	"errors"
    11  	"flag"
    12  	"fmt"
    13  	"io"
    14  	"net/http"
    15  	"os"
    16  	"path/filepath"
    17  	"regexp"
    18  	"time"
    19  
    20  	"github.com/google/syzkaller/pkg/build"
    21  	"github.com/google/syzkaller/pkg/db"
    22  	"github.com/google/syzkaller/pkg/log"
    23  	"github.com/google/syzkaller/pkg/manager"
    24  	"github.com/google/syzkaller/pkg/mgrconfig"
    25  	"github.com/google/syzkaller/pkg/osutil"
    26  	"github.com/google/syzkaller/prog"
    27  	"github.com/google/syzkaller/syz-cluster/pkg/api"
    28  	"github.com/google/syzkaller/syz-cluster/pkg/app"
    29  	"github.com/google/syzkaller/syz-cluster/pkg/fuzzconfig"
    30  	"golang.org/x/sync/errgroup"
    31  )
    32  
    33  var (
    34  	flagConfig       = flag.String("config", "", "path to the fuzz config")
    35  	flagSession      = flag.String("session", "", "session ID")
    36  	flagBaseBuild    = flag.String("base_build", "", "base build ID")
    37  	flagPatchedBuild = flag.String("patched_build", "", "patched build ID")
    38  	flagTime         = flag.String("time", "1h", "how long to fuzz")
    39  	flagWorkdir      = flag.String("workdir", "/workdir", "base workdir path")
    40  )
    41  
    42  func main() {
    43  	flag.Parse()
    44  	if *flagConfig == "" || *flagSession == "" || *flagTime == "" {
    45  		app.Fatalf("--config, --session and --time must be set")
    46  	}
    47  	client := app.DefaultClient()
    48  	d, err := time.ParseDuration(*flagTime)
    49  	if err != nil {
    50  		app.Fatalf("invalid --time: %v", err)
    51  	}
    52  	if !prog.GitRevisionKnown() {
    53  		log.Fatalf("the binary is built without the git revision information")
    54  	}
    55  
    56  	config := readFuzzConfig()
    57  	ctx := context.Background()
    58  	if err := reportStatus(ctx, config, client, api.TestRunning, nil); err != nil {
    59  		app.Fatalf("failed to report the test: %v", err)
    60  	}
    61  
    62  	artifactsDir := filepath.Join(*flagWorkdir, "artifacts")
    63  	osutil.MkdirAll(artifactsDir)
    64  	store := &manager.DiffFuzzerStore{BasePath: artifactsDir}
    65  
    66  	// We want to only cancel the run() operation in order to be able to also report
    67  	// the final test result back.
    68  	runCtx, cancel := context.WithTimeout(context.Background(), d)
    69  	defer cancel()
    70  	err = run(runCtx, config, client, d, store)
    71  	status := api.TestPassed // TODO: what about TestFailed?
    72  	if errors.Is(err, errSkipFuzzing) {
    73  		status = api.TestSkipped
    74  	} else if err != nil && !errors.Is(err, context.DeadlineExceeded) {
    75  		app.Errorf("the step failed: %v", err)
    76  		status = api.TestError
    77  	}
    78  	log.Logf(0, "fuzzing is finished")
    79  	logFinalState(store)
    80  	if err := reportStatus(ctx, config, client, status, store); err != nil {
    81  		app.Fatalf("failed to update the test: %v", err)
    82  	}
    83  }
    84  
    85  func readFuzzConfig() *api.FuzzConfig {
    86  	raw, err := os.ReadFile(*flagConfig)
    87  	if err != nil {
    88  		app.Fatalf("failed to read config: %v", err)
    89  		return nil
    90  	}
    91  	var req api.FuzzConfig
    92  	err = json.Unmarshal(raw, &req)
    93  	if err != nil {
    94  		app.Fatalf("failed to unmarshal request: %v, %s", err, raw)
    95  		return nil
    96  	}
    97  	return &req
    98  }
    99  
   100  func logFinalState(store *manager.DiffFuzzerStore) {
   101  	log.Logf(0, "status at the end:\n%s", store.PlainTextDump())
   102  
   103  	// There can be findings that we did not report only because we failed
   104  	// to come up with a reproducer.
   105  	// Let's log such cases so that it's easier to find and manually review them.
   106  	const countCutOff = 10
   107  	for _, bug := range store.List() {
   108  		if bug.Base.Crashes == 0 && bug.Patched.Crashes >= countCutOff {
   109  			log.Logf(0, "possibly patched-only: %s", bug.Title)
   110  		}
   111  	}
   112  }
   113  
   114  var errSkipFuzzing = errors.New("skip")
   115  
   116  func run(baseCtx context.Context, config *api.FuzzConfig, client *api.Client,
   117  	timeout time.Duration, store *manager.DiffFuzzerStore) error {
   118  	series, err := client.GetSessionSeries(baseCtx, *flagSession)
   119  	if err != nil {
   120  		return fmt.Errorf("failed to query the series info: %w", err)
   121  	}
   122  
   123  	// Until there's a way to pass the log.Logger object and capture all,
   124  	// use the global log collection.
   125  	const MB = 1000000
   126  	log.EnableLogCaching(100000, 10*MB)
   127  
   128  	base, patched, err := generateConfigs(config)
   129  	if err != nil {
   130  		return fmt.Errorf("failed to load configs: %w", err)
   131  	}
   132  
   133  	baseSymbols, patchedSymbols, err := readSymbolHashes()
   134  	if err != nil {
   135  		app.Errorf("failed to read symbol hashes: %v", err)
   136  	}
   137  
   138  	if shouldSkipFuzzing(baseSymbols, patchedSymbols) {
   139  		return errSkipFuzzing
   140  	}
   141  	manager.PatchFocusAreas(patched, series.PatchBodies(), baseSymbols.Text, patchedSymbols.Text)
   142  
   143  	if len(config.CorpusURLs) > 0 {
   144  		err := prepareCorpus(baseCtx, patched.Workdir, config.CorpusURLs, patched.Target)
   145  		if err != nil {
   146  			app.Errorf("failed to download the corpus: %v", err)
   147  		}
   148  	}
   149  
   150  	eg, ctx := errgroup.WithContext(baseCtx)
   151  	bugs := make(chan *manager.UniqueBug)
   152  	baseCrashes := make(chan string, 16)
   153  	eg.Go(func() error {
   154  		defer log.Logf(0, "bug reporting terminated")
   155  		for {
   156  			select {
   157  			case title := <-baseCrashes:
   158  				err := client.UploadBaseFinding(ctx, &api.BaseFindingInfo{
   159  					BuildID: *flagBaseBuild,
   160  					Title:   title,
   161  				})
   162  				if err != nil {
   163  					app.Errorf("failed to report a base kernel crash %q: %v", title, err)
   164  				}
   165  			case bug := <-bugs:
   166  				err := reportFinding(ctx, config, client, bug)
   167  				if err != nil {
   168  					app.Errorf("failed to report a finding %q: %v", bug.Report.Title, err)
   169  				}
   170  			case <-ctx.Done():
   171  				return nil
   172  			}
   173  		}
   174  	})
   175  	eg.Go(func() error {
   176  		defer log.Logf(0, "diff fuzzing terminated")
   177  		return manager.RunDiffFuzzer(ctx, base, patched, manager.DiffFuzzerConfig{
   178  			Debug:              false,
   179  			PatchedOnly:        bugs,
   180  			BaseCrashes:        baseCrashes,
   181  			Store:              store,
   182  			MaxTriageTime:      timeout / 2,
   183  			FuzzToReachPatched: fuzzToReachPatched(config),
   184  			IgnoreCrash: func(ctx context.Context, title string) (bool, error) {
   185  				if !titleMatchesFilter(config, title) {
   186  					log.Logf(1, "crash %q doesn't match the filter", title)
   187  					return true, nil
   188  				}
   189  				ret, err := client.BaseFindingStatus(ctx, &api.BaseFindingInfo{
   190  					BuildID: *flagBaseBuild,
   191  					Title:   title,
   192  				})
   193  				if err != nil {
   194  					return false, err
   195  				}
   196  				if ret.Observed {
   197  					log.Logf(1, "crash %q is already known", title)
   198  				}
   199  				return ret.Observed, nil
   200  			},
   201  		})
   202  	})
   203  	const (
   204  		updatePeriod         = 5 * time.Minute
   205  		artifactUploadPeriod = 30 * time.Minute
   206  	)
   207  	lastArtifactUpdate := time.Now()
   208  	eg.Go(func() error {
   209  		defer log.Logf(0, "status reporting terminated")
   210  		for {
   211  			select {
   212  			case <-ctx.Done():
   213  				return nil
   214  			case <-time.After(updatePeriod):
   215  			}
   216  			var useStore *manager.DiffFuzzerStore
   217  			if time.Since(lastArtifactUpdate) > artifactUploadPeriod {
   218  				lastArtifactUpdate = time.Now()
   219  				useStore = store
   220  			}
   221  			err := reportStatus(ctx, config, client, api.TestRunning, useStore)
   222  			if err != nil {
   223  				app.Errorf("failed to update status: %v", err)
   224  			}
   225  		}
   226  	})
   227  	err = eg.Wait()
   228  	if errors.Is(err, manager.ErrPatchedAreaNotReached) {
   229  		// We did not reach the modified parts of the kernel, but that's fine.
   230  		return nil
   231  	}
   232  	return err
   233  }
   234  
   235  func prepareCorpus(ctx context.Context, workdir string, urls []string, target *prog.Target) error {
   236  	corpusFile := filepath.Join(workdir, "corpus.db")
   237  	var otherFiles []string
   238  	for i, url := range urls {
   239  		log.Logf(0, "downloading corpus #%d: %q", i+1, url)
   240  		downloadTo := corpusFile
   241  		if i > 0 {
   242  			downloadTo = fmt.Sprintf("%s.%d", corpusFile, i)
   243  			otherFiles = append(otherFiles, downloadTo)
   244  		}
   245  		out, err := os.Create(corpusFile)
   246  		if err != nil {
   247  			return err
   248  		}
   249  		defer out.Close()
   250  		req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   251  		if err != nil {
   252  			return err
   253  		}
   254  		resp, err := (&http.Client{}).Do(req)
   255  		if err != nil {
   256  			return err
   257  		}
   258  		defer resp.Body.Close()
   259  		if resp.StatusCode != http.StatusOK {
   260  			return fmt.Errorf("status is not 200: %s", resp.Status)
   261  		}
   262  		_, err = io.Copy(out, resp.Body)
   263  		if err != nil {
   264  			return err
   265  		}
   266  	}
   267  	if len(otherFiles) > 0 {
   268  		log.Logf(0, "merging corpuses")
   269  		skipped, err := db.Merge(corpusFile, otherFiles, target)
   270  		if err != nil {
   271  			return err
   272  		} else if len(skipped) > 0 {
   273  			log.Logf(0, "skipped %d entries", len(skipped))
   274  		}
   275  	}
   276  	return nil
   277  }
   278  
   279  func generateConfigs(config *api.FuzzConfig) (*mgrconfig.Config, *mgrconfig.Config, error) {
   280  	base, err := fuzzconfig.GenerateBase(config)
   281  	if err != nil {
   282  		return nil, nil, fmt.Errorf("failed to prepare base config: %w", err)
   283  	}
   284  	patched, err := fuzzconfig.GeneratePatched(config)
   285  	if err != nil {
   286  		return nil, nil, fmt.Errorf("failed to prepare patched config: %w", err)
   287  	}
   288  	base.Workdir = filepath.Join(*flagWorkdir, "base")
   289  	osutil.MkdirAll(base.Workdir)
   290  	patched.Workdir = filepath.Join(*flagWorkdir, "patched")
   291  	osutil.MkdirAll(patched.Workdir)
   292  	err = mgrconfig.Complete(base)
   293  	if err != nil {
   294  		return nil, nil, fmt.Errorf("failed to complete the base config: %w", err)
   295  	}
   296  	err = mgrconfig.Complete(patched)
   297  	if err != nil {
   298  		return nil, nil, fmt.Errorf("failed to complete the patched config: %w", err)
   299  	}
   300  	return base, patched, nil
   301  }
   302  
   303  func reportStatus(ctx context.Context, config *api.FuzzConfig, client *api.Client,
   304  	status string, store *manager.DiffFuzzerStore) error {
   305  	testName := getTestName(config)
   306  	testResult := &api.TestResult{
   307  		SessionID:      *flagSession,
   308  		TestName:       testName,
   309  		BaseBuildID:    *flagBaseBuild,
   310  		PatchedBuildID: *flagPatchedBuild,
   311  		Result:         status,
   312  		Log:            []byte(log.CachedLogOutput()),
   313  	}
   314  	err := client.UploadTestResult(ctx, testResult)
   315  	if err != nil {
   316  		return fmt.Errorf("failed to upload the status: %w", err)
   317  	}
   318  	if store == nil {
   319  		return nil
   320  	}
   321  	tarGzReader, err := compressArtifacts(store.BasePath)
   322  	if errors.Is(err, errWriteOverLimit) {
   323  		app.Errorf("the artifacts archive is too big to upload")
   324  	} else if err != nil {
   325  		return fmt.Errorf("failed to compress the artifacts dir: %w", err)
   326  	} else {
   327  		err = client.UploadTestArtifacts(ctx, *flagSession, testName, tarGzReader)
   328  		if err != nil {
   329  			return fmt.Errorf("failed to upload the status: %w", err)
   330  		}
   331  	}
   332  	return nil
   333  }
   334  
   335  func reportFinding(ctx context.Context, config *api.FuzzConfig, client *api.Client, bug *manager.UniqueBug) error {
   336  	finding := &api.NewFinding{
   337  		SessionID: *flagSession,
   338  		TestName:  getTestName(config),
   339  		Title:     bug.Report.Title,
   340  		Report:    bug.Report.Report,
   341  		Log:       bug.Report.Output,
   342  	}
   343  	if repro := bug.Repro; repro != nil {
   344  		if repro.Prog != nil {
   345  			finding.SyzRepro = repro.Prog.Serialize()
   346  			finding.SyzReproOpts = repro.Opts.Serialize()
   347  		}
   348  		if repro.CRepro {
   349  			var err error
   350  			finding.CRepro, err = repro.CProgram()
   351  			if err != nil {
   352  				app.Errorf("failed to generate C program: %v", err)
   353  			}
   354  		}
   355  	}
   356  	return client.UploadFinding(ctx, finding)
   357  }
   358  
   359  func getTestName(config *api.FuzzConfig) string {
   360  	return fmt.Sprintf("[%s] Fuzzing", config.Track)
   361  }
   362  
   363  var ignoreLinuxVariables = map[string]bool{
   364  	"raw_data": true, // from arch/x86/entry/vdso/vdso-image
   365  	// Build versions / timestamps.
   366  	"linux_banner": true,
   367  	"vermagic":     true,
   368  	"init_uts_ns":  true,
   369  }
   370  
   371  func shouldSkipFuzzing(base, patched build.SectionHashes) bool {
   372  	if len(base.Text) == 0 || len(patched.Text) == 0 {
   373  		// Likely, something went wrong during the kernel build step.
   374  		log.Logf(0, "skipped the binary equality check because some of them have 0 symbols")
   375  		return false
   376  	}
   377  	same := len(base.Text) == len(patched.Text) && len(base.Data) == len(patched.Data)
   378  	// For .text, demand all symbols to be equal.
   379  	for name, hash := range base.Text {
   380  		if patched.Text[name] != hash {
   381  			same = false
   382  			break
   383  		}
   384  	}
   385  	// For data sections ignore some of them.
   386  	for name, hash := range base.Data {
   387  		if !ignoreLinuxVariables[name] && patched.Data[name] != hash {
   388  			log.Logf(1, "symbol %q has different values in base vs patch", name)
   389  			same = false
   390  			break
   391  		}
   392  	}
   393  	if same {
   394  		log.Logf(0, "binaries are the same, no sense to do fuzzing")
   395  		return true
   396  	}
   397  	log.Logf(0, "binaries are different, continuing fuzzing")
   398  	return false
   399  }
   400  
   401  func titleMatchesFilter(config *api.FuzzConfig, title string) bool {
   402  	matched, err := regexp.MatchString(config.BugTitleRe, title)
   403  	if err != nil {
   404  		app.Fatalf("invalid BugTitleRe regexp: %v", err)
   405  	}
   406  	return matched
   407  }
   408  
   409  func readSymbolHashes() (base, patched build.SectionHashes, err error) {
   410  	// These are saved by the build step.
   411  	base, err = readSectionHashes("/base/symbol_hashes.json")
   412  	if err != nil {
   413  		return build.SectionHashes{}, build.SectionHashes{}, fmt.Errorf("failed to read base hashes: %w", err)
   414  	}
   415  	patched, err = readSectionHashes("/patched/symbol_hashes.json")
   416  	if err != nil {
   417  		return build.SectionHashes{}, build.SectionHashes{}, fmt.Errorf("failed to read patched hashes: %w", err)
   418  	}
   419  	log.Logf(0, "extracted %d text symbol hashes for base and %d for patched", len(base.Text), len(patched.Text))
   420  	return
   421  }
   422  
   423  func readSectionHashes(file string) (build.SectionHashes, error) {
   424  	f, err := os.Open(file)
   425  	if err != nil {
   426  		return build.SectionHashes{}, err
   427  	}
   428  	defer f.Close()
   429  
   430  	var data build.SectionHashes
   431  	err = json.NewDecoder(f).Decode(&data)
   432  	if err != nil {
   433  		return build.SectionHashes{}, err
   434  	}
   435  	return data, nil
   436  }
   437  
   438  func fuzzToReachPatched(config *api.FuzzConfig) time.Duration {
   439  	if config.SkipCoverCheck {
   440  		return 0
   441  	}
   442  	// Allow up to 30 minutes after the corpus triage to reach the patched code.
   443  	return time.Minute * 30
   444  }
   445  
   446  func compressArtifacts(dir string) (io.Reader, error) {
   447  	var buf bytes.Buffer
   448  	lw := &LimitedWriter{
   449  		writer: &buf,
   450  		// Don't create an archive larger than 64MB.
   451  		limit: 64 * 1000 * 1000,
   452  	}
   453  	err := osutil.TarGzDirectory(dir, lw)
   454  	if err != nil {
   455  		return nil, err
   456  	}
   457  	return &buf, nil
   458  }
   459  
   460  type LimitedWriter struct {
   461  	written int
   462  	limit   int
   463  	writer  io.Writer
   464  }
   465  
   466  var errWriteOverLimit = errors.New("the writer exceeded the limit")
   467  
   468  func (lw *LimitedWriter) Write(p []byte) (n int, err error) {
   469  	if len(p)+lw.written > lw.limit {
   470  		return 0, errWriteOverLimit
   471  	}
   472  	n, err = lw.writer.Write(p)
   473  	lw.written += n
   474  	return
   475  }