Skip to content

Commit 9f14f77

Browse files
committed
DRAFT: Add launcher binary that allows for debugging missing DLL dependencies at load time
1 parent 66cdd47 commit 9f14f77

File tree

1 file changed

+291
-0
lines changed

1 file changed

+291
-0
lines changed

Sources/swblauncher/main.swift

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

0 commit comments

Comments
 (0)