From: Alex Brainman Date: Thu, 28 Feb 2019 09:21:32 +0000 (+1100) Subject: os: make Readlink work with symlinks with target like \??\Volume{ABCD}\ X-Git-Tag: go1.13beta1~1288 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=2edd559223f3b3fd54e354c9a9703248a935c91a;p=gostls13.git os: make Readlink work with symlinks with target like \??\Volume{ABCD}\ 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 TryBot-Result: Gobot Gobot Reviewed-by: Ian Lance Taylor --- diff --git a/src/internal/syscall/windows/reparse_windows.go b/src/internal/syscall/windows/reparse_windows.go index 7c6ad8fb7e..610b733c4a 100644 --- a/src/internal/syscall/windows/reparse_windows.go +++ b/src/internal/syscall/windows/reparse_windows.go @@ -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]) +} diff --git a/src/os/file_posix.go b/src/os/file_posix.go index 1c0de5c3a1..2343079219 100644 --- a/src/os/file_posix.go +++ b/src/os/file_posix.go @@ -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()) diff --git a/src/os/file_unix.go b/src/os/file_unix.go index 2615df9d5b..857cbdb68d 100644 --- a/src/os/file_unix.go +++ b/src/os/file_unix.go @@ -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 + } + } +} diff --git a/src/os/file_windows.go b/src/os/file_windows.go index 85f248774c..b0206d9200 100644 --- a/src/os/file_windows.go +++ b/src/os/file_windows.go @@ -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 +} diff --git a/src/os/os_windows_test.go b/src/os/os_windows_test.go index dc9e629b01..0b42e089bd 100644 --- a/src/os/os_windows_test.go +++ b/src/os/os_windows_test.go @@ -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") +}