Skip to content

Commit 7986e26

Browse files
committed
os: don't treat mount points as symbolic links
This CL changes the behavior of os.Lstat to stop setting the os.ModeSymlink type mode bit for mount points on Windows. As a result, filepath.EvalSymlinks no longer evaluates mount points, which was the cause of many inconsistencies and bugs. Additionally, os.Lstat starts setting the os.ModeIrregular type mode bit for all reparse tags on Windows, except for those that are explicitly supported by the os package, which, since this CL, doesn't include mount points. This helps to identify files that need special handling outside of the os package. This behavior is controlled by the `winsymlink` GODEBUG setting. For Go 1.23, it defaults to `winsymlink=1`. Previous versions default to `winsymlink=0`. Fixes #39786 Fixes #40176 Fixes #61893 Updates #63703 Updates #40180 Updates #63429 Cq-Include-Trybots: luci.golang.try:gotip-windows-amd64-longtest,gotip-windows-arm64 Change-Id: I2e7372ab8862f5062667d30db6958d972bce5407 Reviewed-on: https://go-review.googlesource.com/c/go/+/565136 Reviewed-by: Bryan Mills <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Michael Knyszek <[email protected]>
1 parent 90796f4 commit 7986e26

File tree

9 files changed

+253
-57
lines changed

9 files changed

+253
-57
lines changed

doc/godebug.md

+13
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,19 @@ for example,
126126
see the [runtime documentation](/pkg/runtime#hdr-Environment_Variables)
127127
and the [go command documentation](/cmd/go#hdr-Build_and_test_caching).
128128

129+
### Go 1.23
130+
131+
Go 1.23 changed the mode bits reported by [`os.Lstat`](/pkg/os#Lstat) and [`os.Stat`](/pkg/os#Stat)
132+
for reparse points, which can be controlled with the `winsymlink` setting.
133+
As of Go 1.23 (`winsymlink=1`), mount points no longer have [`os.ModeSymlink`](/pkg/os#ModeSymlink)
134+
set, and reparse points that are not symlinks, Unix sockets, or dedup files now
135+
always have [`os.ModeIrregular`](/pkg/os#ModeIrregular) set. As a result of these changes,
136+
[`filepath.EvalSymlinks`](/pkg/path/filepath#EvalSymlinks) no longer evaluates
137+
mount points, which was a source of many inconsistencies and bugs.
138+
At previous versions (`winsymlink=0`), mount points are treated as symlinks,
139+
and other reparse points with non-default [`os.ModeType`](/pkg/os#ModeType) bits
140+
(such as [`os.ModeDir`](/pkg/os#ModeDir)) do not have the `ModeIrregular` bit set.
141+
129142
### Go 1.22
130143

131144
Go 1.22 adds a configurable limit to control the maximum acceptable RSA key size
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
On Windows, the mode bits reported by [`os.Lstat`](/pkg/os#Lstat) and [`os.Stat`](/pkg/os#Stat)
2+
for reparse points changed. Mount points no longer have [`os.ModeSymlink`](/pkg/os#ModeSymlink) set,
3+
and reparse points that are not symlinks, Unix sockets, or dedup files now
4+
always have [`os.ModeIrregular`](/pkg/os#ModeIrregular) set.
5+
This behavior is controlled by the `winsymlink` setting.
6+
For Go 1.23, it defaults to `winsymlink=1`.
7+
Previous versions default to `winsymlink=0`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
On Windows, [`filepath.EvalSymlinks`](/pkg/path/filepath#EvalSymlinks) no longer evaluates
2+
mount points, which was a source of many inconsistencies and bugs.
3+
This behavior is controlled by the `winsymlink` setting.
4+
For Go 1.23, it defaults to `winsymlink=1`.
5+
Previous versions default to `winsymlink=0`.

src/internal/godebugs/table.go

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ var All = []Info{
4949
{Name: "tlsmaxrsasize", Package: "crypto/tls"},
5050
{Name: "tlsrsakex", Package: "crypto/tls", Changed: 22, Old: "1"},
5151
{Name: "tlsunsafeekm", Package: "crypto/tls", Changed: 22, Old: "1"},
52+
{Name: "winsymlink", Package: "os", Changed: 22, Old: "0"},
5253
{Name: "x509sha1", Package: "crypto/x509"},
5354
{Name: "x509usefallbackroots", Package: "crypto/x509"},
5455
{Name: "x509usepolicies", Package: "crypto/x509"},

src/os/os_windows_test.go

+26-22
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package os_test
77
import (
88
"errors"
99
"fmt"
10+
"internal/godebug"
1011
"internal/poll"
1112
"internal/syscall/windows"
1213
"internal/syscall/windows/registry"
@@ -27,6 +28,8 @@ import (
2728
"unsafe"
2829
)
2930

31+
var winsymlink = godebug.New("winsymlink")
32+
3033
// For TestRawConnReadWrite.
3134
type syscallDescriptor = syscall.Handle
3235

@@ -90,9 +93,10 @@ func TestSameWindowsFile(t *testing.T) {
9093
}
9194

9295
type dirLinkTest struct {
93-
name string
94-
mklink func(link, target string) error
95-
issueNo int // correspondent issue number (for broken tests)
96+
name string
97+
mklink func(link, target string) error
98+
issueNo int // correspondent issue number (for broken tests)
99+
isMountPoint bool
96100
}
97101

98102
func testDirLinks(t *testing.T, tests []dirLinkTest) {
@@ -140,8 +144,8 @@ func testDirLinks(t *testing.T, tests []dirLinkTest) {
140144
t.Errorf("failed to stat link %v: %v", link, err)
141145
continue
142146
}
143-
if !fi1.IsDir() {
144-
t.Errorf("%q should be a directory", link)
147+
if tp := fi1.Mode().Type(); tp != fs.ModeDir {
148+
t.Errorf("Stat(%q) is type %v; want %v", link, tp, fs.ModeDir)
145149
continue
146150
}
147151
if fi1.Name() != filepath.Base(link) {
@@ -158,13 +162,16 @@ func testDirLinks(t *testing.T, tests []dirLinkTest) {
158162
t.Errorf("failed to lstat link %v: %v", link, err)
159163
continue
160164
}
161-
if m := fi2.Mode(); m&fs.ModeSymlink == 0 {
162-
t.Errorf("%q should be a link, but is not (mode=0x%x)", link, uint32(m))
163-
continue
165+
var wantType fs.FileMode
166+
if test.isMountPoint && winsymlink.Value() != "0" {
167+
// Mount points are reparse points, and we no longer treat them as symlinks.
168+
wantType = fs.ModeIrregular
169+
} else {
170+
// This is either a real symlink, or a mount point treated as a symlink.
171+
wantType = fs.ModeSymlink
164172
}
165-
if m := fi2.Mode(); m&fs.ModeDir != 0 {
166-
t.Errorf("%q should be a link, not a directory (mode=0x%x)", link, uint32(m))
167-
continue
173+
if tp := fi2.Mode().Type(); tp != wantType {
174+
t.Errorf("Lstat(%q) is type %v; want %v", link, tp, fs.ModeDir)
168175
}
169176
}
170177
}
@@ -272,7 +279,8 @@ func TestDirectoryJunction(t *testing.T) {
272279
var tests = []dirLinkTest{
273280
{
274281
// Create link similar to what mklink does, by inserting \??\ at the front of absolute target.
275-
name: "standard",
282+
name: "standard",
283+
isMountPoint: true,
276284
mklink: func(link, target string) error {
277285
var t reparseData
278286
t.addSubstituteName(`\??\` + target)
@@ -282,7 +290,8 @@ func TestDirectoryJunction(t *testing.T) {
282290
},
283291
{
284292
// Do as junction utility https://learn.microsoft.com/en-us/sysinternals/downloads/junction does - set PrintNameLength to 0.
285-
name: "have_blank_print_name",
293+
name: "have_blank_print_name",
294+
isMountPoint: true,
286295
mklink: func(link, target string) error {
287296
var t reparseData
288297
t.addSubstituteName(`\??\` + target)
@@ -296,7 +305,8 @@ func TestDirectoryJunction(t *testing.T) {
296305
if mklinkSupportsJunctionLinks {
297306
tests = append(tests,
298307
dirLinkTest{
299-
name: "use_mklink_cmd",
308+
name: "use_mklink_cmd",
309+
isMountPoint: true,
300310
mklink: func(link, target string) error {
301311
output, err := testenv.Command(t, "cmd", "/c", "mklink", "/J", link, target).CombinedOutput()
302312
if err != nil {
@@ -1414,16 +1424,10 @@ func TestAppExecLinkStat(t *testing.T) {
14141424
if lfi.Name() != pythonExeName {
14151425
t.Errorf("Stat %s: got %q, but wanted %q", pythonPath, lfi.Name(), pythonExeName)
14161426
}
1417-
if m := lfi.Mode(); m&fs.ModeSymlink != 0 {
1418-
t.Errorf("%q should be a file, not a link (mode=0x%x)", pythonPath, uint32(m))
1419-
}
1420-
if m := lfi.Mode(); m&fs.ModeDir != 0 {
1421-
t.Errorf("%q should be a file, not a directory (mode=0x%x)", pythonPath, uint32(m))
1422-
}
1423-
if m := lfi.Mode(); m&fs.ModeIrregular == 0 {
1427+
if tp := lfi.Mode().Type(); tp != fs.ModeIrregular {
14241428
// A reparse point is not a regular file, but we don't have a more appropriate
14251429
// ModeType bit for it, so it should be marked as irregular.
1426-
t.Errorf("%q should not be a regular file (mode=0x%x)", pythonPath, uint32(m))
1430+
t.Errorf("%q should not be a an irregular file (mode=0x%x)", pythonPath, uint32(tp))
14271431
}
14281432

14291433
if sfi.Name() != pythonExeName {

src/os/types_windows.go

+70-32
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package os
66

77
import (
8+
"internal/godebug"
89
"internal/syscall/windows"
910
"sync"
1011
"syscall"
@@ -151,37 +152,86 @@ func newFileStatFromWin32finddata(d *syscall.Win32finddata) *fileStat {
151152
// and https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-point-tags.
152153
func (fs *fileStat) isReparseTagNameSurrogate() bool {
153154
// True for IO_REPARSE_TAG_SYMLINK and IO_REPARSE_TAG_MOUNT_POINT.
154-
return fs.ReparseTag&0x20000000 != 0
155-
}
156-
157-
func (fs *fileStat) isSymlink() bool {
158-
// As of https://go.dev/cl/86556, we treat MOUNT_POINT reparse points as
159-
// symlinks because otherwise certain directory junction tests in the
160-
// path/filepath package would fail.
161-
//
162-
// However,
163-
// https://learn.microsoft.com/en-us/windows/win32/fileio/hard-links-and-junctions
164-
// seems to suggest that directory junctions should be treated like hard
165-
// links, not symlinks.
166-
//
167-
// TODO(bcmills): Get more input from Microsoft on what the behavior ought to
168-
// be for MOUNT_POINT reparse points.
169-
170-
return fs.ReparseTag == syscall.IO_REPARSE_TAG_SYMLINK ||
171-
fs.ReparseTag == windows.IO_REPARSE_TAG_MOUNT_POINT
155+
return fs.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 && fs.ReparseTag&0x20000000 != 0
172156
}
173157

174158
func (fs *fileStat) Size() int64 {
175159
return int64(fs.FileSizeHigh)<<32 + int64(fs.FileSizeLow)
176160
}
177161

162+
var winsymlink = godebug.New("winsymlink")
163+
178164
func (fs *fileStat) Mode() (m FileMode) {
165+
if winsymlink.Value() == "0" {
166+
return fs.modePreGo1_23()
167+
}
168+
if fs.FileAttributes&syscall.FILE_ATTRIBUTE_READONLY != 0 {
169+
m |= 0444
170+
} else {
171+
m |= 0666
172+
}
173+
174+
// Windows reports the FILE_ATTRIBUTE_DIRECTORY bit for reparse points
175+
// that refer to directories, such as symlinks and mount points.
176+
// However, we follow symlink POSIX semantics and do not set the mode bits.
177+
// This allows users to walk directories without following links
178+
// by just calling "fi, err := os.Lstat(name); err == nil && fi.IsDir()".
179+
// Note that POSIX only defines the semantics for symlinks, not for
180+
// mount points or other surrogate reparse points, but we treat them
181+
// the same way for consistency. Also, mount points can contain infinite
182+
// loops, so it is not safe to walk them without special handling.
183+
if !fs.isReparseTagNameSurrogate() {
184+
if fs.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
185+
m |= ModeDir | 0111
186+
}
187+
188+
switch fs.filetype {
189+
case syscall.FILE_TYPE_PIPE:
190+
m |= ModeNamedPipe
191+
case syscall.FILE_TYPE_CHAR:
192+
m |= ModeDevice | ModeCharDevice
193+
}
194+
}
195+
196+
if fs.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
197+
switch fs.ReparseTag {
198+
case syscall.IO_REPARSE_TAG_SYMLINK:
199+
m |= ModeSymlink
200+
case windows.IO_REPARSE_TAG_AF_UNIX:
201+
m |= ModeSocket
202+
case windows.IO_REPARSE_TAG_DEDUP:
203+
// If the Data Deduplication service is enabled on Windows Server, its
204+
// Optimization job may convert regular files to IO_REPARSE_TAG_DEDUP
205+
// whenever that job runs.
206+
//
207+
// However, DEDUP reparse points remain similar in most respects to
208+
// regular files: they continue to support random-access reads and writes
209+
// of persistent data, and they shouldn't add unexpected latency or
210+
// unavailability in the way that a network filesystem might.
211+
//
212+
// Go programs may use ModeIrregular to filter out unusual files (such as
213+
// raw device files on Linux, POSIX FIFO special files, and so on), so
214+
// to avoid files changing unpredictably from regular to irregular we will
215+
// consider DEDUP files to be close enough to regular to treat as such.
216+
default:
217+
m |= ModeIrregular
218+
}
219+
}
220+
return
221+
}
222+
223+
// modePreGo1_23 returns the FileMode for the fileStat, using the pre-Go 1.23
224+
// logic for determining the file mode.
225+
// The logic is subtle and not well-documented, so it is better to keep it
226+
// separate from the new logic.
227+
func (fs *fileStat) modePreGo1_23() (m FileMode) {
179228
if fs.FileAttributes&syscall.FILE_ATTRIBUTE_READONLY != 0 {
180229
m |= 0444
181230
} else {
182231
m |= 0666
183232
}
184-
if fs.isSymlink() {
233+
if fs.ReparseTag == syscall.IO_REPARSE_TAG_SYMLINK ||
234+
fs.ReparseTag == windows.IO_REPARSE_TAG_MOUNT_POINT {
185235
return m | ModeSymlink
186236
}
187237
if fs.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
@@ -199,19 +249,7 @@ func (fs *fileStat) Mode() (m FileMode) {
199249
}
200250
if m&ModeType == 0 {
201251
if fs.ReparseTag == windows.IO_REPARSE_TAG_DEDUP {
202-
// If the Data Deduplication service is enabled on Windows Server, its
203-
// Optimization job may convert regular files to IO_REPARSE_TAG_DEDUP
204-
// whenever that job runs.
205-
//
206-
// However, DEDUP reparse points remain similar in most respects to
207-
// regular files: they continue to support random-access reads and writes
208-
// of persistent data, and they shouldn't add unexpected latency or
209-
// unavailability in the way that a network filesystem might.
210-
//
211-
// Go programs may use ModeIrregular to filter out unusual files (such as
212-
// raw device files on Linux, POSIX FIFO special files, and so on), so
213-
// to avoid files changing unpredictably from regular to irregular we will
214-
// consider DEDUP files to be close enough to regular to treat as such.
252+
// See comment in fs.Mode.
215253
} else {
216254
m |= ModeIrregular
217255
}

src/path/filepath/path_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -1980,3 +1980,16 @@ func TestEscaping(t *testing.T) {
19801980
}
19811981
}
19821982
}
1983+
1984+
func TestEvalSymlinksTooManyLinks(t *testing.T) {
1985+
testenv.MustHaveSymlink(t)
1986+
dir := filepath.Join(t.TempDir(), "dir")
1987+
err := os.Symlink(dir, dir)
1988+
if err != nil {
1989+
t.Fatal(err)
1990+
}
1991+
_, err = filepath.EvalSymlinks(dir)
1992+
if err == nil {
1993+
t.Fatal("expected error, got nil")
1994+
}
1995+
}

0 commit comments

Comments
 (0)