Refactor fuzzing

Structure the fuzz implementation to be closer to what go native will support.
Add Makefile target to enable smoketesting fuzzers.
Add smoketest as CI workflow.

Signed-off-by: Paulo Gomes <paulo.gomes@weave.works>
This commit is contained in:
Paulo Gomes 2022-01-14 15:38:24 +00:00
parent 7f8441672e
commit 50c043eb4f
No known key found for this signature in database
GPG Key ID: 9995233870E99BEE
14 changed files with 734 additions and 639 deletions

20
.github/workflows/cifuzz.yaml vendored Normal file
View File

@ -0,0 +1,20 @@
name: CIFuzz
on:
pull_request:
branches:
- main
jobs:
Fuzzing:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Restore Go cache
uses: actions/cache@v1
with:
path: /home/runner/work/_temp/_github_home/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Smoke test Fuzzers
run: make fuzz-smoketest

3
.gitignore vendored
View File

@ -13,8 +13,9 @@
# Dependency directories (remove the comment below to include it)
# vendor/
testbin/
bin/
config/release/
config/crd/bases/gitrepositories.yaml
config/crd/bases/buckets.yaml
build/

View File

@ -22,7 +22,7 @@ ENVTEST_ARCH ?= amd64
all: manager
# Download the envtest binaries to testbin
ENVTEST_ASSETS_DIR=$(shell pwd)/testbin
ENVTEST_ASSETS_DIR=$(shell pwd)/build/testbin
ENVTEST_KUBERNETES_VERSION?=latest
install-envtest: setup-envtest
mkdir -p ${ENVTEST_ASSETS_DIR}
@ -147,3 +147,22 @@ GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\
rm -rf $$TMP_DIR ;\
}
endef
# Build fuzzers
fuzz-build:
rm -rf $(shell pwd)/build/fuzz/
mkdir -p $(shell pwd)/build/fuzz/out/
docker build . --tag local-fuzzing:latest -f tests/fuzz/Dockerfile.builder
docker run --rm \
-e FUZZING_LANGUAGE=go -e SANITIZER=address \
-e CIFUZZ_DEBUG='True' -e OSS_FUZZ_PROJECT_NAME=fluxcd \
-v "$(shell pwd)/build/fuzz/out":/out \
local-fuzzing:latest
fuzz-smoketest: fuzz-build
docker run --rm \
-v "$(shell pwd)/build/fuzz/out":/out \
-v "$(shell pwd)/tests/fuzz/oss_fuzz_run.sh":/runner.sh \
local-fuzzing:latest \
bash -c "/runner.sh"

View File

@ -69,7 +69,7 @@ var (
debugMode = os.Getenv("DEBUG_TEST") != ""
)
func TestMain(m *testing.M) {
func runInContext(registerControllers func(*testenv.Environment), run func() error, crdPath string) error {
var err error
utilruntime.Must(sourcev1.AddToScheme(scheme.Scheme))
utilruntime.Must(kustomizev1.AddToScheme(scheme.Scheme))
@ -78,9 +78,7 @@ func TestMain(m *testing.M) {
controllerLog.SetLogger(zap.New(zap.WriteTo(os.Stderr), zap.UseDevMode(false)))
}
testEnv = testenv.New(testenv.WithCRDPath(
filepath.Join("..", "config", "crd", "bases"),
))
testEnv = testenv.New(testenv.WithCRDPath(crdPath))
testServer, err = testserver.NewTempArtifactServer()
if err != nil {
@ -89,18 +87,7 @@ func TestMain(m *testing.M) {
fmt.Println("Starting the test storage server")
testServer.Start()
controllerName := "kustomize-controller"
testEventsH = controller.MakeEvents(testEnv, controllerName, nil)
testMetricsH = controller.MustMakeMetrics(testEnv)
reconciler := &KustomizationReconciler{
ControllerName: controllerName,
Client: testEnv,
EventRecorder: testEventsH.EventRecorder,
MetricsRecorder: testMetricsH.MetricsRecorder,
}
if err := (reconciler).SetupWithManager(testEnv, KustomizationReconcilerOptions{MaxConcurrentReconciles: 4}); err != nil {
panic(fmt.Sprintf("Failed to start GitRepositoryReconciler: %v", err))
}
registerControllers(testEnv)
go func() {
fmt.Println("Starting the test environment")
@ -129,7 +116,7 @@ func TestMain(m *testing.M) {
panic(fmt.Sprintf("Failed to create k8s client: %v", err))
}
code := m.Run()
runErr := run()
if debugMode {
events := &corev1.EventList{}
@ -152,6 +139,30 @@ func TestMain(m *testing.M) {
panic(fmt.Sprintf("Failed to remove storage server dir: %v", err))
}
return runErr
}
func TestMain(m *testing.M) {
code := 0
runInContext(func(testEnv *testenv.Environment) {
controllerName := "kustomize-controller"
testEventsH = controller.MakeEvents(testEnv, controllerName, nil)
testMetricsH = controller.MustMakeMetrics(testEnv)
reconciler := &KustomizationReconciler{
ControllerName: controllerName,
Client: testEnv,
EventRecorder: testEventsH.EventRecorder,
MetricsRecorder: testMetricsH.MetricsRecorder,
}
if err := (reconciler).SetupWithManager(testEnv, KustomizationReconcilerOptions{MaxConcurrentReconciles: 4}); err != nil {
panic(fmt.Sprintf("Failed to start KustomizationReconciler: %v", err))
}
}, func() error {
code = m.Run()
return nil
}, filepath.Join("..", "config", "crd", "bases"))
os.Exit(code)
}

View File

@ -1,29 +0,0 @@
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

View File

@ -1,591 +0,0 @@
// +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"
"net/http"
"os"
"os/exec"
"sync"
"path/filepath"
"strings"
"time"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1"
"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/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"
"k8s.io/apimachinery/pkg/types"
"github.com/fluxcd/pkg/apis/meta"
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
var runningInOssfuzz = false
var (
localCRDpath = []string{filepath.Join("..", "config", "crd", "bases")}
// Variables for the OSS-fuzz environment:
downloadLink = "https://storage.googleapis.com/kubebuilder-tools/kubebuilder-tools-1.19.2-linux-amd64.tar.gz"
downloadPath = "/tmp/envtest-bins.tar.gz"
binariesDir = "/tmp/test-binaries"
ossFuzzCrdPath = []string{filepath.Join(".", "bases")}
ossFuzzCrdYaml = "https://raw.githubusercontent.com/fluxcd/kustomize-controller/main/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml"
ossFuzzGitYaml = "https://raw.githubusercontent.com/fluxcd/source-controller/v0.15.4/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml"
ossFuzzBucYaml = "https://raw.githubusercontent.com/fluxcd/source-controller/v0.15.4/config/crd/bases/source.toolkit.fluxcd.io_buckets.yaml"
crdPath []string
pgpAscFile = "https://raw.githubusercontent.com/fluxcd/kustomize-controller/main/controllers/testdata/sops/pgp.asc"
ageTxtFile = "https://raw.githubusercontent.com/fluxcd/kustomize-controller/main/controllers/testdata/sops/age.txt"
)
// createKUBEBUILDER_ASSETS runs "setup-envtest use" and
// returns the path of the 3 binaries that are created.
// If this one fails, it means that setup-envtest is not
// available in the runtime environment, and that means
// that the fuzzer is being run by OSS-fuzz. In that case
// we set "runningInOssfuzz" to true which will later trigger
// download of all required files so the OSS-fuzz can run
// the fuzzer.
func createKUBEBUILDER_ASSETS() string {
out, err := exec.Command("setup-envtest", "use").Output()
if err != nil {
// If there is an error here, the fuzzer is running
// in OSS-fuzz where the binary setup-envtest is
// not available, so we have to get them in an
// alternative way
runningInOssfuzz = true
return ""
}
// split the output:
splitString := strings.Split(string(out), " ")
binPath := strings.TrimSuffix(splitString[len(splitString)-1], "\n")
if err != nil {
panic(err)
}
return binPath
}
// Downloads a file to a path. This is only needed when
// the fuzzer is running in OSS-fuzz.
func DownloadFile(filepath string, url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// Create the file
out, err := os.Create(filepath)
if err != nil {
return err
}
defer out.Close()
// Write the body to file
_, err = io.Copy(out, resp.Body)
return err
}
// When OSS-fuzz runs the fuzzer a few files need to
// be download during initialization. No files from the
// kustomize-controller repo are available at runtime,
// and each of the files that are needed must be downloaded
// manually.
func downloadFilesForOssFuzz() error {
err := DownloadFile(downloadPath, downloadLink)
if err != nil {
return err
}
err = os.MkdirAll(binariesDir, 0777)
if err != nil {
return err
}
cmd := exec.Command("tar", "xvf", downloadPath, "-C", binariesDir)
err = cmd.Run()
if err != nil {
return err
}
// Download crds
err = os.MkdirAll("bases", 0777)
if err != nil {
return err
}
err = DownloadFile("./bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml", ossFuzzCrdYaml)
if err != nil {
return err
}
err = DownloadFile("./bases/source.toolkit.fluxcd.io_gitrepositories.yaml", ossFuzzGitYaml)
if err != nil {
return err
}
err = DownloadFile("./bases/source.toolkit.fluxcd.io_buckets.yaml", ossFuzzBucYaml)
if err != nil {
return err
}
// Download sops files
err = os.MkdirAll("testdata/sops", 0777)
if err != nil {
return err
}
err = DownloadFile("./testdata/sops/pgp.asc", pgpAscFile)
if err != nil {
return err
}
err = DownloadFile("./testdata/sops/age.txt", ageTxtFile)
if err != nil {
return err
}
return nil
}
// 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)
// Set up things for fuzzing in the OSS-fuzz environment:
if runningInOssfuzz {
err := downloadFilesForOssFuzz()
if err != nil {
panic(err)
}
os.Setenv("KUBEBUILDER_ASSETS", binariesDir+"/kubebuilder/bin")
crdPath = ossFuzzCrdPath
runningInOssfuzz = false
} else {
crdPath = localCRDpath
}
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: crdPath,
}
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 := createNamespaceForFuzzing(id)
if err != nil {
return 0
}
defer k8sClient.Delete(context.Background(), namespace)
fmt.Println("createKubeConfigSecretForFuzzing...")
secret, err := createKubeConfigSecretForFuzzing(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 createNamespaceForFuzzing(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 createKubeConfigSecretForFuzzing(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
}

View File

@ -0,0 +1,6 @@
FROM gcr.io/oss-fuzz-base/base-builder-go
COPY ./ $GOPATH/src/github.com/fluxcd/kustomize-controller/
COPY ./tests/fuzz/oss_fuzz_build.sh $SRC/build.sh
WORKDIR $SRC

45
tests/fuzz/README.md Normal file
View File

@ -0,0 +1,45 @@
# 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].
For fuzzers to be called, they must be compiled within [oss_fuzz_build.sh](./oss_fuzz_build.sh).
### Testing locally
Build fuzzers:
```bash
make fuzz-build
```
All fuzzers will be built into `./build/fuzz/out`.
Smoke test fuzzers:
```bash
make fuzz-smoketest
```
The smoke test runs each fuzzer once to ensure they are fully functional.
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
```
[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

47
tests/fuzz/age_fuzzer.go Normal file
View File

@ -0,0 +1,47 @@
//go:build gofuzz
// +build gofuzz
/*
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.
*/
package age
import (
fuzz "github.com/AdaLogics/go-fuzz-headers"
)
// FuzzAge implements a fuzzer that targets functions within age/keysource.go.
func FuzzAge(data []byte) int {
f := fuzz.NewConsumer(data)
masterKey := MasterKey{}
if err := f.GenerateStruct(&masterKey); err != nil {
return 0
}
_ = masterKey.Encrypt(data)
_ = masterKey.EncryptIfNeeded(data)
receipts, err := f.GetString()
if err != nil {
return 0
}
_, _ = MasterKeysFromRecipients(receipts)
_, _ = MasterKeyFromRecipient(receipts)
return 1
}

View File

@ -0,0 +1,441 @@
//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 (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"sync"
"time"
"embed"
"io/fs"
securejoin "github.com/cyphar/filepath-securejoin"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
"github.com/fluxcd/kustomize-controller/controllers"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/testenv"
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"
fuzz "github.com/AdaLogics/go-fuzz-headers"
)
var doOnce sync.Once
const defaultBinVersion = "1.23"
func envtestBinVersion() string {
if binVersion := os.Getenv("ENVTEST_BIN_VERSION"); binVersion != "" {
return binVersion
}
return defaultBinVersion
}
//go:embed testdata/crd/*.yaml
//go:embed testdata/sops/pgp.asc
//go:embed testdata/sops/age.txt
var testFiles embed.FS
// ensureDependencies ensure that:
// a) setup-envtest is installed and a specific version of envtest is deployed.
// b) the embedded crd files are exported onto the "runner container".
//
// The steps above are important as the fuzzers tend to be built in an
// environment (or container) and executed in other.
func ensureDependencies() error {
// only install dependencies when running inside a container
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
// "testdata/sops" does not need to be saved in disk
// as it is being consumed directly from the embed.FS.
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)
}
}
}
return nil
}
// FuzzControllers implements a fuzzer that targets the Kustomize controller.
//
// The test must ensure a valid test state around Kubernetes objects, as the
// focus is to ensure the controller behaves properly, not Kubernetes nor
// testing infrastructure.
func FuzzControllers(data []byte) int {
// Fuzzing has to be deterministic, so that input A can be
// associated with crash B consistently. The current approach
// taken by go-fuzz-headers to achieve that uses the data input
// as a buffer which is used until its end (i.e. EOF).
//
// The problem with this approach is when the test setup requires
// a higher amount of input than the available in the buffer,
// resulting in an invalid test state.
//
// This is currently being countered by openning two consumers on
// the data input, and requiring at least a length of 1000 to
// run the tests.
//
// During the migration to the native fuzz feature in go we should
// review this approach.
if len(data) < 1000 {
return 0
}
fmt.Printf("Data input length: %d\n", len(data))
f := fuzz.NewConsumer(data)
ff := fuzz.NewConsumer(data)
doOnce.Do(func() {
if err := ensureDependencies(); err != nil {
panic(fmt.Sprintf("Failed to ensure dependencies: %v", err))
}
})
err := runInContext(func(testEnv *testenv.Environment) {
controllerName := "kustomize-controller"
reconciler := &controllers.KustomizationReconciler{
ControllerName: controllerName,
Client: testEnv,
}
if err := (reconciler).SetupWithManager(testEnv, controllers.KustomizationReconcilerOptions{MaxConcurrentReconciles: 1}); err != nil {
panic(fmt.Sprintf("Failed to start GitRepositoryReconciler: %v", err))
}
}, func() error {
dname, err := os.MkdirTemp("", "artifact-dir")
if err != nil {
return err
}
defer os.RemoveAll(dname)
if err = createFiles(ff, dname); err != nil {
return err
}
namespaceName, err := randStringRange(f, 1, 63)
if err != nil {
return err
}
namespace, err := createNamespaceForFuzzing(namespaceName)
if err != nil {
return err
}
defer k8sClient.Delete(context.Background(), namespace)
fmt.Println("createKubeConfigSecretForFuzzing...")
secret, err := createKubeConfigSecretForFuzzing(namespaceName)
if err != nil {
fmt.Println(err)
return err
}
defer k8sClient.Delete(context.Background(), secret)
artifactFile, err := randStringRange(f, 1, 63)
if err != nil {
return err
}
fmt.Println("createArtifact...")
artifactChecksum, err := createArtifact(testServer, dname, artifactFile)
if err != nil {
fmt.Println(err)
return err
}
repName, err := randStringRange(f, 1, 63)
if err != nil {
return err
}
repositoryName := types.NamespacedName{
Name: repName,
Namespace: namespaceName,
}
fmt.Println("ApplyGitRepository...")
err = applyGitRepository(repositoryName, artifactFile, "main/"+artifactChecksum)
if err != nil {
return err
}
pgpKey, err := testFiles.ReadFile("testdata/sops/pgp.asc")
if err != nil {
return err
}
ageKey, err := testFiles.ReadFile("testdata/sops/age.txt")
if err != nil {
return err
}
sskName, err := randStringRange(f, 1, 63)
if err != nil {
return err
}
sopsSecretKey := types.NamespacedName{
Name: sskName,
Namespace: namespaceName,
}
sopsSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: sopsSecretKey.Name,
Namespace: sopsSecretKey.Namespace,
},
StringData: map[string]string{
"pgp.asc": string(pgpKey),
"age.agekey": string(ageKey),
},
}
if err = k8sClient.Create(context.Background(), sopsSecret); err != nil {
return err
}
defer k8sClient.Delete(context.Background(), sopsSecret)
kkName, err := randStringRange(f, 1, 63)
if err != nil {
return err
}
kustomizationKey := types.NamespacedName{
Name: kkName,
Namespace: namespaceName,
}
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: namespaceName,
},
}
if err = k8sClient.Create(context.TODO(), kustomization); err != nil {
return err
}
// ensure reconciliation of the kustomization above took place before moving on
time.Sleep(10 * time.Second)
if err = k8sClient.Delete(context.TODO(), kustomization); err != nil {
return err
}
// ensure the deferred deletion of all objects (namespace, secret, sopSecret) and
// the kustomization above were reconciled before moving on. This avoids unneccessary
// errors whilst tearing down the testing infrastructure.
time.Sleep(10 * time.Second)
return nil
}, "testdata/crd")
if err != nil {
fmt.Println(err)
return 0
}
return 1
}
// Allows the fuzzer to create a random lowercase string within a given range
func randStringRange(f *fuzz.ConsumeFuzzer, minLen, maxLen int) (string, error) {
stringLength, err := f.GetInt()
if err != nil {
return "", err
}
len := stringLength % maxLen
if len < minLen {
len += minLen
}
return f.GetStringFrom(string(letterRunes), len)
}
// Creates a namespace and returns the created object.
func createNamespaceForFuzzing(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 and returns the created object.
func createKubeConfigSecretForFuzzing(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
}
// Creates pseudo-random files in rootDir.
// Will create subdirs and place the files there.
// It is the callers responsibility to ensure that
// rootDir exists.
//
// Original source:
// https://github.com/AdaLogics/go-fuzz-headers/blob/9f22f86e471065b8d56861991dc885e27b1ae7de/consumer.go#L345
//
// The change assures that as long as the f buffer has
// enough length to set numberOfFiles and the first fileName,
// this is returned without errors.
// Effectively making subDir and FileContent optional.
func createFiles(f *fuzz.ConsumeFuzzer, rootDir string) error {
noOfCreatedFiles := 0
numberOfFiles, err := f.GetInt()
if err != nil {
return err
}
maxNoFiles := numberOfFiles % 10000 //
for i := 0; i <= maxNoFiles; i++ {
fileName, err := f.GetString()
if err != nil {
if noOfCreatedFiles > 0 {
return nil
} else {
return errors.New("Could not get fileName")
}
}
if fileName == "" {
continue
}
// leave subDir empty if no more strings are available.
subDir, _ := f.GetString()
// Avoid going outside the root dir
if strings.Contains(subDir, "../") || (len(subDir) > 0 && subDir[0] == 47) || strings.Contains(subDir, "\\") {
continue // continue as this is not a permanent error
}
dirPath, err := securejoin.SecureJoin(rootDir, subDir)
if err != nil {
continue // some errors here are not permanent, so we can try again with different values
}
err = os.MkdirAll(dirPath, 0o755)
if err != nil {
if noOfCreatedFiles > 0 {
return nil
} else {
return errors.New("Could not create the subDir")
}
}
fullFilePath, err := securejoin.SecureJoin(dirPath, fileName)
if err != nil {
continue // potentially not a permanent error
}
// leave fileContents empty if no more bytes are available,
// afterall empty files is a valid test case.
fileContents, _ := f.GetBytes()
createdFile, err := os.Create(fullFilePath)
if err != nil {
createdFile.Close()
continue // some errors here are not permanent, so we can try again with different values
}
_, err = createdFile.Write(fileContents)
if err != nil {
createdFile.Close()
if noOfCreatedFiles > 0 {
return nil
} else {
return errors.New("Could not write the file")
}
}
createdFile.Close()
noOfCreatedFiles++
}
return nil
}

5
tests/fuzz/go.mod Normal file
View File

@ -0,0 +1,5 @@
module github.com/fluxcd/kustomize-controller/tests/fuzz
// This module is used only to avoid polluting the main module
// with fuzz dependencies.
go 1.17

61
tests/fuzz/oss_fuzz_build.sh Executable file
View File

@ -0,0 +1,61 @@
#!/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
GOPATH="${GOPATH:-/root/go}"
GO_SRC="${GOPATH}/src"
PROJECT_PATH="github.com/fluxcd/kustomize-controller"
cd "${GO_SRC}"
# Move fuzzer to their respective directories.
# This removes dependency noises from the modules' go.mod and go.sum files.
mv "${PROJECT_PATH}/tests/fuzz/age_fuzzer.go" "${PROJECT_PATH}/internal/sops/age/"
mv "${PROJECT_PATH}/tests/fuzz/pgp_fuzzer.go" "${PROJECT_PATH}/internal/sops/pgp/"
# Some private functions within suite_test.go are extremly useful for testing.
# Instead of duplicating them here, or refactoring them away, this simply renames
# the file to make it available to "non-testing code".
# This is a temporary fix, which will cease once the implementation is migrated to
# the built-in fuzz support in golang 1.18.
cp "${PROJECT_PATH}/controllers/suite_test.go" "${PROJECT_PATH}/tests/fuzz/fuzzer_helper.go"
sed -i 's;KustomizationReconciler;abc.KustomizationReconciler;g' "${PROJECT_PATH}/tests/fuzz/fuzzer_helper.go"
sed -i 's;import (;import(\n abc "github.com/fluxcd/kustomize-controller/controllers";g' "${PROJECT_PATH}/tests/fuzz/fuzzer_helper.go"
pushd "${PROJECT_PATH}"
go mod tidy
compile_go_fuzzer "${PROJECT_PATH}/internal/sops/age/" FuzzAge fuzz_age
compile_go_fuzzer "${PROJECT_PATH}/internal/sops/pgp/" FuzzPgp fuzz_pgp
popd
pushd "${PROJECT_PATH}/tests/fuzz"
# Setup files to be embedded into controllers_fuzzer.go's testFiles variable.
mkdir -p testdata/crd
mkdir -p testdata/sops
cp ../../config/crd/bases/*.yaml testdata/crd
cp ../../controllers/testdata/sops/age.txt testdata/sops
cp ../../controllers/testdata/sops/pgp.asc testdata/sops
go mod tidy
compile_go_fuzzer "${PROJECT_PATH}/tests/fuzz/" FuzzControllers fuzz_controllers
popd

20
tests/fuzz/oss_fuzz_run.sh Executable file
View File

@ -0,0 +1,20 @@
#!/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
# run each fuzzer once to ensure they are working properly
find /out -type f -name "fuzz*" -exec echo {} -runs=1 \; | bash -e

39
tests/fuzz/pgp_fuzzer.go Normal file
View File

@ -0,0 +1,39 @@
//go:build gofuzz
// +build gofuzz
/*
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.
*/
package pgp
import (
fuzz "github.com/AdaLogics/go-fuzz-headers"
)
// FuzzPgp implements a fuzzer that targets functions within pgp/keysource.go.
func FuzzPgp(data []byte) int {
f := fuzz.NewConsumer(data)
masterKey := MasterKey{}
if err := f.GenerateStruct(&masterKey); err != nil {
return 0
}
_ = masterKey.Encrypt(data)
_ = masterKey.EncryptIfNeeded(data)
return 1
}