SCP/SFTP: avoid busy loop after EAGAIN

The ssh libraries do not reveal if they still have data buffered from
the peer. Only when their buffers are read empty can curl be sure that
it is safe to rely on socket polling.

This change adds detection of EGAIN on receive in the transfer loop and
allows SFTP/SCP transfers to avoid a busy loop in such a case (which
should happen often when CPU exceeds network bandwidth).

Closes #17533
This commit is contained in:
Stefan Eissing 2025-06-04 15:26:55 +02:00 committed by Daniel Stenberg
parent e67a500841
commit 34d694f89c
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2

View file

@ -115,7 +115,7 @@ char *Curl_checkheaders(const struct Curl_easy *data,
}
#endif
static int data_pending(struct Curl_easy *data)
static int data_pending(struct Curl_easy *data, bool rcvd_eagain)
{
struct connectdata *conn = data->conn;
@ -124,8 +124,9 @@ static int data_pending(struct Curl_easy *data)
/* in the case of libssh2, we can never be really sure that we have emptied
its internal buffers so we MUST always try until we get EAGAIN back */
return conn->handler->protocol&(CURLPROTO_SCP|CURLPROTO_SFTP) ||
Curl_conn_data_pending(data, FIRSTSOCKET);
return (!rcvd_eagain &&
conn->handler->protocol&(CURLPROTO_SCP|CURLPROTO_SFTP)) ||
Curl_conn_data_pending(data, FIRSTSOCKET);
}
/*
@ -271,6 +272,7 @@ static CURLcode sendrecv_dl(struct Curl_easy *data,
int maxloops = 10;
curl_off_t total_received = 0;
bool is_multiplex = FALSE;
bool rcvd_eagain = FALSE;
result = Curl_multi_xfer_buf_borrow(data, &xfer_buf, &xfer_blen);
if(result)
@ -303,23 +305,25 @@ static CURLcode sendrecv_dl(struct Curl_easy *data,
bytestoread = (size_t)data->set.max_recv_speed;
}
rcvd_eagain = FALSE;
nread = xfer_recv_resp(data, buf, bytestoread, is_multiplex, &result);
if(nread < 0) {
if(CURLE_AGAIN != result)
goto out; /* real error */
rcvd_eagain = TRUE;
result = CURLE_OK;
if(data->req.download_done && data->req.no_body &&
!data->req.resp_trailer) {
DEBUGF(infof(data, "EAGAIN, download done, no trailer announced, "
"not waiting for EOS"));
nread = 0;
/* continue as if we read the EOS */
/* continue as if we received the EOS */
}
else
break; /* get out of loop */
}
/* We only get a 0-length read on EndOfStream */
/* We only get a 0-length receive at the end of the response */
blen = (size_t)nread;
is_eos = (blen == 0);
*didwhat |= KEEP_RECV;
@ -355,12 +359,14 @@ static CURLcode sendrecv_dl(struct Curl_easy *data,
} while(maxloops--);
if((maxloops <= 0) || data_pending(data)) {
/* did not read until EAGAIN or there is still pending data, mark as
read-again-please */
if(!rcvd_eagain || data_pending(data, rcvd_eagain)) {
/* Did not read until EAGAIN or there is still data pending
* in buffers. Mark as read-again via simulated SELECT results. */
data->state.select_bits = CURL_CSELECT_IN;
if((k->keepon & KEEP_SENDBITS) == KEEP_SEND)
data->state.select_bits |= CURL_CSELECT_OUT;
CURL_TRC_M(data, "sendrecv_dl() no EAGAIN/pending data, "
"set select_bits=%x", data->state.select_bits);
}
if(((k->keepon & (KEEP_RECV|KEEP_SEND)) == KEEP_SEND) &&