diff --git a/.github/workflows/cifuzz.yaml b/.github/workflows/cifuzz.yaml new file mode 100644 index 000000000..202ce966d --- /dev/null +++ b/.github/workflows/cifuzz.yaml @@ -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 diff --git a/.gitignore b/.gitignore index efab6fe4b..dbd3c44c9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ *.so *.dylib bin -testbin # Test binary, build with `go test -c` *.test @@ -22,3 +21,5 @@ testbin *.swp *.swo *~ + +build/ diff --git a/Makefile b/Makefile index 95c3e33ab..43f511b7e 100644 --- a/Makefile +++ b/Makefile @@ -113,7 +113,7 @@ GEN_CRD_API_REFERENCE_DOCS = $(shell pwd)/bin/gen-crd-api-reference-docs gen-crd-api-reference-docs: $(call go-install-tool,$(GEN_CRD_API_REFERENCE_DOCS),github.com/ahmetb/gen-crd-api-reference-docs@v0.3.0) -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} @@ -137,3 +137,23 @@ 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 + +# Run each fuzzer once to ensure they are working +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" diff --git a/api/v2beta1/helmrelease_types.go b/api/v2beta1/helmrelease_types.go index 3dca93e34..6fa683d18 100644 --- a/api/v2beta1/helmrelease_types.go +++ b/api/v2beta1/helmrelease_types.go @@ -820,8 +820,10 @@ func (in HelmReleaseStatus) GetHelmChart() (string, string) { if in.HelmChart == "" { return "", "" } - split := strings.Split(in.HelmChart, string(types.Separator)) - return split[0], split[1] + if split := strings.Split(in.HelmChart, string(types.Separator)); len(split) > 1 { + return split[0], split[1] + } + return "", "" } // HelmReleaseProgressing resets any failures and registers progress toward diff --git a/tests/fuzz/Dockerfile.builder b/tests/fuzz/Dockerfile.builder new file mode 100644 index 000000000..0e8cbaf3e --- /dev/null +++ b/tests/fuzz/Dockerfile.builder @@ -0,0 +1,6 @@ +FROM gcr.io/oss-fuzz-base/base-builder-go + +COPY ./ $GOPATH/src/github.com/fluxcd/helm-controller/ +COPY ./tests/fuzz/oss_fuzz_build.sh $SRC/build.sh + +WORKDIR $SRC diff --git a/tests/fuzz/README.md b/tests/fuzz/README.md new file mode 100644 index 000000000..f2d233967 --- /dev/null +++ b/tests/fuzz/README.md @@ -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 diff --git a/tests/fuzz/fuzz_controllers.go b/tests/fuzz/fuzz_controllers.go new file mode 100644 index 000000000..061308534 --- /dev/null +++ b/tests/fuzz/fuzz_controllers.go @@ -0,0 +1,139 @@ +//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 controllers + +import ( + "context" + "sync" + + v2 "github.com/fluxcd/helm-controller/api/v2beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + fuzz "github.com/AdaLogics/go-fuzz-headers" +) + +var ( + doOnce sync.Once + scheme = runtime.NewScheme() +) + +// An init function that is invoked by way of sync.Do +func initFunc() { + _ = corev1.AddToScheme(scheme) + _ = v2.AddToScheme(scheme) + _ = sourcev1.AddToScheme(scheme) +} + +// FuzzHelmreleaseComposeValues implements a fuzzer +// that targets HelmReleaseReconciler.composeValues(). +func FuzzHelmreleaseComposeValues(data []byte) int { + doOnce.Do(initFunc) + + f := fuzz.NewConsumer(data) + + resources, err := getResources(f) + if err != nil { + return 0 + } + + c := fake.NewFakeClientWithScheme(scheme, resources...) + r := &HelmReleaseReconciler{Client: c} + + hr := v2.HelmRelease{} + err = f.GenerateStruct(&hr) + if err != nil { + return 0 + } + + _, _ = r.composeValues(logr.NewContext(context.TODO(), logr.Discard()), hr) + + return 1 +} + +// FuzzHelmreleaseComposeValues implements a fuzzer +// that targets HelmReleaseReconciler.reconcile(). +func FuzzHelmreleaseReconcile(data []byte) int { + doOnce.Do(initFunc) + + f := fuzz.NewConsumer(data) + + resources, err := getResources(f) + if err != nil { + return 0 + } + + hr := v2.HelmRelease{} + err = f.GenerateStruct(&hr) + if err != nil { + return 0 + } + + hc := sourcev1.HelmChart{} + err = f.GenerateStruct(&hc) + if err != nil { + return 0 + } + + hc.ObjectMeta.Name = hr.GetHelmChartName() + hc.ObjectMeta.Namespace = hr.Spec.Chart.GetNamespace(hr.Namespace) + resources = append(resources, &hc) + + c := fake.NewFakeClientWithScheme(scheme, resources...) + r := &HelmReleaseReconciler{Client: c} + + _, _, _ = r.reconcile(logr.NewContext(context.TODO(), logr.Discard()), hr) + + return 1 +} + +func getResources(f *fuzz.ConsumeFuzzer) ([]runtime.Object, error) { + resources := make([]runtime.Object, 0) + + name, err := f.GetString() + if err != nil { + return nil, err + } + + if createSecret, _ := f.GetBool(); createSecret { + inputByte := make(map[string][]byte) + f.FuzzMap(&inputByte) // ignore error, as empty is still valid + resources = append(resources, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + Data: inputByte, + }) + } + + if createConfigMap, _ := f.GetBool(); createConfigMap { + inputString := make(map[string]string) + f.FuzzMap(&inputString) // ignore error, as empty is still valid + resources = append(resources, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + Data: inputString, + }) + } + + return resources, nil +} diff --git a/tests/fuzz/go.mod b/tests/fuzz/go.mod new file mode 100644 index 000000000..bea70c011 --- /dev/null +++ b/tests/fuzz/go.mod @@ -0,0 +1,5 @@ +module github.com/fluxcd/helm-controller/tests/fuzz +// This module is used only to avoid polluting the main module +// with fuzz dependencies. + +go 1.17 diff --git a/tests/fuzz/oss_fuzz_build.sh b/tests/fuzz/oss_fuzz_build.sh new file mode 100755 index 000000000..25c5b4195 --- /dev/null +++ b/tests/fuzz/oss_fuzz_build.sh @@ -0,0 +1,37 @@ +#!/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/helm-controller" + +cd "${GO_SRC}" + +# Move fuzzer to their respective directories. +# This removes dependency noises from the modules' go.mod and go.sum files. +cp "${PROJECT_PATH}/tests/fuzz/fuzz_controllers.go" "${PROJECT_PATH}/controllers/" + + +# compile fuzz tests for the runtime module +pushd "${PROJECT_PATH}" + +go mod tidy +compile_go_fuzzer "${PROJECT_PATH}/controllers/" FuzzHelmreleaseComposeValues fuzz_helmrelease_composevalues +compile_go_fuzzer "${PROJECT_PATH}/controllers/" FuzzHelmreleaseReconcile fuzz_helmrelease_reconcile + +popd diff --git a/tests/fuzz/oss_fuzz_run.sh b/tests/fuzz/oss_fuzz_run.sh new file mode 100755 index 000000000..4c87f489b --- /dev/null +++ b/tests/fuzz/oss_fuzz_run.sh @@ -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