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-17 20:45:16 +00:00
parent c4d512399c
commit 5572b8a553
No known key found for this signature in database
GPG Key ID: 9995233870E99BEE
12 changed files with 299 additions and 172 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

@ -5,7 +5,6 @@
*.so
*.dylib
bin
testbin
# Test binary, build with `go test -c`
*.test
@ -22,3 +21,5 @@ testbin
*.swp
*.swo
*~
build/

View File

@ -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"

View File

@ -820,8 +820,10 @@ func (in HelmReleaseStatus) GetHelmChart() (string, string) {
if in.HelmChart == "" {
return "", ""
}
split := strings.Split(in.HelmChart, string(types.Separator))
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

View File

@ -1,21 +0,0 @@
FROM golang:1.16-buster as builder
RUN apt-get update && apt-get install -y clang git
RUN git clone --branch fuzz1 --depth 1 https://github.com/AdamKorcz/helm-controller /helm-controller
COPY fuzz.go /helm-controller/controllers/
RUN cd /helm-controller/controllers \
&& go get github.com/AdaLogics/go-fuzz-headers
RUN cd / \
&& go get -u github.com/mdempsky/go114-fuzz-build
RUN mkdir /fuzzers
RUN cd /helm-controller/controllers \
&& go114-fuzz-build -o FuzzHelmreleaseComposeValues.a -func FuzzHelmreleaseComposeValues . \
&& clang -o /fuzzers/FuzzHelmreleaseComposeValues FuzzHelmreleaseComposeValues.a -fsanitize=fuzzer
RUN cd /helm-controller/controllers \
&& go114-fuzz-build -o FuzzHelmreleasereconcile.a -func FuzzHelmreleasereconcile . \
&& clang -o /fuzzers/FuzzHelmreleasereconcile FuzzHelmreleasereconcile.a -fsanitize=fuzzer
# Uncomment here to run the fuzzer
#RUN /fuzzers/FuzzHelmreleaseComposeValues

View File

@ -1,147 +0,0 @@
//go:build gofuzz
// +build gofuzz
/*
Copyright 2020 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"
v2 "github.com/fluxcd/helm-controller/api/v2beta1"
"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"
"sigs.k8s.io/controller-runtime/pkg/log"
"sync"
fuzz "github.com/AdaLogics/go-fuzz-headers"
)
var (
initter sync.Once
scheme *runtime.Scheme
)
// An init function that is invoked by way of sync.Do
func initFunc() {
scheme = runtime.NewScheme()
err := corev1.AddToScheme(scheme)
if err != nil {
panic(err)
}
err = v2.AddToScheme(scheme)
if err != nil {
panic(err)
}
}
// FuzzHelmreleaseComposeValues implements a fuzzer
// that targets HelmReleaseReconciler.composeValues()
func FuzzHelmreleaseComposeValues(data []byte) int {
initter.Do(initFunc)
f := fuzz.NewConsumer(data)
hr := v2.HelmRelease{}
err := f.GenerateStruct(&hr)
if err != nil {
return 0
}
r, err := createReconciler(f)
if err != nil {
return 0
}
_, _ = r.composeValues(logr.NewContext(context.TODO(), log.NullLogger{}), hr)
return 1
}
// FuzzHelmreleasereconcile implements a fuzzer
// that targets HelmReleaseReconciler.reconcile()
func FuzzHelmreleasereconcile(data []byte) int {
initter.Do(initFunc)
f := fuzz.NewConsumer(data)
hr := v2.HelmRelease{}
err := f.GenerateStruct(&hr)
if err != nil {
return 0
}
r, err := createReconciler(f)
if err != nil {
return 0
}
_, _, _ = r.reconcile(logr.NewContext(context.TODO(), log.NullLogger{}), hr)
return 1
}
// Allows the fuzzer to create a reconciler
func createReconciler(f *fuzz.ConsumeFuzzer) (*HelmReleaseReconciler, error) {
// Get the type of object:
var resources []runtime.Object
r := &HelmReleaseReconciler{}
getSecret, err := f.GetBool()
if err != nil {
return r, err
}
name, err := f.GetString()
if err != nil {
return r, err
}
if getSecret {
inputMap := make(map[string][]byte)
err = f.FuzzMap(&inputMap)
if err != nil {
return r, err
}
resources = []runtime.Object{
valuesSecret(name, inputMap),
}
} else {
inputMap := make(map[string]string)
err = f.FuzzMap(&inputMap)
if err != nil {
return r, err
}
resources = []runtime.Object{
valuesConfigMap(name, inputMap),
}
}
c := fake.NewFakeClientWithScheme(scheme, resources...)
r.Client = c
return r, nil
}
// Taken from
// https://github.com/fluxcd/helm-controller/blob/main/controllers/helmrelease_controller_test.go#L282
func valuesSecret(name string, data map[string][]byte) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: name},
Data: data,
}
}
// Taken from
// https://github.com/fluxcd/helm-controller/blob/main/controllers/helmrelease_controller_test.go#L289
func valuesConfigMap(name string, data map[string]string) *corev1.ConfigMap {
return &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: name},
Data: data,
}
}

View File

@ -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

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

View File

@ -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
}

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

@ -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

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

@ -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

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