Merge pull request #5785 from AdamKorcz/fuzz5
Fuzzing: Add container fuzzer
This commit is contained in:
		
							
								
								
									
										351
									
								
								contrib/fuzz/container_fuzzer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										351
									
								
								contrib/fuzz/container_fuzzer.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,351 @@ | ||||
| // +build gofuzz | ||||
|  | ||||
| /* | ||||
|    Copyright The containerd 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. | ||||
| */ | ||||
|  | ||||
| /* | ||||
| 	To run this fuzzer, it must first be moved to | ||||
| 	integration/client. OSS-fuzz does this automatically | ||||
| 	everytime it builds the fuzzers. | ||||
| */ | ||||
|  | ||||
| package client | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	fuzz "github.com/AdaLogics/go-fuzz-headers" | ||||
| 	"github.com/containerd/containerd" | ||||
| 	"github.com/containerd/containerd/oci" | ||||
| 	"github.com/containerd/containerd/sys" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	err := updatePathEnv() | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| func tearDown() error { | ||||
| 	if err := ctrd.Stop(); err != nil { | ||||
| 		if err := ctrd.Kill(); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	if err := ctrd.Wait(); err != nil { | ||||
| 		if _, ok := err.(*exec.ExitError); !ok { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	if err := sys.ForceRemoveAll(defaultRoot); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // checkIfShouldRestart() checks if an error indicates that | ||||
| // the daemon is not running. If the daemon is not running, | ||||
| // it deletes it to allow the fuzzer to create a new and | ||||
| // working socket. | ||||
| func checkIfShouldRestart(err error) { | ||||
| 	if strings.Contains(err.Error(), "daemon is not running") { | ||||
| 		err2 := deleteSocket() | ||||
| 		if err2 != nil { | ||||
| 			panic(err2) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // startDaemon() starts the daemon. | ||||
| func startDaemon(ctx context.Context, shouldTearDown bool) { | ||||
| 	buf := bytes.NewBuffer(nil) | ||||
| 	stdioFile, err := ioutil.TempFile("", "") | ||||
| 	if err != nil { | ||||
| 		// We panic here as it is a fuzz-blocker that | ||||
| 		// may need fixing | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	defer func() { | ||||
| 		stdioFile.Close() | ||||
| 		os.Remove(stdioFile.Name()) | ||||
| 	}() | ||||
| 	ctrdStdioFilePath = stdioFile.Name() | ||||
| 	stdioWriter := io.MultiWriter(stdioFile, buf) | ||||
| 	err = ctrd.start("containerd", address, []string{ | ||||
| 		"--root", defaultRoot, | ||||
| 		"--state", defaultState, | ||||
| 		"--log-level", "debug", | ||||
| 		"--config", createShimDebugConfig(), | ||||
| 	}, stdioWriter, stdioWriter) | ||||
| 	if err != nil { | ||||
| 		// We are fine if the error is that the daemon is already running, | ||||
| 		// but if the error is another, then it will be a fuzz blocker, | ||||
| 		// so we panic | ||||
| 		if !strings.Contains(err.Error(), "daemon is already running") { | ||||
| 			fmt.Fprintf(os.Stderr, "%s: %s\n", err, buf.String()) | ||||
| 			panic(err) | ||||
| 		} | ||||
| 	} | ||||
| 	if shouldTearDown { | ||||
| 		defer func() { | ||||
| 			err = tearDown() | ||||
| 			if err != nil { | ||||
| 				checkIfShouldRestart(err) | ||||
| 			} | ||||
| 		}() | ||||
| 	} | ||||
| 	seconds := 4 * time.Second | ||||
| 	waitCtx, waitCancel := context.WithTimeout(ctx, seconds) | ||||
|  | ||||
| 	_, err = ctrd.waitForStart(waitCtx) | ||||
| 	waitCancel() | ||||
| 	if err != nil { | ||||
| 		ctrd.Stop() | ||||
| 		ctrd.Kill() | ||||
| 		ctrd.Wait() | ||||
| 		fmt.Fprintf(os.Stderr, "%s: %s\n", err, buf.String()) | ||||
| 		return | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // deleteSocket() deletes the socket in the file system. | ||||
| // This is needed because the socket occasionally will | ||||
| // refuse a connection to it, and deleting it allows us | ||||
| // to create a new socket when invoking containerd.New() | ||||
| func deleteSocket() error { | ||||
| 	err := os.Remove("/run/containerd-test/containerd.sock") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // updatePathEnv() creates an empty directory in which | ||||
| // the fuzzer will create the containerd socket. | ||||
| // updatePathEnv() furthermore adds "/out/containerd-binaries" | ||||
| // to $PATH, since the binaries are available there. | ||||
| func updatePathEnv() error { | ||||
| 	// Create test dir for socket | ||||
| 	err := os.MkdirAll("/run/containerd-test", 0777) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	oldPathEnv := os.Getenv("PATH") | ||||
| 	newPathEnv := oldPathEnv + ":/out/containerd-binaries" | ||||
| 	err = os.Setenv("PATH", newPathEnv) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // checkAndDoUnpack checks if an image is unpacked. | ||||
| // If it is not unpacked, then we may or may not | ||||
| // unpack it. The fuzzer decides. | ||||
| func checkAndDoUnpack(image containerd.Image, ctx context.Context, f *fuzz.ConsumeFuzzer) { | ||||
| 	unpacked, err := image.IsUnpacked(ctx, testSnapshotter) | ||||
| 	if err == nil && unpacked { | ||||
| 		shouldUnpack, err := f.GetBool() | ||||
| 		if err == nil && shouldUnpack { | ||||
| 			_ = image.Unpack(ctx, testSnapshotter) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // getImage() returns an image from the client. | ||||
| // The fuzzer decides which image is returned. | ||||
| func getImage(client *containerd.Client, f *fuzz.ConsumeFuzzer) (containerd.Image, error) { | ||||
| 	images, err := client.ListImages(nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	imageIndex, err := f.GetInt() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	image := images[imageIndex%len(images)] | ||||
| 	return image, nil | ||||
|  | ||||
| } | ||||
|  | ||||
| // newContainer creates and returns a container | ||||
| // The fuzzer decides how the container is created | ||||
| func newContainer(client *containerd.Client, f *fuzz.ConsumeFuzzer, ctx context.Context) (containerd.Container, error) { | ||||
|  | ||||
| 	// determiner determines how we should create the container | ||||
| 	determiner, err := f.GetInt() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	id, err := f.GetString() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if determiner%1 == 0 { | ||||
| 		// Create a container with oci specs | ||||
| 		spec := &oci.Spec{} | ||||
| 		err = f.GenerateStruct(spec) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		container, err := client.NewContainer(ctx, id, | ||||
| 			containerd.WithSpec(spec)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		return container, nil | ||||
| 	} else if determiner%2 == 0 { | ||||
| 		// Create a container with fuzzed oci specs | ||||
| 		// and an image | ||||
| 		image, err := getImage(client, f) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		// Fuzz a few image APIs | ||||
| 		_, _ = image.Size(ctx) | ||||
| 		checkAndDoUnpack(image, ctx, f) | ||||
|  | ||||
| 		spec := &oci.Spec{} | ||||
| 		err = f.GenerateStruct(spec) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		container, err := client.NewContainer(ctx, | ||||
| 			id, | ||||
| 			containerd.WithImage(image), | ||||
| 			containerd.WithSpec(spec)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		return container, nil | ||||
| 	} else { | ||||
| 		// Create a container with an image | ||||
| 		image, err := getImage(client, f) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		// Fuzz a few image APIs | ||||
| 		_, _ = image.Size(ctx) | ||||
| 		checkAndDoUnpack(image, ctx, f) | ||||
|  | ||||
| 		container, err := client.NewContainer(ctx, | ||||
| 			id, | ||||
| 			containerd.WithImage(image)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		return container, nil | ||||
| 	} | ||||
| 	return nil, errors.New("Could not create container") | ||||
| } | ||||
|  | ||||
| // doFuzz() implements the logic of FuzzCreateContainerNoTearDown() | ||||
| // and FuzzCreateContainerWithTearDown() and allows for | ||||
| // the option to turn on/off teardown after each iteration. | ||||
| // From a high level it: | ||||
| // - Creates a client | ||||
| // - Imports a bunch of fuzzed tar archives | ||||
| // - Creates a bunch of containers | ||||
| func doFuzz(data []byte, shouldTearDown bool) int { | ||||
| 	ctx, cancel := testContext(nil) | ||||
| 	defer cancel() | ||||
|  | ||||
| 	// Check if daemon is running and start it if it isn't | ||||
| 	if ctrd.cmd == nil { | ||||
| 		startDaemon(ctx, shouldTearDown) | ||||
| 	} | ||||
| 	client, err := containerd.New(address) | ||||
| 	if err != nil { | ||||
| 		// The error here is most likely with the socket. | ||||
| 		// Deleting it will allow the creation of a new | ||||
| 		// socket during next fuzz iteration. | ||||
| 		deleteSocket() | ||||
| 		return -1 | ||||
| 	} | ||||
| 	defer client.Close() | ||||
| 	f := fuzz.NewConsumer(data) | ||||
|  | ||||
| 	// Begin import tars: | ||||
| 	noOfImports, err := f.GetInt() | ||||
| 	if err != nil { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	// maxImports is currently completely arbitrarily defined | ||||
| 	maxImports := 30 | ||||
| 	for i := 0; i < noOfImports%maxImports; i++ { | ||||
|  | ||||
| 		// f.TarBytes() returns valid tar bytes. | ||||
| 		tarBytes, err := f.TarBytes() | ||||
| 		if err != nil { | ||||
| 			return 0 | ||||
| 		} | ||||
| 		_, _ = client.Import(ctx, bytes.NewReader(tarBytes)) | ||||
| 	} | ||||
| 	// End import tars | ||||
|  | ||||
| 	// Begin create containers: | ||||
| 	existingImages, err := client.ListImages(ctx) | ||||
| 	if err != nil { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	if len(existingImages) > 0 { | ||||
| 		noOfContainers, err := f.GetInt() | ||||
| 		if err != nil { | ||||
| 			return 0 | ||||
| 		} | ||||
| 		// maxNoOfContainers is currently | ||||
| 		// completely arbitrarily defined | ||||
| 		maxNoOfContainers := 50 | ||||
| 		for i := 0; i < noOfContainers%maxNoOfContainers; i++ { | ||||
| 			container, err := newContainer(client, f, ctx) | ||||
| 			if err == nil { | ||||
| 				defer container.Delete(ctx, containerd.WithSnapshotCleanup) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	// End create containers | ||||
|  | ||||
| 	return 1 | ||||
| } | ||||
|  | ||||
| // FuzzCreateContainerNoTearDown() implements a fuzzer | ||||
| // similar to FuzzCreateContainerWithTearDown() with | ||||
| // with one minor distinction: One tears down the | ||||
| // daemon after each iteration whereas the other doesn't. | ||||
| // The two fuzzers' performance will be compared over time. | ||||
| func FuzzCreateContainerNoTearDown(data []byte) int { | ||||
| 	ret := doFuzz(data, false) | ||||
| 	return ret | ||||
| } | ||||
|  | ||||
| // FuzzCreateContainerWithTearDown() is similar to | ||||
| // FuzzCreateContainerNoTearDown() except that | ||||
| // FuzzCreateContainerWithTearDown tears down the daemon | ||||
| // after each iteration. | ||||
| func FuzzCreateContainerWithTearDown(data []byte) int { | ||||
| 	ret := doFuzz(data, true) | ||||
| 	return ret | ||||
| } | ||||
| @@ -17,10 +17,46 @@ | ||||
| cd "$(dirname "${BASH_SOURCE[0]}")" | ||||
| cd ../../ | ||||
|  | ||||
| # Don't move docker_fuzzer.go back into contrib/fuzz | ||||
| # Move all fuzzers that don't have the "fuzz" package out of this dir | ||||
| mv contrib/fuzz/docker_fuzzer.go remotes/docker/ | ||||
| compile_go_fuzzer github.com/containerd/containerd/remotes/docker FuzzFetcher fuzz_fetcher | ||||
| mv contrib/fuzz/container_fuzzer.go integration/client/ | ||||
|  | ||||
|  | ||||
| compile_go_fuzzer github.com/containerd/containerd/remotes/docker FuzzFetcher fuzz_fetcher | ||||
| compile_go_fuzzer github.com/containerd/containerd/contrib/fuzz FuzzFiltersParse fuzz_filters_parse | ||||
| compile_go_fuzzer github.com/containerd/containerd/contrib/fuzz FuzzPlatformsParse fuzz_platforms_parse | ||||
| compile_go_fuzzer github.com/containerd/containerd/contrib/fuzz FuzzApply fuzz_apply | ||||
|  | ||||
|  | ||||
| # FuzzCreateContainer requires more setup than the fuzzers above. | ||||
| # We need the binaries from "make". | ||||
| wget -c https://github.com/protocolbuffers/protobuf/releases/download/v3.11.4/protoc-3.11.4-linux-x86_64.zip | ||||
| unzip protoc-3.11.4-linux-x86_64.zip -d /usr/local | ||||
|  | ||||
| export CGO_ENABLED=1 | ||||
| export GOARCH=amd64 | ||||
|  | ||||
| # Build runc | ||||
| cd $SRC/ | ||||
| git clone https://github.com/opencontainers/runc | ||||
| cd runc | ||||
| make | ||||
| make install | ||||
|  | ||||
| # Build static containerd | ||||
| cd $SRC/containerd | ||||
| make EXTRA_FLAGS="-buildmode pie" \ | ||||
| 	EXTRA_LDFLAGS='-linkmode external -extldflags "-fno-PIC -static"' \ | ||||
| 	BUILDTAGS="netgo osusergo static_build" | ||||
|  | ||||
|  | ||||
| mkdir $OUT/containerd-binaries || true | ||||
| cd $SRC/containerd/bin && cp * $OUT/containerd-binaries/ && cd - | ||||
|  | ||||
| cd integration/client | ||||
| # Rename all *_test.go to *_test_fuzz.go to use their declarations: | ||||
| for i in $( ls *_test.go ); do mv $i ./${i%.*}_fuzz.go; done | ||||
| # Remove windows test to avoid double declarations | ||||
| rm ./client_windows_test_fuzz.go | ||||
| compile_go_fuzzer . FuzzCreateContainerNoTearDown fuzz_create_container_no_teardown | ||||
| compile_go_fuzzer . FuzzCreateContainerWithTearDown fuzz_create_container_with_teardown | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Derek McGowan
					Derek McGowan