cfilters: fix event-based connection shutdown

- Adjust pollset during connection shutdown.

- Separate the FIRSTSOCKET and SECONDSOCKET check so that one being in
  connect or shutdown no longer adds poll events for the other.

- Fix shutdown state evaluation (as detailed below).

- Add a unit test for Curl_conn_adjust_pollset.

- Add a client for event testing.

- Test that shutdown sockets stay with the socket callback until done.

The pollset predicate now reads the connection's own shutdown state
rather than going through data->conn, and the EXPIRE_SHUTDOWN arming in
cshutdn_perform() works again: next_expire_ms started at 0 and could
never be set, so a peer that never sends a close_notify would still park
its connection forever, timeout or not.

Reported-by: pszemus@users.noreply.github.com

Fixes https://github.com/curl/curl/issues/22282
Closes https://github.com/curl/curl/pull/22304
This commit is contained in:
Graham Campbell 2026-07-12 17:52:16 +01:00 committed by Jay Satiro
parent 33dc64fd0e
commit 820c014578
12 changed files with 612 additions and 21 deletions

View file

@ -206,7 +206,7 @@ CURLcode Curl_conn_shutdown(struct Curl_easy *data, int sockindex, bool *done)
}
*done = FALSE;
if(!Curl_shutdown_started(data, sockindex)) {
if(!Curl_shutdown_started(data->conn, sockindex)) {
Curl_shutdown_start(data, sockindex, 0);
}
else {
@ -748,23 +748,29 @@ CURLcode Curl_conn_adjust_pollset(struct Curl_easy *data,
struct easy_pollset *ps)
{
CURLcode result = CURLE_OK;
bool want_io = !!ps->n;
int i;
DEBUGASSERT(data);
DEBUGASSERT(conn);
/* During connect time, connection filters may add sockets to the pollset
* even when the transfer neither wants to send nor receive. And those
* sockets, when having events, are served.
* Once connected however, a transfer that neither wants to send nor receive
* sockets, when having events, are served. The same applies to a
* filter chain whose shutdown has started.
* Once a filter chain is connected however and before its shutdown
* starts, a transfer that neither wants to send nor receive
* will never call the connection filters. Any sockets added by the filters
* will not change state and POLLIN/POLLOUT events will trigger forever,
* making us busy loop. See #21671 */
if(ps->n || !Curl_conn_is_connected(conn, FIRSTSOCKET) ||
(conn->cfilter[SECONDARYSOCKET] &&
!Curl_conn_is_connected(conn, SECONDARYSOCKET))) {
for(i = 0; (i < 2) && !result && conn; ++i) {
* making us busy loop. See #21671.
* Gate each filter chain on its own state, so that one chain being in
* connect or shutdown does not add poll events for the other. Check
* against the transfer's own interest, before any chain added sockets
* of its own. */
for(i = 0; (i < 2) && !result; ++i) {
if(conn->cfilter[i] &&
(want_io || !Curl_conn_is_connected(conn, i) ||
Curl_shutdown_started(conn, i)))
result = Curl_conn_cf_adjust_pollset(conn->cfilter[i], data, ps);
}
}
return result;
}

View file

@ -75,7 +75,7 @@ UNITTEST timediff_t timeleft_now_ms(struct Curl_easy *data,
timediff_t timeleft_ms = 0;
timediff_t ctimeleft_ms = 0;
if(Curl_shutdown_started(data, FIRSTSOCKET))
if(data->conn && Curl_shutdown_started(data->conn, FIRSTSOCKET))
return Curl_shutdown_timeleft(data, data->conn, FIRSTSOCKET);
else if(Curl_is_connecting(data)) {
timediff_t ctimeout_ms = (data->set.connecttimeout > 0) ?
@ -164,13 +164,10 @@ void Curl_shutdown_clear(struct Curl_easy *data, int sockindex)
memset(pt, 0, sizeof(*pt));
}
bool Curl_shutdown_started(struct Curl_easy *data, int sockindex)
bool Curl_shutdown_started(struct connectdata *conn, int sockindex)
{
if(data->conn) {
struct curltime *pt = &data->conn->shutdown.start[sockindex];
return (pt->tv_sec > 0) || (pt->tv_usec > 0);
}
return FALSE;
const struct curltime *pt = &conn->shutdown.start[sockindex];
return (pt->tv_sec > 0) || (pt->tv_usec > 0);
}
/*

View file

@ -58,7 +58,7 @@ timediff_t Curl_conn_shutdown_timeleft(struct Curl_easy *data,
void Curl_shutdown_clear(struct Curl_easy *data, int sockindex);
/* TRUE iff shutdown has been started */
bool Curl_shutdown_started(struct Curl_easy *data, int sockindex);
bool Curl_shutdown_started(struct connectdata *conn, int sockindex);
/*
* Used to extract socket and connectdata struct for the most recent

View file

@ -76,7 +76,7 @@ static void cshutdn_run_once(struct Curl_easy *data,
/* We expect to be attached when called */
DEBUGASSERT(data->conn == conn);
if(!Curl_shutdown_started(data, FIRSTSOCKET)) {
if(!Curl_shutdown_started(conn, FIRSTSOCKET)) {
Curl_shutdown_start(data, FIRSTSOCKET, 0);
}
@ -253,7 +253,7 @@ static void cshutdn_perform(struct cshutdn *cshutdn,
/* idata has one timer list, but maybe more than one connection.
* Set EXPIRE_SHUTDOWN to the smallest time left for all. */
ms = Curl_conn_shutdown_timeleft(data, conn);
if(ms && ms < next_expire_ms)
if(ms && (!next_expire_ms || (ms < next_expire_ms)))
next_expire_ms = ms;
}
e = enext;

View file

@ -155,7 +155,7 @@ static bool xfer_recv_shutdown_started(struct Curl_easy *data)
{
if(!data || !data->conn)
return FALSE;
return Curl_shutdown_started(data, data->conn->recv_idx);
return Curl_shutdown_started(data->conn, data->conn->recv_idx);
}
CURLcode Curl_xfer_send_shutdown(struct Curl_easy *data, bool *done)