From 8c159ef70e79839c7a43d5d8f6bd37f25ad76082 Mon Sep 17 00:00:00 2001 From: Alex Stan Date: Mon, 18 Jul 2022 17:43:16 +0300 Subject: [PATCH] Add fuzz tests for storage_fs This commit uses native go fuzzing to fuzz test implementations of storage in storage_fs. moved fuzzing testdata for storage_fs in separate repo added make target and script for importing fuzz data and running all fuzz tests Signed-off-by: Alex Stan --- Makefile | 11 + README_fuzz.md | 13 + pkg/storage/local.go | 32 +- pkg/storage/local_test.go | 791 +++++++++++++++++++++++++++++++++++- pkg/storage/storage_test.go | 4 + test/scripts/fuzzAll.sh | 18 + 6 files changed, 857 insertions(+), 12 deletions(-) create mode 100644 README_fuzz.md create mode 100644 test/scripts/fuzzAll.sh diff --git a/Makefile b/Makefile index 01dc9ca2e3..faca7eb898 100644 --- a/Makefile +++ b/Makefile @@ -283,3 +283,14 @@ bats-metrics: binary check-skopeo $(BATS) bats-metrics-verbose: EXTENSIONS=metrics bats-metrics-verbose: binary check-skopeo $(BATS) $(BATS) --trace -p --verbose-run --print-output-on-failure --show-output-of-passing-tests test/blackbox/metrics.bats + +.PHONY: fuzz-all +fuzz-all: fuzztime=${1} +fuzz-all: + rm -rf test-data; \ + rm -rf pkg/storage/testdata; \ + git clone https://github.com/project-zot/test-data.git; \ + mv test-data/storage pkg/storage/testdata; \ + rm -rf test-data; \ + bash test/scripts/fuzzAll.sh ${fuzztime}; \ + rm -rf pkg/storage/testdata; \ diff --git a/README_fuzz.md b/README_fuzz.md new file mode 100644 index 0000000000..5fb09e1e20 --- /dev/null +++ b/README_fuzz.md @@ -0,0 +1,13 @@ +# Fuzzing in Zot + +This project makes use of native Go 1.18 fuzzing. An in-depth tutorial for fuzzing in Go can be found [here](https://go.dev/doc/fuzz/). +As language specifies, fuzz tests are included among unit-tests, inside the the same `*_test.go files`, in the packages they intend to fuzz. See [fuzzing for local storage](pkg/storage/local_test.go) + +Zot doesn't store the test data for fuzzing in the same repo, nor it is added before fuzzing with `(*testing.F).Add` . Instead, it is stored in a separate repo called [test-data](https://github.com/project-zot/test-data). + +To start fuzzing locally, one can use the Make target [fuzz-all](Makefile) . +**The default runtime for each fuzz test is 10s**, which can be overriden with the **fuzztime** variable +``` +make fuzz-all fuzztime=20 +``` +By running this target, testdata for fuzzing gets downloaded from the previously mentioned repo and the fuzzing begins for every fuzz function that is found. \ No newline at end of file diff --git a/pkg/storage/local.go b/pkg/storage/local.go index 7a768494f8..59d3e6dcbb 100644 --- a/pkg/storage/local.go +++ b/pkg/storage/local.go @@ -7,13 +7,16 @@ import ( "errors" "fmt" "io" + "io/fs" "io/ioutil" "os" "path" "path/filepath" "strings" "sync" + "syscall" "time" + "unicode/utf8" apexlog "github.com/apex/log" guuid "github.com/gofrs/uuid" @@ -186,6 +189,13 @@ func (is *ImageStoreLocal) Unlock(lockStart *time.Time) { func (is *ImageStoreLocal) initRepo(name string) error { repoDir := path.Join(is.rootDir, name) + + if !utf8.ValidString(name) { + is.log.Error().Msg("input is not valid UTF-8") + + return zerr.ErrInvalidRepositoryName + } + // create "blobs" subdir err := ensureDir(path.Join(repoDir, "blobs"), is.log) if err != nil { @@ -850,6 +860,7 @@ func (is *ImageStoreLocal) NewBlobUpload(repo string) (string, error) { if err != nil { return "", zerr.ErrRepoNotFound } + defer file.Close() return uid, nil @@ -859,6 +870,12 @@ func (is *ImageStoreLocal) NewBlobUpload(repo string) (string, error) { func (is *ImageStoreLocal) GetBlobUpload(repo, uuid string) (int64, error) { blobUploadPath := is.BlobUploadPath(repo, uuid) + if !utf8.ValidString(blobUploadPath) { + is.log.Error().Msg("input is not valid UTF-8") + + return -1, zerr.ErrInvalidRepositoryName + } + binfo, err := os.Stat(blobUploadPath) if err != nil { if os.IsNotExist(err) { @@ -1673,12 +1690,23 @@ func ifOlderThan(imgStore *ImageStoreLocal, repo string, delay time.Duration) ca } func DirExists(d string) bool { - fi, err := os.Stat(d) + if !utf8.ValidString(d) { + return false + } + + fileInfo, err := os.Stat(d) + if err != nil { + if e, ok := err.(*fs.PathError); ok && errors.Is(e.Err, syscall.ENAMETOOLONG) || //nolint: errorlint + errors.Is(e.Err, syscall.EINVAL) { + return false + } + } + if err != nil && os.IsNotExist(err) { return false } - if !fi.IsDir() { + if !fileInfo.IsDir() { return false } diff --git a/pkg/storage/local_test.go b/pkg/storage/local_test.go index 23acfcf409..bafde6a609 100644 --- a/pkg/storage/local_test.go +++ b/pkg/storage/local_test.go @@ -5,22 +5,26 @@ import ( "crypto/rand" _ "crypto/sha256" "encoding/json" + "errors" "fmt" "io" + "io/fs" "io/ioutil" "math/big" "os" "path" "strings" + "syscall" "testing" "time" godigest "github.com/opencontainers/go-digest" + imeta "github.com/opencontainers/image-spec/specs-go" ispec "github.com/opencontainers/image-spec/specs-go/v1" artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" "github.com/rs/zerolog" . "github.com/smartystreets/goconvey/convey" - "zotregistry.io/zot/errors" + zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/extensions/monitoring" "zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/storage" @@ -40,10 +44,8 @@ func TestStorageFSAPIs(t *testing.T) { true, log, metrics, nil) Convey("Repo layout", t, func(c C) { - repoName := "test" - Convey("Bad image manifest", func() { - upload, err := imgStore.NewBlobUpload("test") + upload, err := imgStore.NewBlobUpload(repoName) So(err, ShouldBeNil) So(upload, ShouldNotBeEmpty) @@ -56,17 +58,17 @@ func TestStorageFSAPIs(t *testing.T) { So(err, ShouldBeNil) So(blob, ShouldEqual, buflen) - err = imgStore.FinishBlobUpload("test", upload, buf, digest.String()) + err = imgStore.FinishBlobUpload(repoName, upload, buf, digest.String()) So(err, ShouldBeNil) annotationsMap := make(map[string]string) annotationsMap[ispec.AnnotationRefName] = tag cblob, cdigest := test.GetRandomImageConfig() - _, clen, err := imgStore.FullBlobUpload("test", bytes.NewReader(cblob), cdigest.String()) + _, clen, err := imgStore.FullBlobUpload(repoName, bytes.NewReader(cblob), cdigest.String()) So(err, ShouldBeNil) So(clen, ShouldEqual, len(cblob)) - hasBlob, _, err := imgStore.CheckBlob("test", cdigest.String()) + hasBlob, _, err := imgStore.CheckBlob(repoName, cdigest.String()) So(err, ShouldBeNil) So(hasBlob, ShouldEqual, true) @@ -195,7 +197,6 @@ func TestGetReferrers(t *testing.T) { Size: int64(buflen), } artifactManifest.Blobs = []artifactspec.Descriptor{} - manBuf, err := json.Marshal(artifactManifest) manBufLen := len(manBuf) So(err, ShouldBeNil) @@ -214,6 +215,711 @@ func TestGetReferrers(t *testing.T) { }) } +func FuzzNewBlobUpload(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + dir := t.TempDir() + defer os.RemoveAll(dir) + t.Logf("Input argument is %s", data) + log := log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, log) + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, log, metrics, nil) + + _, err := imgStore.NewBlobUpload(data) + if err != nil { + if isKnownErr(err) { + return + } + + t.Error(err) + } + }) +} + +func FuzzPutBlobChunk(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + dir := t.TempDir() + defer os.RemoveAll(dir) + t.Logf("Input argument is %s", data) + log := log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, log) + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, log, metrics, nil) + + repoName := data + uuid, err := imgStore.NewBlobUpload(repoName) + if err != nil { + if isKnownErr(err) { + return + } + + t.Error(err) + } + + buf := bytes.NewBuffer([]byte(data)) + buflen := buf.Len() + _, err = imgStore.PutBlobChunk(repoName, uuid, 0, int64(buflen), buf) + if err != nil { + t.Error(err) + } + }) +} + +func FuzzPutBlobChunkStreamed(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + dir := t.TempDir() + defer os.RemoveAll(dir) + t.Logf("Input argument is %s", data) + log := log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, log) + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, log, metrics, nil) + + repoName := data + + uuid, err := imgStore.NewBlobUpload(repoName) + if err != nil { + if isKnownErr(err) { + return + } + + t.Error(err) + } + + buf := bytes.NewBuffer([]byte(data)) + _, err = imgStore.PutBlobChunkStreamed(repoName, uuid, buf) + if err != nil { + t.Error(err) + } + }) +} + +func FuzzGetBlobUpload(f *testing.F) { + f.Fuzz(func(t *testing.T, data1 string, data2 string) { + dir := t.TempDir() + defer os.RemoveAll(dir) + log := log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, log) + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, log, metrics, nil) + + _, err := imgStore.GetBlobUpload(data1, data2) + if err != nil { + if errors.Is(err, zerr.ErrUploadNotFound) || isKnownErr(err) { + return + } + t.Error(err) + } + }) +} + +func FuzzTestPutGetImageManifest(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + + cblob, cdigest := test.GetRandomImageConfig() + + ldigest, lblob, err := newRandomBlobForFuzz(data) + if err != nil { + t.Errorf("error occurred while generating random blob, %v", err) + } + + _, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(cblob), cdigest.String()) + if err != nil { + t.Error(err) + } + _, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(lblob), ldigest.String()) + if err != nil { + t.Error(err) + } + + manifest, err := NewRandomImgManifest(data, cdigest, ldigest, cblob, lblob) + if err != nil { + t.Error(err) + } + manifestBuf, err := json.Marshal(manifest) + if err != nil { + t.Errorf("Error %v occurred while marshaling manifest", err) + } + mdigest := godigest.FromBytes(manifestBuf) + _, err = imgStore.PutImageManifest(repoName, mdigest.String(), ispec.MediaTypeImageManifest, manifestBuf) + if err != nil && errors.Is(err, zerr.ErrBadManifest) { + t.Errorf("the error that occurred is %v \n", err) + } + _, _, _, err = imgStore.GetImageManifest(repoName, mdigest.String()) + if err != nil { + t.Errorf("the error that occurred is %v \n", err) + } + }) +} + +func FuzzTestPutDeleteImageManifest(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + + cblob, cdigest := test.GetRandomImageConfig() + + ldigest, lblob, err := newRandomBlobForFuzz(data) + if err != nil { + t.Errorf("error occurred while generating random blob, %v", err) + } + + _, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(cblob), cdigest.String()) + if err != nil { + t.Error(err) + } + + _, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(lblob), ldigest.String()) + if err != nil { + t.Error(err) + } + + manifest, err := NewRandomImgManifest(data, cdigest, ldigest, cblob, lblob) + if err != nil { + t.Error(err) + } + + manifestBuf, err := json.Marshal(manifest) + if err != nil { + t.Errorf("Error %v occurred while marshaling manifest", err) + } + mdigest := godigest.FromBytes(manifestBuf) + _, err = imgStore.PutImageManifest(repoName, mdigest.String(), ispec.MediaTypeImageManifest, manifestBuf) + if err != nil && errors.Is(err, zerr.ErrBadManifest) { + t.Errorf("the error that occurred is %v \n", err) + } + + err = imgStore.DeleteImageManifest(repoName, mdigest.String()) + if err != nil { + if isKnownErr(err) { + return + } + t.Errorf("the error that occurred is %v \n", err) + } + }) +} + +// no integration with PutImageManifest, just throw fuzz data. +func FuzzTestDeleteImageManifest(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + + digest, _, err := newRandomBlobForFuzz(data) + if err != nil { + return + } + err = imgStore.DeleteImageManifest(string(data), digest.String()) + if err != nil { + if errors.Is(err, zerr.ErrRepoNotFound) || isKnownErr(err) { + return + } + t.Error(err) + } + }) +} + +func FuzzDirExists(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { //nolint: unusedparams + _ = storage.DirExists(data) + }) +} + +func FuzzInitRepo(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + err := imgStore.InitRepo(data) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + }) +} + +func FuzzInitValidateRepo(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + err := imgStore.InitRepo(data) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + _, err = imgStore.ValidateRepo(data) + if err != nil { + if errors.Is(err, zerr.ErrRepoNotFound) || errors.Is(err, zerr.ErrRepoBadVersion) || isKnownErr(err) { + return + } + t.Error(err) + } + }) +} + +func FuzzGetImageTags(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + _, err := imgStore.GetImageTags(data) + if err != nil { + if errors.Is(err, zerr.ErrRepoNotFound) || isKnownErr(err) { + return + } + t.Error(err) + } + }) +} + +func FuzzBlobUploadPath(f *testing.F) { + f.Fuzz(func(t *testing.T, repo, uuid string) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + + _ = imgStore.BlobUploadPath(repo, uuid) + }) +} + +func FuzzBlobUploadInfo(f *testing.F) { + f.Fuzz(func(t *testing.T, data string, uuid string) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + repo := data + + _, err := imgStore.BlobUploadInfo(repo, uuid) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + }) +} + +func FuzzTestGetImageManifest(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + dir := t.TempDir() + defer os.RemoveAll(dir) + + log := log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, log) + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, log, metrics, nil) + + repoName := data + + digest := godigest.FromBytes([]byte(data)) + + _, _, _, err := imgStore.GetImageManifest(repoName, digest.String()) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + }) +} + +func FuzzFinishBlobUpload(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + dir := t.TempDir() + defer os.RemoveAll(dir) + + log := log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, log) + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, log, metrics, nil) + + repoName := data + + upload, err := imgStore.NewBlobUpload(repoName) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + + content := []byte(data) + buf := bytes.NewBuffer(content) + buflen := buf.Len() + digest := godigest.FromBytes(content) + + _, err = imgStore.PutBlobChunk(repoName, upload, 0, int64(buflen), buf) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + + err = imgStore.FinishBlobUpload(repoName, upload, buf, digest.String()) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + }) +} + +func FuzzFullBlobUpload(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + repoName := "test" + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + + ldigest, lblob, err := newRandomBlobForFuzz(data) + if err != nil { + t.Errorf("error occurred while generating random blob, %v", err) + } + + _, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(lblob), ldigest.String()) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + }) +} + +func FuzzDedupeBlob(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + + blobDigest := godigest.FromString(data) + + // replacement for .uploads folder, usually retrieved from BlobUploadPath + src := path.Join(imgStore.RootDir(), "src") + blob := bytes.NewReader([]byte(data)) + + _, _, err := imgStore.FullBlobUpload("repoName", blob, blobDigest.String()) + if err != nil { + t.Error(err) + } + + dst := imgStore.BlobPath("repoName", blobDigest) + + err = os.MkdirAll(src, 0o755) + if err != nil { + t.Error(err) + } + + err = imgStore.DedupeBlob(src, blobDigest, dst) + if err != nil { + t.Error(err) + } + }) +} + +func FuzzDeleteBlobUpload(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + repoName := data + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + + uuid, err := imgStore.NewBlobUpload(repoName) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + + err = imgStore.DeleteBlobUpload(repoName, uuid) + if err != nil { + t.Error(err) + } + }) +} + +func FuzzBlobPath(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + repoName := data + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + digest := godigest.FromString(data) + + _ = imgStore.BlobPath(repoName, digest) + }) +} + +func FuzzCheckBlob(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + repoName := data + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + digest := godigest.FromString(data) + + _, _, err := imgStore.FullBlobUpload(repoName, bytes.NewReader([]byte(data)), digest.String()) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + _, _, err = imgStore.CheckBlob(repoName, digest.String()) + if err != nil { + t.Error(err) + } + }) +} + +func FuzzGetBlob(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + repoName := data + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + digest := godigest.FromString(data) + + _, _, err := imgStore.FullBlobUpload(repoName, bytes.NewReader([]byte(data)), digest.String()) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + + _, _, err = imgStore.GetBlob(repoName, digest.String(), "application/vnd.oci.image.layer.v1.tar+gzip") + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + }) +} + +func FuzzDeleteBlob(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + repoName := data + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + digest := godigest.FromString(data) + + _, _, err := imgStore.FullBlobUpload(repoName, bytes.NewReader([]byte(data)), digest.String()) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + + err = imgStore.DeleteBlob(repoName, digest.String()) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + }) +} + +func FuzzGetIndexContent(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + repoName := data + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + digest := godigest.FromString(data) + + _, _, err := imgStore.FullBlobUpload(repoName, bytes.NewReader([]byte(data)), digest.String()) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + + _, err = imgStore.GetIndexContent(repoName) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + }) +} + +func FuzzGetBlobContent(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + repoName := data + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + digest := godigest.FromString(data) + + _, _, err := imgStore.FullBlobUpload(repoName, bytes.NewReader([]byte(data)), digest.String()) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + + _, err = imgStore.GetBlobContent(repoName, digest.String()) + if err != nil { + if isKnownErr(err) { + return + } + t.Error(err) + } + }) +} + +func FuzzGetReferrers(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + + err := test.CopyFiles("../../test/data/zot-test", path.Join(dir, "zot-test")) + if err != nil { + t.Error(err) + } + digest := godigest.FromBytes([]byte(data)) + buf := bytes.NewBuffer([]byte(data)) + buflen := buf.Len() + err = ioutil.WriteFile(path.Join(imgStore.RootDir(), //nolint: gosec + "zot-test", "blobs", digest.Algorithm().String(), digest.Encoded()), + buf.Bytes(), 0o644) + if err != nil { + t.Error(err) + } + _, _, err = imgStore.FullBlobUpload("zot-test", buf, digest.String()) + if err != nil { + t.Error(err) + } + + artifactManifest := artifactspec.Manifest{} + artifactManifest.ArtifactType = data + artifactManifest.Subject = artifactspec.Descriptor{ + MediaType: ispec.MediaTypeImageManifest, + Digest: digest, + Size: int64(buflen), + } + artifactManifest.Blobs = []artifactspec.Descriptor{} + + manBuf, err := json.Marshal(artifactManifest) + if err != nil { + t.Error(err) + } + manDigest := godigest.FromBytes(manBuf) + _, err = imgStore.PutImageManifest("zot-test", manDigest.Encoded(), artifactspec.MediaTypeArtifactManifest, manBuf) + if err != nil { + t.Error(err) + } + _, err = imgStore.GetReferrers("zot-test", digest.String(), data) + if err != nil { + if errors.Is(err, zerr.ErrManifestNotFound) || isKnownErr(err) { + return + } + t.Error(err) + } + }) +} + +func FuzzRunGCRepo(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + log := &log.Logger{Logger: zerolog.New(os.Stdout)} + metrics := monitoring.NewMetricsServer(false, *log) + dir := t.TempDir() + defer os.RemoveAll(dir) + + imgStore := storage.NewImageStore(dir, true, storage.DefaultGCDelay, true, true, *log, metrics, nil) + + imgStore.RunGCRepo(data) + }) +} + func TestDedupeLinks(t *testing.T) { dir := t.TempDir() @@ -416,6 +1122,10 @@ func TestNegativeCases(t *testing.T) { err = imgStore.InitRepo("test-dir") So(err, ShouldBeNil) + + // Init repo should fail if repo is invalid UTF-8 + err = imgStore.InitRepo("hi \255") + So(err, ShouldNotBeNil) }) Convey("Invalid validate repo", t, func(c C) { @@ -438,7 +1148,7 @@ func TestNegativeCases(t *testing.T) { } _, err = imgStore.ValidateRepo("invalid-test") So(err, ShouldNotBeNil) - So(err, ShouldEqual, errors.ErrRepoNotFound) + So(err, ShouldEqual, zerr.ErrRepoNotFound) err = os.Chmod(path.Join(dir, "invalid-test"), 0o755) // remove all perms if err != nil { @@ -483,7 +1193,7 @@ func TestNegativeCases(t *testing.T) { isValid, err = imgStore.ValidateRepo("invalid-test") So(err, ShouldNotBeNil) - So(err, ShouldEqual, errors.ErrRepoBadVersion) + So(err, ShouldEqual, zerr.ErrRepoBadVersion) So(isValid, ShouldEqual, false) files, err := ioutil.ReadDir(path.Join(dir, "test")) @@ -676,6 +1386,14 @@ func TestNegativeCases(t *testing.T) { ok := storage.DirExists(filePath) So(ok, ShouldBeFalse) }) + + Convey("DirExists call with invalid UTF-8 as argument", t, func(c C) { + dir := t.TempDir() + + filePath := path.Join(dir, "hi \255") + ok := storage.DirExists(filePath) + So(ok, ShouldBeFalse) + }) } func TestHardLink(t *testing.T) { @@ -1311,3 +2029,56 @@ func TestPutBlobChunkStreamed(t *testing.T) { So(err, ShouldNotBeNil) }) } + +func NewRandomImgManifest(data []byte, cdigest, ldigest godigest.Digest, cblob, lblob []byte) (*ispec.Manifest, error) { + annotationsMap := make(map[string]string) + + key := string(data) + val := string(data) + annotationsMap[key] = val + + schemaVersion := 2 + + manifest := ispec.Manifest{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: cdigest, + Size: int64(len(cblob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: ldigest, + Size: int64(len(lblob)), + }, + }, + Annotations: annotationsMap, + Versioned: imeta.Versioned{ + SchemaVersion: schemaVersion, + }, + } + + return &manifest, nil +} + +func newRandomBlobForFuzz(data []byte) (godigest.Digest, []byte, error) { + return godigest.FromBytes(data), data, nil +} + +func isKnownErr(err error) bool { + if errors.Is(err, zerr.ErrInvalidRepositoryName) || errors.Is(err, zerr.ErrManifestNotFound) || + errors.Is(err, zerr.ErrRepoNotFound) || + errors.Is(err, zerr.ErrBadManifest) { + return true + } + + if err, ok := err.(*fs.PathError); ok && errors.Is(err.Err, syscall.EACCES) || //nolint: errorlint + errors.Is(err.Err, syscall.ENAMETOOLONG) || + errors.Is(err.Err, syscall.EINVAL) || + errors.Is(err.Err, syscall.ENOENT) { + return true + } + + return false +} diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go index f0d8be4ea3..61bdc5b59c 100644 --- a/pkg/storage/storage_test.go +++ b/pkg/storage/storage_test.go @@ -191,6 +191,10 @@ func TestStorageAPIs(t *testing.T) { So(err, ShouldNotBeNil) So(bupload, ShouldEqual, -1) + bupload, err = imgStore.GetBlobUpload("hi", " \255") + So(err, ShouldNotBeNil) + So(bupload, ShouldEqual, -1) + bupload, err = imgStore.GetBlobUpload("test", upload) So(err, ShouldBeNil) So(bupload, ShouldBeGreaterThanOrEqualTo, 0) diff --git a/test/scripts/fuzzAll.sh b/test/scripts/fuzzAll.sh new file mode 100644 index 0000000000..a92e8706c4 --- /dev/null +++ b/test/scripts/fuzzAll.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e + +fuzzTime=${1:-10} + +files=$(grep -r --include='**_test.go' --files-with-matches 'func Fuzz' .) + +for file in ${files} +do + funcs=$(grep -oP 'func \K(Fuzz\w*)' $file) + for func in ${funcs} + do + echo "Fuzzing $func in $file" + parentDir=$(dirname $file) + go test $parentDir -run=$func -fuzz=$func$ -fuzztime=${fuzzTime}s -tags sync,metrics,search,scrub,ui_base,containers_image_openpgp | grep -oP -x '^(?:(?!\blevel\b).)*$' + done +done \ No newline at end of file