New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Back up and restore SecurityDescriptors on Windows #4708
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for taking so long to review this PR, somehow it always ended up in my "it would take too long to review the PR today" category :-( . I've added quite a few comments below, although most of them are just nitpicks.
The only larger issue is the encoding of the SECURITY_DESCRIPTOR struct. I don't think it's a good idea to store the raw data structure. Instead, I think the PR should use the SDDL encoding, see the comment for more details.
return string(utf16.Decode(displayNameBuffer[:displayBufSize])) | ||
} | ||
|
||
func adjustTokenPrivileges(token windows.Token, releaseAll bool, input *byte, outputSize uint32, output *byte, requiredSize *uint32) (success bool, err error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess those functions are autogenerated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably, these were copied from winio.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've found the source file: https://github.com/microsoft/go-winio/blob/main/zsyscall_windows.go . I don't want to see copied code ever again that does not clearly state where it originates from. It's just ridiculous that I have to search for the original source of these functions.
Omitting that information also forces me to waste time on reviewing known good code.
To make matter worse, there have been a few small changes to those functions (apparently to fix linter warnings), which when compared to the latest upstream version of the autogenerated code seem to be buggy. See my other comments for details.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a little harsh. I wrote this code 2 years ago before I decided to submit it back to restic.
The upstream code was changed only last month - microsoft/go-winio@3c9576c
Eg.
- r0, _, _ := syscall.Syscall(procHcsFormatWritableLayerVhd.Addr(), 1, uintptr(handle), 0, 0)
+ r0, _, _ := syscall.SyscallN(procHcsFormatWritableLayerVhd.Addr(), uintptr(handle))
The changes I made 2 years ago were to fix the deprecations -
Deprecated: Use SyscallN instead.
Extra 0s at the end of SyscallN are ignored and hence this slip wasn't caught, and it also doesn't cause any bugs, though I agree it should be cleaned.
Also, I get your point on including links to the source and will try to do that, this was missed because the code was created from upstream a long time ago.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a little harsh.
Let's just say that I was rather annoyed after noticing that I have to diff the syscall wrappers and search for the corresponding source code.
I wrote this code 2 years ago before I decided to submit it back to restic.
That perfectly explains why the code looks the way it does. Thanks for contributing those changes!
internal/fs/sd_windows.go
Outdated
} | ||
r0, _, e1 := syscall.SyscallN(procAdjustTokenPrivileges.Addr(), uintptr(token), uintptr(_p0), uintptr(unsafe.Pointer(input)), uintptr(outputSize), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(requiredSize))) | ||
success = r0 != 0 | ||
if !success { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The autogenerated code contains if true {
here. Why deviate here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This windows api always returns an error even in case of success, warnings (partial success) and error cases.
Full success - When we call this with admin permissions, it returns DNS_ERROR_RCODE_NO_ERROR (0). This gets translated to errErrorEinval and ultimately in adjustTokenPrivileges, it gets ignored.
Partial success - If we call this api without admin privileges, privileges related to SACLs do not get set and though the api returns success, it returns an error - golang.org/x/sys/windows.ERROR_NOT_ALL_ASSIGNED (1300)
This gets propagated up the call hierarchy and then gets printed as a debug log ("error enabling backup/restore priviledge") in enableRestorePrivilege or enableBackupPrivilege.
Now that we have changed the handling in those functions to print debug log, it will start printing debug logs even in case ERROR_NOT_ALL_ASSIGNED was thrown, which is a warning and in our case is expected in case we run restic without admin permissions. In that case adjustPrivileges will return a PrivilegeError even though other privileges other than SACL access were still set.
Instead of eating the warning on one extreme or throwing a Privilege error on the other extreme, I can handle this warning in adjustPrivileges and add a debug statement -
debug.Log("Not all requested privileges were fully set: %v. AdjustTokenPrivileges returned warning: %v", privileges, err)
This debug statement will get logged when we call restic without admin permissions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The debug.Log is fine.
Judging from https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-adjusttokenprivileges if true {
is in fact the correct variant (even though it looks weird. However, calling errnoErr doesn't really make sense in that case), as GetLastError
can return either ERROR_SUCCESS or ERROR_NOT_ALL_ASSIGNED if adjustTokenPrivileges succeeded.
return string(utf16.Decode(displayNameBuffer[:displayBufSize])) | ||
} | ||
|
||
func adjustTokenPrivileges(token windows.Token, releaseAll bool, input *byte, outputSize uint32, output *byte, requiredSize *uint32) (success bool, err error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've found the source file: https://github.com/microsoft/go-winio/blob/main/zsyscall_windows.go . I don't want to see copied code ever again that does not clearly state where it originates from. It's just ridiculous that I have to search for the original source of these functions.
Omitting that information also forces me to waste time on reviewing known good code.
To make matter worse, there have been a few small changes to those functions (apparently to fix linter warnings), which when compared to the latest upstream version of the autogenerated code seem to be buggy. See my other comments for details.
Change lowerPrivileges from bool to atomic.Bool. Add missing cleanup from upstream go-winio. Add handling for ERROR_NOT_ALL_ASSIGNED warning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Thanks!
What does this PR change? What problem does it solve?
Restic did not back up SecurityDescriptors of files on Windows.
Restic now backs up and restores SecurityDescriptors (which includes owner, group, discretionary access control list (DACL), system access control list (SACL)) when backing up files and folders on Windows. This requires the user to be a member of backup operators or the application must be run as admin.
If that is not the case, only the current user's owner, group and DACL will be backed up and during restore only the DACL of the backed file will be restored while the current user's owner and group will be set during the restore.
We may want to add a backup/restore flag for disabling Security descriptor for Windows as there may be valid cases where users do not want to backup/restore security descriptors of files in Windows.
This can be done as a separate PR.
Was the change previously discussed in an issue or on the forum?
Follow-up to #4611
Checklist
changelog/unreleased/
that describes the changes for our users (see template).gofmt
on the code in all commits.