Skip to content

Commit f9d3e7e

Browse files
committed
DRAFT: Add launcher binary that allows for debugging missing DLL dependencies at load time
1 parent f49864e commit f9d3e7e

File tree

1 file changed

+267
-0
lines changed

1 file changed

+267
-0
lines changed

Sources/swblauncher/main.swift

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import WinSDK
14+
import struct Foundation.Data
15+
import class Foundation.FileHandle
16+
import struct Foundation.URL
17+
18+
// Also see winternl.h in the Windows SDK for the definitions of a number of these structures.
19+
20+
// https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess#parameters
21+
fileprivate let ProcessBasicInformation: CInt = 0
22+
23+
// https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/show-loader-snaps
24+
fileprivate let FLG_SHOW_LOADER_SNAPS: ULONG = 0x2
25+
26+
// https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess#process_basic_information
27+
fileprivate struct PROCESS_BASIC_INFORMATION {
28+
var ExitStatus: NTSTATUS = 0
29+
var PebBaseAddress: ULONG_PTR = 0
30+
var AffinityMask: ULONG_PTR = 0
31+
var BasePriority: LONG = 0
32+
var UniqueProcessId: ULONG_PTR = 0
33+
var InheritedFromUniqueProcessId: ULONG_PTR = 0
34+
}
35+
36+
// https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess
37+
fileprivate typealias NtQueryInformationProcessFunction = @convention(c) (_ ProcessHandle: HANDLE, _ ProcessInformationClass: CInt, _ ProcessInformation: PVOID, _ ProcessInformationLength: ULONG, _ ReturnLength: PULONG) -> NTSTATUS
38+
39+
fileprivate struct _Win32Error: Error {
40+
let functionName: String
41+
let error: DWORD
42+
}
43+
44+
fileprivate nonisolated var KF_FLAG_DEFAULT: DWORD {
45+
DWORD(WinSDK.KF_FLAG_DEFAULT.rawValue)
46+
}
47+
48+
fileprivate func SUCCEEDED(_ hr: HRESULT) -> Bool {
49+
hr >= 0
50+
}
51+
52+
fileprivate func _url(for id: KNOWNFOLDERID) throws -> URL {
53+
var pszPath: PWSTR?
54+
let hr: HRESULT = withUnsafePointer(to: id) { id in
55+
SHGetKnownFolderPath(id, KF_FLAG_DEFAULT, nil, &pszPath)
56+
}
57+
guard SUCCEEDED(hr) else { throw _Win32Error(functionName: "SHGetKnownFolderPath", error: GetLastError()) }
58+
defer { CoTaskMemFree(pszPath) }
59+
return URL(filePath: String(decodingCString: pszPath!, as: UTF16.self), directoryHint: .isDirectory)
60+
}
61+
62+
extension String {
63+
fileprivate func withLPWSTR<T>(_ body: (UnsafeMutablePointer<WCHAR>) throws -> T) rethrows -> T {
64+
try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: self.utf16.count + 1, { outBuffer in
65+
try self.withCString(encodedAs: UTF16.self) { inBuffer in
66+
outBuffer.baseAddress!.initialize(from: inBuffer, count: self.utf16.count)
67+
outBuffer[outBuffer.count - 1] = 0
68+
return try body(outBuffer.baseAddress!)
69+
}
70+
})
71+
}
72+
}
73+
74+
extension PROCESS_BASIC_INFORMATION {
75+
fileprivate init(_ hProcess: HANDLE, _ NtQueryInformation: NtQueryInformationProcessFunction) throws {
76+
self.init()
77+
78+
let processBasicInformationSize = MemoryLayout.size(ofValue: self)
79+
#if arch(x86_64) || arch(arm64)
80+
precondition(processBasicInformationSize == 48)
81+
#elseif arch(i386) || arch(arm)
82+
precondition(processBasicInformationSize == 24)
83+
#else
84+
#error("Unsupported architecture")
85+
#endif
86+
87+
var len: ULONG = 0
88+
guard NtQueryInformation(hProcess, ProcessBasicInformation, &self, ULONG(processBasicInformationSize), &len) == 0 else {
89+
throw _Win32Error(functionName: "NtQueryInformationProcess", error: GetLastError())
90+
}
91+
}
92+
93+
// FIXME: Does this work for mixed architecture scenarios? WoW64 seems to be OK.
94+
fileprivate var PebBaseAddress_NtGlobalFlag: ULONG_PTR {
95+
#if arch(x86_64) || arch(arm64)
96+
PebBaseAddress + 0xBC // https://github.com/wine-mirror/wine/blob/e1af2ae201c9853133ef3af1dafe15fe992fed92/include/winternl.h#L990 (undocumented officially)
97+
#elseif arch(i386) || arch(arm)
98+
PebBaseAddress + 0x68 // https://github.com/wine-mirror/wine/blob/e1af2ae201c9853133ef3af1dafe15fe992fed92/include/winternl.h#L880 (undocumented officially)
99+
#else
100+
#error("Unsupported architecture")
101+
#endif
102+
}
103+
}
104+
105+
fileprivate func withGFlags(_ hProcess: HANDLE, _ ProcessBasicInformation: PROCESS_BASIC_INFORMATION, _ block: (_ gflags: inout ULONG) -> ()) throws {
106+
// https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/gflags-flag-table
107+
var gflags: ULONG = 0
108+
var actual: SIZE_T = 0
109+
guard ReadProcessMemory(hProcess, UnsafeMutableRawPointer(bitPattern: Int(ProcessBasicInformation.PebBaseAddress_NtGlobalFlag)), &gflags, SIZE_T(MemoryLayout.size(ofValue: gflags)), &actual) else {
110+
throw _Win32Error(functionName: "ReadProcessMemory", error: GetLastError())
111+
}
112+
113+
block(&gflags)
114+
guard WriteProcessMemory(hProcess, UnsafeMutableRawPointer(bitPattern: Int(ProcessBasicInformation.PebBaseAddress_NtGlobalFlag)), &gflags, SIZE_T(MemoryLayout.size(ofValue: gflags)), &actual) else {
115+
throw _Win32Error(functionName: "WriteProcessMemory", error: GetLastError())
116+
}
117+
}
118+
119+
func withDebugEventLoop(_ hProcess: HANDLE, _ handle: (_ event: String) throws -> ()) throws {
120+
let ntdllURL = try _url(for: FOLDERID_System).appendingPathComponent("ntdll.dll")
121+
guard let ntdll = ntdllURL.withUnsafeFileSystemRepresentation({ s in s.map({ String(cString: $0) }) ?? String() }).withLPWSTR({ LoadLibraryW($0) }) else {
122+
throw _Win32Error(functionName: "LoadLibraryW", error: GetLastError())
123+
}
124+
125+
defer {
126+
_ = FreeLibrary(ntdll)
127+
}
128+
129+
guard let ntQueryInformationProc = GetProcAddress(ntdll, "NtQueryInformationProcess") else {
130+
throw _Win32Error(functionName: "GetProcAddress", error: GetLastError())
131+
}
132+
133+
let processBasicInformation = try PROCESS_BASIC_INFORMATION(hProcess, unsafeBitCast(ntQueryInformationProc, to: NtQueryInformationProcessFunction.self))
134+
135+
try withGFlags(hProcess, processBasicInformation) { gflags in
136+
gflags |= FLG_SHOW_LOADER_SNAPS
137+
}
138+
139+
func debugOutputString(_ hProcess: HANDLE, _ dbgEvent: inout DEBUG_EVENT) throws -> String {
140+
let size = SIZE_T(dbgEvent.u.DebugString.nDebugStringLength)
141+
return try withUnsafeTemporaryAllocation(of: UInt8.self, capacity: Int(size) + 2) { buffer in
142+
guard ReadProcessMemory(hProcess, dbgEvent.u.DebugString.lpDebugStringData, buffer.baseAddress, size, nil) else {
143+
throw _Win32Error(functionName: "ReadProcessMemory", error: GetLastError())
144+
}
145+
146+
buffer[Int(size)] = 0
147+
buffer[Int(size + 1)] = 0
148+
149+
if dbgEvent.u.DebugString.fUnicode != 0 {
150+
return buffer.withMemoryRebound(to: UInt16.self) { String(decoding: $0, as: UTF16.self) }
151+
} else {
152+
return try withUnsafeTemporaryAllocation(of: UInt16.self, capacity: Int(size)) { wideBuffer in
153+
if MultiByteToWideChar(UINT(CP_ACP), 0, buffer.baseAddress, Int32(size), wideBuffer.baseAddress, Int32(size)) == 0 {
154+
throw _Win32Error(functionName: "MultiByteToWideChar", error: GetLastError())
155+
}
156+
return String(decoding: wideBuffer, as: UTF16.self)
157+
}
158+
}
159+
}
160+
}
161+
162+
func _WaitForDebugEventEx() throws -> DEBUG_EVENT {
163+
// WARNING: Only the thread that created the process being debugged can call WaitForDebugEventEx.
164+
var dbgEvent = DEBUG_EVENT()
165+
guard WaitForDebugEventEx(&dbgEvent, INFINITE) else {
166+
// WaitForDebugEventEx will fail if dwCreationFlags did not contain DEBUG_ONLY_THIS_PROCESS
167+
throw _Win32Error(functionName: "WaitForDebugEventEx", error: GetLastError())
168+
}
169+
return dbgEvent
170+
}
171+
172+
func runDebugEventLoop() throws {
173+
do {
174+
while true {
175+
var dbgEvent = try _WaitForDebugEventEx()
176+
if dbgEvent.dwProcessId == GetProcessId(hProcess) {
177+
switch dbgEvent.dwDebugEventCode {
178+
case DWORD(OUTPUT_DEBUG_STRING_EVENT):
179+
try handle(debugOutputString(hProcess, &dbgEvent))
180+
case DWORD(EXIT_PROCESS_DEBUG_EVENT):
181+
return // done!
182+
default:
183+
break
184+
}
185+
}
186+
187+
guard ContinueDebugEvent(dbgEvent.dwProcessId, dbgEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED) else {
188+
throw _Win32Error(functionName: "WaitForDebugEventEx", error: GetLastError())
189+
}
190+
}
191+
} catch {
192+
throw error
193+
}
194+
}
195+
196+
try runDebugEventLoop()
197+
}
198+
199+
func createProcessTrampoline(_ commandLine: String) throws -> Int32 {
200+
var processInformation = PROCESS_INFORMATION()
201+
guard commandLine.withLPWSTR({ wCommandLine in
202+
var startupInfo = STARTUPINFOW()
203+
startupInfo.cb = DWORD(MemoryLayout.size(ofValue: startupInfo))
204+
return CreateProcessW(
205+
nil,
206+
wCommandLine,
207+
nil,
208+
nil,
209+
false,
210+
DWORD(DEBUG_ONLY_THIS_PROCESS),
211+
nil,
212+
nil,
213+
&startupInfo,
214+
&processInformation,
215+
)
216+
}) else {
217+
throw _Win32Error(functionName: "CreateProcessW", error: GetLastError())
218+
}
219+
defer {
220+
_ = CloseHandle(processInformation.hThread)
221+
_ = CloseHandle(processInformation.hProcess)
222+
}
223+
var missingDLLs: [String] = []
224+
try withDebugEventLoop(processInformation.hProcess) { message in
225+
if let match = try #/ ERROR: Unable to load DLL: "(?<moduleName>.*?)",/#.firstMatch(in: message) {
226+
missingDLLs.append(String(match.output.moduleName))
227+
}
228+
}
229+
// Don't need to call WaitForSingleObject because the process will have exited after withDebugEventLoop is called
230+
var exitCode: DWORD = .max
231+
guard GetExitCodeProcess(processInformation.hProcess, &exitCode) else {
232+
throw _Win32Error(functionName: "GetExitCodeProcess", error: GetLastError())
233+
}
234+
if exitCode == STATUS_DLL_NOT_FOUND {
235+
let stderr = FileHandle.standardError
236+
for missingDLL in missingDLLs {
237+
try stderr.write(contentsOf: Data("This application has failed to start because \(missingDLL) was not found.\r\n".utf8))
238+
}
239+
}
240+
return Int32(bitPattern: exitCode)
241+
}
242+
243+
func main() -> Int32 {
244+
do {
245+
var commandLine = String(decodingCString: GetCommandLineW(), as: UTF16.self)
246+
247+
// FIXME: This could probably be more robust
248+
if commandLine.first == "\"" {
249+
commandLine = String(commandLine.dropFirst())
250+
if let index = commandLine.firstIndex(of: "\"") {
251+
commandLine = String(commandLine.dropFirst(commandLine.distance(from: commandLine.startIndex, to: index) + 2))
252+
}
253+
} else if let index = commandLine.firstIndex(of: " ") {
254+
commandLine = String(commandLine.dropFirst(commandLine.distance(from: commandLine.startIndex, to: index) + 1))
255+
} else {
256+
commandLine = ""
257+
}
258+
259+
return try createProcessTrampoline(commandLine)
260+
} catch {
261+
let stderr = FileHandle.standardError
262+
try? stderr.write(contentsOf: Data("\(error)\r\n".utf8))
263+
return EXIT_FAILURE
264+
}
265+
}
266+
267+
exit(main())

0 commit comments

Comments
 (0)