|
|
@@ -0,0 +1,146 @@
|
|
|
+// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
+// SPDX-License-Identifier: BSD-3-Clause
|
|
|
+
|
|
|
+package atomicfile
|
|
|
+
|
|
|
+import (
|
|
|
+ "os"
|
|
|
+ "testing"
|
|
|
+ "unsafe"
|
|
|
+
|
|
|
+ "golang.org/x/sys/windows"
|
|
|
+)
|
|
|
+
|
|
|
+var _SECURITY_RESOURCE_MANAGER_AUTHORITY = windows.SidIdentifierAuthority{[6]byte{0, 0, 0, 0, 0, 9}}
|
|
|
+
|
|
|
+// makeRandomSID generates a SID derived from a v4 GUID.
|
|
|
+// This is basically the same algorithm used by browser sandboxes for generating
|
|
|
+// random SIDs.
|
|
|
+func makeRandomSID() (*windows.SID, error) {
|
|
|
+ guid, err := windows.GenerateGUID()
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ rids := *((*[4]uint32)(unsafe.Pointer(&guid)))
|
|
|
+
|
|
|
+ var pSID *windows.SID
|
|
|
+ if err := windows.AllocateAndInitializeSid(&_SECURITY_RESOURCE_MANAGER_AUTHORITY, 4, rids[0], rids[1], rids[2], rids[3], 0, 0, 0, 0, &pSID); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ defer windows.FreeSid(pSID)
|
|
|
+
|
|
|
+ // Make a copy that lives on the Go heap
|
|
|
+ return pSID.Copy()
|
|
|
+}
|
|
|
+
|
|
|
+func getExistingFileSD(name string) (*windows.SECURITY_DESCRIPTOR, error) {
|
|
|
+ const infoFlags = windows.DACL_SECURITY_INFORMATION
|
|
|
+ return windows.GetNamedSecurityInfo(name, windows.SE_FILE_OBJECT, infoFlags)
|
|
|
+}
|
|
|
+
|
|
|
+func getExistingFileDACL(name string) (*windows.ACL, error) {
|
|
|
+ sd, err := getExistingFileSD(name)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ dacl, _, err := sd.DACL()
|
|
|
+ return dacl, err
|
|
|
+}
|
|
|
+
|
|
|
+func addDenyACEForRandomSID(dacl *windows.ACL) (*windows.ACL, error) {
|
|
|
+ randomSID, err := makeRandomSID()
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ randomSIDTrustee := windows.TRUSTEE{nil, windows.NO_MULTIPLE_TRUSTEE,
|
|
|
+ windows.TRUSTEE_IS_SID, windows.TRUSTEE_IS_UNKNOWN,
|
|
|
+ windows.TrusteeValueFromSID(randomSID)}
|
|
|
+
|
|
|
+ entries := []windows.EXPLICIT_ACCESS{
|
|
|
+ {
|
|
|
+ windows.GENERIC_ALL,
|
|
|
+ windows.DENY_ACCESS,
|
|
|
+ windows.NO_INHERITANCE,
|
|
|
+ randomSIDTrustee,
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ return windows.ACLFromEntries(entries, dacl)
|
|
|
+}
|
|
|
+
|
|
|
+func setExistingFileDACL(name string, dacl *windows.ACL) error {
|
|
|
+ return windows.SetNamedSecurityInfo(name, windows.SE_FILE_OBJECT,
|
|
|
+ windows.DACL_SECURITY_INFORMATION, nil, nil, dacl, nil)
|
|
|
+}
|
|
|
+
|
|
|
+// makeOrigFileWithCustomDACL creates a new, temporary file with a custom
|
|
|
+// DACL that we can check for later. It returns the name of the temporary
|
|
|
+// file and the security descriptor for the file in SDDL format.
|
|
|
+func makeOrigFileWithCustomDACL() (name, sddl string, err error) {
|
|
|
+ f, err := os.CreateTemp("", "foo*.tmp")
|
|
|
+ if err != nil {
|
|
|
+ return "", "", err
|
|
|
+ }
|
|
|
+ name = f.Name()
|
|
|
+ if err := f.Close(); err != nil {
|
|
|
+ return "", "", err
|
|
|
+ }
|
|
|
+ f = nil
|
|
|
+ defer func() {
|
|
|
+ if err != nil {
|
|
|
+ os.Remove(name)
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ dacl, err := getExistingFileDACL(name)
|
|
|
+ if err != nil {
|
|
|
+ return "", "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add a harmless, deny-only ACE for a random SID that isn't used for anything
|
|
|
+ // (but that we can check for later).
|
|
|
+ dacl, err = addDenyACEForRandomSID(dacl)
|
|
|
+ if err != nil {
|
|
|
+ return "", "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := setExistingFileDACL(name, dacl); err != nil {
|
|
|
+ return "", "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ sd, err := getExistingFileSD(name)
|
|
|
+ if err != nil {
|
|
|
+ return "", "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ return name, sd.String(), nil
|
|
|
+}
|
|
|
+
|
|
|
+func TestPreserveSecurityInfo(t *testing.T) {
|
|
|
+ // Make a test file with a custom ACL.
|
|
|
+ origFileName, want, err := makeOrigFileWithCustomDACL()
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("makeOrigFileWithCustomDACL returned %v", err)
|
|
|
+ }
|
|
|
+ t.Cleanup(func() {
|
|
|
+ os.Remove(origFileName)
|
|
|
+ })
|
|
|
+
|
|
|
+ if err := WriteFile(origFileName, []byte{}, 0); err != nil {
|
|
|
+ t.Fatalf("WriteFile returned %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // We expect origFileName's security descriptor to be unchanged despite
|
|
|
+ // the WriteFile call.
|
|
|
+ sd, err := getExistingFileSD(origFileName)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("getExistingFileSD(%q) returned %v", origFileName, err)
|
|
|
+ }
|
|
|
+
|
|
|
+ if got := sd.String(); got != want {
|
|
|
+ t.Errorf("security descriptor comparison failed: got %q, want %q", got, want)
|
|
|
+ }
|
|
|
+}
|