Skip to content

Commit 7f1597d

Browse files
committed
Store the Windows process HANDLE in Execution to avoid pid reuse races
Unlike Unix, Windows doesn't have a "zombie" state where the numeric pid remains valid after the subprocess has exited. Instead, the numeric process ID becomes invalid immediately once the process has exited. Per the Windows documentation of GetProcessId and related APIs, "Until a process terminates, its process identifier uniquely identifies it on the system." On Windows, instead of closing/discarding the process HANDLE immediately after CreateProcess returns, retain the HANDLE in Execution and only close it once monitorProcessTermination returns. This resolve a race condition where the process could have exited between the period where CloseProcess was called and monitorProcessTermination is called. Now we simply use the original process handle to wait for the process to exit and retrieve its exit code, rather than "reverse engineering" its HANDLE from its numeric pid using OpenProcess. Closes #92
1 parent b66271c commit 7f1597d

File tree

5 files changed

+16
-48
lines changed

5 files changed

+16
-48
lines changed

Sources/Subprocess/Configuration.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public struct Configuration: Sendable {
103103
// even if `body` throws, and we are not leaving zombie processes in the
104104
// process table which will cause the process termination monitoring thread
105105
// to effectively hang due to the pid never being awaited
106-
let terminationStatus = try await Subprocess.monitorProcessTermination(forProcessWithIdentifier: processIdentifier)
106+
let terminationStatus = try await Subprocess.monitorProcessTermination(forExecution: _spawnResult.execution)
107107

108108
return try ExecutionResult(terminationStatus: terminationStatus, value: result.get())
109109
}

Sources/Subprocess/Execution.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,16 @@ public struct Execution: Sendable {
3535
public let processIdentifier: ProcessIdentifier
3636

3737
#if os(Windows)
38+
internal nonisolated(unsafe) let processHandle: HANDLE
3839
internal let consoleBehavior: PlatformOptions.ConsoleBehavior
3940

4041
init(
4142
processIdentifier: ProcessIdentifier,
43+
processHandle: HANDLE,
4244
consoleBehavior: PlatformOptions.ConsoleBehavior
4345
) {
4446
self.processIdentifier = processIdentifier
47+
self.processHandle = processHandle
4548
self.consoleBehavior = consoleBehavior
4649
}
4750
#else

Sources/Subprocess/Platforms/Subprocess+Darwin.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,18 +485,18 @@ extension String {
485485
// MARK: - Process Monitoring
486486
@Sendable
487487
internal func monitorProcessTermination(
488-
forProcessWithIdentifier pid: ProcessIdentifier
488+
forExecution execution: Execution
489489
) async throws -> TerminationStatus {
490490
return try await withCheckedThrowingContinuation { continuation in
491491
let source = DispatchSource.makeProcessSource(
492-
identifier: pid.value,
492+
identifier: execution.processIdentifier.value,
493493
eventMask: [.exit],
494494
queue: .global()
495495
)
496496
source.setEventHandler {
497497
source.cancel()
498498
var siginfo = siginfo_t()
499-
let rc = waitid(P_PID, id_t(pid.value), &siginfo, WEXITED)
499+
let rc = waitid(P_PID, id_t(execution.processIdentifier.value), &siginfo, WEXITED)
500500
guard rc == 0 else {
501501
continuation.resume(
502502
throwing: SubprocessError(

Sources/Subprocess/Platforms/Subprocess+Linux.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ extension String {
265265
// MARK: - Process Monitoring
266266
@Sendable
267267
internal func monitorProcessTermination(
268-
forProcessWithIdentifier pid: ProcessIdentifier
268+
forExecution execution: Execution
269269
) async throws -> TerminationStatus {
270270
try await withCheckedThrowingContinuation { continuation in
271271
_childProcessContinuations.withLock { continuations in
@@ -274,7 +274,7 @@ internal func monitorProcessTermination(
274274
// the child process has terminated and manages to acquire the lock before
275275
// we add this continuation to the dictionary, then it will simply loop
276276
// and report the status again.
277-
let oldContinuation = continuations.updateValue(continuation, forKey: pid.value)
277+
let oldContinuation = continuations.updateValue(continuation, forKey: execution.processIdentifier.value)
278278
precondition(oldContinuation == nil)
279279

280280
// Wake up the waiter thread if it is waiting for more child processes.

Sources/Subprocess/Platforms/Subprocess+Windows.swift

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -132,21 +132,6 @@ extension Configuration {
132132
underlyingError: .init(rawValue: windowsError)
133133
)
134134
}
135-
guard CloseHandle(processInfo.hProcess) else {
136-
let windowsError = GetLastError()
137-
try self.safelyCloseMultiple(
138-
inputRead: inputReadFileDescriptor,
139-
inputWrite: inputWriteFileDescriptor,
140-
outputRead: outputReadFileDescriptor,
141-
outputWrite: outputWriteFileDescriptor,
142-
errorRead: errorReadFileDescriptor,
143-
errorWrite: errorWriteFileDescriptor
144-
)
145-
throw SubprocessError(
146-
code: .init(.spawnFailed),
147-
underlyingError: .init(rawValue: windowsError)
148-
)
149-
}
150135

151136
try self.safelyCloseMultiple(
152137
inputRead: inputReadFileDescriptor,
@@ -162,6 +147,7 @@ extension Configuration {
162147
)
163148
let execution = Execution(
164149
processIdentifier: pid,
150+
processHandle: processInfo.hProcess,
165151
consoleBehavior: self.platformOptions.consoleBehavior
166152
)
167153
return SpawnResult(
@@ -275,21 +261,6 @@ extension Configuration {
275261
underlyingError: .init(rawValue: windowsError)
276262
)
277263
}
278-
guard CloseHandle(processInfo.hProcess) else {
279-
let windowsError = GetLastError()
280-
try self.safelyCloseMultiple(
281-
inputRead: inputReadFileDescriptor,
282-
inputWrite: inputWriteFileDescriptor,
283-
outputRead: outputReadFileDescriptor,
284-
outputWrite: outputWriteFileDescriptor,
285-
errorRead: errorReadFileDescriptor,
286-
errorWrite: errorWriteFileDescriptor
287-
)
288-
throw SubprocessError(
289-
code: .init(.spawnFailed),
290-
underlyingError: .init(rawValue: windowsError)
291-
)
292-
}
293264

294265
// After spawn finishes, close all child side fds
295266
try self.safelyCloseMultiple(
@@ -306,6 +277,7 @@ extension Configuration {
306277
)
307278
let execution = Execution(
308279
processIdentifier: pid,
280+
processHandle: processInfo.hProcess,
309281
consoleBehavior: self.platformOptions.consoleBehavior
310282
)
311283
return SpawnResult(
@@ -464,7 +436,7 @@ extension PlatformOptions: CustomStringConvertible, CustomDebugStringConvertible
464436
// MARK: - Process Monitoring
465437
@Sendable
466438
internal func monitorProcessTermination(
467-
forProcessWithIdentifier pid: ProcessIdentifier
439+
forExecution execution: Execution
468440
) async throws -> TerminationStatus {
469441
// Once the continuation resumes, it will need to unregister the wait, so
470442
// yield the wait handle back to the calling scope.
@@ -473,15 +445,8 @@ internal func monitorProcessTermination(
473445
if let waitHandle {
474446
_ = UnregisterWait(waitHandle)
475447
}
476-
}
477-
guard
478-
let processHandle = OpenProcess(
479-
DWORD(PROCESS_QUERY_INFORMATION | SYNCHRONIZE),
480-
false,
481-
pid.value
482-
)
483-
else {
484-
return .exited(1)
448+
449+
_ = CloseHandle(execution.processHandle)
485450
}
486451

487452
try? await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
@@ -500,7 +465,7 @@ internal func monitorProcessTermination(
500465
guard
501466
RegisterWaitForSingleObject(
502467
&waitHandle,
503-
processHandle,
468+
execution.processHandle,
504469
callback,
505470
context,
506471
INFINITE,
@@ -518,7 +483,7 @@ internal func monitorProcessTermination(
518483
}
519484

520485
var status: DWORD = 0
521-
guard GetExitCodeProcess(processHandle, &status) else {
486+
guard GetExitCodeProcess(execution.processHandle, &status) else {
522487
// The child process terminated but we couldn't get its status back.
523488
// Assume generic failure.
524489
return .exited(1)

0 commit comments

Comments
 (0)