Skip to content

Concurrent async TLS setup can abort the process with truststore 0.10.4; workaround #20

Description

@parttimediarybehavior

TypeSafe SDK 0.7.2's default async HTTP stack can abort the interpreter during concurrent TLS connection setup on Linux when truststore 0.10.4 is installed. This is an upstream dependency race, but documenting the workaround here would help SDK users.

The SDK creates an httpx2.AsyncClient, whose default SSL context is truststore.SSLContext in the tested environment. AnyIO offloads its wrap_bio() calls to worker threads. truststore 0.10.4 reconfigures the shared OpenSSL context without the lock used by its socket path, allowing concurrent context mutations.

Upstream tracking: sethmlarson/truststore#221
Fix: sethmlarson/truststore#219 — merged September 29, 2026, but the latest published release checked today is still 0.10.4.

Independent reproduction (October 2)

No TypeSafe API calls, credentials, external connections, pandas, or pyarrow were needed:

  • Direct shared-context stress test: 16 threads × 300 wrap_bio() calls, 2 of 5 completed trials aborted with SIGABRT (double free or corruption (fasttop)).
  • Actual anyio.streams.tls.TLSStream.wrap(), using the context from a default AsyncTypeSafeClient and an in-memory failing peer: 16 async workers × 30 attempts, 2/3 trials aborted.
  • Same AnyIO probe with http_client=httpx2.AsyncClient(verify=ssl.create_default_context()): 0/3 native crashes, all 480 attempts per trial reached the expected simulated-peer failure.
  • Same AnyIO probe with truststore's wrap_bio() patched to use the exact lock scope in merged PR #219: 0/3 native crashes.

The async crash stack includes truststore/_openssl.py:38 _configure_context ← truststore/_api.py:144 wrap_bio ← AnyIO's asyncio worker thread. The failing-peer probe exercises context wrapping, not a successful TLS handshake or a live SDK request. Counts demonstrate reproducibility, not an estimated production crash rate.

Minimal network-free reproducer

Run this in a disposable Python process; it may terminate natively.

import faulthandler
import ssl
import threading
import truststore

faulthandler.enable()
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

def worker():
    for _ in range(300):
        ctx.wrap_bio(
            ssl.MemoryBIO(), ssl.MemoryBIO(),
            server_hostname="api.example.com",
        )

threads = [threading.Thread(target=worker) for _ in range(16)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()
print("completed")

Workaround

import ssl
import httpx2
from typesafe_sdk import AsyncTypeSafeClient

async with httpx2.AsyncClient(verify=ssl.create_default_context()) as http:
    async with AsyncTypeSafeClient(http_client=http) as client:
        ...  # normal SDK calls

This retains certificate verification and hostname checking while bypassing truststore. It uses OpenSSL's configured CA paths instead of truststore's native platform integration. Configured SSL_CERT_FILE/SSL_CERT_DIR or another custom HTTP client can also change the default context, so this does not affect every async configuration.

Suggested action: document the workaround alongside async/concurrent usage (and #10's HTTP/2 example), then recommend the fixed truststore release once published. Reusing a client reduces new handshakes but does not guarantee avoidance.

Test environment: Ubuntu 24.04, Python 3.12.3, OpenSSL 3.0.13, typesafe-sdk 0.7.2, httpx2/httpcore2 2.13.1, AnyIO 4.15.1, truststore 0.10.4.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions