code-intelligence.com/cifuzz@v0.40.0/internal/bundler/bundler.go (about)

     1  package bundler
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/pkg/errors"
    11  
    12  	"code-intelligence.com/cifuzz/internal/bundler/archive"
    13  	"code-intelligence.com/cifuzz/internal/cmdutils"
    14  	"code-intelligence.com/cifuzz/internal/config"
    15  	"code-intelligence.com/cifuzz/pkg/log"
    16  	"code-intelligence.com/cifuzz/pkg/vcs"
    17  	"code-intelligence.com/cifuzz/util/fileutil"
    18  	"code-intelligence.com/cifuzz/util/sliceutil"
    19  )
    20  
    21  // The (possibly empty) directory inside the fuzzing artifact archive that will
    22  // be the fuzzers working directory.
    23  const archiveWorkDirPath = "work_dir"
    24  
    25  type Bundler struct {
    26  	opts *Opts
    27  }
    28  
    29  func New(opts *Opts) *Bundler {
    30  	return &Bundler{opts: opts}
    31  }
    32  
    33  func (b *Bundler) Bundle() (string, error) {
    34  	var err error
    35  
    36  	// Create temp dir
    37  	b.opts.tempDir, err = os.MkdirTemp("", "cifuzz-bundle-")
    38  	if err != nil {
    39  		return "", errors.WithStack(err)
    40  	}
    41  	defer fileutil.Cleanup(b.opts.tempDir)
    42  
    43  	var bundle *os.File
    44  	bundle, err = b.createEmptyBundle()
    45  	if err != nil {
    46  		return "", err
    47  	}
    48  	// if an error occurs during bundling we should make sure that
    49  	// the bundle gets removed
    50  	defer func() {
    51  		bundle.Close()
    52  		if err != nil {
    53  			os.Remove(bundle.Name())
    54  		}
    55  	}()
    56  
    57  	// Create archive writer
    58  	bufWriter := bufio.NewWriter(bundle)
    59  	archiveWriter := archive.NewTarArchiveWriter(bufWriter, true)
    60  
    61  	var fuzzers []*archive.Fuzzer
    62  	switch b.opts.BuildSystem {
    63  	case config.BuildSystemCMake, config.BuildSystemBazel, config.BuildSystemOther:
    64  		fuzzers, err = newLibfuzzerBundler(b.opts, archiveWriter).bundle()
    65  	case config.BuildSystemMaven, config.BuildSystemGradle:
    66  		fuzzers, err = newJazzerBundler(b.opts, archiveWriter).bundle()
    67  	default:
    68  		err = errors.Errorf("Unknown build system for bundler: %s", b.opts.BuildSystem)
    69  	}
    70  	if err != nil {
    71  		return "", err
    72  	}
    73  
    74  	dockerImageUsedInBundle := b.determineDockerImageForBundle()
    75  	err = b.createMetadataFileInArchive(fuzzers, archiveWriter, dockerImageUsedInBundle)
    76  	if err != nil {
    77  		return "", err
    78  	}
    79  
    80  	err = b.createWorkDirInArchive(archiveWriter)
    81  	if err != nil {
    82  		return "", err
    83  	}
    84  
    85  	err = b.copyAdditionalFilesToArchive(archiveWriter)
    86  	if err != nil {
    87  		return "", err
    88  	}
    89  
    90  	// Container bundle does not define build.log?
    91  	if b.opts.BundleBuildLogFile != "" {
    92  		err = archiveWriter.WriteFile("build.log", b.opts.BundleBuildLogFile)
    93  		if err != nil {
    94  			return "", errors.WithStack(err)
    95  		}
    96  	}
    97  
    98  	err = archiveWriter.Close()
    99  	if err != nil {
   100  		return "", errors.WithStack(err)
   101  	}
   102  	err = bufWriter.Flush()
   103  	if err != nil {
   104  		return "", errors.WithStack(err)
   105  	}
   106  	err = bundle.Close()
   107  	if err != nil {
   108  		return "", errors.WithStack(err)
   109  	}
   110  
   111  	return bundle.Name(), nil
   112  }
   113  
   114  func (b *Bundler) createEmptyBundle() (*os.File, error) {
   115  	archiveExt := ".tar.gz"
   116  
   117  	if b.opts.OutputPath != "" {
   118  		// do nothing
   119  	} else if len(b.opts.FuzzTests) == 1 {
   120  		fuzzTestName := strings.ReplaceAll(b.opts.FuzzTests[0], "::", "_")
   121  		b.opts.OutputPath = filepath.Base(fuzzTestName) + archiveExt
   122  	} else {
   123  		b.opts.OutputPath = "fuzz_tests" + archiveExt
   124  	}
   125  
   126  	bundle, err := os.Create(b.opts.OutputPath)
   127  	if err != nil {
   128  		return nil, errors.Wrap(errors.WithStack(err), "failed to create fuzzing artifact archive")
   129  	}
   130  
   131  	return bundle, nil
   132  }
   133  
   134  func (b *Bundler) determineDockerImageForBundle() string {
   135  	dockerImageUsedInBundle := b.opts.DockerImage
   136  	if dockerImageUsedInBundle == "" {
   137  		switch b.opts.BuildSystem {
   138  		case config.BuildSystemCMake, config.BuildSystemBazel, config.BuildSystemOther:
   139  			// Use default Ubuntu Docker image for CMake, Bazel, and other build systems
   140  			dockerImageUsedInBundle = "ubuntu:rolling"
   141  		case config.BuildSystemMaven, config.BuildSystemGradle:
   142  			// Maven and Gradle should use a Docker image with Java
   143  			dockerImageUsedInBundle = "eclipse-temurin:20"
   144  		}
   145  	}
   146  
   147  	return dockerImageUsedInBundle
   148  }
   149  
   150  func (b *Bundler) createMetadataFileInArchive(fuzzers []*archive.Fuzzer, archiveWriter archive.ArchiveWriter, dockerImageUsedInBundle string) error {
   151  	// Create and add the top-level metadata file.
   152  	metadata := &archive.Metadata{
   153  		Fuzzers: fuzzers,
   154  		RunEnvironment: &archive.RunEnvironment{
   155  			Docker: dockerImageUsedInBundle,
   156  		},
   157  		CodeRevision: b.getCodeRevision(),
   158  	}
   159  
   160  	metadataYamlContent, err := metadata.ToYaml()
   161  	if err != nil {
   162  		return err
   163  	}
   164  	metadataYamlPath := filepath.Join(b.opts.tempDir, archive.MetadataFileName)
   165  	err = os.WriteFile(metadataYamlPath, metadataYamlContent, 0o644)
   166  	if err != nil {
   167  		return errors.Wrapf(errors.WithStack(err), "failed to write %s", archive.MetadataFileName)
   168  	}
   169  	err = archiveWriter.WriteFile(archive.MetadataFileName, metadataYamlPath)
   170  	if err != nil {
   171  		return err
   172  	}
   173  
   174  	return nil
   175  }
   176  
   177  func (b *Bundler) createWorkDirInArchive(archiveWriter archive.ArchiveWriter) error {
   178  	// The fuzzing artifact archive spec requires this directory even if it is empty.
   179  	tempWorkDirPath := filepath.Join(b.opts.tempDir, archiveWorkDirPath)
   180  	err := os.Mkdir(tempWorkDirPath, 0o755)
   181  	if err != nil {
   182  		return errors.WithStack(err)
   183  	}
   184  	err = archiveWriter.WriteDir(archiveWorkDirPath, tempWorkDirPath)
   185  	if err != nil {
   186  		return err
   187  	}
   188  
   189  	return nil
   190  }
   191  
   192  func (b *Bundler) copyAdditionalFilesToArchive(archiveWriter archive.ArchiveWriter) error {
   193  	for _, arg := range b.opts.AdditionalFiles {
   194  		source, target, err := parseAdditionalFilesArgument(arg)
   195  		if err != nil {
   196  			return err
   197  		}
   198  
   199  		if !filepath.IsAbs(source) {
   200  			source = filepath.Join(b.opts.ProjectDir, source)
   201  		}
   202  
   203  		if fileutil.IsDir(source) {
   204  			err = archiveWriter.WriteDir(target, source)
   205  			if err != nil {
   206  				return err
   207  			}
   208  		} else {
   209  			err = archiveWriter.WriteFile(target, source)
   210  			if err != nil {
   211  				return err
   212  			}
   213  		}
   214  	}
   215  
   216  	return nil
   217  }
   218  
   219  // getCodeRevision returns the code revision of the project, if it can be
   220  // determined. If it cannot be determined, nil is returned.
   221  func (b *Bundler) getCodeRevision() *archive.CodeRevision {
   222  	var err error
   223  	var gitCommit string
   224  	var gitBranch string
   225  
   226  	if b.opts.Commit == "" {
   227  		gitCommit, err = vcs.GitCommit()
   228  		if err != nil {
   229  			// if this returns an error (e.g. if users don't have git installed), we
   230  			// don't want to fail the bundle creation, so we just log that we
   231  			// couldn't get the git commit and branch and continue without it.
   232  			log.Debugf("failed to get Git commit. continuing without Git commit and branch. error: %+v",
   233  				cmdutils.WrapSilentError(err))
   234  			return nil
   235  		}
   236  	} else {
   237  		gitCommit = b.opts.Commit
   238  	}
   239  
   240  	if b.opts.Branch == "" {
   241  		gitBranch, err = vcs.GitBranch()
   242  		if err != nil {
   243  			log.Debugf("failed to get Git branch. continuing without Git commit and branch. error: %+v",
   244  				cmdutils.WrapSilentError(err))
   245  			return nil
   246  		}
   247  	} else {
   248  		gitBranch = b.opts.Branch
   249  	}
   250  
   251  	if vcs.GitIsDirty() {
   252  		log.Warnf("The Git repository has uncommitted changes. Archive metadata may be inaccurate.")
   253  	}
   254  
   255  	return &archive.CodeRevision{
   256  		Git: &archive.GitRevision{
   257  			Commit: gitCommit,
   258  			Branch: gitBranch,
   259  		},
   260  	}
   261  }
   262  
   263  func prepareSeeds(seedCorpusDirs []string, archiveSeedsDir string, archiveWriter archive.ArchiveWriter) error {
   264  	var targetDirs []string
   265  	for _, sourceDir := range seedCorpusDirs {
   266  		// Put the seeds into subdirectories of the "seeds" directory
   267  		// to avoid seeds with the same name to override each other.
   268  
   269  		// Choose a name for the target directory which wasn't used
   270  		// before
   271  		basename := filepath.Join(archiveSeedsDir, filepath.Base(sourceDir))
   272  		targetDir := basename
   273  		i := 1
   274  		for sliceutil.Contains(targetDirs, targetDir) {
   275  			targetDir = fmt.Sprintf("%s-%d", basename, i)
   276  			i++
   277  		}
   278  		targetDirs = append(targetDirs, targetDir)
   279  
   280  		// Add the seeds of the seed corpus directory to the target directory
   281  		err := archiveWriter.WriteDir(targetDir, sourceDir)
   282  		if err != nil {
   283  			return err
   284  		}
   285  	}
   286  	return nil
   287  }
   288  
   289  func parseAdditionalFilesArgument(arg string) (string, string, error) {
   290  	var source, target string
   291  	parts := strings.Split(arg, ";")
   292  
   293  	if len(parts) == 1 {
   294  		// if there is no ; separator just use the work_dir
   295  		// handles "test.txt"
   296  		source = parts[0]
   297  		target = filepath.Join(archiveWorkDirPath, filepath.Base(arg))
   298  	} else {
   299  		// handles test.txt;test2.txt
   300  		source = parts[0]
   301  		target = parts[1]
   302  	}
   303  
   304  	if len(parts) > 2 || source == "" || target == "" {
   305  		return "", "", errors.New("could not parse '--add' argument")
   306  	}
   307  
   308  	if filepath.IsAbs(target) {
   309  		return "", "", errors.New("when using '--add source;target', target has to be a relative path")
   310  	}
   311  
   312  	return source, target, nil
   313  }