dotcloud/docker

View on GitHub
daemon/runtime_unix.go

Summary

Maintainability
A
0 mins
Test Coverage
//go:build !windows

package daemon

import (
    "bytes"
    "context"
    "crypto/sha256"
    "encoding/base32"
    "encoding/json"
    "fmt"
    "io"
    "os"
    "os/exec"
    "path/filepath"
    "strings"

    "github.com/containerd/containerd/plugin"
    v2runcoptions "github.com/containerd/containerd/runtime/v2/runc/options"
    "github.com/containerd/log"
    "github.com/docker/docker/daemon/config"
    "github.com/docker/docker/errdefs"
    "github.com/docker/docker/libcontainerd/shimopts"
    "github.com/docker/docker/pkg/ioutils"
    "github.com/docker/docker/pkg/system"
    "github.com/opencontainers/runtime-spec/specs-go/features"
    "github.com/pkg/errors"
)

const (
    defaultRuntimeName = "runc"

    // The runtime used to specify the containerd v2 runc shim
    linuxV2RuntimeName = "io.containerd.runc.v2"
)

type shimConfig struct {
    Shim     string
    Opts     interface{}
    Features *features.Features

    // Check if the ShimConfig is valid given the current state of the system.
    PreflightCheck func() error
}

type runtimes struct {
    Default    string
    configured map[string]*shimConfig
}

func stockRuntimes() map[string]string {
    return map[string]string{
        linuxV2RuntimeName:      defaultRuntimeName,
        config.StockRuntimeName: defaultRuntimeName,
    }
}

func defaultV2ShimConfig(conf *config.Config, runtimePath string) *shimConfig {
    shim := &shimConfig{
        Shim: plugin.RuntimeRuncV2,
        Opts: &v2runcoptions.Options{
            BinaryName:    runtimePath,
            Root:          filepath.Join(conf.ExecRoot, "runtime-"+defaultRuntimeName),
            SystemdCgroup: UsingSystemd(conf),
            NoPivotRoot:   os.Getenv("DOCKER_RAMDISK") != "",
        },
    }

    var featuresStderr bytes.Buffer
    featuresCmd := exec.Command(runtimePath, "features")
    featuresCmd.Stderr = &featuresStderr
    if featuresB, err := featuresCmd.Output(); err != nil {
        log.G(context.TODO()).WithError(err).Warnf("Failed to run %v: %q", featuresCmd.Args, featuresStderr.String())
    } else {
        var features features.Features
        if jsonErr := json.Unmarshal(featuresB, &features); jsonErr != nil {
            log.G(context.TODO()).WithError(err).Warnf("Failed to unmarshal the output of %v as a JSON", featuresCmd.Args)
        } else {
            shim.Features = &features
        }
    }

    return shim
}

func runtimeScriptsDir(cfg *config.Config) string {
    return filepath.Join(cfg.Root, "runtimes")
}

// initRuntimesDir creates a fresh directory where we'll store the runtime
// scripts (i.e. in order to support runtimeArgs).
func initRuntimesDir(cfg *config.Config) error {
    runtimeDir := runtimeScriptsDir(cfg)
    if err := os.RemoveAll(runtimeDir); err != nil {
        return err
    }
    return system.MkdirAll(runtimeDir, 0o700)
}

func setupRuntimes(cfg *config.Config) (runtimes, error) {
    if _, ok := cfg.Runtimes[config.StockRuntimeName]; ok {
        return runtimes{}, errors.Errorf("runtime name '%s' is reserved", config.StockRuntimeName)
    }

    newrt := runtimes{
        Default:    cfg.DefaultRuntime,
        configured: make(map[string]*shimConfig),
    }
    for name, path := range stockRuntimes() {
        newrt.configured[name] = defaultV2ShimConfig(cfg, path)
    }

    if newrt.Default != "" {
        _, isStock := newrt.configured[newrt.Default]
        _, isConfigured := cfg.Runtimes[newrt.Default]
        if !isStock && !isConfigured && !isPermissibleC8dRuntimeName(newrt.Default) {
            return runtimes{}, errors.Errorf("specified default runtime '%s' does not exist", newrt.Default)
        }
    } else {
        newrt.Default = config.StockRuntimeName
    }

    dir := runtimeScriptsDir(cfg)
    for name, rt := range cfg.Runtimes {
        var c *shimConfig
        if rt.Path == "" && rt.Type == "" {
            return runtimes{}, errors.Errorf("runtime %s: either a runtimeType or a path must be configured", name)
        }
        if rt.Path != "" {
            if rt.Type != "" {
                return runtimes{}, errors.Errorf("runtime %s: cannot configure both path and runtimeType for the same runtime", name)
            }
            if len(rt.Options) > 0 {
                return runtimes{}, errors.Errorf("runtime %s: options cannot be used with a path runtime", name)
            }

            binaryName := rt.Path
            needsWrapper := len(rt.Args) > 0
            if needsWrapper {
                var err error
                binaryName, err = wrapRuntime(dir, name, rt.Path, rt.Args)
                if err != nil {
                    return runtimes{}, err
                }
            }
            c = defaultV2ShimConfig(cfg, binaryName)
            if needsWrapper {
                path := rt.Path
                c.PreflightCheck = func() error {
                    // Check that the runtime path actually exists so that we can return a well known error.
                    _, err := exec.LookPath(path)
                    return errors.Wrap(err, "error while looking up the specified runtime path")
                }
            }
        } else {
            if len(rt.Args) > 0 {
                return runtimes{}, errors.Errorf("runtime %s: args cannot be used with a runtimeType runtime", name)
            }
            // Unlike implicit runtimes, there is no restriction on configuring a shim by path.
            c = &shimConfig{Shim: rt.Type}
            if len(rt.Options) > 0 {
                // It has to be a pointer type or there'll be a panic in containerd/typeurl when we try to start the container.
                var err error
                c.Opts, err = shimopts.Generate(rt.Type, rt.Options)
                if err != nil {
                    return runtimes{}, errors.Wrapf(err, "runtime %v", name)
                }
            }
        }
        newrt.configured[name] = c
    }

    return newrt, nil
}

// A non-standard Base32 encoding which lacks vowels to avoid accidentally
// spelling naughty words. Don't use this to encode any data which requires
// compatibility with anything outside of the currently-running process.
var base32Disemvoweled = base32.NewEncoding("0123456789BCDFGHJKLMNPQRSTVWXYZ-")

// wrapRuntime writes a shell script to dir which will execute binary with args
// concatenated to the script's argv. This is needed because the
// io.containerd.runc.v2 shim has no options for passing extra arguments to the
// runtime binary.
func wrapRuntime(dir, name, binary string, args []string) (string, error) {
    var wrapper bytes.Buffer
    sum := sha256.New()
    _, _ = fmt.Fprintf(io.MultiWriter(&wrapper, sum), "#!/bin/sh\n%s %s $@\n", binary, strings.Join(args, " "))
    // Generate a consistent name for the wrapper script derived from the
    // contents so that multiple wrapper scripts can coexist with the same
    // base name. The existing scripts might still be referenced by running
    // containers.
    suffix := base32Disemvoweled.EncodeToString(sum.Sum(nil))
    scriptPath := filepath.Join(dir, name+"."+suffix)
    if err := ioutils.AtomicWriteFile(scriptPath, wrapper.Bytes(), 0o700); err != nil {
        return "", err
    }
    return scriptPath, nil
}

// Get returns the containerd runtime and options for name, suitable to pass
// into containerd.WithRuntime(). The runtime and options for the default
// runtime are returned when name is the empty string.
func (r *runtimes) Get(name string) (string, interface{}, error) {
    if name == "" {
        name = r.Default
    }

    rt := r.configured[name]
    if rt != nil {
        if rt.PreflightCheck != nil {
            if err := rt.PreflightCheck(); err != nil {
                return "", nil, err
            }
        }
        return rt.Shim, rt.Opts, nil
    }

    if !isPermissibleC8dRuntimeName(name) {
        return "", nil, errdefs.InvalidParameter(errors.Errorf("unknown or invalid runtime name: %s", name))
    }
    return name, nil, nil
}

func (r *runtimes) Features(name string) *features.Features {
    if name == "" {
        name = r.Default
    }

    rt := r.configured[name]
    if rt != nil {
        return rt.Features
    }
    return nil
}

// isPermissibleC8dRuntimeName tests whether name is safe to pass into
// containerd as a runtime name, and whether the name is well-formed.
// It does not check if the runtime is installed.
//
// A runtime name containing slash characters is interpreted by containerd as
// the path to a runtime binary. If we allowed this, anyone with Engine API
// access could get containerd to execute an arbitrary binary as root. Although
// Engine API access is already equivalent to root on the host, the runtime name
// has not historically been a vector to run arbitrary code as root so users are
// not expecting it to become one.
//
// This restriction is not configurable. There are viable workarounds for
// legitimate use cases: administrators and runtime developers can make runtimes
// available for use with Docker by installing them onto PATH following the
// [binary naming convention] for containerd Runtime v2.
//
// [binary naming convention]: https://github.com/containerd/containerd/blob/main/runtime/v2/README.md#binary-naming
func isPermissibleC8dRuntimeName(name string) bool {
    // containerd uses a rather permissive test to validate runtime names:
    //
    //   - Any name for which filepath.IsAbs(name) is interpreted as the absolute
    //     path to a shim binary. We want to block this behaviour.
    //   - Any name which contains at least one '.' character and no '/' characters
    //     and does not begin with a '.' character is a valid runtime name. The shim
    //     binary name is derived from the final two components of the name and
    //     searched for on the PATH. The name "a.." is technically valid per
    //     containerd's implementation: it would resolve to a binary named
    //     "containerd-shim---".
    //
    // https://github.com/containerd/containerd/blob/11ded166c15f92450958078cd13c6d87131ec563/runtime/v2/manager.go#L297-L317
    if filepath.IsAbs(name) || strings.ContainsRune(name, '/') {
        return false
    }

    // runtime name should format like $prefix.name.version
    // see: https://github.com/containerd/containerd/blob/11ded166c15f92450958078cd13c6d87131ec563/runtime/v2/shim/util.go#L83-L93
    //
    // FIXME(thaJeztah): add a utility to the containerd module for this; some parts (like [shim.BinaryName]) are in the shim package, which comes with a large number of dependencies.
    if prefix, _, ok := strings.Cut(name, "."); !ok || prefix == "" {
        return false
    }

    return true
}