Skip to content

Commit a5aebfe

Browse files
committed
Fix Windows copy operations with protected system files at mount roots
Fixes copying from Windows container mount roots by detecting and skipping protected system files ('System Volume Information' and 'WcSandboxState') that cause 'Access is denied' errors. Changes: - Added platformCopy wrapper for Windows copy operations - Implemented manual directory enumeration to skip protected files - Enabled test case testCopyRelativeParents and testCopyParentsMissingDirectory on Windows The fix is Windows-specific and only activates when copying from mount roots where protected files exist. All other copy operations use the standard path. Issue: Resolves #6635 [v0.26] WCOW: Fix COPY --parents tests Signed-off-by: Dawei Wei <[email protected]>
1 parent 3cdccce commit a5aebfe

File tree

4 files changed

+252
-7
lines changed

4 files changed

+252
-7
lines changed

frontend/dockerfile/dockerfile_parents_test.go

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ COPY --parents foo1/foo2/ba* .
7676
}
7777

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

82-
dockerfile := []byte(`
81+
dockerfile := []byte(integration.UnixOrWindows(
82+
`
8383
FROM alpine AS base
8484
WORKDIR /test
8585
RUN <<eot
@@ -155,7 +155,61 @@ RUN <<eot
155155
[ -f /out/d/e2/baz ]
156156
[ -f /out/c/d/e/bar ] # via b2
157157
eot
158-
`)
158+
`,
159+
`
160+
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS base
161+
WORKDIR /test
162+
RUN mkdir a && mkdir a\b && mkdir a\b\c && mkdir a\b\c\d && mkdir a\b\c\d\e
163+
RUN mkdir a\b2 && mkdir a\b2\c && mkdir a\b2\c\d && mkdir a\b2\c\d\e
164+
RUN mkdir a\b\c2 && mkdir a\b\c2\d && mkdir a\b\c2\d\e
165+
RUN mkdir a\b\c2\d\e2
166+
RUN cmd /C "echo. > a\b\c\d\foo"
167+
RUN cmd /C "echo. > a\b\c\d\e\bay"
168+
RUN cmd /C "echo. > a\b2\c\d\e\bar"
169+
RUN cmd /C "echo. > a\b\c2\d\e\baz"
170+
RUN cmd /C "echo. > a\b\c2\d\e2\baz"
171+
172+
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS middle
173+
COPY --from=base --parents /test/a/b/./c/d /out/
174+
RUN if not exist \out\c\d\e exit /b 1
175+
RUN if not exist \out\c\d\foo exit /b 1
176+
RUN if exist \out\a exit /b 1
177+
RUN if exist \out\e exit /b 1
178+
179+
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS end
180+
COPY --from=base --parents /test/a/b/c/d/. /out/
181+
RUN if not exist \out\test\a\b\c\d\e exit /b 1
182+
RUN if not exist \out\test\a\b\c\d\foo exit /b 1
183+
184+
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS start
185+
COPY --from=base --parents ./test/a/b/c/d /out/
186+
RUN if not exist \out\test\a\b\c\d\e exit /b 1
187+
RUN if not exist \out\test\a\b\c\d\foo exit /b 1
188+
189+
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS double
190+
COPY --from=base --parents /test/a/./b/./c /out/
191+
RUN if not exist \out\b\c\d\e exit /b 1
192+
RUN if not exist \out\b\c\d\foo exit /b 1
193+
194+
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS wildcard
195+
COPY --from=base --parents /test/a/./*/c /out/
196+
RUN if not exist \out\b\c\d\e exit /b 1
197+
RUN if not exist \out\b2\c\d\e\bar exit /b 1
198+
199+
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS doublewildcard
200+
COPY --from=base --parents /test/a/b*/./c/**/e /out/
201+
RUN if not exist \out\c\d\e exit /b 1
202+
RUN if not exist \out\c\d\e\bay exit /b 1
203+
RUN if not exist \out\c\d\e\bar exit /b 1
204+
205+
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS doubleinputs
206+
COPY --from=base --parents /test/a/b/c*/./d/**/baz /test/a/b*/./c/**/bar /out/
207+
RUN if not exist \out\d\e\baz exit /b 1
208+
RUN if exist \out\d\e\bay exit /b 1
209+
RUN if not exist \out\d\e2\baz exit /b 1
210+
RUN if not exist \out\c\d\e\bar exit /b 1
211+
`,
212+
))
159213

160214
dir := integration.Tmpdir(
161215
t,
@@ -182,10 +236,10 @@ eot
182236
}
183237

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

188-
dockerfile := []byte(`
241+
dockerfile := []byte(integration.UnixOrWindows(
242+
`
189243
FROM alpine AS base
190244
WORKDIR /test
191245
RUN <<eot
@@ -234,7 +288,43 @@ RUN <<eot
234288
[ ! -d /out/a ]
235289
[ ! -d /out/c* ]
236290
eot
237-
`)
291+
`,
292+
`
293+
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS base
294+
WORKDIR /test
295+
RUN mkdir a && mkdir a\b && mkdir a\b\c && mkdir a\b\c\d && mkdir a\b\c\d\e
296+
RUN cmd /C "echo. > a\b\c\d\foo"
297+
RUN cmd /C "echo. > a\b\c\d\e\bay"
298+
299+
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS normal
300+
COPY --from=base --parents /test/a/b/c/d /out/
301+
RUN if not exist \out\test\a\b\c\d\e exit /b 1
302+
RUN if not exist \out\test\a\b\c\d\e\bay exit /b 1
303+
RUN if exist \out\e exit /b 1
304+
RUN if exist \out\a exit /b 1
305+
306+
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS withpivot
307+
COPY --from=base --parents /test/a/b/./c/d /out/
308+
RUN if not exist \out\c\d\e exit /b 1
309+
RUN if not exist \out\c\d\foo exit /b 1
310+
RUN if exist \out\a exit /b 1
311+
RUN if exist \out\e exit /b 1
312+
313+
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS nonexistentfile
314+
COPY --from=base --parents /test/nonexistent-file /out/
315+
316+
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS wildcard-nonexistent
317+
COPY --from=base --parents /test/a/b2*/c /out/
318+
RUN if not exist \out exit /b 1
319+
RUN if exist \out\a exit /b 1
320+
321+
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 AS wildcard-afterpivot
322+
COPY --from=base --parents /test/a/b/./c2* /out/
323+
RUN if not exist \out exit /b 1
324+
RUN if exist \out\a exit /b 1
325+
RUN if exist \out\c exit /b 1
326+
`,
327+
))
238328

239329
dir := integration.Tmpdir(
240330
t,

solver/llbsolver/file/backend.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ func docopy(ctx context.Context, src, dest string, action *pb.FileActionCopy, u
261261
continue
262262
}
263263
}
264-
if err := copy.Copy(ctx, src, s, dest, destPath, opt...); err != nil {
264+
if err := platformCopy(ctx, src, s, dest, destPath, opt...); err != nil {
265265
return errors.WithStack(err)
266266
}
267267
}

solver/llbsolver/file/backend_unix.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
package file
44

55
import (
6+
"context"
7+
68
"github.com/moby/sys/user"
79
"github.com/pkg/errors"
810
copy "github.com/tonistiigi/fsutil/copy"
@@ -41,3 +43,7 @@ func mapUserToChowner(user *copy.User, idmap *user.IdentityMapping) (copy.Chowne
4143
return &u, nil
4244
}, nil
4345
}
46+
47+
func platformCopy(ctx context.Context, srcRoot string, src string, destRoot string, dest string, opt ...copy.Opt) error {
48+
return copy.Copy(ctx, srcRoot, src, destRoot, dest, opt...)
49+
}

solver/llbsolver/file/backend_windows.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
package file
22

33
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
9+
"github.com/containerd/continuity/fs"
410
"github.com/moby/buildkit/util/windows"
511
"github.com/moby/sys/user"
612
copy "github.com/tonistiigi/fsutil/copy"
713
)
814

15+
// windowsProtectedFiles contains Windows system files/folders that must be skipped
16+
// during copy operations to avoid "Access denied" errors.
17+
var windowsProtectedFiles = map[string]struct{}{
18+
"system volume information": {},
19+
"wcsandboxstate": {},
20+
}
21+
922
func mapUserToChowner(user *copy.User, _ *user.IdentityMapping) (copy.Chowner, error) {
1023
if user == nil || user.SID == "" {
1124
return func(old *copy.User) (*copy.User, error) {
@@ -21,3 +34,139 @@ func mapUserToChowner(user *copy.User, _ *user.IdentityMapping) (copy.Chowner, e
2134
return user, nil
2235
}, nil
2336
}
37+
38+
// platformCopy wraps copy.Copy to handle Windows protected system folders.
39+
// On Windows, container snapshots mounted to the host filesystem include protected folders
40+
// ("System Volume Information" and "WcSandboxState") at the mount root, which cause "Access is denied"
41+
// errors. When copying from the mount root, we manually enumerate and skip these folders.
42+
func platformCopy(ctx context.Context, srcRoot string, src string, destRoot string, dest string, opt ...copy.Opt) error {
43+
// Resolve the source path to check if we're copying from the mount root
44+
srcPath, err := fs.RootPath(srcRoot, src)
45+
if err != nil {
46+
return err
47+
}
48+
49+
// Check if copying from mount root where protected folders exist
50+
if filepath.Clean(srcPath) == filepath.Clean(srcRoot) {
51+
// Check if any protected files exist (indicates a Windows mount root)
52+
for protectedFile := range windowsProtectedFiles {
53+
protectedPath := filepath.Join(srcRoot, protectedFile)
54+
if _, err := os.Stat(protectedPath); err == nil {
55+
// Use manual enumeration to skip protected folders
56+
return copyByEnumeratingChildren(ctx, srcRoot, src, destRoot, dest, opt...)
57+
}
58+
}
59+
}
60+
// Normal case - use standard copy
61+
return copy.Copy(ctx, srcRoot, src, destRoot, dest, opt...)
62+
}
63+
64+
// copyByEnumeratingChildren manually enumerates the root directory and copies each child,
65+
// skipping Windows protected files. This is necessary because copy.Copy calls os.Lstat
66+
// before checking exclude patterns, which causes "Access denied" errors.
67+
//
68+
// When IncludePatterns are present, this function adjusts them to be relative to each
69+
// child being copied. For example, if copying from "/" with pattern "test/a/b/c/d",
70+
// when copying the "test" child, the pattern is adjusted to "a/b/c/d".
71+
func copyByEnumeratingChildren(ctx context.Context, srcRoot string, src string, destRoot string, dest string, opt ...copy.Opt) error {
72+
// Extract CopyInfo to access IncludePatterns that control which files to copy
73+
ci := copy.CopyInfo{}
74+
for _, o := range opt {
75+
o(&ci)
76+
}
77+
78+
// Resolve the actual filesystem path we're copying from
79+
srcPath, err := fs.RootPath(srcRoot, src)
80+
if err != nil {
81+
return err
82+
}
83+
84+
// Enumerate all entries at the root level (before os.Lstat can fail on protected files)
85+
entries, err := os.ReadDir(srcPath)
86+
if err != nil {
87+
return err
88+
}
89+
90+
// Resolve destination path
91+
destPath, err := fs.RootPath(destRoot, dest)
92+
if err != nil {
93+
return err
94+
}
95+
96+
// Create the destination directory with same permissions as source
97+
srcInfo, err := os.Lstat(srcPath)
98+
if err != nil {
99+
return err
100+
}
101+
if srcInfo.IsDir() {
102+
if err := os.MkdirAll(destPath, srcInfo.Mode()); err != nil && !os.IsExist(err) {
103+
return err
104+
}
105+
}
106+
107+
// Process each child entry individually
108+
for _, entry := range entries {
109+
name := entry.Name()
110+
111+
// Skip protected files that would cause "Access denied" errors
112+
if _, isProtected := windowsProtectedFiles[strings.ToLower(name)]; isProtected {
113+
continue
114+
}
115+
116+
// Build source and destination paths for this child
117+
// Handle special case where src is root ("/", ".", or "")
118+
childSrc := filepath.Join(src, name)
119+
if src == "/" || src == "." || src == "" {
120+
childSrc = "/" + name
121+
}
122+
childDest := filepath.Join(dest, name)
123+
124+
// Adjust patterns to be relative to this child's path
125+
// E.g., pattern "test/a/b" becomes "a/b" when copying child "test"
126+
adjustedIncludePatterns := adjustIncludePatternsForChild(ci.IncludePatterns, name)
127+
128+
// Determine whether to copy this child based on patterns
129+
var childOpts []copy.Opt
130+
if len(adjustedIncludePatterns) > 0 {
131+
// Patterns match this child - use adjusted patterns
132+
childCi := ci
133+
childCi.IncludePatterns = adjustedIncludePatterns
134+
childOpts = append(childOpts, copy.WithCopyInfo(childCi))
135+
} else if len(ci.IncludePatterns) == 0 {
136+
// No filtering needed - copy everything
137+
childOpts = opt
138+
} else {
139+
// Patterns specified but don't match this child - skip it
140+
continue
141+
}
142+
143+
// Recursively copy this child using standard copy.Copy
144+
// (safe now because we've already enumerated past the protected files)
145+
if err := copy.Copy(ctx, srcRoot, childSrc, destRoot, childDest, childOpts...); err != nil {
146+
return err
147+
}
148+
}
149+
150+
return nil
151+
}
152+
153+
// adjustIncludePatternsForChild adjusts include patterns to be relative to a child directory.
154+
// When copying from "/" with pattern "test/a/b/c" and processing child "test",
155+
// the pattern is adjusted to "a/b/c" so it matches correctly under the new source root.
156+
func adjustIncludePatternsForChild(patterns []string, childName string) []string {
157+
var adjusted []string
158+
prefix := childName + "/"
159+
160+
for _, pattern := range patterns {
161+
// Remove the child name prefix from the pattern
162+
// Example: "test/a/b/c" → "a/b/c" for child "test"
163+
if adjustedPattern, ok := strings.CutPrefix(pattern, prefix); ok {
164+
adjusted = append(adjusted, adjustedPattern)
165+
} else if pattern == childName {
166+
// Pattern exactly matches this child, include everything under it
167+
adjusted = append(adjusted, "**")
168+
}
169+
}
170+
171+
return adjusted
172+
}

0 commit comments

Comments
 (0)