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 }