Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 96 additions & 6 deletions frontend/dockerfile/dockerfile_parents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ COPY --parents foo1/foo2/ba* .
}

func testCopyRelativeParents(t *testing.T, sb integration.Sandbox) {
integration.SkipOnPlatform(t, "windows")
f := getFrontend(t, sb)

dockerfile := []byte(`
dockerfile := []byte(integration.UnixOrWindows(
`
FROM alpine AS base
WORKDIR /test
RUN <<eot
Expand Down Expand Up @@ -155,7 +155,61 @@ RUN <<eot
[ -f /out/d/e2/baz ]
[ -f /out/c/d/e/bar ] # via b2
eot
`)
`,
`
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS base
WORKDIR /test
RUN mkdir a && mkdir a\b && mkdir a\b\c && mkdir a\b\c\d && mkdir a\b\c\d\e
RUN mkdir a\b2 && mkdir a\b2\c && mkdir a\b2\c\d && mkdir a\b2\c\d\e
RUN mkdir a\b\c2 && mkdir a\b\c2\d && mkdir a\b\c2\d\e
RUN mkdir a\b\c2\d\e2
RUN cmd /C "echo. > a\b\c\d\foo"
RUN cmd /C "echo. > a\b\c\d\e\bay"
RUN cmd /C "echo. > a\b2\c\d\e\bar"
RUN cmd /C "echo. > a\b\c2\d\e\baz"
RUN cmd /C "echo. > a\b\c2\d\e2\baz"

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS middle
COPY --from=base --parents /test/a/b/./c/d /out/
RUN if not exist \out\c\d\e exit /b 1
RUN if not exist \out\c\d\foo exit /b 1
RUN if exist \out\a exit /b 1
RUN if exist \out\e exit /b 1

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS end
COPY --from=base --parents /test/a/b/c/d/. /out/
RUN if not exist \out\test\a\b\c\d\e exit /b 1
RUN if not exist \out\test\a\b\c\d\foo exit /b 1

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS start
COPY --from=base --parents ./test/a/b/c/d /out/
RUN if not exist \out\test\a\b\c\d\e exit /b 1
RUN if not exist \out\test\a\b\c\d\foo exit /b 1

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS double
COPY --from=base --parents /test/a/./b/./c /out/
RUN if not exist \out\b\c\d\e exit /b 1
RUN if not exist \out\b\c\d\foo exit /b 1

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS wildcard
COPY --from=base --parents /test/a/./*/c /out/
RUN if not exist \out\b\c\d\e exit /b 1
RUN if not exist \out\b2\c\d\e\bar exit /b 1

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS doublewildcard
COPY --from=base --parents /test/a/b*/./c/**/e /out/
RUN if not exist \out\c\d\e exit /b 1
RUN if not exist \out\c\d\e\bay exit /b 1
RUN if not exist \out\c\d\e\bar exit /b 1

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS doubleinputs
COPY --from=base --parents /test/a/b/c*/./d/**/baz /test/a/b*/./c/**/bar /out/
RUN if not exist \out\d\e\baz exit /b 1
RUN if exist \out\d\e\bay exit /b 1
RUN if not exist \out\d\e2\baz exit /b 1
RUN if not exist \out\c\d\e\bar exit /b 1
`,
))

dir := integration.Tmpdir(
t,
Expand All @@ -182,10 +236,10 @@ eot
}

func testCopyParentsMissingDirectory(t *testing.T, sb integration.Sandbox) {
integration.SkipOnPlatform(t, "windows")
f := getFrontend(t, sb)

dockerfile := []byte(`
dockerfile := []byte(integration.UnixOrWindows(
`
FROM alpine AS base
WORKDIR /test
RUN <<eot
Expand Down Expand Up @@ -234,7 +288,43 @@ RUN <<eot
[ ! -d /out/a ]
[ ! -d /out/c* ]
eot
`)
`,
`
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS base
WORKDIR /test
RUN mkdir a && mkdir a\b && mkdir a\b\c && mkdir a\b\c\d && mkdir a\b\c\d\e
RUN cmd /C "echo. > a\b\c\d\foo"
RUN cmd /C "echo. > a\b\c\d\e\bay"

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS normal
COPY --from=base --parents /test/a/b/c/d /out/
RUN if not exist \out\test\a\b\c\d\e exit /b 1
RUN if not exist \out\test\a\b\c\d\e\bay exit /b 1
RUN if exist \out\e exit /b 1
RUN if exist \out\a exit /b 1

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS withpivot
COPY --from=base --parents /test/a/b/./c/d /out/
RUN if not exist \out\c\d\e exit /b 1
RUN if not exist \out\c\d\foo exit /b 1
RUN if exist \out\a exit /b 1
RUN if exist \out\e exit /b 1

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS nonexistentfile
COPY --from=base --parents /test/nonexistent-file /out/

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS wildcard-nonexistent
COPY --from=base --parents /test/a/b2*/c /out/
RUN if not exist \out exit /b 1
RUN if exist \out\a exit /b 1

FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS wildcard-afterpivot
COPY --from=base --parents /test/a/b/./c2* /out/
RUN if not exist \out exit /b 1
RUN if exist \out\a exit /b 1
RUN if exist \out\c exit /b 1
`,
))

dir := integration.Tmpdir(
t,
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ require (
github.com/spdx/tools-golang v0.5.5
github.com/stretchr/testify v1.11.1
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f
github.com/tonistiigi/go-actions-cache v0.0.0-20250626083717-378c5ed1ddd9
github.com/tonistiigi/go-archvariant v1.0.0
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -398,8 +398,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4=
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f h1:MoxeMfHAe5Qj/ySSBfL8A7l1V+hxuluj8owsIEEZipI=
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f h1:Z4NEQ86qFl1mHuCu9gwcE+EYCwDKfXAYXZbdIXyxmEA=
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
github.com/tonistiigi/go-actions-cache v0.0.0-20250626083717-378c5ed1ddd9 h1:GWuTlpuUQBaK6u0R3HwE+eWaQ2aXwHgo8CaXgqtDQZU=
github.com/tonistiigi/go-actions-cache v0.0.0-20250626083717-378c5ed1ddd9/go.mod h1:cD0SB2270BYw6HYKriFn4H6NRLhGj6ytf48YTpsm8LY=
github.com/tonistiigi/go-archvariant v1.0.0 h1:5LC1eDWiBNflnTF1prCiX09yfNHIxDC/aukdhCdTyb0=
Expand Down
2 changes: 1 addition & 1 deletion solver/llbsolver/file/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ func docopy(ctx context.Context, src, dest string, action *pb.FileActionCopy, u
continue
}
}
if err := copy.Copy(ctx, src, s, dest, destPath, opt...); err != nil {
if err := platformCopy(ctx, src, s, dest, destPath, opt...); err != nil {
return errors.WithStack(err)
}
}
Expand Down
6 changes: 6 additions & 0 deletions solver/llbsolver/file/backend_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package file

import (
"context"

"github.com/moby/sys/user"
"github.com/pkg/errors"
copy "github.com/tonistiigi/fsutil/copy"
Expand Down Expand Up @@ -41,3 +43,7 @@ func mapUserToChowner(user *copy.User, idmap *user.IdentityMapping) (copy.Chowne
return &u, nil
}, nil
}

func platformCopy(ctx context.Context, srcRoot string, src string, destRoot string, dest string, opt ...copy.Opt) error {
return copy.Copy(ctx, srcRoot, src, destRoot, dest, opt...)
}
18 changes: 18 additions & 0 deletions solver/llbsolver/file/backend_windows.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package file

import (
"context"
"path/filepath"

"github.com/moby/buildkit/util/windows"
"github.com/moby/sys/user"
copy "github.com/tonistiigi/fsutil/copy"
Expand All @@ -21,3 +24,18 @@ func mapUserToChowner(user *copy.User, _ *user.IdentityMapping) (copy.Chowner, e
return user, nil
}, nil
}

// platformCopy wraps copy.Copy to exclude Windows protected system folders.
// On Windows, container snapshots mounted to the host filesystem include protected folders
// ("System Volume Information" and "WcSandboxState") at the mount root, which cause "Access is denied"
// errors. With the fsutil fix, these are excluded before os.Lstat() is called.
func platformCopy(ctx context.Context, srcRoot string, src string, destRoot string, dest string, opt ...copy.Opt) error {
// Only exclude protected folders when copying from the mount root.
if filepath.Clean(src) == string(filepath.Separator) {
opt = append(opt,
copy.WithExcludePattern("System Volume Information"),
copy.WithExcludePattern("WcSandboxState"),
)
}
return copy.Copy(ctx, srcRoot, src, destRoot, dest, opt...)
}
54 changes: 54 additions & 0 deletions solver/llbsolver/file/backend_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//go:build windows

package file

import (
"context"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
)

func TestPlatformCopy_RootOnlyProtectedExcludes(t *testing.T) {
ctx := context.Background()

srcRoot := t.TempDir()
destRoot := t.TempDir()

// Root has protected folders + a normal folder.
require.NoError(t, os.MkdirAll(filepath.Join(srcRoot, "foo"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(srcRoot, "foo", "ok.txt"), []byte("ok"), 0o644))

require.NoError(t, os.MkdirAll(filepath.Join(srcRoot, "System Volume Information"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(srcRoot, "System Volume Information", "root.txt"), []byte("root"), 0o644))
require.NoError(t, os.MkdirAll(filepath.Join(srcRoot, "WcSandboxState"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(srcRoot, "WcSandboxState", "root.txt"), []byte("root"), 0o644))

// Copying from mount root should exclude protected folders.
require.NoError(t, platformCopy(ctx, srcRoot, "/", destRoot, "/"))

_, err := os.Stat(filepath.Join(destRoot, "foo", "ok.txt"))
require.NoError(t, err)

_, err = os.Stat(filepath.Join(destRoot, "System Volume Information"))
require.Error(t, err)
require.True(t, os.IsNotExist(err))

_, err = os.Stat(filepath.Join(destRoot, "WcSandboxState"))
require.Error(t, err)
require.True(t, os.IsNotExist(err))

// Copying from a subdir should not add those excludes.
destRoot2 := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(srcRoot, "foo", "System Volume Information"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(srcRoot, "foo", "System Volume Information", "nested.txt"), []byte("nested"), 0o644))

require.NoError(t, platformCopy(ctx, srcRoot, "/foo", destRoot2, "/"))

// Depending on copy semantics, the directory may land at dest root or under foo.
_, err1 := os.Stat(filepath.Join(destRoot2, "System Volume Information", "nested.txt"))
_, err2 := os.Stat(filepath.Join(destRoot2, "foo", "System Volume Information", "nested.txt"))
require.True(t, err1 == nil || err2 == nil, "expected nested protected folder to be copied on non-root copy")
}
62 changes: 52 additions & 10 deletions vendor/github.com/tonistiigi/fsutil/copy/copy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading