]> Cypherpunks repositories - gostls13.git/commitdiff
os: make Readlink work with symlinks with target like \??\Volume{ABCD}\
authorAlex Brainman <alex.brainman@gmail.com>
Thu, 28 Feb 2019 09:21:32 +0000 (20:21 +1100)
committerAlex Brainman <alex.brainman@gmail.com>
Fri, 1 Mar 2019 07:44:37 +0000 (07:44 +0000)
windows-arm TMP directory live inside such link (see
https://github.com/golang/go/issues/29746#issuecomment-456526811 for
details), so symlinks like that will be common at least on windows-arm.

This CL builds on current syscall.Readlink implementation. Main
difference between the two is how new code handles symlink targets,
like \??\Volume{ABCD}\.

New implementation uses Windows CreateFile API with
FILE_FLAG_OPEN_REPARSE_POINT flag to get \??\Volume{ABCD}\ file handle.
And then it uses Windows GetFinalPathNameByHandle with VOLUME_NAME_DOS
flag to convert that handle into standard Windows path.
FILE_FLAG_OPEN_REPARSE_POINT flag ensures that symlink is not followed
when CreateFile opens the file.

Fixes #30463

Change-Id: I33b18227ce36144caed694169ef2e429fd995fb4
Reviewed-on: https://go-review.googlesource.com/c/164201
Run-TryBot: Alex Brainman <alex.brainman@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
src/internal/syscall/windows/reparse_windows.go
src/os/file_posix.go
src/os/file_unix.go
src/os/file_windows.go
src/os/os_windows_test.go

index 7c6ad8fb7e064b4fcf99fd55fd753bb639251b31..610b733c4a9dc38096d531704b13ec1e9c9bc460 100644 (file)
@@ -4,6 +4,11 @@
 
 package windows
 
+import (
+       "syscall"
+       "unsafe"
+)
+
 const (
        FSCTL_SET_REPARSE_POINT    = 0x000900A4
        IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003
@@ -15,6 +20,13 @@ const (
 // in https://msdn.microsoft.com/en-us/library/cc232007.aspx
 // and https://msdn.microsoft.com/en-us/library/cc232006.aspx.
 
+type REPARSE_DATA_BUFFER struct {
+       ReparseTag        uint32
+       ReparseDataLength uint16
+       Reserved          uint16
+       DUMMYUNIONNAME    byte
+}
+
 // REPARSE_DATA_BUFFER_HEADER is a common part of REPARSE_DATA_BUFFER structure.
 type REPARSE_DATA_BUFFER_HEADER struct {
        ReparseTag uint32
@@ -46,6 +58,12 @@ type SymbolicLinkReparseBuffer struct {
        PathBuffer [1]uint16
 }
 
+// Path returns path stored in rb.
+func (rb *SymbolicLinkReparseBuffer) Path() string {
+       p := (*[0xffff]uint16)(unsafe.Pointer(&rb.PathBuffer[0]))
+       return syscall.UTF16ToString(p[rb.SubstituteNameOffset/2 : (rb.SubstituteNameOffset+rb.SubstituteNameLength)/2])
+}
+
 type MountPointReparseBuffer struct {
        // The integer that contains the offset, in bytes,
        // of the substitute name string in the PathBuffer array,
@@ -62,3 +80,9 @@ type MountPointReparseBuffer struct {
        PrintNameLength uint16
        PathBuffer      [1]uint16
 }
+
+// Path returns path stored in rb.
+func (rb *MountPointReparseBuffer) Path() string {
+       p := (*[0xffff]uint16)(unsafe.Pointer(&rb.PathBuffer[0]))
+       return syscall.UTF16ToString(p[rb.SubstituteNameOffset/2 : (rb.SubstituteNameOffset+rb.SubstituteNameLength)/2])
+}
index 1c0de5c3a163e1cdbabff4ec86debc4635b8a456..23430792191faec1c5aa3755f3aa783c4271666c 100644 (file)
@@ -7,32 +7,12 @@
 package os
 
 import (
-       "runtime"
        "syscall"
        "time"
 )
 
 func sigpipe() // implemented in package runtime
 
-// Readlink returns the destination of the named symbolic link.
-// If there is an error, it will be of type *PathError.
-func Readlink(name string) (string, error) {
-       for len := 128; ; len *= 2 {
-               b := make([]byte, len)
-               n, e := fixCount(syscall.Readlink(fixLongPath(name), b))
-               // buffer too small
-               if runtime.GOOS == "aix" && e == syscall.ERANGE {
-                       continue
-               }
-               if e != nil {
-                       return "", &PathError{"readlink", name, e}
-               }
-               if n < len {
-                       return string(b[0:n]), nil
-               }
-       }
-}
-
 // syscallMode returns the syscall-specific mode bits from Go's portable mode bits.
 func syscallMode(i FileMode) (o uint32) {
        o |= uint32(i.Perm())
index 2615df9d5b7cc36976ae4590863d9ddbcd430964..857cbdb68d87ad8afd918853345c961b48eef6a6 100644 (file)
@@ -399,3 +399,22 @@ func (f *File) readdir(n int) (fi []FileInfo, err error) {
        }
        return fi, err
 }
+
+// Readlink returns the destination of the named symbolic link.
+// If there is an error, it will be of type *PathError.
+func Readlink(name string) (string, error) {
+       for len := 128; ; len *= 2 {
+               b := make([]byte, len)
+               n, e := fixCount(syscall.Readlink(name, b))
+               // buffer too small
+               if runtime.GOOS == "aix" && e == syscall.ERANGE {
+                       continue
+               }
+               if e != nil {
+                       return "", &PathError{"readlink", name, e}
+               }
+               if n < len {
+                       return string(b[0:n]), nil
+               }
+       }
+}
index 85f248774c77e3f210d0c18eddf6089c335fd094..b0206d9200c508a13eba334a93f41e10cb249549 100644 (file)
@@ -5,6 +5,7 @@
 package os
 
 import (
+       "errors"
        "internal/poll"
        "internal/syscall/windows"
        "runtime"
@@ -396,3 +397,122 @@ func Symlink(oldname, newname string) error {
        }
        return nil
 }
+
+// openSymlink calls CreateFile Windows API with FILE_FLAG_OPEN_REPARSE_POINT
+// parameter, so that Windows does not follow symlink, if path is a symlink.
+// openSymlink returns opened file handle.
+func openSymlink(path string) (syscall.Handle, error) {
+       p, err := syscall.UTF16PtrFromString(path)
+       if err != nil {
+               return 0, err
+       }
+       attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS)
+       // Use FILE_FLAG_OPEN_REPARSE_POINT, otherwise CreateFile will follow symlink.
+       // See https://docs.microsoft.com/en-us/windows/desktop/FileIO/symbolic-link-effects-on-file-systems-functions#createfile-and-createfiletransacted
+       attrs |= syscall.FILE_FLAG_OPEN_REPARSE_POINT
+       h, err := syscall.CreateFile(p, 0, 0, nil, syscall.OPEN_EXISTING, attrs, 0)
+       if err != nil {
+               return 0, err
+       }
+       return h, nil
+}
+
+// normaliseLinkPath converts absolute paths returned by
+// DeviceIoControl(h, FSCTL_GET_REPARSE_POINT, ...)
+// into paths acceptable by all Windows APIs.
+// For example, it coverts
+//  \??\C:\foo\bar into C:\foo\bar
+//  \??\UNC\foo\bar into \\foo\bar
+//  \??\Volume{abc}\ into C:\
+func normaliseLinkPath(path string) (string, error) {
+       if len(path) < 4 || path[:4] != `\??\` {
+               // unexpected path, return it as is
+               return path, nil
+       }
+       // we have path that start with \??\
+       s := path[4:]
+       switch {
+       case len(s) >= 2 && s[1] == ':': // \??\C:\foo\bar
+               return s, nil
+       case len(s) >= 4 && s[:4] == `UNC\`: // \??\UNC\foo\bar
+               return `\\` + s[4:], nil
+       }
+
+       // handle paths, like \??\Volume{abc}\...
+
+       err := windows.LoadGetFinalPathNameByHandle()
+       if err != nil {
+               // we must be using old version of Windows
+               return "", err
+       }
+
+       h, err := openSymlink(path)
+       if err != nil {
+               return "", err
+       }
+       defer syscall.CloseHandle(h)
+
+       buf := make([]uint16, 100)
+       for {
+               n, err := windows.GetFinalPathNameByHandle(h, &buf[0], uint32(len(buf)), windows.VOLUME_NAME_DOS)
+               if err != nil {
+                       return "", err
+               }
+               if n < uint32(len(buf)) {
+                       break
+               }
+               buf = make([]uint16, n)
+       }
+       s = syscall.UTF16ToString(buf)
+       if len(s) > 4 && s[:4] == `\\?\` {
+               s = s[4:]
+               if len(s) > 3 && s[:3] == `UNC` {
+                       // return path like \\server\share\...
+                       return `\` + s[3:], nil
+               }
+               return s, nil
+       }
+       return "", errors.New("GetFinalPathNameByHandle returned unexpected path: " + s)
+}
+
+func readlink(path string) (string, error) {
+       h, err := openSymlink(path)
+       if err != nil {
+               return "", 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
+       }
+
+       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))
+               s := rb.Path()
+               if rb.Flags&windows.SYMLINK_FLAG_RELATIVE != 0 {
+                       return s, nil
+               }
+               return normaliseLinkPath(s)
+       case windows.IO_REPARSE_TAG_MOUNT_POINT:
+               return normaliseLinkPath((*windows.MountPointReparseBuffer)(unsafe.Pointer(&rdb.DUMMYUNIONNAME)).Path())
+       default:
+               // the path is not a symlink or junction but another type of reparse
+               // point
+               return "", syscall.ENOENT
+       }
+}
+
+// Readlink returns the destination of the named symbolic link.
+// If there is an error, it will be of type *PathError.
+func Readlink(name string) (string, error) {
+       s, err := readlink(fixLongPath(name))
+       if err != nil {
+               return "", &PathError{"readlink", name, err}
+       }
+       return s, nil
+}
index dc9e629b013d2e23480d4d10b7e652b1cbeb0a50..0b42e089bd10ff76a547579119885ccea1b738ff 100644 (file)
@@ -1050,3 +1050,118 @@ func TestRootDirAsTemp(t *testing.T) {
                t.Fatalf("unexpected child process output %q, want %q", have, want)
        }
 }
+
+func testReadlink(t *testing.T, path, want string) {
+       got, err := os.Readlink(path)
+       if err != nil {
+               t.Error(err)
+               return
+       }
+       if got != want {
+               t.Errorf(`Readlink(%q): got %q, want %q`, path, got, want)
+       }
+}
+
+func mklink(t *testing.T, link, target string) {
+       output, err := osexec.Command("cmd", "/c", "mklink", link, target).CombinedOutput()
+       if err != nil {
+               t.Fatalf("failed to run mklink %v %v: %v %q", link, target, err, output)
+       }
+}
+
+func mklinkj(t *testing.T, link, target string) {
+       output, err := osexec.Command("cmd", "/c", "mklink", "/J", link, target).CombinedOutput()
+       if err != nil {
+               t.Fatalf("failed to run mklink %v %v: %v %q", link, target, err, output)
+       }
+}
+
+func mklinkd(t *testing.T, link, target string) {
+       output, err := osexec.Command("cmd", "/c", "mklink", "/D", link, target).CombinedOutput()
+       if err != nil {
+               t.Fatalf("failed to run mklink %v %v: %v %q", link, target, err, output)
+       }
+}
+
+func TestWindowsReadlink(t *testing.T) {
+       tmpdir, err := ioutil.TempDir("", "TestWindowsReadlink")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(tmpdir)
+
+       // Make sure tmpdir is not a symlink, otherwise tests will fail.
+       tmpdir, err = filepath.EvalSymlinks(tmpdir)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       wd, err := os.Getwd()
+       if err != nil {
+               t.Fatal(err)
+       }
+       err = os.Chdir(tmpdir)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.Chdir(wd)
+
+       vol := filepath.VolumeName(tmpdir)
+       output, err := osexec.Command("cmd", "/c", "mountvol", vol, "/L").CombinedOutput()
+       if err != nil {
+               t.Fatalf("failed to run mountvol %v /L: %v %q", vol, err, output)
+       }
+       ntvol := strings.Trim(string(output), " \n\r")
+
+       dir := filepath.Join(tmpdir, "dir")
+       err = os.MkdirAll(dir, 0777)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       absdirjlink := filepath.Join(tmpdir, "absdirjlink")
+       mklinkj(t, absdirjlink, dir)
+       testReadlink(t, absdirjlink, dir)
+
+       ntdirjlink := filepath.Join(tmpdir, "ntdirjlink")
+       mklinkj(t, ntdirjlink, ntvol+absdirjlink[len(filepath.VolumeName(absdirjlink)):])
+       testReadlink(t, ntdirjlink, absdirjlink)
+
+       ntdirjlinktolink := filepath.Join(tmpdir, "ntdirjlinktolink")
+       mklinkj(t, ntdirjlinktolink, ntvol+absdirjlink[len(filepath.VolumeName(absdirjlink)):])
+       testReadlink(t, ntdirjlinktolink, absdirjlink)
+
+       mklinkj(t, "reldirjlink", "dir")
+       testReadlink(t, "reldirjlink", dir) // relative directory junction resolves to absolute path
+
+       // Make sure we have sufficient privilege to run mklink command.
+       testenv.MustHaveSymlink(t)
+
+       absdirlink := filepath.Join(tmpdir, "absdirlink")
+       mklinkd(t, absdirlink, dir)
+       testReadlink(t, absdirlink, dir)
+
+       ntdirlink := filepath.Join(tmpdir, "ntdirlink")
+       mklinkd(t, ntdirlink, ntvol+absdirlink[len(filepath.VolumeName(absdirlink)):])
+       testReadlink(t, ntdirlink, absdirlink)
+
+       mklinkd(t, "reldirlink", "dir")
+       testReadlink(t, "reldirlink", "dir")
+
+       file := filepath.Join(tmpdir, "file")
+       err = ioutil.WriteFile(file, []byte(""), 0666)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       filelink := filepath.Join(tmpdir, "filelink")
+       mklink(t, filelink, file)
+       testReadlink(t, filelink, file)
+
+       linktofilelink := filepath.Join(tmpdir, "linktofilelink")
+       mklink(t, linktofilelink, ntvol+filelink[len(filepath.VolumeName(filelink)):])
+       testReadlink(t, linktofilelink, filelink)
+
+       mklink(t, "relfilelink", "file")
+       testReadlink(t, "relfilelink", "file")
+}