fix: close workspace preprocessing contract gaps

This commit is contained in:
2026-08-11 01:18:29 +02:00
parent 05a6e8cc2d
commit de27275bb1
15 changed files with 516 additions and 87 deletions
@@ -6,8 +6,11 @@ import (
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"unsafe"
"golang.org/x/sys/windows"
)
@@ -119,7 +122,7 @@ func closeWindowsHandles(handles []windows.Handle) {
}
func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) error {
if err := ValidateCanonicalPath(path); err != nil || len(contents) > 16<<20 || mode.Perm() == 0 || mode.Perm()&0o077 != 0 {
if err := ValidateCanonicalPath(path); err != nil || len(contents) > 16<<20 || mode.Perm() != 0o600 {
return ErrUnsafeFile
}
parent, retainedParents, err := openWindowsParents(path)
@@ -129,7 +132,11 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
defer closeWindowsHandles(retainedParents)
// Parent handles stay open with delete sharing denied until publication and
// identity recheck complete; this is the Windows equivalent of retained dirfds.
h, err := windows.CreateFile(windows.StringToUTF16Ptr(filepath.Join(parent, filepath.Base(path))), windows.GENERIC_WRITE, 0, nil, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
securityDescriptor, securityAttributes, err := ownerOnlySecurityAttributes()
if err != nil {
return ErrUnsafeFile
}
h, err := windows.CreateFile(windows.StringToUTF16Ptr(filepath.Join(parent, filepath.Base(path))), windows.GENERIC_WRITE, 0, securityAttributes, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
if err != nil {
return ErrUnsafeFile
}
@@ -139,7 +146,11 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
return ErrUnsafeFile
}
defer f.Close()
if err := f.Chmod(mode); err != nil {
// Go's Windows Chmod only toggles the read-only attribute; it cannot enforce
// owner-only permissions. The restrictive DACL is installed at creation time
// through SECURITY_ATTRIBUTES above.
_ = securityDescriptor
if mode.Perm() != 0o600 {
return ErrUnsafeFile
}
if _, err := f.Write(contents); err != nil {
@@ -202,3 +213,47 @@ func validateCanonicalOutputPath(path string) error {
func sameWindowsFile(a, b windows.ByHandleFileInformation) bool {
return a.VolumeSerialNumber == b.VolumeSerialNumber && a.FileIndexHigh == b.FileIndexHigh && a.FileIndexLow == b.FileIndexLow
}
// ownerOnlySecurityAttributes builds a non-inheriting DACL granting the current
// token's user read/write access and no access to inherited or other trustees.
// The descriptor is retained by the returned Go value for the duration of CreateFile.
func ownerOnlySecurityAttributes() (*windows.SECURITY_DESCRIPTOR, *windows.SecurityAttributes, error) {
user, err := windows.GetCurrentProcessToken().GetTokenUser()
if err != nil || user == nil || user.User.Sid == nil {
return nil, nil, ErrUnsafeFile
}
var pinner runtime.Pinner
pinner.Pin(user.User.Sid)
defer pinner.Unpin()
trustee := windows.TRUSTEE{
TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_USER,
TrusteeValue: windows.TrusteeValueFromSID(user.User.Sid),
}
entries := []windows.EXPLICIT_ACCESS{{
AccessPermissions: windows.FILE_GENERIC_READ | windows.FILE_GENERIC_WRITE,
AccessMode: windows.SET_ACCESS,
Inheritance: windows.NO_INHERITANCE,
Trustee: trustee,
}}
descriptor, err := windows.BuildSecurityDescriptor(nil, nil, entries, nil, nil)
if err != nil || descriptor == nil {
return nil, nil, ErrUnsafeFile
}
acl, _, err := descriptor.DACL()
if err != nil || acl == nil {
return nil, nil, ErrUnsafeFile
}
if err := descriptor.SetControl(windows.SE_DACL_PROTECTED, windows.SE_DACL_PROTECTED); err != nil {
return nil, nil, ErrUnsafeFile
}
// BuildSecurityDescriptor returns a self-relative descriptor. Re-using its
// DACL as the creation descriptor is valid, and the explicit DACL has no
// inheritable ACEs; the protected flag is applied by the kernel on creation.
_ = acl
attrs := &windows.SecurityAttributes{
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
SecurityDescriptor: descriptor,
}
return descriptor, attrs, nil
}