Skip to content
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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: util/path: CheckSystemDriveAndRemoveDriveLetter to preserve / #5317

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

profnandaa
Copy link
Collaborator

@profnandaa profnandaa commented Sep 10, 2024

The call to CheckSystemDriveAndRemoveDriveLetter() does not preserve the trailing / or \\. This happens because filepath.Clean() strips away any trailing slashes. For example /sample/ will be \\sample on Windows. This function was mainly written for Windows scenarios, which have System Drive Letters like C:/, etc.

This was causing cases like COPY testfile /testdir/ to be intepreted as COPY testfile /testdir, and if testdir is not explictly created before the call, it ends up being treated as a destination file other than a directory.

Fix this by checking that if we have a trailing / or \\, we preserve it after the call to filepath.Clean().

Fixes #5249

PS.

Also fixed for cross-building Windows from Linux, that would fail silently:

Repro dockerfile without RUN:

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022
COPY test1.txt /sample/
#RUN type \sample\test1.txt

Build log:

$ docker buildx build --platform windows/amd64 `
    --builder buildkitd-dev --no-cache --tag=windows-test . `
    --progress plain `
    --output type=local,dest=./output

#6 [2/2] COPY test1.txt /sample/
#6 DONE 0.1s

#7 exporting to client directory
#7 copying files 31B
#7 copying files 230.88MB 0.8s done
#7 DONE 0.8s

Checking results, sample is a file instead of a directory:

# before
$ ls -l output/sample
-rw-r--r-- 1 root root 6 Sep 24 08:57 output/sample

# after
$ ls -l output/sample
total 4
-rw-r--r-- 1 root root 6 Sep 24 08:57 test1.txt

NOTE: also covered cases like, where platform-specific filepath.Clean() won't strip out the \\ on Linux:

COPY test1.txt \\sample\\

@profnandaa profnandaa changed the title fix: util/path: CheckSystemDriveAndRemoveDriveLetter to preserve / fix: util/path: CheckSystemDriveAndRemoveDriveLetter to preserve / Sep 10, 2024
@profnandaa
Copy link
Collaborator Author

Looking into the CI failures...

@profnandaa
Copy link
Collaborator Author

Found the regression caused by this, thankfully coz of the checked in integration tests!

Dockerfile:

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022
COPY test1.txt /sample/
RUN type \sample\test1.txt

COPY test1.txt /
COPY test1.txt /test2.txt

RUN type test1.txt
RUN type test2.txt

Build log:

Dockerfile:5
--------------------
   3 |     RUN type \sample\test1.txt
   4 |     
   5 | >>> COPY test1.txt /
   6 |     COPY test1.txt /test2.txt
   7 |     
--------------------
error: failed to solve: removing drive letter: UNC paths are not supported
Process 5812 has exited with status 1

Fixing.

@profnandaa profnandaa force-pushed the fix-5249-copy-dir-path branch 4 times, most recently from ac8552f to af77ee9 Compare September 11, 2024 08:00
util/system/path.go Outdated Show resolved Hide resolved
util/system/path.go Outdated Show resolved Hide resolved
util/system/path.go Outdated Show resolved Hide resolved
// Path does not have a drive letter. Just return it.
if len(parts) < 2 {
return ToSlash(filepath.Clean(path), inputOS), nil
return ToSlash(filepath.Clean(path), inputOS) + keepTrailingSlash(parts[0], inputOS), nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filepath.Clean is still current system dependent here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me rethink the whole of this part, to a more simplified approach.

Copy link
Collaborator Author

@profnandaa profnandaa Sep 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LMK what you think about this new update. I've made sure that the trailing slash is only kept in the form that it was in the original path \\ or /; and since filepath.Clean() is system dependent, it's only appended if it has not been cleaned up in the first place.

On Linux vs. Windows for instance:

p := "\\sample\\"

// on linux
fmt.Printf("%v\n", p == filepath.Clean(p)) // true

// on windows
fmt.Printf("%v\n", p == filepath.Clean(p)) // false

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tonistiigi -- what do you think about handling this for WCOW scenarios only for now? So I made platform specific implementations of keepTrailingSlash.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this issue not appearing in linux? Isn't the issue in https://github.com/moby/buildkit/blob/v0.16.0/util/system/path.go#L75-L76 that is currently linux-only and doesn't handle inputOS .

Copy link
Collaborator Author

@profnandaa profnandaa Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the catch, yes, the issue also silently appears in Linux cross-builds. Silently coz there's no RUN to assertain the failure.

Repro dockerfile without RUN:

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022
COPY test1.txt /sample/
#RUN type \sample\test1.txt

Cross-build:

$ docker buildx build --platform windows/amd64 `
    --builder buildkitd-dev --no-cache --tag=windows-test . `
    --progress plain `
    --output type=local,dest=./output

#6 [2/2] COPY test1.txt /sample/
#6 DONE 0.1s

#7 exporting to client directory
#7 copying files 31B
#7 copying files 230.88MB 0.8s done
#7 DONE 0.8s

Checking results, sample is a file instead of a directory:

$ ls -l output/sample
-rw-r--r-- 1 root root 6 Sep 24 08:57 output/sample

Fixing for Linux too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And as for this:

Isn't the issue in https://github.com/moby/buildkit/blob/v0.16.0/util/system/path.go#L75-L76 that is currently linux-only and doesn't handle inputOS .

by the time we get to calling NormalizePath, origPath will already be getting a stripped out /, so there's nothing to keep.

> github.com/moby/buildkit/util/system.NormalizePath() /home/nandaa/dev/oss/buildkit/util/system/path.go:40 (PC: 0x1cfeb6d)
    35: func NormalizePath(parent, newPath, inputOS string, keepSlash bool) (string, error) {
    36:         if inputOS == "" {
    37:                 inputOS = "linux"
    38:         }
    39:
=>  40:         newPath = ToSlash(newPath, inputOS)
    41:         parent = ToSlash(parent, inputOS)
    42:         origPath := newPath
    43:
    44:         if parent == "" {
    45:                 parent = "/"
(dlv) p newPath
"/sample"

It is stripped out at CheckSystemDriveAndRemoveDriveLetter a few steps before, L1787:
image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, NormalizePath expected that there will be a trailing slash, hence the keepSlash bool, but it ends up being stripped away before we call into it.

Copy link
Collaborator Author

@profnandaa profnandaa Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now fixed for both WCOW and cross-building from Linux, PTAL.

@profnandaa profnandaa force-pushed the fix-5249-copy-dir-path branch 3 times, most recently from 36e398e to 1a5289c Compare September 13, 2024 08:21
@profnandaa profnandaa marked this pull request as draft September 13, 2024 08:51
@profnandaa profnandaa marked this pull request as ready for review September 17, 2024 17:25
@profnandaa profnandaa force-pushed the fix-5249-copy-dir-path branch 2 times, most recently from aca0ccb to e8ae74c Compare September 24, 2024 08:27
@profnandaa profnandaa force-pushed the fix-5249-copy-dir-path branch 2 times, most recently from 54b0fbd to 4056da0 Compare September 25, 2024 04:02
@profnandaa
Copy link
Collaborator Author

CI failure seems unrelated?

=== FAIL: client TestClientGatewayIntegration/TestClientGatewayContainerHostNetworkingValidation/worker=containerd/netmode=host (0.97s)
    build_test.go:2133: 
        	Error Trace:	/src/client/build_test.go:2133
        	            				/src/client/build_test.go:2031
        	            				/src/util/testutil/integration/run.go:97
        	            				/src/util/testutil/integration/run.go:211
        	Error:      	Received unexpected error:
        	            	expecting started message, got *moby_buildkit_v1_frontend.ExecMessage_Exit

// and keeps it. See https://github.com/moby/buildkit/issues/5249
// Expects cleanedPath to have gone through filepath.Clean()
// which returns the path with platform specific separators.
func keepTrailingSlash(cleanedPath, origPath, inputOS string) string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Would it make sense to create a CleanPath() function in this package that incorporates keepTrailingSlash()? We could then replace calls to filepath.Clean() with CleanPath()

Perhaps something like: https://go.dev/play/p/lcvDPMnT8xS ? What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that, looks neat, let me do that. Thanks!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps the only modification I'll do is to make our CleanPath take an additional keepSlash bool param, to be consistent with NormalizePath; as much as rn, it's always true.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I've given it a little of some thought, I think we can minimize the surface area to just focus on our adaptation of filepath.Clean to retain the trailing slash. If you don't need to retain the trailing slash, then you just call filepath.Clean. I think this will improve composability and avoid any future surprises.

I have also dropped the inputOS param since the initial intention for it was for the ToSlash function. So we clean and pass it to ToSlash as before.

As for exporting it, I'm not entirely sure. Can wait until the time it will be needed outside?

PTAL.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here's the side-by-side - https://go.dev/play/p/UbsFcgnqvA3

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are the two calls I'm investigating:

$ grep -rn "\.CheckSystemDriveAndRemoveDriveLetter("
solver/llbsolver/file/backend.go:381:   s, err := system.CheckSystemDriveAndRemoveDriveLetter(s, runtime.GOOS)
frontend/dockerfile/dockerfile2llb/convert.go:1517:             f, err := system.CheckSystemDriveAndRemoveDriveLetter(src.Path, d.platform.OS)

I have run both, hard-coding (swapping linux with windows) and the runs are successful for both permutations.

Results:

#6 [2/2] COPY test1.txt /sample/
#6 DONE 0.1s

#7 exporting to client directory
#7 copying files 19.71MB 0.1s
#7 copying files 230.88MB 0.9s done
#7 DONE 0.9s

$ ls output/sample
test1.txt

So, it's all good.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For completeness, also did the reverse on Windows side (swapping windows with linux) and it's successful:

PS> go test -v --run=TestIntegration/TestPreserveDestDirSlash/worker=containerd 
<snip>
=== NAME  TestIntegration
    run.go:431: Skipped on windows
--- SKIP: TestIntegration (34.52s)
    --- PASS: TestIntegration/TestPreserveDestDirSlash/worker=containerd/frontend=builtin (19.11s)
    --- PASS: TestIntegration/TestPreserveDestDirSlash/worker=containerd/frontend=client (15.34s)
PASS
ok      github.com/moby/buildkit/frontend/dockerfile    34.704s

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildkit can build Windows images on Linux. It uses the inputOS to know the target OS. So when buildkitd runs on Linux and is adding a file inside a Windows image, this code will break if the path is a Windows style path of the form pth4 := "\\a\\b\\..\\c\\"

Please see the last examples from here: https://go.dev/play/p/ZnAPJ2__nis

This is the reason I added the call to cleaned := ToSlash(origPath, inputOS) in my previous example.

Copy link
Collaborator Author

@profnandaa profnandaa Oct 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was testing the cross-build case from Linux. Here's using the example path:

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022
COPY test1.txt \\a\\b\\..\\c\\

Build command:

$ docker buildx build --platform windows/amd64 `
    --builder buildkitd-dev --no-cache --tag=windows-test . `
    --progress plain `
    --output type=local,dest=./output

Build results:

#6 [2/2] COPY test1.txt \a\b\..\c\
#6 DONE 0.1s

#7 exporting to client directory
#7 copying files 21.05MB 0.1s
#7 copying files 230.88MB 0.9s done
#7 DONE 0.9s

$ ls output/
a  License.txt  ProgramData  Users  Windows

$ ls output/a/c
test1.txt

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

previous convo here - #5317 (comment)

The call to CheckSystemDriveAndRemoveDriveLetter() does not preserve
the trailing `/` or `\\`. This happens because `filepath.Clean()`
strips away any trailing slashes. For example `/sample/` will be
`\\sample` on Windows and `/sample` on Linux.
This function was mainly written for Windows scenarios, which
have System Drive Letters like C:/, etc.

This was causing cases like `COPY testfile /testdir/` to
be intepreted as `COPY testfile /testdir`, and if `testdir` is
not explictly created before the call, it ends up being treated
as a destination file other than a directory.

Fix this by checking that if we have a trailing `/` or `\\`, we
preserve it after the call to `filepath.Clean()`.

Fixes moby#5249

PS. Also fixed for cross-building from Linux scenario, taking care
for paths like `\\sample\\` that are not changed when run
through `filepath.Clean()`.

Signed-off-by: Anthony Nandaa <profnandaa@gmail.com>
@profnandaa
Copy link
Collaborator Author

PTAL again, addressed the comments. @tonistiigi @gabriel-samfira

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

WCOW: trailing / ignored during COPY if destination dir not present
3 participants