diff --git a/changelog/unreleased/pull-4980 b/changelog/unreleased/pull-4980 new file mode 100644 index 000000000..264f347fa --- /dev/null +++ b/changelog/unreleased/pull-4980 @@ -0,0 +1,12 @@ +Bugfix: Skip EA processing in Windows for volumes that do not support EA + +Restic was failing to backup files on some windows paths like network +drives because of errors while fetching extended attributes. +Either they return error codes like windows.E_NOT_SET or +windows.ERROR_INVALID_FUNCTION or it results in slower backups. +Restic now completely skips the attempt to fetch extended attributes +for such volumes where it is not supported. + +https://github.com/restic/restic/pull/4980 +https://github.com/restic/restic/issues/4955 +https://github.com/restic/restic/issues/4950 diff --git a/internal/fs/ea_windows.go b/internal/fs/ea_windows.go index 08466c33f..d19a1ee6a 100644 --- a/internal/fs/ea_windows.go +++ b/internal/fs/ea_windows.go @@ -283,3 +283,18 @@ func setFileEA(handle windows.Handle, iosb *ioStatusBlock, buf *uint8, bufLen ui status = ntStatus(r0) return } + +// PathSupportsExtendedAttributes returns true if the path supports extended attributes. +func PathSupportsExtendedAttributes(path string) (supported bool, err error) { + var fileSystemFlags uint32 + utf16Path, err := windows.UTF16PtrFromString(path) + if err != nil { + return false, err + } + err = windows.GetVolumeInformation(utf16Path, nil, 0, nil, nil, &fileSystemFlags, nil, 0) + if err != nil { + return false, err + } + supported = (fileSystemFlags & windows.FILE_SUPPORTS_EXTENDED_ATTRIBUTES) != 0 + return supported, nil +} diff --git a/internal/fs/sd_windows.go b/internal/fs/sd_windows.go index 2da1c5df4..0a73cbe53 100644 --- a/internal/fs/sd_windows.go +++ b/internal/fs/sd_windows.go @@ -11,6 +11,7 @@ import ( "unsafe" "github.com/restic/restic/internal/debug" + "github.com/restic/restic/internal/errors" "golang.org/x/sys/windows" ) @@ -60,6 +61,8 @@ func GetSecurityDescriptor(filePath string) (securityDescriptor *[]byte, err err if err != nil { return nil, fmt.Errorf("get low-level named security info failed with: %w", err) } + } else if errors.Is(err, windows.ERROR_NOT_SUPPORTED) { + return nil, nil } else { return nil, fmt.Errorf("get named security info failed with: %w", err) } diff --git a/internal/restic/node_windows.go b/internal/restic/node_windows.go index 2785e0412..ceb304d0c 100644 --- a/internal/restic/node_windows.go +++ b/internal/restic/node_windows.go @@ -8,6 +8,7 @@ import ( "reflect" "runtime" "strings" + "sync" "syscall" "unsafe" @@ -32,6 +33,15 @@ var ( modAdvapi32 = syscall.NewLazyDLL("advapi32.dll") procEncryptFile = modAdvapi32.NewProc("EncryptFileW") procDecryptFile = modAdvapi32.NewProc("DecryptFileW") + + // eaSupportedVolumesMap is a map of volumes to boolean values indicating if they support extended attributes. + eaSupportedVolumesMap = sync.Map{} +) + +const ( + extendedPathPrefix = `\\?\` + uncPathPrefix = `\\?\UNC\` + globalRootPrefix = `\\?\GLOBALROOT\` ) // mknod is not supported on Windows. @@ -351,32 +361,84 @@ func decryptFile(pathPointer *uint16) error { } // fillGenericAttributes fills in the generic attributes for windows like File Attributes, -// Created time etc. +// Created time and Security Descriptors. +// It also checks if the volume supports extended attributes and stores the result in a map +// so that it does not have to be checked again for subsequent calls for paths in the same volume. func (node *Node) fillGenericAttributes(path string, fi os.FileInfo, stat *statT) (allowExtended bool, err error) { if strings.Contains(filepath.Base(path), ":") { - //Do not process for Alternate Data Streams in Windows + // Do not process for Alternate Data Streams in Windows // Also do not allow processing of extended attributes for ADS. return false, nil } - if !strings.HasSuffix(filepath.Clean(path), `\`) { - // Do not process file attributes and created time for windows directories like - // C:, D: - // Filepath.Clean(path) ends with '\' for Windows root drives only. - var sd *[]byte - if node.Type == "file" || node.Type == "dir" { - if sd, err = fs.GetSecurityDescriptor(path); err != nil { - return true, err - } - } - // Add Windows attributes - node.GenericAttributes, err = WindowsAttrsToGenericAttributes(WindowsAttributes{ - CreationTime: getCreationTime(fi, path), - FileAttributes: &stat.FileAttributes, - SecurityDescriptor: sd, - }) + if strings.HasSuffix(filepath.Clean(path), `\`) { + // filepath.Clean(path) ends with '\' for Windows root volume paths only + // Do not process file attributes, created time and sd for windows root volume paths + // Security descriptors are not supported for root volume paths. + // Though file attributes and created time are supported for root volume paths, + // we ignore them and we do not want to replace them during every restore. + allowExtended, err = checkAndStoreEASupport(path) + if err != nil { + return false, err + } + return allowExtended, nil } - return true, err + + var sd *[]byte + if node.Type == "file" || node.Type == "dir" { + // Check EA support and get security descriptor for file/dir only + allowExtended, err = checkAndStoreEASupport(path) + if err != nil { + return false, err + } + if sd, err = fs.GetSecurityDescriptor(path); err != nil { + return allowExtended, err + } + } + // Add Windows attributes + node.GenericAttributes, err = WindowsAttrsToGenericAttributes(WindowsAttributes{ + CreationTime: getCreationTime(fi, path), + FileAttributes: &stat.FileAttributes, + SecurityDescriptor: sd, + }) + return allowExtended, err +} + +// checkAndStoreEASupport checks if the volume of the path supports extended attributes and stores the result in a map +// If the result is already in the map, it returns the result from the map. +func checkAndStoreEASupport(path string) (isEASupportedVolume bool, err error) { + // Check if it's an extended length path + if strings.HasPrefix(path, uncPathPrefix) { + // Convert \\?\UNC\ extended path to standard path to get the volume name correctly + path = `\\` + path[len(uncPathPrefix):] + } else if strings.HasPrefix(path, extendedPathPrefix) { + //Extended length path prefix needs to be trimmed to get the volume name correctly + path = path[len(extendedPathPrefix):] + } else if strings.HasPrefix(path, globalRootPrefix) { + // EAs are not supported for \\?\GLOBALROOT i.e. VSS snapshots + return false, nil + } else { + // Use the absolute path + path, err = filepath.Abs(path) + if err != nil { + return false, fmt.Errorf("failed to get absolute path: %w", err) + } + } + volumeName := filepath.VolumeName(path) + if volumeName == "" { + return false, nil + } + eaSupportedValue, exists := eaSupportedVolumesMap.Load(volumeName) + if exists { + return eaSupportedValue.(bool), nil + } + + // Add backslash to the volume name to ensure it is a valid path + isEASupportedVolume, err = fs.PathSupportsExtendedAttributes(volumeName + `\`) + if err == nil { + eaSupportedVolumesMap.Store(volumeName, isEASupportedVolume) + } + return isEASupportedVolume, err } // windowsAttrsToGenericAttributes converts the WindowsAttributes to a generic attributes map using reflection