asyncio: InvalidStateError in BaseSubprocessTransport._call_connection_lost() due to double set_result on exit waiters · Issue #145541 · python/cpython · GitHub
When SIGINT arrives while subprocess _connect_pipes tasks are in flight, asyncio.run() cancels the main task which cascades into cancelling _connect_pipes. This leaves _pipes_connected=False.
The fix in GH-140805 (backported as GH-141446) added code to _try_finish() that wakes up exit waiters when _pipes_connected is False to prevent hangs. However, _try_finish() then also checks whether all pipes are disconnected — and when _pipes is empty or all pipes are disconnected, it schedules _call_connection_lost(). _call_connection_lost() iterates the same _exit_waiters list and calls set_result() again, raising InvalidStateError.
The flow:
_connect_pipes is cancelled → _pipes_connected stays False
Process exits → _process_exited() → _try_finish()
_try_finish() sees _pipes_connected is False → calls waiter.set_result(self._returncode) for all non-cancelled waiters
_try_finish() checks all(p is not None and p.disconnected for p in self._pipes.values()) — this is True (vacuously when _pipes is empty, or because pipes were disconnected)
Schedules _call_connection_lost()
_call_connection_lost() iterates _exit_waiters → waiter.set_result(self._returncode) → InvalidStateError because the waiter already has a result from step 3
Suggested fix
Change if not waiter.cancelled() to if not waiter.done() in _call_connection_lost():
def_call_connection_lost(self, exc):
try:
self._protocol.connection_lost(exc)
finally:
forwaiterinself._exit_waiters:
ifnotwaiter.done(): # was: if not waiter.cancelled()waiter.set_result(self._returncode)
self._exit_waiters=None
...
Exception in callback BaseSubprocessTransport._call_connection_lost()
handle: <Handle BaseSubprocessTransport._call_connection_lost()>
Traceback (most recent call last):
File "/.../asyncio/events.py", line 94, in _run
self._context.run(self._callback, *self._args)
File "/.../asyncio/base_subprocess.py", line 282, in _call_connection_lost
waiter.set_result(self._returncode)
asyncio.exceptions.InvalidStateError: invalid state
There are actually two variations of this bug. Both _try_finish() and _call_connection_lost() need the fix.
Variation 1 (already described above): _try_finish() wakes waiters, then _call_connection_lost() wakes them again.
Variation 2: _try_finish() wakes waiters (because _pipes_connected=False), then later a pipe transport's _call_connection_lost() fires (e.g. _UnixWritePipeTransport._call_connection_lost()), which calls _pipe_connection_lost() → _try_finish() a second time. Since _finished was not set on the first call (not all pipes were disconnected yet), and _pipes_connected is still False, it tries set_result() on the same already-resolved waiters.
Exception in callback _UnixWritePipeTransport._call_connection_lost()
handle: <Handle _UnixWritePipeTransport._call_connection_lost()>
Traceback (most recent call last):
File "/usr/lib/python3.14/asyncio/events.py", line 94, in _run
self._context.run(self._callback, *self._args)
File "/usr/lib/python3.14/asyncio/unix_events.py", line 825, in _call_connection_lost
self._protocol.connection_lost(exc)
File "/usr/lib/python3.14/asyncio/base_subprocess.py", line 305, in connection_lost
self.proc._pipe_connection_lost(self.fd, exc)
File "/usr/lib/python3.14/asyncio/base_subprocess.py", line 227, in _pipe_connection_lost
self._try_finish()
File "/usr/lib/python3.14/asyncio/base_subprocess.py", line 269, in _try_finish
waiter.set_result(self._returncode)
asyncio.exceptions.InvalidStateError: invalid state
So the fix needs if not waiter.done() in both places:
# In _try_finish():ifnotself._pipes_connected:
forwaiterinself._exit_waiters:
ifnotwaiter.done(): # was: if not waiter.cancelled()waiter.set_result(self._returncode)
# In _call_connection_lost():forwaiterinself._exit_waiters:
ifnotwaiter.done(): # was: if not waiter.cancelled()waiter.set_result(self._returncode)
Activity
daandemeyer commented on Mar 5, 2026
cc @kumaraditya303
daandemeyer commented on Mar 5, 2026
There are actually two variations of this bug. Both _try_finish() and _call_connection_lost() need the fix.
Variation 1 (already described above): _try_finish() wakes waiters, then _call_connection_lost() wakes them again.
Variation 2: _try_finish() wakes waiters (because _pipes_connected=False), then later a pipe transport's _call_connection_lost() fires (e.g. _UnixWritePipeTransport._call_connection_lost()), which calls _pipe_connection_lost() → _try_finish() a second time. Since _finished was not set on the first call (not all pipes were disconnected yet), and _pipes_connected is still False, it tries set_result() on the same already-resolved waiters.
Exception in callback _UnixWritePipeTransport._call_connection_lost() handle: <Handle _UnixWritePipeTransport._call_connection_lost()> Traceback (most recent call last): File "/usr/lib/python3.14/asyncio/events.py", line 94, in _run self._context.run(self._callback, *self._args) File "/usr/lib/python3.14/asyncio/unix_events.py", line 825, in _call_connection_lost self._protocol.connection_lost(exc) File "/usr/lib/python3.14/asyncio/base_subprocess.py", line 305, in connection_lost self.proc._pipe_connection_lost(self.fd, exc) File "/usr/lib/python3.14/asyncio/base_subprocess.py", line 227, in _pipe_connection_lost self._try_finish() File "/usr/lib/python3.14/asyncio/base_subprocess.py", line 269, in _try_finish waiter.set_result(self._returncode) asyncio.exceptions.InvalidStateError: invalid stateSo the fix needs if not waiter.done() in both places:
kumaraditya303 commented on Mar 5, 2026
Do you want to submit a PR?
daandemeyer commented on Mar 5, 2026
I can submit a PR if you think the fix is the right way to go
pythongh-145541: Fix InvalidStateError in BaseSubprocessTransport._ca…
pythongh-145541: Fix InvalidStateError in BaseSubprocessTransport._ca…
pythongh-145541: Fix InvalidStateError in BaseSubprocessTransport._ca…
pythongh-145541: Fix InvalidStateError in BaseSubprocessTransport._ca…
pythongh-145541: Fix InvalidStateError in BaseSubprocessTransport._ca…
gh-145541: Fix `InvalidStateError` in `BaseSubprocessTransport._call_…
pythongh-145541: Fix `InvalidStateError` in `BaseSubprocessTransport.…
pythongh-145541: Fix `InvalidStateError` in `BaseSubprocessTransport.…
[3.13] gh-145541: Fix `InvalidStateError` in `BaseSubprocessTransport…
[3.14] gh-145541: Fix `InvalidStateError` in `BaseSubprocessTransport…
pythongh-145541: Fix `InvalidStateError` in `BaseSubprocessTransport.…