Skip to content

os,path/filepath: make os.Readlink and filepath.EvalSymlinks on Windows (mostly) only evaluate symlinks #63703

@bcmills

Description

@bcmills

Background

Today (as of Go 1.21.3), the documentation for path/filepath.EvalSymlinks (including on Windows!) states:

EvalSymlinks returns 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. EvalSymlinks calls Clean on 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:

Readlink returns 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:

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.Lstat would continue to consider both symlinks and mount points to be ModeSymlink.
  • os.Readlink would continue to try to resolve both symlinks and mount points by calling windows.GetFinalPathNameByHandle and interpreting the result.
  • Since the existing behavior of os.Readlink has 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.Lstat would only report ModeSymlink for IO_REPARSE_TAG_SYMLINK. All other reparse tags — including mount points — would be reported as either ModeIrregular or regular files (for DEDUP reparse 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.Readlink would only evaluate the target of a symbolic link (that is, a reparse point with tag IO_REPARSE_TAG_SYMLINK), and return a value that is analogous to the value returned by POSIX readlink (as os.Readlink already does on POSIX platforms).
  • As a result, filepath.EvalSymlinks would only do two transformations:
    1. Evaluate symlinks (— “what it says on the tin”).
    2. 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.

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 Readlink functions may be left unchanged, or may be changed to also follow the winsymlink GODEBUG setting.

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)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions