Skip to content

asyncio: avoid sharing exception object between StreamReader and close waiter (gh-156278) - #156286

Draft
aryansk wants to merge 2 commits into
python:mainfrom
aryansk:fix-asyncio-shared-exc-traceback-156278
Draft

asyncio: avoid sharing exception object between StreamReader and close waiter (gh-156278)#156286
aryansk wants to merge 2 commits into
python:mainfrom
aryansk:fix-asyncio-shared-exc-traceback-156278

Conversation

@aryansk

@aryansk aryansk commented Aug 23, 2026

Copy link
Copy Markdown

Fixes #156278

Problem

StreamReaderProtocol.connection_lost() set the same exception object on both the StreamReader waiter and the stream's _closed waiter:

reader.set_exception(exc)
self._closed.set_exception(exc)   # same object

Since gh-90082, Future snapshots the traceback at set_exception() time and restores it with with_traceback() on every result() call, which mutates the exception in place. Sharing one object between two futures caused await writer.wait_closed() (commonly in an except block) to rewrite the traceback of the in-flight exception being handled, erasing the real failure site (readexactly/_wait_for_data) and replacing it with wait_closed frames. The reproducer in the issue shows BEFORE wait_closed: main:38 <- readexactly becoming AFTER wait_closed: main:43 <- wait_closed.

Change

Copy the exception for the _closed waiter so each future owns an independent object:

exc_copy = copy.copy(exc)  # fallback to type(exc)(*exc.args) if copy fails
self._closed.set_exception(exc_copy)

The reader keeps the original, the close waiter gets the copy. with_traceback() then mutates independent objects.

Validation

  • python -m py_compile Lib/asyncio/streams.py passes
  • Manual check that copy.copy(exc) is not exc and that Future.result() on one copy does not rewrite the other's __traceback__
  • Issue reproducer (socket RST + readexactly/wait_closed interleaving) now preserves the readexactly traceback after wait_closed is suppressed
  • Lib/test/test_asyncio streams tests expected to be unaffected (change is isolated to connection_lost sharing)

Notes: the interaction is between (a) sharing and (b) the with_traceback restore; fixing the sharing side is the narrowest change. A general Future fix to avoid in-place mutation would be broader and is left as a follow-up if maintainers prefer it.

…e waiter

StreamReaderProtocol.connection_lost() set the same exception object
on both the StreamReader's waiter and the Stream's _closed waiter.
Since pythongh-90082, Future stores the traceback at set_exception() time
and restores it with with_traceback() on every result() call, which
mutates the exception in place. Sharing one object between two
futures caused the second await (typically writer.wait_closed() in an
except block) to rewrite the traceback of the in-flight exception
being handled, erasing the real failure site (readexactly) and
replacing it with wait_closed frames.

Fix by copying the exception for the _closed waiter so each future
owns an independent object. Copy falls back to reconstructing via
type(exc)(*exc.args) when copy.copy fails.

Fixes python#156278
@python-cla-bot

Copy link
Copy Markdown

The following commit authors need to sign the Contributor License Agreement:

CLA not signed

@bedevere-app

bedevere-app Bot commented Aug 23, 2026

Copy link
Copy Markdown

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant