You are viewing a plain text version of this content. The canonical link for it is here.
Posted to c-commits@axis.apache.org by ro...@apache.org on 2026/09/25 12:51:15 UTC
(axis-axis2-c-core) 03/06: Bound the h2 client's handshake writes by connect_timeout_ms
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit cac64816b243a2829874c9c1f69db8deb27c850e
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Thu Sep 24 13:22:28 2026 -1000
Bound the h2 client's handshake writes by connect_timeout_ms
A handshake write that blocked waited io_timeout_ms, not the remaining
connect deadline. Also: monotonic time is 64-bit (a 32-bit long of ms
wraps after 24.8 days), response-buffer doubling cannot overflow size_t,
and the file gains comments on how bytes flow through TLS and nghttp2.
Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
---
.../transport/h2/sender/axis2_h2_json_client.c | 140 ++++++++++++++++-----
1 file changed, 108 insertions(+), 32 deletions(-)
diff --git a/src/core/transport/h2/sender/axis2_h2_json_client.c b/src/core/transport/h2/sender/axis2_h2_json_client.c
index 2a5734347..535f812ab 100644
--- a/src/core/transport/h2/sender/axis2_h2_json_client.c
+++ b/src/core/transport/h2/sender/axis2_h2_json_client.c
@@ -23,6 +23,18 @@
* The memory BIOs keep every byte of socket I/O in this file, so each wait is
* a poll() with a deadline, a write to a closed peer cannot raise SIGPIPE
* (send() with MSG_NOSIGNAL), and nothing ever blocks without a timeout.
+ *
+ * Bytes move in two directions, and every function below is one step:
+ *
+ * out: nghttp2_session_mem_send -> SSL_write -> wbio -> h2c_flush -> send()
+ * (h2c_pump_send drives the whole chain)
+ * in: recv() -> h2c_fill -> rbio -> SSL_read -> nghttp2_session_mem_recv
+ * -> the h2c_on_* callbacks (h2c_pump_recv drives the whole chain)
+ *
+ * OpenSSL never touches the socket: it reads ciphertext from rbio and writes
+ * ciphertext to wbio, and this file moves it between the BIOs and the fd.
+ * nghttp2 likewise never touches TLS: it hands out and takes in plaintext
+ * frames. One request is in flight at a time; its state lives in the struct.
*/
#include <axis2_h2_json_client.h>
@@ -45,6 +57,7 @@
#include <unistd.h>
#include <errno.h>
#include <stdarg.h>
+#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
@@ -90,19 +103,20 @@ struct axis2_h2_json_client
BIO *wbio; /* TLS -> network; owned by ssl */
nghttp2_session *session;
- /* the request in flight */
+ /* the request in flight -- written by the nghttp2 callbacks, read by
+ * axis2_h2_json_client_post once the stream closes */
const axutil_env_t *env;
int32_t stream_id;
- int stream_closed;
- uint32_t stream_error;
- int http_status;
- const axis2_char_t *req_body;
+ int stream_closed; /* set by h2c_on_stream_close */
+ uint32_t stream_error; /* RST_STREAM code; NGHTTP2_NO_ERROR if clean */
+ int http_status; /* from :status; 0 until the headers arrive */
+ const axis2_char_t *req_body; /* caller's buffer, not copied */
size_t req_len;
- size_t req_off;
- axis2_char_t *resp;
+ size_t req_off; /* how much of req_body nghttp2 has taken */
+ axis2_char_t *resp; /* grows in h2c_on_data; handed to the caller */
size_t resp_len;
size_t resp_cap;
- int resp_too_large;
+ int resp_too_large; /* max_response hit; the stream was cancelled */
axis2_char_t error[256];
};
@@ -135,12 +149,14 @@ h2c_ssl_reason(char *buf, size_t len)
return buf;
}
-static long
+/* 64-bit so the arithmetic cannot overflow where long is 32 bits: a 32-bit
+ * long of milliseconds wraps after 24.8 days of uptime. */
+static int64_t
h2c_now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
- return (long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
+ return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
/* poll() one descriptor, retrying on EINTR against the same deadline.
@@ -148,11 +164,11 @@ h2c_now_ms(void)
static int
h2c_wait(int fd, short events, int timeout_ms)
{
- long deadline = h2c_now_ms() + timeout_ms;
+ int64_t deadline = h2c_now_ms() + timeout_ms;
for (;;)
{
struct pollfd p;
- long left = deadline - h2c_now_ms();
+ int64_t left = deadline - h2c_now_ms();
int r;
if (left < 0)
left = 0;
@@ -173,9 +189,15 @@ h2c_wait(int fd, short events, int timeout_ms)
/* socket I/O under the memory BIOs */
/* ------------------------------------------------------------------------ */
+/* deadline for h2c_send_all / h2c_flush: none, so each wait for the socket
+ * to drain gets io_timeout_ms. The handshake passes its connect deadline
+ * instead, so a server that stalls its receive window cannot stretch
+ * connect_timeout_ms to io_timeout_ms. */
+#define H2C_NO_DEADLINE ((int64_t)-1)
+
static axis2_status_t
h2c_send_all(axis2_h2_json_client_t *c, const axutil_env_t *env,
- const char *buf, size_t len)
+ const char *buf, size_t len, int64_t deadline)
{
while (len > 0)
{
@@ -190,7 +212,9 @@ h2c_send_all(axis2_h2_json_client_t *c, const axutil_env_t *env,
continue;
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
{
- int w = h2c_wait(c->fd, POLLOUT, c->io_timeout_ms);
+ int64_t left = deadline == H2C_NO_DEADLINE ? c->io_timeout_ms
+ : deadline - h2c_now_ms();
+ int w = left > 0 ? h2c_wait(c->fd, POLLOUT, (int)left) : 0;
if (w > 0)
continue;
h2c_fail(c, env, w == 0 ? "timed out sending to the server"
@@ -203,21 +227,26 @@ h2c_send_all(axis2_h2_json_client_t *c, const axutil_env_t *env,
return AXIS2_SUCCESS;
}
-/* Send whatever TLS has produced. */
+/* Send whatever TLS has produced -- the ciphertext waiting in wbio. Called
+ * after anything that can make OpenSSL write: a handshake step, SSL_write,
+ * SSL_shutdown, and SSL_read (which can owe the peer a reply). */
static axis2_status_t
-h2c_flush(axis2_h2_json_client_t *c, const axutil_env_t *env)
+h2c_flush(axis2_h2_json_client_t *c, const axutil_env_t *env, int64_t deadline)
{
char buf[H2C_IO_CHUNK];
int n;
while ((n = BIO_read(c->wbio, buf, (int)sizeof(buf))) > 0)
{
- if (h2c_send_all(c, env, buf, (size_t)n) != AXIS2_SUCCESS)
+ if (h2c_send_all(c, env, buf, (size_t)n, deadline) != AXIS2_SUCCESS)
return AXIS2_FAILURE;
}
return AXIS2_SUCCESS;
}
-/* Wait up to timeout_ms for bytes from the server and hand them to TLS. */
+/* Wait up to timeout_ms for bytes from the server and hand them to TLS.
+ * One recv() per call: the caller asks OpenSSL for plaintext afterwards and
+ * calls again only if OpenSSL wants more. timeout_ms 0 is a non-blocking
+ * check (see h2c_peer_gone). */
static axis2_status_t
h2c_fill(axis2_h2_json_client_t *c, const axutil_env_t *env, int timeout_ms)
{
@@ -261,6 +290,12 @@ h2c_fill(axis2_h2_json_client_t *c, const axutil_env_t *env, int timeout_ms)
/* nghttp2 callbacks */
/* ------------------------------------------------------------------------ */
+/* nghttp2 calls the h2c_on_* callbacks from inside nghttp2_session_mem_recv,
+ * i.e. from h2c_pump_recv. They only record into the struct; frames for any
+ * stream other than the request in flight are ignored. */
+
+/* Keep :status. A valid status is exactly three digits (RFC 9110 15);
+ * anything else leaves http_status 0, which the post reports. */
static int
h2c_on_header(nghttp2_session *session, const nghttp2_frame *frame,
const uint8_t *name, size_t namelen,
@@ -286,9 +321,14 @@ h2c_on_data(nghttp2_session *session, uint8_t flags, int32_t stream_id,
(void)flags;
if (stream_id != c->stream_id || c->resp_too_large)
return 0;
+ /* Written as a subtraction so it cannot overflow; resp_len never exceeds
+ * max_response, so the right-hand side cannot underflow. */
if (len > c->max_response - c->resp_len)
{
- /* Stop the server rather than keep buffering; the post reports it. */
+ /* Stop the server rather than keep buffering; the post reports it.
+ * Returning 0 rather than an error keeps the session parseable until
+ * the stream closes, so the failure is reported as "too large" and
+ * not as a protocol error. */
c->resp_too_large = 1;
nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, stream_id, NGHTTP2_CANCEL);
return 0;
@@ -297,8 +337,11 @@ h2c_on_data(nghttp2_session *session, uint8_t flags, int32_t stream_id,
{
size_t cap = c->resp_cap ? c->resp_cap : 4096;
axis2_char_t *grown;
- while (cap < c->resp_len + len + 1)
- cap *= 2;
+ /* need cannot wrap: resp_len + len <= max_response < SIZE_MAX (see
+ * create). Doubling stops short of overflow and takes need instead. */
+ size_t need = c->resp_len + len + 1;
+ while (cap < need)
+ cap = cap > SIZE_MAX / 2 ? need : cap * 2;
grown = AXIS2_REALLOC(c->env->allocator, c->resp, cap);
if (!grown)
return NGHTTP2_ERR_CALLBACK_FAILURE;
@@ -324,6 +367,8 @@ h2c_on_stream_close(nghttp2_session *session, int32_t stream_id,
return 0;
}
+/* nghttp2 pulls the request body through this as flow control allows,
+ * `length` bytes at most per call; EOF is flagged with the last chunk. */
#ifdef H2C_NGHTTP2_V2
static nghttp2_ssize
h2c_read_body(nghttp2_session *session, int32_t stream_id, uint8_t *buf,
@@ -380,11 +425,16 @@ h2c_pump_send(axis2_h2_json_client_t *c, const axutil_env_t *env)
return AXIS2_FAILURE;
}
}
- return h2c_flush(c, env);
+ return h2c_flush(c, env, H2C_NO_DEADLINE);
}
/* Read what the server has sent -- waiting up to io_timeout for the first
- * byte -- and feed it to nghttp2. Returns once TLS has nothing more to give. */
+ * byte -- and feed it to nghttp2. Returns once TLS has nothing more to give.
+ *
+ * SSL_read returning WANT_READ means "no complete record buffered". Before
+ * anything has been decoded that means wait on the socket; after something
+ * has, return so the caller can act on it (send WINDOW_UPDATEs, see whether
+ * the stream closed) instead of blocking for more. */
static axis2_status_t
h2c_pump_recv(axis2_h2_json_client_t *c, const axutil_env_t *env)
{
@@ -415,7 +465,7 @@ h2c_pump_recv(axis2_h2_json_client_t *c, const axutil_env_t *env)
if (got_any)
return AXIS2_SUCCESS;
/* TLS may owe the server something first (a key update). */
- if (h2c_flush(c, env) != AXIS2_SUCCESS ||
+ if (h2c_flush(c, env, H2C_NO_DEADLINE) != AXIS2_SUCCESS ||
h2c_fill(c, env, c->io_timeout_ms) != AXIS2_SUCCESS)
return AXIS2_FAILURE;
break;
@@ -441,12 +491,15 @@ h2c_disconnect(axis2_h2_json_client_t *c, const axutil_env_t *env, int graceful)
{
if (graceful && c->session && c->ssl)
{
- /* Best effort: GOAWAY and close_notify, without waiting for replies. */
+ /* Best effort: GOAWAY and close_notify, without waiting for replies.
+ * Only on free(); after a failure the connection is dropped outright.
+ * c->error is saved and restored so a failure here cannot overwrite
+ * the error the caller is about to read. */
axis2_char_t saved[sizeof(c->error)];
memcpy(saved, c->error, sizeof(saved));
nghttp2_session_terminate_session(c->session, NGHTTP2_NO_ERROR);
if (h2c_pump_send(c, env) == AXIS2_SUCCESS && SSL_shutdown(c->ssl) >= 0)
- h2c_flush(c, env);
+ h2c_flush(c, env, H2C_NO_DEADLINE);
memcpy(c->error, saved, sizeof(saved));
ERR_clear_error();
}
@@ -469,8 +522,12 @@ h2c_disconnect(axis2_h2_json_client_t *c, const axutil_env_t *env, int graceful)
}
}
+/* Try each address getaddrinfo returns (IPv6 and IPv4, in its order) until
+ * one connects. The socket is non-blocking from the start so the connect
+ * itself honours the deadline; it stays non-blocking for the connection's
+ * life, which is what lets every later wait go through h2c_wait. */
static axis2_status_t
-h2c_tcp_connect(axis2_h2_json_client_t *c, const axutil_env_t *env, long deadline)
+h2c_tcp_connect(axis2_h2_json_client_t *c, const axutil_env_t *env, int64_t deadline)
{
struct addrinfo hints, *res = NULL, *ai;
char port[16];
@@ -492,7 +549,7 @@ h2c_tcp_connect(axis2_h2_json_client_t *c, const axutil_env_t *env, long deadlin
int fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
int one = 1, err = 0;
socklen_t errlen = sizeof(err);
- long left;
+ int64_t left;
if (fd < 0)
{
last_errno = errno;
@@ -535,7 +592,7 @@ h2c_tcp_connect(axis2_h2_json_client_t *c, const axutil_env_t *env, long deadlin
static axis2_status_t
h2c_connect(axis2_h2_json_client_t *c, const axutil_env_t *env)
{
- long deadline = h2c_now_ms() + c->connect_timeout_ms;
+ int64_t deadline = h2c_now_ms() + c->connect_timeout_ms;
const unsigned char *alpn = NULL;
unsigned int alpn_len = 0;
nghttp2_session_callbacks *cbs = NULL;
@@ -585,11 +642,15 @@ h2c_connect(axis2_h2_json_client_t *c, const axutil_env_t *env)
}
}
+ /* Handshake: each SSL_do_handshake step may leave records in wbio (sent
+ * now) and then want the server's reply (waited for against the same
+ * deadline as the TCP connect). Certificate and name verification
+ * happen inside the handshake, so a failure here names the reason. */
for (;;)
{
- long left;
+ int64_t left;
rv = SSL_do_handshake(c->ssl);
- if (h2c_flush(c, env) != AXIS2_SUCCESS)
+ if (h2c_flush(c, env, deadline) != AXIS2_SUCCESS)
return AXIS2_FAILURE;
if (rv == 1)
break;
@@ -637,6 +698,10 @@ h2c_connect(axis2_h2_json_client_t *c, const axutil_env_t *env)
return AXIS2_FAILURE;
}
+ /* No server push (nothing here could use it). Larger receive windows
+ * than the 64 KiB default so a big response is not throttled to one
+ * window per round trip; max_response still bounds what is kept. The
+ * SETTINGS frame is only queued here -- the first post sends it. */
settings[0].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH;
settings[0].value = 0;
settings[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE;
@@ -757,13 +822,18 @@ axis2_h2_json_client_create(
c->connect_timeout_ms = o->connect_timeout_ms > 0 ? o->connect_timeout_ms : H2C_DEFAULT_CONNECT_MS;
c->io_timeout_ms = o->io_timeout_ms > 0 ? o->io_timeout_ms : H2C_DEFAULT_IO_MS;
c->max_response = o->max_response_bytes > 0 ? o->max_response_bytes : H2C_DEFAULT_MAX_RESPONSE;
+ if (c->max_response == SIZE_MAX)
+ c->max_response = SIZE_MAX - 1; /* room for the terminating NUL */
c->host = axutil_strdup(env, o->host);
c->verify_name = axutil_strdup(env, (o->verify_name && o->verify_name[0]) ? o->verify_name : o->host);
if (!c->host || !c->verify_name)
goto fail;
c->verify_name_is_ip = h2c_is_ip_literal(c->verify_name);
- /* :authority -- bracketed when an IPv6 literal. */
+ /* :authority -- bracketed when an IPv6 literal. The name the certificate
+ * is checked against is also the one the request names, so a virtual
+ * host behind a load balancer's address routes correctly. +16 covers
+ * "[", "]", ":", five port digits and the NUL. */
alen = strlen(c->verify_name) + 16;
c->authority = AXIS2_MALLOC(env->allocator, alen);
if (!c->authority)
@@ -912,6 +982,10 @@ axis2_h2_json_client_post(
}
c->stream_id = sid;
+ /* Alternate: send what nghttp2 has queued (headers, body as the window
+ * opens, WINDOW_UPDATEs, SETTINGS ACKs), then wait for the server,
+ * until the callbacks mark the stream closed. If the session wants
+ * neither to read nor write, the server has GOAWAYed us mid-request. */
while (!c->stream_closed)
{
if (h2c_pump_send(c, env) != AXIS2_SUCCESS)
@@ -957,6 +1031,8 @@ axis2_h2_json_client_post(
goto fail;
}
}
+ /* The terminator is not counted in resp_len; h2c_on_data always leaves
+ * room for it. Ownership of the buffer passes to the caller. */
c->resp[c->resp_len] = '\0';
*response_out = c->resp;
if (response_len_out)