diff --git a/tests/fuzz/gitrepository_fuzzer.go b/controllers/gitrepository_controller_fuzz_test.go similarity index 77% rename from tests/fuzz/gitrepository_fuzzer.go rename to controllers/gitrepository_controller_fuzz_test.go index 1f7a89ba7..f16779f0f 100644 --- a/tests/fuzz/gitrepository_fuzzer.go +++ b/controllers/gitrepository_controller_fuzz_test.go @@ -1,5 +1,5 @@ -//go:build gofuzz -// +build gofuzz +//go:build gofuzz_libfuzzer +// +build gofuzz_libfuzzer /* Copyright 2022 The Flux authors @@ -61,7 +61,6 @@ import ( "github.com/fluxcd/pkg/gittestserver" "github.com/fluxcd/pkg/runtime/testenv" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" - "github.com/fluxcd/source-controller/controllers" ) var ( @@ -75,7 +74,7 @@ var ( cfg *rest.Config testEnv *testenv.Environment - storage *controllers.Storage + storage *Storage examplePublicKey []byte examplePrivateKey []byte @@ -87,277 +86,140 @@ var ( var testFiles embed.FS const ( - defaultBinVersion = "1.23" + defaultBinVersion = "1.24" lettersAndNumbers = "abcdefghijklmnopqrstuvwxyz123456789" lettersNumbersAndDash = "abcdefghijklmnopqrstuvwxyz123456789-" ) -func envtestBinVersion() string { - if binVersion := os.Getenv("ENVTEST_BIN_VERSION"); binVersion != "" { - return binVersion - } - return defaultBinVersion -} - -func ensureDependencies() error { - if _, err := os.Stat("/.dockerenv"); os.IsNotExist(err) { - return nil - } +// FuzzRandomGitFiles implements a fuzzer that +// targets the GitRepository reconciler. +func FuzzRandomGitFiles(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + initter.Do(func() { + utilruntime.Must(ensureDependencies()) + }) + + f := fuzz.NewConsumer(data) + namespace, deleteNamespace, err := createNamespace(f) + if err != nil { + return + } + defer deleteNamespace() - if os.Getenv("KUBEBUILDER_ASSETS") == "" { - binVersion := envtestBinVersion() - cmd := exec.Command("/usr/bin/bash", "-c", fmt.Sprintf(`go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest && \ - /root/go/bin/setup-envtest use -p path %s`, binVersion)) + gitServerURL, stopGitServer := createGitServer(f) + defer stopGitServer() - cmd.Env = append(os.Environ(), "GOPATH=/root/go") - assetsPath, err := cmd.Output() + fs := memfs.New() + gitrepo, err := git.Init(memory.NewStorage(), fs) if err != nil { - return err + panic(err) } - os.Setenv("KUBEBUILDER_ASSETS", string(assetsPath)) - } - - // Output all embedded testdata files - embedDirs := []string{"testdata/crd", "testdata/certs"} - for _, dir := range embedDirs { - err := os.MkdirAll(dir, 0o700) + wt, err := gitrepo.Worktree() if err != nil { - return fmt.Errorf("mkdir %s: %v", dir, err) + panic(err) } - templates, err := fs.ReadDir(testFiles, dir) + // Create random files for the git source + err = createRandomFiles(f, fs, wt) if err != nil { - return fmt.Errorf("reading embedded dir: %v", err) + return } - for _, template := range templates { - fileName := fmt.Sprintf("%s/%s", dir, template.Name()) - fmt.Println(fileName) - - data, err := testFiles.ReadFile(fileName) - if err != nil { - return fmt.Errorf("reading embedded file %s: %v", fileName, err) - } - - os.WriteFile(fileName, data, 0o600) - if err != nil { - return fmt.Errorf("writing %s: %v", fileName, err) - } + commit, err := pushFilesToGit(gitrepo, wt, gitServerURL.String()) + if err != nil { + return } - } - - startEnvServer(func(m manager.Manager) { - utilruntime.Must((&controllers.GitRepositoryReconciler{ - Client: m.GetClient(), - Storage: storage, - }).SetupWithManager(m)) - }) - - return nil -} - -func startEnvServer(setupReconcilers func(manager.Manager)) *envtest.Environment { - testEnv := &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("testdata", "crd")}, - } - fmt.Println("Starting the test environment") - cfg, err := testEnv.Start() - if err != nil { - panic(fmt.Sprintf("Failed to start the test environment manager: %v", err)) - } - - utilruntime.Must(loadExampleKeys()) - utilruntime.Must(sourcev1.AddToScheme(scheme.Scheme)) - - tmpStoragePath, err := os.MkdirTemp("", "source-controller-storage-") - if err != nil { - panic(err) - } - defer os.RemoveAll(tmpStoragePath) - storage, err = controllers.NewStorage(tmpStoragePath, "localhost:5050", time.Minute*1, 2) - if err != nil { - panic(err) - } - // serve artifacts from the filesystem, as done in main.go - fs := http.FileServer(http.Dir(tmpStoragePath)) - http.Handle("/", fs) - go http.ListenAndServe(":5050", nil) - - cert, err := tls.X509KeyPair(examplePublicKey, examplePrivateKey) - if err != nil { - panic(err) - } - - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM(exampleCA) - - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{cert}, - RootCAs: caCertPool, - } - tlsConfig.BuildNameToCertificate() - - var transport = httptransport.NewClient(&http.Client{ - Transport: &http.Transport{ - TLSClientConfig: tlsConfig, - }, - }) - gitclient.InstallProtocol("https", transport) - - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) - if err != nil { - panic(err) - } - if k8sClient == nil { - panic("cfg is nil but should not be") - } - - k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ - Scheme: scheme.Scheme, - }) - if err != nil { - panic(err) - } - - setupReconcilers(k8sManager) - - time.Sleep(2 * time.Second) - go func() { - fmt.Println("Starting k8sManager...") - utilruntime.Must(k8sManager.Start(context.TODO())) - }() - - return testEnv -} + created, err := createGitRepository(f, gitServerURL.String(), commit.String(), namespace.Name) + if err != nil { + return + } + err = k8sClient.Create(context.Background(), created) + if err != nil { + return + } + defer k8sClient.Delete(context.Background(), created) -// FuzzRandomGitFiles implements a fuzzer that -// targets the GitRepository reconciler. -func FuzzRandomGitFiles(data []byte) int { - initter.Do(func() { - utilruntime.Must(ensureDependencies()) + // Let the reconciler do its thing: + time.Sleep(60 * time.Millisecond) }) - - f := fuzz.NewConsumer(data) - namespace, deleteNamespace, err := createNamespace(f) - if err != nil { - return 0 - } - defer deleteNamespace() - - gitServerURL, stopGitServer := createGitServer(f) - defer stopGitServer() - - fs := memfs.New() - gitrepo, err := git.Init(memory.NewStorage(), fs) - if err != nil { - panic(err) - } - wt, err := gitrepo.Worktree() - if err != nil { - panic(err) - } - - // Create random files for the git source - err = createRandomFiles(f, fs, wt) - if err != nil { - return 0 - } - - commit, err := pushFilesToGit(gitrepo, wt, gitServerURL.String()) - if err != nil { - return 0 - } - created, err := createGitRepository(f, gitServerURL.String(), commit.String(), namespace.Name) - if err != nil { - return 0 - } - err = k8sClient.Create(context.Background(), created) - if err != nil { - return 0 - } - defer k8sClient.Delete(context.Background(), created) - - // Let the reconciler do its thing: - time.Sleep(60 * time.Millisecond) - - return 1 } // FuzzGitResourceObject implements a fuzzer that targets // the GitRepository reconciler. -func FuzzGitResourceObject(data []byte) int { - initter.Do(func() { - utilruntime.Must(ensureDependencies()) - }) - - f := fuzz.NewConsumer(data) - - // Create this early because if it fails, then the fuzzer - // does not need to proceed. - repository := &sourcev1.GitRepository{} - err := f.GenerateStruct(repository) - if err != nil { - return 0 - } +func FuzzGitResourceObject(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + initter.Do(func() { + utilruntime.Must(ensureDependencies()) + }) + + f := fuzz.NewConsumer(data) + + // Create this early because if it fails, then the fuzzer + // does not need to proceed. + repository := &sourcev1.GitRepository{} + err := f.GenerateStruct(repository) + if err != nil { + return + } - metaName, err := f.GetStringFrom(lettersNumbersAndDash, 59) - if err != nil { - return 0 - } + metaName, err := f.GetStringFrom(lettersNumbersAndDash, 59) + if err != nil { + return + } - gitServerURL, stopGitServer := createGitServer(f) - defer stopGitServer() + gitServerURL, stopGitServer := createGitServer(f) + defer stopGitServer() - fs := memfs.New() - gitrepo, err := git.Init(memory.NewStorage(), fs) - if err != nil { - return 0 - } - wt, err := gitrepo.Worktree() - if err != nil { - return 0 - } + fs := memfs.New() + gitrepo, err := git.Init(memory.NewStorage(), fs) + if err != nil { + return + } + wt, err := gitrepo.Worktree() + if err != nil { + return + } - // Add a file - ff, _ := fs.Create("fixture") - _ = ff.Close() - _, err = wt.Add(fs.Join("fixture")) - if err != nil { - return 0 - } + // Add a file + ff, _ := fs.Create("fixture") + _ = ff.Close() + _, err = wt.Add(fs.Join("fixture")) + if err != nil { + return + } - commit, err := pushFilesToGit(gitrepo, wt, gitServerURL.String()) - if err != nil { - return 0 - } + commit, err := pushFilesToGit(gitrepo, wt, gitServerURL.String()) + if err != nil { + return + } - namespace, deleteNamespace, err := createNamespace(f) - if err != nil { - return 0 - } - defer deleteNamespace() + namespace, deleteNamespace, err := createNamespace(f) + if err != nil { + return + } + defer deleteNamespace() - repository.Spec.URL = gitServerURL.String() - repository.Spec.Verification.Mode = "head" - repository.Spec.SecretRef = nil + repository.Spec.URL = gitServerURL.String() + repository.Spec.Verification.Mode = "head" + repository.Spec.SecretRef = nil - reference := &sourcev1.GitRepositoryRef{Branch: "some-branch"} - reference.Commit = strings.Replace(reference.Commit, "", commit.String(), 1) - repository.Spec.Reference = reference + reference := &sourcev1.GitRepositoryRef{Branch: "some-branch"} + reference.Commit = strings.Replace(reference.Commit, "", commit.String(), 1) + repository.Spec.Reference = reference - repository.ObjectMeta = metav1.ObjectMeta{ - Name: metaName, - Namespace: namespace.Name, - } - err = k8sClient.Create(context.Background(), repository) - if err != nil { - return 0 - } - defer k8sClient.Delete(context.Background(), repository) + repository.ObjectMeta = metav1.ObjectMeta{ + Name: metaName, + Namespace: namespace.Name, + } + err = k8sClient.Create(context.Background(), repository) + if err != nil { + return + } + defer k8sClient.Delete(context.Background(), repository) - // Let the reconciler do its thing. - time.Sleep(50 * time.Millisecond) - return 1 + // Let the reconciler do its thing. + time.Sleep(50 * time.Millisecond) + }) } func loadExampleKeys() (err error) { @@ -527,3 +389,141 @@ func createRandomFiles(f *fuzz.ConsumeFuzzer, fs billy.Filesystem, wt *git.Workt } return nil } + +func envtestBinVersion() string { + if binVersion := os.Getenv("ENVTEST_BIN_VERSION"); binVersion != "" { + return binVersion + } + return defaultBinVersion +} + +func ensureDependencies() error { + if _, err := os.Stat("/.dockerenv"); os.IsNotExist(err) { + return nil + } + + if os.Getenv("KUBEBUILDER_ASSETS") == "" { + binVersion := envtestBinVersion() + cmd := exec.Command("/usr/bin/bash", "-c", fmt.Sprintf(`go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest && \ + /root/go/bin/setup-envtest use -p path %s`, binVersion)) + + cmd.Env = append(os.Environ(), "GOPATH=/root/go") + assetsPath, err := cmd.Output() + if err != nil { + return err + } + os.Setenv("KUBEBUILDER_ASSETS", string(assetsPath)) + } + + // Output all embedded testdata files + embedDirs := []string{"testdata/crd", "testdata/certs"} + for _, dir := range embedDirs { + err := os.MkdirAll(dir, 0o700) + if err != nil { + return fmt.Errorf("mkdir %s: %v", dir, err) + } + + templates, err := fs.ReadDir(testFiles, dir) + if err != nil { + return fmt.Errorf("reading embedded dir: %v", err) + } + + for _, template := range templates { + fileName := fmt.Sprintf("%s/%s", dir, template.Name()) + fmt.Println(fileName) + + data, err := testFiles.ReadFile(fileName) + if err != nil { + return fmt.Errorf("reading embedded file %s: %v", fileName, err) + } + + os.WriteFile(fileName, data, 0o600) + if err != nil { + return fmt.Errorf("writing %s: %v", fileName, err) + } + } + } + + startEnvServer(func(m manager.Manager) { + utilruntime.Must((&GitRepositoryReconciler{ + Client: m.GetClient(), + Storage: storage, + }).SetupWithManager(m)) + }) + + return nil +} + +func startEnvServer(setupReconcilers func(manager.Manager)) *envtest.Environment { + testEnv := &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("testdata", "crd")}, + } + fmt.Println("Starting the test environment") + cfg, err := testEnv.Start() + if err != nil { + panic(fmt.Sprintf("Failed to start the test environment manager: %v", err)) + } + + utilruntime.Must(loadExampleKeys()) + utilruntime.Must(sourcev1.AddToScheme(scheme.Scheme)) + + tmpStoragePath, err := os.MkdirTemp("", "source-controller-storage-") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpStoragePath) + storage, err = NewStorage(tmpStoragePath, "localhost:5050", time.Minute*1, 2) + if err != nil { + panic(err) + } + // serve artifacts from the filesystem, as done in main.go + fs := http.FileServer(http.Dir(tmpStoragePath)) + http.Handle("/", fs) + go http.ListenAndServe(":5050", nil) + + cert, err := tls.X509KeyPair(examplePublicKey, examplePrivateKey) + if err != nil { + panic(err) + } + + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(exampleCA) + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + } + tlsConfig.BuildNameToCertificate() + + var transport = httptransport.NewClient(&http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + }) + gitclient.InstallProtocol("https", transport) + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + if err != nil { + panic(err) + } + if k8sClient == nil { + panic("cfg is nil but should not be") + } + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + if err != nil { + panic(err) + } + + setupReconcilers(k8sManager) + + time.Sleep(2 * time.Second) + go func() { + fmt.Println("Starting k8sManager...") + utilruntime.Must(k8sManager.Start(context.TODO())) + }() + + return testEnv +} diff --git a/tests/fuzz/Dockerfile.builder b/tests/fuzz/Dockerfile.builder index c98a6d819..a09a8e6e2 100644 --- a/tests/fuzz/Dockerfile.builder +++ b/tests/fuzz/Dockerfile.builder @@ -1,6 +1,9 @@ -FROM gcr.io/oss-fuzz-base/base-builder-go-codeintelligencetesting +FROM gcr.io/oss-fuzz-base/base-builder-go + +RUN apt-get update && apt-get install -y cmake pkg-config COPY ./ $GOPATH/src/github.com/fluxcd/source-controller/ COPY ./tests/fuzz/oss_fuzz_build.sh $SRC/build.sh +COPY tests/fuzz/compile_native_go_fuzzer /usr/local/bin/ WORKDIR $SRC diff --git a/tests/fuzz/README.md b/tests/fuzz/README.md new file mode 100644 index 000000000..2ae2cddb6 --- /dev/null +++ b/tests/fuzz/README.md @@ -0,0 +1,82 @@ +# fuzz testing + +Flux is part of Google's [oss fuzz] program which provides continuous fuzzing for +open source projects. + +The long running fuzzing execution is configured in the [oss-fuzz repository]. +Shorter executions are done on a per-PR basis, configured as a [github workflow]. + +### Testing locally + +Build fuzzers: + +```bash +make fuzz-build +``` +All fuzzers will be built into `./build/fuzz/out`. + +Smoke test fuzzers: + +All the fuzzers will be built and executed once, to ensure they are fully functional. + +```bash +make fuzz-smoketest +``` + +Run fuzzer locally: +```bash +./build/fuzz/out/fuzz_conditions_match +``` + +Run fuzzer inside a container: + +```bash + docker run --rm -ti \ + -v "$(pwd)/build/fuzz/out":/out \ + gcr.io/oss-fuzz/fluxcd \ + /out/fuzz_conditions_match +``` + +### Caveats of creating oss-fuzz compatible tests + +#### Segregate fuzz tests + +OSS-Fuzz does not properly support mixed `*_test.go` files, in which there is a combination +of fuzz and non-fuzz tests. To mitigate this problem, ensure your fuzz tests are not in the +same file as other Go tests. As a pattern, call your fuzz test files `*_fuzz_test.go`. + +#### Build tags to avoid conflicts when running Go tests + +Due to the issue above, code duplication will occur when creating fuzz tests that rely on +helper functions that are shared with other tests. To avoid build issues, add a conditional +build tag at the top of the `*_fuzz_test.go` file: +```go +//go:build gofuzz_libfuzzer +// +build gofuzz_libfuzzer +``` + +The build tag above is set at [go-118-fuzz-build]. +At this point in time we can't pass on specific tags from [compile_native_go_fuzzer]. + +### Running oss-fuzz locally + +The `make fuzz-smoketest` is meant to be an easy way to reproduce errors that may occur +upstream. If our checks ever run out of sync with upstream, the upstream tests can be +executed locally with: + +``` +git clone --depth 1 https://github.com/google/oss-fuzz +cd oss-fuzz +python infra/helper.py build_image fluxcd +python infra/helper.py build_fuzzers --sanitizer address --architecture x86_64 fluxcd +python infra/helper.py check_build --sanitizer address --architecture x86_64 fluxcd +``` + +For latest info on testing oss-fuzz locally, refer to the [upstream guide]. + +[oss fuzz]: https://github.com/google/oss-fuzz +[oss-fuzz repository]: https://github.com/google/oss-fuzz/tree/master/projects/fluxcd +[github workflow]: .github/workflows/cifuzz.yaml +[upstream guide]: https://google.github.io/oss-fuzz/getting-started/new-project-guide/#testing-locally +[go-118-fuzz-build]: https://github.com/AdamKorcz/go-118-fuzz-build/blob/b2031950a318d4f2dcf3ec3e128f904d5cf84623/main.go#L40 +[compile_native_go_fuzzer]: https://github.com/google/oss-fuzz/blob/c2d827cb78529fdc757c9b0b4fea0f1238a54814/infra/base-images/base-builder/compile_native_go_fuzzer#L32 diff --git a/tests/fuzz/compile_native_go_fuzzer b/tests/fuzz/compile_native_go_fuzzer new file mode 100755 index 000000000..447c7477e --- /dev/null +++ b/tests/fuzz/compile_native_go_fuzzer @@ -0,0 +1,62 @@ +#!/bin/bash -eux +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +# This is a copy of the upstream script which is only needed to link +# additional static libraries. Orignal source: +# +# https://github.com/google/oss-fuzz/blob/9e8dd47cb902545efc60a5580126adc36d70bae3/infra/base-images/base-builder/compile_native_go_fuzzer + +function build_native_go_fuzzer() { + fuzzer=$1 + function=$2 + path=$3 + tags="-tags gofuzz" + + if [[ $SANITIZER == *coverage* ]]; then + current_dir=$(pwd) + mkdir $OUT/rawfuzzers || true + cd $abs_file_dir + go test -c -run $fuzzer -o $OUT/$fuzzer -cover + cp "${fuzzer_filename}" "${OUT}/rawfuzzers/${fuzzer}" + cd $current_dir + else + go-118-fuzz-build -o $fuzzer.a -func $function $abs_file_dir + # TODO: upstream support for linking $ADDITIONAL_LIBS + $CXX $CXXFLAGS $LIB_FUZZING_ENGINE $fuzzer.a -o $OUT/$fuzzer \ + $ADDITIONAL_LIBS + fi +} + + +path=$1 +function=$2 +fuzzer=$3 +tags="-tags gofuzz" + +# Get absolute path. +abs_file_dir=$(go list $tags -f {{.Dir}} $path) + +# TODO(adamkorcz): Get rid of "-r" flag here. +fuzzer_filename=$(grep -r -l --include='*.go' -s "$function" "${abs_file_dir}") + +# Test if file contains a line with "func $function" and "testing.F". +if [ $(grep -r "func $function" $fuzzer_filename | grep "testing.F" | wc -l) -eq 1 ] +then + build_native_go_fuzzer $fuzzer $function $abs_file_dir +else + echo "Could not find the function: func ${function}(f *testing.F)" +fi diff --git a/tests/fuzz/native_go_run.sh b/tests/fuzz/native_go_run.sh new file mode 100755 index 000000000..a62410273 --- /dev/null +++ b/tests/fuzz/native_go_run.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +# Copyright 2022 The Flux authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euxo pipefail + +# This script iterates through all go fuzzing targets, running each one +# through the period of time established by FUZZ_TIME. + +FUZZ_TIME=${FUZZ_TIME:-"5s"} + +# kustomization_fuzzer_test is not fully compatible with Go native fuzz, +# so it is ignored here. +test_files=$(grep -r --include='**_test.go' --files-with-matches 'func Fuzz' . | \ + grep -v "controllers_fuzzer_test.go") + +for file in ${test_files} +do + targets=$(grep -oP 'func \K(Fuzz\w*)' "${file}") + for target_name in ${targets} + do + echo "Running ${file}.${target_name} for ${FUZZ_TIME}." + file_dir=$(dirname "${file}") + + go test -fuzz="^${target_name}\$" -fuzztime "${FUZZ_TIME}" "${file_dir}" + done +done diff --git a/tests/fuzz/oss_fuzz_build.sh b/tests/fuzz/oss_fuzz_build.sh index 8bc1d2542..45c2e2785 100755 --- a/tests/fuzz/oss_fuzz_build.sh +++ b/tests/fuzz/oss_fuzz_build.sh @@ -16,93 +16,65 @@ set -euxo pipefail -LIBGIT2_TAG="${LIBGIT2_TAG:-v0.4.0}" +# This file aims for: +# - Dynamically discover and build all fuzz tests within the repository. +# - Work for both local make fuzz-smoketest and the upstream oss-fuzz. + GOPATH="${GOPATH:-/root/go}" GO_SRC="${GOPATH}/src" PROJECT_PATH="github.com/fluxcd/source-controller" -pushd "${GO_SRC}/${PROJECT_PATH}" - -export TARGET_DIR="$(/bin/pwd)/build/libgit2/${LIBGIT2_TAG}" - -# For most cases, libgit2 will already be present. -# The exception being at the oss-fuzz integration. -if [ ! -d "${TARGET_DIR}" ]; then - curl -o output.tar.gz -LO "https://github.com/fluxcd/golang-with-libgit2/releases/download/${LIBGIT2_TAG}/linux-x86_64-libgit2-only.tar.gz" - - DIR=linux-libgit2-only - NEW_DIR="$(/bin/pwd)/build/libgit2/${LIBGIT2_TAG}" - INSTALLED_DIR="/home/runner/work/golang-with-libgit2/golang-with-libgit2/build/${DIR}" +# install_deps installs all dependencies needed for upstream oss-fuzz. +# Unfortunately we can't pin versions here, as we want to always +# have the latest, so that we can reproduce errors occuring upstream. +install_deps(){ + if ! command -v go-118-fuzz-build &> /dev/null; then + go install github.com/AdamKorcz/go-118-fuzz-build@latest + fi +} - mkdir -p ./build/libgit2 +install_deps - tar -xf output.tar.gz - rm output.tar.gz - mv "${DIR}" "${LIBGIT2_TAG}" - mv "${LIBGIT2_TAG}/" "./build/libgit2" +cd "${GO_SRC}/${PROJECT_PATH}" - # Update the prefix paths included in the .pc files. - # This will make it easier to update to the location in which they will be used. - find "${NEW_DIR}" -type f -name "*.pc" | xargs -I {} sed -i "s;${INSTALLED_DIR};${NEW_DIR};g" {} +# Ensure any project-specific requirements are catered for ahead of +# the generic build process. +if [ -f "tests/fuzz/oss_fuzz_prebuild.sh" ]; then + . tests/fuzz/oss_fuzz_prebuild.sh fi -apt-get update && apt-get install -y pkg-config - -export CGO_ENABLED=1 -export PKG_CONFIG_PATH="${TARGET_DIR}/lib/pkgconfig" -export CGO_LDFLAGS="$(pkg-config --libs --static --cflags libgit2)" -export LIBRARY_PATH="${TARGET_DIR}/lib" -export CGO_CFLAGS="-I${TARGET_DIR}/include" - -go get -d github.com/AdaLogics/go-fuzz-headers - -# The implementation of libgit2 is sensitive to the versions of git2go. -# Leaving it to its own devices, the minimum version of git2go used may not -# be compatible with the currently implemented version. Hence the modifications -# of the existing go.mod. -sed "s;\./api;$(/bin/pwd)/api;g" go.mod > tests/fuzz/go.mod -sed -i 's;module github.com/fluxcd/source-controller;module github.com/fluxcd/source-controller/tests/fuzz;g' tests/fuzz/go.mod -echo "replace github.com/fluxcd/source-controller => $(/bin/pwd)/" >> tests/fuzz/go.mod - -cp go.sum tests/fuzz/go.sum - -pushd "tests/fuzz" - -go mod download - -go get -d github.com/AdaLogics/go-fuzz-headers -go get -d github.com/fluxcd/source-controller - -# Setup files to be embedded into controllers_fuzzer.go's testFiles variable. -mkdir -p testdata/crd -cp ../../config/crd/bases/*.yaml testdata/crd/ -cp -r ../../controllers/testdata/certs testdata/ - -go get -d github.com/AdaLogics/go-fuzz-headers - -# Using compile_go_fuzzer to compile fails when statically linking libgit2 dependencies -# via CFLAGS/CXXFLAGS. -function go_compile(){ - function=$1 - fuzzer=$2 - - if [[ $SANITIZER = *coverage* ]]; then - # ref: https://github.com/google/oss-fuzz/blob/master/infra/base-images/base-builder/compile_go_fuzzer - compile_go_fuzzer "${PROJECT_PATH}/tests/fuzz" "${function}" "${fuzzer}" - else - go-fuzz -tags gofuzz -func="${function}" -o "${fuzzer}.a" . - ${CXX} ${CXXFLAGS} ${LIB_FUZZING_ENGINE} -o "${OUT}/${fuzzer}" \ - "${fuzzer}.a" "${TARGET_DIR}/lib/libgit2.a" \ - -fsanitize="${SANITIZER}" - fi -} - -go_compile FuzzRandomGitFiles fuzz_gitrepository_fuzzer -go_compile FuzzGitResourceObject fuzz_git_resource_object - -# By now testdata is embedded in the binaries and no longer needed. -# Remove the dir given that it will be owned by root otherwise. -rm -rf testdata/ - -popd -popd +modules=$(find . -mindepth 1 -maxdepth 4 -type f -name 'go.mod' | cut -c 3- | sed 's|/[^/]*$$||' | sort -u | sed 's;/go.mod;;g' | sed 's;go.mod;.;g') + +for module in ${modules}; do + + cd "${GO_SRC}/${PROJECT_PATH}/${module}" + + test_files=$(grep -r --include='**_test.go' --files-with-matches 'func Fuzz' . || echo "") + if [ -z "${test_files}" ]; then + continue + fi + + go get github.com/AdamKorcz/go-118-fuzz-build/testing + + # Iterate through all Go Fuzz targets, compiling each into a fuzzer. + for file in ${test_files}; do + # If the subdir is a module, skip this file, as it will be handled + # at the next iteration of the outer loop. + if [ -f "$(dirname "${file}")/go.mod" ]; then + continue + fi + + targets=$(grep -oP 'func \K(Fuzz\w*)' "${file}") + for target_name in ${targets}; do + # Transform module path into module name (e.g. git/libgit2 to git_libgit2). + module_name="$(echo ${module} | tr / _)_" + # Compose fuzzer name based on the lowercase version of the func names. + # The module name is added after the fuzz prefix, for better discoverability. + fuzzer_name=$(echo "${target_name}" | tr '[:upper:]' '[:lower:]' | sed "s;fuzz_;fuzz_${module_name//._/};g") + target_dir=$(dirname "${file}") + + echo "Building ${file}.${target_name} into ${fuzzer_name}" + compile_native_go_fuzzer "${target_dir}" "${target_name}" "${fuzzer_name}" + done + done +done diff --git a/tests/fuzz/oss_fuzz_prebuild.sh b/tests/fuzz/oss_fuzz_prebuild.sh new file mode 100755 index 000000000..29cd7d615 --- /dev/null +++ b/tests/fuzz/oss_fuzz_prebuild.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +# Copyright 2022 The Flux authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euxo pipefail + +# This file is executed by upstream oss-fuzz for any requirements that +# are specific for building this project. + +# Some tests requires embedded resources. Embedding does not allow +# for traversing into ascending dirs, therefore we copy those contents here: +mkdir -p controllers/testdata/crd +cp config/crd/bases/*.yaml controllers/testdata/crd/ + +# libgit2, cmake and pkg-config are requirements to support libgit2. +LIBGIT2_TAG="${LIBGIT2_TAG:-v0.4.0}" + +# Avoid updating apt get and installing dependencies, if they are already in place. +if (! command -v cmake &> /dev/null) || (! command -v pkg-config &> /dev/null) then + apt-get update && apt-get install -y cmake pkg-config +fi + +export TARGET_DIR="$(/bin/pwd)/build/libgit2/${LIBGIT2_TAG}" + +# For most cases, libgit2 will already be present. +# The exception being at the oss-fuzz integration. +if [ ! -d "${TARGET_DIR}" ]; then + curl --connect-timeout 2 --retry 3 --retry-delay 1 --retry-max-time 30 \ + -o output.tar.gz -LO "https://github.com/fluxcd/golang-with-libgit2/releases/download/${LIBGIT2_TAG}/linux-$(uname -m)-libgit2-only.tar.gz" + + DIR=linux-libgit2-only + NEW_DIR="$(/bin/pwd)/build/libgit2/${LIBGIT2_TAG}" + INSTALLED_DIR="/home/runner/work/golang-with-libgit2/golang-with-libgit2/build/${DIR}" + + mkdir -p ./build/libgit2 + + tar -xf output.tar.gz + rm output.tar.gz + mv "${DIR}" "${LIBGIT2_TAG}" + mv "${LIBGIT2_TAG}/" "./build/libgit2" + + # Update the prefix paths included in the .pc files. + # This will make it easier to update to the location in which they will be used. + find "${NEW_DIR}" -type f -name "*.pc" | xargs -I {} sed -i "s;${INSTALLED_DIR};${NEW_DIR};g" {} +fi + +export CGO_ENABLED=1 +export LIBRARY_PATH="${TARGET_DIR}/lib" +export PKG_CONFIG_PATH="${TARGET_DIR}/lib/pkgconfig" +export CGO_CFLAGS="-I${TARGET_DIR}/include" +export CGO_LDFLAGS="$(pkg-config --libs --static --cflags libgit2)" + +export ADDITIONAL_LIBS="${TARGET_DIR}/lib/libgit2.a"