diff --git a/docs/libcurl/curl_easy_send.md b/docs/libcurl/curl_easy_send.md index 016a6756a8..f16cc0c057 100644 --- a/docs/libcurl/curl_easy_send.md +++ b/docs/libcurl/curl_easy_send.md @@ -92,7 +92,10 @@ 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**, 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 f0d2f80230..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,13 @@ 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) { - DEBUGASSERT(backend->send_blocked_len <= 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); len = backend->send_blocked_len; @@ -1314,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", [