fix Task1 publication and secret preflight
This commit is contained in:
@@ -92,8 +92,6 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
if err != nil {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
// Every superseded descriptor is closed in the traversal. Capture the
|
||||
// variable, rather than its initial value, so the final parent is closed too.
|
||||
defer func() { _ = unix.Close(dir) }()
|
||||
for _, component := range components[:len(components)-1] {
|
||||
next, openErr := unix.Openat(dir, component, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
|
||||
@@ -103,14 +101,12 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
_ = unix.Close(dir)
|
||||
dir = next
|
||||
}
|
||||
// This is a capability publication API: the retained parent is the namespace
|
||||
// anchor. The lexical check is only a pre-commit diagnostic and cannot close an
|
||||
// ancestor rename race atomically.
|
||||
if !recheckUnixParentPath(components[:len(components)-1], dir) {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
|
||||
// O_TMPFILE creates an inode with no directory entry. Consequently no
|
||||
// same-identity actor can discover or hard-link candidate bytes while they
|
||||
// are being written. AT_EMPTY_PATH then links that already-synced inode into
|
||||
// the destination in one no-replace operation.
|
||||
fd, err := unix.Openat(dir, ".", unix.O_RDWR|unix.O_CLOEXEC|unix.O_TMPFILE, uint32(mode.Perm()))
|
||||
if err != nil {
|
||||
return ErrUnsafeFile
|
||||
@@ -128,30 +124,18 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
closed = true
|
||||
return file.Close()
|
||||
}
|
||||
var expected unix.Stat_t
|
||||
if err := unix.Fstat(fd, &expected); err != nil || expected.Nlink != 0 || expected.Mode&unix.S_IFMT != unix.S_IFREG {
|
||||
_ = closeFile()
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
cleanup := func() {
|
||||
// expected is immutable. Never use a post-race stat as the identity to
|
||||
// remove: a replacement at the public name must survive our cleanup.
|
||||
var current unix.Stat_t
|
||||
leaf := components[len(components)-1]
|
||||
if unix.Fstatat(dir, leaf, ¤t, unix.AT_SYMLINK_NOFOLLOW) == nil && current.Ino == expected.Ino && current.Dev == expected.Dev {
|
||||
_ = unix.Unlinkat(dir, leaf, 0)
|
||||
}
|
||||
}
|
||||
fail := func() error {
|
||||
_ = closeFile()
|
||||
cleanup()
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
var expected unix.Stat_t
|
||||
if err := unix.Fstat(fd, &expected); err != nil || expected.Nlink != 0 || expected.Mode&unix.S_IFMT != unix.S_IFREG {
|
||||
return fail()
|
||||
}
|
||||
if err := file.Chmod(mode); err != nil {
|
||||
return fail()
|
||||
}
|
||||
n, err := file.Write(contents)
|
||||
if err != nil || n != len(contents) {
|
||||
if n, err := file.Write(contents); err != nil || n != len(contents) {
|
||||
return fail()
|
||||
}
|
||||
if err := file.Sync(); err != nil {
|
||||
@@ -164,24 +148,24 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
if !recheckUnixParentPath(components[:len(components)-1], dir) {
|
||||
return fail()
|
||||
}
|
||||
leaf := components[len(components)-1]
|
||||
if err := unix.Linkat(fd, "", dir, leaf, unix.AT_EMPTY_PATH); err != nil {
|
||||
// All fallible preparation, checks, and syncs happen before the linearization
|
||||
// point. A directory sync here is best effort durability for the retained
|
||||
// parent; it cannot be used to report failure after Linkat.
|
||||
if err := unix.Fsync(dir); err != nil {
|
||||
return fail()
|
||||
}
|
||||
var published unix.Stat_t
|
||||
if err := unix.Fstatat(dir, leaf, &published, unix.AT_SYMLINK_NOFOLLOW); err != nil || published.Ino != expected.Ino || published.Dev != expected.Dev || published.Nlink != 1 {
|
||||
if !recheckUnixParentPath(components[:len(components)-1], dir) {
|
||||
return fail()
|
||||
}
|
||||
if err := unix.Fsync(dir); err != nil || !recheckUnixParentPath(components[:len(components)-1], dir) {
|
||||
if err := unix.Linkat(fd, "", dir, components[len(components)-1], unix.AT_EMPTY_PATH); err != nil {
|
||||
return fail()
|
||||
}
|
||||
if err := closeFile(); err != nil {
|
||||
cleanup()
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
// Linkat is the final commit. Close errors and any post-commit observations
|
||||
// are deliberately ignored: returning ErrUnsafeFile here would lie about a
|
||||
// candidate that is already committed, and pathname cleanup would be racy.
|
||||
_ = closeFile()
|
||||
return nil
|
||||
}
|
||||
|
||||
func privateStageName() (string, error) {
|
||||
var random [16]byte
|
||||
if _, err := rand.Read(random[:]); err != nil {
|
||||
|
||||
@@ -80,6 +80,11 @@ func closeUnixDescriptors(descriptors []int) {
|
||||
}
|
||||
}
|
||||
|
||||
// Darwin uses a named stage because it has no relinkable O_TMPFILE equivalent.
|
||||
// The caller must provide a trusted parent namespace: same-UID namespace mutation
|
||||
// (including stage hard-link/replacement races and ancestor replacement) is outside
|
||||
// this mode's threat model. Under that precondition renameatx_np(RENAME_EXCL) is the
|
||||
// final fallible no-replace commit and removes the stage atomically.
|
||||
func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) error {
|
||||
if err := ValidateCanonicalPath(path); err != nil || len(contents) > 16<<20 || mode.Perm() != 0o600 {
|
||||
return ErrUnsafeFile
|
||||
@@ -92,24 +97,18 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
if err != nil {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
// Close the descriptor that owns the final retained parent. The traversal
|
||||
// closes each superseded descriptor explicitly; evaluating unix.Close(dir)
|
||||
// at defer time would close only the original root descriptor.
|
||||
defer func() { _ = unix.Close(dir) }()
|
||||
for _, component := range components[:len(components)-1] {
|
||||
next, openErr := unix.Openat(dir, component, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
|
||||
if openErr != nil {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
unix.Close(dir)
|
||||
_ = unix.Close(dir)
|
||||
dir = next
|
||||
}
|
||||
if !recheckUnixParentPath(components[:len(components)-1], dir) {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
|
||||
// Build the candidate under a private, same-parent name. Only after it is fully
|
||||
// written, synced, and identity-checked do we link it into the requested leaf.
|
||||
stage, err := privateStageName()
|
||||
if err != nil {
|
||||
return ErrUnsafeFile
|
||||
@@ -120,90 +119,72 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
}
|
||||
stageFile := os.NewFile(uintptr(stageFD), "thothctl-safeio-stage")
|
||||
if stageFile == nil {
|
||||
unix.Close(stageFD)
|
||||
_ = unix.Close(stageFD)
|
||||
_ = unix.Unlinkat(dir, stage, 0)
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
stageCreated := true
|
||||
closed := false
|
||||
closeStage := func() error {
|
||||
if closed {
|
||||
return nil
|
||||
}
|
||||
closed = true
|
||||
return stageFile.Close()
|
||||
}
|
||||
defer func() { _ = closeStage() }()
|
||||
var staged unix.Stat_t
|
||||
if err := unix.Fstat(stageFD, &staged); err != nil || staged.Nlink != 1 || staged.Mode&unix.S_IFMT != unix.S_IFREG {
|
||||
_ = stageFile.Close()
|
||||
_ = closeStage()
|
||||
_ = unix.Unlinkat(dir, stage, 0)
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
published := false
|
||||
// Keep the inode identity immutable across every check and cleanup path.
|
||||
var publishedIdentity = staged
|
||||
cleanup := func() {
|
||||
if published {
|
||||
var current unix.Stat_t
|
||||
if unix.Fstatat(dir, components[len(components)-1], ¤t, unix.AT_SYMLINK_NOFOLLOW) == nil &&
|
||||
current.Ino == publishedIdentity.Ino && current.Dev == publishedIdentity.Dev {
|
||||
_ = unix.Unlinkat(dir, components[len(components)-1], 0)
|
||||
}
|
||||
}
|
||||
if stageCreated {
|
||||
var current unix.Stat_t
|
||||
if unix.Fstatat(dir, stage, ¤t, unix.AT_SYMLINK_NOFOLLOW) == nil &&
|
||||
current.Ino == staged.Ino && current.Dev == staged.Dev {
|
||||
_ = unix.Unlinkat(dir, stage, 0)
|
||||
}
|
||||
// Trusted-parent mode makes this identity check + unlink pre-commit safe;
|
||||
// never unlink a replacement observed at the stage name.
|
||||
var current unix.Stat_t
|
||||
if unix.Fstatat(dir, stage, ¤t, unix.AT_SYMLINK_NOFOLLOW) == nil && current.Ino == staged.Ino && current.Dev == staged.Dev {
|
||||
_ = unix.Unlinkat(dir, stage, 0)
|
||||
}
|
||||
}
|
||||
fail := func() error {
|
||||
_ = stageFile.Close()
|
||||
cleanup()
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
fail := func() error { _ = closeStage(); cleanup(); return ErrUnsafeFile }
|
||||
if err := stageFile.Chmod(mode); err != nil {
|
||||
return fail()
|
||||
}
|
||||
n, err := stageFile.Write(contents)
|
||||
if err != nil || n != len(contents) {
|
||||
if n, err := stageFile.Write(contents); err != nil || n != len(contents) {
|
||||
return fail()
|
||||
}
|
||||
if err := stageFile.Sync(); err != nil {
|
||||
return fail()
|
||||
}
|
||||
// On platforms without O_TMPFILE, a same-user actor can hard-link the
|
||||
// nameable stage. Extra links are therefore not a post-write rejection:
|
||||
// once observed, the bytes are already public and rejecting would leave the
|
||||
// attacker's link behind. Cleanup still uses the immutable inode identity.
|
||||
var afterWrite unix.Stat_t
|
||||
if err := unix.Fstat(stageFD, &afterWrite); err != nil || afterWrite.Nlink < 1 || afterWrite.Mode&unix.S_IFMT != unix.S_IFREG || afterWrite.Size != int64(len(contents)) {
|
||||
var after unix.Stat_t
|
||||
if err := unix.Fstat(stageFD, &after); err != nil || after.Nlink != 1 || after.Mode&unix.S_IFMT != unix.S_IFREG || after.Size != int64(len(contents)) {
|
||||
return fail()
|
||||
}
|
||||
if err := stageFile.Close(); err != nil {
|
||||
cleanup()
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
stageCreated = true
|
||||
if !recheckUnixParentPath(components[:len(components)-1], dir) {
|
||||
return fail()
|
||||
}
|
||||
if err := unix.Linkat(dir, stage, dir, components[len(components)-1], 0); err != nil {
|
||||
// Verify the named stage and its retained handle immediately before commit.
|
||||
var named unix.Stat_t
|
||||
if err := unix.Fstatat(dir, stage, &named, unix.AT_SYMLINK_NOFOLLOW); err != nil || named.Ino != staged.Ino || named.Dev != staged.Dev || named.Nlink != 1 {
|
||||
return fail()
|
||||
}
|
||||
published = true
|
||||
var linked unix.Stat_t
|
||||
if err := unix.Fstatat(dir, components[len(components)-1], &linked, unix.AT_SYMLINK_NOFOLLOW); err != nil ||
|
||||
linked.Ino != staged.Ino || linked.Dev != staged.Dev || linked.Nlink < 2 {
|
||||
if err := unix.Fstat(stageFD, &after); err != nil || after.Ino != staged.Ino || after.Dev != staged.Dev || after.Nlink != 1 {
|
||||
return fail()
|
||||
}
|
||||
if err := unix.Unlinkat(dir, stage, 0); err != nil {
|
||||
if err := unix.Fsync(dir); err != nil {
|
||||
return fail()
|
||||
}
|
||||
stageCreated = false
|
||||
var finalIdentity unix.Stat_t
|
||||
if err := unix.Fstatat(dir, components[len(components)-1], &finalIdentity, unix.AT_SYMLINK_NOFOLLOW); err != nil || finalIdentity.Ino != publishedIdentity.Ino || finalIdentity.Dev != publishedIdentity.Dev || finalIdentity.Nlink < 1 {
|
||||
if !recheckUnixParentPath(components[:len(components)-1], dir) {
|
||||
return fail()
|
||||
}
|
||||
if err := unix.Fsync(dir); err != nil || !recheckUnixParentPath(components[:len(components)-1], dir) {
|
||||
if err := unix.RenameatxNp(dir, stage, dir, components[len(components)-1], unix.RENAME_EXCL); err != nil {
|
||||
return fail()
|
||||
}
|
||||
// renameatx_np is the final commit. Closing the still-open handle is ignored
|
||||
// after success and never triggers pathname cleanup or a false failure.
|
||||
_ = closeStage()
|
||||
return nil
|
||||
}
|
||||
|
||||
func privateStageName() (string, error) {
|
||||
var random [16]byte
|
||||
if _, err := rand.Read(random[:]); err != nil {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
const expectedWindowsRetainedHandleShareMode = windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE
|
||||
const expectedWindowsOutputHandleShareMode = windows.FILE_SHARE_READ
|
||||
const expectedWindowsOutputHandleShareMode = 0
|
||||
|
||||
// Keep this contract compile-enforced so Windows cross-test compilation catches a future
|
||||
// FILE_SHARE_DELETE regression even when the tests are compiled on a non-Windows host.
|
||||
@@ -80,6 +80,51 @@ func TestOpenWindowsComponentBlocksMutationWhileHandleIsRetained(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestWindowsStageHandleDeniesReadRenameDeleteAndHardlink(t *testing.T) {
|
||||
root := filepath.Join(t.TempDir(), "parent")
|
||||
if err := os.Mkdir(root, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
securityDescriptor, securityAttributes, err := ownerOnlySecurityAttributes()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = securityDescriptor
|
||||
stagePath := filepath.Join(root, ".thothctl-candidate-test")
|
||||
h, err := windows.CreateFile(windows.StringToUTF16Ptr(stagePath), windows.GENERIC_WRITE|windows.DELETE, 0, securityAttributes, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
closed := false
|
||||
defer func() {
|
||||
if !closed {
|
||||
_ = windows.CloseHandle(h)
|
||||
}
|
||||
}()
|
||||
if _, err := windows.CreateFile(windows.StringToUTF16Ptr(stagePath), windows.GENERIC_READ, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0); err == nil {
|
||||
t.Fatal("stage read succeeded while zero-share handle was open")
|
||||
}
|
||||
if err := os.Rename(stagePath, stagePath+"-renamed"); err == nil {
|
||||
t.Fatal("stage rename succeeded while handle was open")
|
||||
}
|
||||
if err := os.Link(stagePath, filepath.Join(root, "stolen")); err == nil {
|
||||
t.Fatal("stage hardlink succeeded while handle was open")
|
||||
}
|
||||
if err := os.Remove(stagePath); err == nil {
|
||||
t.Fatal("stage delete succeeded while handle was open")
|
||||
}
|
||||
if err := deleteWindowsHandle(h); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := windows.CloseHandle(h); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
closed = true
|
||||
if _, err := os.Stat(stagePath); !os.IsNotExist(err) {
|
||||
t.Fatalf("disposed stage remains: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteCanonicalExclusiveRequiresRestrictiveMode(t *testing.T) {
|
||||
if err := writeCanonicalExclusive(`C:\\tmp\\thothctl-output.yaml`, []byte("x"), 0o640); err == nil {
|
||||
t.Fatal("accepted non-restrictive output mode")
|
||||
|
||||
Reference in New Issue
Block a user