123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- //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/containerd/runtime/v2/shim"
- "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
- // https://github.com/containerd/containerd/blob/11ded166c15f92450958078cd13c6d87131ec563/runtime/v2/shim/util.go#L83-L93
- return !filepath.IsAbs(name) && !strings.ContainsRune(name, '/') && shim.BinaryName(name) != ""
- }
|