runtime_unix.go 9.2 KB

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