New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix Windows args and ArgsEscaped
handling
#4723
base: master
Are you sure you want to change the base?
Conversation
I was surprised to see `ArgsEscaped` being set on Linux images -- it's Windows specific and should never be set on Linux images. In a case of perfect timing, we got our first official `buildkitd.exe` builds and I realized the handling of this goes deeper now (involving the runtime/executors now too). Previously to this, we were not properly escaping Windows command/arguments while constructing `CommandLine`, which has unexpected behavior. To illustrate, I created a simple Go program that does nothing but `fmt.Printf("%#v\n", os.Args)`. I installed it at `C:\foo bar\args.exe` and set `CMD ["C:\\foo bar\\args.exe", "foo bar", "baz buzz"]`. With just that, we get the expected `[]string{"C:\\foo bar\\args.exe", "foo bar", "baz buzz"}` output from our program. However, when we *also* install `args.exe` as `C:\\foo.exe`, `C:\\foo bar\\args.exe` being unescaped at the start of `CommandLine` (thanks to `ArgsEscaped: true`) becomes ambiguous, and Windows chooses the more conservative path, and our output becomes `[]string{"C:\\foo", "bar\\args.exe", "foo bar", "baz buzz"}` instead (even though we used the imperative/JSON form of `CMD` which should've avoided this!). In the case of the new `RUN` support inside the builder, things were actually even worse! Our output (from `RUN ["C:\\foo bar\\args.exe", "foo bar", "baz buzz"]`) was instead `[]string{"C:\\foo", "bar\\args.exe", "foo", "bar", "baz", "buzz"}` because the code was effectively just `CommandLine = strings.Join(args, " ")`, which is definitely not enough. 馃槄 See the PR for several references to related discussions/code in Moby. 馃殌 Signed-off-by: Tianon Gravi <admwiggin@gmail.com>
Oh, and testing! There should probably be some tests for this, but I wasn't sure where to look or start. This has taken me days to work through already. 馃槄 |
Thanks for this, will take a look at this, this weekend. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here are comments from my first pass. I'm also testing locally and will drop comments here.
Also just a general observation, first thanks for noting the TODOs! Any way we could just close on them as part of this PR? If the scope is a little wide, I'm open to splitting the work and helping.
d.image.Config.Cmd = args | ||
d.image.Config.ArgsEscaped = true //nolint:staticcheck // ignore SA1019: field is deprecated in OCI Image spec, but used for backward-compatibility with Docker image spec. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: does moving this into the windows
if-block have any effect on the Linux case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it absolutely does -- that was the original intent of my looking into this in the first place. In short, ArgsEscaped
should never be set on a non-Windows image, and certainly never to true
, and any case where it is gets (correctly) ignored by the runtimes anyhow. It's a 100% Windows-specific field.
@@ -1398,6 +1407,23 @@ func dispatchEntrypoint(d *dispatchState, c *instructions.EntrypointCommand) err | |||
if c.PrependShell { | |||
args = withShell(d.image, args) | |||
} | |||
if d.image.OS == "windows" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this block is repeated twice, L1390 and here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct -- we need separate, similar (but slightly different) handling for ENTRYPOINT
vs CMD
(see the linked original Moby PR/implementation).
if d.image.Config.ArgsEscaped != argsEscaped && | ||
(len(d.image.Config.Cmd) > 1 || | ||
(len(d.image.Config.Cmd) == 1 && | ||
strings.ToLower(d.image.Config.Cmd[0]) != `c:\windows\system32\cmd.exe` && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: how about if the user just had cmd.exe
instead of the absolute path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the comment above (and the linked original PR) -- this is just to handle the special case of the default Cmd
set in the published base images.
Some notes from testing=> Is there anything you are doing to build FROM mcr.microsoft.com/windows/nanoserver:ltsc2022
RUN mkdir foo
RUN mkdir "foo bar"
# need to check how to excape spaces for COPY
COPY ./ ./
# COPY ./args.exe ./foo/args.exe
CMD [ "C:\\foo bar\\args.exe", "foo bar", "baz buzz" ] build gives this error:
However, it's successful for PS > docker build -t buildkit-args .
Sending build context to Docker daemon 3.827MB
Step 1/5 : FROM mcr.microsoft.com/windows/nanoserver:ltsc2022
---> 6ad91fb31728
Step 2/5 : RUN mkdir foo
---> Using cache
---> 522ec323b2ef
Step 3/5 : RUN mkdir "foo bar"
---> Using cache
---> 0df2959b2f98
Step 4/5 : COPY ./ ./
---> Using cache
---> 4c0a8b317934
Step 5/5 : CMD [ "C:\\foo bar\\args.exe", "foo bar", "baz buzz" ]
---> Using cache
---> 8c5e638d0180
Successfully built 8c5e638d0180
Successfully tagged buildkit-args:latest
PS > docker run --rm buildkit-args
[]string{"C:\\foo bar\\args.exe", "foo bar", "baz buzz"} |
This looks related to fsutil: https://github.com/tonistiigi/fsutil/blob/7525a1af2bb545e89dc9bced785bff7a3b7f08c2/validator.go#L31 |
Yes, absolutely! Sorry for not being more clear -- I do not intend for this to land with the |
Here's the FROM --platform=$BUILDPLATFORM golang AS build
COPY args.go ./
RUN GOOS=windows GOARCH=amd64 go build -o '/args.exe' ./args.go
FROM mcr.microsoft.com/windows/servercore:ltsc2022
COPY --from=build ["/args.exe","/foo bar/"]
COPY --from=build ["/args.exe","/foo.exe"]
CMD ["C:\\foo bar\\args.exe", "foo bar", "baz buzz"] // args.go
package main
import (
"fmt"
"os"
)
func main() {
fmt.Printf("%#v\n", os.Args)
} You can find a built copy of the image at |
I was surprised to see
ArgsEscaped
being set on Linux images -- it's Windows specific and should never be set on Linux images. In a case of perfect timing, we got our first officialbuildkitd.exe
builds and I realized the handling of this goes deeper now (involving the runtime/executors now too).Previously to this, we were not properly escaping Windows command/arguments while constructing
CommandLine
, which has unexpected behavior.To illustrate, I created a simple Go program that does nothing but
fmt.Printf("%#v\n", os.Args)
. I installed it atC:\foo bar\args.exe
and setCMD ["C:\\foo bar\\args.exe", "foo bar", "baz buzz"]
.With just that, we get the expected
[]string{"C:\\foo bar\\args.exe", "foo bar", "baz buzz"}
output from our program. However, when we also installargs.exe
asC:\\foo.exe
,C:\\foo bar\\args.exe
being unescaped at the start ofCommandLine
(thanks toArgsEscaped: true
) becomes ambiguous, and Windows chooses the more conservative path, and our output becomes[]string{"C:\\foo", "bar\\args.exe", "foo bar", "baz buzz"}
instead (even though we used the imperative/JSON form ofCMD
which should've avoided this!).In the case of the new
RUN
support inside the builder, things were actually even worse! Our output (fromRUN ["C:\\foo bar\\args.exe", "foo bar", "baz buzz"]
) was instead[]string{"C:\\foo", "bar\\args.exe", "foo", "bar", "baz", "buzz"}
because the code was effectively justCommandLine = strings.Join(args, " ")
, which is definitely not enough. 馃槄Several references to related discussions/code in Moby: 馃殌
There are several
TODO
s in the code that I'm not actually sure how to resolve -- how to accessArgsEscaped
fromsetArgs
andwithProcessArgs
(which I'm not actually sure whyRun
andExec
are so separate in thatcontainerdexecutor
package 馃槄), and either how to emit a warning indispatchCmd
/dispatchEntrypoint
or whether an error seems more appropriate (so I can actually test whether I've copied that conditional logic correctly 馃槀 馃槶 鉂わ笍).