Skip to content

Commit ce8d50d

Browse files
committed
Fix an issue where process handles are leaked in exec on Windows
Because _exit immediately terminates the process, the CloseHandle calls in the defer blocks in the body of the exec function are never called. This leads to the handles leaking on Windows. Move the body of the function into a do block above the _exit call to resolve this.
1 parent bfafdeb commit ce8d50d

File tree

1 file changed

+54
-51
lines changed

1 file changed

+54
-51
lines changed

Sources/TSCBasic/misc.swift

Lines changed: 54 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -93,71 +93,74 @@ private func quote(_ arguments: [String]) -> String {
9393
public func exec(path: String, args: [String]) throws -> Never {
9494
let cArgs = CStringArray(args)
9595
#if os(Windows)
96-
var hJob: HANDLE
96+
// Wrap body in a do block to ensure closing handles in defer blocks occurs prior to the call to _exit
97+
var dwExitCode: DWORD = DWORD(bitPattern: -1)
98+
do {
99+
var hJob: HANDLE
97100

98-
hJob = CreateJobObjectA(nil, nil)
99-
if hJob == HANDLE(bitPattern: 0) {
100-
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
101-
}
102-
defer { CloseHandle(hJob) }
101+
hJob = CreateJobObjectA(nil, nil)
102+
if hJob == HANDLE(bitPattern: 0) {
103+
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
104+
}
105+
defer { CloseHandle(hJob) }
103106

104-
let hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nil, 0, 1)
105-
if hPort == HANDLE(bitPattern: 0) {
106-
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
107-
}
107+
let hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nil, 0, 1)
108+
if hPort == HANDLE(bitPattern: 0) {
109+
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
110+
}
108111

109-
var acpAssociation: JOBOBJECT_ASSOCIATE_COMPLETION_PORT = JOBOBJECT_ASSOCIATE_COMPLETION_PORT()
110-
acpAssociation.CompletionKey = hJob
111-
acpAssociation.CompletionPort = hPort
112-
if !SetInformationJobObject(hJob, JobObjectAssociateCompletionPortInformation,
113-
&acpAssociation, DWORD(MemoryLayout<JOBOBJECT_ASSOCIATE_COMPLETION_PORT>.size)) {
114-
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
115-
}
112+
var acpAssociation: JOBOBJECT_ASSOCIATE_COMPLETION_PORT = JOBOBJECT_ASSOCIATE_COMPLETION_PORT()
113+
acpAssociation.CompletionKey = hJob
114+
acpAssociation.CompletionPort = hPort
115+
if !SetInformationJobObject(hJob, JobObjectAssociateCompletionPortInformation,
116+
&acpAssociation, DWORD(MemoryLayout<JOBOBJECT_ASSOCIATE_COMPLETION_PORT>.size)) {
117+
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
118+
}
116119

117-
var eliLimits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
118-
eliLimits.BasicLimitInformation.LimitFlags =
119-
DWORD(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE) | DWORD(JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK)
120-
if !SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &eliLimits,
121-
DWORD(MemoryLayout<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>.size)) {
122-
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
123-
}
120+
var eliLimits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
121+
eliLimits.BasicLimitInformation.LimitFlags =
122+
DWORD(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE) | DWORD(JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK)
123+
if !SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &eliLimits,
124+
DWORD(MemoryLayout<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>.size)) {
125+
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
126+
}
124127

125128

126-
var siInfo: STARTUPINFOW = STARTUPINFOW()
127-
siInfo.cb = DWORD(MemoryLayout<STARTUPINFOW>.size)
129+
var siInfo: STARTUPINFOW = STARTUPINFOW()
130+
siInfo.cb = DWORD(MemoryLayout<STARTUPINFOW>.size)
128131

129-
var piInfo: PROCESS_INFORMATION = PROCESS_INFORMATION()
132+
var piInfo: PROCESS_INFORMATION = PROCESS_INFORMATION()
130133

131-
try quote(args).withCString(encodedAs: UTF16.self) { pwszCommandLine in
132-
if !CreateProcessW(nil,
133-
UnsafeMutablePointer<WCHAR>(mutating: pwszCommandLine),
134-
nil, nil, false,
135-
DWORD(CREATE_SUSPENDED) | DWORD(CREATE_NEW_PROCESS_GROUP),
136-
nil, nil, &siInfo, &piInfo) {
137-
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
134+
try quote(args).withCString(encodedAs: UTF16.self) { pwszCommandLine in
135+
if !CreateProcessW(nil,
136+
UnsafeMutablePointer<WCHAR>(mutating: pwszCommandLine),
137+
nil, nil, false,
138+
DWORD(CREATE_SUSPENDED) | DWORD(CREATE_NEW_PROCESS_GROUP),
139+
nil, nil, &siInfo, &piInfo) {
140+
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
141+
}
138142
}
139-
}
140143

141-
defer { CloseHandle(piInfo.hThread) }
142-
defer { CloseHandle(piInfo.hProcess) }
144+
defer { CloseHandle(piInfo.hThread) }
145+
defer { CloseHandle(piInfo.hProcess) }
143146

144-
if !AssignProcessToJobObject(hJob, piInfo.hProcess) {
145-
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
146-
}
147+
if !AssignProcessToJobObject(hJob, piInfo.hProcess) {
148+
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
149+
}
147150

148-
_ = ResumeThread(piInfo.hThread)
151+
_ = ResumeThread(piInfo.hThread)
149152

150-
var dwCompletionCode: DWORD = 0
151-
var ulCompletionKey: ULONG_PTR = 0
152-
var lpOverlapped: LPOVERLAPPED?
153-
repeat {
154-
} while GetQueuedCompletionStatus(hPort, &dwCompletionCode, &ulCompletionKey,
155-
&lpOverlapped, INFINITE) &&
156-
!(ulCompletionKey == ULONG_PTR(UInt(bitPattern: hJob)) &&
157-
dwCompletionCode == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO)
153+
var dwCompletionCode: DWORD = 0
154+
var ulCompletionKey: ULONG_PTR = 0
155+
var lpOverlapped: LPOVERLAPPED?
156+
repeat {
157+
} while GetQueuedCompletionStatus(hPort, &dwCompletionCode, &ulCompletionKey,
158+
&lpOverlapped, INFINITE) &&
159+
!(ulCompletionKey == ULONG_PTR(UInt(bitPattern: hJob)) &&
160+
dwCompletionCode == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO)
158161

159-
var dwExitCode: DWORD = DWORD(bitPattern: -1)
160-
_ = GetExitCodeProcess(piInfo.hProcess, &dwExitCode)
162+
_ = GetExitCodeProcess(piInfo.hProcess, &dwExitCode)
163+
}
161164
_exit(Int32(bitPattern: dwExitCode))
162165
#elseif (!canImport(Darwin) || os(macOS))
163166
guard execv(path, cArgs.cArray) != -1 else {

0 commit comments

Comments
 (0)