| //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) != "" |
| } |