11package file
22
33import (
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+
922func 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