Skip to content

Commit dfcef2f

Browse files
committed
Switch to a safer technique for obtaining the working directory on Windows
Instead of looping 8 times to work around the TOCTOU issue with sizing the current directory buffer, instead keep doubling the buffer up until the 32767 character limit until the result fits. This ensures we always get a working directory if GetWorkingDirectoryW didn't return some other error, rather than returning nil in the case of a race condition.
1 parent 7ae9160 commit dfcef2f

File tree

2 files changed

+36
-15
lines changed

2 files changed

+36
-15
lines changed

Sources/FoundationEssentials/FileManager/FileManager+Directories.swift

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -489,22 +489,10 @@ extension _FileManagerImpl {
489489

490490
var currentDirectoryPath: String? {
491491
#if os(Windows)
492-
var dwLength: DWORD = GetCurrentDirectoryW(0, nil)
493-
guard dwLength > 0 else { return nil }
494-
495-
for _ in 0 ... 8 {
496-
if let szCurrentDirectory = withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength), {
497-
let dwResult: DWORD = GetCurrentDirectoryW(dwLength, $0.baseAddress)
498-
if dwResult == dwLength - 1 {
499-
return String(decodingCString: $0.baseAddress!, as: UTF16.self)
500-
}
501-
dwLength = dwResult
502-
return nil
503-
}) {
504-
return szCurrentDirectory
505-
}
492+
let dwSize = GetCurrentDirectoryW(0, nil)
493+
return try? FillNullTerminatedWideStringBuffer(initialSize: dwSize >= 0 ? dwSize : DWORD(MAX_PATH), maxSize: DWORD(Int16.max)) {
494+
GetCurrentDirectoryW(DWORD($0.count), $0.baseAddress)
506495
}
507-
return nil
508496
#else
509497
withUnsafeTemporaryAllocation(of: CChar.self, capacity: FileManager.MAX_PATH_SIZE) { buffer in
510498
guard getcwd(buffer.baseAddress!, FileManager.MAX_PATH_SIZE) != nil else {

Sources/FoundationEssentials/WinSDK+Extensions.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ package var ERROR_FILENAME_EXCED_RANGE: DWORD {
8181
DWORD(WinSDK.ERROR_FILENAME_EXCED_RANGE)
8282
}
8383

84+
package var ERROR_ILLEGAL_CHARACTER: DWORD {
85+
DWORD(WinSDK.ERROR_ILLEGAL_CHARACTER)
86+
}
87+
88+
package var ERROR_INSUFFICIENT_BUFFER: DWORD {
89+
DWORD(WinSDK.ERROR_INSUFFICIENT_BUFFER)
90+
}
91+
8492
package var ERROR_INVALID_ACCESS: DWORD {
8593
DWORD(WinSDK.ERROR_INVALID_ACCESS)
8694
}
@@ -288,4 +296,29 @@ internal func WIN32_FROM_HRESULT(_ hr: HRESULT) -> DWORD {
288296
return DWORD(hr)
289297
}
290298

299+
internal func FillNullTerminatedWideStringBuffer(initialSize: DWORD, maxSize: DWORD, _ body: (UnsafeMutableBufferPointer<WCHAR>) throws -> DWORD) throws -> String {
300+
var bufferCount = max(1, min(initialSize, maxSize))
301+
while bufferCount < maxSize {
302+
if let result = try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(bufferCount), { buffer in
303+
let count = try body(buffer)
304+
switch count {
305+
case 0:
306+
throw Win32Error(GetLastError())
307+
case 1..<DWORD(buffer.count):
308+
guard let result = String.decodeCString(buffer.baseAddress!, as: UTF16.self)?.result else {
309+
throw Win32Error(ERROR_ILLEGAL_CHARACTER)
310+
}
311+
assert(result.utf16.count == count, "Parsed UTF-16 count \(result.utf16.count) != reported UTF-16 count \(count)")
312+
return result
313+
default:
314+
bufferCount *= 2
315+
return nil
316+
}
317+
}) {
318+
return result
319+
}
320+
}
321+
throw Win32Error(ERROR_INSUFFICIENT_BUFFER)
322+
}
323+
291324
#endif

0 commit comments

Comments
 (0)