summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Howard <jhoward@microsoft.com>2019-01-17 16:03:29 -0800
committerJohn Howard <jhoward@microsoft.com>2019-03-07 10:28:11 -0800
commit4d2948b6f7377aafe2c5bd790f5ad5b5b13680e5 (patch)
treebfa668b96ae82ac60a601b01c45528397a6ad0dd
parentf023b590768a59c2e673abd5491f830504abe531 (diff)
downloaddocker-4d2948b6f7377aafe2c5bd790f5ad5b5b13680e5.tar.gz
Windows: (WCOW) Generate OCI spec that remote runtime can escape
Signed-off-by: John Howard <jhoward@microsoft.com> Also fixes https://github.com/moby/moby/issues/22874 This commit is a pre-requisite to moving moby/moby on Windows to using Containerd for its runtime. The reason for this is that the interface between moby and containerd for the runtime is an OCI spec which must be unambigious. It is the responsibility of the runtime (runhcs in the case of containerd on Windows) to ensure that arguments are escaped prior to calling into HCS and onwards to the Win32 CreateProcess call. Previously, the builder was always escaping arguments which has led to several bugs in moby. Because the local runtime in libcontainerd had context of whether or not arguments were escaped, it was possible to hack around in daemon/oci_windows.go with knowledge of the context of the call (from builder or not). With a remote runtime, this is not possible as there's rightly no context of the caller passed across in the OCI spec. Put another way, as I put above, the OCI spec must be unambigious. The other previous limitation (which leads to various subtle bugs) is that moby is coded entirely from a Linux-centric point of view. Unfortunately, Windows != Linux. Windows CreateProcess uses a command line, not an array of arguments. And it has very specific rules about how to escape a command line. Some interesting reading links about this are: https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ https://stackoverflow.com/questions/31838469/how-do-i-convert-argv-to-lpcommandline-parameter-of-createprocess https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments?view=vs-2017 For this reason, the OCI spec has recently been updated to cater for more natural syntax by including a CommandLine option in Process. What does this commit do? Primary objective is to ensure that the built OCI spec is unambigious. It changes the builder so that `ArgsEscaped` as commited in a layer is only controlled by the use of CMD or ENTRYPOINT. Subsequently, when calling in to create a container from the builder, if follows a different path to both `docker run` and `docker create` using the added `ContainerCreateIgnoreImagesArgsEscaped`. This allows a RUN from the builder to control how to escape in the OCI spec. It changes the builder so that when shell form is used for RUN, CMD or ENTRYPOINT, it builds (for WCOW) a more natural command line using the original as put by the user in the dockerfile, not the parsed version as a set of args which loses fidelity. This command line is put into args[0] and `ArgsEscaped` is set to true for CMD or ENTRYPOINT. A RUN statement does not commit `ArgsEscaped` to the commited layer regardless or whether shell or exec form were used.
-rw-r--r--api/types/container/config.go2
-rw-r--r--builder/builder.go4
-rw-r--r--builder/dockerfile/containerbackend.go2
-rw-r--r--builder/dockerfile/dispatchers.go54
-rw-r--r--builder/dockerfile/dispatchers_test.go36
-rw-r--r--builder/dockerfile/dispatchers_unix.go13
-rw-r--r--builder/dockerfile/dispatchers_windows.go46
-rw-r--r--builder/dockerfile/internals.go6
-rw-r--r--builder/dockerfile/internals_windows.go1
-rw-r--r--builder/dockerfile/mockbackend_test.go2
-rw-r--r--daemon/commit.go1
-rw-r--r--daemon/create.go77
-rw-r--r--daemon/exec_windows.go2
-rw-r--r--daemon/oci_windows.go14
-rw-r--r--image/image.go4
-rw-r--r--integration-cli/docker_cli_build_test.go52
-rw-r--r--libcontainerd/local/local_windows.go (renamed from libcontainerd/local/windows.go)37
-rw-r--r--pkg/system/args_windows.go16
18 files changed, 276 insertions, 93 deletions
diff --git a/api/types/container/config.go b/api/types/container/config.go
index 89ad08c234..f767195b94 100644
--- a/api/types/container/config.go
+++ b/api/types/container/config.go
@@ -54,7 +54,7 @@ type Config struct {
Env []string // List of environment variable to set in the container
Cmd strslice.StrSlice // Command to run when starting the container
Healthcheck *HealthConfig `json:",omitempty"` // Healthcheck describes how to check the container is healthy
- ArgsEscaped bool `json:",omitempty"` // True if command is already escaped (Windows specific)
+ ArgsEscaped bool `json:",omitempty"` // True if command is already escaped (meaning treat as a command line) (Windows specific).
Image string // Name of the image as it was passed by the operator (e.g. could be symbolic)
Volumes map[string]struct{} // List of volumes (mounts) used for the container
WorkingDir string // Current directory (PWD) in the command will be launched
diff --git a/builder/builder.go b/builder/builder.go
index 3eb0341417..cf4d737e2b 100644
--- a/builder/builder.go
+++ b/builder/builder.go
@@ -60,8 +60,8 @@ type ImageBackend interface {
type ExecBackend interface {
// ContainerAttachRaw attaches to container.
ContainerAttachRaw(cID string, stdin io.ReadCloser, stdout, stderr io.Writer, stream bool, attached chan struct{}) error
- // ContainerCreate creates a new Docker container and returns potential warnings
- ContainerCreate(config types.ContainerCreateConfig) (container.ContainerCreateCreatedBody, error)
+ // ContainerCreateIgnoreImagesArgsEscaped creates a new Docker container and returns potential warnings
+ ContainerCreateIgnoreImagesArgsEscaped(config types.ContainerCreateConfig) (container.ContainerCreateCreatedBody, error)
// ContainerRm removes a container specified by `id`.
ContainerRm(name string, config *types.ContainerRmConfig) error
// ContainerKill stops the container execution abruptly.
diff --git a/builder/dockerfile/containerbackend.go b/builder/dockerfile/containerbackend.go
index 54adfb13f7..8b70c6289d 100644
--- a/builder/dockerfile/containerbackend.go
+++ b/builder/dockerfile/containerbackend.go
@@ -29,7 +29,7 @@ func newContainerManager(docker builder.ExecBackend) *containerManager {
// Create a container
func (c *containerManager) Create(runConfig *container.Config, hostConfig *container.HostConfig) (container.ContainerCreateCreatedBody, error) {
- container, err := c.backend.ContainerCreate(types.ContainerCreateConfig{
+ container, err := c.backend.ContainerCreateIgnoreImagesArgsEscaped(types.ContainerCreateConfig{
Config: runConfig,
HostConfig: hostConfig,
})
diff --git a/builder/dockerfile/dispatchers.go b/builder/dockerfile/dispatchers.go
index f2da08ed4d..07c434259b 100644
--- a/builder/dockerfile/dispatchers.go
+++ b/builder/dockerfile/dispatchers.go
@@ -16,7 +16,6 @@ import (
"github.com/containerd/containerd/platforms"
"github.com/docker/docker/api"
- "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/builder"
"github.com/docker/docker/errdefs"
@@ -330,14 +329,6 @@ func dispatchWorkdir(d dispatchRequest, c *instructions.WorkdirCommand) error {
return d.builder.commitContainer(d.state, containerID, runConfigWithCommentCmd)
}
-func resolveCmdLine(cmd instructions.ShellDependantCmdLine, runConfig *container.Config, os string) []string {
- result := cmd.CmdLine
- if cmd.PrependShell && result != nil {
- result = append(getShell(runConfig, os), result...)
- }
- return result
-}
-
// RUN some command yo
//
// run a command and commit the image. Args are automatically prepended with
@@ -353,7 +344,7 @@ func dispatchRun(d dispatchRequest, c *instructions.RunCommand) error {
return system.ErrNotSupportedOperatingSystem
}
stateRunConfig := d.state.runConfig
- cmdFromArgs := resolveCmdLine(c.ShellDependantCmdLine, stateRunConfig, d.state.operatingSystem)
+ cmdFromArgs, argsEscaped := resolveCmdLine(c.ShellDependantCmdLine, stateRunConfig, d.state.operatingSystem, c.Name(), c.String())
buildArgs := d.state.buildArgs.FilterAllowed(stateRunConfig.Env)
saveCmd := cmdFromArgs
@@ -363,6 +354,7 @@ func dispatchRun(d dispatchRequest, c *instructions.RunCommand) error {
runConfigForCacheProbe := copyRunConfig(stateRunConfig,
withCmd(saveCmd),
+ withArgsEscaped(argsEscaped),
withEntrypointOverride(saveCmd, nil))
if hit, err := d.builder.probeCache(d.state, runConfigForCacheProbe); err != nil || hit {
return err
@@ -370,13 +362,11 @@ func dispatchRun(d dispatchRequest, c *instructions.RunCommand) error {
runConfig := copyRunConfig(stateRunConfig,
withCmd(cmdFromArgs),
+ withArgsEscaped(argsEscaped),
withEnv(append(stateRunConfig.Env, buildArgs...)),
withEntrypointOverride(saveCmd, strslice.StrSlice{""}),
withoutHealthcheck())
- // set config as already being escaped, this prevents double escaping on windows
- runConfig.ArgsEscaped = true
-
cID, err := d.builder.create(runConfig)
if err != nil {
return err
@@ -399,6 +389,12 @@ func dispatchRun(d dispatchRequest, c *instructions.RunCommand) error {
return err
}
+ // Don't persist the argsEscaped value in the committed image. Use the original
+ // from previous build steps (only CMD and ENTRYPOINT persist this).
+ if d.state.operatingSystem == "windows" {
+ runConfigForCacheProbe.ArgsEscaped = stateRunConfig.ArgsEscaped
+ }
+
return d.builder.commitContainer(d.state, cID, runConfigForCacheProbe)
}
@@ -434,15 +430,23 @@ func prependEnvOnCmd(buildArgs *BuildArgs, buildArgVars []string, cmd strslice.S
//
func dispatchCmd(d dispatchRequest, c *instructions.CmdCommand) error {
runConfig := d.state.runConfig
- cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.state.operatingSystem)
+ cmd, argsEscaped := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.state.operatingSystem, c.Name(), c.String())
+
+ // We warn here as Windows shell processing operates differently to Linux.
+ // Linux: /bin/sh -c "echo hello" world --> hello
+ // Windows: cmd /s /c "echo hello" world --> hello world
+ if d.state.operatingSystem == "windows" &&
+ len(runConfig.Entrypoint) > 0 &&
+ d.state.runConfig.ArgsEscaped != argsEscaped {
+ fmt.Fprintf(d.builder.Stderr, " ---> [Warning] Shell-form ENTRYPOINT and exec-form CMD may have unexpected results\n")
+ }
+
runConfig.Cmd = cmd
- // set config as already being escaped, this prevents double escaping on windows
- runConfig.ArgsEscaped = true
+ runConfig.ArgsEscaped = argsEscaped
if err := d.builder.commit(d.state, fmt.Sprintf("CMD %q", cmd)); err != nil {
return err
}
-
if len(c.ShellDependantCmdLine.CmdLine) != 0 {
d.state.cmdSet = true
}
@@ -477,8 +481,22 @@ func dispatchHealthcheck(d dispatchRequest, c *instructions.HealthCheckCommand)
//
func dispatchEntrypoint(d dispatchRequest, c *instructions.EntrypointCommand) error {
runConfig := d.state.runConfig
- cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.state.operatingSystem)
+ cmd, argsEscaped := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.state.operatingSystem, c.Name(), c.String())
+
+ // This warning is a little more complex than in dispatchCmd(), as the Windows base images (similar
+ // universally to almost every Linux image out there) have a single .Cmd field populated so that
+ // `docker run --rm image` starts the default shell which would typically be sh on Linux,
+ // or cmd on Windows. The catch to this is that if a dockerfile had `CMD ["c:\\windows\\system32\\cmd.exe"]`,
+ // we wouldn't be able to tell the difference. However, that would be highly unlikely, and besides, this
+ // is only trying to give a helpful warning of possibly unexpected results.
+ if d.state.operatingSystem == "windows" &&
+ d.state.runConfig.ArgsEscaped != argsEscaped &&
+ ((len(runConfig.Cmd) == 1 && strings.ToLower(runConfig.Cmd[0]) != `c:\windows\system32\cmd.exe` && len(runConfig.Shell) == 0) || (len(runConfig.Cmd) > 1)) {
+ fmt.Fprintf(d.builder.Stderr, " ---> [Warning] Shell-form CMD and exec-form ENTRYPOINT may have unexpected results\n")
+ }
+
runConfig.Entrypoint = cmd
+ runConfig.ArgsEscaped = argsEscaped
if !d.state.cmdSet {
runConfig.Cmd = nil
}
diff --git a/builder/dockerfile/dispatchers_test.go b/builder/dockerfile/dispatchers_test.go
index d767d318fa..fb823ff41c 100644
--- a/builder/dockerfile/dispatchers_test.go
+++ b/builder/dockerfile/dispatchers_test.go
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"runtime"
+ "strings"
"testing"
"github.com/docker/docker/api/types"
@@ -15,6 +16,7 @@ import (
"github.com/docker/docker/pkg/system"
"github.com/docker/go-connections/nat"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
+ "github.com/moby/buildkit/frontend/dockerfile/parser"
"github.com/moby/buildkit/frontend/dockerfile/shell"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
@@ -436,7 +438,14 @@ func TestRunWithBuildArgs(t *testing.T) {
runConfig := &container.Config{}
origCmd := strslice.StrSlice([]string{"cmd", "in", "from", "image"})
- cmdWithShell := strslice.StrSlice(append(getShell(runConfig, runtime.GOOS), "echo foo"))
+
+ var cmdWithShell strslice.StrSlice
+ if runtime.GOOS == "windows" {
+ cmdWithShell = strslice.StrSlice([]string{strings.Join(append(getShell(runConfig, runtime.GOOS), []string{"echo foo"}...), " ")})
+ } else {
+ cmdWithShell = strslice.StrSlice(append(getShell(runConfig, runtime.GOOS), "echo foo"))
+ }
+
envVars := []string{"|1", "one=two"}
cachedCmd := strslice.StrSlice(append(envVars, cmdWithShell...))
@@ -478,13 +487,24 @@ func TestRunWithBuildArgs(t *testing.T) {
err := initializeStage(sb, from)
assert.NilError(t, err)
sb.state.buildArgs.AddArg("one", strPtr("two"))
- run := &instructions.RunCommand{
- ShellDependantCmdLine: instructions.ShellDependantCmdLine{
- CmdLine: strslice.StrSlice{"echo foo"},
- PrependShell: true,
- },
- }
- assert.NilError(t, dispatch(sb, run))
+
+ // This is hugely annoying. On the Windows side, it relies on the
+ // RunCommand being able to emit String() and Name() (as implemented by
+ // withNameAndCode). Unfortunately, that is internal, and no way to directly
+ // set. However, we can fortunately use ParseInstruction in the instructions
+ // package to parse a fake node which can be used as our instructions.RunCommand
+ // instead.
+ node := &parser.Node{
+ Original: `RUN echo foo`,
+ Value: "run",
+ }
+ runint, err := instructions.ParseInstruction(node)
+ assert.NilError(t, err)
+ runinst := runint.(*instructions.RunCommand)
+ runinst.CmdLine = strslice.StrSlice{"echo foo"}
+ runinst.PrependShell = true
+
+ assert.NilError(t, dispatch(sb, runinst))
// Check that runConfig.Cmd has not been modified by run
assert.Check(t, is.DeepEqual(origCmd, sb.state.runConfig.Cmd))
diff --git a/builder/dockerfile/dispatchers_unix.go b/builder/dockerfile/dispatchers_unix.go
index b3ba380323..866bc6264d 100644
--- a/builder/dockerfile/dispatchers_unix.go
+++ b/builder/dockerfile/dispatchers_unix.go
@@ -6,6 +6,9 @@ import (
"errors"
"os"
"path/filepath"
+
+ "github.com/docker/docker/api/types/container"
+ "github.com/moby/buildkit/frontend/dockerfile/instructions"
)
// normalizeWorkdir normalizes a user requested working directory in a
@@ -21,3 +24,13 @@ func normalizeWorkdir(_ string, current string, requested string) (string, error
}
return requested, nil
}
+
+// resolveCmdLine takes a command line arg set and optionally prepends a platform-specific
+// shell in front of it.
+func resolveCmdLine(cmd instructions.ShellDependantCmdLine, runConfig *container.Config, os, _, _ string) ([]string, bool) {
+ result := cmd.CmdLine
+ if cmd.PrependShell && result != nil {
+ result = append(getShell(runConfig, os), result...)
+ }
+ return result, false
+}
diff --git a/builder/dockerfile/dispatchers_windows.go b/builder/dockerfile/dispatchers_windows.go
index 7824d1169b..4800ec9b8d 100644
--- a/builder/dockerfile/dispatchers_windows.go
+++ b/builder/dockerfile/dispatchers_windows.go
@@ -9,7 +9,9 @@ import (
"regexp"
"strings"
+ "github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/system"
+ "github.com/moby/buildkit/frontend/dockerfile/instructions"
)
var pattern = regexp.MustCompile(`^[a-zA-Z]:\.$`)
@@ -93,3 +95,47 @@ func normalizeWorkdirWindows(current string, requested string) (string, error) {
// Upper-case drive letter
return (strings.ToUpper(string(requested[0])) + requested[1:]), nil
}
+
+// resolveCmdLine takes a command line arg set and optionally prepends a platform-specific
+// shell in front of it. It returns either an array of arguments and an indication that
+// the arguments are not yet escaped; Or, an array containing a single command line element
+// along with an indication that the arguments are escaped so the runtime shouldn't escape.
+//
+// A better solution could be made, but it would be exceptionally invasive throughout
+// many parts of the daemon which are coded assuming Linux args array only only, not taking
+// account of Windows-natural command line semantics and it's argv handling. Put another way,
+// while what is here is good-enough, it could be improved, but would be highly invasive.
+//
+// The commands when this function is called are RUN, ENTRYPOINT and CMD.
+func resolveCmdLine(cmd instructions.ShellDependantCmdLine, runConfig *container.Config, os, command, original string) ([]string, bool) {
+
+ // Make sure we return an empty array if there is no cmd.CmdLine
+ if len(cmd.CmdLine) == 0 {
+ return []string{}, runConfig.ArgsEscaped
+ }
+
+ if os == "windows" { // ie WCOW
+ if cmd.PrependShell {
+ // WCOW shell-form. Return a single-element array containing the original command line prepended with the shell.
+ // Also indicate that it has not been escaped (so will be passed through directly to HCS). Note that
+ // we go back to the original un-parsed command line in the dockerfile line, strip off both the command part of
+ // it (RUN/ENTRYPOINT/CMD), and also strip any leading white space. IOW, we deliberately ignore any prior parsing
+ // so as to ensure it is treated exactly as a command line. For those interested, `RUN mkdir "c:/foo"` is a particularly
+ // good example of why this is necessary if you fancy debugging how cmd.exe and its builtin mkdir works. (Windows
+ // doesn't have a mkdir.exe, and I'm guessing cmd.exe has some very long unavoidable and unchangeable historical
+ // design decisions over how both its built-in echo and mkdir are coded. Probably more too.)
+ original = original[len(command):] // Strip off the command
+ original = strings.TrimLeft(original, " \t\v\n") // Strip of leading whitespace
+ return []string{strings.Join(getShell(runConfig, os), " ") + " " + original}, true
+ }
+
+ // WCOW JSON/"exec" form.
+ return cmd.CmdLine, false
+ }
+
+ // LCOW - use args as an array, same as LCOL.
+ if cmd.PrependShell && cmd.CmdLine != nil {
+ return append(getShell(runConfig, os), cmd.CmdLine...), false
+ }
+ return cmd.CmdLine, false
+}
diff --git a/builder/dockerfile/internals.go b/builder/dockerfile/internals.go
index 1635981f17..fbe301cdae 100644
--- a/builder/dockerfile/internals.go
+++ b/builder/dockerfile/internals.go
@@ -308,6 +308,12 @@ func withCmd(cmd []string) runConfigModifier {
}
}
+func withArgsEscaped(argsEscaped bool) runConfigModifier {
+ return func(runConfig *container.Config) {
+ runConfig.ArgsEscaped = argsEscaped
+ }
+}
+
// withCmdComment sets Cmd to a nop comment string. See withCmdCommentString for
// why there are two almost identical versions of this.
func withCmdComment(comment string, platform string) runConfigModifier {
diff --git a/builder/dockerfile/internals_windows.go b/builder/dockerfile/internals_windows.go
index 82135a12ae..9287703bb6 100644
--- a/builder/dockerfile/internals_windows.go
+++ b/builder/dockerfile/internals_windows.go
@@ -83,7 +83,6 @@ func lookupNTAccount(builder *Builder, accountName string, state *dispatchState)
runConfig := copyRunConfig(state.runConfig,
withCmdCommentString("internal run to obtain NT account information.", optionsPlatform.OS))
- runConfig.ArgsEscaped = true
runConfig.Cmd = []string{targetExecutable, "getaccountsid", accountName}
hostConfig := &container.HostConfig{Mounts: []mount.Mount{
diff --git a/builder/dockerfile/mockbackend_test.go b/builder/dockerfile/mockbackend_test.go
index 45cba00a8c..d4526eafad 100644
--- a/builder/dockerfile/mockbackend_test.go
+++ b/builder/dockerfile/mockbackend_test.go
@@ -28,7 +28,7 @@ func (m *MockBackend) ContainerAttachRaw(cID string, stdin io.ReadCloser, stdout
return nil
}
-func (m *MockBackend) ContainerCreate(config types.ContainerCreateConfig) (container.ContainerCreateCreatedBody, error) {
+func (m *MockBackend) ContainerCreateIgnoreImagesArgsEscaped(config types.ContainerCreateConfig) (container.ContainerCreateCreatedBody, error) {
if m.containerCreateFunc != nil {
return m.containerCreateFunc(config)
}
diff --git a/daemon/commit.go b/daemon/commit.go
index 0f6f440514..f20290f47c 100644
--- a/daemon/commit.go
+++ b/daemon/commit.go
@@ -68,7 +68,6 @@ func merge(userConf, imageConf *containertypes.Config) error {
if len(userConf.Entrypoint) == 0 {
if len(userConf.Cmd) == 0 {
userConf.Cmd = imageConf.Cmd
- userConf.ArgsEscaped = imageConf.ArgsEscaped
}
if userConf.Entrypoint == nil {
diff --git a/daemon/create.go b/daemon/create.go
index 1afb1bebea..b411218416 100644
--- a/daemon/create.go
+++ b/daemon/create.go
@@ -21,25 +21,46 @@ import (
"github.com/sirupsen/logrus"
)
+type createOpts struct {
+ params types.ContainerCreateConfig
+ managed bool
+ ignoreImagesArgsEscaped bool
+}
+
// CreateManagedContainer creates a container that is managed by a Service
func (daemon *Daemon) CreateManagedContainer(params types.ContainerCreateConfig) (containertypes.ContainerCreateCreatedBody, error) {
- return daemon.containerCreate(params, true)
+ return daemon.containerCreate(createOpts{
+ params: params,
+ managed: true,
+ ignoreImagesArgsEscaped: false})
}
// ContainerCreate creates a regular container
func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig) (containertypes.ContainerCreateCreatedBody, error) {
- return daemon.containerCreate(params, false)
+ return daemon.containerCreate(createOpts{
+ params: params,
+ managed: false,
+ ignoreImagesArgsEscaped: false})
+}
+
+// ContainerCreateIgnoreImagesArgsEscaped creates a regular container. This is called from the builder RUN case
+// and ensures that we do not take the images ArgsEscaped
+func (daemon *Daemon) ContainerCreateIgnoreImagesArgsEscaped(params types.ContainerCreateConfig) (containertypes.ContainerCreateCreatedBody, error) {
+ return daemon.containerCreate(createOpts{
+ params: params,
+ managed: false,
+ ignoreImagesArgsEscaped: true})
}
-func (daemon *Daemon) containerCreate(params types.ContainerCreateConfig, managed bool) (containertypes.ContainerCreateCreatedBody, error) {
+func (daemon *Daemon) containerCreate(opts createOpts) (containertypes.ContainerCreateCreatedBody, error) {
start := time.Now()
- if params.Config == nil {
+ if opts.params.Config == nil {
return containertypes.ContainerCreateCreatedBody{}, errdefs.InvalidParameter(errors.New("Config cannot be empty in order to create a container"))
}
os := runtime.GOOS
- if params.Config.Image != "" {
- img, err := daemon.imageService.GetImage(params.Config.Image)
+ if opts.params.Config.Image != "" {
+ img, err := daemon.imageService.GetImage(opts.params.Config.Image)
if err == nil {
os = img.OS
}
@@ -51,25 +72,25 @@ func (daemon *Daemon) containerCreate(params types.ContainerCreateConfig, manage
}
}
- warnings, err := daemon.verifyContainerSettings(os, params.HostConfig, params.Config, false)
+ warnings, err := daemon.verifyContainerSettings(os, opts.params.HostConfig, opts.params.Config, false)
if err != nil {
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, errdefs.InvalidParameter(err)
}
- err = verifyNetworkingConfig(params.NetworkingConfig)
+ err = verifyNetworkingConfig(opts.params.NetworkingConfig)
if err != nil {
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, errdefs.InvalidParameter(err)
}
- if params.HostConfig == nil {
- params.HostConfig = &containertypes.HostConfig{}
+ if opts.params.HostConfig == nil {
+ opts.params.HostConfig = &containertypes.HostConfig{}
}
- err = daemon.adaptContainerSettings(params.HostConfig, params.AdjustCPUShares)
+ err = daemon.adaptContainerSettings(opts.params.HostConfig, opts.params.AdjustCPUShares)
if err != nil {
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, errdefs.InvalidParameter(err)
}
- container, err := daemon.create(params, managed)
+ container, err := daemon.create(opts)
if err != nil {
return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, err
}
@@ -79,7 +100,7 @@ func (daemon *Daemon) containerCreate(params types.ContainerCreateConfig, manage
}
// Create creates a new container from the given configuration with a given name.
-func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) (retC *container.Container, retErr error) {
+func (daemon *Daemon) create(opts createOpts) (retC *container.Container, retErr error) {
var (
container *container.Container
img *image.Image
@@ -88,8 +109,8 @@ func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) (
)
os := runtime.GOOS
- if params.Config.Image != "" {
- img, err = daemon.imageService.GetImage(params.Config.Image)
+ if opts.params.Config.Image != "" {
+ img, err = daemon.imageService.GetImage(opts.params.Config.Image)
if err != nil {
return nil, err
}
@@ -112,15 +133,23 @@ func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) (
}
}
- if err := daemon.mergeAndVerifyConfig(params.Config, img); err != nil {
+ // On WCOW, if are not being invoked by the builder to create this container (where
+ // ignoreImagesArgEscaped will be true) - if the image already has its arguments escaped,
+ // ensure that this is replicated across to the created container to avoid double-escaping
+ // of the arguments/command line when the runtime attempts to run the container.
+ if os == "windows" && !opts.ignoreImagesArgsEscaped && img != nil && img.RunConfig().ArgsEscaped {
+ opts.params.Config.ArgsEscaped = true
+ }
+
+ if err := daemon.mergeAndVerifyConfig(opts.params.Config, img); err != nil {
return nil, errdefs.InvalidParameter(err)
}
- if err := daemon.mergeAndVerifyLogConfig(&params.HostConfig.LogConfig); err != nil {
+ if err := daemon.mergeAndVerifyLogConfig(&opts.params.HostConfig.LogConfig); err != nil {
return nil, errdefs.InvalidParameter(err)
}
- if container, err = daemon.newContainer(params.Name, os, params.Config, params.HostConfig, imgID, managed); err != nil {
+ if container, err = daemon.newContainer(opts.params.Name, os, opts.params.Config, opts.params.HostConfig, imgID, opts.managed); err != nil {
return nil, err
}
defer func() {
@@ -131,11 +160,11 @@ func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) (
}
}()
- if err := daemon.setSecurityOptions(container, params.HostConfig); err != nil {
+ if err := daemon.setSecurityOptions(container, opts.params.HostConfig); err != nil {
return nil, err
}
- container.HostConfig.StorageOpt = params.HostConfig.StorageOpt
+ container.HostConfig.StorageOpt = opts.params.HostConfig.StorageOpt
// Fixes: https://github.com/moby/moby/issues/34074 and
// https://github.com/docker/for-win/issues/999.
@@ -170,17 +199,17 @@ func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) (
return nil, err
}
- if err := daemon.setHostConfig(container, params.HostConfig); err != nil {
+ if err := daemon.setHostConfig(container, opts.params.HostConfig); err != nil {
return nil, err
}
- if err := daemon.createContainerOSSpecificSettings(container, params.Config, params.HostConfig); err != nil {
+ if err := daemon.createContainerOSSpecificSettings(container, opts.params.Config, opts.params.HostConfig); err != nil {
return nil, err
}
var endpointsConfigs map[string]*networktypes.EndpointSettings
- if params.NetworkingConfig != nil {
- endpointsConfigs = params.NetworkingConfig.EndpointsConfig
+ if opts.params.NetworkingConfig != nil {
+ endpointsConfigs = opts.params.NetworkingConfig.EndpointsConfig
}
// Make sure NetworkMode has an acceptable value. We do this to ensure
// backwards API compatibility.
diff --git a/daemon/exec_windows.go b/daemon/exec_windows.go
index c37ea9f31a..32f16e9282 100644
--- a/daemon/exec_windows.go
+++ b/daemon/exec_windows.go
@@ -7,9 +7,7 @@ import (
)
func (daemon *Daemon) execSetPlatformOpt(c *container.Container, ec *exec.Config, p *specs.Process) error {
- // Process arguments need to be escaped before sending to OCI.
if c.OS == "windows" {
- p.Args = escapeArgs(p.Args)
p.User.Username = ec.User
}
return nil
diff --git a/daemon/oci_windows.go b/daemon/oci_windows.go
index 01452d45bb..b635384f11 100644
--- a/daemon/oci_windows.go
+++ b/daemon/oci_windows.go
@@ -126,11 +126,6 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
}
// In s.Process
- s.Process.Args = append([]string{c.Path}, c.Args...)
- if !c.Config.ArgsEscaped && img.OS == "windows" {
- s.Process.Args = escapeArgs(s.Process.Args)
- }
-
s.Process.Cwd = c.Config.WorkingDir
s.Process.Env = c.CreateDaemonEnvironment(c.Config.Tty, linkedEnv)
if c.Config.Tty {
@@ -244,6 +239,14 @@ func (daemon *Daemon) createSpecWindowsFields(c *container.Container, s *specs.S
s.Process.Cwd = `C:\`
}
+ if c.Config.ArgsEscaped {
+ s.Process.CommandLine = c.Path
+ if len(c.Args) > 0 {
+ s.Process.CommandLine += " " + system.EscapeArgs(c.Args)
+ }
+ } else {
+ s.Process.Args = append([]string{c.Path}, c.Args...)
+ }
s.Root.Readonly = false // Windows does not support a read-only root filesystem
if !isHyperV {
if c.BaseFS == nil {
@@ -360,6 +363,7 @@ func (daemon *Daemon) createSpecLinuxFields(c *container.Container, s *specs.Spe
if len(s.Process.Cwd) == 0 {
s.Process.Cwd = `/`
}
+ s.Process.Args = append([]string{c.Path}, c.Args...)
s.Root.Path = "rootfs"
s.Root.Readonly = c.HostConfig.ReadonlyRootfs
diff --git a/image/image.go b/image/image.go
index 7e0646f072..079ecb8131 100644
--- a/image/image.go
+++ b/image/image.go
@@ -144,7 +144,7 @@ type ChildConfig struct {
}
// NewChildImage creates a new Image as a child of this image.
-func NewChildImage(img *Image, child ChildConfig, platform string) *Image {
+func NewChildImage(img *Image, child ChildConfig, os string) *Image {
isEmptyLayer := layer.IsEmpty(child.DiffID)
var rootFS *RootFS
if img.RootFS != nil {
@@ -167,7 +167,7 @@ func NewChildImage(img *Image, child ChildConfig, platform string) *Image {
DockerVersion: dockerversion.Version,
Config: child.Config,
Architecture: img.BaseImgArch(),
- OS: platform,
+ OS: os,
Container: child.ContainerID,
ContainerConfig: *child.ContainerConfig,
Author: child.Author,
diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go
index b8cf47269e..da33b24164 100644
--- a/integration-cli/docker_cli_build_test.go
+++ b/integration-cli/docker_cli_build_test.go
@@ -3203,7 +3203,7 @@ func (s *DockerSuite) TestBuildCmdShDashC(c *check.C) {
res := inspectFieldJSON(c, name, "Config.Cmd")
expected := `["/bin/sh","-c","echo cmd"]`
if testEnv.OSType == "windows" {
- expected = `["cmd","/S","/C","echo cmd"]`
+ expected = `["cmd /S /C echo cmd"]`
}
if res != expected {
c.Fatalf("Expected value %s not in Config.Cmd: %s", expected, res)
@@ -3276,7 +3276,7 @@ func (s *DockerSuite) TestBuildEntrypointCanBeOverriddenByChildInspect(c *check.
)
if testEnv.OSType == "windows" {
- expected = `["cmd","/S","/C","echo quux"]`
+ expected = `["cmd /S /C echo quux"]`
}
buildImageSuccessfully(c, name, build.WithDockerfile("FROM busybox\nENTRYPOINT /foo/bar"))
@@ -3362,8 +3362,8 @@ func (s *DockerSuite) TestBuildWithTabs(c *check.C) {
expected1 := `["/bin/sh","-c","echo\tone\t\ttwo"]`
expected2 := `["/bin/sh","-c","echo\u0009one\u0009\u0009two"]` // syntactically equivalent, and what Go 1.3 generates
if testEnv.OSType == "windows" {
- expected1 = `["cmd","/S","/C","echo\tone\t\ttwo"]`
- expected2 = `["cmd","/S","/C","echo\u0009one\u0009\u0009two"]` // syntactically equivalent, and what Go 1.3 generates
+ expected1 = `["cmd /S /C echo\tone\t\ttwo"]`
+ expected2 = `["cmd /S /C echo\u0009one\u0009\u0009two"]` // syntactically equivalent, and what Go 1.3 generates
}
if res != expected1 && res != expected2 {
c.Fatalf("Missing tabs.\nGot: %s\nExp: %s or %s", res, expected1, expected2)
@@ -5335,25 +5335,45 @@ func (s *DockerSuite) TestBuildEscapeNotBackslashWordTest(c *check.C) {
})
}
-// #22868. Make sure shell-form CMD is marked as escaped in the config of the image
+// #22868. Make sure shell-form CMD is not marked as escaped in the config of the image,
+// but an exec-form CMD is marked.
func (s *DockerSuite) TestBuildCmdShellArgsEscaped(c *check.C) {
testRequires(c, DaemonIsWindows)
- name := "testbuildcmdshellescaped"
- buildImageSuccessfully(c, name, build.WithDockerfile(`
+ name1 := "testbuildcmdshellescapedshellform"
+ buildImageSuccessfully(c, name1, build.WithDockerfile(`
FROM `+minimalBaseImage()+`
CMD "ipconfig"
`))
- res := inspectFieldJSON(c, name, "Config.ArgsEscaped")
+ res := inspectFieldJSON(c, name1, "Config.ArgsEscaped")
if res != "true" {
c.Fatalf("CMD did not update Config.ArgsEscaped on image: %v", res)
}
- dockerCmd(c, "run", "--name", "inspectme", name)
- dockerCmd(c, "wait", "inspectme")
- res = inspectFieldJSON(c, name, "Config.Cmd")
+ dockerCmd(c, "run", "--name", "inspectme1", name1)
+ dockerCmd(c, "wait", "inspectme1")
+ res = inspectFieldJSON(c, name1, "Config.Cmd")
+
+ if res != `["cmd /S /C \"ipconfig\""]` {
+ c.Fatalf("CMD incorrect in Config.Cmd: got %v", res)
+ }
- if res != `["cmd","/S","/C","\"ipconfig\""]` {
- c.Fatalf("CMD was not escaped Config.Cmd: got %v", res)
+ // Now in JSON/exec-form
+ name2 := "testbuildcmdshellescapedexecform"
+ buildImageSuccessfully(c, name2, build.WithDockerfile(`
+ FROM `+minimalBaseImage()+`
+ CMD ["ipconfig"]
+ `))
+ res = inspectFieldJSON(c, name2, "Config.ArgsEscaped")
+ if res != "false" {
+ c.Fatalf("CMD set Config.ArgsEscaped on image: %v", res)
}
+ dockerCmd(c, "run", "--name", "inspectme2", name2)
+ dockerCmd(c, "wait", "inspectme2")
+ res = inspectFieldJSON(c, name2, "Config.Cmd")
+
+ if res != `["ipconfig"]` {
+ c.Fatalf("CMD incorrect in Config.Cmd: got %v", res)
+ }
+
}
// Test case for #24912.
@@ -6150,7 +6170,11 @@ CMD echo foo
`))
out, _ := dockerCmd(c, "inspect", "--format", "{{ json .Config.Cmd }}", "build2")
- c.Assert(strings.TrimSpace(out), checker.Equals, `["/bin/sh","-c","echo foo"]`)
+ expected := `["/bin/sh","-c","echo foo"]`
+ if testEnv.OSType == "windows" {
+ expected = `["/bin/sh -c echo foo"]`
+ }
+ c.Assert(strings.TrimSpace(out), checker.Equals, expected)
}
// FIXME(vdemeester) should migrate to docker/cli tests
diff --git a/libcontainerd/local/windows.go b/libcontainerd/local/local_windows.go
index cd2fc0ae6d..7875337dd1 100644
--- a/libcontainerd/local/windows.go
+++ b/libcontainerd/local/local_windows.go
@@ -651,11 +651,11 @@ func (c *client) Start(_ context.Context, id, _ string, withStdin bool, attachSt
// Configure the environment for the process
createProcessParms.Environment = setupEnvironmentVariables(ctr.ociSpec.Process.Env)
- if ctr.isWindows {
- createProcessParms.CommandLine = strings.Join(ctr.ociSpec.Process.Args, " ")
- } else {
- createProcessParms.CommandArgs = ctr.ociSpec.Process.Args
- }
+
+ // Configure the CommandLine/CommandArgs
+ setCommandLineAndArgs(ctr.isWindows, ctr.ociSpec.Process, createProcessParms)
+ logger.Debugf("start commandLine: %s", createProcessParms.CommandLine)
+
createProcessParms.User = ctr.ociSpec.Process.User.Username
// LCOW requires the raw OCI spec passed through HCS and onwards to
@@ -741,6 +741,19 @@ func (c *client) Start(_ context.Context, id, _ string, withStdin bool, attachSt
return p.pid, nil
}
+// setCommandLineAndArgs configures the HCS ProcessConfig based on an OCI process spec
+func setCommandLineAndArgs(isWindows bool, process *specs.Process, createProcessParms *hcsshim.ProcessConfig) {
+ if isWindows {
+ if process.CommandLine != "" {
+ createProcessParms.CommandLine = process.CommandLine
+ } else {
+ createProcessParms.CommandLine = system.EscapeArgs(process.Args)
+ }
+ } else {
+ createProcessParms.CommandArgs = process.Args
+ }
+}
+
func newIOFromProcess(newProcess hcsshim.Process, terminal bool) (*cio.DirectIO, error) {
stdin, stdout, stderr, err := newProcess.Stdio()
if err != nil {
@@ -780,7 +793,7 @@ func (c *client) Exec(ctx context.Context, containerID, processID string, spec *
// docker can always grab the output through logs. We also tell HCS to always
// create stdin, even if it's not used - it will be closed shortly. Stderr
// is only created if it we're not -t.
- createProcessParms := hcsshim.ProcessConfig{
+ createProcessParms := &hcsshim.ProcessConfig{
CreateStdInPipe: true,
CreateStdOutPipe: true,
CreateStdErrPipe: !spec.Terminal,
@@ -803,17 +816,15 @@ func (c *client) Exec(ctx context.Context, containerID, processID string, spec *
// Configure the environment for the process
createProcessParms.Environment = setupEnvironmentVariables(spec.Env)
- if ctr.isWindows {
- createProcessParms.CommandLine = strings.Join(spec.Args, " ")
- } else {
- createProcessParms.CommandArgs = spec.Args
- }
- createProcessParms.User = spec.User.Username
+ // Configure the CommandLine/CommandArgs
+ setCommandLineAndArgs(ctr.isWindows, spec, createProcessParms)
logger.Debugf("exec commandLine: %s", createProcessParms.CommandLine)
+ createProcessParms.User = spec.User.Username
+
// Start the command running in the container.
- newProcess, err := ctr.hcsContainer.CreateProcess(&createProcessParms)
+ newProcess, err := ctr.hcsContainer.CreateProcess(createProcessParms)
if err != nil {
logger.WithError(err).Errorf("exec's CreateProcess() failed")
return -1, err
diff --git a/pkg/system/args_windows.go b/pkg/system/args_windows.go
new file mode 100644
index 0000000000..b7c9487a06
--- /dev/null
+++ b/pkg/system/args_windows.go
@@ -0,0 +1,16 @@
+package system // import "github.com/docker/docker/pkg/system"
+
+import (
+ "strings"
+
+ "golang.org/x/sys/windows"
+)
+
+// EscapeArgs makes a Windows-style escaped command line from a set of arguments
+func EscapeArgs(args []string) string {
+ escapedArgs := make([]string, len(args))
+ for i, a := range args {
+ escapedArgs[i] = windows.EscapeArg(a)
+ }
+ return strings.Join(escapedArgs, " ")
+}