Merge pull request #572 from pjbgf/new-fuzz

This commit is contained in:
Hidde Beydals 2022-02-09 11:39:55 +01:00 committed by GitHub
commit 657f80bf6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 680 additions and 3 deletions

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

@ -178,7 +178,7 @@ install-envtest: setup-envtest ## Download envtest binaries locally.
mkdir -p ${ENVTEST_ASSETS_DIR}
$(ENVTEST) use $(ENVTEST_KUBERNETES_VERSION) --arch=$(ENVTEST_ARCH) --bin-dir=$(ENVTEST_ASSETS_DIR)
# setup-envtest sets anything below k8s to 0555
chmod -R u+w $(BUILD_DIR)
chmod -R u+w $(BUILD_DIR)/testbin
libgit2: $(LIBGIT2) ## Detect or download libgit2 library
@ -221,3 +221,24 @@ go install $(2) ;\
rm -rf $$TMP_DIR ;\
}
endef
# Build fuzzers
fuzz-build: $(LIBGIT2)
rm -rf $(shell pwd)/build/fuzz/
mkdir -p $(shell pwd)/build/fuzz/out/
# TODO: remove mapping of current libgit2 dir and pull binaries from release or build dependency chain on demand.
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 \
-v "$(shell pwd)/build/libgit2":"/root/go/src/github.com/fluxcd/source-controller/build/libgit2" \
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

@ -56,7 +56,7 @@ var exampleCA []byte
var ctx context.Context
var cancel context.CancelFunc
const timeout = time.Second * 30
const timeout = time.Second * 60
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
@ -161,7 +161,7 @@ var _ = BeforeSuite(func() {
Expect(k8sClient).ToNot(BeNil())
Eventually(done, timeout).Should(BeClosed())
}, 60)
}, timeout.Seconds())
var _ = AfterSuite(func() {
cancel()

View File

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

View File

@ -0,0 +1,529 @@
//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"
"crypto/tls"
"crypto/x509"
"embed"
"errors"
"fmt"
"io/fs"
"net/http"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"sync"
"time"
fuzz "github.com/AdaLogics/go-fuzz-headers"
"github.com/fluxcd/pkg/gittestserver"
"github.com/fluxcd/pkg/runtime/testenv"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/fluxcd/source-controller/controllers"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs"
"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"
gitclient "github.com/go-git/go-git/v5/plumbing/transport/client"
httptransport "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/storage/memory"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"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"
)
var (
noOfCreatedFiles = 0
interval = time.Millisecond * 10
indexInterval = time.Millisecond * 10
pullInterval = time.Second * 3
initter sync.Once
gitServer *gittestserver.GitServer
k8sClient client.Client
cfg *rest.Config
testEnv *testenv.Environment
storage *controllers.Storage
examplePublicKey []byte
examplePrivateKey []byte
exampleCA []byte
)
//go:embed testdata/crd/*.yaml
//go:embed testdata/certs/*
var testFiles embed.FS
const (
defaultBinVersion = "1.23"
lettersAndNumbers = "abcdefghijklmnopqrstuvwxyz123456789"
lettersNumbersAndDash = "abcdefghijklmnopqrstuvwxyz123456789-"
)
func envtestBinVersion() string {
if binVersion := os.Getenv("ENVTEST_BIN_VERSION"); binVersion != "" {
return binVersion
}
return defaultBinVersion
}
func ensureDependencies() 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/crd", "testdata/certs"}
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)
}
}
}
startEnvServer(func(m manager.Manager) {
utilruntime.Must((&controllers.GitRepositoryReconciler{
Client: m.GetClient(),
Scheme: scheme.Scheme,
Storage: storage,
}).SetupWithManager(m))
})
return nil
}
func startEnvServer(setupReconcilers func(manager.Manager)) *envtest.Environment {
testEnv := &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("testdata", "crd")},
}
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(loadExampleKeys())
utilruntime.Must(sourcev1.AddToScheme(scheme.Scheme))
tmpStoragePath, err := os.MkdirTemp("", "source-controller-storage-")
if err != nil {
panic(err)
}
defer os.RemoveAll(tmpStoragePath)
storage, err = controllers.NewStorage(tmpStoragePath, "localhost:5050", time.Second*30)
if err != nil {
panic(err)
}
// serve artifacts from the filesystem, as done in main.go
fs := http.FileServer(http.Dir(tmpStoragePath))
http.Handle("/", fs)
go http.ListenAndServe(":5050", nil)
cert, err := tls.X509KeyPair(examplePublicKey, examplePrivateKey)
if err != nil {
panic(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(exampleCA)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
}
tlsConfig.BuildNameToCertificate()
var transport = httptransport.NewClient(&http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
})
gitclient.InstallProtocol("https", transport)
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 testEnv
}
// FuzzRandomGitFiles implements a fuzzer that
// targets the GitRepository reconciler.
func FuzzRandomGitFiles(data []byte) int {
initter.Do(func() {
utilruntime.Must(ensureDependencies())
})
f := fuzz.NewConsumer(data)
namespace, deleteNamespace, err := createNamespace(f)
if err != nil {
return 0
}
defer deleteNamespace()
gitServerURL, stopGitServer := createGitServer(f)
defer stopGitServer()
fs := memfs.New()
gitrepo, err := git.Init(memory.NewStorage(), fs)
if err != nil {
panic(err)
}
wt, err := gitrepo.Worktree()
if err != nil {
panic(err)
}
// Create random files for the git source
err = createRandomFiles(f, fs, wt)
if err != nil {
return 0
}
commit, err := pushFilesToGit(gitrepo, wt, gitServerURL.String())
if err != nil {
return 0
}
created, err := createGitRepository(f, gitServerURL.String(), commit.String(), namespace.Name)
if err != nil {
return 0
}
err = k8sClient.Create(context.Background(), created)
if err != nil {
return 0
}
defer k8sClient.Delete(context.Background(), created)
// Let the reconciler do its thing:
time.Sleep(60 * time.Millisecond)
return 1
}
// FuzzGitResourceObject implements a fuzzer that targets
// the GitRepository reconciler.
func FuzzGitResourceObject(data []byte) int {
initter.Do(func() {
utilruntime.Must(ensureDependencies())
})
f := fuzz.NewConsumer(data)
// Create this early because if it fails, then the fuzzer
// does not need to proceed.
repository := &sourcev1.GitRepository{}
err := f.GenerateStruct(repository)
if err != nil {
return 0
}
metaName, err := f.GetStringFrom(lettersNumbersAndDash, 59)
if err != nil {
return 0
}
gitServerURL, stopGitServer := createGitServer(f)
defer stopGitServer()
fs := memfs.New()
gitrepo, err := git.Init(memory.NewStorage(), fs)
if err != nil {
return 0
}
wt, err := gitrepo.Worktree()
if err != nil {
return 0
}
// Add a file
ff, _ := fs.Create("fixture")
_ = ff.Close()
_, err = wt.Add(fs.Join("fixture"))
if err != nil {
return 0
}
commit, err := pushFilesToGit(gitrepo, wt, gitServerURL.String())
if err != nil {
return 0
}
namespace, deleteNamespace, err := createNamespace(f)
if err != nil {
return 0
}
defer deleteNamespace()
repository.Spec.URL = gitServerURL.String()
repository.Spec.Verification.Mode = "head"
repository.Spec.SecretRef = nil
reference := &sourcev1.GitRepositoryRef{Branch: "some-branch"}
reference.Commit = strings.Replace(reference.Commit, "<commit>", commit.String(), 1)
repository.Spec.Reference = reference
repository.ObjectMeta = metav1.ObjectMeta{
Name: metaName,
Namespace: namespace.Name,
}
err = k8sClient.Create(context.Background(), repository)
if err != nil {
return 0
}
defer k8sClient.Delete(context.Background(), repository)
// Let the reconciler do its thing.
time.Sleep(50 * time.Millisecond)
return 1
}
func loadExampleKeys() (err error) {
examplePublicKey, err = os.ReadFile("testdata/certs/server.pem")
if err != nil {
return err
}
examplePrivateKey, err = os.ReadFile("testdata/certs/server-key.pem")
if err != nil {
return err
}
exampleCA, err = os.ReadFile("testdata/certs/ca.pem")
return err
}
// createGitRepository is a helper function to create GitRepository objects.
func createGitRepository(f *fuzz.ConsumeFuzzer, specUrl, commit, namespaceName string) (*sourcev1.GitRepository, error) {
reference := &sourcev1.GitRepositoryRef{Branch: "some-branch"}
reference.Commit = strings.Replace(reference.Commit, "<commit>", commit, 1)
nnID, err := f.GetStringFrom(lettersAndNumbers, 10)
if err != nil {
return &sourcev1.GitRepository{}, err
}
key := types.NamespacedName{
Name: fmt.Sprintf("git-ref-test-%s", nnID),
Namespace: namespaceName,
}
return &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: key.Name,
Namespace: key.Namespace,
},
Spec: sourcev1.GitRepositorySpec{
URL: specUrl,
Interval: metav1.Duration{Duration: indexInterval},
Reference: reference,
},
}, nil
}
// createNamespace is a helper function to create kubernetes namespaces.
func createNamespace(f *fuzz.ConsumeFuzzer) (*corev1.Namespace, func(), error) {
namespace := &corev1.Namespace{}
nnID, err := f.GetStringFrom(lettersAndNumbers, 10)
if err != nil {
return namespace, func() {}, err
}
namespace.ObjectMeta = metav1.ObjectMeta{Name: "git-repository-test" + nnID}
err = k8sClient.Create(context.Background(), namespace)
if err != nil {
return namespace, func() {}, err
}
return namespace, func() {
k8sClient.Delete(context.Background(), namespace)
}, nil
}
// createGitServer is a helper function to create a git server.
func createGitServer(f *fuzz.ConsumeFuzzer) (*url.URL, func()) {
repoID, err := f.GetStringFrom(lettersAndNumbers, 10)
if err != nil {
return &url.URL{}, func() {}
}
gitServer, err := gittestserver.NewTempGitServer()
if err != nil {
panic(err)
}
gitServer.AutoCreate()
defer os.RemoveAll(gitServer.Root())
utilruntime.Must(gitServer.StartHTTPS(examplePublicKey, examplePrivateKey, exampleCA, "example.com"))
u, err := url.Parse(gitServer.HTTPAddress())
if err != nil {
panic(err)
}
u.Path = path.Join(u.Path, fmt.Sprintf("repository-%s.git", repoID))
return u, func() { gitServer.StopHTTP() }
}
// pushFilesToGit is a helper function to push files to a git server.
func pushFilesToGit(gitrepo *git.Repository, wt *git.Worktree, gitServerURL string) (plumbing.Hash, error) {
commit, err := wt.Commit("Sample", &git.CommitOptions{Author: &object.Signature{
Name: "John Doe",
Email: "john@example.com",
When: time.Now(),
}})
if err != nil {
return plumbing.ZeroHash, err
}
hRef := plumbing.NewHashReference(plumbing.ReferenceName("refs/heads/some-branch"), commit)
err = gitrepo.Storer.SetReference(hRef)
if err != nil {
return plumbing.ZeroHash, err
}
remote, err := gitrepo.CreateRemote(&config.RemoteConfig{
Name: "origin",
URLs: []string{gitServerURL},
})
if err != nil {
return plumbing.ZeroHash, err
}
err = remote.Push(&git.PushOptions{
RefSpecs: []config.RefSpec{"refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"},
})
if err != nil {
return plumbing.ZeroHash, err
}
return commit, nil
}
// createRandomFiles is a helper function to create files in a billy.Filesystem.
func createRandomFiles(f *fuzz.ConsumeFuzzer, fs billy.Filesystem, wt *git.Worktree) error {
numberOfFiles, err := f.GetInt()
if err != nil {
return err
}
maxNumberOfFiles := 4000 // This number is completely arbitrary
if numberOfFiles%maxNumberOfFiles == 0 {
return errors.New("We don't want to create 0 files...")
}
for i := 0; i < numberOfFiles%maxNumberOfFiles; i++ {
dirPath, err := f.GetString()
if err != nil {
return err
}
// Check for ".." cases
if strings.Contains(dirPath, "..") {
return errors.New("Dir contains '..'")
}
err = fs.MkdirAll(dirPath, 0777)
if err != nil {
return errors.New("Could not create the subDir")
}
fileName, err := f.GetString()
if err != nil {
return errors.New("Could not get fileName")
}
fullFilePath := fs.Join(dirPath, fileName)
fileContents, err := f.GetBytes()
if err != nil {
return errors.New("Could not create the subDir")
}
createdFile, err := fs.Create(fullFilePath)
if err != nil {
return errors.New("Could not create the subDir")
}
_, err = createdFile.Write(fileContents)
if err != nil {
createdFile.Close()
return errors.New("Could not create the subDir")
}
createdFile.Close()
_, err = wt.Add(fullFilePath)
if err != nil {
panic(err)
}
noOfCreatedFiles++
}
return nil
}

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

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

View File

@ -0,0 +1,74 @@
#!/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-6}"
GOPATH="${GOPATH:-/root/go}"
GO_SRC="${GOPATH}/src"
PROJECT_PATH="github.com/fluxcd/source-controller"
cd "${GO_SRC}"
pushd "${PROJECT_PATH}"
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"
# Setup files to be embedded into controllers_fuzzer.go's testFiles variable.
mkdir -p testdata/crd
cp ../../config/crd/bases/*.yaml testdata/crd/
cp -r ../../controllers/testdata/certs testdata/
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=FuzzRandomGitFiles -o gitrepository_fuzzer.a .
clang -o /out/fuzz_random_git_files \
gitrepository_fuzzer.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=FuzzGitResourceObject -o fuzz_git_resource_object.a .
clang -o /out/fuzz_git_resource_object \
fuzz_git_resource_object.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
# By now testdata is embedded in the binaries and no longer needed.
rm -rf testdata/
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