diff --git a/Makefile b/Makefile index b0386cce..18f49275 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,10 @@ BUILD_DIR := $(REPOSITORY_ROOT)/build # Other dependency versions ENVTEST_BIN_VERSION ?= 1.19.2 +# FUZZ_TIME defines the max amount of time, in Go Duration, +# each fuzzer should run for. +FUZZ_TIME ?= 1m + # Caches libgit2 versions per tag, "forcing" rebuild only when needed. LIBGIT2_PATH := $(BUILD_DIR)/libgit2/$(LIBGIT2_TAG) LIBGIT2_LIB_PATH := $(LIBGIT2_PATH)/lib @@ -219,7 +223,7 @@ ENVTEST = $(GOBIN)/setup-envtest setup-envtest: ## Download envtest-setup locally if necessary. $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest) -# Build fuzzers +# Build fuzzers used by oss-fuzz. fuzz-build: $(LIBGIT2) rm -rf $(shell pwd)/build/fuzz/ mkdir -p $(shell pwd)/build/fuzz/out/ @@ -231,6 +235,7 @@ fuzz-build: $(LIBGIT2) -v "$(shell pwd)/build/fuzz/out":/out \ local-fuzzing:latest +# Run each fuzzer once to ensure they will work when executed by oss-fuzz. fuzz-smoketest: fuzz-build docker run --rm \ -v "$(shell pwd)/build/fuzz/out":/out \ @@ -238,6 +243,12 @@ fuzz-smoketest: fuzz-build local-fuzzing:latest \ bash -c "/runner.sh" +# Run fuzz tests for the duration set in FUZZ_TIME. +fuzz-native: + KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) \ + FUZZ_TIME=$(FUZZ_TIME) \ + ./tests/fuzz/native_go_run.sh + # go-install-tool will 'go install' any package $2 and install it to $1. define go-install-tool @[ -f $(1) ] || { \ diff --git a/tests/fuzz/image_update_fuzzer.go b/controllers/controllers_fuzzer_test.go similarity index 54% rename from tests/fuzz/image_update_fuzzer.go rename to controllers/controllers_fuzzer_test.go index 94ec5177..88abf26d 100644 --- a/tests/fuzz/image_update_fuzzer.go +++ b/controllers/controllers_fuzzer_test.go @@ -64,361 +64,267 @@ import ( var ( cfgFuzz *rest.Config k8sClient client.Client - imageAutoReconcilerFuzz *controllers.ImageUpdateAutomationReconciler + imageAutoReconcilerFuzz *ImageUpdateAutomationReconciler testEnvFuzz *testenv.Environment initter sync.Once ) -const defaultBinVersion = "1.23" +const defaultBinVersion = "1.24" -//go:embed testdata/crds +//go:embed testdata/crd var testFiles embed.FS -func envtestBinVersion() string { - if binVersion := os.Getenv("ENVTEST_BIN_VERSION"); binVersion != "" { - return binVersion - } - return defaultBinVersion -} - -func ensureDependencies(setupReconcilers func(manager.Manager)) 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() +// This fuzzer randomized 2 things: +// 1: The files in the git repository +// 2: The values of ImageUpdateAutomationSpec +// and ImagePolicy resources +func Fuzz_ImageUpdateReconciler(f *testing.F) { + f.Fuzz(func(t *testing.T, seed []byte) { + initter.Do(func() { + utilruntime.Must(ensureDependencies(func(m manager.Manager) { + utilruntime.Must((&ImageUpdateAutomationReconciler{ + Client: m.GetClient(), + }).SetupWithManager(m, ImageUpdateAutomationReconcilerOptions{MaxConcurrentReconciles: 4})) + })) + }) + + f := fuzz.NewConsumer(seed) + + // We start by creating a lot of the values that + // need for the various resources later on + runes := "abcdefghijklmnopqrstuvwxyz1234567890" + branch, err := f.GetStringFrom(runes, 80) if err != nil { - return err + return } - os.Setenv("KUBEBUILDER_ASSETS", string(assetsPath)) - } - - // Output all embedded testdata files - embedDirs := []string{"testdata/crds"} - for _, dir := range embedDirs { - err := os.MkdirAll(dir, 0o755) + repPath, err := f.GetStringFrom(runes, 80) if err != nil { - return fmt.Errorf("mkdir %s: %v", dir, err) + return } + repositoryPath := "/config-" + repPath + ".git" - templates, err := fs.ReadDir(testFiles, dir) + namespaceName, err := f.GetStringFrom(runes, 59) 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, 0o644) - if err != nil { - return fmt.Errorf("writing %s: %v", fileName, err) - } + gitRepoKeyName, err := f.GetStringFrom(runes, 80) + if err != nil { + return } - } - - testEnv := &envtest.Environment{ - CRDDirectoryPaths: []string{ - filepath.Join("testdata", "crds"), - }, - } - 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(sourcev1.AddToScheme(scheme.Scheme)) - utilruntime.Must(image_reflectv1.AddToScheme(scheme.Scheme)) - utilruntime.Must(image_automationv1.AddToScheme(scheme.Scheme)) - - 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 nil -} - -// This fuzzer randomized 2 things: -// 1: The files in the git repository -// 2: The values of ImageUpdateAutomationSpec -// and ImagePolicy resources -func FuzzImageUpdateReconciler(data []byte) int { - initter.Do(func() { - utilruntime.Must(ensureDependencies(func(m manager.Manager) { - utilruntime.Must((&controllers.ImageUpdateAutomationReconciler{ - Client: m.GetClient(), - }).SetupWithManager(m, controllers.ImageUpdateAutomationReconcilerOptions{MaxConcurrentReconciles: 4})) - })) - }) - - f := fuzz.NewConsumer(data) - - // We start by creating a lot of the values that - // need for the various resources later on - runes := "abcdefghijklmnopqrstuvwxyz1234567890" - branch, err := f.GetStringFrom(runes, 80) - if err != nil { - return 0 - } - repPath, err := f.GetStringFrom(runes, 80) - if err != nil { - return 0 - } - repositoryPath := "/config-" + repPath + ".git" - - namespaceName, err := f.GetStringFrom(runes, 59) - if err != nil { - return 0 - } - - gitRepoKeyName, err := f.GetStringFrom(runes, 80) - if err != nil { - return 0 - } + username, err := f.GetStringFrom(runes, 80) + if err != nil { + return + } + password, err := f.GetStringFrom(runes, 80) + if err != nil { + return + } - username, err := f.GetStringFrom(runes, 80) - if err != nil { - return 0 - } - password, err := f.GetStringFrom(runes, 80) - if err != nil { - return 0 - } + ipSpec := image_reflectv1.ImagePolicySpec{} + err = f.GenerateStruct(&ipSpec) + if err != nil { + return + } - ipSpec := image_reflectv1.ImagePolicySpec{} - err = f.GenerateStruct(&ipSpec) - if err != nil { - return 0 - } + ipStatus := image_reflectv1.ImagePolicyStatus{} + err = f.GenerateStruct(&ipStatus) + if err != nil { + return + } - ipStatus := image_reflectv1.ImagePolicyStatus{} - err = f.GenerateStruct(&ipStatus) - if err != nil { - return 0 - } + iuaSpec := image_automationv1.ImageUpdateAutomationSpec{} + err = f.GenerateStruct(&iuaSpec) + if err != nil { + return + } + gitSpec := &image_automationv1.GitSpec{} + err = f.GenerateStruct(&gitSpec) + if err != nil { + return + } - iuaSpec := image_automationv1.ImageUpdateAutomationSpec{} - err = f.GenerateStruct(&iuaSpec) - if err != nil { - return 0 - } - gitSpec := &image_automationv1.GitSpec{} - err = f.GenerateStruct(&gitSpec) - if err != nil { - return 0 - } + policyKeyName, err := f.GetStringFrom(runes, 80) + if err != nil { + return + } - policyKeyName, err := f.GetStringFrom(runes, 80) - if err != nil { - return 0 - } + updateKeyName, err := f.GetStringFrom("abcdefghijklmnopqrstuvwxy.-", 120) + if err != nil { + return + } - updateKeyName, err := f.GetStringFrom("abcdefghijklmnopqrstuvwxy.-", 120) - if err != nil { - return 0 - } + // Create random git files + gitPath, err := os.MkdirTemp("", "git-dir-") + if err != nil { + return + } + defer os.RemoveAll(gitPath) + err = f.CreateFiles(gitPath) + if err != nil { + return + } - // Create random git files - gitPath, err := os.MkdirTemp("", "git-dir-") - if err != nil { - return 0 - } - defer os.RemoveAll(gitPath) - err = f.CreateFiles(gitPath) - if err != nil { - return 0 - } + // Done with creating the random values - // Done with creating the random values + // Create a namespace + namespace := &corev1.Namespace{} + namespace.Name = namespaceName + err = k8sClient.Create(context.Background(), namespace) + if err != nil { + return + } + defer func() { + err = k8sClient.Delete(context.Background(), namespace) + if err != nil { + panic(err) + } + time.Sleep(80 * time.Millisecond) + }() - // Create a namespace - namespace := &corev1.Namespace{} - namespace.Name = namespaceName - err = k8sClient.Create(context.Background(), namespace) - if err != nil { - return 0 - } - defer func() { - err = k8sClient.Delete(context.Background(), namespace) + // Set up git-related stuff + gitServer, err := gittestserver.NewTempGitServer() if err != nil { - panic(err) + return } - time.Sleep(80 * time.Millisecond) - }() + gitServer.Auth(username, password) + gitServer.AutoCreate() + err = gitServer.StartHTTP() + if err != nil { + return + } + defer func() { + gitServer.StopHTTP() + os.RemoveAll(gitServer.Root()) + }() + gitServer.KeyDir(filepath.Join(gitServer.Root(), "keys")) + err = gitServer.ListenSSH() + if err != nil { + return + } + err = initGitRepo(gitServer, gitPath, branch, repositoryPath) + if err != nil { + return + } + repoURL := gitServer.HTTPAddressWithCredentials() + repositoryPath + // Done with setting up git related stuff - // Set up git-related stuff - gitServer, err := gittestserver.NewTempGitServer() - if err != nil { - return 0 - } - gitServer.Auth(username, password) - gitServer.AutoCreate() - err = gitServer.StartHTTP() - if err != nil { - return 0 - } - defer func() { - gitServer.StopHTTP() - os.RemoveAll(gitServer.Root()) - }() - gitServer.KeyDir(filepath.Join(gitServer.Root(), "keys")) - err = gitServer.ListenSSH() - if err != nil { - return 0 - } - err = initGitRepo(gitServer, gitPath, branch, repositoryPath) - if err != nil { - return 0 - } - repoURL := gitServer.HTTPAddressWithCredentials() + repositoryPath - // Done with setting up git related stuff + // Create git repository object + gitRepoKey := types.NamespacedName{ + Name: "image-auto-" + gitRepoKeyName, + Namespace: namespace.Name, + } - // Create git repository object - gitRepoKey := types.NamespacedName{ - Name: "image-auto-" + gitRepoKeyName, - Namespace: namespace.Name, - } + gitRepo := &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: gitRepoKey.Name, + Namespace: namespace.Name, + }, + Spec: sourcev1.GitRepositorySpec{ + URL: repoURL, + Interval: metav1.Duration{Duration: time.Minute}, + }, + } + err = k8sClient.Create(context.Background(), gitRepo) + if err != nil { + return + } + defer k8sClient.Delete(context.Background(), gitRepo) - gitRepo := &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: gitRepoKey.Name, + // Create image policy object + policyKey := types.NamespacedName{ + Name: "policy-" + policyKeyName, Namespace: namespace.Name, - }, - Spec: sourcev1.GitRepositorySpec{ - URL: repoURL, - Interval: metav1.Duration{Duration: time.Minute}, - }, - } - err = k8sClient.Create(context.Background(), gitRepo) - if err != nil { - return 0 - } - defer k8sClient.Delete(context.Background(), gitRepo) - - // Create image policy object - policyKey := types.NamespacedName{ - Name: "policy-" + policyKeyName, - Namespace: namespace.Name, - } - policy := &image_reflectv1.ImagePolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: policyKey.Name, - Namespace: policyKey.Namespace, - }, - Spec: ipSpec, - Status: ipStatus, - } - err = k8sClient.Create(context.Background(), policy) - if err != nil { - return 0 - } - err = k8sClient.Status().Update(context.Background(), policy) - if err != nil { - return 0 - } + } + policy := &image_reflectv1.ImagePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: policyKey.Name, + Namespace: policyKey.Namespace, + }, + Spec: ipSpec, + Status: ipStatus, + } + err = k8sClient.Create(context.Background(), policy) + if err != nil { + return + } + err = k8sClient.Status().Update(context.Background(), policy) + if err != nil { + return + } - // Create ImageUpdateAutomation object - updateKey := types.NamespacedName{ - Namespace: namespace.Name, - Name: updateKeyName, - } + // Create ImageUpdateAutomation object + updateKey := types.NamespacedName{ + Namespace: namespace.Name, + Name: updateKeyName, + } - // Setting these fields manually to help the fuzzer - gitSpec.Checkout.Reference.Branch = branch - iuaSpec.GitSpec = gitSpec - iuaSpec.SourceRef.Kind = "GitRepository" - iuaSpec.SourceRef.Name = gitRepoKey.Name - iuaSpec.Update.Strategy = image_automationv1.UpdateStrategySetters - - iua := &image_automationv1.ImageUpdateAutomation{ - ObjectMeta: metav1.ObjectMeta{ - Name: updateKey.Name, - Namespace: updateKey.Namespace, - }, - Spec: iuaSpec, - } - err = k8sClient.Create(context.Background(), iua) - if err != nil { - return 0 - } - defer k8sClient.Delete(context.Background(), iua) - time.Sleep(time.Millisecond * 70) - return 1 + // Setting these fields manually to help the fuzzer + gitSpec.Checkout.Reference.Branch = branch + iuaSpec.GitSpec = gitSpec + iuaSpec.SourceRef.Kind = "GitRepository" + iuaSpec.SourceRef.Name = gitRepoKey.Name + iuaSpec.Update.Strategy = image_automationv1.UpdateStrategySetters + + iua := &image_automationv1.ImageUpdateAutomation{ + ObjectMeta: metav1.ObjectMeta{ + Name: updateKey.Name, + Namespace: updateKey.Namespace, + }, + Spec: iuaSpec, + } + err = k8sClient.Create(context.Background(), iua) + if err != nil { + return + } + defer k8sClient.Delete(context.Background(), iua) + time.Sleep(time.Millisecond * 70) + }) } // A fuzzer that is more focused on UpdateWithSetters // that the reconciler fuzzer is -func FuzzUpdateWithSetters(data []byte) int { - f := fuzz.NewConsumer(data) +func FuzzUpdateWithSetters(f *testing.F) { + f.Fuzz(func(t *testing.T, seed []byte) { + f := fuzz.NewConsumer(seed) - // Create dir1 - tmp1, err := ioutil.TempDir("", "fuzztest1") - if err != nil { - return 0 - } - defer os.RemoveAll(tmp1) - // Add files to dir1 - err = f.CreateFiles(tmp1) - if err != nil { - return 0 - } + // Create dir1 + tmp1, err := ioutil.TempDir("", "fuzztest1") + if err != nil { + return + } + defer os.RemoveAll(tmp1) + // Add files to dir1 + err = f.CreateFiles(tmp1) + if err != nil { + return + } - // Create dir2 - tmp2, err := ioutil.TempDir("", "fuzztest2") - if err != nil { - return 0 - } - defer os.RemoveAll(tmp2) + // Create dir2 + tmp2, err := ioutil.TempDir("", "fuzztest2") + if err != nil { + return + } + defer os.RemoveAll(tmp2) - // Create policies - policies := make([]image_reflectv1.ImagePolicy, 0) - noOfPolicies, err := f.GetInt() - if err != nil { - return 0 - } - for i := 0; i < noOfPolicies%10; i++ { - policy := image_reflectv1.ImagePolicy{} - err = f.GenerateStruct(&policy) + // Create policies + policies := make([]image_reflectv1.ImagePolicy, 0) + noOfPolicies, err := f.GetInt() if err != nil { - return 0 + return + } + for i := 0; i < noOfPolicies%10; i++ { + policy := image_reflectv1.ImagePolicy{} + err = f.GenerateStruct(&policy) + if err != nil { + return + } + policies = append(policies, policy) } - policies = append(policies, policy) - } - // Call the target - _, _ = update.UpdateWithSetters(logr.Discard(), tmp1, tmp2, policies) - return 1 + _, _ = update.UpdateWithSetters(logr.Discard(), tmp1, tmp2, policies) + }) } // Initialise a git server with a repo including the files in dir. @@ -517,3 +423,98 @@ func populateRepoFromFixture(repo *gogit.Repository, fixture string) error { return nil } + +func envtestBinVersion() string { + if binVersion := os.Getenv("ENVTEST_BIN_VERSION"); binVersion != "" { + return binVersion + } + return defaultBinVersion +} + +func ensureDependencies(setupReconcilers func(manager.Manager)) 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"} + for _, dir := range embedDirs { + err := os.MkdirAll(dir, 0o755) + 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, 0o644) + if err != nil { + return fmt.Errorf("writing %s: %v", fileName, err) + } + } + } + + testEnv := &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("testdata", "crds"), + }, + } + 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(sourcev1.AddToScheme(scheme.Scheme)) + utilruntime.Must(image_reflectv1.AddToScheme(scheme.Scheme)) + utilruntime.Must(image_automationv1.AddToScheme(scheme.Scheme)) + + 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 nil +} diff --git a/controllers/imageupdateautomation_controller_test.go b/controllers/imageupdateautomation_controller_test.go new file mode 100644 index 00000000..91400521 --- /dev/null +++ b/controllers/imageupdateautomation_controller_test.go @@ -0,0 +1,19 @@ +package controllers + +import ( + "testing" + + fuzz "github.com/AdaLogics/go-fuzz-headers" +) + +func Fuzz_templateMsg(f *testing.F) { + f.Add("template", []byte{}) + f.Add("", []byte{}) + + f.Fuzz(func(t *testing.T, template string, seed []byte) { + var values TemplateData + fuzz.NewConsumer(seed).GenerateStruct(&values) + + _, _ = templateMsg(template, &values) + }) +} diff --git a/go.mod b/go.mod index c8b73142..551222f1 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ replace github.com/fluxcd/image-automation-controller/api => ./api replace github.com/libgit2/git2go/v33 => github.com/pjbgf/git2go/v33 v33.0.9-nothread-check require ( + github.com/AdaLogics/go-fuzz-headers v0.0.0-20220903154154-e8044f6e4c72 github.com/Masterminds/sprig/v3 v3.2.2 github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 github.com/cyphar/filepath-securejoin v0.2.3 @@ -27,6 +28,8 @@ require ( github.com/fluxcd/pkg/ssh v0.6.0 github.com/fluxcd/source-controller v0.28.0 github.com/fluxcd/source-controller/api v0.28.0 + github.com/go-git/go-billy/v5 v5.3.1 + github.com/go-git/go-git/v5 v5.4.2 github.com/go-logr/logr v1.2.3 github.com/google/go-containerregistry v0.11.0 github.com/libgit2/git2go/v33 v33.0.9 @@ -71,8 +74,6 @@ require ( github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/go-errors/errors v1.0.1 // indirect github.com/go-git/gcfg v1.5.0 // indirect - github.com/go-git/go-billy/v5 v5.3.1 // indirect - github.com/go-git/go-git/v5 v5.4.2 // indirect github.com/go-logr/zapr v1.2.3 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect diff --git a/go.sum b/go.sum index 0901fa6f..82bf0037 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20220903154154-e8044f6e4c72 h1:1sCHCT0xRr7UArrI1WJxsl9S8QeYdf0fmuGIl2xb7YI= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20220903154154-e8044f6e4c72/go.mod h1:i9fr2JpcEcY/IHEvzCM3qXUZYOQHgR89dt4es1CgMhc= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= diff --git a/tests/fuzz/Dockerfile.builder b/tests/fuzz/Dockerfile.builder index 8484f264..4564200c 100644 --- a/tests/fuzz/Dockerfile.builder +++ b/tests/fuzz/Dockerfile.builder @@ -1,13 +1,9 @@ -FROM golang:1.18 AS go - FROM gcr.io/oss-fuzz-base/base-builder-go -# ensures golang 1.18 to enable go native fuzzing. -COPY --from=go /usr/local/go /usr/local/ - RUN apt-get update && apt-get install -y cmake pkg-config COPY ./ $GOPATH/src/github.com/fluxcd/image-automation-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/compile_native_go_fuzzer b/tests/fuzz/compile_native_go_fuzzer new file mode 100755 index 00000000..82f3bdfc --- /dev/null +++ b/tests/fuzz/compile_native_go_fuzzer @@ -0,0 +1,100 @@ +#!/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/81326f0a39eadebfa9b7a98aa9f0553707875696/infra/base-images/base-builder/compile_go_fuzzer + +# Rewrites a copy of the fuzzer to allow for +# libFuzzer instrumentation. +function rewrite_go_fuzz_harness() { + fuzzer_filename=$1 + fuzz_function=$2 + + # Create a copy of the fuzzer to not modify the existing fuzzer. + cp $fuzzer_filename "${fuzzer_filename}"_fuzz_.go + mv $fuzzer_filename /tmp/ + fuzzer_fn="${fuzzer_filename}"_fuzz_.go + + # Replace *testing.F with *go118fuzzbuildutils.F. + echo "replacing *testing.F" + sed -i "s/func $fuzz_function(\([a-zA-Z0-9]*\) \*testing\.F)/func $fuzz_function(\1 \*go118fuzzbuildutils\.F)/g" "${fuzzer_fn}" + + # Import https://github.com/AdamKorcz/go-118-fuzz-build. + # This changes the line numbers from the original fuzzer. + addimport -path "${fuzzer_fn}" +} + +function build_native_go_fuzzer() { + fuzzer=$1 + function=$2 + path=$3 + tags="-tags gofuzz" + + if [[ $SANITIZER = *coverage* ]]; then + echo "here we perform coverage build" + fuzzed_package=`go list $tags -f '{{.Name}}' $path` + abspath=`go list $tags -f {{.Dir}} $path` + cd $abspath + cp $GOPATH/native_ossfuzz_coverage_runner.go ./"${function,,}"_test.go + sed -i -e 's/FuzzFunction/'$function'/' ./"${function,,}"_test.go + sed -i -e 's/mypackagebeingfuzzed/'$fuzzed_package'/' ./"${function,,}"_test.go + sed -i -e 's/TestFuzzCorpus/Test'$function'Corpus/' ./"${function,,}"_test.go + + # The repo is the module path/name, which is already created above + # in case it doesn't exist, but not always the same as the module + # path. This is necessary to handle SIV properly. + fuzzed_repo=$(go list $tags -f {{.Module}} "$path") + abspath_repo=`go list -m $tags -f {{.Dir}} $fuzzed_repo || go list $tags -f {{.Dir}} $fuzzed_repo` + # give equivalence to absolute paths in another file, as go test -cover uses golangish pkg.Dir + echo "s=$fuzzed_repo"="$abspath_repo"= > $OUT/$fuzzer.gocovpath + go test -run Test${function}Corpus -v $tags -coverpkg $fuzzed_repo/... -c -o $OUT/$fuzzer $path + + rm ./"${function,,}"_test.go + else + go-118-fuzz-build -o $fuzzer.a -func $function $abs_file_dir + $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 + + rewrite_go_fuzz_harness $fuzzer_filename $function + build_native_go_fuzzer $fuzzer $function $abs_file_dir + + # Clean up. + rm "${fuzzer_filename}_fuzz_.go" + mv /tmp/$(basename $fuzzer_filename) $fuzzer_filename +else + echo "Could not find the function: func ${function}(f *testing.F)" +fi diff --git a/tests/fuzz/go.mod b/tests/fuzz/go.mod deleted file mode 100644 index 56936047..00000000 --- a/tests/fuzz/go.mod +++ /dev/null @@ -1,10 +0,0 @@ -// Used to exclude it from Go mod in repository root. -// Replaced by oss_fuzz_build.sh. -module github.com/fluxcd/image-automation-controller/tests/fuzz - -go 1.18 - -replace ( - github.com/fluxcd/image-automation-controller/api => ../../api - github.com/fluxcd/image-automation-controller => ../../ -) diff --git a/tests/fuzz/native_go_run.sh b/tests/fuzz/native_go_run.sh new file mode 100755 index 00000000..a6241027 --- /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 29c62e59..856317a0 100755 --- a/tests/fuzz/oss_fuzz_build.sh +++ b/tests/fuzz/oss_fuzz_build.sh @@ -20,8 +20,47 @@ LIBGIT2_TAG="${LIBGIT2_TAG:-v0.2.0}" GOPATH="${GOPATH:-/root/go}" GO_SRC="${GOPATH}/src" PROJECT_PATH="github.com/fluxcd/image-automation-controller" +TMP_DIR=$(mktemp -d /tmp/oss_fuzz-XXXXXX) -pushd "${GO_SRC}/${PROJECT_PATH}" +cleanup(){ + rm -rf "${TMP_DIR}" +} +trap cleanup EXIT + +install_deps(){ + if ! command -v go-118-fuzz-build &> /dev/null || ! command -v addimport &> /dev/null; then + mkdir -p "${TMP_DIR}/go-118-fuzz-build" + + git clone https://github.com/AdamKorcz/go-118-fuzz-build "${TMP_DIR}/go-118-fuzz-build" + cd "${TMP_DIR}/go-118-fuzz-build" + go build -o "${GOPATH}/bin/go-118-fuzz-build" + + cd addimport + go build -o "${GOPATH}/bin/addimport" + fi + + if ! command -v goimports &> /dev/null; then + go install golang.org/x/tools/cmd/goimports@latest + fi +} + +# Removes the content of test funcs which could cause the Fuzz +# tests to break. +remove_test_funcs(){ + filename=$1 + + echo "removing co-located *testing.T" + sed -i -e '/func Test.*testing.T) {$/ {:r;/\n}/!{N;br}; s/\n.*\n/\n/}' "${filename}" + # Remove gomega reference as it is not used by Fuzz tests. + sed -i 's;. "github.com/onsi/gomega";;g' "${filename}" + + # After removing the body of the go testing funcs, consolidate the imports. + goimports -w "${filename}" +} + +install_deps + +cd "${GO_SRC}/${PROJECT_PATH}" export TARGET_DIR="$(/bin/pwd)/build/libgit2/${LIBGIT2_TAG}" @@ -46,27 +85,17 @@ if [ ! -d "${TARGET_DIR}" ]; then find "${NEW_DIR}" -type f -name "*.pc" | xargs -I {} sed -i "s;${INSTALLED_DIR};${NEW_DIR};g" {} fi -apt-get update && apt-get install -y pkg-config - -export CGO_ENABLED=1 -export LIBRARY_PATH="${TARGET_DIR}/lib:${TARGET_DIR}/lib64" -export PKG_CONFIG_PATH="${TARGET_DIR}/lib/pkgconfig:${TARGET_DIR}/lib64/pkgconfig" -export CGO_CFLAGS="-I${TARGET_DIR}/include -I${TARGET_DIR}/include/openssl" -export CGO_LDFLAGS="$(pkg-config --libs --static --cflags libssh2 openssl libgit2)" - -pushd "tests/fuzz" +go get github.com/AdamKorcz/go-118-fuzz-build/utils # Setup files to be embedded into controllers_fuzzer.go's testFiles variable. -mkdir -p testdata/crds -cp ../../config/crd/bases/*.yaml testdata/crds/ +mkdir -p controllers/testdata/crd +cp config/crd/bases/*.yaml controllers/testdata/crd -# Use main go.mod in order to conserve the same version across all dependencies. -cp ../../go.mod . -cp ../../go.sum . - -sed -i 's;module .*;module github.com/fluxcd/image-automation-controller/tests/fuzz;g' go.mod -sed -i 's;api => ./api;api => ../../api;g' go.mod -echo "replace github.com/fluxcd/image-automation-controller => ../../" >> go.mod +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)" # Version of the source-controller from which to get the GitRepository CRD. # Pulls source-controller/api's version set in go.mod. @@ -76,42 +105,29 @@ SOURCE_VER=$(go list -m github.com/fluxcd/source-controller/api | awk '{print $2 # Pulls image-reflector-controller/api's version set in go.mod. REFLECTOR_VER=$(go list -m github.com/fluxcd/image-reflector-controller/api | awk '{print $2}') -go mod download -go get -d github.com/fluxcd/image-automation-controller -go get -d github.com/AdaLogics/go-fuzz-headers - if [ -d "../../controllers/testdata/crds" ]; then cp ../../controllers/testdata/crds/*.yaml testdata/crds -# Fetch the CRDs if not present since we need them when running fuzz tests on CI. else - curl -s --fail https://raw.githubusercontent.com/fluxcd/source-controller/${SOURCE_VER}/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml -o testdata/crds/gitrepositories.yaml - - curl -s --fail https://raw.githubusercontent.com/fluxcd/image-reflector-controller/${REFLECTOR_VER}/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml -o testdata/crds/imagepolicies.yaml + # Fetch the CRDs if not present since we need them when running fuzz tests on CI. + curl -s --fail https://raw.githubusercontent.com/fluxcd/source-controller/${SOURCE_VER}/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml -o controllers/testdata/crd/gitrepositories.yaml + curl -s --fail https://raw.githubusercontent.com/fluxcd/image-reflector-controller/${REFLECTOR_VER}/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml -o controllers/testdata/crd/imagepolicies.yaml fi -# 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 -} +export ADDITIONAL_LIBS="${TARGET_DIR}/lib/libgit2.a" -go_compile FuzzImageUpdateReconciler fuzz_image_update_reconciler -go_compile FuzzUpdateWithSetters fuzz_update_with_setters +# Iterate through all Go Fuzz targets, compiling each into a fuzzer. +test_files=$(grep -r --include='**_test.go' --files-with-matches 'func Fuzz' .) +for file in ${test_files} +do + remove_test_funcs "${file}" -# By now testdata is embedded in the binaries and no longer needed. -rm -rf testdata/ + targets=$(grep -oP 'func \K(Fuzz\w*)' "${file}") + for target_name in ${targets} + do + fuzzer_name=$(echo "${target_name}" | tr '[:upper:]' '[:lower:]') + target_dir=$(dirname "${file}") -popd -popd + echo "Building ${file}.${target_name} into ${fuzzer_name}" + compile_native_go_fuzzer "${target_dir}" "${target_name}" "${fuzzer_name}" + done +done