Merge pull request #565 from pjbgf/fix-broken-fuzz
build: Fix cifuzz and improve fuzz tests' reliability
This commit is contained in:
		
						commit
						8d1afa6994
					
				|  | @ -0,0 +1,273 @@ | |||
| //go:build gofuzz_libfuzzer
 | ||||
| // +build gofuzz_libfuzzer
 | ||||
| 
 | ||||
| /* | ||||
| 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" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/go-logr/logr" | ||||
| 	corev1 "k8s.io/api/core/v1" | ||||
| 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/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/yaml" | ||||
| 
 | ||||
| 	v2 "github.com/fluxcd/helm-controller/api/v2beta1" | ||||
| 	sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" | ||||
| ) | ||||
| 
 | ||||
| func FuzzHelmReleaseReconciler_composeValues(f *testing.F) { | ||||
| 	scheme := testScheme() | ||||
| 
 | ||||
| 	tests := []struct { | ||||
| 		targetPath   string | ||||
| 		valuesKey    string | ||||
| 		hrValues     string | ||||
| 		createObject bool | ||||
| 		secretData   []byte | ||||
| 		configData   string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			targetPath: "flat", | ||||
| 			valuesKey:  "custom-values.yaml", | ||||
| 			secretData: []byte(`flat: | ||||
|   nested: value | ||||
| nested: value | ||||
| `), | ||||
| 			configData: `flat: value | ||||
| nested: | ||||
|   configuration: value | ||||
| `, | ||||
| 			hrValues: ` | ||||
| other: values | ||||
| `, | ||||
| 			createObject: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			targetPath: "'flat'", | ||||
| 			valuesKey:  "custom-values.yaml", | ||||
| 			secretData: []byte(`flat: | ||||
|   nested: value | ||||
| nested: value | ||||
| `), | ||||
| 			configData: `flat: value | ||||
| nested: | ||||
|   configuration: value | ||||
| `, | ||||
| 			hrValues: ` | ||||
| other: values | ||||
| `, | ||||
| 			createObject: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			targetPath: "flat[0]", | ||||
| 			secretData: []byte(``), | ||||
| 			configData: `flat: value`, | ||||
| 			hrValues: ` | ||||
| other: values | ||||
| `, | ||||
| 			createObject: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			secretData: []byte(`flat: | ||||
|   nested: value | ||||
| nested: value | ||||
| `), | ||||
| 			configData: `flat: value | ||||
| nested: | ||||
|   configuration: value | ||||
| `, | ||||
| 			hrValues: ` | ||||
| other: values | ||||
| `, | ||||
| 			createObject: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			targetPath: "some-value", | ||||
| 			hrValues: ` | ||||
| other: values | ||||
| `, | ||||
| 			createObject: false, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		f.Add(tt.targetPath, tt.valuesKey, tt.hrValues, tt.createObject, tt.secretData, tt.configData) | ||||
| 	} | ||||
| 
 | ||||
| 	f.Fuzz(func(t *testing.T, | ||||
| 		targetPath, valuesKey, hrValues string, createObject bool, secretData []byte, configData string) { | ||||
| 
 | ||||
| 		// objectName represents a core Kubernetes name (Secret/ConfigMap) which is validated
 | ||||
| 		// upstream, and also validated by us in the OpenAPI-based validation set in
 | ||||
| 		// v2.ValuesReference. Therefore a static value here suffices, and instead we just
 | ||||
| 		// play with the objects presence/absence.
 | ||||
| 		objectName := "values" | ||||
| 		resources := []runtime.Object{} | ||||
| 
 | ||||
| 		if createObject { | ||||
| 			resources = append(resources, | ||||
| 				valuesConfigMap(objectName, map[string]string{valuesKey: configData}), | ||||
| 				valuesSecret(objectName, map[string][]byte{valuesKey: secretData}), | ||||
| 			) | ||||
| 		} | ||||
| 
 | ||||
| 		references := []v2.ValuesReference{ | ||||
| 			{ | ||||
| 				Kind:       "ConfigMap", | ||||
| 				Name:       objectName, | ||||
| 				ValuesKey:  valuesKey, | ||||
| 				TargetPath: targetPath, | ||||
| 			}, | ||||
| 			{ | ||||
| 				Kind:       "Secret", | ||||
| 				Name:       objectName, | ||||
| 				ValuesKey:  valuesKey, | ||||
| 				TargetPath: targetPath, | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		c := fake.NewFakeClientWithScheme(scheme, resources...) | ||||
| 		r := &HelmReleaseReconciler{Client: c} | ||||
| 		var values *apiextensionsv1.JSON | ||||
| 		if hrValues != "" { | ||||
| 			v, _ := yaml.YAMLToJSON([]byte(hrValues)) | ||||
| 			values = &apiextensionsv1.JSON{Raw: v} | ||||
| 		} | ||||
| 
 | ||||
| 		hr := v2.HelmRelease{ | ||||
| 			Spec: v2.HelmReleaseSpec{ | ||||
| 				ValuesFrom: references, | ||||
| 				Values:     values, | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		// OpenAPI-based validation on schema is not verified here.
 | ||||
| 		// Therefore some false positives may be arise, as the apiserver
 | ||||
| 		// would not allow such values to make their way into the control plane.
 | ||||
| 		//
 | ||||
| 		// Testenv could be used so the fuzzing covers the entire E2E.
 | ||||
| 		// The downsize being the resource and time cost per test would be a lot higher.
 | ||||
| 		//
 | ||||
| 		// Another approach could be to add validation to reject invalid inputs before
 | ||||
| 		// the r.composeValues call.
 | ||||
| 		_, _ = r.composeValues(logr.NewContext(context.TODO(), logr.Discard()), hr) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func FuzzHelmReleaseReconciler_reconcile(f *testing.F) { | ||||
| 	scheme := testScheme() | ||||
| 	tests := []struct { | ||||
| 		valuesKey  string | ||||
| 		hrValues   string | ||||
| 		secretData []byte | ||||
| 		configData string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			valuesKey: "custom-values.yaml", | ||||
| 			secretData: []byte(`flat: | ||||
|   nested: value | ||||
| nested: value | ||||
| `), | ||||
| 			configData: `flat: value | ||||
| nested: | ||||
|   configuration: value | ||||
| `, | ||||
| 			hrValues: ` | ||||
| other: values | ||||
| `, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		f.Add(tt.valuesKey, tt.hrValues, tt.secretData, tt.configData) | ||||
| 	} | ||||
| 
 | ||||
| 	f.Fuzz(func(t *testing.T, | ||||
| 		valuesKey, hrValues string, secretData []byte, configData string) { | ||||
| 
 | ||||
| 		var values *apiextensionsv1.JSON | ||||
| 		if hrValues != "" { | ||||
| 			v, _ := yaml.YAMLToJSON([]byte(hrValues)) | ||||
| 			values = &apiextensionsv1.JSON{Raw: v} | ||||
| 		} | ||||
| 
 | ||||
| 		hr := v2.HelmRelease{ | ||||
| 			Spec: v2.HelmReleaseSpec{ | ||||
| 				Values: values, | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		hc := sourcev1.HelmChart{} | ||||
| 		hc.ObjectMeta.Name = hr.GetHelmChartName() | ||||
| 		hc.ObjectMeta.Namespace = hr.Spec.Chart.GetNamespace(hr.Namespace) | ||||
| 
 | ||||
| 		resources := []runtime.Object{ | ||||
| 			valuesConfigMap("values", map[string]string{valuesKey: configData}), | ||||
| 			valuesSecret("values", map[string][]byte{valuesKey: secretData}), | ||||
| 			&hc, | ||||
| 		} | ||||
| 
 | ||||
| 		c := fake.NewFakeClientWithScheme(scheme, resources...) | ||||
| 		r := &HelmReleaseReconciler{ | ||||
| 			Client:        c, | ||||
| 			EventRecorder: &DummyRecorder{}, | ||||
| 		} | ||||
| 
 | ||||
| 		_, _, _ = r.reconcile(logr.NewContext(context.TODO(), logr.Discard()), hr) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func valuesSecret(name string, data map[string][]byte) *corev1.Secret { | ||||
| 	return &corev1.Secret{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{Name: name}, | ||||
| 		Data:       data, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func valuesConfigMap(name string, data map[string]string) *corev1.ConfigMap { | ||||
| 	return &corev1.ConfigMap{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{Name: name}, | ||||
| 		Data:       data, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func testScheme() *runtime.Scheme { | ||||
| 	scheme := runtime.NewScheme() | ||||
| 	_ = corev1.AddToScheme(scheme) | ||||
| 	_ = v2.AddToScheme(scheme) | ||||
| 	_ = sourcev1.AddToScheme(scheme) | ||||
| 	return scheme | ||||
| } | ||||
| 
 | ||||
| // DummyRecorder serves as a dummy for kuberecorder.EventRecorder.
 | ||||
| type DummyRecorder struct{} | ||||
| 
 | ||||
| func (r *DummyRecorder) Event(object runtime.Object, eventtype, reason, message string) { | ||||
| } | ||||
| 
 | ||||
| func (r *DummyRecorder) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) { | ||||
| } | ||||
| 
 | ||||
| func (r *DummyRecorder) AnnotatedEventf(object runtime.Object, annotations map[string]string, | ||||
| 	eventtype, reason string, messageFmt string, args ...interface{}) { | ||||
| } | ||||
|  | @ -34,7 +34,6 @@ import ( | |||
| 	"sigs.k8s.io/yaml" | ||||
| 
 | ||||
| 	v2 "github.com/fluxcd/helm-controller/api/v2beta1" | ||||
| 	sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" | ||||
| ) | ||||
| 
 | ||||
| func TestHelmReleaseReconciler_composeValues(t *testing.T) { | ||||
|  | @ -447,208 +446,6 @@ func TestValuesReferenceValidation(t *testing.T) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func FuzzHelmReleaseReconciler_composeValues(f *testing.F) { | ||||
| 	scheme := testScheme() | ||||
| 
 | ||||
| 	tests := []struct { | ||||
| 		targetPath   string | ||||
| 		valuesKey    string | ||||
| 		hrValues     string | ||||
| 		createObject bool | ||||
| 		secretData   []byte | ||||
| 		configData   string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			targetPath: "flat", | ||||
| 			valuesKey:  "custom-values.yaml", | ||||
| 			secretData: []byte(`flat: | ||||
|   nested: value | ||||
| nested: value | ||||
| `), | ||||
| 			configData: `flat: value | ||||
| nested: | ||||
|   configuration: value | ||||
| `, | ||||
| 			hrValues: ` | ||||
| other: values | ||||
| `, | ||||
| 			createObject: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			targetPath: "'flat'", | ||||
| 			valuesKey:  "custom-values.yaml", | ||||
| 			secretData: []byte(`flat: | ||||
|   nested: value | ||||
| nested: value | ||||
| `), | ||||
| 			configData: `flat: value | ||||
| nested: | ||||
|   configuration: value | ||||
| `, | ||||
| 			hrValues: ` | ||||
| other: values | ||||
| `, | ||||
| 			createObject: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			targetPath: "flat[0]", | ||||
| 			secretData: []byte(``), | ||||
| 			configData: `flat: value`, | ||||
| 			hrValues: ` | ||||
| other: values | ||||
| `, | ||||
| 			createObject: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			secretData: []byte(`flat: | ||||
|   nested: value | ||||
| nested: value | ||||
| `), | ||||
| 			configData: `flat: value | ||||
| nested: | ||||
|   configuration: value | ||||
| `, | ||||
| 			hrValues: ` | ||||
| other: values | ||||
| `, | ||||
| 			createObject: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			targetPath: "some-value", | ||||
| 			hrValues: ` | ||||
| other: values | ||||
| `, | ||||
| 			createObject: false, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		f.Add(tt.targetPath, tt.valuesKey, tt.hrValues, tt.createObject, tt.secretData, tt.configData) | ||||
| 	} | ||||
| 
 | ||||
| 	f.Fuzz(func(t *testing.T, | ||||
| 		targetPath, valuesKey, hrValues string, createObject bool, secretData []byte, configData string) { | ||||
| 
 | ||||
| 		// objectName represents a core Kubernetes name (Secret/ConfigMap) which is validated
 | ||||
| 		// upstream, and also validated by us in the OpenAPI-based validation set in
 | ||||
| 		// v2.ValuesReference. Therefore a static value here suffices, and instead we just
 | ||||
| 		// play with the objects presence/absence.
 | ||||
| 		objectName := "values" | ||||
| 		resources := []runtime.Object{} | ||||
| 
 | ||||
| 		if createObject { | ||||
| 			resources = append(resources, | ||||
| 				valuesConfigMap(objectName, map[string]string{valuesKey: configData}), | ||||
| 				valuesSecret(objectName, map[string][]byte{valuesKey: secretData}), | ||||
| 			) | ||||
| 		} | ||||
| 
 | ||||
| 		references := []v2.ValuesReference{ | ||||
| 			{ | ||||
| 				Kind:       "ConfigMap", | ||||
| 				Name:       objectName, | ||||
| 				ValuesKey:  valuesKey, | ||||
| 				TargetPath: targetPath, | ||||
| 			}, | ||||
| 			{ | ||||
| 				Kind:       "Secret", | ||||
| 				Name:       objectName, | ||||
| 				ValuesKey:  valuesKey, | ||||
| 				TargetPath: targetPath, | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		c := fake.NewFakeClientWithScheme(scheme, resources...) | ||||
| 		r := &HelmReleaseReconciler{Client: c} | ||||
| 		var values *apiextensionsv1.JSON | ||||
| 		if hrValues != "" { | ||||
| 			v, _ := yaml.YAMLToJSON([]byte(hrValues)) | ||||
| 			values = &apiextensionsv1.JSON{Raw: v} | ||||
| 		} | ||||
| 
 | ||||
| 		hr := v2.HelmRelease{ | ||||
| 			Spec: v2.HelmReleaseSpec{ | ||||
| 				ValuesFrom: references, | ||||
| 				Values:     values, | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		// OpenAPI-based validation on schema is not verified here.
 | ||||
| 		// Therefore some false positives may be arise, as the apiserver
 | ||||
| 		// would not allow such values to make their way into the control plane.
 | ||||
| 		//
 | ||||
| 		// Testenv could be used so the fuzzing covers the entire E2E.
 | ||||
| 		// The downsize being the resource and time cost per test would be a lot higher.
 | ||||
| 		//
 | ||||
| 		// Another approach could be to add validation to reject invalid inputs before
 | ||||
| 		// the r.composeValues call.
 | ||||
| 		_, _ = r.composeValues(logr.NewContext(context.TODO(), logr.Discard()), hr) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func FuzzHelmReleaseReconciler_reconcile(f *testing.F) { | ||||
| 	scheme := testScheme() | ||||
| 	tests := []struct { | ||||
| 		valuesKey  string | ||||
| 		hrValues   string | ||||
| 		secretData []byte | ||||
| 		configData string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			valuesKey: "custom-values.yaml", | ||||
| 			secretData: []byte(`flat: | ||||
|   nested: value | ||||
| nested: value | ||||
| `), | ||||
| 			configData: `flat: value | ||||
| nested: | ||||
|   configuration: value | ||||
| `, | ||||
| 			hrValues: ` | ||||
| other: values | ||||
| `, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		f.Add(tt.valuesKey, tt.hrValues, tt.secretData, tt.configData) | ||||
| 	} | ||||
| 
 | ||||
| 	f.Fuzz(func(t *testing.T, | ||||
| 		valuesKey, hrValues string, secretData []byte, configData string) { | ||||
| 
 | ||||
| 		var values *apiextensionsv1.JSON | ||||
| 		if hrValues != "" { | ||||
| 			v, _ := yaml.YAMLToJSON([]byte(hrValues)) | ||||
| 			values = &apiextensionsv1.JSON{Raw: v} | ||||
| 		} | ||||
| 
 | ||||
| 		hr := v2.HelmRelease{ | ||||
| 			Spec: v2.HelmReleaseSpec{ | ||||
| 				Values: values, | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		hc := sourcev1.HelmChart{} | ||||
| 		hc.ObjectMeta.Name = hr.GetHelmChartName() | ||||
| 		hc.ObjectMeta.Namespace = hr.Spec.Chart.GetNamespace(hr.Namespace) | ||||
| 
 | ||||
| 		resources := []runtime.Object{ | ||||
| 			valuesConfigMap("values", map[string]string{valuesKey: configData}), | ||||
| 			valuesSecret("values", map[string][]byte{valuesKey: secretData}), | ||||
| 			&hc, | ||||
| 		} | ||||
| 
 | ||||
| 		c := fake.NewFakeClientWithScheme(scheme, resources...) | ||||
| 		r := &HelmReleaseReconciler{ | ||||
| 			Client:        c, | ||||
| 			EventRecorder: &DummyRecorder{}, | ||||
| 		} | ||||
| 
 | ||||
| 		_, _, _ = r.reconcile(logr.NewContext(context.TODO(), logr.Discard()), hr) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func valuesSecret(name string, data map[string][]byte) *corev1.Secret { | ||||
| 	return &corev1.Secret{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{Name: name}, | ||||
|  | @ -662,24 +459,3 @@ func valuesConfigMap(name string, data map[string]string) *corev1.ConfigMap { | |||
| 		Data:       data, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func testScheme() *runtime.Scheme { | ||||
| 	scheme := runtime.NewScheme() | ||||
| 	_ = corev1.AddToScheme(scheme) | ||||
| 	_ = v2.AddToScheme(scheme) | ||||
| 	_ = sourcev1.AddToScheme(scheme) | ||||
| 	return scheme | ||||
| } | ||||
| 
 | ||||
| // DummyRecorder serves as a dummy for kuberecorder.EventRecorder.
 | ||||
| type DummyRecorder struct{} | ||||
| 
 | ||||
| func (r *DummyRecorder) Event(object runtime.Object, eventtype, reason, message string) { | ||||
| } | ||||
| 
 | ||||
| func (r *DummyRecorder) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) { | ||||
| } | ||||
| 
 | ||||
| func (r *DummyRecorder) AnnotatedEventf(object runtime.Object, annotations map[string]string, | ||||
| 	eventtype, reason string, messageFmt string, args ...interface{}) { | ||||
| } | ||||
|  |  | |||
|  | @ -6,8 +6,6 @@ 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: | ||||
|  | @ -19,12 +17,12 @@ All fuzzers will be built into `./build/fuzz/out`. | |||
| 
 | ||||
| Smoke test fuzzers: | ||||
| 
 | ||||
| All the fuzzers will be built and executed once, to ensure they are fully functional. | ||||
| 
 | ||||
| ```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 | ||||
|  | @ -39,7 +37,46 @@ Run fuzzer inside a container: | |||
| 		/out/fuzz_conditions_match | ||||
| ``` | ||||
| 
 | ||||
| ### Caveats of creating oss-fuzz compatible tests | ||||
| 
 | ||||
| #### Segregate fuzz tests | ||||
| 
 | ||||
| OSS-Fuzz does not properly support mixed `*_test.go` files, in which there is a combination | ||||
| of fuzz and non-fuzz tests. To mitigate this problem, ensure your fuzz tests are not in the | ||||
| same file as other Go tests. As a pattern, call your fuzz test files `*_fuzz_test.go`. | ||||
| 
 | ||||
| #### Build tags to avoid conflicts when running Go tests | ||||
| 
 | ||||
| Due to the issue above, code duplication will occur when creating fuzz tests that rely on | ||||
| helper functions that are shared with other tests. To avoid build issues, add a conditional | ||||
| build tag at the top of the `*_fuzz_test.go` file: | ||||
| ```go | ||||
| //go:build gofuzz_libfuzzer | ||||
| // +build gofuzz_libfuzzer | ||||
| ``` | ||||
| 
 | ||||
| The build tag above is set at [go-118-fuzz-build]. | ||||
| At this point in time we can't pass on specific tags from [compile_native_go_fuzzer]. | ||||
| 
 | ||||
| ### Running oss-fuzz locally | ||||
| 
 | ||||
| The `make fuzz-smoketest` is meant to be an easy way to reproduce errors that may occur | ||||
| upstream. If our checks ever run out of sync with upstream, the upstream tests can be | ||||
| executed locally with: | ||||
| 
 | ||||
| ``` | ||||
| git clone --depth 1 https://github.com/google/oss-fuzz | ||||
| cd oss-fuzz | ||||
| python infra/helper.py build_image fluxcd | ||||
| python infra/helper.py build_fuzzers --sanitizer address --architecture x86_64 fluxcd | ||||
| python infra/helper.py check_build --sanitizer address --architecture x86_64 fluxcd | ||||
| ``` | ||||
| 
 | ||||
| For latest info on testing oss-fuzz locally, refer to the [upstream guide]. | ||||
| 
 | ||||
| [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 | ||||
| [upstream guide]: https://google.github.io/oss-fuzz/getting-started/new-project-guide/#testing-locally | ||||
| [go-118-fuzz-build]: https://github.com/AdamKorcz/go-118-fuzz-build/blob/b2031950a318d4f2dcf3ec3e128f904d5cf84623/main.go#L40 | ||||
| [compile_native_go_fuzzer]: https://github.com/google/oss-fuzz/blob/c2d827cb78529fdc757c9b0b4fea0f1238a54814/infra/base-images/base-builder/compile_native_go_fuzzer#L32 | ||||
|  | @ -16,64 +16,66 @@ | |||
| 
 | ||||
| set -euxo pipefail | ||||
| 
 | ||||
| # This file aims for: | ||||
| # - Dynamically discover and build all fuzz tests within the repository. | ||||
| # - Work for both local make fuzz-smoketest and the upstream oss-fuzz. | ||||
| 
 | ||||
| GOPATH="${GOPATH:-/root/go}" | ||||
| GO_SRC="${GOPATH}/src" | ||||
| PROJECT_PATH="github.com/fluxcd/helm-controller" | ||||
| TMP_DIR=$(mktemp -d /tmp/oss_fuzz-XXXXXX) | ||||
| 
 | ||||
| cleanup(){ | ||||
| 	rm -rf "${TMP_DIR}" | ||||
| } | ||||
| trap cleanup EXIT | ||||
| 
 | ||||
| # install_deps installs all dependencies needed for upstream oss-fuzz. | ||||
| # Unfortunately we can't pin versions here, as we want to always | ||||
| # have the latest, so that we can reproduce errors occuring upstream. | ||||
| 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" | ||||
| 	if ! command -v go-118-fuzz-build &> /dev/null; then | ||||
| 		go install github.com/AdamKorcz/go-118-fuzz-build@latest | ||||
| 	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}" | ||||
| 
 | ||||
| 	# After removing the body of the go testing funcs, consolidate the imports. | ||||
| 	goimports -w "${filename}" | ||||
| } | ||||
| 
 | ||||
| install_deps | ||||
| 
 | ||||
| cd "${GO_SRC}/${PROJECT_PATH}" | ||||
| 
 | ||||
| go get github.com/AdamKorcz/go-118-fuzz-build/utils | ||||
| # Ensure any project-specific requirements are catered for ahead of | ||||
| # the generic build process. | ||||
| if [ -f "tests/fuzz/oss_fuzz_prebuild.sh" ]; then | ||||
| 	tests/fuzz/oss_fuzz_prebuild.sh | ||||
| fi | ||||
| 
 | ||||
| # 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}" | ||||
| modules=$(find . -mindepth 1 -maxdepth 4 -type f -name 'go.mod' | cut -c 3- | sed 's|/[^/]*$$||' | sort -u | sed 's;/go.mod;;g' | sed 's;go.mod;.;g') | ||||
| 
 | ||||
| 	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}") | ||||
| for module in ${modules}; do | ||||
| 
 | ||||
| 		echo "Building ${file}.${target_name} into ${fuzzer_name}" | ||||
| 		compile_native_go_fuzzer "${target_dir}" "${target_name}" "${fuzzer_name}" | ||||
| 	cd "${GO_SRC}/${PROJECT_PATH}/${module}" | ||||
| 
 | ||||
| 	# TODO: stop ignoring recorder_fuzzer_test.go. Temporary fix for fuzzing building issues. | ||||
| 	test_files=$(grep -r --include='**_test.go' --files-with-matches 'func Fuzz' . || echo "") | ||||
| 	if [ -z "${test_files}" ]; then | ||||
| 		continue | ||||
| 	fi | ||||
| 
 | ||||
| 	go get github.com/AdamKorcz/go-118-fuzz-build/testing | ||||
| 
 | ||||
| 	# Iterate through all Go Fuzz targets, compiling each into a fuzzer. | ||||
| 	for file in ${test_files}; do | ||||
| 		# If the subdir is a module, skip this file, as it will be handled | ||||
| 		# at the next iteration of the outer loop.  | ||||
| 		if [ -f "$(dirname "${file}")/go.mod" ]; then | ||||
| 			continue | ||||
| 		fi | ||||
| 
 | ||||
| 		targets=$(grep -oP 'func \K(Fuzz\w*)' "${file}") | ||||
| 		for target_name in ${targets}; do | ||||
| 			# Transform module path into module name (e.g. git/libgit2 to git_libgit2). | ||||
| 			module_name=$(echo ${module} | tr / _) | ||||
| 			# Compose fuzzer name based on the lowercase version of the func names. | ||||
| 			# The module name is added after the fuzz prefix, for better discoverability. | ||||
| 			fuzzer_name=$(echo "${target_name}" | tr '[:upper:]' '[:lower:]' | sed "s;fuzz_;fuzz_${module_name}_;g") | ||||
| 			target_dir=$(dirname "${file}") | ||||
| 
 | ||||
| 			echo "Building ${file}.${target_name} into ${fuzzer_name}" | ||||
| 			compile_native_go_fuzzer "${target_dir}" "${target_name}" "${fuzzer_name}" | ||||
| 		done | ||||
| 	done | ||||
| done | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue