Skip to content

Commit b8371d4

Browse files
committed
runtime: support long paths without fixup on Windows 10 >= 1607
Windows 10 >= 1607 allows CreateFile and friends to use long paths if bit 0x80 of the PEB's BitField member is set. In time this means we'll be able to entirely drop our long path hacks, which have never really worked right (see bugs below). Until that point, we'll simply have things working well on recent Windows. Updates #41734. Updates #21782. Updates #36375. Change-Id: I765de6ea4859dd4e4b8ca80af7f337994734118e Reviewed-on: https://go-review.googlesource.com/c/go/+/291291 Trust: Jason A. Donenfeld <[email protected]> Run-TryBot: Jason A. Donenfeld <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent b182ba7 commit b8371d4

File tree

4 files changed

+118
-10
lines changed

4 files changed

+118
-10
lines changed

src/os/export_windows_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package os
88

99
var (
1010
FixLongPath = fixLongPath
11+
CanUseLongPaths = canUseLongPaths
1112
NewConsoleFile = newConsoleFile
1213
CommandLineToArgv = commandLineToArgv
1314
)

src/os/path_windows.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ func dirname(path string) string {
128128
return vol + dir
129129
}
130130

131+
// This is set via go:linkname on runtime.canUseLongPaths, and is true when the OS
132+
// supports opting into proper long path handling without the need for fixups.
133+
var canUseLongPaths bool
134+
131135
// fixLongPath returns the extended-length (\\?\-prefixed) form of
132136
// path when needed, in order to avoid the default 260 character file
133137
// path limit imposed by Windows. If path is not easily converted to
@@ -137,6 +141,9 @@ func dirname(path string) string {
137141
//
138142
// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
139143
func fixLongPath(path string) string {
144+
if canUseLongPaths {
145+
return path
146+
}
140147
// Do nothing (and don't allocate) if the path is "short".
141148
// Empirically (at least on the Windows Server 2013 builder),
142149
// the kernel is arbitrarily okay with < 248 bytes. That

src/os/path_windows_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import (
1212
)
1313

1414
func TestFixLongPath(t *testing.T) {
15+
if os.CanUseLongPaths {
16+
return
17+
}
1518
// 248 is long enough to trigger the longer-than-248 checks in
1619
// fixLongPath, but short enough not to make a path component
1720
// longer than 255, which is illegal on Windows. (which
@@ -46,6 +49,26 @@ func TestFixLongPath(t *testing.T) {
4649
}
4750
}
4851

52+
func TestMkdirAllLongPath(t *testing.T) {
53+
tmpDir, err := os.MkdirTemp("", "TestMkdirAllLongPath")
54+
if err != nil {
55+
t.Fatal(err)
56+
}
57+
defer os.RemoveAll(tmpDir)
58+
path := tmpDir
59+
for i := 0; i < 100; i++ {
60+
path += `\another-path-component`
61+
}
62+
err = os.MkdirAll(path, 0777)
63+
if err != nil {
64+
t.Fatalf("MkdirAll(%q) failed; %v", path, err)
65+
}
66+
err = os.RemoveAll(tmpDir)
67+
if err != nil {
68+
t.Fatalf("RemoveAll(%q) failed; %v", tmpDir, err)
69+
}
70+
}
71+
4972
func TestMkdirAllExtendedLength(t *testing.T) {
5073
tmpDir, err := os.MkdirTemp("", "TestMkdirAllExtendedLength")
5174
if err != nil {

src/runtime/os_windows.go

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const (
1818
//go:cgo_import_dynamic runtime._AddVectoredExceptionHandler AddVectoredExceptionHandler%2 "kernel32.dll"
1919
//go:cgo_import_dynamic runtime._CloseHandle CloseHandle%1 "kernel32.dll"
2020
//go:cgo_import_dynamic runtime._CreateEventA CreateEventA%4 "kernel32.dll"
21+
//go:cgo_import_dynamic runtime._CreateFileA CreateFileA%7 "kernel32.dll"
2122
//go:cgo_import_dynamic runtime._CreateIoCompletionPort CreateIoCompletionPort%4 "kernel32.dll"
2223
//go:cgo_import_dynamic runtime._CreateThread CreateThread%6 "kernel32.dll"
2324
//go:cgo_import_dynamic runtime._CreateWaitableTimerA CreateWaitableTimerA%3 "kernel32.dll"
@@ -67,6 +68,7 @@ var (
6768
_AddVectoredExceptionHandler,
6869
_CloseHandle,
6970
_CreateEventA,
71+
_CreateFileA,
7072
_CreateIoCompletionPort,
7173
_CreateThread,
7274
_CreateWaitableTimerA,
@@ -132,7 +134,9 @@ var (
132134
// Load ntdll.dll manually during startup, otherwise Mingw
133135
// links wrong printf function to cgo executable (see issue
134136
// 12030 for details).
135-
_NtWaitForSingleObject stdFunction
137+
_NtWaitForSingleObject stdFunction
138+
_RtlGetCurrentPeb stdFunction
139+
_RtlGetNtVersionNumbers stdFunction
136140

137141
// These are from non-kernel32.dll, so we prefer to LoadLibraryEx them.
138142
_timeBeginPeriod,
@@ -219,21 +223,22 @@ func windowsFindfunc(lib uintptr, name []byte) stdFunction {
219223
return stdFunction(unsafe.Pointer(f))
220224
}
221225

222-
var sysDirectory [521]byte
226+
const _MAX_PATH = 260 // https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
227+
var sysDirectory [_MAX_PATH + 1]byte
223228
var sysDirectoryLen uintptr
224229

225230
func windowsLoadSystemLib(name []byte) uintptr {
231+
if sysDirectoryLen == 0 {
232+
l := stdcall2(_GetSystemDirectoryA, uintptr(unsafe.Pointer(&sysDirectory[0])), uintptr(len(sysDirectory)-1))
233+
if l == 0 || l > uintptr(len(sysDirectory)-1) {
234+
throw("Unable to determine system directory")
235+
}
236+
sysDirectory[l] = '\\'
237+
sysDirectoryLen = l + 1
238+
}
226239
if useLoadLibraryEx {
227240
return stdcall3(_LoadLibraryExA, uintptr(unsafe.Pointer(&name[0])), 0, _LOAD_LIBRARY_SEARCH_SYSTEM32)
228241
} else {
229-
if sysDirectoryLen == 0 {
230-
l := stdcall2(_GetSystemDirectoryA, uintptr(unsafe.Pointer(&sysDirectory[0])), uintptr(len(sysDirectory)-1))
231-
if l == 0 || l > uintptr(len(sysDirectory)-1) {
232-
throw("Unable to determine system directory")
233-
}
234-
sysDirectory[l] = '\\'
235-
sysDirectoryLen = l + 1
236-
}
237242
absName := append(sysDirectory[:sysDirectoryLen], name...)
238243
return stdcall1(_LoadLibraryA, uintptr(unsafe.Pointer(&absName[0])))
239244
}
@@ -266,6 +271,8 @@ func loadOptionalSyscalls() {
266271
throw("ntdll.dll not found")
267272
}
268273
_NtWaitForSingleObject = windowsFindfunc(n32, []byte("NtWaitForSingleObject\000"))
274+
_RtlGetCurrentPeb = windowsFindfunc(n32, []byte("RtlGetCurrentPeb\000"))
275+
_RtlGetNtVersionNumbers = windowsFindfunc(n32, []byte("RtlGetNtVersionNumbers\000"))
269276

270277
if !haveCputicksAsm {
271278
_QueryPerformanceCounter = windowsFindfunc(k32, []byte("QueryPerformanceCounter\000"))
@@ -471,6 +478,74 @@ func initHighResTimer() {
471478
}
472479
}
473480

481+
//go:linkname canUseLongPaths os.canUseLongPaths
482+
var canUseLongPaths bool
483+
484+
// We want this to be large enough to hold the contents of sysDirectory, *plus*
485+
// a slash and another component that itself is greater than MAX_PATH.
486+
var longFileName [(_MAX_PATH+1)*2 + 1]byte
487+
488+
// initLongPathSupport initializes the canUseLongPaths variable, which is
489+
// linked into os.canUseLongPaths for determining whether or not long paths
490+
// need to be fixed up. In the best case, this function is running on newer
491+
// Windows 10 builds, which have a bit field member of the PEB called
492+
// "IsLongPathAwareProcess." When this is set, we don't need to go through the
493+
// error-prone fixup function in order to access long paths. So this init
494+
// function first checks the Windows build number, sets the flag, and then
495+
// tests to see if it's actually working. If everything checks out, then
496+
// canUseLongPaths is set to true, and later when called, os.fixLongPath
497+
// returns early without doing work.
498+
func initLongPathSupport() {
499+
const (
500+
IsLongPathAwareProcess = 0x80
501+
PebBitFieldOffset = 3
502+
OPEN_EXISTING = 3
503+
ERROR_PATH_NOT_FOUND = 3
504+
)
505+
506+
// Check that we're ≥ 10.0.15063.
507+
var maj, min, build uint32
508+
stdcall3(_RtlGetNtVersionNumbers, uintptr(unsafe.Pointer(&maj)), uintptr(unsafe.Pointer(&min)), uintptr(unsafe.Pointer(&build)))
509+
if maj < 10 || (maj == 10 && min == 0 && build&0xffff < 15063) {
510+
return
511+
}
512+
513+
// Set the IsLongPathAwareProcess flag of the PEB's bit field.
514+
bitField := (*byte)(unsafe.Pointer(stdcall0(_RtlGetCurrentPeb) + PebBitFieldOffset))
515+
originalBitField := *bitField
516+
*bitField |= IsLongPathAwareProcess
517+
518+
// Check that this actually has an effect, by constructing a large file
519+
// path and seeing whether we get ERROR_PATH_NOT_FOUND, rather than
520+
// some other error, which would indicate the path is too long, and
521+
// hence long path support is not successful. This whole section is NOT
522+
// strictly necessary, but is a nice validity check for the near to
523+
// medium term, when this functionality is still relatively new in
524+
// Windows.
525+
getRandomData(longFileName[len(longFileName)-33 : len(longFileName)-1])
526+
start := copy(longFileName[:], sysDirectory[:sysDirectoryLen])
527+
const dig = "0123456789abcdef"
528+
for i := 0; i < 32; i++ {
529+
longFileName[start+i*2] = dig[longFileName[len(longFileName)-33+i]>>4]
530+
longFileName[start+i*2+1] = dig[longFileName[len(longFileName)-33+i]&0xf]
531+
}
532+
start += 64
533+
for i := start; i < len(longFileName)-1; i++ {
534+
longFileName[i] = 'A'
535+
}
536+
stdcall7(_CreateFileA, uintptr(unsafe.Pointer(&longFileName[0])), 0, 0, 0, OPEN_EXISTING, 0, 0)
537+
// The ERROR_PATH_NOT_FOUND error value is distinct from
538+
// ERROR_FILE_NOT_FOUND or ERROR_INVALID_NAME, the latter of which we
539+
// expect here due to the final component being too long.
540+
if getlasterror() == ERROR_PATH_NOT_FOUND {
541+
*bitField = originalBitField
542+
println("runtime: warning: IsLongPathAwareProcess failed to enable long paths; proceeding in fixup mode")
543+
return
544+
}
545+
546+
canUseLongPaths = true
547+
}
548+
474549
func osinit() {
475550
asmstdcallAddr = unsafe.Pointer(funcPC(asmstdcall))
476551

@@ -487,6 +562,8 @@ func osinit() {
487562
initHighResTimer()
488563
timeBeginPeriodRetValue = osRelax(false)
489564

565+
initLongPathSupport()
566+
490567
ncpu = getproccount()
491568

492569
physPageSize = getPageSize()

0 commit comments

Comments
 (0)