fix Task1 publication and secret preflight

This commit is contained in:
2026-08-11 04:06:23 +02:00
parent fcc45520ad
commit c7f7a6e1b0
8 changed files with 324 additions and 215 deletions
+66 -72
View File
@@ -20,9 +20,10 @@ const (
// Retained input handles deny delete sharing while permitting ordinary reads
// and writes by trusted callers.
windowsRetainedHandleShareMode uint32 = windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE
// Outputs are opened for exclusive publication: deny write/delete sharing,
// but permit the exact-identity read recheck below.
windowsOutputHandleShareMode uint32 = windows.FILE_SHARE_READ
// A discoverable stage is protected by mandatory zero-share semantics for its
// entire lifetime. This denies reads, writes, rename, delete, and hard-link
// acquisition by another handle until our final handle-relative rename.
windowsOutputHandleShareMode uint32 = 0
)
// ReadCanonicalRegular opens each component with FILE_FLAG_OPEN_REPARSE_POINT and rejects a
@@ -139,6 +140,10 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
return ErrUnsafeFile
}
defer closeWindowsHandles(retainedParents)
if len(retainedParents) == 0 {
return ErrUnsafeFile
}
parentHandle := retainedParents[len(retainedParents)-1]
securityDescriptor, securityAttributes, err := ownerOnlySecurityAttributes()
if err != nil {
return ErrUnsafeFile
@@ -149,77 +154,86 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
return ErrUnsafeFile
}
stagePath := filepath.Join(parent, stageName)
stageHandle, err := windows.CreateFile(windows.StringToUTF16Ptr(stagePath), windows.GENERIC_WRITE, windowsOutputHandleShareMode, securityAttributes, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
stageHandle, err := windows.CreateFile(windows.StringToUTF16Ptr(stagePath), windows.GENERIC_WRITE|windows.DELETE, windowsOutputHandleShareMode, securityAttributes, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
if err != nil {
return ErrUnsafeFile
}
stageFile := os.NewFile(uintptr(stageHandle), "thothctl-safeio-stage")
if stageFile == nil {
windows.CloseHandle(stageHandle)
_ = windows.DeleteFile(windows.StringToUTF16Ptr(stagePath))
_ = deleteWindowsHandle(stageHandle)
_ = windows.CloseHandle(stageHandle)
return ErrUnsafeFile
}
stageCreated := true
published := false
var staged, publishedIdentity windows.ByHandleFileInformation
if err := windows.GetFileInformationByHandle(stageHandle, &staged); err != nil || staged.NumberOfLinks != 1 {
_ = stageFile.Close()
_ = windows.DeleteFile(windows.StringToUTF16Ptr(stagePath))
return ErrUnsafeFile
}
cleanup := func() {
if published {
removeWindowsIfIdentity(filepath.Join(parent, filepath.Base(path)), publishedIdentity)
}
if stageCreated {
removeWindowsIfIdentity(stagePath, staged)
closed := false
closeStage := func() error {
if closed {
return nil
}
closed = true
return stageFile.Close()
}
fail := func() error {
_ = stageFile.Close()
cleanup()
return ErrUnsafeFile
defer func() { _ = closeStage() }()
var staged windows.ByHandleFileInformation
if err := windows.GetFileInformationByHandle(stageHandle, &staged); err != nil || staged.NumberOfLinks != 1 || staged.FileAttributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 || staged.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY != 0 {
return failWindowsStage(stageHandle, closeStage)
}
fail := func() error { _ = deleteWindowsHandle(stageHandle); _ = closeStage(); return ErrUnsafeFile }
if n, err := stageFile.Write(contents); err != nil || n != len(contents) {
return fail()
}
if err := stageFile.Sync(); err != nil {
return fail()
}
// The stage name is visible on Win32. If a same-user actor hard-links it,
// the bytes are already public; do not turn that observation into a failure
// whose cleanup could not remove the attacker's link.
var afterWrite windows.ByHandleFileInformation
if err := windows.GetFileInformationByHandle(stageHandle, &afterWrite); err != nil || afterWrite.NumberOfLinks == 0 || afterWrite.FileSizeHigh != uint32(uint64(len(contents))>>32) || afterWrite.FileSizeLow != uint32(len(contents)) {
var after windows.ByHandleFileInformation
if err := windows.GetFileInformationByHandle(stageHandle, &after); err != nil || after.NumberOfLinks != 1 || after.FileSizeHigh != uint32(uint64(len(contents))>>32) || after.FileSizeLow != uint32(len(contents)) {
return fail()
}
if err := stageFile.Close(); err != nil {
cleanup()
return ErrUnsafeFile
}
// CreateHardLink is an atomic, same-volume, no-replace publication. The final
// pathname can never refer to a partially written candidate.
finalPath := filepath.Join(parent, filepath.Base(path))
if err := windows.CreateHardLink(windows.StringToUTF16Ptr(finalPath), windows.StringToUTF16Ptr(stagePath), 0); err != nil {
return fail()
}
published = true
publishedIdentity = staged
check, identityErr := windowsFileIdentity(finalPath)
if identityErr != nil || check.NumberOfLinks < 2 || !sameWindowsFile(staged, check) {
return fail()
}
if err := windows.DeleteFile(windows.StringToUTF16Ptr(stagePath)); err != nil {
return fail()
}
stageCreated = false
finalIdentity, identityErr := windowsFileIdentity(finalPath)
if identityErr != nil || finalIdentity.NumberOfLinks == 0 || !sameWindowsFile(staged, finalIdentity) {
// Keep the exact stage handle open with zero sharing through this final check
// and atomic no-replace rename. No pathname reopen or cleanup is needed.
if err := renameWindowsHandle(stageHandle, parentHandle, filepath.Base(path)); err != nil {
return fail()
}
// Handle-relative rename is the final commit. Handle close is intentionally
// ignored after success; no fallible observation or pathname cleanup follows.
_ = closeStage()
return nil
}
// failWindowsStage disposes an exact handle when initial identity inspection fails.
func failWindowsStage(handle windows.Handle, closeStage func() error) error {
_ = deleteWindowsHandle(handle)
_ = closeStage()
return ErrUnsafeFile
}
func deleteWindowsHandle(handle windows.Handle) error {
var disposition byte = 1
return windows.SetFileInformationByHandle(handle, windows.FileDispositionInfo, &disposition, uint32(unsafe.Sizeof(disposition)))
}
type windowsFileRenameInfo struct {
ReplaceIfExists uint8
RootDirectory windows.Handle
FileNameLength uint32
FileName [1]uint16
}
func renameWindowsHandle(handle, parent windows.Handle, leaf string) error {
name, err := windows.UTF16FromString(leaf)
if err != nil {
return err
}
name = name[:len(name)-1]
base := unsafe.Offsetof(windowsFileRenameInfo{}.FileName)
buffer := make([]byte, int(base)+len(name)*2)
info := (*windowsFileRenameInfo)(unsafe.Pointer(&buffer[0]))
info.ReplaceIfExists = 0
info.RootDirectory = parent
info.FileNameLength = uint32(len(name) * 2)
nameBytes := unsafe.Slice((*uint16)(unsafe.Pointer(&buffer[base])), len(name))
copy(nameBytes, name)
return windows.SetFileInformationByHandle(handle, windows.FileRenameInfo, &buffer[0], uint32(len(buffer)))
}
func privateWindowsStageName() (string, error) {
var random [16]byte
if _, err := rand.Read(random[:]); err != nil {
@@ -228,26 +242,6 @@ func privateWindowsStageName() (string, error) {
return ".thothctl-candidate-" + hex.EncodeToString(random[:]), nil
}
func windowsFileIdentity(path string) (windows.ByHandleFileInformation, error) {
h, err := windows.CreateFile(windows.StringToUTF16Ptr(path), windows.GENERIC_READ, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
if err != nil {
return windows.ByHandleFileInformation{}, err
}
defer windows.CloseHandle(h)
var information windows.ByHandleFileInformation
if err := windows.GetFileInformationByHandle(h, &information); err != nil || information.NumberOfLinks == 0 || information.FileAttributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 || information.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY != 0 {
return windows.ByHandleFileInformation{}, ErrUnsafeFile
}
return information, nil
}
func removeWindowsIfIdentity(path string, expected windows.ByHandleFileInformation) {
got, err := windowsFileIdentity(path)
if err == nil && sameWindowsFile(got, expected) {
_ = windows.DeleteFile(windows.StringToUTF16Ptr(path))
}
}
func openWindowsParents(path string) (string, []windows.Handle, error) {
volume := filepath.VolumeName(path)
root := volume + string(filepath.Separator)
@@ -348,7 +342,7 @@ func ownerOnlySecurityAttributes() (*windows.SECURITY_DESCRIPTOR, *windows.Secur
TrusteeValue: windows.TrusteeValueFromSID(user.User.Sid),
}
entries := []windows.EXPLICIT_ACCESS{{
AccessPermissions: windows.FILE_GENERIC_READ | windows.FILE_GENERIC_WRITE,
AccessPermissions: windows.FILE_GENERIC_READ | windows.FILE_GENERIC_WRITE | windows.DELETE,
AccessMode: windows.SET_ACCESS,
Inheritance: windows.NO_INHERITANCE,
Trustee: trustee,