go: os: Symlink on Windows with a to-be-created directory path silently creates a link of the wrong type

(Discovered via debugging on #38772.)

What version of Go are you using (go version)?

gopher@SERVER-2016-V7- C:\workdir\go\src>..\bin\go version
go version devel gomote.XXXXX windows/amd64

Does this issue reproduce with the latest release?

Yes.

What operating system and processor architecture are you using (go env)?

go env Output
gopher@SERVER-2016-V7- C:\workdir\go\src>..\bin\go version
go version devel gomote.XXXXX windows/amd64

gopher@SERVER-2016-V7- C:\workdir\go\src>…\bin\go env set GO111MODULE= set GOARCH=amd64 set GOBIN= set GOCACHE=C:\Users\gopher\AppData\Local\go-build set GOENV=C:\Users\gopher\AppData\Roaming\go\env set GOEXE=.exe set GOFLAGS= set GOHOSTARCH=amd64 set GOHOSTOS=windows set GOINSECURE= set GOMODCACHE=C:\Users\gopher\go\pkg\mod set GONOPROXY= set GONOSUMDB= set GOOS=windows set GOPATH=C:\Users\gopher\go set GOPRIVATE= set GOPROXY=https://proxy.golang.org,direct set GOROOT=C:\workdir\go set GOSUMDB=sum.golang.org set GOTMPDIR= set GOTOOLDIR=C:\workdir\go\pkg\tool\windows_amd64 set GCCGO=gccgo set GOAMD64=alignedjumps set AR=ar set CC=gcc set CXX=g++ set CGO_ENABLED=0 set GOMOD=C:\workdir\go\src\go.mod set CGO_CFLAGS=-g -O2 set CGO_CPPFLAGS= set CGO_CXXFLAGS=-g -O2 set CGO_FFLAGS=-g -O2 set CGO_LDFLAGS=-g -O2 set PKG_CONFIG=pkg-config set GOGCCFLAGS=-m64 -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map= C:\Users\gopher\AppData\Local\Temp\go-build141789646=/tmp/go-build -gno-record-gcc-switches

What did you do?

	err := os.Symlink(to, from)
	if err != nil {
		t.Fatalf(…)
	}

	err = os.Mkdir(to, 0755)
	if err != nil {
		t.Fatalf(…)
	}

	…

	file, err := os.Open(from)
	if err != nil {
		t.Fatalf(…)
	}
	file.Close()

CL 234737 contains a complete example.

What did you expect to see?

Either a non-nil error from the call to os.Symlink, or a successful call to os.Open.

What did you see instead?

gopher@SERVER-2016-V7- C:\workdir\go\src>..\bin\go test os -run=TestSymlink.*
--- FAIL: TestSymlinkBeforeTargetFileExists (0.00s)
    os_test.go:908: Open("earlysymlinktestfrom") failed: open earlysymlinktestfrom: Access is denied
.
FAIL
FAIL    os      0.052s
FAIL

Diagnosis

The Windows CreateSymbolicLinkA system call requires a flag to indicate whether the destination is a file or a directory. (If SYMBOLIC_LINK_FLAG_DIRECTORY is set, the symlink is a directory symlink; otherwise, it is a file symlink.)

The current implementation of os.Symlink on Windows uses a call to os.Stat to determine which kind of link to create. Unfortunately, if that Stat call fails, Symlink assumes that it is a file rather than reporting the error to the caller: https://github.com/golang/go/blob/567556d78657326c99b8fa84ec2a5ee511a0941b/src/os/file_windows.go#L337-L338

That assumption seems like a mistake. If a Windows user needs to create a symlink to a not-yet-existing file or directory on Windows, they should be made aware that the call is missing essential information (the destination type), and can then make an explicit choice to use golang.org/x/sys/windows.CreateSymbolicLink instead of os.Syscall if they are able to supply the missing information.

I think we should change os.Symlink at the beginning of the Go 1.16 cycle so that it propagates the Stat error instead of implicitly assuming that the destination is a file.

CC @fraenkel @alexbrainman @ianlancetaylor

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 2
  • Comments: 49 (29 by maintainers)

Commits related to this issue

Most upvoted comments

Thanks for writing down all the cases. I agree that we should just document.

For people using Windows who need to create a symlink to a directory that does not yet exist, I think the approach is either 1) create the directory first; 2) use x/sys/windows.

I don’t think we need an AlignSymlink function. It should be reliable to remove the symlink and recreate it. Accessing the symlink won’t work while it is removed, but it won’t work before it is removed wither.

I don’t have any creative solutions for this problem, unfortunately. We run into a similar issue when trying to determine what symlink type to create when creating symbolic links in WSL. If the target doesn’t exist, or if the target type changes after creating the symlink, then we get it wrong. This breaks symlink accesses from Windows (but not from WSL since we interpret the symlinks there, ignoring the type flag).

Ideally we would change Windows to not require this distinction, but so far we don’t think that’s practical.

@bcmills

I don’t see why we should second guess API intentions and fail os.Symlink.

The API of os.Symlink is narrower than CreateSymbolicLink. When the link target does not exist, CreateSymbolicLink receives an extra bit of explicit information that os.Symlink lacks: the intended type of the link target, passed as a flag to CreateSymbolicLink but not available to os.Symlink.

But that is not what symlinks created with CreateSymbolicLink API do. For example, they could point to a file on another volume / drive, and that drive might not be mounted all the time. It is obviously fine for symlinks point to non-existing file / directory.

Did you read

https://docs.microsoft.com/en-us/windows/win32/fileio/hard-links-and-junctions

?

Alex