-
Notifications
You must be signed in to change notification settings - Fork 18.4k
Description
Background
Today (as of Go 1.21.3), the documentation for path/filepath.EvalSymlinks (including on Windows!) states:
EvalSymlinksreturns the path name after the evaluation of any symbolic links. If path is relative the result will be relative to the current directory, unless one of the components is an absolute symbolic link.EvalSymlinkscallsCleanon the result.
Notably, EvalSymlinks is not documented to evaluate non-symlinks, just as one would not label a jar as “cinnamon” if it actually contains a mix of many ingredients.
Today, the documentation for os.Readlink states:
“
Readlinkreturns the destination of the named symbolic link.”
#57766 notwithstanding, that documentation seems unambiguously intended to match the behavior of the POSIX readlink function.
Unfortunately, today the implementation of os.Readlink on Windows tries to do much more than what it says on the tin. Specifically, it tries to evaluate the named path to a “canonical” path using GetFinalPathNameByHandle, not just evaluate its symlink components to their targets.
Because it tries to do so much more, its implementation has some fairly serious bugs — and those bugs also come through in filepath.EvalSymlinks:
- path/filepath: EvalSymlinks of a directory which is mapped from volumeID (instead of drive letter) does not work #39786
- path/filepath: endless loop in EvalSymlinks because it resolves a link to the same path it asked for #40176
In addition, filepath.EvalSymlinks on Windows currently canonicalizes the path elements in case-insensitive paths to use the casing found in the filesystem. I believe that most existing callers of filepath.EvalSymlinks on Windows care primarily about evaluating symlinks and getting canonical casing for the path elements, and not at all about other kinds of canonicalization (because those other kinds are where most of the bugs are).
In the past, we have proposed to do ...something(?) (I'm not clear exactly what) with filepath.EvalSymlinks, and to add some new function — perhaps called filepath.Resolve — to evaluate “canonical” paths, providing the existing (aggressive, Windows-specific) behavior of the filepath.EvalSymlinks function as well as the Unix behavior of filepath.EvalSymlinks (which, to reiterate, only evaluates symbolic links).
#42201 was rejected due to a lack of consensus. The sticking point seems to have been the definition of “canonical”, which I agree is not well-defined — and I argue is not well-defined on Unix either, where we could imagine such things as platform-specific paths for mounted GUID volumes, hard-link targets, open files in /proc filesystems, and so on.
This proposal is more limited in scope, aided by the deeper GODEBUG support added for #56986.
Proposal
I propose that we add a GODEBUG setting (perhaps winsymlink?) that changes the behavior of os.Readlink and os.Lstat, with a default that varies with the Go version.
At old Go releases (winsymlink=0):
os.Lstatwould continue to consider both symlinks and mount points to beModeSymlink.os.Readlinkwould continue to try to resolve both symlinks and mount points by callingwindows.GetFinalPathNameByHandleand interpreting the result.- Since the existing behavior of
os.Readlinkhas little to do with the function's documented behavior, I don't believe it is meaningful to even try to define what constitutes a “bug” in that implementation, let alone try to fix it. I propose that we mostly leave it as-is, bugs and all.
At new Go releases (winsymlink=1):
os.Lstatwould only reportModeSymlinkforIO_REPARSE_TAG_SYMLINK. All other reparse tags — including mount points — would be reported as eitherModeIrregularor regular files (forDEDUPreparse points in particular) per os: treat all non-symlink reparse points as ModeIrregular on Windows #61893 and os: NTFS deduped file changed from regular to irregular #63429.os.Readlinkwould only evaluate the target of a symbolic link (that is, a reparse point with tagIO_REPARSE_TAG_SYMLINK), and return a value that is analogous to the value returned by POSIXreadlink(asos.Readlinkalready does on POSIX platforms).- If the symlink refers to an absolute path, the absolute path should be returned as an Win32 path (not an NT path) when that is feasible (see os,path/filepath: make os.Readlink and filepath.EvalSymlinks on Windows (mostly) only evaluate symlinks #63703 (comment)).
- If the symlink refers to a relative path, the returned path fragment should be relative to the directory containing the symlink.
- As a result,
filepath.EvalSymlinkswould only do two transformations:- Evaluate symlinks (— “what it says on the tin”).
- Canonicalize the casing of path elements (to maintain compatibility where it is likely to matter).
- In addition, this behavior should be added to the doc comment for
EvalSymlinks.
- In addition, this behavior should be added to the doc comment for
Since filepath.walkSymlinks is written in a platform-independent way, I believe that fixing os.Readlink may suffice to fix filepath.EvalSymlinks to the above specification. However, if we discover any other bugs in filepath.EvalSymlinks, we should fix them to be in line with that.
In addition, syscall.Readlink should be deprecated on Windows, and golang.org/x/sys/windows.Readlink should be deprecated: the Win32 API itself does not define a readlink system call, and platform-agnostic abstractions belong in os, not syscall.
- The behavior of these
Readlinkfunctions may be left unchanged, or may be changed to also follow thewinsymlinkGODEBUGsetting.
Compatibility
This proposal would provide backward-compatibility using a GODEBUG setting whose default varies with the Go version in use.
Where it changes the behavior of functions, it would change them to better align with the existing documentation for those functions — a kind of change explicitly allowed by the Go 1 compatibility guidelines.
This proposal does not attempt to define a notion of “canonical path” on Windows. Programs would remain free to define their own notion of “canonical” using lower-level syscalls like GetFinalPathNameByHandle if desired.
(attn @golang/windows; CC @networkimprov)