Merge pull request #4980 from zmanda/unsupported-ea-handling

Skip ExtendedAttribute processing in Windows for volumes that do not support EA
This commit is contained in:
Michael Eischer 2024-08-10 17:11:12 +00:00 committed by GitHub
commit 86390b453d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 111 additions and 19 deletions

View file

@ -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

View file

@ -283,3 +283,18 @@ func setFileEA(handle windows.Handle, iosb *ioStatusBlock, buf *uint8, bufLen ui
status = ntStatus(r0) status = ntStatus(r0)
return 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
}

View file

@ -11,6 +11,7 @@ import (
"unsafe" "unsafe"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
@ -60,6 +61,8 @@ func GetSecurityDescriptor(filePath string) (securityDescriptor *[]byte, err err
if err != nil { if err != nil {
return nil, fmt.Errorf("get low-level named security info failed with: %w", err) 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 { } else {
return nil, fmt.Errorf("get named security info failed with: %w", err) return nil, fmt.Errorf("get named security info failed with: %w", err)
} }

View file

@ -8,6 +8,7 @@ import (
"reflect" "reflect"
"runtime" "runtime"
"strings" "strings"
"sync"
"syscall" "syscall"
"unsafe" "unsafe"
@ -32,6 +33,15 @@ var (
modAdvapi32 = syscall.NewLazyDLL("advapi32.dll") modAdvapi32 = syscall.NewLazyDLL("advapi32.dll")
procEncryptFile = modAdvapi32.NewProc("EncryptFileW") procEncryptFile = modAdvapi32.NewProc("EncryptFileW")
procDecryptFile = modAdvapi32.NewProc("DecryptFileW") 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. // 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, // 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) { func (node *Node) fillGenericAttributes(path string, fi os.FileInfo, stat *statT) (allowExtended bool, err error) {
if strings.Contains(filepath.Base(path), ":") { 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. // Also do not allow processing of extended attributes for ADS.
return false, nil return false, nil
} }
if !strings.HasSuffix(filepath.Clean(path), `\`) {
// Do not process file attributes and created time for windows directories like if strings.HasSuffix(filepath.Clean(path), `\`) {
// C:, D: // filepath.Clean(path) ends with '\' for Windows root volume paths only
// Filepath.Clean(path) ends with '\' for Windows root drives only. // Do not process file attributes, created time and sd for windows root volume paths
var sd *[]byte // Security descriptors are not supported for root volume paths.
if node.Type == "file" || node.Type == "dir" { // Though file attributes and created time are supported for root volume paths,
if sd, err = fs.GetSecurityDescriptor(path); err != nil { // we ignore them and we do not want to replace them during every restore.
return true, err allowExtended, err = checkAndStoreEASupport(path)
if err != nil {
return false, err
} }
return allowExtended, nil
} }
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 // Add Windows attributes
node.GenericAttributes, err = WindowsAttrsToGenericAttributes(WindowsAttributes{ node.GenericAttributes, err = WindowsAttrsToGenericAttributes(WindowsAttributes{
CreationTime: getCreationTime(fi, path), CreationTime: getCreationTime(fi, path),
FileAttributes: &stat.FileAttributes, FileAttributes: &stat.FileAttributes,
SecurityDescriptor: sd, 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)
} }
return true, 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 // windowsAttrsToGenericAttributes converts the WindowsAttributes to a generic attributes map using reflection