1
0
mirror of https://github.com/golang/go synced 2024-09-29 07:24:32 -06:00

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 <alex.brainman@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
This commit is contained in:
Alex Brainman 2019-02-28 20:21:32 +11:00
parent e27402aee0
commit 2edd559223
5 changed files with 278 additions and 20 deletions

View 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])
}

View 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())

View 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
}
}
}

View 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
}

View 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")
}