Add support for fuzzing tests using oss-fuzz-build.

Co-authored-by: Paulo Gomes <paulo.gomes@weave.works>
Co-authored-by: AdamKorcz <adam@adalogics.com>
Signed-off-by: Sanskar Jaiswal <sanskar.jaiswal@weave.works>
This commit is contained in:
Sanskar Jaiswal 2022-02-10 14:13:27 +05:30
parent a348d9f394
commit fea92bd44c
9 changed files with 707 additions and 3 deletions

View File

@ -1 +1 @@
hack/libgit2/ build/libgit2/

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

@ -0,0 +1,24 @@
name: CIFuzz
on:
pull_request:
branches:
- main
permissions:
contents: read
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

View File

@ -3,7 +3,7 @@ ARG GO_VERSION=1.17
ARG XX_VERSION=1.1.0 ARG XX_VERSION=1.1.0
ARG LIBGIT2_IMG=ghcr.io/fluxcd/golang-with-libgit2 ARG LIBGIT2_IMG=ghcr.io/fluxcd/golang-with-libgit2
ARG LIBGIT2_TAG=libgit2-1.1.1-6 ARG LIBGIT2_TAG=libgit2-1.1.1-7
FROM ${LIBGIT2_IMG}:${LIBGIT2_TAG} AS libgit2-libs FROM ${LIBGIT2_IMG}:${LIBGIT2_TAG} AS libgit2-libs

View File

@ -8,7 +8,7 @@ CRD_OPTIONS ?= crd:crdVersions=v1
# Base image used to build the Go binary # Base image used to build the Go binary
LIBGIT2_IMG ?= ghcr.io/fluxcd/golang-with-libgit2 LIBGIT2_IMG ?= ghcr.io/fluxcd/golang-with-libgit2
LIBGIT2_TAG ?= libgit2-1.1.1-6 LIBGIT2_TAG ?= libgit2-1.1.1-7
# Allows for defining additional Docker buildx arguments, # Allows for defining additional Docker buildx arguments,
# e.g. '--push'. # e.g. '--push'.
@ -236,6 +236,25 @@ ENVTEST = $(GOBIN)/setup-envtest
setup-envtest: ## Download envtest-setup locally if necessary. setup-envtest: ## Download envtest-setup locally if necessary.
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest) $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest)
# Build fuzzers
fuzz-build: $(LIBGIT2)
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"
# go-install-tool will 'go install' any package $2 and install it to $1. # go-install-tool will 'go install' any package $2 and install it to $1.
define go-install-tool define go-install-tool
@[ -f $(1) ] || { \ @[ -f $(1) ] || { \

View File

@ -0,0 +1,8 @@
FROM gcr.io/oss-fuzz-base/base-builder-go
RUN apt-get update && apt-get install -y cmake pkg-config
COPY ./ $GOPATH/src/github.com/fluxcd/image-automation-controller/
COPY ./tests/fuzz/oss_fuzz_build.sh $SRC/build.sh
WORKDIR $SRC

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

@ -0,0 +1,3 @@
module github.com/fluxcd/image-automation-controller/tests/fuzz
go 1.17

View File

@ -0,0 +1,520 @@
//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"
"embed"
"fmt"
"io/fs"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"sync"
"time"
fuzz "github.com/AdaLogics/go-fuzz-headers"
"github.com/fluxcd/image-automation-controller/controllers"
"github.com/fluxcd/image-automation-controller/pkg/update"
"github.com/fluxcd/pkg/gittestserver"
"github.com/fluxcd/pkg/runtime/testenv"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/types"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"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"
"sigs.k8s.io/controller-runtime/pkg/manager"
image_automationv1 "github.com/fluxcd/image-automation-controller/api/v1beta1"
image_reflectv1 "github.com/fluxcd/image-reflector-controller/api/v1beta1"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var (
cfgFuzz *rest.Config
k8sClient client.Client
imageAutoReconcilerFuzz *controllers.ImageUpdateAutomationReconciler
testEnvFuzz *testenv.Environment
initter sync.Once
)
const defaultBinVersion = "1.23"
//go:embed testdata/crds
var testFiles embed.FS
func envtestBinVersion() string {
if binVersion := os.Getenv("ENVTEST_BIN_VERSION"); binVersion != "" {
return binVersion
}
return defaultBinVersion
}
func ensureDependencies(setupReconcilers func(manager.Manager)) error {
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
embedDirs := []string{"testdata/crds"}
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)
}
}
}
testEnv := &envtest.Environment{
CRDDirectoryPaths: []string{
filepath.Join("testdata", "crds"),
},
}
fmt.Println("Starting the test environment")
cfg, err := testEnv.Start()
if err != nil {
panic(fmt.Sprintf("Failed to start the test environment manager: %v", err))
}
utilruntime.Must(sourcev1.AddToScheme(scheme.Scheme))
utilruntime.Must(image_reflectv1.AddToScheme(scheme.Scheme))
utilruntime.Must(image_automationv1.AddToScheme(scheme.Scheme))
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
if err != nil {
panic(err)
}
if k8sClient == nil {
panic("cfg is nil but should not be")
}
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme.Scheme,
})
if err != nil {
panic(err)
}
setupReconcilers(k8sManager)
time.Sleep(2 * time.Second)
go func() {
fmt.Println("Starting k8sManager...")
utilruntime.Must(k8sManager.Start(context.TODO()))
}()
return nil
}
// This fuzzer randomized 2 things:
// 1: The files in the git repository
// 2: The values of ImageUpdateAutomationSpec
// and ImagePolicy resources
func FuzzImageUpdateReconciler(data []byte) int {
initter.Do(func() {
utilruntime.Must(ensureDependencies(func(m manager.Manager) {
utilruntime.Must((&controllers.ImageUpdateAutomationReconciler{
Client: m.GetClient(),
Scheme: scheme.Scheme,
}).SetupWithManager(m, controllers.ImageUpdateAutomationReconcilerOptions{MaxConcurrentReconciles: 4}))
}))
})
f := fuzz.NewConsumer(data)
// We start by creating a lot of the values that
// need for the various resources later on
runes := "abcdefghijklmnopqrstuvwxyz1234567890"
branch, err := f.GetStringFrom(runes, 80)
if err != nil {
return 0
}
repPath, err := f.GetStringFrom(runes, 80)
if err != nil {
return 0
}
repositoryPath := "/config-" + repPath + ".git"
namespaceName, err := f.GetStringFrom(runes, 59)
if err != nil {
return 0
}
gitRepoKeyName, err := f.GetStringFrom(runes, 80)
if err != nil {
return 0
}
username, err := f.GetStringFrom(runes, 80)
if err != nil {
return 0
}
password, err := f.GetStringFrom(runes, 80)
if err != nil {
return 0
}
ipSpec := image_reflectv1.ImagePolicySpec{}
err = f.GenerateStruct(&ipSpec)
if err != nil {
return 0
}
ipStatus := image_reflectv1.ImagePolicyStatus{}
err = f.GenerateStruct(&ipStatus)
if err != nil {
return 0
}
iuaSpec := image_automationv1.ImageUpdateAutomationSpec{}
err = f.GenerateStruct(&iuaSpec)
if err != nil {
return 0
}
gitSpec := &image_automationv1.GitSpec{}
err = f.GenerateStruct(&gitSpec)
if err != nil {
return 0
}
policyKeyName, err := f.GetStringFrom(runes, 80)
if err != nil {
return 0
}
updateKeyName, err := f.GetStringFrom("abcdefghijklmnopqrstuvwxy.-", 120)
if err != nil {
return 0
}
// Create random git files
gitPath, err := os.MkdirTemp("", "git-dir-")
if err != nil {
return 0
}
defer os.RemoveAll(gitPath)
err = f.CreateFiles(gitPath)
if err != nil {
return 0
}
// Done with creating the random values
// Create a namespace
namespace := &corev1.Namespace{}
namespace.Name = namespaceName
err = k8sClient.Create(context.Background(), namespace)
if err != nil {
return 0
}
defer func() {
err = k8sClient.Delete(context.Background(), namespace)
if err != nil {
panic(err)
}
time.Sleep(80 * time.Millisecond)
}()
// Set up git-related stuff
gitServer, err := gittestserver.NewTempGitServer()
if err != nil {
return 0
}
gitServer.Auth(username, password)
gitServer.AutoCreate()
err = gitServer.StartHTTP()
if err != nil {
return 0
}
defer func() {
gitServer.StopHTTP()
os.RemoveAll(gitServer.Root())
}()
gitServer.KeyDir(filepath.Join(gitServer.Root(), "keys"))
err = gitServer.ListenSSH()
if err != nil {
return 0
}
err = initGitRepo(gitServer, gitPath, branch, repositoryPath)
if err != nil {
return 0
}
repoURL := gitServer.HTTPAddressWithCredentials() + repositoryPath
// Done with setting up git related stuff
// Create git repository object
gitRepoKey := types.NamespacedName{
Name: "image-auto-" + gitRepoKeyName,
Namespace: namespace.Name,
}
gitRepo := &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: gitRepoKey.Name,
Namespace: namespace.Name,
},
Spec: sourcev1.GitRepositorySpec{
URL: repoURL,
Interval: metav1.Duration{Duration: time.Minute},
},
}
err = k8sClient.Create(context.Background(), gitRepo)
if err != nil {
return 0
}
defer k8sClient.Delete(context.Background(), gitRepo)
// Create image policy object
policyKey := types.NamespacedName{
Name: "policy-" + policyKeyName,
Namespace: namespace.Name,
}
policy := &image_reflectv1.ImagePolicy{
ObjectMeta: metav1.ObjectMeta{
Name: policyKey.Name,
Namespace: policyKey.Namespace,
},
Spec: ipSpec,
Status: ipStatus,
}
err = k8sClient.Create(context.Background(), policy)
if err != nil {
return 0
}
err = k8sClient.Status().Update(context.Background(), policy)
if err != nil {
return 0
}
// Create ImageUpdateAutomation object
updateKey := types.NamespacedName{
Namespace: namespace.Name,
Name: updateKeyName,
}
// Setting these fields manually to help the fuzzer
gitSpec.Checkout.Reference.Branch = branch
iuaSpec.GitSpec = gitSpec
iuaSpec.SourceRef.Kind = "GitRepository"
iuaSpec.SourceRef.Name = gitRepoKey.Name
iuaSpec.Update.Strategy = image_automationv1.UpdateStrategySetters
iua := &image_automationv1.ImageUpdateAutomation{
ObjectMeta: metav1.ObjectMeta{
Name: updateKey.Name,
Namespace: updateKey.Namespace,
},
Spec: iuaSpec,
}
err = k8sClient.Create(context.Background(), iua)
if err != nil {
return 0
}
defer k8sClient.Delete(context.Background(), iua)
time.Sleep(time.Millisecond * 70)
return 1
}
// A fuzzer that is more focused on UpdateWithSetters
// that the reconciler fuzzer is
func FuzzUpdateWithSetters(data []byte) int {
f := fuzz.NewConsumer(data)
// Create dir1
tmp1, err := ioutil.TempDir("", "fuzztest1")
if err != nil {
return 0
}
defer os.RemoveAll(tmp1)
// Add files to dir1
err = f.CreateFiles(tmp1)
if err != nil {
return 0
}
// Create dir2
tmp2, err := ioutil.TempDir("", "fuzztest2")
if err != nil {
return 0
}
defer os.RemoveAll(tmp2)
// Create policies
policies := make([]image_reflectv1.ImagePolicy, 0)
noOfPolicies, err := f.GetInt()
if err != nil {
return 0
}
for i := 0; i < noOfPolicies%10; i++ {
policy := image_reflectv1.ImagePolicy{}
err = f.GenerateStruct(&policy)
if err != nil {
return 0
}
policies = append(policies, policy)
}
// Call the target
_, _ = update.UpdateWithSetters(logr.Discard(), tmp1, tmp2, policies)
return 1
}
// Initialise a git server with a repo including the files in dir.
func initGitRepo(gitServer *gittestserver.GitServer, fixture, branch, repositoryPath string) error {
fs := memfs.New()
repo, err := git.Init(memory.NewStorage(), fs)
if err != nil {
return err
}
err = populateRepoFromFixture(repo, fixture)
if err != nil {
return err
}
working, err := repo.Worktree()
if err != nil {
return err
}
if err = working.Checkout(&git.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName(branch),
Create: true,
}); err != nil {
return err
}
remote, err := repo.CreateRemote(&config.RemoteConfig{
Name: "origin",
URLs: []string{gitServer.HTTPAddressWithCredentials() + repositoryPath},
})
if err != nil {
return err
}
return remote.Push(&git.PushOptions{
RefSpecs: []config.RefSpec{
config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch)),
},
})
}
func populateRepoFromFixture(repo *gogit.Repository, fixture string) error {
working, err := repo.Worktree()
if err != nil {
return err
}
fs := working.Filesystem
if err = filepath.Walk(fixture, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return fs.MkdirAll(fs.Join(path[len(fixture):]), info.Mode())
}
// copy symlinks as-is, so I can test what happens with broken symlinks
if info.Mode()&os.ModeSymlink > 0 {
target, err := os.Readlink(path)
if err != nil {
return err
}
return fs.Symlink(target, path[len(fixture):])
}
fileBytes, err := os.ReadFile(path)
if err != nil {
return err
}
ff, err := fs.Create(path[len(fixture):])
if err != nil {
return err
}
defer ff.Close()
_, err = ff.Write(fileBytes)
return err
}); err != nil {
return err
}
_, err = working.Add(".")
if err != nil {
return err
}
if _, err = working.Commit("Initial revision from "+fixture, &gogit.CommitOptions{
Author: &object.Signature{
Name: "Testbot",
Email: "test@example.com",
When: time.Now(),
},
}); err != nil {
return err
}
return nil
}

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

@ -0,0 +1,110 @@
#!/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
LIBGIT2_TAG="${LIBGIT2_TAG:-libgit2-1.1.1-7}"
GOPATH="${GOPATH:-/root/go}"
GO_SRC="${GOPATH}/src"
PROJECT_PATH="github.com/fluxcd/image-automation-controller"
cd "${GO_SRC}"
pushd "${PROJECT_PATH}"
export TARGET_DIR="$(/bin/pwd)/build/libgit2/${LIBGIT2_TAG}"
# For most cases, libgit2 will already be present.
# The exception being at the oss-fuzz integration.
if [ ! -d "${TARGET_DIR}" ]; then
curl -o output.tar.gz -LO "https://github.com/fluxcd/golang-with-libgit2/releases/download/${LIBGIT2_TAG}/linux-$(uname -m)-libs.tar.gz"
DIR=libgit2-linux
NEW_DIR="$(/bin/pwd)/build/libgit2/${LIBGIT2_TAG}"
INSTALLED_DIR="/home/runner/work/golang-with-libgit2/golang-with-libgit2/build/${DIR}"
mkdir -p ./build/libgit2
tar -xf output.tar.gz
rm output.tar.gz
mv "${DIR}" "${LIBGIT2_TAG}"
mv "${LIBGIT2_TAG}/" "./build/libgit2"
# Update the prefix paths included in the .pc files.
# This will make it easier to update to the location in which they will be used.
find "${NEW_DIR}" -type f -name "*.pc" | xargs -I {} sed -i "s;${INSTALLED_DIR};${NEW_DIR};g" {}
fi
apt-get update && apt-get install -y pkg-config
export TARGET_DIR="$(/bin/pwd)/build/libgit2/${LIBGIT2_TAG}"
export CGO_ENABLED=1
export LIBRARY_PATH="${TARGET_DIR}/lib:${TARGET_DIR}/lib64"
export PKG_CONFIG_PATH="${TARGET_DIR}/lib/pkgconfig:${TARGET_DIR}/lib64/pkgconfig"
export CGO_CFLAGS="-I${TARGET_DIR}/include -I${TARGET_DIR}/include/openssl"
export CGO_LDFLAGS="$(pkg-config --libs --static --cflags libssh2 openssl libgit2)"
go mod tidy -compat=1.17
popd
pushd "${PROJECT_PATH}/tests/fuzz"
# Version of the source-controller from which to get the GitRepository CRD.
# Change this if you bump the source-controller/api version in go.mod.
SOURCE_VER=v0.21.1
# Version of the image-reflector-controller from which to get the ImagePolicy CRD.
# Change this if you bump the image-reflector-controller/api version in go.mod.
REFLECTOR_VER=v0.16.0
# Setup files to be embedded into controllers_fuzzer.go's testFiles variable.
mkdir -p testdata/crds
cp ../../config/crd/bases/*.yaml testdata/crds/
if [ -d "../../controllers/testdata/crds" ]; then
cp ../../controllers/testdata/crds/*.yaml testdata/crds
# Fetch the CRDs if not present since we need them when running fuzz tests on CI.
else
curl -s --fail https://raw.githubusercontent.com/fluxcd/source-controller/${SOURCE_VER}/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml -o testdata/crds/gitrepositories.yaml
curl -s --fail https://raw.githubusercontent.com/fluxcd/image-reflector-controller/${REFLECTOR_VER}/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml -o testdata/crds/imagepolicies.yaml
fi
go mod tidy -compat=1.17
# ref: https://github.com/google/oss-fuzz/blob/master/infra/base-images/base-builder/compile_go_fuzzer
go-fuzz -tags gofuzz -func=FuzzImageUpdateReconciler -o fuzz_image_update_reconciler.a .
clang -o /out/fuzz_image_update_reconciler \
fuzz_image_update_reconciler.a \
"${TARGET_DIR}/lib/libgit2.a" \
"${TARGET_DIR}/lib/libssh2.a" \
"${TARGET_DIR}/lib/libz.a" \
"${TARGET_DIR}/lib64/libssl.a" \
"${TARGET_DIR}/lib64/libcrypto.a" \
-fsanitize=fuzzer
go-fuzz -tags gofuzz -func=FuzzUpdateWithSetters -o fuzz_update_with_setters.a .
clang -o /out/fuzz_update_with_setters \
fuzz_update_with_setters.a \
"${TARGET_DIR}/lib/libgit2.a" \
"${TARGET_DIR}/lib/libssh2.a" \
"${TARGET_DIR}/lib/libz.a" \
"${TARGET_DIR}/lib64/libssl.a" \
"${TARGET_DIR}/lib64/libcrypto.a" \
-fsanitize=fuzzer
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