]> Cypherpunks repositories - gostls13.git/commitdiff
os: add new tests for symbolic links and directory junctions
authorAlex Brainman <alex.brainman@gmail.com>
Thu, 28 Jul 2016 00:33:26 +0000 (10:33 +1000)
committerAlex Brainman <alex.brainman@gmail.com>
Wed, 12 Oct 2016 05:59:16 +0000 (05:59 +0000)
Updates #15978
Updates #16145

Change-Id: I161f5bc97d41c08bf5e1405ccafa86232d70886d
Reviewed-on: https://go-review.googlesource.com/25320
Run-TryBot: Alex Brainman <alex.brainman@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Russ Cox <rsc@golang.org>
src/internal/syscall/windows/mksyscall.go
src/internal/syscall/windows/reparse_windows.go [new file with mode: 0644]
src/internal/syscall/windows/security_windows.go [new file with mode: 0644]
src/internal/syscall/windows/syscall_windows.go
src/internal/syscall/windows/zsyscall_windows.go
src/os/os_windows_test.go

index 21a2b4e777328a5b35d3ea4a3693c76ebf253447..0b01938b871c09cb2be65aba4fef119bb8fdf550 100644 (file)
@@ -4,4 +4,4 @@
 
 package windows
 
-//go:generate go run $GOROOT/src/syscall/mksyscall_windows.go -output zsyscall_windows.go syscall_windows.go
+//go:generate go run $GOROOT/src/syscall/mksyscall_windows.go -output zsyscall_windows.go syscall_windows.go security_windows.go
diff --git a/src/internal/syscall/windows/reparse_windows.go b/src/internal/syscall/windows/reparse_windows.go
new file mode 100644 (file)
index 0000000..7c6ad8f
--- /dev/null
@@ -0,0 +1,64 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package windows
+
+const (
+       FSCTL_SET_REPARSE_POINT    = 0x000900A4
+       IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003
+
+       SYMLINK_FLAG_RELATIVE = 1
+)
+
+// These structures are described
+// in https://msdn.microsoft.com/en-us/library/cc232007.aspx
+// and https://msdn.microsoft.com/en-us/library/cc232006.aspx.
+
+// REPARSE_DATA_BUFFER_HEADER is a common part of REPARSE_DATA_BUFFER structure.
+type REPARSE_DATA_BUFFER_HEADER struct {
+       ReparseTag uint32
+       // The size, in bytes, of the reparse data that follows
+       // the common portion of the REPARSE_DATA_BUFFER element.
+       // This value is the length of the data starting at the
+       // SubstituteNameOffset field.
+       ReparseDataLength uint16
+       Reserved          uint16
+}
+
+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
+}
+
+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
+}
diff --git a/src/internal/syscall/windows/security_windows.go b/src/internal/syscall/windows/security_windows.go
new file mode 100644 (file)
index 0000000..2c145e1
--- /dev/null
@@ -0,0 +1,57 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package windows
+
+import (
+       "syscall"
+)
+
+const (
+       SecurityAnonymous      = 0
+       SecurityIdentification = 1
+       SecurityImpersonation  = 2
+       SecurityDelegation     = 3
+)
+
+//sys  ImpersonateSelf(impersonationlevel uint32) (err error) = advapi32.ImpersonateSelf
+//sys  RevertToSelf() (err error) = advapi32.RevertToSelf
+
+const (
+       TOKEN_ADJUST_PRIVILEGES = 0x0020
+       SE_PRIVILEGE_ENABLED    = 0x00000002
+)
+
+type LUID struct {
+       LowPart  uint32
+       HighPart int32
+}
+
+type LUID_AND_ATTRIBUTES struct {
+       Luid       LUID
+       Attributes uint32
+}
+
+type TOKEN_PRIVILEGES struct {
+       PrivilegeCount uint32
+       Privileges     [1]LUID_AND_ATTRIBUTES
+}
+
+//sys  OpenThreadToken(h syscall.Handle, access uint32, openasself bool, token *syscall.Token) (err error) = advapi32.OpenThreadToken
+//sys  LookupPrivilegeValue(systemname *uint16, name *uint16, luid *LUID) (err error) = advapi32.LookupPrivilegeValueW
+//sys  adjustTokenPrivileges(token syscall.Token, disableAllPrivileges bool, newstate *TOKEN_PRIVILEGES, buflen uint32, prevstate *TOKEN_PRIVILEGES, returnlen *uint32) (ret uint32, err error) [true] = advapi32.AdjustTokenPrivileges
+
+func AdjustTokenPrivileges(token syscall.Token, disableAllPrivileges bool, newstate *TOKEN_PRIVILEGES, buflen uint32, prevstate *TOKEN_PRIVILEGES, returnlen *uint32) error {
+       ret, err := adjustTokenPrivileges(token, disableAllPrivileges, newstate, buflen, prevstate, returnlen)
+       if ret == 0 {
+               // AdjustTokenPrivileges call failed
+               return err
+       }
+       // AdjustTokenPrivileges call succeeded
+       if err == syscall.EINVAL {
+               // GetLastError returned ERROR_SUCCESS
+               return nil
+       }
+       return err
+}
index 015862d71336f480c8dd3e9e9ee202dc1765e08e..77d6033a353d62ca97cc00cab115389484de8e5a 100644 (file)
@@ -140,3 +140,4 @@ func Rename(oldpath, newpath string) error {
 //sys  GetACP() (acp uint32) = kernel32.GetACP
 //sys  GetConsoleCP() (ccp uint32) = kernel32.GetConsoleCP
 //sys  MultiByteToWideChar(codePage uint32, dwFlags uint32, str *byte, nstr int32, wchar *uint16, nwchar int32) (nwrite int32, err error) = kernel32.MultiByteToWideChar
+//sys  GetCurrentThread() (pseudoHandle syscall.Handle, err error) = kernel32.GetCurrentThread
index 0b814e9b4a460986be41cd1887defef1fb44be6a..f6a89540725a2625f1fad103d1eff8b6f514613b 100644 (file)
@@ -38,13 +38,20 @@ func errnoErr(e syscall.Errno) error {
 var (
        modiphlpapi = syscall.NewLazyDLL(sysdll.Add("iphlpapi.dll"))
        modkernel32 = syscall.NewLazyDLL(sysdll.Add("kernel32.dll"))
+       modadvapi32 = syscall.NewLazyDLL(sysdll.Add("advapi32.dll"))
 
-       procGetAdaptersAddresses = modiphlpapi.NewProc("GetAdaptersAddresses")
-       procGetComputerNameExW   = modkernel32.NewProc("GetComputerNameExW")
-       procMoveFileExW          = modkernel32.NewProc("MoveFileExW")
-       procGetACP               = modkernel32.NewProc("GetACP")
-       procGetConsoleCP         = modkernel32.NewProc("GetConsoleCP")
-       procMultiByteToWideChar  = modkernel32.NewProc("MultiByteToWideChar")
+       procGetAdaptersAddresses  = modiphlpapi.NewProc("GetAdaptersAddresses")
+       procGetComputerNameExW    = modkernel32.NewProc("GetComputerNameExW")
+       procMoveFileExW           = modkernel32.NewProc("MoveFileExW")
+       procGetACP                = modkernel32.NewProc("GetACP")
+       procGetConsoleCP          = modkernel32.NewProc("GetConsoleCP")
+       procMultiByteToWideChar   = modkernel32.NewProc("MultiByteToWideChar")
+       procGetCurrentThread      = modkernel32.NewProc("GetCurrentThread")
+       procImpersonateSelf       = modadvapi32.NewProc("ImpersonateSelf")
+       procRevertToSelf          = modadvapi32.NewProc("RevertToSelf")
+       procOpenThreadToken       = modadvapi32.NewProc("OpenThreadToken")
+       procLookupPrivilegeValueW = modadvapi32.NewProc("LookupPrivilegeValueW")
+       procAdjustTokenPrivileges = modadvapi32.NewProc("AdjustTokenPrivileges")
 )
 
 func GetAdaptersAddresses(family uint32, flags uint32, reserved uintptr, adapterAddresses *IpAdapterAddresses, sizePointer *uint32) (errcode error) {
@@ -103,3 +110,89 @@ func MultiByteToWideChar(codePage uint32, dwFlags uint32, str *byte, nstr int32,
        }
        return
 }
+
+func GetCurrentThread() (pseudoHandle syscall.Handle, err error) {
+       r0, _, e1 := syscall.Syscall(procGetCurrentThread.Addr(), 0, 0, 0, 0)
+       pseudoHandle = syscall.Handle(r0)
+       if pseudoHandle == 0 {
+               if e1 != 0 {
+                       err = errnoErr(e1)
+               } else {
+                       err = syscall.EINVAL
+               }
+       }
+       return
+}
+
+func ImpersonateSelf(impersonationlevel uint32) (err error) {
+       r1, _, e1 := syscall.Syscall(procImpersonateSelf.Addr(), 1, uintptr(impersonationlevel), 0, 0)
+       if r1 == 0 {
+               if e1 != 0 {
+                       err = errnoErr(e1)
+               } else {
+                       err = syscall.EINVAL
+               }
+       }
+       return
+}
+
+func RevertToSelf() (err error) {
+       r1, _, e1 := syscall.Syscall(procRevertToSelf.Addr(), 0, 0, 0, 0)
+       if r1 == 0 {
+               if e1 != 0 {
+                       err = errnoErr(e1)
+               } else {
+                       err = syscall.EINVAL
+               }
+       }
+       return
+}
+
+func OpenThreadToken(h syscall.Handle, access uint32, openasself bool, token *syscall.Token) (err error) {
+       var _p0 uint32
+       if openasself {
+               _p0 = 1
+       } else {
+               _p0 = 0
+       }
+       r1, _, e1 := syscall.Syscall6(procOpenThreadToken.Addr(), 4, uintptr(h), uintptr(access), uintptr(_p0), uintptr(unsafe.Pointer(token)), 0, 0)
+       if r1 == 0 {
+               if e1 != 0 {
+                       err = errnoErr(e1)
+               } else {
+                       err = syscall.EINVAL
+               }
+       }
+       return
+}
+
+func LookupPrivilegeValue(systemname *uint16, name *uint16, luid *LUID) (err error) {
+       r1, _, e1 := syscall.Syscall(procLookupPrivilegeValueW.Addr(), 3, uintptr(unsafe.Pointer(systemname)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(luid)))
+       if r1 == 0 {
+               if e1 != 0 {
+                       err = errnoErr(e1)
+               } else {
+                       err = syscall.EINVAL
+               }
+       }
+       return
+}
+
+func adjustTokenPrivileges(token syscall.Token, disableAllPrivileges bool, newstate *TOKEN_PRIVILEGES, buflen uint32, prevstate *TOKEN_PRIVILEGES, returnlen *uint32) (ret uint32, err error) {
+       var _p0 uint32
+       if disableAllPrivileges {
+               _p0 = 1
+       } else {
+               _p0 = 0
+       }
+       r0, _, e1 := syscall.Syscall6(procAdjustTokenPrivileges.Addr(), 6, uintptr(token), uintptr(_p0), uintptr(unsafe.Pointer(newstate)), uintptr(buflen), uintptr(unsafe.Pointer(prevstate)), uintptr(unsafe.Pointer(returnlen)))
+       ret = uint32(r0)
+       if true {
+               if e1 != 0 {
+                       err = errnoErr(e1)
+               } else {
+                       err = syscall.EINVAL
+               }
+       }
+       return
+}
index a6085f13680a908669c17c43608db68704326555..acdf4f17a658fb9ad5d282fc36f9d329cf388e41 100644 (file)
@@ -5,26 +5,21 @@
 package os_test
 
 import (
+       "fmt"
+       "internal/syscall/windows"
        "internal/testenv"
        "io/ioutil"
        "os"
        osexec "os/exec"
        "path/filepath"
+       "runtime"
        "sort"
        "strings"
        "syscall"
        "testing"
+       "unsafe"
 )
 
-var supportJunctionLinks = true
-
-func init() {
-       b, _ := osexec.Command("cmd", "/c", "mklink", "/?").Output()
-       if !strings.Contains(string(b), " /J ") {
-               supportJunctionLinks = false
-       }
-}
-
 func TestSameWindowsFile(t *testing.T) {
        temp, err := ioutil.TempDir("", "TestSameWindowsFile")
        if err != nil {
@@ -78,34 +73,331 @@ func TestSameWindowsFile(t *testing.T) {
        }
 }
 
-func TestStatJunctionLink(t *testing.T) {
-       if !supportJunctionLinks {
-               t.Skip("skipping because junction links are not supported")
+type dirLinkTest struct {
+       name    string
+       mklink  func(link, target string) error
+       issueNo int // correspondent issue number (for broken tests)
+}
+
+func testDirLinks(t *testing.T, tests []dirLinkTest) {
+       tmpdir, err := ioutil.TempDir("", "testDirLinks")
+       if err != nil {
+               t.Fatal(err)
        }
+       defer os.RemoveAll(tmpdir)
 
-       dir, err := ioutil.TempDir("", "go-build")
+       oldwd, err := os.Getwd()
        if err != nil {
-               t.Fatalf("failed to create temp directory: %v", err)
+               t.Fatal(err)
        }
-       defer os.RemoveAll(dir)
+       err = os.Chdir(tmpdir)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.Chdir(oldwd)
 
-       link := filepath.Join(filepath.Dir(dir), filepath.Base(dir)+"-link")
+       dir := filepath.Join(tmpdir, "dir")
+       err = os.Mkdir(dir, 0777)
+       if err != nil {
+               t.Fatal(err)
+       }
+       err = ioutil.WriteFile(filepath.Join(dir, "abc"), []byte("abc"), 0644)
+       if err != nil {
+               t.Fatal(err)
+       }
+       for _, test := range tests {
+               link := filepath.Join(tmpdir, test.name+"_link")
+               err := test.mklink(link, dir)
+               if err != nil {
+                       t.Errorf("creating link for %s test failed: %v", test.name, err)
+                       continue
+               }
+
+               data, err := ioutil.ReadFile(filepath.Join(link, "abc"))
+               if err != nil {
+                       t.Errorf("failed to read abc file: %v", err)
+                       continue
+               }
+               if string(data) != "abc" {
+                       t.Errorf(`abc file is expected to have "abc" in it, but has %v`, data)
+                       continue
+               }
+
+               if test.issueNo > 0 {
+                       t.Logf("skipping broken %q test: see issue %d", test.name, test.issueNo)
+                       continue
+               }
+
+               fi, err := os.Stat(link)
+               if err != nil {
+                       t.Errorf("failed to stat link %v: %v", link, err)
+                       continue
+               }
+               expected := filepath.Base(dir)
+               got := fi.Name()
+               if !fi.IsDir() || expected != got {
+                       t.Errorf("link should point to %v but points to %v instead", expected, got)
+                       continue
+               }
+       }
+}
+
+// reparseData is used to build reparse buffer data required for tests.
+type reparseData struct {
+       substituteName namePosition
+       printName      namePosition
+       pathBuf        []uint16
+}
 
-       output, err := osexec.Command("cmd", "/c", "mklink", "/J", link, dir).CombinedOutput()
+type namePosition struct {
+       offset uint16
+       length uint16
+}
+
+func (rd *reparseData) addUTF16s(s []uint16) (offset uint16) {
+       off := len(rd.pathBuf) * 2
+       rd.pathBuf = append(rd.pathBuf, s...)
+       return uint16(off)
+}
+
+func (rd *reparseData) addString(s string) (offset, length uint16) {
+       p := syscall.StringToUTF16(s)
+       return rd.addUTF16s(p), uint16(len(p)-1) * 2 // do not include terminating NUL in the legth (as per PrintNameLength and SubstituteNameLength documentation)
+}
+
+func (rd *reparseData) addSubstituteName(name string) {
+       rd.substituteName.offset, rd.substituteName.length = rd.addString(name)
+}
+
+func (rd *reparseData) addPrintName(name string) {
+       rd.printName.offset, rd.printName.length = rd.addString(name)
+}
+
+func (rd *reparseData) addStringNoNUL(s string) (offset, length uint16) {
+       p := syscall.StringToUTF16(s)
+       p = p[:len(p)-1]
+       return rd.addUTF16s(p), uint16(len(p)) * 2
+}
+
+func (rd *reparseData) addSubstituteNameNoNUL(name string) {
+       rd.substituteName.offset, rd.substituteName.length = rd.addStringNoNUL(name)
+}
+
+func (rd *reparseData) addPrintNameNoNUL(name string) {
+       rd.printName.offset, rd.printName.length = rd.addStringNoNUL(name)
+}
+
+// pathBuffeLen returns length of rd pathBuf in bytes.
+func (rd *reparseData) pathBuffeLen() uint16 {
+       return uint16(len(rd.pathBuf)) * 2
+}
+
+// Windows REPARSE_DATA_BUFFER contains union member, and cannot be
+// translated into Go directly. _REPARSE_DATA_BUFFER type is to help
+// construct alternative versions of Windows REPARSE_DATA_BUFFER with
+// union part of SymbolicLinkReparseBuffer or MountPointReparseBuffer type.
+type _REPARSE_DATA_BUFFER struct {
+       header windows.REPARSE_DATA_BUFFER_HEADER
+       detail [syscall.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]byte
+}
+
+func createDirLink(link string, rdb *_REPARSE_DATA_BUFFER) error {
+       err := os.Mkdir(link, 0777)
+       if err != nil {
+               return err
+       }
+
+       linkp := syscall.StringToUTF16(link)
+       fd, err := syscall.CreateFile(&linkp[0], syscall.GENERIC_WRITE, 0, nil, syscall.OPEN_EXISTING,
+               syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
+       if err != nil {
+               return err
+       }
+       defer syscall.CloseHandle(fd)
+
+       buflen := uint32(rdb.header.ReparseDataLength) + uint32(unsafe.Sizeof(rdb.header))
+       var bytesReturned uint32
+       return syscall.DeviceIoControl(fd, windows.FSCTL_SET_REPARSE_POINT,
+               (*byte)(unsafe.Pointer(&rdb.header)), buflen, nil, 0, &bytesReturned, nil)
+}
+
+func createMountPoint(link string, target *reparseData) error {
+       var buf *windows.MountPointReparseBuffer
+       buflen := uint16(unsafe.Offsetof(buf.PathBuffer)) + target.pathBuffeLen() // see ReparseDataLength documentation
+       byteblob := make([]byte, buflen)
+       buf = (*windows.MountPointReparseBuffer)(unsafe.Pointer(&byteblob[0]))
+       buf.SubstituteNameOffset = target.substituteName.offset
+       buf.SubstituteNameLength = target.substituteName.length
+       buf.PrintNameOffset = target.printName.offset
+       buf.PrintNameLength = target.printName.length
+       copy((*[2048]uint16)(unsafe.Pointer(&buf.PathBuffer[0]))[:], target.pathBuf)
+
+       var rdb _REPARSE_DATA_BUFFER
+       rdb.header.ReparseTag = windows.IO_REPARSE_TAG_MOUNT_POINT
+       rdb.header.ReparseDataLength = buflen
+       copy(rdb.detail[:], byteblob)
+
+       return createDirLink(link, &rdb)
+}
+
+func TestDirectoryJunction(t *testing.T) {
+       var tests = []dirLinkTest{
+               {
+                       // Create link similar to what mklink does, by inserting \??\ at the front of absolute target.
+                       name: "standard",
+                       mklink: func(link, target string) error {
+                               var t reparseData
+                               t.addSubstituteName(`\??\` + target)
+                               t.addPrintName(target)
+                               return createMountPoint(link, &t)
+                       },
+               },
+               {
+                       // Do as junction utility https://technet.microsoft.com/en-au/sysinternals/bb896768.aspx does - set PrintNameLength to 0.
+                       name: "have_blank_print_name",
+                       mklink: func(link, target string) error {
+                               var t reparseData
+                               t.addSubstituteName(`\??\` + target)
+                               t.addPrintName("")
+                               return createMountPoint(link, &t)
+                       },
+                       issueNo: 16145,
+               },
+       }
+       output, _ := osexec.Command("cmd", "/c", "mklink", "/?").Output()
+       mklinkSupportsJunctionLinks := strings.Contains(string(output), " /J ")
+       if mklinkSupportsJunctionLinks {
+               tests = append(tests,
+                       dirLinkTest{
+                               name: "use_mklink_cmd",
+                               mklink: func(link, target string) error {
+                                       output, err := osexec.Command("cmd", "/c", "mklink", "/J", link, target).CombinedOutput()
+                                       if err != nil {
+                                               fmt.Errorf("failed to run mklink %v %v: %v %q", link, target, err, output)
+                                       }
+                                       return nil
+                               },
+                       },
+               )
+       } else {
+               t.Log(`skipping "use_mklink_cmd" test, mklink does not supports directory junctions`)
+       }
+       testDirLinks(t, tests)
+}
+
+func enableCurrentThreadPrivilege(privilegeName string) error {
+       ct, err := windows.GetCurrentThread()
        if err != nil {
-               t.Fatalf("failed to run mklink %v %v: %v %q", link, dir, err, output)
+               return err
        }
-       defer os.Remove(link)
+       var t syscall.Token
+       err = windows.OpenThreadToken(ct, syscall.TOKEN_QUERY|windows.TOKEN_ADJUST_PRIVILEGES, false, &t)
+       if err != nil {
+               return err
+       }
+       defer syscall.CloseHandle(syscall.Handle(t))
+
+       var tp windows.TOKEN_PRIVILEGES
 
-       fi, err := os.Stat(link)
+       privStr, err := syscall.UTF16PtrFromString(privilegeName)
        if err != nil {
-               t.Fatalf("failed to stat link %v: %v", link, err)
+               return err
        }
-       expected := filepath.Base(dir)
-       got := fi.Name()
-       if !fi.IsDir() || expected != got {
-               t.Fatalf("link should point to %v but points to %v instead", expected, got)
+       err = windows.LookupPrivilegeValue(nil, privStr, &tp.Privileges[0].Luid)
+       if err != nil {
+               return err
+       }
+       tp.PrivilegeCount = 1
+       tp.Privileges[0].Attributes = windows.SE_PRIVILEGE_ENABLED
+       return windows.AdjustTokenPrivileges(t, false, &tp, 0, nil, nil)
+}
+
+func createSymbolicLink(link string, target *reparseData, isrelative bool) error {
+       var buf *windows.SymbolicLinkReparseBuffer
+       buflen := uint16(unsafe.Offsetof(buf.PathBuffer)) + target.pathBuffeLen() // see ReparseDataLength documentation
+       byteblob := make([]byte, buflen)
+       buf = (*windows.SymbolicLinkReparseBuffer)(unsafe.Pointer(&byteblob[0]))
+       buf.SubstituteNameOffset = target.substituteName.offset
+       buf.SubstituteNameLength = target.substituteName.length
+       buf.PrintNameOffset = target.printName.offset
+       buf.PrintNameLength = target.printName.length
+       if isrelative {
+               buf.Flags = windows.SYMLINK_FLAG_RELATIVE
+       }
+       copy((*[2048]uint16)(unsafe.Pointer(&buf.PathBuffer[0]))[:], target.pathBuf)
+
+       var rdb _REPARSE_DATA_BUFFER
+       rdb.header.ReparseTag = syscall.IO_REPARSE_TAG_SYMLINK
+       rdb.header.ReparseDataLength = buflen
+       copy(rdb.detail[:], byteblob)
+
+       return createDirLink(link, &rdb)
+}
+
+func TestDirectorySymbolicLink(t *testing.T) {
+       var tests []dirLinkTest
+       output, _ := osexec.Command("cmd", "/c", "mklink", "/?").Output()
+       mklinkSupportsDirectorySymbolicLinks := strings.Contains(string(output), " /D ")
+       if mklinkSupportsDirectorySymbolicLinks {
+               tests = append(tests,
+                       dirLinkTest{
+                               name: "use_mklink_cmd",
+                               mklink: func(link, target string) error {
+                                       output, err := osexec.Command("cmd", "/c", "mklink", "/D", link, target).CombinedOutput()
+                                       if err != nil {
+                                               fmt.Errorf("failed to run mklink %v %v: %v %q", link, target, err, output)
+                                       }
+                                       return nil
+                               },
+                       },
+               )
+       } else {
+               t.Log(`skipping "use_mklink_cmd" test, mklink does not supports directory symbolic links`)
+       }
+
+       // The rest of these test requires SeCreateSymbolicLinkPrivilege to be held.
+       runtime.LockOSThread()
+       defer runtime.UnlockOSThread()
+
+       err := windows.ImpersonateSelf(windows.SecurityImpersonation)
+       if err != nil {
+               t.Fatal(err)
        }
+       defer windows.RevertToSelf()
+
+       err = enableCurrentThreadPrivilege("SeCreateSymbolicLinkPrivilege")
+       if err != nil {
+               t.Skipf(`skipping some tests, could not enable "SeCreateSymbolicLinkPrivilege": %v`, err)
+       }
+       tests = append(tests,
+               dirLinkTest{
+                       name: "use_os_pkg",
+                       mklink: func(link, target string) error {
+                               return os.Symlink(target, link)
+                       },
+               },
+               dirLinkTest{
+                       // Create link similar to what mklink does, by inserting \??\ at the front of absolute target.
+                       name: "standard",
+                       mklink: func(link, target string) error {
+                               var t reparseData
+                               t.addPrintName(target)
+                               t.addSubstituteName(`\??\` + target)
+                               return createSymbolicLink(link, &t, false)
+                       },
+               },
+               dirLinkTest{
+                       name: "relative",
+                       mklink: func(link, target string) error {
+                               var t reparseData
+                               t.addSubstituteNameNoNUL(filepath.Base(target))
+                               t.addPrintNameNoNUL(filepath.Base(target))
+                               return createSymbolicLink(link, &t, true)
+                       },
+                       issueNo: 15978,
+               },
+       )
+       testDirLinks(t, tests)
 }
 
 func TestStartProcessAttr(t *testing.T) {