Skip to content

Commit 9b47baa

Browse files
committed
Fix Windows COPY --parents with SeBackupPrivilege
Fixes copying from Windows container mount roots by using SeBackupPrivilege to handle protected system files ('System Volume Information' and 'WcSandboxState') that cause 'Access is denied' errors when reading file metadata. Implementation: - Uses process-wide EnableProcessPrivileges (not thread-local RunWithPrivileges) - Process-wide elevation required because copy.Copy spawns goroutines in different threads - Matches approach used in exporters (commit e7ceb63) but adapted for multi-threaded context - Simple wrapper around copy.Copy with privilege elevation and restoration Test changes: - Enabled testCopyRelativeParents for Windows with nanoserver:ltsc2022 - Added Windows-specific test implementations for 7 target stages - Enabled testCopyParentsMissingDirectory on Windows - Modified path patterns to include explicit pivot points (/./): - Without pivots, COPY --parents defaults to '/' causing mount root access The access denied errors occur during os.Lstat() when reading file metadata. SeBackupPrivilege allows reading this metadata even with restrictive ACLs on Windows container volumes. Fixes #6635 Signed-off-by: Dawei Wei <[email protected]>
1 parent 975d8f0 commit 9b47baa

File tree

4 files changed

+131
-7
lines changed

4 files changed

+131
-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 := copyWithElevatedPrivileges(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 copyWithElevatedPrivileges(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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package file
22

33
import (
4+
"context"
5+
6+
"github.com/Microsoft/go-winio"
47
"github.com/moby/buildkit/util/windows"
58
"github.com/moby/sys/user"
69
copy "github.com/tonistiigi/fsutil/copy"
@@ -21,3 +24,28 @@ func mapUserToChowner(user *copy.User, _ *user.IdentityMapping) (copy.Chowner, e
2124
return user, nil
2225
}, nil
2326
}
27+
28+
// copyWithElevatedPrivileges wraps copy.Copy to handle Windows protected system folders.
29+
// On Windows, container snapshots mounted to the host filesystem include protected folders
30+
// ("System Volume Information" and "WcSandboxState") at the mount root, which cause "Access is denied"
31+
// errors when attempting to read their metadata. This function uses SeBackupPrivilege to allow
32+
// reading these protected files.
33+
//
34+
// SeBackupPrivilege must be enabled process-wide (not thread-local) because copy.Copy spawns
35+
// goroutines that may execute in different OS threads.
36+
func copyWithElevatedPrivileges(ctx context.Context, srcRoot string, src string, destRoot string, dest string, opt ...copy.Opt) error {
37+
privileges := []string{winio.SeBackupPrivilege}
38+
39+
if err := winio.EnableProcessPrivileges(privileges); err != nil {
40+
// Continue even if privilege elevation fails - it may already be enabled
41+
// or the process may not have permission to enable it
42+
_ = err
43+
}
44+
defer func() {
45+
// Restore previous privilege state
46+
_ = winio.DisableProcessPrivileges(privileges)
47+
}()
48+
49+
// Perform copy with elevated privileges
50+
return copy.Copy(ctx, srcRoot, src, destRoot, dest, opt...)
51+
}

0 commit comments

Comments
 (0)