SSL._create calls @SSL_do_handshake(_ssl) for a client and discards the return. SSL_ERROR_WANT_READ is not the only possible outcome.
Two configurations reach a different one, measured against OpenSSL 3.6.2 with memory BIOs and no input, the same shape _create builds:
| context |
SSL_get_error |
queue |
output BIO |
| library default |
2 WANT_READ |
— |
1533 bytes, a ClientHello |
| min TLS 1.3, max TLS 1.2 |
1 SSL_ERROR_SSL |
lib=20 reason=191 no protocols available |
7 bytes, 15 03 03 00 02 02 46 |
max TLS 1.2, ciphers aNULL |
1 SSL_ERROR_SSL |
lib=20 reason=181 no ciphers available |
7 bytes, 15 03 01 00 02 02 50 |
Both of those 7-byte payloads are fatal alerts — protocol_version and internal_error — not handshake flights.
At the Pony level, with public API only: SSLContext.client() returns a session that reports state() as SSLHandshake, reports can_send() == true, and whose send() yields that alert. Nothing has classified the failure. The first read sets SSLError.
Through SSLConnection, _poll falls past the state match and calls read, and read is what finally classifies it, so on that pass _poll does not close the connection — it writes the alert bytes and returns. An application driving SSL directly can transmit the alert and then wait for a reply no peer has reason to send.
The classification is correct whenever it happens. The defect is the window before it: the session reports a state it is not in, and hands out alert bytes as if they were a handshake flight.
Adjacent but separate from #120, which covers SSL_get_error results that read and receive do not handle. That one does not reach _create, which never calls SSL_get_error at all.
SSL._createcalls@SSL_do_handshake(_ssl)for a client and discards the return.SSL_ERROR_WANT_READis not the only possible outcome.Two configurations reach a different one, measured against OpenSSL 3.6.2 with memory BIOs and no input, the same shape
_createbuilds:SSL_get_errorWANT_READSSL_ERROR_SSLlib=20 reason=191 no protocols available15 03 03 00 02 02 46aNULLSSL_ERROR_SSLlib=20 reason=181 no ciphers available15 03 01 00 02 02 50Both of those 7-byte payloads are fatal alerts —
protocol_versionandinternal_error— not handshake flights.At the Pony level, with public API only:
SSLContext.client()returns a session that reportsstate()asSSLHandshake, reportscan_send() == true, and whosesend()yields that alert. Nothing has classified the failure. The firstreadsetsSSLError.Through
SSLConnection,_pollfalls past the state match and callsread, andreadis what finally classifies it, so on that pass_polldoes not close the connection — it writes the alert bytes and returns. An application drivingSSLdirectly can transmit the alert and then wait for a reply no peer has reason to send.The classification is correct whenever it happens. The defect is the window before it: the session reports a state it is not in, and hands out alert bytes as if they were a handshake flight.
Adjacent but separate from #120, which covers
SSL_get_errorresults thatreadandreceivedo not handle. That one does not reach_create, which never callsSSL_get_errorat all.