openssl: check SSL_write() length on retries

When an SSL_write() blocks we need to retry it with the
same length as before or stupid OpenSSL freaks out. Remember
it, limit any longer sends and fail shorter ones.

Fixes #18121
Reported-by: adamse on github
Closes #18132
This commit is contained in:
Stefan Eissing 2025-08-01 14:55:52 +02:00 committed by Daniel Stenberg
parent 8df0a5ba32
commit ec4c559104
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
4 changed files with 33 additions and 6 deletions

View file

@ -380,8 +380,14 @@ CURLcode Curl_req_send(struct Curl_easy *data, struct dynbuf *req,
data->req.httpversion_sent = httpversion;
buf = curlx_dyn_ptr(req);
blen = curlx_dyn_len(req);
if(!Curl_creader_total_length(data)) {
/* Request without body. Try to send directly from the buf given. */
/* if the sendbuf is empty and the request without body and
* the length to send fits info a sendbuf chunk, we send it directly.
* If `blen` is larger then `chunk_size`, we can not. Because we
* might have to retry a blocked send later from sendbuf and that
* would result in retry sends with a shrunken length. That is trouble. */
if(Curl_bufq_is_empty(&data->req.sendbuf) &&
!Curl_creader_total_length(data) &&
(blen <= data->req.sendbuf.chunk_size)) {
data->req.eos_read = TRUE;
result = xfer_send(data, buf, blen, blen, &nwritten);
if(result)

View file

@ -5316,6 +5316,17 @@ static CURLcode ossl_send(struct Curl_cfilter *cf,
connssl->io_need = CURL_SSL_IO_NEED_NONE;
memlen = (len > (size_t)INT_MAX) ? INT_MAX : (int)len;
if(octx->blocked_ssl_write_len && (octx->blocked_ssl_write_len != memlen)) {
/* The previous SSL_write() call was blocked, using that length.
* We need to use that again or OpenSSL will freak out. A shorter
* length should not happen and is a bug in libcurl. */
if(octx->blocked_ssl_write_len > memlen) {
DEBUGASSERT(0);
return CURLE_BAD_FUNCTION_ARGUMENT;
}
memlen = octx->blocked_ssl_write_len;
}
octx->blocked_ssl_write_len = 0;
nwritten = SSL_write(octx->ssl, mem, memlen);
if(nwritten > 0)
@ -5326,16 +5337,19 @@ static CURLcode ossl_send(struct Curl_cfilter *cf,
switch(err) {
case SSL_ERROR_WANT_READ:
connssl->io_need = CURL_SSL_IO_NEED_RECV;
octx->blocked_ssl_write_len = memlen;
result = CURLE_AGAIN;
goto out;
case SSL_ERROR_WANT_WRITE:
result = CURLE_AGAIN;
octx->blocked_ssl_write_len = memlen;
goto out;
case SSL_ERROR_SYSCALL:
{
int sockerr = SOCKERRNO;
if(octx->io_result == CURLE_AGAIN) {
octx->blocked_ssl_write_len = memlen;
result = CURLE_AGAIN;
goto out;
}

View file

@ -68,6 +68,8 @@ struct ossl_ctx {
X509* server_cert;
BIO_METHOD *bio_method;
CURLcode io_result; /* result of last BIO cfilter operation */
/* blocked writes need to retry with same length, remember it */
int blocked_ssl_write_len;
#ifndef HAVE_KEYLOG_CALLBACK
/* Set to true once a valid keylog entry has been created to avoid dupes.
This is a bool and not a bitfield because it is passed by address. */

View file

@ -754,7 +754,7 @@ class TestDownload:
# download with looong urls
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
@pytest.mark.parametrize("url_junk", [1024, 16*1024, 32*1024, 64*1024])
@pytest.mark.parametrize("url_junk", [1024, 16*1024, 32*1024, 64*1024, 80*1024, 96*1024])
def test_02_36_looong_urls(self, env: Env, httpd, nghttpx, proto, url_junk):
if proto == 'h3' and not env.have_h3():
pytest.skip("h3 not supported")
@ -784,6 +784,11 @@ class TestDownload:
# h2 is unable to send such large headers (frame limits)
r.check_exit_code(55)
elif proto == 'h3':
r.check_exit_code(0)
# nghttpx reports 431 Request Header Field too Large
r.check_response(http_status=431)
if url_junk <= 64*1024:
r.check_exit_code(0)
# nghttpx reports 431 Request Header Field too Large
r.check_response(http_status=431)
else:
# nghttpx destroys the connection with internal error
# ERR_QPACK_HEADER_TOO_LARGE
r.check_exit_code(56)