From 4a999def99902dd6d8911a4ae33ba6d17e7e8082 Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Wed, 20 May 2026 09:01:25 +0200 Subject: [PATCH 1/2] mbedtls: return error when retrying a blocked send with a shorter buffer mbedtls_ssl_write() requires the retry call after WANT_WRITE to use at least as many bytes as the original blocked call. The DEBUGASSERT enforcing this was compiled out in release builds, leaving the len inflation free to exceed the caller's buffer and produce n > buflen. Replace the assert with an explicit CURLE_SEND_ERROR so the constraint is enforced in release builds too. Document the retry buflen requirement in curl_easy_send(3). --- docs/libcurl/curl_easy_send.md | 4 +++- lib/vtls/mbedtls.c | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/libcurl/curl_easy_send.md b/docs/libcurl/curl_easy_send.md index 016a6756a8..012831b46b 100644 --- a/docs/libcurl/curl_easy_send.md +++ b/docs/libcurl/curl_easy_send.md @@ -92,7 +92,9 @@ send. On failure, returns the appropriate error code. This function may return **CURLE_AGAIN**. In this case, use your operating -system facilities to wait until the socket is writable, and retry. +system facilities to wait until the socket is writable, and retry. The +**buflen** passed to the retry should be no smaller than the one passed to +the call that returned **CURLE_AGAIN**. If there is no socket available to use from the previous transfer, this function returns **CURLE_UNSUPPORTED_PROTOCOL**. diff --git a/lib/vtls/mbedtls.c b/lib/vtls/mbedtls.c index f0d2f80230..1c7831467e 100644 --- a/lib/vtls/mbedtls.c +++ b/lib/vtls/mbedtls.c @@ -1280,7 +1280,8 @@ static CURLcode mbed_send(struct Curl_cfilter *cf, struct Curl_easy *data, * loses bytes, e.g. reporting all was sent but they were not. * Remember the blocked length and use that when set. */ if(backend->send_blocked) { - DEBUGASSERT(backend->send_blocked_len <= len); + if(len < backend->send_blocked_len) + return CURLE_SEND_ERROR; CURL_TRC_CF(data, cf, "mbedtls_ssl_write(len=%zu) -> previously blocked " "on %zu bytes", len, backend->send_blocked_len); len = backend->send_blocked_len; From 355ba2ceb87ce9dce8dbfe809d86e1f5babb0b8f Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Sat, 23 May 2026 18:04:13 +0200 Subject: [PATCH 2/2] mbedtls: also require unchanged buffer on retry of a blocked send --- docs/libcurl/curl_easy_send.md | 3 ++- lib/vtls/mbedtls.c | 11 +++++++---- tests/http/test_07_upload.py | 21 +++++++++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docs/libcurl/curl_easy_send.md b/docs/libcurl/curl_easy_send.md index 012831b46b..f16cc0c057 100644 --- a/docs/libcurl/curl_easy_send.md +++ b/docs/libcurl/curl_easy_send.md @@ -94,7 +94,8 @@ On failure, returns the appropriate error code. This function may return **CURLE_AGAIN**. In this case, use your operating system facilities to wait until the socket is writable, and retry. The **buflen** passed to the retry should be no smaller than the one passed to -the call that returned **CURLE_AGAIN**. +the call that returned **CURLE_AGAIN**, and **buffer** should hold the same +data. If there is no socket available to use from the previous transfer, this function returns **CURLE_UNSUPPORTED_PROTOCOL**. diff --git a/lib/vtls/mbedtls.c b/lib/vtls/mbedtls.c index 1c7831467e..2913a7c648 100644 --- a/lib/vtls/mbedtls.c +++ b/lib/vtls/mbedtls.c @@ -87,6 +87,7 @@ struct mbed_ssl_backend_data { const char *protocols[3]; #endif int *ciphersuites; + const void *send_blocked_buf; size_t send_blocked_len; BIT(initialized); /* mbedtls_ssl_context is initialized */ BIT(sent_shutdown); @@ -1276,11 +1277,12 @@ static CURLcode mbed_send(struct Curl_cfilter *cf, struct Curl_easy *data, *pnwritten = 0; connssl->io_need = CURL_SSL_IO_NEED_NONE; /* mbedTLS is picky when a mbedtls_ssl_write() was previously blocked. - * It requires to be called with the same amount of bytes again, or it - * loses bytes, e.g. reporting all was sent but they were not. - * Remember the blocked length and use that when set. */ + * It requires to be called with the same buffer and at least the same + * amount of bytes again, or it loses bytes, e.g. reporting all was sent + * but they were not. Remember the blocked buffer and length and use them + * when set. */ if(backend->send_blocked) { - if(len < backend->send_blocked_len) + if(len < backend->send_blocked_len || mem != backend->send_blocked_buf) return CURLE_SEND_ERROR; CURL_TRC_CF(data, cf, "mbedtls_ssl_write(len=%zu) -> previously blocked " "on %zu bytes", len, backend->send_blocked_len); @@ -1315,6 +1317,7 @@ static CURLcode mbed_send(struct Curl_cfilter *cf, struct Curl_easy *data, if((result == CURLE_AGAIN) && !backend->send_blocked) { backend->send_blocked = TRUE; backend->send_blocked_len = len; + backend->send_blocked_buf = mem; } } diff --git a/tests/http/test_07_upload.py b/tests/http/test_07_upload.py index 7ad396d73c..24cc33242e 100644 --- a/tests/http/test_07_upload.py +++ b/tests/http/test_07_upload.py @@ -653,6 +653,27 @@ class TestUpload: ]) r.check_exit_code(0) + # issue #21689, h2 + mbedtls: a blocked mbedtls_ssl_write() must be + # retried with the same buffer; mbed_send() returns CURLE_SEND_ERROR + # otherwise. chunk_delay creates server-side backpressure so the + # kernel send buffer saturates and the partial-write resume path + # actually triggers on typical hosts. + @pytest.mark.skipif(condition=not Env.have_h2_curl(), reason="curl without h2") + def test_07_64_h2_mbedtls_blocked_send(self, env: Env, httpd, nghttpx): + if not env.curl_uses_lib('mbedtls'): + pytest.skip('test specific to mbedtls') + proto = 'h2' + fdata = os.path.join(env.gen_dir, 'data-10m') + curl = CurlClient(env=env) + url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put?'\ + f'id=[0-0]&chunk_delay=2ms' + r = curl.http_put(urls=[url], fdata=fdata, alpn_proto=proto, + extra_args=['--trace-config', 'ssl']) + r.check_stats(count=1, http_status=200, exitcode=0) + if not any('previously blocked' in line for line in r.trace_lines): + log.warning('mbedtls partial-write resume path not exercised; ' + 'test passed only because no backpressure occurred') + # nghttpx is the only server we have that supports TLS early data @pytest.mark.skipif(condition=not Env.have_nghttpx(), reason="no nghttpx") @pytest.mark.parametrize("proto,upload_size", [