curl/tests/unit/unit2606.c
Graham Campbell 820c014578 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
2026-07-17 04:00:56 -04:00

221 lines
6.9 KiB
C

/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "unitcheck.h"
#include "urldata.h"
#include "cfilters.h"
#include "curl_trc.h"
#include "select.h"
/* Test the gating in Curl_conn_adjust_pollset(): each filter chain
* of a connection must only get to adjust the pollset when the transfer
* wants to send or receive, or when that chain itself is connecting or
* shutting down. One chain being in connect or shutdown must not add
* poll events for the other, see #21671. */
struct t2606_ctx {
int calls;
bool add_socket;
curl_socket_t sock;
};
static void t2606_cf_destroy(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
(void)data;
/* ctx is owned by the test case, nothing to free */
cf->ctx = NULL;
}
static CURLcode t2606_cf_connect(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *done)
{
(void)cf;
(void)data;
*done = TRUE;
return CURLE_OK;
}
static CURLcode t2606_cf_adjust_pollset(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct easy_pollset *ps)
{
struct t2606_ctx *ctx = cf->ctx;
ctx->calls++;
if(ctx->add_socket)
return Curl_pollset_add_in(data, ps, ctx->sock);
return CURLE_OK;
}
static const struct Curl_cftype t2606_cft = {
"TEST-PS",
CF_TYPE_IP_CONNECT,
CURL_LOG_LVL_NONE,
t2606_cf_destroy,
t2606_cf_connect,
Curl_cf_def_shutdown,
t2606_cf_adjust_pollset,
Curl_cf_def_data_pending,
Curl_cf_def_send,
Curl_cf_def_recv,
Curl_cf_def_cntrl,
Curl_cf_def_conn_is_alive,
Curl_cf_def_conn_keep_alive,
Curl_cf_def_query,
};
struct t2606_case {
int id;
bool has_c1; /* a SECONDARYSOCKET chain exists */
bool c0_connected;
bool c1_connected;
bool c0_shutdown; /* shutdown started on the chain */
bool c1_shutdown;
bool xfer_io; /* the pollset holds a transfer socket already */
bool c0_adds; /* chain 0 adds a socket when called */
int exp_c0_calls;
int exp_c1_calls;
};
static CURLcode t2606_setup(CURL **easy)
{
CURLcode result = CURLE_OK;
global_init(CURL_GLOBAL_ALL);
*easy = curl_easy_init();
if(!*easy) {
curl_global_cleanup();
return CURLE_OUT_OF_MEMORY;
}
return result;
}
static void t2606_stop(CURL *easy)
{
curl_easy_cleanup(easy);
curl_global_cleanup();
}
static void t2606_run(struct Curl_easy *data, const struct t2606_case *tc)
{
struct connectdata *conn;
struct Curl_cfilter *cf0 = NULL, *cf1 = NULL;
struct t2606_ctx ctx0, ctx1;
struct easy_pollset ps;
CURLcode result;
char msg[128];
conn = curlx_calloc(1, sizeof(*conn));
abort_if(!conn, "could not allocate a connection");
memset(&ctx0, 0, sizeof(ctx0));
memset(&ctx1, 0, sizeof(ctx1));
ctx0.sock = 42;
ctx0.add_socket = tc->c0_adds;
ctx1.sock = 43;
result = Curl_cf_create(&cf0, &t2606_cft, &ctx0);
fail_unless(!result, "creating filter chain 0 failed");
if(!result) {
Curl_conn_cf_add(data, conn, FIRSTSOCKET, cf0);
cf0->connected = tc->c0_connected;
if(tc->c0_shutdown)
conn->shutdown.start[FIRSTSOCKET] = curlx_now();
}
if(tc->has_c1) {
result = Curl_cf_create(&cf1, &t2606_cft, &ctx1);
fail_unless(!result, "creating filter chain 1 failed");
if(!result) {
Curl_conn_cf_add(data, conn, SECONDARYSOCKET, cf1);
cf1->connected = tc->c1_connected;
if(tc->c1_shutdown)
conn->shutdown.start[SECONDARYSOCKET] = curlx_now();
}
}
Curl_pollset_init(&ps);
if(tc->xfer_io) {
result = Curl_pollset_add_in(data, &ps, 7);
fail_unless(!result, "preloading the pollset failed");
}
result = Curl_conn_adjust_pollset(data, conn, &ps);
curl_msprintf(msg, "case %d: adjust_pollset failed", tc->id);
fail_unless(!result, msg);
curl_msprintf(msg, "case %d: chain 0 called %d times, expected %d",
tc->id, ctx0.calls, tc->exp_c0_calls);
fail_unless(ctx0.calls == tc->exp_c0_calls, msg);
curl_msprintf(msg, "case %d: chain 1 called %d times, expected %d",
tc->id, ctx1.calls, tc->exp_c1_calls);
fail_unless(ctx1.calls == tc->exp_c1_calls, msg);
if(tc->c0_adds && tc->exp_c0_calls) {
curl_msprintf(msg, "case %d: chain 0 did not add its socket", tc->id);
fail_unless(ps.n > 0, msg);
}
Curl_pollset_cleanup(&ps);
Curl_conn_cf_discard_all(data, conn, SECONDARYSOCKET);
Curl_conn_cf_discard_all(data, conn, FIRSTSOCKET);
unit_test_abort:
curlx_free(conn);
}
static CURLcode test_unit2606(const char *arg)
{
CURL *easy;
UNITTEST_BEGIN(t2606_setup(&easy))
static const struct t2606_case TEST_CASES[] = {
/* both chains connected and quiet, transfer idle: nothing runs */
{ 1, TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, 0, 0 },
/* transfer wants IO: both chains run */
{ 2, TRUE, TRUE, TRUE, FALSE, FALSE, TRUE, FALSE, 1, 1 },
/* chain 0 connecting, chain 1 connected and quiet: only chain 0 */
{ 3, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, 1, 0 },
/* chain 1 connecting, chain 0 connected and quiet: only chain 1 */
{ 4, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, 0, 1 },
/* chain 0 shutting down, chain 1 connected and quiet: only chain 0 */
{ 5, TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, 1, 0 },
/* chain 1 shutting down, chain 0 connected and quiet: only chain 1 */
{ 6, TRUE, TRUE, TRUE, FALSE, TRUE, FALSE, FALSE, 0, 1 },
/* single connected quiet chain, transfer idle: nothing runs */
{ 7, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, 0, 0 },
/* single chain shutting down: runs */
{ 8, FALSE, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE, 1, 0 },
/* a socket added by connecting chain 0 must not run quiet chain 1 */
{ 9, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE, TRUE, 1, 0 },
};
size_t i;
for(i = 0; i < CURL_ARRAYSIZE(TEST_CASES); ++i) {
t2606_run(easy, &TEST_CASES[i]);
}
UNITTEST_END(t2606_stop(easy))
}