HexBrain

curl · Report #4118643

OpenSSL provider OSSL_LIB_CTX used after free via the connection shutdown list (CVE-2026-80229 fix incomplete)

Duplicatestatus
lowseverity
3upvotes
26 Oct 2026disclosed

How it was found

Summary:

The CVE-2026-80229 fix (7ea37abc6a, 8.22.0) only marks provider connections connclose(), but a closed TLS connection is handed to the multi handle's shutdown list (lib/conncache.c:242) and outlives its easy handle, while curl_easy_cleanup() frees the handle's OSSL_LIB_CTX (ossl_provider_cleanup(), lib/vtls/openssl.c:1747, via Curl_close() at lib/url.c:244) that the connection's SSL_CTX was created from (SSL_CTX_new_ex(), openssl.c:3748). When the shutdown list later reads from or destroys that connection, OpenSSL uses the freed context: SSL_read() handling a NewSessionTicket or KeyUpdate fetches algorithms through it, and SSL_CTX_free() in Curl_cshutdn_terminate() runs the provider teardown, which takes a lock stored inside it (a write lock with OpenSSL 3.6, a read lock with 3.0). With OpenSSL's shipped legacy provider, curl -Z --engine legacy URL1 URL2 segfaults on every run in plain release builds of 8.23.0 on macOS and on Debian 12 (stock OpenSSL 3.0.22) and in Homebrew's 8.22.0 release, against a server that does nothing unusual, while the same command without --engine, or with a single URL, exits 0. Under AddressSanitizer the freed object is the OSSL_LIB_CTX allocated by ossl_set_provider() (openssl.c:1786) and freed by curl_easy_cleanup(), and both later accesses fault on the free-fill pattern. This is not the CVE-2026-80229 scenario, which needed a second easy handle to reuse the pooled connection: no reuse happens here, and the freed context is reached through the shutdown list that the 80229 fix now sends every provider connection to, in 8.22.0 and later, which that advisory lists as not affected.

Affected version

8.22.0 through 8.23.0 and master (780405b584). Tested on release builds of the curl-8_23_0 tag on macOS 27.0.1 (OpenSSL 3.6.4) and Debian 12 (OpenSSL 3.0.22, in the official debian:bookworm image), an AddressSanitizer build of the same tag, and the Homebrew 8.22.0 release:

curl 8.23.0-DEV (Darwin) libcurl/8.23.0-DEV OpenSSL/3.6.4 zlib/1.2.12
curl 8.23.0-DEV (Linux) libcurl/8.23.0-DEV OpenSSL/3.0.22
curl 8.22.0 (aarch64-apple-darwin25.6.0) libcurl/8.22.0 OpenSSL/3.6.4 zlib/1.2.12 brotli/1.2.0 zstd/1.5.7 AppleIDN libpsl/0.23.3 libssh2/1.11.1 nghttp2/1.70.0 ngtcp2/1.25.0 nghttp3/1.18.0 mit-krb5/1.7-prerelease OpenLDAP/2.4.28/Apple

Steps To Reproduce:

All on 127.0.0.1, sources in the attached zip under poc/.

  1. openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 7 -subj /CN=localhost -addext subjectAltName=DNS:localhost -keyout server.key -out server.crt
  2. cc poc/tls_ku_server.c -lssl -lcrypto -o tls_ku_server (a minimal OpenSSL TLS 1.3 HTTPS server that answers one request per connection)
  3. ./tls_ku_server 22002 none 300 0 &
  4. curl -Z --engine legacy --cacert server.crt https://localhost:22002/a https://localhost:22002/slow ends with Segmentation fault, exit 139.
  5. Controls: the same command without --engine legacy, or with only the first URL, exits 0.

Release-build backtrace, macOS (lldb); the Debian gdb trace in 04-linux-debian-bookworm.txt has the same curl frames:

EXC_BAD_ACCESS  pthread_rwlock_wrlock
  CRYPTO_THREAD_write_lock
  ossl_provider_deregister_child_cb
  OSSL_LIB_CTX_free
  legacy_teardown            (legacy.dylib)
  ossl_provider_free
  evp_cipher_free_int
  SSL_CTX_free
  ossl_close                 (curl)
  ssl_cf_destroy
  Curl_conn_cf_discard_all
  Curl_cshutdn_terminate
  cshutdn_perform
  curl_multi_perform

With ./tls_ku_server 22002 nst 300 0 (one NewSessionTicket 300 ms after the response) the fault moves to SSL_read() -> tls_process_new_session_ticket() -> evp_generic_fetch() in the shutdown read. The libcurl version (poc/poc_provider_shutdown.c, PROV=legacy ./poc multi2 https://localhost:22002/) does the usual remove and cleanup of the easy handle on CURLMSG_DONE and keeps driving the multi handle; calling curl_multi_cleanup() right after that crashes the same way. Scope: the multi interface, which includes curl -Z; the plain easy interface (curl_easy_perform() then curl_easy_cleanup()) does not reach it. Raw output: 01-release-curl-cli-segfault.txt, 02-release-backtrace.txt, 03-asan-freed-by-and-use.txt, 04-linux-debian-bookworm.txt (with gdb backtrace).

Fix

Make the OSSL_LIB_CTX outlive every SSL_CTX and SSL created from it, for example by reference counting it and letting each connection hold a reference until its TLS filter is destroyed, or by tying it to the multi handle instead of the easy handle.

AI disclosure: I used an AI assistant (Claude) for code review, test setup and drafting. I reproduced every result myself on curl built from the curl-8_23_0 tag (macOS, and Debian 12 in Docker) and on the Homebrew 8.22.0 release; the attached logs are the raw output.

Impact

A libcurl application that uses an OpenSSL 3 provider through CURLOPT_SSLENGINE with the multi interface, and the curl tool with --engine and -Z, uses freed memory every time a provider-backed connection is closed after its easy handle was cleaned up, which the CVE-2026-80229 fix makes every such connection. The freed context is reused by the allocator, and libcrypto reads it and takes a lock through a pointer stored in it (a write lock with OpenSSL 3.6) on the way to updating the provider bookkeeping it holds, so this is a use-after-free in live code paths rather than a plain crash. A TLS server can additionally drive the read path at a moment it chooses by sending a NewSessionTicket or KeyUpdate while curl waits for close_notify. Providers are how OpenSSL 3 exposes hardware keys (pkcs11, tpm2) and legacy algorithms, which is the documented purpose of CURLOPT_SSLENGINE on OpenSSL 3.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original