go: os/exec: calling Command on file with UWP reparse point fails with "file does not exist"

Windows 10 ships with some 0 length files in C:\Users\<User>\AppData\Local\Microsoft\WindowsApps with special reparse points which when run trigger the windows app store app to be launched.

Once installed, the same files result in the installed app to be run going forward. For instance, Windows 10 ships with python3.exe which is in fact a link to python3.8 available on the Windows App Store. C:\Users\<User>\AppData\Local\Microsoft\WindowsApps happens to be on the PATH by default so once installed, the python3 interpreter should be invoked. However, when trying to run the same through Go’s exec.Cmd we get a file does not exist error (see below for simple repro).

The main issue seems to be a call to os.Stat from the exec.Cmd run path. os.Stat on windows doesn’t pass syscall.FILE_FLAG_OPEN_REPARSE_POINT resulting in the error (unlike Lstat).

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

$ go version
1.15.5

Does this issue reproduce with the latest release?

Yes

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

Windows 10

go env Output
$ go env

set GO111MODULE= set GOARCH=amd64 set GOBIN= set GOCACHE=C:\Users\User\AppData\Local\go-build set GOENV=C:\Users\User\AppData\Roaming\go\env set GOEXE=.exe set GOFLAGS= set GOHOSTARCH=amd64 set GOHOSTOS=windows set GOINSECURE= set GOMODCACHE=C:\Users\User\go\pkg\mod set GONOPROXY= set GONOSUMDB= set GOOS=windows set GOPATH=C:\Users\User\go set GOPRIVATE= set GOPROXY=https://proxy.golang.org,direct set GOROOT=c:\go set GOSUMDB=sum.golang.org set GOTMPDIR= set GOTOOLDIR=c:\go\pkg\tool\windows_amd64 set GCCGO=gccgo set AR=ar set CC=gcc set CXX=g++ set CGO_ENABLED=1 set GOMOD= 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 -mthreads -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=C:\Users\User\AppData\Local\Temp\go-build056150306=/tmp/go-build -gno-record-gcc-switches

What did you do?

$ cat main.go
package main

import (
	"fmt"
	"os"
	"os/exec"
)

func do() error {
	res, err := os.Lstat("C:\\Users\\User\\AppData\\Local\\Microsoft\\WindowsApps\\python3.exe")
	if err != nil {
		return err
	}
	fmt.Println("python3 exists!")
	fmt.Printf("%#v\n", res)
	cmd := exec.Command("C:\\Users\\User\\AppData\\Local\\Microsoft\\WindowsApps\\python3.exe", "-c", "print('hello')")
	btes, err := cmd.CombinedOutput()
	if err != nil {
		return err
	}
	fmt.Printf("%v\n", btes)
	return nil
}

func main() {
	err := do()
	if err != nil {
		panic(err)
	}
}

PS C:\Users\User> go run main.go

What did you expect to see?

python3 exists!
&os.fileStat{name:"python3.exe", FileAttributes:0x420, CreationTime:syscall.Filetime{LowDateTime:0x5094a4bc, HighDateTime:0x1d6a0b0}, LastAccessTime:syscall.Filetime{LowDateTime:0xb3aada47, HighDateTime:0x1d6be0b}, LastWriteTime:syscall.Filetime{LowDateTime:0xb3aada47, HighDateTime:0x1d6be0b}, FileSizeHigh:0x0, FileSizeLow:0x0, Reserved0:0x8000001b, filetype:0x0, Mutex:sync.Mutex{state:0, sema:0x0}, path:"", vol:0x16999dcb, idxhi:0x30000, idxlo:0x14bc9, appendNameToPath:false}
hello

What did you see instead?

python3 exists!
&os.fileStat{name:"python3.exe", FileAttributes:0x420, CreationTime:syscall.Filetime{LowDateTime:0x5094a4bc, HighDateTime:0x1d6a0b0}, LastAccessTime:syscall.Filetime{LowDateTime:0xb3aada47, HighDateTime:0x1d6be0b}, LastWriteTime:syscall.Filetime{LowDateTime:0xb3aada47, HighDateTime:0x1d6be0b}, FileSizeHigh:0x0, FileSizeLow:0x0, Reserved0:0x8000001b, filetype:0x0, Mutex:sync.Mutex{state:0, sema:0x0}, path:"", vol:0x16999dcb, idxhi:0x30000, idxlo:0x14bc9, appendNameToPath:false}
panic: exec: "C:\\Users\\User\\AppData\\Local\\Microsoft\\WindowsApps\\python3.exe": file does not exist

goroutine 1 [running]:
main.main()
        C:/Users/User/main.go:28 +0x4b
exit status 2

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 1
  • Comments: 21 (7 by maintainers)

Commits related to this issue

Most upvoted comments

I think the problem here is more fundamental than just APPEXECLINK reparse points — the APPEXECLINK special cases in CL 384160 are treating a symptom but not the root cause.

I have mailed an alternative approach in CL 460595, which attempts to generalize the solution by treating non-symlink reparse points as irregular files (instead of treating all reparse points like symlinks as we currently do).

Sure!

@sachinjoseph yes, stat doesn’t resolve symlinks so I didn’t touch it.

It caused by that findExecutable uses os.Stat instead of os.Lstat. os.Lstat resolve reparse-points.

diff --git a/src/os/exec/lp_windows.go b/src/os/exec/lp_windows.go
index 9ea3d76575..12c7e04e63 100644
--- a/src/os/exec/lp_windows.go
+++ b/src/os/exec/lp_windows.go
@@ -15,7 +15,7 @@ import (
 var ErrNotFound = errors.New("executable file not found in %PATH%")
 
 func chkStat(file string) error {
-	d, err := os.Stat(file)
+	d, err := os.Lstat(file)
 	if err != nil {
 		return err
 	}

This fixes the issue the program can not execute python.exe, but the python.exe exit with 9009 exit code. Probably, it is another issue.