[release-branch.go1.21] Revert "os: use handle based APIs to read directories on windows"

This reverts CL 452995.

Reason for revert: caused os.File.ReadDir to fail on
filesystems that do not support FILE_ID_BOTH_DIR_INFO.

This is an alternative to a fix-forward change in CL 518196.
Since the original change was mostly a performance improvement,
reverting to the previous implementation seems less risky than
backporting a larger fix.

Fixes #61910
Fixes #61964

Change-Id: I60f1602b9eb6ea353e7eb23429f19f1ffa16b394
Reviewed-on: https://go-review.googlesource.com/c/go/+/520156
Run-TryBot: Bryan Mills <bcmills@google.com>
Reviewed-by: Austin Clements <austin@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
This commit is contained in:
Bryan Mills 2023-08-16 17:23:10 +00:00 committed by Carlos Amedee
parent cb6ea94996
commit 7c97cc7d97
6 changed files with 158 additions and 197 deletions

View File

@ -375,25 +375,5 @@ func ErrorLoadingGetTempPath2() error {
//sys RtlGenRandom(buf []byte) (err error) = advapi32.SystemFunction036
type FILE_ID_BOTH_DIR_INFO struct {
NextEntryOffset uint32
FileIndex uint32
CreationTime syscall.Filetime
LastAccessTime syscall.Filetime
LastWriteTime syscall.Filetime
ChangeTime syscall.Filetime
EndOfFile uint64
AllocationSize uint64
FileAttributes uint32
FileNameLength uint32
EaSize uint32
ShortNameLength uint32
ShortName [12]uint16
FileID uint64
FileName [1]uint16
}
//sys GetVolumeInformationByHandle(file syscall.Handle, volumeNameBuffer *uint16, volumeNameSize uint32, volumeNameSerialNumber *uint32, maximumComponentLength *uint32, fileSystemFlags *uint32, fileSystemNameBuffer *uint16, fileSystemNameSize uint32) (err error) = GetVolumeInformationByHandleW
//sys RtlLookupFunctionEntry(pc uintptr, baseAddress *uintptr, table *byte) (ret uintptr) = kernel32.RtlLookupFunctionEntry
//sys RtlVirtualUnwind(handlerType uint32, baseAddress uintptr, pc uintptr, entry uintptr, ctxt uintptr, data *uintptr, frame *uintptr, ctxptrs *byte) (ret uintptr) = kernel32.RtlVirtualUnwind

View File

@ -45,43 +45,42 @@ var (
moduserenv = syscall.NewLazyDLL(sysdll.Add("userenv.dll"))
modws2_32 = syscall.NewLazyDLL(sysdll.Add("ws2_32.dll"))
procAdjustTokenPrivileges = modadvapi32.NewProc("AdjustTokenPrivileges")
procDuplicateTokenEx = modadvapi32.NewProc("DuplicateTokenEx")
procImpersonateSelf = modadvapi32.NewProc("ImpersonateSelf")
procLookupPrivilegeValueW = modadvapi32.NewProc("LookupPrivilegeValueW")
procOpenThreadToken = modadvapi32.NewProc("OpenThreadToken")
procRevertToSelf = modadvapi32.NewProc("RevertToSelf")
procSetTokenInformation = modadvapi32.NewProc("SetTokenInformation")
procSystemFunction036 = modadvapi32.NewProc("SystemFunction036")
procGetAdaptersAddresses = modiphlpapi.NewProc("GetAdaptersAddresses")
procCreateEventW = modkernel32.NewProc("CreateEventW")
procGetACP = modkernel32.NewProc("GetACP")
procGetComputerNameExW = modkernel32.NewProc("GetComputerNameExW")
procGetConsoleCP = modkernel32.NewProc("GetConsoleCP")
procGetCurrentThread = modkernel32.NewProc("GetCurrentThread")
procGetFileInformationByHandleEx = modkernel32.NewProc("GetFileInformationByHandleEx")
procGetFinalPathNameByHandleW = modkernel32.NewProc("GetFinalPathNameByHandleW")
procGetModuleFileNameW = modkernel32.NewProc("GetModuleFileNameW")
procGetTempPath2W = modkernel32.NewProc("GetTempPath2W")
procGetVolumeInformationByHandleW = modkernel32.NewProc("GetVolumeInformationByHandleW")
procLockFileEx = modkernel32.NewProc("LockFileEx")
procModule32FirstW = modkernel32.NewProc("Module32FirstW")
procModule32NextW = modkernel32.NewProc("Module32NextW")
procMoveFileExW = modkernel32.NewProc("MoveFileExW")
procMultiByteToWideChar = modkernel32.NewProc("MultiByteToWideChar")
procRtlLookupFunctionEntry = modkernel32.NewProc("RtlLookupFunctionEntry")
procRtlVirtualUnwind = modkernel32.NewProc("RtlVirtualUnwind")
procSetFileInformationByHandle = modkernel32.NewProc("SetFileInformationByHandle")
procUnlockFileEx = modkernel32.NewProc("UnlockFileEx")
procVirtualQuery = modkernel32.NewProc("VirtualQuery")
procNetShareAdd = modnetapi32.NewProc("NetShareAdd")
procNetShareDel = modnetapi32.NewProc("NetShareDel")
procNetUserGetLocalGroups = modnetapi32.NewProc("NetUserGetLocalGroups")
procGetProcessMemoryInfo = modpsapi.NewProc("GetProcessMemoryInfo")
procCreateEnvironmentBlock = moduserenv.NewProc("CreateEnvironmentBlock")
procDestroyEnvironmentBlock = moduserenv.NewProc("DestroyEnvironmentBlock")
procGetProfilesDirectoryW = moduserenv.NewProc("GetProfilesDirectoryW")
procWSASocketW = modws2_32.NewProc("WSASocketW")
procAdjustTokenPrivileges = modadvapi32.NewProc("AdjustTokenPrivileges")
procDuplicateTokenEx = modadvapi32.NewProc("DuplicateTokenEx")
procImpersonateSelf = modadvapi32.NewProc("ImpersonateSelf")
procLookupPrivilegeValueW = modadvapi32.NewProc("LookupPrivilegeValueW")
procOpenThreadToken = modadvapi32.NewProc("OpenThreadToken")
procRevertToSelf = modadvapi32.NewProc("RevertToSelf")
procSetTokenInformation = modadvapi32.NewProc("SetTokenInformation")
procSystemFunction036 = modadvapi32.NewProc("SystemFunction036")
procGetAdaptersAddresses = modiphlpapi.NewProc("GetAdaptersAddresses")
procCreateEventW = modkernel32.NewProc("CreateEventW")
procGetACP = modkernel32.NewProc("GetACP")
procGetComputerNameExW = modkernel32.NewProc("GetComputerNameExW")
procGetConsoleCP = modkernel32.NewProc("GetConsoleCP")
procGetCurrentThread = modkernel32.NewProc("GetCurrentThread")
procGetFileInformationByHandleEx = modkernel32.NewProc("GetFileInformationByHandleEx")
procGetFinalPathNameByHandleW = modkernel32.NewProc("GetFinalPathNameByHandleW")
procGetModuleFileNameW = modkernel32.NewProc("GetModuleFileNameW")
procGetTempPath2W = modkernel32.NewProc("GetTempPath2W")
procLockFileEx = modkernel32.NewProc("LockFileEx")
procModule32FirstW = modkernel32.NewProc("Module32FirstW")
procModule32NextW = modkernel32.NewProc("Module32NextW")
procMoveFileExW = modkernel32.NewProc("MoveFileExW")
procMultiByteToWideChar = modkernel32.NewProc("MultiByteToWideChar")
procRtlLookupFunctionEntry = modkernel32.NewProc("RtlLookupFunctionEntry")
procRtlVirtualUnwind = modkernel32.NewProc("RtlVirtualUnwind")
procSetFileInformationByHandle = modkernel32.NewProc("SetFileInformationByHandle")
procUnlockFileEx = modkernel32.NewProc("UnlockFileEx")
procVirtualQuery = modkernel32.NewProc("VirtualQuery")
procNetShareAdd = modnetapi32.NewProc("NetShareAdd")
procNetShareDel = modnetapi32.NewProc("NetShareDel")
procNetUserGetLocalGroups = modnetapi32.NewProc("NetUserGetLocalGroups")
procGetProcessMemoryInfo = modpsapi.NewProc("GetProcessMemoryInfo")
procCreateEnvironmentBlock = moduserenv.NewProc("CreateEnvironmentBlock")
procDestroyEnvironmentBlock = moduserenv.NewProc("DestroyEnvironmentBlock")
procGetProfilesDirectoryW = moduserenv.NewProc("GetProfilesDirectoryW")
procWSASocketW = modws2_32.NewProc("WSASocketW")
)
func adjustTokenPrivileges(token syscall.Token, disableAllPrivileges bool, newstate *TOKEN_PRIVILEGES, buflen uint32, prevstate *TOKEN_PRIVILEGES, returnlen *uint32) (ret uint32, err error) {
@ -242,14 +241,6 @@ func GetTempPath2(buflen uint32, buf *uint16) (n uint32, err error) {
return
}
func GetVolumeInformationByHandle(file syscall.Handle, volumeNameBuffer *uint16, volumeNameSize uint32, volumeNameSerialNumber *uint32, maximumComponentLength *uint32, fileSystemFlags *uint32, fileSystemNameBuffer *uint16, fileSystemNameSize uint32) (err error) {
r1, _, e1 := syscall.Syscall9(procGetVolumeInformationByHandleW.Addr(), 8, uintptr(file), uintptr(unsafe.Pointer(volumeNameBuffer)), uintptr(volumeNameSize), uintptr(unsafe.Pointer(volumeNameSerialNumber)), uintptr(unsafe.Pointer(maximumComponentLength)), uintptr(unsafe.Pointer(fileSystemFlags)), uintptr(unsafe.Pointer(fileSystemNameBuffer)), uintptr(fileSystemNameSize), 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func LockFileEx(file syscall.Handle, flags uint32, reserved uint32, bytesLow uint32, bytesHigh uint32, overlapped *syscall.Overlapped) (err error) {
r1, _, e1 := syscall.Syscall6(procLockFileEx.Addr(), 6, uintptr(file), uintptr(flags), uintptr(reserved), uintptr(bytesLow), uintptr(bytesHigh), uintptr(unsafe.Pointer(overlapped)))
if r1 == 0 {

View File

@ -5,138 +5,60 @@
package os
import (
"internal/syscall/windows"
"io"
"io/fs"
"runtime"
"sync"
"syscall"
"unsafe"
)
// Auxiliary information if the File describes a directory
type dirInfo struct {
// buf is a slice pointer so the slice header
// does not escape to the heap when returning
// buf to dirBufPool.
buf *[]byte // buffer for directory I/O
bufp int // location of next record in buf
vol uint32
}
const (
// dirBufSize is the size of the dirInfo buffer.
// The buffer must be big enough to hold at least a single entry.
// The filename alone can be 512 bytes (MAX_PATH*2), and the fixed part of
// the FILE_ID_BOTH_DIR_INFO structure is 105 bytes, so dirBufSize
// should not be set below 1024 bytes (512+105+safety buffer).
// Windows 8.1 and earlier only works with buffer sizes up to 64 kB.
dirBufSize = 64 * 1024 // 64kB
)
var dirBufPool = sync.Pool{
New: func() any {
// The buffer must be at least a block long.
buf := make([]byte, dirBufSize)
return &buf
},
}
func (d *dirInfo) close() {
if d.buf != nil {
dirBufPool.Put(d.buf)
d.buf = nil
}
}
func (file *File) readdir(n int, mode readdirMode) (names []string, dirents []DirEntry, infos []FileInfo, err error) {
// If this file has no dirinfo, create one.
var infoClass uint32 = windows.FileIdBothDirectoryInfo
needdata := true
if file.dirinfo == nil {
// vol is used by os.SameFile.
// It is safe to query it once and reuse the value.
// Hard links are not allowed to reference files in other volumes.
// Junctions and symbolic links can reference files and directories in other volumes,
// but the reparse point should still live in the parent volume.
var vol uint32
err = windows.GetVolumeInformationByHandle(file.pfd.Sysfd, nil, 0, &vol, nil, nil, nil, 0)
runtime.KeepAlive(file)
needdata = false
file.dirinfo, err = openDir(file.name)
if err != nil {
err = &PathError{Op: "readdir", Path: file.name, Err: err}
return
}
infoClass = windows.FileIdBothDirectoryRestartInfo
file.dirinfo = new(dirInfo)
file.dirinfo.buf = dirBufPool.Get().(*[]byte)
file.dirinfo.vol = vol
}
d := file.dirinfo
wantAll := n <= 0
if wantAll {
n = -1
}
for n != 0 {
// Refill the buffer if necessary
if d.bufp == 0 {
err = windows.GetFileInformationByHandleEx(file.pfd.Sysfd, infoClass, (*byte)(unsafe.Pointer(&(*d.buf)[0])), uint32(len(*d.buf)))
d := &file.dirinfo.data
for n != 0 && !file.dirinfo.isempty {
if needdata {
e := syscall.FindNextFile(file.dirinfo.h, d)
runtime.KeepAlive(file)
if err != nil {
if err == syscall.ERROR_NO_MORE_FILES {
if e != nil {
if e == syscall.ERROR_NO_MORE_FILES {
break
}
if infoClass == windows.FileIdBothDirectoryRestartInfo && err == syscall.ERROR_FILE_NOT_FOUND {
// GetFileInformationByHandleEx doesn't document the return error codes when the info class is FileIdBothDirectoryRestartInfo,
// but MS-FSA 2.1.5.6.3 [1] specifies that the underlying file system driver should return STATUS_NO_SUCH_FILE when
// reading an empty root directory, which is mapped to ERROR_FILE_NOT_FOUND by Windows.
// Note that some file system drivers may never return this error code, as the spec allows to return the "." and ".."
// entries in such cases, making the directory appear non-empty.
// The chances of false positive are very low, as we know that the directory exists, else GetVolumeInformationByHandle
// would have failed, and that the handle is still valid, as we haven't closed it.
// See go.dev/issue/61159.
// [1] https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fsa/fa8194e0-53ec-413b-8315-e8fa85396fd8
break
}
if s, _ := file.Stat(); s != nil && !s.IsDir() {
err = &PathError{Op: "readdir", Path: file.name, Err: syscall.ENOTDIR}
} else {
err = &PathError{Op: "GetFileInformationByHandleEx", Path: file.name, Err: err}
err = &PathError{Op: "FindNextFile", Path: file.name, Err: e}
return
}
return
}
infoClass = windows.FileIdBothDirectoryInfo
}
// Drain the buffer
var islast bool
for n != 0 && !islast {
info := (*windows.FILE_ID_BOTH_DIR_INFO)(unsafe.Pointer(&(*d.buf)[d.bufp]))
d.bufp += int(info.NextEntryOffset)
islast = info.NextEntryOffset == 0
if islast {
d.bufp = 0
}
nameslice := unsafe.Slice(&info.FileName[0], info.FileNameLength/2)
name := syscall.UTF16ToString(nameslice)
if name == "." || name == ".." { // Useless names
continue
}
if mode == readdirName {
names = append(names, name)
needdata = true
name := syscall.UTF16ToString(d.FileName[0:])
if name == "." || name == ".." { // Useless names
continue
}
if mode == readdirName {
names = append(names, name)
} else {
f := newFileStatFromWin32finddata(d)
f.name = name
f.path = file.dirinfo.path
f.appendNameToPath = true
if mode == readdirDirEntry {
dirents = append(dirents, dirEntry{f})
} else {
f := newFileStatFromFileIDBothDirInfo(info)
f.name = name
f.vol = d.vol
// f.path is used by os.SameFile to decide if it needs
// to fetch vol, idxhi and idxlo. But these are already set,
// so set f.path to "" to prevent os.SameFile doing it again.
f.path = ""
if mode == readdirDirEntry {
dirents = append(dirents, dirEntry{f})
} else {
infos = append(infos, f)
}
infos = append(infos, f)
}
n--
}
n--
}
if !wantAll && len(names)+len(dirents)+len(infos) == 0 {
return nil, nil, nil, io.EOF

View File

@ -87,6 +87,18 @@ func NewFile(fd uintptr, name string) *File {
return newFile(h, name, "file")
}
// Auxiliary information if the File describes a directory
type dirInfo struct {
h syscall.Handle // search handle created with FindFirstFile
data syscall.Win32finddata
path string
isempty bool // set if FindFirstFile returns ERROR_FILE_NOT_FOUND
}
func (d *dirInfo) close() error {
return syscall.FindClose(d.h)
}
func epipecheck(file *File, e error) {
}
@ -94,6 +106,63 @@ func epipecheck(file *File, e error) {
// On Unix-like systems, it is "/dev/null"; on Windows, "NUL".
const DevNull = "NUL"
func openDir(name string) (d *dirInfo, e error) {
var mask string
path := fixLongPath(name)
if len(path) == 2 && path[1] == ':' { // it is a drive letter, like C:
mask = path + `*`
} else if len(path) > 0 {
lc := path[len(path)-1]
if lc == '/' || lc == '\\' {
mask = path + `*`
} else {
mask = path + `\*`
}
} else {
mask = `\*`
}
maskp, e := syscall.UTF16PtrFromString(mask)
if e != nil {
return nil, e
}
d = new(dirInfo)
d.h, e = syscall.FindFirstFile(maskp, &d.data)
if e != nil {
// FindFirstFile returns ERROR_FILE_NOT_FOUND when
// no matching files can be found. Then, if directory
// exists, we should proceed.
// If FindFirstFile failed because name does not point
// to a directory, we should return ENOTDIR.
var fa syscall.Win32FileAttributeData
pathp, e1 := syscall.UTF16PtrFromString(path)
if e1 != nil {
return nil, e
}
e1 = syscall.GetFileAttributesEx(pathp, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
if e1 != nil {
return nil, e
}
if fa.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY == 0 {
return nil, syscall.ENOTDIR
}
if e != syscall.ERROR_FILE_NOT_FOUND {
return nil, e
}
d.isempty = true
}
d.path = path
if !isAbs(d.path) {
d.path, e = syscall.FullPath(d.path)
if e != nil {
d.close()
return nil, e
}
}
return d, nil
}
// openFileNolog is the Windows implementation of OpenFile.
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
if name == "" {

View File

@ -1453,3 +1453,15 @@ func TestNewFileInvalid(t *testing.T) {
t.Errorf("NewFile(InvalidHandle) got %v want nil", f)
}
}
func TestReadDirPipe(t *testing.T) {
dir := `\\.\pipe\`
fi, err := os.Stat(dir)
if err != nil || !fi.IsDir() {
t.Skipf("%s is not a directory", dir)
}
_, err = os.ReadDir(dir)
if err != nil {
t.Errorf("ReadDir(%q) = %v", dir, err)
}
}

View File

@ -16,7 +16,7 @@ import (
type fileStat struct {
name string
// from ByHandleFileInformation, Win32FileAttributeData, Win32finddata, and GetFileInformationByHandleEx
// from ByHandleFileInformation, Win32FileAttributeData and Win32finddata
FileAttributes uint32
CreationTime syscall.Filetime
LastAccessTime syscall.Filetime
@ -24,7 +24,7 @@ type fileStat struct {
FileSizeHigh uint32
FileSizeLow uint32
// from Win32finddata and GetFileInformationByHandleEx
// from Win32finddata
ReparseTag uint32
// what syscall.GetFileType returns
@ -32,10 +32,11 @@ type fileStat struct {
// used to implement SameFile
sync.Mutex
path string
vol uint32
idxhi uint32
idxlo uint32
path string
vol uint32
idxhi uint32
idxlo uint32
appendNameToPath bool
}
// newFileStatFromGetFileInformationByHandle calls GetFileInformationByHandle
@ -79,26 +80,6 @@ func newFileStatFromGetFileInformationByHandle(path string, h syscall.Handle) (f
}, nil
}
// newFileStatFromFileIDBothDirInfo copies all required information
// from windows.FILE_ID_BOTH_DIR_INFO d into the newly created fileStat.
func newFileStatFromFileIDBothDirInfo(d *windows.FILE_ID_BOTH_DIR_INFO) *fileStat {
// The FILE_ID_BOTH_DIR_INFO MSDN documentations isn't completely correct.
// FileAttributes can contain any file attributes that is currently set on the file,
// not just the ones documented.
// EaSize contains the reparse tag if the file is a reparse point.
return &fileStat{
FileAttributes: d.FileAttributes,
CreationTime: d.CreationTime,
LastAccessTime: d.LastAccessTime,
LastWriteTime: d.LastWriteTime,
FileSizeHigh: uint32(d.EndOfFile >> 32),
FileSizeLow: uint32(d.EndOfFile),
ReparseTag: d.EaSize,
idxhi: uint32(d.FileID >> 32),
idxlo: uint32(d.FileID),
}
}
// newFileStatFromWin32finddata copies all required information
// from syscall.Win32finddata d into the newly created fileStat.
func newFileStatFromWin32finddata(d *syscall.Win32finddata) *fileStat {
@ -188,7 +169,13 @@ func (fs *fileStat) loadFileId() error {
// already done
return nil
}
pathp, err := syscall.UTF16PtrFromString(fs.path)
var path string
if fs.appendNameToPath {
path = fs.path + `\` + fs.name
} else {
path = fs.path
}
pathp, err := syscall.UTF16PtrFromString(path)
if err != nil {
return err
}