Skip to content

Commit 3f03706

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 3f03706

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+
do {
98+
var hJob: HANDLE
9799

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) }
100+
hJob = CreateJobObjectA(nil, nil)
101+
if hJob == HANDLE(bitPattern: 0) {
102+
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
103+
}
104+
defer { CloseHandle(hJob) }
103105

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-
}
106+
let hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nil, 0, 1)
107+
if hPort == HANDLE(bitPattern: 0) {
108+
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
109+
}
108110

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

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

125127

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

129-
var piInfo: PROCESS_INFORMATION = PROCESS_INFORMATION()
131+
var piInfo: PROCESS_INFORMATION = PROCESS_INFORMATION()
130132

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

141-
defer { CloseHandle(piInfo.hThread) }
142-
defer { CloseHandle(piInfo.hProcess) }
143+
defer { CloseHandle(piInfo.hThread) }
144+
defer { CloseHandle(piInfo.hProcess) }
143145

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

148-
_ = ResumeThread(piInfo.hThread)
150+
_ = ResumeThread(piInfo.hThread)
149151

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)
152+
var dwCompletionCode: DWORD = 0
153+
var ulCompletionKey: ULONG_PTR = 0
154+
var lpOverlapped: LPOVERLAPPED?
155+
repeat {
156+
} while GetQueuedCompletionStatus(hPort, &dwCompletionCode, &ulCompletionKey,
157+
&lpOverlapped, INFINITE) &&
158+
!(ulCompletionKey == ULONG_PTR(UInt(bitPattern: hJob)) &&
159+
dwCompletionCode == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO)
158160

159-
var dwExitCode: DWORD = DWORD(bitPattern: -1)
160-
_ = GetExitCodeProcess(piInfo.hProcess, &dwExitCode)
161+
var dwExitCode: DWORD = DWORD(bitPattern: -1)
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)