Skip to content

Commit f108158

Browse files
committed
os: use CreateFile for Stat of symlinks
Stat uses Windows FindFirstFile + CreateFile to gather symlink information - FindFirstFile determines if file is a symlink, and then CreateFile follows symlink to capture target details. Lstat only uses FindFirstFile. This CL replaces current approach with just a call to CreateFile. Lstat uses FILE_FLAG_OPEN_REPARSE_POINT flag, that instructs CreateFile not to follow symlink. Other than that both Stat and Lstat look the same now. New code is simpler. CreateFile + GetFileInformationByHandle (unlike FindFirstFile) does not report reparse tag of a file. I tried to ignore reparse tag altogether. And it works for symlinks and mount points. Unfortunately (see moby/moby#37026), files on deduped disk volumes are reported with FILE_ATTRIBUTE_REPARSE_POINT attribute set and reparse tag set to IO_REPARSE_TAG_DEDUP. So, if we ignore reparse tag, Lstat interprets deduped volume files as symlinks. That is incorrect. So I had to add GetFileInformationByHandleEx call to gather reparse tag after calling CreateFile and GetFileInformationByHandle. Fixes #27225 Fixes #27515 Change-Id: If60233bcf18836c147597cc17450d82f3f88c623 Reviewed-on: https://go-review.googlesource.com/c/143578 Run-TryBot: Alex Brainman <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Reviewed-by: Kirill Kolyshkin <[email protected]>
1 parent d154ef6 commit f108158

File tree

6 files changed

+126
-122
lines changed

6 files changed

+126
-122
lines changed

src/internal/syscall/windows/mksyscall.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
package windows
66

7-
//go:generate go run $GOROOT/src/syscall/mksyscall_windows.go -output zsyscall_windows.go syscall_windows.go security_windows.go psapi_windows.go
7+
//go:generate go run $GOROOT/src/syscall/mksyscall_windows.go -output zsyscall_windows.go syscall_windows.go security_windows.go psapi_windows.go symlink_windows.go

src/internal/syscall/windows/symlink_windows.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,29 @@ const (
1111

1212
// symlink support for CreateSymbolicLink() starting with Windows 10 (1703, v10.0.14972)
1313
SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 0x2
14+
15+
// FileInformationClass values
16+
FileBasicInfo = 0 // FILE_BASIC_INFO
17+
FileStandardInfo = 1 // FILE_STANDARD_INFO
18+
FileNameInfo = 2 // FILE_NAME_INFO
19+
FileStreamInfo = 7 // FILE_STREAM_INFO
20+
FileCompressionInfo = 8 // FILE_COMPRESSION_INFO
21+
FileAttributeTagInfo = 9 // FILE_ATTRIBUTE_TAG_INFO
22+
FileIdBothDirectoryInfo = 0xa // FILE_ID_BOTH_DIR_INFO
23+
FileIdBothDirectoryRestartInfo = 0xb // FILE_ID_BOTH_DIR_INFO
24+
FileRemoteProtocolInfo = 0xd // FILE_REMOTE_PROTOCOL_INFO
25+
FileFullDirectoryInfo = 0xe // FILE_FULL_DIR_INFO
26+
FileFullDirectoryRestartInfo = 0xf // FILE_FULL_DIR_INFO
27+
FileStorageInfo = 0x10 // FILE_STORAGE_INFO
28+
FileAlignmentInfo = 0x11 // FILE_ALIGNMENT_INFO
29+
FileIdInfo = 0x12 // FILE_ID_INFO
30+
FileIdExtdDirectoryInfo = 0x13 // FILE_ID_EXTD_DIR_INFO
31+
FileIdExtdDirectoryRestartInfo = 0x14 // FILE_ID_EXTD_DIR_INFO
1432
)
33+
34+
type FILE_ATTRIBUTE_TAG_INFO struct {
35+
FileAttributes uint32
36+
ReparseTag uint32
37+
}
38+
39+
//sys GetFileInformationByHandleEx(handle syscall.Handle, class uint32, info *byte, bufsize uint32) (err error)

src/internal/syscall/windows/zsyscall_windows.go

Lines changed: 35 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/os/stat_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,10 +250,6 @@ func TestFileAndSymlinkStats(t *testing.T) {
250250

251251
// see issue 27225 for details
252252
func TestSymlinkWithTrailingSlash(t *testing.T) {
253-
if runtime.GOOS == "windows" {
254-
t.Skip("skipping on windows; issue 27225")
255-
}
256-
257253
testenv.MustHaveSymlink(t)
258254

259255
tmpdir, err := ioutil.TempDir("", "TestSymlinkWithTrailingSlash")
@@ -274,7 +270,11 @@ func TestSymlinkWithTrailingSlash(t *testing.T) {
274270
}
275271
dirlinkWithSlash := dirlink + string(os.PathSeparator)
276272

277-
testDirStats(t, dirlinkWithSlash)
273+
if runtime.GOOS == "windows" {
274+
testSymlinkStats(t, dirlinkWithSlash, true)
275+
} else {
276+
testDirStats(t, dirlinkWithSlash)
277+
}
278278

279279
fi1, err := os.Stat(dir)
280280
if err != nil {

src/os/stat_windows.go

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
package os
66

77
import (
8+
"internal/syscall/windows"
89
"syscall"
10+
"unsafe"
911
)
1012

1113
// isNulName returns true if name is NUL file name.
@@ -58,33 +60,59 @@ func (file *File) Stat() (FileInfo, error) {
5860
return fs, err
5961
}
6062

61-
// statNolog implements Stat for Windows.
62-
func statNolog(name string) (FileInfo, error) {
63+
// stat implements both Stat and Lstat of a file.
64+
func stat(funcname, name string, createFileAttrs uint32) (FileInfo, error) {
6365
if len(name) == 0 {
64-
return nil, &PathError{"Stat", name, syscall.Errno(syscall.ERROR_PATH_NOT_FOUND)}
66+
return nil, &PathError{funcname, name, syscall.Errno(syscall.ERROR_PATH_NOT_FOUND)}
6567
}
6668
if isNulName(name) {
6769
return &devNullStat, nil
6870
}
6971
namep, err := syscall.UTF16PtrFromString(fixLongPath(name))
7072
if err != nil {
71-
return nil, &PathError{"Stat", name, err}
73+
return nil, &PathError{funcname, name, err}
7274
}
73-
fs, err := newFileStatFromGetFileAttributesExOrFindFirstFile(name, namep)
74-
if err != nil {
75-
return nil, err
75+
76+
// Try GetFileAttributesEx first, because it is faster than CreateFile.
77+
// See https://golang.org/issues/19922#issuecomment-300031421 for details.
78+
var fa syscall.Win32FileAttributeData
79+
err = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
80+
if err == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 {
81+
// Not a symlink.
82+
fs := &fileStat{
83+
path: name,
84+
FileAttributes: fa.FileAttributes,
85+
CreationTime: fa.CreationTime,
86+
LastAccessTime: fa.LastAccessTime,
87+
LastWriteTime: fa.LastWriteTime,
88+
FileSizeHigh: fa.FileSizeHigh,
89+
FileSizeLow: fa.FileSizeLow,
90+
}
91+
// Gather full path to be used by os.SameFile later.
92+
if !isAbs(fs.path) {
93+
fs.path, err = syscall.FullPath(fs.path)
94+
if err != nil {
95+
return nil, &PathError{"FullPath", name, err}
96+
}
97+
}
98+
fs.name = basename(name)
99+
return fs, nil
76100
}
77-
if !fs.isSymlink() {
78-
err = fs.updatePathAndName(name)
101+
// GetFileAttributesEx fails with ERROR_SHARING_VIOLATION error for
102+
// files, like c:\pagefile.sys. Use FindFirstFile for such files.
103+
if err == windows.ERROR_SHARING_VIOLATION {
104+
var fd syscall.Win32finddata
105+
sh, err := syscall.FindFirstFile(namep, &fd)
79106
if err != nil {
80-
return nil, err
107+
return nil, &PathError{"FindFirstFile", name, err}
81108
}
82-
return fs, nil
109+
syscall.FindClose(sh)
110+
return newFileStatFromWin32finddata(&fd), nil
83111
}
84-
// Use Windows I/O manager to dereference the symbolic link, as per
85-
// https://blogs.msdn.microsoft.com/oldnewthing/20100212-00/?p=14963/
112+
113+
// Finally use CreateFile.
86114
h, err := syscall.CreateFile(namep, 0, 0, nil,
87-
syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
115+
syscall.OPEN_EXISTING, createFileAttrs, 0)
88116
if err != nil {
89117
return nil, &PathError{"CreateFile", name, err}
90118
}
@@ -93,25 +121,16 @@ func statNolog(name string) (FileInfo, error) {
93121
return newFileStatFromGetFileInformationByHandle(name, h)
94122
}
95123

124+
// statNolog implements Stat for Windows.
125+
func statNolog(name string) (FileInfo, error) {
126+
return stat("Stat", name, syscall.FILE_FLAG_BACKUP_SEMANTICS)
127+
}
128+
96129
// lstatNolog implements Lstat for Windows.
97130
func lstatNolog(name string) (FileInfo, error) {
98-
if len(name) == 0 {
99-
return nil, &PathError{"Lstat", name, syscall.Errno(syscall.ERROR_PATH_NOT_FOUND)}
100-
}
101-
if isNulName(name) {
102-
return &devNullStat, nil
103-
}
104-
namep, err := syscall.UTF16PtrFromString(fixLongPath(name))
105-
if err != nil {
106-
return nil, &PathError{"Lstat", name, err}
107-
}
108-
fs, err := newFileStatFromGetFileAttributesExOrFindFirstFile(name, namep)
109-
if err != nil {
110-
return nil, err
111-
}
112-
err = fs.updatePathAndName(name)
113-
if err != nil {
114-
return nil, err
115-
}
116-
return fs, nil
131+
attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS)
132+
// Use FILE_FLAG_OPEN_REPARSE_POINT, otherwise CreateFile will follow symlink.
133+
// See https://docs.microsoft.com/en-us/windows/desktop/FileIO/symbolic-link-effects-on-file-systems-functions#createfile-and-createfiletransacted
134+
attrs |= syscall.FILE_FLAG_OPEN_REPARSE_POINT
135+
return stat("Lstat", name, attrs)
117136
}

src/os/types_windows.go

Lines changed: 8 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ func newFileStatFromGetFileInformationByHandle(path string, h syscall.Handle) (f
4747
if err != nil {
4848
return nil, &PathError{"GetFileInformationByHandle", path, err}
4949
}
50+
51+
var ti windows.FILE_ATTRIBUTE_TAG_INFO
52+
err = windows.GetFileInformationByHandleEx(h, windows.FileAttributeTagInfo, (*byte)(unsafe.Pointer(&ti)), uint32(unsafe.Sizeof(ti)))
53+
if err != nil {
54+
return nil, &PathError{"GetFileInformationByHandleEx", path, err}
55+
}
56+
5057
return &fileStat{
5158
name: basename(path),
5259
FileAttributes: d.FileAttributes,
@@ -58,6 +65,7 @@ func newFileStatFromGetFileInformationByHandle(path string, h syscall.Handle) (f
5865
vol: d.VolumeSerialNumber,
5966
idxhi: d.FileIndexHigh,
6067
idxlo: d.FileIndexLow,
68+
Reserved0: ti.ReparseTag,
6169
// fileStat.path is used by os.SameFile to decide if it needs
6270
// to fetch vol, idxhi and idxlo. But these are already set,
6371
// so set fileStat.path to "" to prevent os.SameFile doing it again.
@@ -78,67 +86,6 @@ func newFileStatFromWin32finddata(d *syscall.Win32finddata) *fileStat {
7886
}
7987
}
8088

81-
// newFileStatFromGetFileAttributesExOrFindFirstFile calls GetFileAttributesEx
82-
// and FindFirstFile to gather all required information about the provided file path pathp.
83-
func newFileStatFromGetFileAttributesExOrFindFirstFile(path string, pathp *uint16) (*fileStat, error) {
84-
// As suggested by Microsoft, use GetFileAttributes() to acquire the file information,
85-
// and if it's a reparse point use FindFirstFile() to get the tag:
86-
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa363940(v=vs.85).aspx
87-
// Notice that always calling FindFirstFile can create performance problems
88-
// (https://golang.org/issues/19922#issuecomment-300031421)
89-
var fa syscall.Win32FileAttributeData
90-
err := syscall.GetFileAttributesEx(pathp, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
91-
if err == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 {
92-
// Not a symlink.
93-
return &fileStat{
94-
FileAttributes: fa.FileAttributes,
95-
CreationTime: fa.CreationTime,
96-
LastAccessTime: fa.LastAccessTime,
97-
LastWriteTime: fa.LastWriteTime,
98-
FileSizeHigh: fa.FileSizeHigh,
99-
FileSizeLow: fa.FileSizeLow,
100-
}, nil
101-
}
102-
// GetFileAttributesEx returns ERROR_INVALID_NAME if called
103-
// for invalid file name like "*.txt". Do not attempt to call
104-
// FindFirstFile with "*.txt", because FindFirstFile will
105-
// succeed. So just return ERROR_INVALID_NAME instead.
106-
// see https://golang.org/issue/24999 for details.
107-
if errno, _ := err.(syscall.Errno); errno == windows.ERROR_INVALID_NAME {
108-
return nil, &PathError{"GetFileAttributesEx", path, err}
109-
}
110-
// We might have symlink here. But some directories also have
111-
// FileAttributes FILE_ATTRIBUTE_REPARSE_POINT bit set.
112-
// For example, OneDrive directory is like that
113-
// (see golang.org/issue/22579 for details).
114-
// So use FindFirstFile instead to distinguish directories like
115-
// OneDrive from real symlinks (see instructions described at
116-
// https://blogs.msdn.microsoft.com/oldnewthing/20100212-00/?p=14963/
117-
// and in particular bits about using both FileAttributes and
118-
// Reserved0 fields).
119-
var fd syscall.Win32finddata
120-
sh, err := syscall.FindFirstFile(pathp, &fd)
121-
if err != nil {
122-
return nil, &PathError{"FindFirstFile", path, err}
123-
}
124-
syscall.FindClose(sh)
125-
126-
return newFileStatFromWin32finddata(&fd), nil
127-
}
128-
129-
func (fs *fileStat) updatePathAndName(name string) error {
130-
fs.path = name
131-
if !isAbs(fs.path) {
132-
var err error
133-
fs.path, err = syscall.FullPath(fs.path)
134-
if err != nil {
135-
return &PathError{"FullPath", name, err}
136-
}
137-
}
138-
fs.name = basename(name)
139-
return nil
140-
}
141-
14289
func (fs *fileStat) isSymlink() bool {
14390
// Use instructions described at
14491
// https://blogs.msdn.microsoft.com/oldnewthing/20100212-00/?p=14963/

0 commit comments

Comments
 (0)