fix(safeio): retain private file parent handles

This commit is contained in:
2026-08-18 08:51:59 +02:00
parent 0b77d1e850
commit e7a7f4f066
11 changed files with 359 additions and 212 deletions
+44 -57
View File
@@ -3,7 +3,6 @@
package safeio
import (
"errors"
"os"
"path/filepath"
"runtime"
@@ -15,38 +14,37 @@ import (
func createPrivateDirectory(path string) error {
parents, target, err := openCanonicalWindowsParent(path)
if err != nil {
if err != nil || parents == nil || len(parents.handles) == 0 {
if parents != nil {
parents.Close()
}
return ErrUnsafeFile
}
defer parents.Close()
security, err := newOwnerOnlySecurityDescriptor()
if err != nil {
return ErrUnsafeFile
}
defer security.Close()
attributes := &windows.SecurityAttributes{
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
SecurityDescriptor: security.descriptor,
}
err = windows.CreateDirectory(windows.StringToUTF16Ptr(filepath.Join(parents.directory, target)), attributes)
runtime.KeepAlive(security)
if errors.Is(err, windows.ERROR_ALREADY_EXISTS) {
handle, err := createWindowsRelativePrivateDirectory(parents.handles[len(parents.handles)-1], target)
if isWindowsRelativeCollision(err) {
return os.ErrExist
}
if err != nil {
return ErrUnsafeFile
}
return ValidatePrivateDirectory(path)
if err := windows.CloseHandle(handle); err != nil {
return ErrUnsafeFile
}
return nil
}
// ProtectPrivateDirectory sets a protected DACL containing only the current owner.
func ProtectPrivateDirectory(path string) error {
parents, target, err := openCanonicalWindowsParent(path)
if err != nil {
if err != nil || parents == nil || len(parents.handles) == 0 {
if parents != nil {
parents.Close()
}
return ErrUnsafeFile
}
defer parents.Close()
handle, err := openWindowsComponentWithAccess(filepath.Join(parents.directory, target), true, windows.GENERIC_READ|windows.WRITE_DAC|windows.WRITE_OWNER)
handle, err := openWindowsRelativeComponent(parents.handles[len(parents.handles)-1], target, true, windows.GENERIC_READ|windows.WRITE_DAC|windows.WRITE_OWNER)
if err != nil {
return ErrUnsafeFile
}
@@ -60,11 +58,14 @@ func ProtectPrivateDirectory(path string) error {
// ValidatePrivateDirectory requires a canonical directory protected for its current owner only.
func ValidatePrivateDirectory(path string) error {
parents, target, err := openCanonicalWindowsParent(path)
if err != nil {
if err != nil || parents == nil || len(parents.handles) == 0 {
if parents != nil {
parents.Close()
}
return ErrUnsafeFile
}
defer parents.Close()
handle, err := openWindowsComponent(filepath.Join(parents.directory, target), true)
handle, err := openWindowsRelativeComponent(parents.handles[len(parents.handles)-1], target, true, windows.GENERIC_READ)
if err != nil {
return ErrUnsafeFile
}
@@ -78,11 +79,14 @@ func ValidatePrivateDirectory(path string) error {
// ProtectPrivateRegular sets a protected DACL containing only the current owner.
func ProtectPrivateRegular(path string) error {
parents, target, err := openCanonicalWindowsParent(path)
if err != nil {
if err != nil || parents == nil || len(parents.handles) == 0 {
if parents != nil {
parents.Close()
}
return ErrUnsafeFile
}
defer parents.Close()
handle, err := openWindowsComponentWithAccess(filepath.Join(parents.directory, target), false, windows.GENERIC_READ|windows.WRITE_DAC|windows.WRITE_OWNER)
handle, err := openWindowsRelativeComponent(parents.handles[len(parents.handles)-1], target, false, windows.GENERIC_READ|windows.WRITE_DAC|windows.WRITE_OWNER)
if err != nil {
return ErrUnsafeFile
}
@@ -109,62 +113,45 @@ func createCanonicalNewPrivateParentReadWriteFile(path string, mode os.FileMode)
func createCanonicalNewFile(path string, mode os.FileMode, requirePrivateParent bool, access uint32) (*os.File, error) {
parents, target, err := openCanonicalWindowsParent(path)
if err != nil || len(parents.handles) == 0 || (requirePrivateParent && validateOwnerOnlyDACL(parents.handles[len(parents.handles)-1]) != nil) {
if err != nil || parents == nil || len(parents.handles) == 0 || (requirePrivateParent && validateOwnerOnlyDACL(parents.handles[len(parents.handles)-1]) != nil) {
if parents != nil {
parents.Close()
}
return nil, ErrUnsafeFile
}
defer parents.Close()
security, err := newOwnerOnlySecurityDescriptor()
if err != nil {
return nil, ErrUnsafeFile
}
defer security.Close()
attributes := &windows.SecurityAttributes{
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
SecurityDescriptor: security.descriptor,
}
handle, err := windows.CreateFile(
windows.StringToUTF16Ptr(filepath.Join(parents.directory, target)),
access,
windowsRetainedHandleShareMode,
attributes,
windows.CREATE_NEW,
windows.FILE_ATTRIBUTE_NORMAL,
0,
)
runtime.KeepAlive(security)
NotifyPrivateDirectoryTestHookForTest("after-canonical-private-file-parent-open")
// mode remains accepted for the existing helper contract; Windows installs the owner-only
// DACL in the NtCreateFile call below rather than relying on inherited file attributes.
_ = mode
value, err := createWindowsPrivateRegularAtWithAccess(parents.handles[len(parents.handles)-1], target, access)
if err != nil {
return nil, err
}
if err := validateOwnerOnlyDACL(handle); err != nil {
_ = windows.CloseHandle(handle)
_ = os.Remove(path)
return nil, ErrUnsafeFile
}
file := os.NewFile(uintptr(handle), "tht-safeio-private")
file := os.NewFile(uintptr(value.handle), "tht-safeio-private")
if file == nil {
_ = windows.CloseHandle(handle)
_ = os.Remove(path)
_ = closeAndDeleteWindowsPrivateRegular(value)
return nil, ErrUnsafeFile
}
value.handle = 0
return file, nil
}
// ValidatePrivateRegular requires a canonical, single-link file protected for its current owner only.
func ValidatePrivateRegular(path string) error {
parents, target, err := openCanonicalWindowsParent(path)
if err != nil {
if err != nil || parents == nil || len(parents.handles) == 0 {
if parents != nil {
parents.Close()
}
return ErrUnsafeFile
}
defer parents.Close()
handle, err := openWindowsComponent(filepath.Join(parents.directory, target), false)
value, err := openWindowsPrivateRegularAt(parents.handles[len(parents.handles)-1], target, windows.GENERIC_READ, 1)
if err != nil {
return ErrUnsafeFile
}
defer windows.CloseHandle(handle)
if err := validateOwnerOnlyDACL(handle); err != nil {
if err := value.Close(); err != nil {
return ErrUnsafeFile
}
return nil
@@ -182,8 +169,8 @@ func (parents *windowsParentHandles) Close() {
}
// openCanonicalWindowsParent retains every directory handle from the volume root through the
// target parent without FILE_SHARE_DELETE. The resulting parent cannot be renamed or replaced by
// a reparse point while an operation uses its absolute child paths.
// target parent without FILE_SHARE_DELETE. Every component after the volume root is resolved
// through the prior retained handle's NT RootDirectory, never by re-opening an absolute prefix.
func openCanonicalWindowsParent(path string) (*windowsParentHandles, string, error) {
if err := ValidateCanonicalPath(path); err != nil {
return nil, "", err
@@ -201,12 +188,12 @@ func openCanonicalWindowsParent(path string) (*windowsParentHandles, string, err
}
parents.handles = append(parents.handles, rootHandle)
for _, component := range components[:len(components)-1] {
parents.directory = filepath.Join(parents.directory, component)
handle, err := openWindowsComponent(parents.directory, true)
handle, err := openWindowsRelativeComponent(parents.handles[len(parents.handles)-1], component, true, windows.GENERIC_READ)
if err != nil {
parents.Close()
return nil, "", err
}
parents.directory = filepath.Join(parents.directory, component)
parents.handles = append(parents.handles, handle)
}
return parents, components[len(components)-1], nil