Skip to content

Fix callback exception handling to be per-Connection instead of per-Context - #1535

Merged
alex merged 3 commits into
mainfrom
claude/pyopenssl-context-exception-race-b3vsr5
Sep 6, 2026
Merged

alex merged 3 commits into
mainfrom
claude/pyopenssl-context-exception-race-b3vsr5

Conversation

@reaperhulk

Copy link
Copy Markdown
Member

Summary

This PR fixes a critical race condition in callback exception handling where exceptions raised by verify, ALPN selection, OCSP, or DTLS cookie callbacks for one Connection could be incorrectly raised on an unrelated Connection created from the same Context when used concurrently from multiple threads.

Key Changes

  • Removed _CallbackExceptionHelper base class: This class stored exceptions in a shared list at the callback wrapper level, which was context-wide and shared across all connections using that context.

  • Moved exception storage to Connection instances: Each Connection now has its own _callback_problems list to track exceptions that occurred during callbacks specific to that connection.

  • Updated all callback helpers: Modified _VerifyHelper, _ALPNSelectHelper, _OCSPServerCallbackHelper, _OCSPClientCallbackHelper, _CookieGenerateCallbackHelper, and _CookieVerifyCallbackHelper to:

    • Remove inheritance from _CallbackExceptionHelper
    • Store exceptions directly on the connection object via connection._callback_problems.append(e)
    • Move connection lookup outside the try block where possible for clarity
  • Centralized exception raising: Added _raise_callback_problem() method to Connection that:

    • Checks for and raises any stored callback exceptions
    • Clears the OpenSSL error queue before raising to avoid confusion
    • Replaces multiple individual helper checks in _raise_ssl_error() and DTLSv1_listen()
  • Added error handling to ALPN callback: The ALPN callback wrapper now specifies error=_lib.SSL_TLSEXT_ERR_ALERT_FATAL to cffi to ensure proper error handling if an exception somehow escapes the wrapper.

  • Added test coverage: New tests verify that:

    • ALPN callback exceptions stay with their connection even when multiple connections share a context
    • Verify callback exceptions are properly propagated to the caller

Implementation Details

The fix ensures thread-safety by storing callback exceptions on the connection object rather than on the shared callback wrapper. This way, when multiple threads are driving different connections from the same context concurrently, each connection only sees its own exceptions. The connection lookup from the SSL object happens before the try block to ensure we always have the correct connection reference for storing exceptions.

https://claude.ai/code/session_01A46SM1jtRoG4Yh5P2k9cUm

Exceptions raised inside verify, ALPN selection, OCSP and DTLS cookie
callbacks cannot be raised from inside the callback, so they were
deferred on a _CallbackExceptionHelper and re-raised by
Connection._raise_ssl_error on the next send/recv/do_handshake. For
callbacks registered on a Context, that helper (and its FIFO of pending
exceptions) was shared by every Connection created from the Context.
Because cffi releases the GIL around the SSL_* calls, a server that
drives connections from several threads could have one connection pop
and raise an exception that another connection's callback produced,
aborting an otherwise healthy connection with a misattributed error,
while the connection that actually failed reported a generic SSL error
(or an IndexError from the unsynchronised check-then-pop).

Record the pending exceptions on the Connection whose callback raised
them instead, and have _raise_ssl_error and DTLSv1_listen only raise
that connection's own. With no shared state left, the
_CallbackExceptionHelper base class has nothing to do and is removed.

This also fixes exceptions raised from a verify callback installed via
Connection.set_verify being silently dropped: _raise_ssl_error only
consulted the Context's verify helper, but the callback OpenSSL actually
invokes for a connection is the one copied at SSL_new time or the one
installed by Connection.set_verify.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A46SM1jtRoG4Yh5P2k9cUm
…ption

_raise_callback_problem drained the thread's OpenSSL error queue by
calling _raise_current_error() and swallowing the resulting Error. That
relies on _raise_current_error() raising even when the queue is empty,
and obscures the intent. _raise_ssl_error already uses ERR_clear_error()
for the same purpose a few lines further down; do the same here.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A46SM1jtRoG4Yh5P2k9cUm
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A46SM1jtRoG4Yh5P2k9cUm
@alex
alex merged commit 07c8f27 into main Sep 6, 2026
75 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants