os,syscall: File.Stat to use file handle for directories on Windows

Updates syscall.Open to support opening directories via CreateFileW.

CreateFileW handles are more versatile than FindFirstFile handles.
They can be used in Win32 APIs like GetFileInformationByHandle and
SetFilePointerEx, which are needed by some Go APIs.

Fixes #52747
Fixes #36019

Change-Id: I26a00cef9844fb4abeeb18d2f9d854162a146651
Reviewed-on: https://go-review.googlesource.com/c/go/+/405275
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Patrik Nyblom <pnyb@google.com>
Reviewed-by: Alex Brainman <alex.brainman@gmail.com>
Reviewed-by: Bryan Mills <bcmills@google.com>
Run-TryBot: Quim Muntal <quimmuntal@gmail.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
qmuntal 2022-05-10 09:52:20 +02:00 committed by Roland Shoemaker
parent dc6b7c86df
commit 0f0aa5d8a6
9 changed files with 115 additions and 74 deletions

View file

@ -268,7 +268,6 @@ const (
kindNet fileKind = iota kindNet fileKind = iota
kindFile kindFile
kindConsole kindConsole
kindDir
kindPipe kindPipe
) )
@ -286,12 +285,10 @@ func (fd *FD) Init(net string, pollable bool) (string, error) {
} }
switch net { switch net {
case "file": case "file", "dir":
fd.kind = kindFile fd.kind = kindFile
case "console": case "console":
fd.kind = kindConsole fd.kind = kindConsole
case "dir":
fd.kind = kindDir
case "pipe": case "pipe":
fd.kind = kindPipe fd.kind = kindPipe
case "tcp", "tcp4", "tcp6", case "tcp", "tcp4", "tcp6",
@ -371,8 +368,6 @@ func (fd *FD) destroy() error {
case kindNet: case kindNet:
// The net package uses the CloseFunc variable for testing. // The net package uses the CloseFunc variable for testing.
err = CloseFunc(fd.Sysfd) err = CloseFunc(fd.Sysfd)
case kindDir:
err = syscall.FindClose(fd.Sysfd)
default: default:
err = syscall.CloseHandle(fd.Sysfd) err = syscall.CloseHandle(fd.Sysfd)
} }
@ -1008,15 +1003,6 @@ func (fd *FD) Seek(offset int64, whence int) (int64, error) {
return syscall.Seek(fd.Sysfd, offset, whence) return syscall.Seek(fd.Sysfd, offset, whence)
} }
// FindNextFile wraps syscall.FindNextFile.
func (fd *FD) FindNextFile(data *syscall.Win32finddata) error {
if err := fd.incref(); err != nil {
return err
}
defer fd.decref()
return syscall.FindNextFile(fd.Sysfd, data)
}
// Fchmod updates syscall.ByHandleFileInformation.Fileattributes when needed. // Fchmod updates syscall.ByHandleFileInformation.Fileattributes when needed.
func (fd *FD) Fchmod(mode uint32) error { func (fd *FD) Fchmod(mode uint32) error {
if err := fd.incref(); err != nil { if err := fd.incref(); err != nil {

View file

@ -11,8 +11,15 @@ import (
) )
func (file *File) readdir(n int, mode readdirMode) (names []string, dirents []DirEntry, infos []FileInfo, err error) { func (file *File) readdir(n int, mode readdirMode) (names []string, dirents []DirEntry, infos []FileInfo, err error) {
if !file.isdir() { // If this file has no dirinfo, create one.
return nil, nil, nil, &PathError{Op: "readdir", Path: file.name, Err: syscall.ENOTDIR} needdata := true
if file.dirinfo == nil {
needdata = false
file.dirinfo, err = openDir(file.name)
if err != nil {
err = &PathError{Op: "readdir", Path: file.name, Err: err}
return
}
} }
wantAll := n <= 0 wantAll := n <= 0
if wantAll { if wantAll {
@ -20,8 +27,8 @@ func (file *File) readdir(n int, mode readdirMode) (names []string, dirents []Di
} }
d := &file.dirinfo.data d := &file.dirinfo.data
for n != 0 && !file.dirinfo.isempty { for n != 0 && !file.dirinfo.isempty {
if file.dirinfo.needdata { if needdata {
e := file.pfd.FindNextFile(d) e := syscall.FindNextFile(file.dirinfo.h, d)
runtime.KeepAlive(file) runtime.KeepAlive(file)
if e != nil { if e != nil {
if e == syscall.ERROR_NO_MORE_FILES { if e == syscall.ERROR_NO_MORE_FILES {
@ -32,7 +39,7 @@ func (file *File) readdir(n int, mode readdirMode) (names []string, dirents []Di
} }
} }
} }
file.dirinfo.needdata = true needdata = true
name := syscall.UTF16ToString(d.FileName[0:]) name := syscall.UTF16ToString(d.FileName[0:])
if name == "." || name == ".." { // Useless names if name == "." || name == ".." { // Useless names
continue continue

View file

@ -225,10 +225,6 @@ func (f *File) WriteAt(b []byte, off int64) (n int, err error) {
// relative to the current offset, and 2 means relative to the end. // relative to the current offset, and 2 means relative to the end.
// It returns the new offset and an error, if any. // It returns the new offset and an error, if any.
// The behavior of Seek on a file opened with O_APPEND is not specified. // The behavior of Seek on a file opened with O_APPEND is not specified.
//
// If f is a directory, the behavior of Seek varies by operating
// system; you can seek to the beginning of the directory on Unix-like
// operating systems, but not on Windows.
func (f *File) Seek(offset int64, whence int) (ret int64, err error) { func (f *File) Seek(offset int64, whence int) (ret int64, err error) {
if err := f.checkValid("seek"); err != nil { if err := f.checkValid("seek"); err != nil {
return 0, err return 0, err

View file

@ -86,10 +86,14 @@ func NewFile(fd uintptr, name string) *File {
// Auxiliary information if the File describes a directory // Auxiliary information if the File describes a directory
type dirInfo struct { type dirInfo struct {
data syscall.Win32finddata h syscall.Handle // search handle created with FindFirstFile
needdata bool data syscall.Win32finddata
path string path string
isempty bool // set if FindFirstFile returns ERROR_FILE_NOT_FOUND 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) { func epipecheck(file *File, e error) {
@ -99,17 +103,7 @@ func epipecheck(file *File, e error) {
// On Unix-like systems, it is "/dev/null"; on Windows, "NUL". // On Unix-like systems, it is "/dev/null"; on Windows, "NUL".
const DevNull = "NUL" const DevNull = "NUL"
func (f *file) isdir() bool { return f != nil && f.dirinfo != nil } func openDir(name string) (d *dirInfo, e error) {
func openFile(name string, flag int, perm FileMode) (file *File, err error) {
r, e := syscall.Open(fixLongPath(name), flag|syscall.O_CLOEXEC, syscallMode(perm))
if e != nil {
return nil, e
}
return newFile(r, name, "file"), nil
}
func openDir(name string) (file *File, err error) {
var mask string var mask string
path := fixLongPath(name) path := fixLongPath(name)
@ -130,25 +124,27 @@ func openDir(name string) (file *File, err error) {
if e != nil { if e != nil {
return nil, e return nil, e
} }
d := new(dirInfo) d = new(dirInfo)
r, e := syscall.FindFirstFile(maskp, &d.data) d.h, e = syscall.FindFirstFile(maskp, &d.data)
if e != nil { if e != nil {
// FindFirstFile returns ERROR_FILE_NOT_FOUND when // FindFirstFile returns ERROR_FILE_NOT_FOUND when
// no matching files can be found. Then, if directory // no matching files can be found. Then, if directory
// exists, we should proceed. // exists, we should proceed.
if e != syscall.ERROR_FILE_NOT_FOUND { // If FindFirstFile failed because name does not point
return nil, e // to a directory, we should return ENOTDIR.
}
var fa syscall.Win32FileAttributeData var fa syscall.Win32FileAttributeData
pathp, e := syscall.UTF16PtrFromString(path) pathp, e1 := syscall.UTF16PtrFromString(path)
if e != nil { if e1 != nil {
return nil, e return nil, e
} }
e = syscall.GetFileAttributesEx(pathp, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa))) e1 = syscall.GetFileAttributesEx(pathp, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
if e != nil { if e1 != nil {
return nil, e return nil, e
} }
if fa.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY == 0 { if fa.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY == 0 {
return nil, syscall.ENOTDIR
}
if e != syscall.ERROR_FILE_NOT_FOUND {
return nil, e return nil, e
} }
d.isempty = true d.isempty = true
@ -157,12 +153,11 @@ func openDir(name string) (file *File, err error) {
if !isAbs(d.path) { if !isAbs(d.path) {
d.path, e = syscall.FullPath(d.path) d.path, e = syscall.FullPath(d.path)
if e != nil { if e != nil {
d.close()
return nil, e return nil, e
} }
} }
f := newFile(r, name, "dir") return d, nil
f.dirinfo = d
return f, nil
} }
// openFileNolog is the Windows implementation of OpenFile. // openFileNolog is the Windows implementation of OpenFile.
@ -170,28 +165,36 @@ func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
if name == "" { if name == "" {
return nil, &PathError{Op: "open", Path: name, Err: syscall.ENOENT} return nil, &PathError{Op: "open", Path: name, Err: syscall.ENOENT}
} }
r, errf := openFile(name, flag, perm) path := fixLongPath(name)
if errf == nil { r, e := syscall.Open(path, flag|syscall.O_CLOEXEC, syscallMode(perm))
return r, nil if e != nil {
} // We should return EISDIR when we are trying to open a directory with write access.
r, errd := openDir(name) if e == syscall.ERROR_ACCESS_DENIED && (flag&O_WRONLY != 0 || flag&O_RDWR != 0) {
if errd == nil { pathp, e1 := syscall.UTF16PtrFromString(path)
if flag&O_WRONLY != 0 || flag&O_RDWR != 0 { if e1 == nil {
r.Close() var fa syscall.Win32FileAttributeData
return nil, &PathError{Op: "open", Path: name, Err: syscall.EISDIR} e1 = syscall.GetFileAttributesEx(pathp, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
if e1 == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
e = syscall.EISDIR
}
}
} }
return r, nil return nil, &PathError{Op: "open", Path: name, Err: e}
} }
return nil, &PathError{Op: "open", Path: name, Err: errf} f, e := newFile(r, name, "file"), nil
if e != nil {
return nil, &PathError{Op: "open", Path: name, Err: e}
}
return f, nil
} }
func (file *file) close() error { func (file *file) close() error {
if file == nil { if file == nil {
return syscall.EINVAL return syscall.EINVAL
} }
if file.isdir() && file.dirinfo.isempty { if file.dirinfo != nil {
// "special" empty directories file.dirinfo.close()
return nil file.dirinfo = nil
} }
var err error var err error
if e := file.pfd.Close(); e != nil { if e := file.pfd.Close(); e != nil {
@ -211,6 +214,12 @@ func (file *file) close() error {
// relative to the current offset, and 2 means relative to the end. // relative to the current offset, and 2 means relative to the end.
// It returns the new offset and an error, if any. // It returns the new offset and an error, if any.
func (f *File) seek(offset int64, whence int) (ret int64, err error) { func (f *File) seek(offset int64, whence int) (ret int64, err error) {
if f.dirinfo != nil {
// Free cached dirinfo, so we allocate a new one if we
// access this file as a directory again. See #35767 and #37161.
f.dirinfo.close()
f.dirinfo = nil
}
ret, err = f.pfd.Seek(offset, whence) ret, err = f.pfd.Seek(offset, whence)
runtime.KeepAlive(f) runtime.KeepAlive(f)
return ret, err return ret, err

View file

@ -2564,9 +2564,6 @@ func TestUserHomeDir(t *testing.T) {
} }
func TestDirSeek(t *testing.T) { func TestDirSeek(t *testing.T) {
if runtime.GOOS == "windows" {
testenv.SkipFlaky(t, 36019)
}
wd, err := Getwd() wd, err := Getwd()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View file

@ -1252,3 +1252,28 @@ func TestWindowsReadlink(t *testing.T) {
mklink(t, "relfilelink", "file") mklink(t, "relfilelink", "file")
testReadlink(t, "relfilelink", "file") testReadlink(t, "relfilelink", "file")
} }
func TestOpenDirTOCTOU(t *testing.T) {
// Check opened directories can't be renamed until the handle is closed.
// See issue 52747.
tmpdir := t.TempDir()
dir := filepath.Join(tmpdir, "dir")
if err := os.Mkdir(dir, 0777); err != nil {
t.Fatal(err)
}
f, err := os.Open(dir)
if err != nil {
t.Fatal(err)
}
newpath := filepath.Join(tmpdir, "dir1")
err = os.Rename(dir, newpath)
if err == nil || !errors.Is(err, windows.ERROR_SHARING_VIOLATION) {
f.Close()
t.Fatalf("Rename(%q, %q) = %v; want windows.ERROR_SHARING_VIOLATION", dir, newpath, err)
}
f.Close()
err = os.Rename(dir, newpath)
if err != nil {
t.Error(err)
}
}

View file

@ -16,10 +16,6 @@ func (file *File) Stat() (FileInfo, error) {
if file == nil { if file == nil {
return nil, ErrInvalid return nil, ErrInvalid
} }
if file.isdir() {
// I don't know any better way to do that for directory
return Stat(file.dirinfo.path)
}
return statHandle(file.name, file.pfd.Sysfd) return statHandle(file.name, file.pfd.Sysfd)
} }

View file

@ -371,8 +371,11 @@ func Open(path string, mode int, perm uint32) (fd Handle, err error) {
} }
} }
} }
h, e := CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0) if createmode == OPEN_EXISTING && access == GENERIC_READ {
return h, e // Necessary for opening directory handles.
attrs |= FILE_FLAG_BACKUP_SEMANTICS
}
return CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0)
} }
func Read(fd Handle, p []byte) (n int, err error) { func Read(fd Handle, p []byte) (n int, err error) {

View file

@ -15,6 +15,28 @@ import (
"testing" "testing"
) )
func TestOpen_Dir(t *testing.T) {
dir := t.TempDir()
h, err := syscall.Open(dir, syscall.O_RDONLY, 0)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
syscall.CloseHandle(h)
h, err = syscall.Open(dir, syscall.O_RDONLY|syscall.O_TRUNC, 0)
if err == nil {
t.Error("Open should have failed")
} else {
syscall.CloseHandle(h)
}
h, err = syscall.Open(dir, syscall.O_RDONLY|syscall.O_CREAT, 0)
if err == nil {
t.Error("Open should have failed")
} else {
syscall.CloseHandle(h)
}
}
func TestWin32finddata(t *testing.T) { func TestWin32finddata(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()