From 3e44b7d07a7b3c6233eb1bf4cf3cb00a0b85adec Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Wed, 4 Jan 2023 17:23:46 -0500 Subject: os: treat non-symlink reparse points as irregular files Prior to this change (as of CL 143578), our stat function attempted to resolve all reparse points as if they were symlinks. This results in an additional call to CreateFile when statting a symlink file: we use CreateFile once to obtain the reparse tag and check whether the file is actually a symlink, and if it is we call CreateFile again without FILE_FLAG_OPEN_REPARSE_POINT to stat the link target. Fortunately, since symlinks are rare on Windows that overhead shouldn't be a big deal in practice. Fixes #42919. Change-Id: If453930c6e98040cd6525ac4aea60a84498c9579 Reviewed-on: https://go-review.googlesource.com/c/go/+/460595 Reviewed-by: Quim Muntal Run-TryBot: Bryan Mills TryBot-Result: Gopher Robot Reviewed-by: Alex Brainman Reviewed-by: Cherry Mui Auto-Submit: Bryan Mills --- src/os/os_windows_test.go | 82 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 2 deletions(-) (limited to 'src/os/os_windows_test.go') diff --git a/src/os/os_windows_test.go b/src/os/os_windows_test.go index 17b03e9508..1133639105 100644 --- a/src/os/os_windows_test.go +++ b/src/os/os_windows_test.go @@ -14,6 +14,7 @@ import ( "io" "io/fs" "os" + "os/exec" "path/filepath" "reflect" "runtime" @@ -713,11 +714,13 @@ func TestReadStdin(t *testing.T) { func TestStatPagefile(t *testing.T) { t.Parallel() - fi, err := os.Stat(`c:\pagefile.sys`) + const path = `c:\pagefile.sys` + fi, err := os.Stat(path) if err == nil { if fi.Name() == "" { - t.Fatal(`FileInfo of c:\pagefile.sys has empty name`) + t.Fatalf("Stat(%q).Name() is empty", path) } + t.Logf("Stat(%q).Size() = %v", path, fi.Size()) return } if os.IsNotExist(err) { @@ -1290,3 +1293,78 @@ func TestOpenDirTOCTOU(t *testing.T) { t.Error(err) } } + +func TestAppExecLinkStat(t *testing.T) { + // We expect executables installed to %LOCALAPPDATA%\Microsoft\WindowsApps to + // be reparse points with tag IO_REPARSE_TAG_APPEXECLINK. Here we check that + // such reparse points are treated as irregular (but executable) files, not + // broken symlinks. + appdata := os.Getenv("LOCALAPPDATA") + if appdata == "" { + t.Skipf("skipping: LOCALAPPDATA not set") + } + + pythonExeName := "python3.exe" + pythonPath := filepath.Join(appdata, `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") + } + + // An APPEXECLINK reparse point is not a symlink, so os.Readlink should return + // a non-nil error for it, and Stat should return results identical to Lstat. + linkName, err := os.Readlink(pythonPath) + if err == nil { + t.Errorf("os.Readlink(%q) = %q, but expected an error\n(should be an APPEXECLINK reparse point, not a symlink)", pythonPath, linkName) + } + + sfi, err := os.Stat(pythonPath) + if err != nil { + t.Fatalf("Stat %s: %v", pythonPath, err) + } + + if lfi.Name() != sfi.Name() { + 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) + } + 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 m := lfi.Mode(); m&fs.ModeIrregular == 0 { + // A reparse point is not a regular file, but we don't have a more appropriate + // ModeType bit for it, so it should be marked as irregular. + t.Errorf("%q should not be a regular file (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)) + } + if m := sfi.Mode(); m&fs.ModeIrregular == 0 { + // A reparse point is not a regular file, but we don't have a more appropriate + // ModeType bit for it, so it should be marked as irregular. + t.Errorf("%q should not be a regular file (mode=0x%x)", pythonPath, uint32(m)) + } + + p, err := exec.LookPath(pythonPath) + if err != nil { + t.Errorf("exec.LookPath(%q): %v", pythonPath, err) + } + if p != pythonPath { + t.Errorf("exec.LookPath(%q) = %q; want %q", pythonPath, p, pythonPath) + } +} -- cgit v1.3