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 11, 2022
1 parent 9b8750f commit 1b731b3
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 0 deletions.
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 @@ -39,6 +39,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
55 changes: 55 additions & 0 deletions src/os/os_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1277,3 +1277,58 @@ func TestOpenDirTOCTOU(t *testing.T) {
t.Error(err)
}
}

func TestAppLinkSymlinkStat(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 AppLinkSymlink files as regular files and do not follow symlinks.
// In case this validation fails, the behaviour of AppLinkSymlink 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 AppLinkSymlink files as symlinks, so it should not indentify 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)
}
}
23 changes: 23 additions & 0 deletions src/os/stat_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,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 @@ -67,6 +68,28 @@ 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 err == windows.ERROR_CANT_ACCESS_FILE {
// We must be dealing with APPEXECLINK, like
// C:\Users\user\AppData\Local\Microsoft\WindowsApps\python3.exe
// Treat it as normal file.
// 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 as any other executable.
// This is why they never exposed the APIs like other symlinks.
// Source: https://github.com/dotnet/runtime/pull/58233#issuecomment-911971904
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 @@ -170,6 +170,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 {
// We must be dealing with APPEXECLINK, like
// C:\Users\user\AppData\Local\Microsoft\WindowsApps\python3.exe
// Use FILE_FLAG_OPEN_REPARSE_POINT, like for symlinks, and try to call CreateFile again.
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 1b731b3

Please sign in to comment.