github.com/moby/docker@v26.1.3+incompatible/daemon/runtime_unix.go (about)

     1  //go:build !windows
     2  
     3  package daemon
     4  
     5  import (
     6  	"bytes"
     7  	"context"
     8  	"crypto/sha256"
     9  	"encoding/base32"
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"strings"
    17  
    18  	"github.com/containerd/containerd/plugin"
    19  	v2runcoptions "github.com/containerd/containerd/runtime/v2/runc/options"
    20  	"github.com/containerd/containerd/runtime/v2/shim"
    21  	"github.com/containerd/log"
    22  	"github.com/docker/docker/daemon/config"
    23  	"github.com/docker/docker/errdefs"
    24  	"github.com/docker/docker/libcontainerd/shimopts"
    25  	"github.com/docker/docker/pkg/ioutils"
    26  	"github.com/docker/docker/pkg/system"
    27  	"github.com/opencontainers/runtime-spec/specs-go/features"
    28  	"github.com/pkg/errors"
    29  )
    30  
    31  const (
    32  	defaultRuntimeName = "runc"
    33  
    34  	// The runtime used to specify the containerd v2 runc shim
    35  	linuxV2RuntimeName = "io.containerd.runc.v2"
    36  )
    37  
    38  type shimConfig struct {
    39  	Shim     string
    40  	Opts     interface{}
    41  	Features *features.Features
    42  
    43  	// Check if the ShimConfig is valid given the current state of the system.
    44  	PreflightCheck func() error
    45  }
    46  
    47  type runtimes struct {
    48  	Default    string
    49  	configured map[string]*shimConfig
    50  }
    51  
    52  func stockRuntimes() map[string]string {
    53  	return map[string]string{
    54  		linuxV2RuntimeName:      defaultRuntimeName,
    55  		config.StockRuntimeName: defaultRuntimeName,
    56  	}
    57  }
    58  
    59  func defaultV2ShimConfig(conf *config.Config, runtimePath string) *shimConfig {
    60  	shim := &shimConfig{
    61  		Shim: plugin.RuntimeRuncV2,
    62  		Opts: &v2runcoptions.Options{
    63  			BinaryName:    runtimePath,
    64  			Root:          filepath.Join(conf.ExecRoot, "runtime-"+defaultRuntimeName),
    65  			SystemdCgroup: UsingSystemd(conf),
    66  			NoPivotRoot:   os.Getenv("DOCKER_RAMDISK") != "",
    67  		},
    68  	}
    69  
    70  	var featuresStderr bytes.Buffer
    71  	featuresCmd := exec.Command(runtimePath, "features")
    72  	featuresCmd.Stderr = &featuresStderr
    73  	if featuresB, err := featuresCmd.Output(); err != nil {
    74  		log.G(context.TODO()).WithError(err).Warnf("Failed to run %v: %q", featuresCmd.Args, featuresStderr.String())
    75  	} else {
    76  		var features features.Features
    77  		if jsonErr := json.Unmarshal(featuresB, &features); jsonErr != nil {
    78  			log.G(context.TODO()).WithError(err).Warnf("Failed to unmarshal the output of %v as a JSON", featuresCmd.Args)
    79  		} else {
    80  			shim.Features = &features
    81  		}
    82  	}
    83  
    84  	return shim
    85  }
    86  
    87  func runtimeScriptsDir(cfg *config.Config) string {
    88  	return filepath.Join(cfg.Root, "runtimes")
    89  }
    90  
    91  // initRuntimesDir creates a fresh directory where we'll store the runtime
    92  // scripts (i.e. in order to support runtimeArgs).
    93  func initRuntimesDir(cfg *config.Config) error {
    94  	runtimeDir := runtimeScriptsDir(cfg)
    95  	if err := os.RemoveAll(runtimeDir); err != nil {
    96  		return err
    97  	}
    98  	return system.MkdirAll(runtimeDir, 0o700)
    99  }
   100  
   101  func setupRuntimes(cfg *config.Config) (runtimes, error) {
   102  	if _, ok := cfg.Runtimes[config.StockRuntimeName]; ok {
   103  		return runtimes{}, errors.Errorf("runtime name '%s' is reserved", config.StockRuntimeName)
   104  	}
   105  
   106  	newrt := runtimes{
   107  		Default:    cfg.DefaultRuntime,
   108  		configured: make(map[string]*shimConfig),
   109  	}
   110  	for name, path := range stockRuntimes() {
   111  		newrt.configured[name] = defaultV2ShimConfig(cfg, path)
   112  	}
   113  
   114  	if newrt.Default != "" {
   115  		_, isStock := newrt.configured[newrt.Default]
   116  		_, isConfigured := cfg.Runtimes[newrt.Default]
   117  		if !isStock && !isConfigured && !isPermissibleC8dRuntimeName(newrt.Default) {
   118  			return runtimes{}, errors.Errorf("specified default runtime '%s' does not exist", newrt.Default)
   119  		}
   120  	} else {
   121  		newrt.Default = config.StockRuntimeName
   122  	}
   123  
   124  	dir := runtimeScriptsDir(cfg)
   125  	for name, rt := range cfg.Runtimes {
   126  		var c *shimConfig
   127  		if rt.Path == "" && rt.Type == "" {
   128  			return runtimes{}, errors.Errorf("runtime %s: either a runtimeType or a path must be configured", name)
   129  		}
   130  		if rt.Path != "" {
   131  			if rt.Type != "" {
   132  				return runtimes{}, errors.Errorf("runtime %s: cannot configure both path and runtimeType for the same runtime", name)
   133  			}
   134  			if len(rt.Options) > 0 {
   135  				return runtimes{}, errors.Errorf("runtime %s: options cannot be used with a path runtime", name)
   136  			}
   137  
   138  			binaryName := rt.Path
   139  			needsWrapper := len(rt.Args) > 0
   140  			if needsWrapper {
   141  				var err error
   142  				binaryName, err = wrapRuntime(dir, name, rt.Path, rt.Args)
   143  				if err != nil {
   144  					return runtimes{}, err
   145  				}
   146  			}
   147  			c = defaultV2ShimConfig(cfg, binaryName)
   148  			if needsWrapper {
   149  				path := rt.Path
   150  				c.PreflightCheck = func() error {
   151  					// Check that the runtime path actually exists so that we can return a well known error.
   152  					_, err := exec.LookPath(path)
   153  					return errors.Wrap(err, "error while looking up the specified runtime path")
   154  				}
   155  			}
   156  		} else {
   157  			if len(rt.Args) > 0 {
   158  				return runtimes{}, errors.Errorf("runtime %s: args cannot be used with a runtimeType runtime", name)
   159  			}
   160  			// Unlike implicit runtimes, there is no restriction on configuring a shim by path.
   161  			c = &shimConfig{Shim: rt.Type}
   162  			if len(rt.Options) > 0 {
   163  				// It has to be a pointer type or there'll be a panic in containerd/typeurl when we try to start the container.
   164  				var err error
   165  				c.Opts, err = shimopts.Generate(rt.Type, rt.Options)
   166  				if err != nil {
   167  					return runtimes{}, errors.Wrapf(err, "runtime %v", name)
   168  				}
   169  			}
   170  		}
   171  		newrt.configured[name] = c
   172  	}
   173  
   174  	return newrt, nil
   175  }
   176  
   177  // A non-standard Base32 encoding which lacks vowels to avoid accidentally
   178  // spelling naughty words. Don't use this to encode any data which requires
   179  // compatibility with anything outside of the currently-running process.
   180  var base32Disemvoweled = base32.NewEncoding("0123456789BCDFGHJKLMNPQRSTVWXYZ-")
   181  
   182  // wrapRuntime writes a shell script to dir which will execute binary with args
   183  // concatenated to the script's argv. This is needed because the
   184  // io.containerd.runc.v2 shim has no options for passing extra arguments to the
   185  // runtime binary.
   186  func wrapRuntime(dir, name, binary string, args []string) (string, error) {
   187  	var wrapper bytes.Buffer
   188  	sum := sha256.New()
   189  	_, _ = fmt.Fprintf(io.MultiWriter(&wrapper, sum), "#!/bin/sh\n%s %s $@\n", binary, strings.Join(args, " "))
   190  	// Generate a consistent name for the wrapper script derived from the
   191  	// contents so that multiple wrapper scripts can coexist with the same
   192  	// base name. The existing scripts might still be referenced by running
   193  	// containers.
   194  	suffix := base32Disemvoweled.EncodeToString(sum.Sum(nil))
   195  	scriptPath := filepath.Join(dir, name+"."+suffix)
   196  	if err := ioutils.AtomicWriteFile(scriptPath, wrapper.Bytes(), 0o700); err != nil {
   197  		return "", err
   198  	}
   199  	return scriptPath, nil
   200  }
   201  
   202  // Get returns the containerd runtime and options for name, suitable to pass
   203  // into containerd.WithRuntime(). The runtime and options for the default
   204  // runtime are returned when name is the empty string.
   205  func (r *runtimes) Get(name string) (string, interface{}, error) {
   206  	if name == "" {
   207  		name = r.Default
   208  	}
   209  
   210  	rt := r.configured[name]
   211  	if rt != nil {
   212  		if rt.PreflightCheck != nil {
   213  			if err := rt.PreflightCheck(); err != nil {
   214  				return "", nil, err
   215  			}
   216  		}
   217  		return rt.Shim, rt.Opts, nil
   218  	}
   219  
   220  	if !isPermissibleC8dRuntimeName(name) {
   221  		return "", nil, errdefs.InvalidParameter(errors.Errorf("unknown or invalid runtime name: %s", name))
   222  	}
   223  	return name, nil, nil
   224  }
   225  
   226  func (r *runtimes) Features(name string) *features.Features {
   227  	if name == "" {
   228  		name = r.Default
   229  	}
   230  
   231  	rt := r.configured[name]
   232  	if rt != nil {
   233  		return rt.Features
   234  	}
   235  	return nil
   236  }
   237  
   238  // isPermissibleC8dRuntimeName tests whether name is safe to pass into
   239  // containerd as a runtime name, and whether the name is well-formed.
   240  // It does not check if the runtime is installed.
   241  //
   242  // A runtime name containing slash characters is interpreted by containerd as
   243  // the path to a runtime binary. If we allowed this, anyone with Engine API
   244  // access could get containerd to execute an arbitrary binary as root. Although
   245  // Engine API access is already equivalent to root on the host, the runtime name
   246  // has not historically been a vector to run arbitrary code as root so users are
   247  // not expecting it to become one.
   248  //
   249  // This restriction is not configurable. There are viable workarounds for
   250  // legitimate use cases: administrators and runtime developers can make runtimes
   251  // available for use with Docker by installing them onto PATH following the
   252  // [binary naming convention] for containerd Runtime v2.
   253  //
   254  // [binary naming convention]: https://github.com/containerd/containerd/blob/main/runtime/v2/README.md#binary-naming
   255  func isPermissibleC8dRuntimeName(name string) bool {
   256  	// containerd uses a rather permissive test to validate runtime names:
   257  	//
   258  	//   - Any name for which filepath.IsAbs(name) is interpreted as the absolute
   259  	//     path to a shim binary. We want to block this behaviour.
   260  	//   - Any name which contains at least one '.' character and no '/' characters
   261  	//     and does not begin with a '.' character is a valid runtime name. The shim
   262  	//     binary name is derived from the final two components of the name and
   263  	//     searched for on the PATH. The name "a.." is technically valid per
   264  	//     containerd's implementation: it would resolve to a binary named
   265  	//     "containerd-shim---".
   266  	//
   267  	// https://github.com/containerd/containerd/blob/11ded166c15f92450958078cd13c6d87131ec563/runtime/v2/manager.go#L297-L317
   268  	// https://github.com/containerd/containerd/blob/11ded166c15f92450958078cd13c6d87131ec563/runtime/v2/shim/util.go#L83-L93
   269  	return !filepath.IsAbs(name) && !strings.ContainsRune(name, '/') && shim.BinaryName(name) != ""
   270  }