mirror of
https://github.com/containerd/containerd.git
synced 2026-08-11 09:37:14 +00:00
This commit makes all of the recommended changes to use the `testing` package helper functions instead of doing the equivalent longhand versions of the same thing. This change was needed in order to properly detect errors, as the code would previously skip running `tenv` stating that it had been deprecated in favor of `usetesting`. Signed-off-by: Enji Cooper <yaneurabeya@gmail.com>
460 lines
12 KiB
Go
460 lines
12 KiB
Go
/*
|
|
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.
|
|
*/
|
|
|
|
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
fuzz "github.com/AdaLogics/go-fuzz-headers"
|
|
containerd "github.com/containerd/containerd/v2/client"
|
|
"github.com/containerd/containerd/v2/pkg/oci"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
downloadLink = "https://github.com/containerd/containerd/releases/download/v2.0.1/containerd-2.0.1-linux-amd64.tar.gz"
|
|
downloadPath = "/tmp/containerd-2.0.1-linux-amd64.tar.gz"
|
|
downloadBinDir = "/tmp/containerd-binaries"
|
|
fuzzBinDir = "/out/containerd-binaries"
|
|
)
|
|
|
|
var (
|
|
// split a fuzz that requires download into 3 steps to avoid timeout in a
|
|
// single fuzz iteration in oss-fuzz.
|
|
// a fuzz should start only if the last step equals to `true`.
|
|
haveDownloadedbinaries = false
|
|
haveExtractedBinaries = false
|
|
haveChangedDownloadPATH = false
|
|
|
|
// for a fuzz that requires the pre-built containerd binaries, we only need
|
|
// one step to add the oss fuzz dst directory into PATH.
|
|
haveChangedOSSFuzzPATH = false
|
|
)
|
|
|
|
// downloadFile downloads a file from a url
|
|
func downloadFile(filepath string, url string) (err error) {
|
|
out, err := os.Create(filepath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
_, err = io.Copy(out, resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// initInSteps() performs initialization in several steps
|
|
// The reason for spreading the initialization out in
|
|
// multiple steps is that each fuzz iteration can maximum
|
|
// take 25 seconds when running through OSS-fuzz.
|
|
// Should an iteration exceed that, then the fuzzer stops.
|
|
func initInSteps(t *testing.T) bool {
|
|
// Download binaries
|
|
if !haveDownloadedbinaries {
|
|
if err := downloadFile(downloadPath, downloadLink); err != nil {
|
|
t.Fatalf("failed to download containerd: %v", err)
|
|
}
|
|
haveDownloadedbinaries = true
|
|
return true
|
|
}
|
|
// Extract binaries
|
|
if !haveExtractedBinaries {
|
|
|
|
if err := os.MkdirAll(downloadBinDir, 0777); err != nil {
|
|
return true
|
|
}
|
|
cmd := exec.Command("tar", "xvf", downloadPath, "-C", downloadBinDir)
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return true
|
|
}
|
|
haveExtractedBinaries = true
|
|
return true
|
|
}
|
|
// Add binaries to $PATH:
|
|
if !haveChangedDownloadPATH {
|
|
oldPathEnv := os.Getenv("PATH")
|
|
newPathEnv := fmt.Sprintf("%s:%s", filepath.Join(downloadBinDir, "bin"), oldPathEnv)
|
|
t.Setenv("PATH", newPathEnv)
|
|
haveChangedDownloadPATH = true
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
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 := 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") {
|
|
deleteSocket()
|
|
}
|
|
}
|
|
|
|
// startDaemon() starts the daemon.
|
|
func startDaemon(ctx context.Context, t *testing.T, shouldTearDown bool) {
|
|
buf := bytes.NewBuffer(nil)
|
|
tmpDir := t.TempDir()
|
|
stdioFile, err := os.Create(filepath.Join(tmpDir, "stdio"))
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
stdioFile.Close()
|
|
})
|
|
|
|
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") {
|
|
t.Fatalf("failed to start daemon: %v: %s", err, buf.String())
|
|
}
|
|
}
|
|
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()
|
|
t.Fatalf("failed to start daemon: %v: %s", err, buf.String())
|
|
}
|
|
}
|
|
|
|
// 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(defaultAddress)
|
|
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(defaultState, 0777)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oldPathEnv := os.Getenv("PATH")
|
|
newPathEnv := oldPathEnv + ":" + fuzzBinDir
|
|
|
|
if ghWorkspace := os.Getenv("GITHUB_WORKSPACE"); ghWorkspace != "" {
|
|
// In `oss_fuzz_build.sh`, we build and install containerd binaries to
|
|
// `$OUT/containerd-binaries`, where `OUT=/out` in oss-fuzz environment.
|
|
// However in GitHub Actions, oss-fuzz maps `OUT` to `$GITHUB_WORKSPACE/build-out`.
|
|
// So here we add this to `$PATH` at the end of `PATH` so the fuzz works on
|
|
// both environments.
|
|
newPathEnv = newPathEnv + ":" + filepath.Join(ghWorkspace, "build-out", "containerd-binaries")
|
|
}
|
|
|
|
err = os.Setenv("PATH", newPathEnv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
haveChangedOSSFuzzPATH = true
|
|
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(ctx context.Context, image containerd.Image, 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(context.Background())
|
|
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(ctx context.Context, client *containerd.Client, f *fuzz.ConsumeFuzzer) (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
|
|
}
|
|
|
|
switch remainder := determiner % 3; remainder {
|
|
case 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
|
|
case 1:
|
|
// 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(ctx, image, 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
|
|
default:
|
|
// 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(ctx, image, f)
|
|
|
|
container, err := client.NewContainer(ctx,
|
|
id,
|
|
containerd.WithImage(image))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return container, nil
|
|
}
|
|
}
|
|
|
|
// doFuzz() implements the logic of FuzzIntegCreateContainerNoTearDown()
|
|
// and FuzzIntegCreateContainerWithTearDown() 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(t *testing.T, data []byte, shouldTearDown bool) {
|
|
ctx, cancel := testContext(nil)
|
|
defer cancel()
|
|
|
|
// Check if daemon is running and start it if it isn't
|
|
if ctrd.cmd == nil {
|
|
startDaemon(ctx, t, shouldTearDown)
|
|
}
|
|
client, err := containerd.New(defaultAddress)
|
|
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
|
|
}
|
|
defer client.Close()
|
|
|
|
f := fuzz.NewConsumer(data)
|
|
|
|
// Begin import tars:
|
|
noOfImports, err := f.GetInt()
|
|
if err != nil {
|
|
return
|
|
}
|
|
// 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
|
|
}
|
|
_, _ = client.Import(ctx, bytes.NewReader(tarBytes))
|
|
}
|
|
// End import tars
|
|
|
|
// Begin create containers:
|
|
existingImages, err := client.ListImages(ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(existingImages) > 0 {
|
|
noOfContainers, err := f.GetInt()
|
|
if err != nil {
|
|
return
|
|
}
|
|
// maxNoOfContainers is currently
|
|
// completely arbitrarily defined
|
|
maxNoOfContainers := 50
|
|
for i := 0; i < noOfContainers%maxNoOfContainers; i++ {
|
|
container, err := newContainer(ctx, client, f)
|
|
if err == nil {
|
|
defer container.Delete(ctx, containerd.WithSnapshotCleanup)
|
|
}
|
|
}
|
|
}
|
|
// End create containers
|
|
}
|
|
|
|
// FuzzIntegNoTearDownWithDownload() implements a fuzzer
|
|
// similar to FuzzIntegCreateContainerNoTearDown() and
|
|
// FuzzIntegCreateContainerWithTearDown(), but it takes a
|
|
// different approach to the initialization. Where
|
|
// the other 2 fuzzers depend on the containerd binaries
|
|
// that were built manually, this fuzzer downloads them
|
|
// when starting a fuzz run.
|
|
// This fuzzer is experimental for now and is being run
|
|
// continuously by OSS-fuzz to collect feedback on
|
|
// its sustainability.
|
|
func FuzzIntegNoTearDownWithDownload(f *testing.F) {
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
|
if !haveChangedDownloadPATH {
|
|
if shouldRestart := initInSteps(t); shouldRestart {
|
|
return
|
|
}
|
|
}
|
|
doFuzz(t, data, false)
|
|
})
|
|
}
|
|
|
|
// FuzzIntegCreateContainerNoTearDown() implements a fuzzer
|
|
// similar to FuzzIntegCreateContainerWithTearDown()
|
|
// 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 FuzzIntegCreateContainerNoTearDown(f *testing.F) {
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
|
if !haveChangedOSSFuzzPATH {
|
|
if err := updatePathEnv(); err != nil {
|
|
return
|
|
}
|
|
}
|
|
doFuzz(t, data, false)
|
|
})
|
|
}
|
|
|
|
// FuzzIntegCreateContainerWithTearDown() is similar to
|
|
// FuzzIntegCreateContainerNoTearDown() except that
|
|
// FuzzIntegCreateContainerWithTearDown tears down the daemon
|
|
// after each iteration.
|
|
func FuzzIntegCreateContainerWithTearDown(f *testing.F) {
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
|
if !haveChangedOSSFuzzPATH {
|
|
if err := updatePathEnv(); err != nil {
|
|
return
|
|
}
|
|
}
|
|
doFuzz(t, data, true)
|
|
})
|
|
}
|