From 2a5bbf170d66556006f3beddd013f56fa564cb3c Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Fri, 1 Nov 2024 23:19:17 +0100 Subject: [PATCH] fs: implement and use filehandle based readlink The implementations are 90% copy&paste from the go standard library as the existing code does not offer any way to read the symlink target based on a filehandle. Fall back to a standard readlink on platforms other than Linux and Windows as those either don't even provide the necessary syscall or in case of macOS are not yet available in Go. --- internal/fs/freadlink_darwin.go | 9 ++ internal/fs/freadlink_linux.go | 47 +++++++++++ internal/fs/freadlink_test.go | 24 ++++++ internal/fs/freadlink_windows.go | 138 +++++++++++++++++++++++++++++++ internal/fs/meta_fd.go | 3 +- 5 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 internal/fs/freadlink_darwin.go create mode 100644 internal/fs/freadlink_linux.go create mode 100644 internal/fs/freadlink_test.go create mode 100644 internal/fs/freadlink_windows.go diff --git a/internal/fs/freadlink_darwin.go b/internal/fs/freadlink_darwin.go new file mode 100644 index 000000000..47a6cf67e --- /dev/null +++ b/internal/fs/freadlink_darwin.go @@ -0,0 +1,9 @@ +package fs + +import "os" + +// TODO macOS versions >= 13 support freadlink. Use that instead of the fallback codepath + +func Freadlink(fd uintptr, name string) (string, error) { + return os.Readlink(name) +} diff --git a/internal/fs/freadlink_linux.go b/internal/fs/freadlink_linux.go new file mode 100644 index 000000000..a8a89179a --- /dev/null +++ b/internal/fs/freadlink_linux.go @@ -0,0 +1,47 @@ +package fs + +import ( + "os" + "syscall" + + "golang.org/x/sys/unix" +) + +// based on readlink from go/src/os/file_unix.go Go 1.23.2 +// modified to use Readlinkat syscall instead of readlink + +// Many functions in package syscall return a count of -1 instead of 0. +// Using fixCount(call()) instead of call() corrects the count. +func fixCount(n int, err error) (int, error) { + if n < 0 { + n = 0 + } + return n, err +} + +func Freadlink(fd uintptr, name string) (string, error) { + for namelen := 128; ; namelen *= 2 { + b := make([]byte, namelen) + var ( + n int + e error + ) + for { + n, e = fixCount(freadlink(int(fd), b)) + if e != syscall.EINTR { + break + } + } + if e != nil { + return "", &os.PathError{Op: "readlink", Path: name, Err: e} + } + if n < namelen { + return string(b[0:n]), nil + } + } +} + +func freadlink(fd int, buf []byte) (n int, err error) { + // pass empty path to process the link itself + return unix.Readlinkat(fd, "", buf) +} diff --git a/internal/fs/freadlink_test.go b/internal/fs/freadlink_test.go new file mode 100644 index 000000000..b472dba84 --- /dev/null +++ b/internal/fs/freadlink_test.go @@ -0,0 +1,24 @@ +//go:build linux || windows || darwin +// +build linux windows darwin + +package fs + +import ( + "os" + "path/filepath" + "testing" + + rtest "github.com/restic/restic/internal/test" +) + +func TestFreadlink(t *testing.T) { + tmpdir := t.TempDir() + link := filepath.Join(tmpdir, "link") + rtest.OK(t, os.Symlink("other", link)) + + f, err := openMetadataHandle(link, O_NOFOLLOW) + rtest.OK(t, err) + target, err := Freadlink(f.Fd(), link) + rtest.OK(t, err) + rtest.Equals(t, "other", target) +} diff --git a/internal/fs/freadlink_windows.go b/internal/fs/freadlink_windows.go new file mode 100644 index 000000000..e00ef69ff --- /dev/null +++ b/internal/fs/freadlink_windows.go @@ -0,0 +1,138 @@ +package fs + +import ( + "os" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +func Freadlink(fd uintptr, name string) (string, error) { + link, err := readReparseLink(windows.Handle(fd)) + if err != nil { + return "", &os.PathError{Op: "readlink", Path: name, Err: err} + } + return link, nil +} + +// based on src/os/file_windows.go from Go 1.23.2 +// internally readReparseLink from the std library uses a filehandle, however, +// the external interface is based on a path. Thus, copy everything and minimally +// tweak it to allow passing in a file handle. + +// normaliseLinkPath converts absolute paths returned by +// DeviceIoControl(h, FSCTL_GET_REPARSE_POINT, ...) +// into paths acceptable by all Windows APIs. +// For example, it converts +// +// \??\C:\foo\bar into C:\foo\bar +// \??\UNC\foo\bar into \\foo\bar +// \??\Volume{abc}\ into \\?\Volume{abc}\ +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 + } + + // \??\Volume{abc}\ + return `\\?\` + path[4:], nil + // modified to remove the legacy codepath for winreadlinkvolume == 0 +} + +func readReparseLink(h windows.Handle) (string, error) { + rdbbuf := make([]byte, windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE) + var bytesReturned uint32 + err := windows.DeviceIoControl(h, windows.FSCTL_GET_REPARSE_POINT, nil, 0, &rdbbuf[0], uint32(len(rdbbuf)), &bytesReturned, nil) + if err != nil { + return "", err + } + + rdb := (*reparseDataBuffer)(unsafe.Pointer(&rdbbuf[0])) + switch rdb.ReparseTag { + case syscall.IO_REPARSE_TAG_SYMLINK: + rb := (*symbolicLinkReparseBuffer)(unsafe.Pointer(&rdb.DUMMYUNIONNAME)) + s := rb.Path() + if rb.Flags&symlinkFlagRelative != 0 { + return s, nil + } + return normaliseLinkPath(s) + case windows.IO_REPARSE_TAG_MOUNT_POINT: + return normaliseLinkPath((*mountPointReparseBuffer)(unsafe.Pointer(&rdb.DUMMYUNIONNAME)).Path()) + default: + // the path is not a symlink or junction but another type of reparse + // point + return "", syscall.ENOENT + } +} + +// copied from src/internal/syscall/windows/reparse_windows.go from Go 1.23.0 +// renamed to not export symbols + +const symlinkFlagRelative = 1 + +type reparseDataBuffer struct { + ReparseTag uint32 + ReparseDataLength uint16 + Reserved uint16 + DUMMYUNIONNAME byte +} + +type symbolicLinkReparseBuffer struct { + // The integer that contains the offset, in bytes, + // of the substitute name string in the PathBuffer array, + // computed as an offset from byte 0 of PathBuffer. Note that + // this offset must be divided by 2 to get the array index. + SubstituteNameOffset uint16 + // The integer that contains the length, in bytes, of the + // substitute name string. If this string is null-terminated, + // SubstituteNameLength does not include the Unicode null character. + SubstituteNameLength uint16 + // PrintNameOffset is similar to SubstituteNameOffset. + PrintNameOffset uint16 + // PrintNameLength is similar to SubstituteNameLength. + PrintNameLength uint16 + // Flags specifies whether the substitute name is a full path name or + // a path name relative to the directory containing the symbolic link. + Flags uint32 + PathBuffer [1]uint16 +} + +// Path returns path stored in rb. +func (rb *symbolicLinkReparseBuffer) Path() string { + n1 := rb.SubstituteNameOffset / 2 + n2 := (rb.SubstituteNameOffset + rb.SubstituteNameLength) / 2 + return syscall.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(&rb.PathBuffer[0]))[n1:n2:n2]) +} + +type mountPointReparseBuffer struct { + // The integer that contains the offset, in bytes, + // of the substitute name string in the PathBuffer array, + // computed as an offset from byte 0 of PathBuffer. Note that + // this offset must be divided by 2 to get the array index. + SubstituteNameOffset uint16 + // The integer that contains the length, in bytes, of the + // substitute name string. If this string is null-terminated, + // SubstituteNameLength does not include the Unicode null character. + SubstituteNameLength uint16 + // PrintNameOffset is similar to SubstituteNameOffset. + PrintNameOffset uint16 + // PrintNameLength is similar to SubstituteNameLength. + PrintNameLength uint16 + PathBuffer [1]uint16 +} + +// Path returns path stored in rb. +func (rb *mountPointReparseBuffer) Path() string { + n1 := rb.SubstituteNameOffset / 2 + n2 := (rb.SubstituteNameOffset + rb.SubstituteNameLength) / 2 + return syscall.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(&rb.PathBuffer[0]))[n1:n2:n2]) +} diff --git a/internal/fs/meta_fd.go b/internal/fs/meta_fd.go index ee67e6e43..e6bbd6c58 100644 --- a/internal/fs/meta_fd.go +++ b/internal/fs/meta_fd.go @@ -31,6 +31,5 @@ func (p *fdMetadataHandle) Stat() (*ExtendedFileInfo, error) { } func (p *fdMetadataHandle) Readlink() (string, error) { - // FIXME - return os.Readlink(fixpath(p.name)) + return Freadlink(p.f.Fd(), fixpath(p.name)) }