//go:build windows package safeio import ( "io/fs" "os" "path/filepath" "strings" "golang.org/x/sys/windows" ) const windowsRetainedHandleShareMode uint32 = windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE // ReadCanonicalRegular opens each component with FILE_FLAG_OPEN_REPARSE_POINT and rejects a // reparse point on the opened handle before opening the next component. Retained handles allow // ordinary read/write sharing but deny delete sharing, which blocks rename or deletion after a // component is opened and throughout the final read. Windows' Win32 API does not expose a // portable descriptor-relative equivalent of POSIX openat, so a hostile local actor can still // replace a not-yet-opened normal component between absolute-path opens. Installation directories // therefore need trusted local filesystem/ACL ownership on Windows. func ReadCanonicalRegular(path string, maximum int64) ([]byte, error) { if err := ValidateCanonicalPath(path); err != nil { return nil, err } volume := filepath.VolumeName(path) root := volume + string(filepath.Separator) components := strings.Split(strings.TrimPrefix(path, root), string(filepath.Separator)) if volume == "" || len(components) == 0 || components[0] == "" { return nil, ErrUnsafeFile } current := root parents := make([]windows.Handle, 0, len(components)-1) defer func() { closeWindowsHandles(parents) }() for _, component := range components[:len(components)-1] { current = filepath.Join(current, component) handle, err := openWindowsComponent(current, true) if err != nil { return nil, ErrUnsafeFile } parents = append(parents, handle) } current = filepath.Join(current, components[len(components)-1]) handle, err := openWindowsComponent(current, false) if err != nil { return nil, ErrUnsafeFile } file := os.NewFile(uintptr(handle), "thothctl-safeio") if file == nil { windows.CloseHandle(handle) return nil, ErrUnsafeFile } defer file.Close() var before, after windows.ByHandleFileInformation if windows.GetFileInformationByHandle(handle, &before) != nil || before.NumberOfLinks > 1 { return nil, ErrUnsafeFile } contents, err := readBoundedRegularFile(file, maximum) if err != nil { return nil, ErrUnsafeFile } if windows.GetFileInformationByHandle(handle, &after) != nil || after.NumberOfLinks > 1 || !sameWindowsFile(before, after) { return nil, ErrUnsafeFile } // Re-opened pathname identity is checked as well as the retained handle. This // catches replacement by an ordinary local actor where share/delete policy permits it. check, err := openWindowsComponent(current, false) if err != nil { return nil, ErrUnsafeFile } defer windows.CloseHandle(check) var named windows.ByHandleFileInformation if windows.GetFileInformationByHandle(check, &named) != nil || named.NumberOfLinks > 1 || !sameWindowsFile(before, named) { return nil, ErrUnsafeFile } return contents, nil } func openWindowsComponent(path string, directory bool) (windows.Handle, error) { flags := uint32(windows.FILE_FLAG_OPEN_REPARSE_POINT) if directory { flags |= windows.FILE_FLAG_BACKUP_SEMANTICS } else { flags |= windows.FILE_ATTRIBUTE_NORMAL } handle, err := windows.CreateFile( windows.StringToUTF16Ptr(path), windows.GENERIC_READ, windowsRetainedHandleShareMode, nil, windows.OPEN_EXISTING, flags, 0, ) if err != nil { return 0, err } var information windows.ByHandleFileInformation if err := windows.GetFileInformationByHandle(handle, &information); err != nil { windows.CloseHandle(handle) return 0, err } if information.NumberOfLinks > 1 || information.FileAttributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 || (directory && information.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY == 0) || (!directory && information.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY != 0) { windows.CloseHandle(handle) return 0, ErrUnsafeFile } return handle, nil } func closeWindowsHandles(handles []windows.Handle) { for _, handle := range handles { windows.CloseHandle(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 { return ErrUnsafeFile } h, err := windows.CreateFile(windows.StringToUTF16Ptr(path), windows.GENERIC_WRITE, 0, nil, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0) if err != nil { return ErrUnsafeFile } f := os.NewFile(uintptr(h), "thothctl-safeio-output") if f == nil { windows.CloseHandle(h) return ErrUnsafeFile } defer f.Close() if _, err := f.Write(contents); err != nil { return ErrUnsafeFile } if err := f.Sync(); err != nil { return ErrUnsafeFile } var opened, named windows.ByHandleFileInformation if windows.GetFileInformationByHandle(h, &opened) != nil || opened.NumberOfLinks != 1 { return ErrUnsafeFile } check, err := openWindowsComponent(path, false) if err != nil { return ErrUnsafeFile } defer windows.CloseHandle(check) if windows.GetFileInformationByHandle(check, &named) != nil || named.NumberOfLinks != 1 || !sameWindowsFile(opened, named) { return ErrUnsafeFile } return nil } func validateCanonicalOutputPath(path string) error { if err := ValidateCanonicalPath(path); err != nil { return err } volume := filepath.VolumeName(path) root := volume + string(filepath.Separator) components := strings.Split(strings.TrimPrefix(path, root), string(filepath.Separator)) if volume == "" || len(components) < 2 || components[0] == "" { return ErrUnsafeFile } current := root for _, component := range components[:len(components)-1] { current = filepath.Join(current, component) h, err := openWindowsComponent(current, true) if err != nil { return ErrUnsafeFile } windows.CloseHandle(h) } if _, err := os.Lstat(filepath.Join(current, components[len(components)-1])); err == nil || !os.IsNotExist(err) { return ErrUnsafeFile } return nil } func sameWindowsFile(a, b windows.ByHandleFileInformation) bool { return a.VolumeSerialNumber == b.VolumeSerialNumber && a.FileIndexHigh == b.FileIndexHigh && a.FileIndexLow == b.FileIndexLow }