runtime_unix.go 9.1 KB

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