Skip to content

Commit

Permalink
os: add support for AppExecLinks
Browse files Browse the repository at this point in the history
This change adds support for AppExecLinks files, like

C:\Users\user\AppData\Local\Microsoft\WindowsApps\python3.exe

The python3.exe can be installed by following these

https://www.microsoft.com/store/productId/9PJPW5LDXLZ5

instructions. The executable is added to your PATH and can be called
from command line, like `python3 --version`.

Calling GetFileAttributesEx on python3.exe returns FileAttributes with
FILE_ATTRIBUTE_REPARSE_POINT set. And os.Stat attempts to follow the
link. But Microsoft does not provide any link target for AppExecLinks
files (see
dotnet/runtime#58233 (comment) ),
and Go should treat AppExecLinks as normal files and not symlinks.

This CL adjusts os.Stat implementation to return normal file
os.FileInfo
for AppExecLinks files instead of symlinks. The AppExecLinks files are
recognised as they return ERROR_CANT_ACCESS_FILE from CreateFile call.
The trick is not documented anywhere. Jan De Dobbeleer discovered the
trick. Also dotnet/runtime#58233 appears to
also
use ERROR_CANT_ACCESS_FILE to distinguish AppExecLinks files.

The CL also adds new tests.

The CL is an extended copy of the Jan De Dobbeleer
https://go-review.googlesource.com/c/go/+/384160 CL.

Fixes golang#42919

Change-Id: I8b5a26d0cac7882d3445393d26b182ad31cd753b
  • Loading branch information
alexbrainman authored and JanDeDobbeleer committed Dec 16, 2022
1 parent dc04f3b commit 785b5d1
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/internal/syscall/windows/reparse_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
const (
FSCTL_SET_REPARSE_POINT = 0x000900A4
IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003
IO_REPARSE_TAG_APPEXECLINK = 0x8000001B

SYMLINK_FLAG_RELATIVE = 1
)
Expand Down
1 change: 1 addition & 0 deletions src/internal/syscall/windows/syscall_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const (
ERROR_INVALID_NAME syscall.Errno = 123
ERROR_LOCK_FAILED syscall.Errno = 167
ERROR_NO_UNICODE_TRANSLATION syscall.Errno = 1113
ERROR_CANT_ACCESS_FILE syscall.Errno = 1920
)

const GAA_FLAG_INCLUDE_PREFIX = 0x00000010
Expand Down
30 changes: 26 additions & 4 deletions src/os/file_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,21 +457,36 @@ func normaliseLinkPath(path string) (string, error) {
return "", errors.New("GetFinalPathNameByHandle returned unexpected path: " + s)
}

func readlink(path string) (string, error) {
func isAppExecLink(path string) bool {
// We can be dealing with APPEXECLINK, like
// C:\Users\user\AppData\Local\Microsoft\WindowsApps\python3.exe
rdb, err := getReparseDataBuffer(path)
return err == nil && rdb.ReparseTag == windows.IO_REPARSE_TAG_APPEXECLINK
}

func getReparseDataBuffer(path string) (*windows.REPARSE_DATA_BUFFER, error) {
h, err := openSymlink(path)
if err != nil {
return "", err
return nil, err
}
defer syscall.CloseHandle(h)

rdbbuf := make([]byte, syscall.MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
var bytesReturned uint32
err = syscall.DeviceIoControl(h, syscall.FSCTL_GET_REPARSE_POINT, nil, 0, &rdbbuf[0], uint32(len(rdbbuf)), &bytesReturned, nil)
if err != nil {
return "", err
return nil, err
}

return (*windows.REPARSE_DATA_BUFFER)(unsafe.Pointer(&rdbbuf[0])), nil
}

func readlink(path string) (string, error) {
rdb, err := getReparseDataBuffer(path)
if err != nil {
return "", nil
}

rdb := (*windows.REPARSE_DATA_BUFFER)(unsafe.Pointer(&rdbbuf[0]))
switch rdb.ReparseTag {
case syscall.IO_REPARSE_TAG_SYMLINK:
rb := (*windows.SymbolicLinkReparseBuffer)(unsafe.Pointer(&rdb.DUMMYUNIONNAME))
Expand All @@ -482,6 +497,13 @@ func readlink(path string) (string, error) {
return normaliseLinkPath(s)
case windows.IO_REPARSE_TAG_MOUNT_POINT:
return normaliseLinkPath((*windows.MountPointReparseBuffer)(unsafe.Pointer(&rdb.DUMMYUNIONNAME)).Path())
case windows.IO_REPARSE_TAG_APPEXECLINK:
// The PowerShell team had a conversation with the Windows team that owns AppExecLinks.
// They explained that you are not supposed to care about the target path.
// AppExecLinks are meant to work seamlessly like any other file/executable.
// This is why they never exposed the APIs like for other symlinks.
// Source: https://github.com/dotnet/runtime/pull/58233#issuecomment-911971904
return path, nil
default:
// the path is not a symlink or junction but another type of reparse
// point
Expand Down
75 changes: 75 additions & 0 deletions src/os/os_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1248,3 +1248,78 @@ func TestMkdirDevNull(t *testing.T) {
t.Fatalf("error %d is not syscall.ENOTDIR", errno)
}
}

func TestAppExecLinkStat(t *testing.T) {
pythonExeName := "python3.exe"
pythonPath := filepath.Join(os.Getenv("LOCALAPPDATA"), `Microsoft\WindowsApps`, pythonExeName)

lfi, err := os.Lstat(pythonPath)
if err != nil {
t.Skip("skipping test, because Python 3 is not installed via the Windows App Store on this system; see https://golang.org/issue/42919")
}

sfi, err := os.Stat(pythonPath)
if err != nil {
t.Fatalf("Stat %s: %v", pythonPath, err)
}

// We open AppExecLink files as regular files and do not follow symlinks.
// In case this validation fails, the behaviour of AppExecLink or app installed
// executables changed, and so should our implementation.
if !os.SameFile(lfi, sfi) {
t.Logf("os.Lstat(%q) = %+v", pythonPath, lfi)
t.Logf("os.Stat(%q) = %+v", pythonPath, sfi)
t.Errorf("files should be same")
}

if lfi.Name() != pythonExeName {
t.Errorf("Stat %s: got %q, but wanted %q", pythonPath, lfi.Name(), pythonExeName)
}
// We do not open AppExecLink files as symlinks, so it should not identify as such.
// If it does, we should update our implementation.
if m := lfi.Mode(); m&fs.ModeSymlink != 0 {
t.Errorf("%q should be a file, not a link (mode=0x%x)", pythonPath, uint32(m))
}
if m := lfi.Mode(); m&fs.ModeDir != 0 {
t.Errorf("%q should be a file, not a directory (mode=0x%x)", pythonPath, uint32(m))
}

if sfi.Name() != pythonExeName {
t.Errorf("Stat %s: got %q, but wanted %q", pythonPath, sfi.Name(), pythonExeName)
}
if m := sfi.Mode(); m&fs.ModeSymlink != 0 {
t.Errorf("%q should be a file, not a link (mode=0x%x)", pythonPath, uint32(m))
}
if m := sfi.Mode(); m&fs.ModeDir != 0 {
t.Errorf("%q should be a file, not a directory (mode=0x%x)", pythonPath, uint32(m))
}

cmd := osexec.Command(pythonPath, "-c", "print('hello')")
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("failed to run python: %v %v", err, string(output))
}
if got, want := string(output), "hello\r\n"; got != want {
t.Errorf(`unexpected python program output: got %q, want %q`, got, want)
}
}

func TestAppExecLinkReadlink(t *testing.T) {
pythonExeName := "python3.exe"
pythonPath := filepath.Join(os.Getenv("LOCALAPPDATA"), `Microsoft\WindowsApps`, pythonExeName)

_, err := os.Lstat(pythonPath)
if err != nil {
t.Skip("skipping test, because Python 3 is not installed via the Windows App Store on this system; see https://golang.org/issue/42919")
}

// We do not open AppExecLink files as symlinks, so it should not identify as such and return the exact same file path.
// If it does, we should update our implementation.
linkName, err := os.Readlink(pythonPath)
if err != nil {
t.Fatalf("Readlink %s: %v", pythonPath, err)
}
if linkName != pythonPath {
t.Errorf("Readlink %s: got %q, but wanted %q", pythonExeName, linkName, pythonPath)
}
}
21 changes: 19 additions & 2 deletions src/os/stat_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ func stat(funcname, name string, createFileAttrs uint32) (FileInfo, error) {
// Try GetFileAttributesEx first, because it is faster than CreateFile.
// See https://golang.org/issues/19922#issuecomment-300031421 for details.
var fa syscall.Win32FileAttributeData
err = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
if err == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 {
faErr := syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
if faErr == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 {
// Not a symlink.
fs := &fileStat{
FileAttributes: fa.FileAttributes,
Expand All @@ -74,6 +74,7 @@ func stat(funcname, name string, createFileAttrs uint32) (FileInfo, error) {
}
return fs, nil
}

// GetFileAttributesEx fails with ERROR_SHARING_VIOLATION error for
// files, like c:\pagefile.sys. Use FindFirstFile for such files.
if err == windows.ERROR_SHARING_VIOLATION {
Expand All @@ -93,6 +94,22 @@ func stat(funcname, name string, createFileAttrs uint32) (FileInfo, error) {
// Finally use CreateFile.
h, err := syscall.CreateFile(namep, 0, 0, nil,
syscall.OPEN_EXISTING, createFileAttrs, 0)
if faErr == nil && err == windows.ERROR_CANT_ACCESS_FILE {
if isAppExecLink(name) {
fs := &fileStat{
FileAttributes: fa.FileAttributes,
CreationTime: fa.CreationTime,
LastAccessTime: fa.LastAccessTime,
LastWriteTime: fa.LastWriteTime,
FileSizeHigh: fa.FileSizeHigh,
FileSizeLow: fa.FileSizeLow,
}
if err := fs.saveInfoFromPath(name); err != nil {
return nil, err
}
return fs, nil
}
}
if err != nil {
return nil, &PathError{Op: "CreateFile", Path: name, Err: err}
}
Expand Down
7 changes: 7 additions & 0 deletions src/os/types_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,13 @@ func (fs *fileStat) loadFileId() error {
attrs |= syscall.FILE_FLAG_OPEN_REPARSE_POINT
}
h, err := syscall.CreateFile(pathp, 0, 0, nil, syscall.OPEN_EXISTING, attrs, 0)
if err == windows.ERROR_CANT_ACCESS_FILE {
// Use FILE_FLAG_OPEN_REPARSE_POINT, like for symlinks, and try to call CreateFile again.
if isAppExecLink(fs.name) {
attrs |= syscall.FILE_FLAG_OPEN_REPARSE_POINT
h, err = syscall.CreateFile(pathp, 0, 0, nil, syscall.OPEN_EXISTING, attrs, 0)
}
}
if err != nil {
return err
}
Expand Down

0 comments on commit 785b5d1

Please sign in to comment.