-
Notifications
You must be signed in to change notification settings - Fork 492
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
Atomic writing of files on Windows with a specific security descriptor #3577
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
1a2aab0
Atomic writing of files on Windows with a specific security descriptor
amartinezfayo 69127ee
Rename createFile function to createFileForWriting
amartinezfayo 77932d3
Reorder functions
amartinezfayo 139754c
Fix linter
amartinezfayo cd9846f
Fix linter
amartinezfayo c2e0ea2
Fix typo
amartinezfayo c3fa012
Address PR comments
amartinezfayo fc16e1a
Merge branch 'main' into win-atomic-write-sd
amartinezfayo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
//go:build !windows | ||
// +build !windows | ||
|
||
package diskutil | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/spiffe/spire/test/spiretest" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestAtomicWritePrivateFile(t *testing.T) { | ||
dir := spiretest.TempDir(t) | ||
|
||
tests := []struct { | ||
name string | ||
data []byte | ||
atomicWriteFunc func(string, []byte) error | ||
expectMode os.FileMode | ||
}{ | ||
{ | ||
name: "basic - AtomicWritePrivateFile", | ||
data: []byte("Hello, World"), | ||
atomicWriteFunc: AtomicWritePrivateFile, | ||
expectMode: 0600, | ||
}, | ||
{ | ||
name: "empty - AtomicWritePrivateFile", | ||
data: []byte{}, | ||
atomicWriteFunc: AtomicWritePrivateFile, | ||
expectMode: 0600, | ||
}, | ||
{ | ||
name: "binary - AtomicWritePrivateFile", | ||
data: []byte{0xFF, 0, 0xFF, 0x3D, 0xD8, 0xA9, 0xDC, 0xF0, 0x9F, 0x92, 0xA9}, | ||
atomicWriteFunc: AtomicWritePrivateFile, | ||
expectMode: 0600, | ||
}, | ||
{ | ||
name: "basic - AtomicWritePubliclyReadableFile", | ||
data: []byte("Hello, World"), | ||
atomicWriteFunc: AtomicWritePubliclyReadableFile, | ||
expectMode: 0644, | ||
}, | ||
{ | ||
name: "empty - AtomicWritePubliclyReadableFile", | ||
data: []byte{}, | ||
atomicWriteFunc: AtomicWritePubliclyReadableFile, | ||
expectMode: 0644, | ||
}, | ||
{ | ||
name: "binary - AtomicWritePubliclyReadableFile", | ||
data: []byte{0xFF, 0, 0xFF, 0x3D, 0xD8, 0xA9, 0xDC, 0xF0, 0x9F, 0x92, 0xA9}, | ||
atomicWriteFunc: AtomicWritePubliclyReadableFile, | ||
expectMode: 0644, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
tt := tt | ||
t.Run(tt.name, func(t *testing.T) { | ||
file := filepath.Join(dir, "file") | ||
err := tt.atomicWriteFunc(file, tt.data) | ||
require.NoError(t, err) | ||
|
||
info, err := os.Stat(file) | ||
require.NoError(t, err) | ||
require.EqualValues(t, tt.expectMode, info.Mode()) | ||
|
||
content, err := os.ReadFile(file) | ||
require.NoError(t, err) | ||
require.Equal(t, tt.data, content) | ||
|
||
require.NoError(t, os.Remove(file)) | ||
}) | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
package diskutil | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"syscall" | ||
"unsafe" | ||
|
@@ -17,16 +18,25 @@ const ( | |
movefileWriteThrough = 0x8 | ||
) | ||
|
||
// AtomicWriteFile writes data out. It writes to a temp file first, fsyncs that file, | ||
// then swaps the file in. Rename file using a custom MoveFileEx that uses 'MOVEFILE_WRITE_THROUGH' witch waits until | ||
// file is synced to disk. | ||
func AtomicWriteFile(path string, data []byte, mode os.FileMode) error { | ||
tmpPath := path + ".tmp" | ||
if err := write(tmpPath, data, mode); err != nil { | ||
return err | ||
} | ||
type fileAttribs struct { | ||
pathUTF16Ptr *uint16 | ||
sa *windows.SecurityAttributes | ||
} | ||
|
||
return atomicRename(tmpPath, path) | ||
// AtomicWritePrivateFile writes data out to a private file. | ||
// It writes to a temp file first, fsyncs that file, then swaps the file in. | ||
// Rename file using a custom MoveFileEx that uses 'MOVEFILE_WRITE_THROUGH' | ||
// witch waits until file is synced to disk. | ||
func AtomicWritePrivateFile(path string, data []byte) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: the two new functions in here also share an implementation other than the SDDL; maybe that implementation can be shared? |
||
return atomicWrite(path, data, sddl.PrivateFile) | ||
} | ||
|
||
// AtomicWritePubliclyReadableFile writes data out to a publicly readable file. | ||
// It writes to a temp file first, fsyncs that file, then swaps the file in. | ||
// Rename file using a custom MoveFileEx that uses 'MOVEFILE_WRITE_THROUGH' | ||
// witch waits until file is synced to disk. | ||
func AtomicWritePubliclyReadableFile(path string, data []byte) error { | ||
return atomicWrite(path, data, sddl.PubliclyReadableFile) | ||
} | ||
|
||
func CreateDataDirectory(path string) error { | ||
|
@@ -78,12 +88,25 @@ func MkdirAll(path string, sddl string) error { | |
return nil | ||
} | ||
|
||
func write(tmpPath string, data []byte, mode os.FileMode) error { | ||
file, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) | ||
func atomicWrite(path string, data []byte, sddl string) error { | ||
tmpPath := path + ".tmp" | ||
if err := write(tmpPath, data, sddl); err != nil { | ||
return err | ||
} | ||
|
||
return atomicRename(tmpPath, path) | ||
} | ||
|
||
func write(tmpPath string, data []byte, sddl string) error { | ||
handle, err := createFileForWriting(tmpPath, sddl) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
file := os.NewFile(uintptr(handle), tmpPath) | ||
if file == nil { | ||
return fmt.Errorf("invalid file descriptor for file %q", tmpPath) | ||
} | ||
if _, err := file.Write(data); err != nil { | ||
file.Close() | ||
return err | ||
|
@@ -97,6 +120,25 @@ func write(tmpPath string, data []byte, mode os.FileMode) error { | |
return file.Close() | ||
} | ||
|
||
func createFileForWriting(path string, sddl string) (windows.Handle, error) { | ||
file, err := getFileWithSecurityAttr(path, sddl) | ||
if err != nil { | ||
return windows.InvalidHandle, err | ||
} | ||
handle, err := windows.CreateFile(file.pathUTF16Ptr, | ||
windows.GENERIC_WRITE, | ||
0, | ||
file.sa, | ||
windows.CREATE_ALWAYS, | ||
windows.FILE_ATTRIBUTE_NORMAL, | ||
0) | ||
|
||
if err != nil { | ||
return windows.InvalidHandle, fmt.Errorf("could not create file %q: %w", path, err) | ||
} | ||
return handle, nil | ||
} | ||
|
||
func atomicRename(oldPath, newPath string) error { | ||
if err := rename(oldPath, newPath); err != nil { | ||
return &os.LinkError{ | ||
|
@@ -129,23 +171,35 @@ func rename(oldPath, newPath string) error { | |
// | ||
// In the same way as os.MkDir, errors returned are of type *os.PathError. | ||
func mkdir(path string, sddl string) error { | ||
sa := windows.SecurityAttributes{Length: 0} | ||
sd, err := windows.SecurityDescriptorFromString(sddl) | ||
file, err := getFileWithSecurityAttr(path, sddl) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = windows.CreateDirectory(file.pathUTF16Ptr, file.sa) | ||
if err != nil { | ||
return &os.PathError{Op: "mkdir", Path: path, Err: err} | ||
return fmt.Errorf("could not create directory: %w", err) | ||
} | ||
sa.Length = uint32(unsafe.Sizeof(sa)) | ||
sa.InheritHandle = 1 | ||
sa.SecurityDescriptor = sd | ||
return nil | ||
} | ||
|
||
pathUTF16, err := windows.UTF16PtrFromString(path) | ||
func getFileWithSecurityAttr(path, sddl string) (*fileAttribs, error) { | ||
sd, err := windows.SecurityDescriptorFromString(sddl) | ||
if err != nil { | ||
return &os.PathError{Op: "mkdir", Path: path, Err: err} | ||
return nil, fmt.Errorf("could not convert SDDL %q into a self-relative security descriptor object: %w", sddl, err) | ||
} | ||
|
||
e := windows.CreateDirectory(pathUTF16, &sa) | ||
if e != nil { | ||
return &os.PathError{Op: "mkdir", Path: path, Err: e} | ||
pathUTF16Ptr, err := windows.UTF16PtrFromString(path) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not get pointer to the UTF-16 encoding of path %q: %w", path, err) | ||
} | ||
return nil | ||
|
||
return &fileAttribs{ | ||
pathUTF16Ptr: pathUTF16Ptr, | ||
sa: &windows.SecurityAttributes{ | ||
InheritHandle: 1, | ||
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})), | ||
SecurityDescriptor: sd, | ||
}, | ||
}, nil | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
nit: seems like the two new functions in here have an identical implementation with exception of the mode; maybe that implementation can be shared?