diff --git a/fuzz/Dockerfile b/fuzz/Dockerfile new file mode 100644 index 000000000..50e0fa890 --- /dev/null +++ b/fuzz/Dockerfile @@ -0,0 +1,29 @@ +FROM golang:1.16-buster as builder + +RUN apt-get update && apt-get install -y git vim clang +RUN git clone https://github.com/fluxcd/kustomize-controller /kustomize-controller + +WORKDIR /kustomize-controller + +# fillippo.io/age v1.0.0-beta7 throws an error +RUN sed 's/filippo.io\/age v1.0.0-beta7/filippo.io\/age v1.0.0/g' -i /kustomize-controller/go.mod +RUN make download-crd-deps +RUN mkdir /kustomize-controller/fuzz +COPY fuzz.go /kustomize-controller/controllers/ + +RUN go mod tidy + +RUN cd / \ + && go get -u github.com/dvyukov/go-fuzz/go-fuzz@latest github.com/dvyukov/go-fuzz/go-fuzz-build@latest +RUN go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest +RUN go get github.com/AdaLogics/go-fuzz-headers +RUN go mod download golang.org/x/sync +RUN go mod download github.com/dvyukov/go-fuzz + +RUN mkdir /fuzzers +RUN cd /kustomize-controller/controllers \ + && go-fuzz-build -libfuzzer -func=Fuzz \ + && clang -o /fuzzers/Fuzz reflect-fuzz.a \ + -fsanitize=fuzzer + +RUN cd /kustomize-controller/controllers && /fuzzers/Fuzz diff --git a/fuzz/fuzz.go b/fuzz/fuzz.go new file mode 100644 index 000000000..a34b2f395 --- /dev/null +++ b/fuzz/fuzz.go @@ -0,0 +1,473 @@ +//go:build gofuzz +// +build gofuzz + +/* +Copyright 2021 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. +*/ + +package controllers + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "crypto/sha1" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "time" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1" + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/runtime/controller" + "github.com/fluxcd/pkg/testserver" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + + fuzz "github.com/AdaLogics/go-fuzz-headers" +) + +var cfg *rest.Config +var k8sClient client.Client +var k8sManager ctrl.Manager +var testEnv *envtest.Environment +var kubeConfig []byte +var testServer *testserver.ArtifactServer +var testEventsH controller.Events +var testMetricsH controller.Metrics +var ctx = ctrl.SetupSignalHandler() +var initter sync.Once + +func createKUBEBUILDER_ASSETS() string { + out, err := exec.Command("setup-envtest", "use").Output() + if err != nil { + panic(err) + } + + // split the output: + splitString := strings.Split(string(out), " ") + binPath := strings.TrimSuffix(splitString[len(splitString)-1], "\n") + if err != nil { + panic(err) + } + return binPath +} + +// customInit implements an init function that +// is invoked by way of sync.Do +func customInit() { + kubebuilder_assets := createKUBEBUILDER_ASSETS() + os.Setenv("KUBEBUILDER_ASSETS", kubebuilder_assets) + var err error + err = sourcev1.AddToScheme(scheme.Scheme) + if err != nil { + panic(err) + } + err = kustomizev1.AddToScheme(scheme.Scheme) + if err != nil { + panic(err) + } + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + } + + testServer, err = testserver.NewTempArtifactServer() + if err != nil { + panic(fmt.Sprintf("Failed to create a temporary storage server: %v", err)) + } + fmt.Println("Starting the test storage server") + testServer.Start() + + cfg, err = testEnv.Start() + + user, err := testEnv.ControlPlane.AddUser(envtest.User{ + Name: "envtest-admin", + Groups: []string{"system:masters"}, + }, nil) + if err != nil { + panic(fmt.Sprintf("Failed to create envtest-admin user: %v", err)) + } + + kubeConfig, err = user.KubeConfig() + if err != nil { + panic(fmt.Sprintf("Failed to create the envtest-admin user kubeconfig: %v", err)) + } + + // client with caching disabled + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + + if err := (&KustomizationReconciler{ + Client: k8sManager.GetClient(), + }).SetupWithManager(k8sManager, KustomizationReconcilerOptions{MaxConcurrentReconciles: 1}); err != nil { + panic(fmt.Sprintf("Failed to start GitRepositoryReconciler: %v", err)) + } + time.Sleep(1 * time.Second) + + go func() { + fmt.Println("Starting the test environment") + if err := k8sManager.Start(ctx); err != nil { + panic(fmt.Sprintf("Failed to start the test environment manager: %v", err)) + } + }() + time.Sleep(time.Second * 1) + <-k8sManager.Elected() +} + +// Fuzz implements the fuzz harness +func Fuzz(data []byte) int { + initter.Do(customInit) + f := fuzz.NewConsumer(data) + dname, err := os.MkdirTemp("", "artifact-dir") + if err != nil { + return 0 + } + defer os.RemoveAll(dname) + err = f.CreateFiles(dname) + if err != nil { + return 0 + } + id, err := randString(f) + if err != nil { + return 0 + } + namespace, err := createNamespace(id) + if err != nil { + return 0 + } + defer k8sClient.Delete(context.Background(), namespace) + + fmt.Println("createKubeConfigSecret...") + secret, err := createKubeConfigSecret(id) + if err != nil { + fmt.Println(err) + return 0 + } + defer k8sClient.Delete(context.Background(), secret) + + artifactFile, err := randString(f) + if err != nil { + return 0 + } + fmt.Println("createArtifact...") + artifactChecksum, err := createArtifact(testServer, dname, artifactFile) + if err != nil { + fmt.Println(err) + return 0 + } + _ = artifactChecksum + fmt.Println("testServer.URLForFile...") + artifactURL, err := testServer.URLForFile(artifactFile) + if err != nil { + fmt.Println("URLForFile error: ", err) + return 0 + } + _ = artifactURL + repName, err := randString(f) + if err != nil { + return 0 + } + repositoryName := types.NamespacedName{ + Name: repName, + Namespace: id, + } + fmt.Println("applyGitRepository...") + err = applyGitRepository(repositoryName, artifactURL, "main/"+artifactChecksum, artifactChecksum) + if err != nil { + fmt.Println(err) + } + pgpKey, err := ioutil.ReadFile("testdata/sops/pgp.asc") + if err != nil { + return 0 + } + ageKey, err := ioutil.ReadFile("testdata/sops/age.txt") + if err != nil { + return 0 + } + sskName, err := randString(f) + if err != nil { + return 0 + } + sopsSecretKey := types.NamespacedName{ + Name: sskName, + Namespace: id, + } + + sopsSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: sopsSecretKey.Name, + Namespace: sopsSecretKey.Namespace, + }, + StringData: map[string]string{ + "pgp.asc": string(pgpKey), + "age.agekey": string(ageKey), + }, + } + err = k8sClient.Create(context.Background(), sopsSecret) + if err != nil { + return 0 + } + defer k8sClient.Delete(context.Background(), sopsSecret) + + kkName, err := randString(f) + if err != nil { + return 0 + } + kustomizationKey := types.NamespacedName{ + Name: kkName, + Namespace: id, + } + kustomization := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kustomizationKey.Name, + Namespace: kustomizationKey.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + Path: "./", + KubeConfig: &kustomizev1.KubeConfig{ + SecretRef: meta.LocalObjectReference{ + Name: "kubeconfig", + }, + }, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + Decryption: &kustomizev1.Decryption{ + Provider: "sops", + SecretRef: &meta.LocalObjectReference{ + Name: sopsSecretKey.Name, + }, + }, + TargetNamespace: id, + }, + } + + err = k8sClient.Create(context.TODO(), kustomization) + if err != nil { + fmt.Println(err) + return 0 + } + defer k8sClient.Delete(context.TODO(), kustomization) + return 1 +} + +// Allows the fuzzer to create a random lowercase string +func randString(f *fuzz.ConsumeFuzzer) (string, error) { + stringLength, err := f.GetInt() + if err != nil { + return "", err + } + maxLength := 50 + var buffer bytes.Buffer + for i := 0; i < stringLength%maxLength; i++ { + getByte, err := f.GetByte() + if err != nil { + return "", nil + } + if getByte < 'a' || getByte > 'z' { + return "", errors.New("Not a good char") + } + buffer.WriteByte(getByte) + } + return buffer.String(), nil +} + +// Creates a namespace. +// Caller must delete the created namespace. +func createNamespace(name string) (*corev1.Namespace, error) { + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + } + err := k8sClient.Create(context.Background(), namespace) + if err != nil { + return namespace, err + } + return namespace, nil +} + +// Creates a secret. +// Caller must delete the created secret. +func createKubeConfigSecret(namespace string) (*corev1.Secret, error) { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kubeconfig", + Namespace: namespace, + }, + Data: map[string][]byte{ + "value.yaml": kubeConfig, + }, + } + err := k8sClient.Create(context.Background(), secret) + if err != nil { + return secret, err + } + return secret, nil +} + +// taken from https://github.com/fluxcd/kustomize-controller/blob/main/controllers/suite_test.go#L222 +func createArtifact(artifactServer *testserver.ArtifactServer, fixture, path string) (string, error) { + if f, err := os.Stat(fixture); os.IsNotExist(err) || !f.IsDir() { + return "", fmt.Errorf("invalid fixture path: %s", fixture) + } + f, err := os.Create(filepath.Join(artifactServer.Root(), path)) + if err != nil { + return "", err + } + defer func() { + if err != nil { + os.Remove(f.Name()) + } + }() + + h := sha1.New() + + mw := io.MultiWriter(h, f) + gw := gzip.NewWriter(mw) + tw := tar.NewWriter(gw) + + if err = filepath.Walk(fixture, func(p string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + + // Ignore anything that is not a file (directories, symlinks) + if !fi.Mode().IsRegular() { + return nil + } + + // Ignore dotfiles + if strings.HasPrefix(fi.Name(), ".") { + return nil + } + + header, err := tar.FileInfoHeader(fi, p) + if err != nil { + return err + } + relFilePath := p + if filepath.IsAbs(fixture) { + relFilePath, err = filepath.Rel(fixture, p) + if err != nil { + return err + } + } + header.Name = relFilePath + + if err := tw.WriteHeader(header); err != nil { + return err + } + + f, err := os.Open(p) + if err != nil { + f.Close() + return err + } + if _, err := io.Copy(tw, f); err != nil { + f.Close() + return err + } + return f.Close() + }); err != nil { + return "", err + } + + if err := tw.Close(); err != nil { + gw.Close() + f.Close() + return "", err + } + if err := gw.Close(); err != nil { + f.Close() + return "", err + } + if err := f.Close(); err != nil { + return "", err + } + + if err := os.Chmod(f.Name(), 0644); err != nil { + return "", err + } + + return fmt.Sprintf("%x", h.Sum(nil)), nil +} + +// Taken from https://github.com/fluxcd/kustomize-controller/blob/main/controllers/suite_test.go#L171 +func applyGitRepository(objKey client.ObjectKey, artifactURL, artifactRevision, artifactChecksum string) error { + repo := &sourcev1.GitRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: sourcev1.GitRepositoryKind, + APIVersion: sourcev1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: objKey.Name, + Namespace: objKey.Namespace, + }, + Spec: sourcev1.GitRepositorySpec{ + URL: "https://github.com/test/repository", + Interval: metav1.Duration{Duration: time.Minute}, + }, + } + + status := sourcev1.GitRepositoryStatus{ + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: sourcev1.GitOperationSucceedReason, + }, + }, + Artifact: &sourcev1.Artifact{ + Path: artifactURL, + URL: artifactURL, + Revision: artifactRevision, + Checksum: artifactChecksum, + LastUpdateTime: metav1.Now(), + }, + } + + opt := []client.PatchOption{ + client.ForceOwnership, + client.FieldOwner("kustomize-controller"), + } + + if err := k8sClient.Patch(context.Background(), repo, client.Apply, opt...); err != nil { + return err + } + + repo.ManagedFields = nil + repo.Status = status + if err := k8sClient.Status().Patch(context.Background(), repo, client.Apply, opt...); err != nil { + return err + } + return nil +}