Commit f2b3c3cbff for openssl.org
commit f2b3c3cbff9df70bdf1baccc937a0362fe6881b0
Author: Matt Caswell <matt@openssl.foundation>
Date: Wed Aug 12 11:19:05 2026 +0100
DTLS 1.3 Support blocking writes on listener connections
A listener connection writes through the listener's socket, which is
shared with every other connection and is always non-blocking. When a
send cannot be completed the DTLS record layer discards the datagram and
reports that the write should be retried - a fair default for an
unreliable transport, but not what an application which asked for
blocking writes expects, since it gets neither the data sent nor a call
which waited.
Add an rlayer callback, OSSL_FUNC_RLAYER_BLOCK_FOR_WRITE, which the
record layer calls instead of reporting a retry. For a listener
connection in blocking mode it waits for the socket to become writable
and returns 1, and tls_retry_write_records() goes round its loop again to
repeat the same send: nothing has been consumed at that point, so the
retry is a genuine second attempt rather than a resend.
Only the BIO_sendmmsg() branch is hooked. That is the one listener
connections take, because their peer address is set, and the BIO_write()
branch below it belongs to connections which have a BIO of their own to
block in. A NULL callback leaves both paths exactly as they were, so TLS
and standalone DTLS are untouched.
The wait itself is one wait per call, not a loop: the caller retries the
send and comes back here if it still cannot proceed, so a wakeup which
turns out not to have left room in the socket buffer costs another
attempt rather than a lost datagram.
A loopback socket's send buffer does not fill, so the test supplies the
transient failure with a filter BIO in front of the listener's write BIO
which rejects one send with a non-fatal error. Its ctrl forwards
everything to the socket underneath, the poll descriptors included, since
those are what the wait itself needs. The client is then read to confirm
the datagram really was sent rather than merely reported as sent.
Assisted-by: Claude Code:claude-opus-5
Reviewed-by: Ryan Hooper <ryanh@openssl.foundation>
Reviewed-by: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Merge-date: Mon Aug 17 08:29:59 2026
Merged-from: https://github.com/openssl/openssl/pull/32324
diff --git a/doc/man3/SSL_set_blocking_mode.pod b/doc/man3/SSL_set_blocking_mode.pod
index 3cf490f15b..afa6ec6e3f 100644
--- a/doc/man3/SSL_set_blocking_mode.pod
+++ b/doc/man3/SSL_set_blocking_mode.pod
@@ -57,7 +57,9 @@ cannot allow a read for one connection to block and thereby stall the others.
Its network BIO is therefore configured for nonblocking operation when it is set,
and blocking mode is provided by waiting for readiness of that socket instead,
as it is for QUIC. This applies to L<SSL_accept_connection(3)> on the listener as
-well as to reads and writes on the connections it returns.
+well as to reads and writes on the connections it returns. A write which the
+socket cannot accept is retried once it can be sent, where a nonblocking
+connection discards the datagram and reports that the write should be retried.
A connection SSL object returned by L<SSL_accept_connection(3)> inherits the
blocking mode of the listener it came from. Calling SSL_set_blocking_mode() on
diff --git a/ssl/d1_lib.c b/ssl/d1_lib.c
index 79e86a97ec..71b7416519 100644
--- a/ssl/d1_lib.c
+++ b/ssl/d1_lib.c
@@ -2522,7 +2522,7 @@ SSL *ossl_dtls_accept_connection(SSL *ssl, uint64_t flags)
* signalling the notifier because it produced readiness on our behalf.
*/
if (!ossl_dtls_block_until_ready(ssl, SSL_POLL_EVENT_IC,
- ossl_time_infinite()))
+ ossl_time_infinite(), /*bound_by_event_timeout=*/1))
break;
}
@@ -3178,7 +3178,7 @@ int ossl_dtls_conn_wait_for_datagram(SSL *s)
* time to retransmit.
*/
if (!ossl_dtls_block_until_ready(s, SSL_POLL_EVENT_R,
- ossl_time_infinite()))
+ ossl_time_infinite(), /*bound_by_event_timeout=*/1))
return 0;
if (!SSL_handle_events(s))
@@ -3195,6 +3195,57 @@ int ossl_dtls_conn_wait_for_datagram(SSL *s)
}
}
+/*
+ * Wait until the listener's socket can accept another datagram, for a
+ * connection which is in blocking mode.
+ *
+ * The socket is shared with every other connection and is always
+ * non-blocking, so a send which cannot be completed has nowhere to wait. For
+ * DTLS the record layer would otherwise discard the datagram - a reasonable
+ * default for an unreliable transport, but not what an application which asked
+ * for blocking writes expects.
+ *
+ * Only one wait is performed. The caller retries the send, and comes back here
+ * if it still cannot proceed, so a wakeup which turns out not to leave room in
+ * the socket buffer costs an extra attempt rather than a lost datagram.
+ *
+ * The retransmission timer deliberately does not shorten this wait, unlike the
+ * one for a datagram above. There the wakeup is useful, because the wait can
+ * service the timer itself; here it cannot. Servicing it would mean
+ * retransmitting a flight from inside tls_retry_write_records(), which is
+ * part-way through sending one and holds write buffer state that a
+ * re-entrant do_dtls1_write() would clobber. Waking for a timer nothing then
+ * services would be worse than not waking: the timeout stays expired, and an
+ * expired timeout reads as a zero deadline, so every later wait would return
+ * at once and the caller's retry loop would spin without sleeping. Waiting for
+ * the socket alone is also what the send actually needs. Retransmission is not
+ * the right response to a flight which has not finished going out, and once it
+ * has, the state machine handles the timer as usual.
+ *
+ * Returns 1 if the send should be retried, or 0 if the wait could not be
+ * performed or the listener has failed.
+ */
+int ossl_dtls_conn_wait_for_write(SSL *s)
+{
+ SSL_CONNECTION *sc = SSL_CONNECTION_FROM_SSL_ONLY(s);
+ DTLS_LISTENER *dl;
+ int fatal;
+
+ if (sc == NULL || sc->d1 == NULL || sc->d1->listener == NULL)
+ return 0;
+
+ dl = (DTLS_LISTENER *)sc->d1->listener;
+
+ ossl_crypto_mutex_lock(dl->mutex);
+ fatal = dl->fatal;
+ ossl_crypto_mutex_unlock(dl->mutex);
+ if (fatal)
+ return 0;
+
+ return ossl_dtls_block_until_ready(s, SSL_POLL_EVENT_W,
+ ossl_time_infinite(), /*bound_by_event_timeout=*/0);
+}
+
void ossl_dtls_listener_enter_blocking_section(SSL *s)
{
DTLS_LISTENER *dl;
diff --git a/ssl/record/methods/recmethod_local.h b/ssl/record/methods/recmethod_local.h
index db64892533..1b2e04e66a 100644
--- a/ssl/record/methods/recmethod_local.h
+++ b/ssl/record/methods/recmethod_local.h
@@ -393,6 +393,7 @@ struct ossl_record_layer_st {
OSSL_FUNC_rlayer_padding_fn *padding;
OSSL_FUNC_rlayer_get_urxe_packet_fn *get_urxe_packet;
OSSL_FUNC_rlayer_release_urxe_packet_fn *release_urxe_packet;
+ OSSL_FUNC_rlayer_block_for_write_fn *block_for_write;
size_t max_pipelines;
diff --git a/ssl/record/methods/tls_common.c b/ssl/record/methods/tls_common.c
index dc2b7facad..180993bf20 100644
--- a/ssl/record/methods/tls_common.c
+++ b/ssl/record/methods/tls_common.c
@@ -1334,6 +1334,9 @@ int tls_int_new_record_layer(OSSL_LIB_CTX *libctx, const char *propq, int vers,
case OSSL_FUNC_RLAYER_RELEASE_URXE_PACKET:
rl->release_urxe_packet = OSSL_FUNC_rlayer_release_urxe_packet(fns);
break;
+ case OSSL_FUNC_RLAYER_BLOCK_FOR_WRITE:
+ rl->block_for_write = OSSL_FUNC_rlayer_block_for_write(fns);
+ break;
default:
/* Just ignore anything we don't understand */
break;
@@ -1967,6 +1970,22 @@ int tls_retry_write_records(OSSL_RECORD_LAYER *rl)
*/
if (BIO_err_is_non_fatal(err)) {
ERR_pop_to_mark();
+
+ /*
+ * A connection in blocking mode waits for the socket to
+ * become writable and sends again, rather than reporting
+ * a retry. Nothing has been consumed, so the next time
+ * round the loop repeats this same send.
+ *
+ * Only listener based connections install this callback.
+ * Anything else either has a socket of its own to block
+ * in or is genuinely non-blocking, and keeps the
+ * behaviour below.
+ */
+ if (rl->block_for_write != NULL
+ && rl->block_for_write(rl->cbarg))
+ continue;
+
ret = OSSL_RECORD_RETURN_RETRY;
i = 0;
tmpwrit = 0;
diff --git a/ssl/record/rec_layer_s3.c b/ssl/record/rec_layer_s3.c
index 81092cc2a2..c24f467fab 100644
--- a/ssl/record/rec_layer_s3.c
+++ b/ssl/record/rec_layer_s3.c
@@ -1207,6 +1207,18 @@ static void rlayer_dtls_release_urxe_packet(void *cbarg, void *packet_handle)
if (s != NULL && s->d1 != NULL && s->d1->rx != NULL && urxe != NULL)
ossl_dtls_rx_release_urxe(s->d1->rx, urxe);
}
+
+static OSSL_FUNC_rlayer_block_for_write_fn rlayer_dtls_block_for_write;
+static int rlayer_dtls_block_for_write(void *cbarg)
+{
+ SSL_CONNECTION *s = cbarg;
+
+ if (s == NULL || s->d1 == NULL || s->d1->listener == NULL
+ || !ossl_dtls_blocking(SSL_CONNECTION_GET_SSL(s)))
+ return 0;
+
+ return ossl_dtls_conn_wait_for_write(SSL_CONNECTION_GET_SSL(s));
+}
#endif
static const OSSL_DISPATCH rlayer_dispatch[] = {
@@ -1217,6 +1229,7 @@ static const OSSL_DISPATCH rlayer_dispatch[] = {
#if !defined(OPENSSL_NO_DTLS) && !defined(OPENSSL_NO_SOCK)
{ OSSL_FUNC_RLAYER_GET_URXE_PACKET, (void (*)(void))rlayer_dtls_get_urxe_packet },
{ OSSL_FUNC_RLAYER_RELEASE_URXE_PACKET, (void (*)(void))rlayer_dtls_release_urxe_packet },
+ { OSSL_FUNC_RLAYER_BLOCK_FOR_WRITE, (void (*)(void))rlayer_dtls_block_for_write },
#endif
OSSL_DISPATCH_END
};
diff --git a/ssl/record/record.h b/ssl/record/record.h
index db01133d1d..7265173c65 100644
--- a/ssl/record/record.h
+++ b/ssl/record/record.h
@@ -211,4 +211,16 @@ OSSL_CORE_MAKE_FUNC(int, rlayer_get_urxe_packet, (void *cbarg, unsigned char **d
#define OSSL_FUNC_RLAYER_RELEASE_URXE_PACKET 6
OSSL_CORE_MAKE_FUNC(void, rlayer_release_urxe_packet, (void *cbarg, void *packet_handle))
+/*
+ * Callback for listener-based connections to wait until their shared socket can
+ * accept a datagram, for a connection which is in blocking mode. Such a
+ * connection cannot block in the socket itself, because the socket is shared
+ * with every other connection and is always non-blocking.
+ *
+ * Returns 1 if the send should be attempted again, or 0 to report the write as
+ * needing a retry in the usual way.
+ */
+#define OSSL_FUNC_RLAYER_BLOCK_FOR_WRITE 7
+OSSL_CORE_MAKE_FUNC(int, rlayer_block_for_write, (void *cbarg))
+
#endif /* !defined(OSSL_SSL_RECORD_RECORD_H) */
diff --git a/ssl/rio/poll_immediate.c b/ssl/rio/poll_immediate.c
index 6a622fc16a..18a47aab23 100644
--- a/ssl/rio/poll_immediate.c
+++ b/ssl/rio/poll_immediate.c
@@ -453,6 +453,7 @@ static int poll_translate(SSL_POLL_ITEM *items,
RIO_POLL_BUILDER *rpb,
OSSL_TIME *p_earliest_wakeup_deadline,
int *abort_blocking,
+ int bound_by_event_timeout,
size_t *p_result_count)
{
int ok = 1;
@@ -527,15 +528,22 @@ static int poll_translate(SSL_POLL_ITEM *items,
* Bound the wait by the DTLS retransmission timer,
* otherwise a poll with no timeout sleeps straight through
* the point at which we should be retransmitting.
+ *
+ * Unless the caller has told us not to. A waiter which
+ * cannot service the timer must not be woken by it: it
+ * would find the timeout still expired on the next wait,
+ * which reads as a zero deadline, and spin.
*/
- if (!SSL_get_event_timeout(ssl, &timeout, &is_infinite))
- FAIL_ITEM(i++); /* need to clean up this item too */
-
- if (!is_infinite)
- earliest_wakeup_deadline
- = ossl_time_min(earliest_wakeup_deadline,
- ossl_time_add(ossl_time_now(),
- ossl_time_from_timeval(timeout)));
+ if (bound_by_event_timeout) {
+ if (!SSL_get_event_timeout(ssl, &timeout, &is_infinite))
+ FAIL_ITEM(i++); /* need to clean up this item too */
+
+ if (!is_infinite)
+ earliest_wakeup_deadline
+ = ossl_time_min(earliest_wakeup_deadline,
+ ossl_time_add(ossl_time_now(),
+ ossl_time_from_timeval(timeout)));
+ }
} else {
ERR_raise_data(ERR_LIB_SSL, SSL_R_POLL_REQUEST_NOT_SUPPORTED,
@@ -585,6 +593,7 @@ static int poll_block(SSL_POLL_ITEM *items,
size_t num_items,
size_t stride,
OSSL_TIME user_deadline,
+ int bound_by_event_timeout,
size_t *p_result_count)
{
int ok = 0, abort_blocking = 0;
@@ -621,6 +630,7 @@ static int poll_block(SSL_POLL_ITEM *items,
if (!poll_translate(items, num_items, stride, &wctx, &rpb,
&earliest_wakeup_deadline,
&abort_blocking,
+ bound_by_event_timeout,
p_result_count))
goto out;
@@ -664,9 +674,16 @@ out:
* becoming readable says nothing about which connection the datagram is for -
* so the caller must re-test its own condition and wait again if needed.
*
+ * bound_by_event_timeout says whether the connection's own event timeout, which
+ * for DTLS is the retransmission timer, should shorten the wait. Pass 1 unless
+ * the caller is unable to service that timer when it fires: waking for a
+ * timeout nothing then handles leaves it expired, and an expired timeout reads
+ * as a zero deadline, so every later wait returns at once.
+ *
* Returns 1 if the wait completed and 0 on error.
*/
-int ossl_dtls_block_until_ready(SSL *ssl, uint64_t events, OSSL_TIME deadline)
+int ossl_dtls_block_until_ready(SSL *ssl, uint64_t events, OSSL_TIME deadline,
+ int bound_by_event_timeout)
{
SSL_POLL_ITEM item;
size_t result_count = 0;
@@ -676,7 +693,8 @@ int ossl_dtls_block_until_ready(SSL *ssl, uint64_t events, OSSL_TIME deadline)
item.events = events;
item.revents = 0;
- return poll_block(&item, 1, sizeof(item), deadline, &result_count);
+ return poll_block(&item, 1, sizeof(item), deadline, bound_by_event_timeout,
+ &result_count);
}
#endif /* OPENSSL_NO_DTLS */
#endif
@@ -832,7 +850,8 @@ int SSL_poll(SSL_POLL_ITEM *items,
*/
do_tick = 1;
#if !defined(OPENSSL_NO_QUIC) || !defined(OPENSSL_NO_DTLS)
- if (!poll_block(items, num_items, stride, deadline, &result_count)) {
+ if (!poll_block(items, num_items, stride, deadline,
+ /*bound_by_event_timeout=*/1, &result_count)) {
ok = 0;
goto out;
}
diff --git a/ssl/ssl_local.h b/ssl/ssl_local.h
index 524317b4c4..96561858fd 100644
--- a/ssl/ssl_local.h
+++ b/ssl/ssl_local.h
@@ -3098,11 +3098,13 @@ int ossl_dtls_conn_poll_events(SSL *s, uint64_t events, int do_tick,
uint64_t *revents);
void ossl_dtls_listener_enter_blocking_section(SSL *s);
void ossl_dtls_listener_leave_blocking_section(SSL *s);
-int ossl_dtls_block_until_ready(SSL *ssl, uint64_t events, OSSL_TIME deadline);
+int ossl_dtls_block_until_ready(SSL *ssl, uint64_t events, OSSL_TIME deadline,
+ int bound_by_event_timeout);
int ossl_dtls_blocking(const SSL *s);
int ossl_dtls_set_blocking_mode(SSL *s, int blocking);
int ossl_dtls_get_blocking_mode(const SSL *s);
int ossl_dtls_conn_wait_for_datagram(SSL *s);
+int ossl_dtls_conn_wait_for_write(SSL *s);
int ossl_dtls_tick(DTLS_LISTENER *dl);
/* DTLS Listener internal cookie callbacks */
diff --git a/test/dtlsssllistenertest.c b/test/dtlsssllistenertest.c
index 9bc395f359..a1e6c5ded9 100644
--- a/test/dtlsssllistenertest.c
+++ b/test/dtlsssllistenertest.c
@@ -5719,6 +5719,209 @@ end:
return testresult;
}
+/*
+ * State for the filter BIO below.
+ */
+struct failing_send_data {
+ int fails_remaining; /* sends still to be rejected */
+ int sends; /* sends attempted through the filter */
+};
+
+static long failing_send_ctrl(BIO *bio, int cmd, long num, void *ptr)
+{
+ BIO *next = BIO_next(bio);
+
+ if (next == NULL)
+ return 0;
+
+ if (cmd == BIO_CTRL_DUP)
+ return 0L;
+
+ /*
+ * Everything else, the poll descriptors in particular, has to reach the
+ * socket underneath: the blocking write waits on the descriptor this
+ * returns.
+ */
+ return BIO_ctrl(next, cmd, num, ptr);
+}
+
+static int failing_send_sendmmsg(BIO *bio, BIO_MSG *msg, size_t stride,
+ size_t num_msg, uint64_t flags, size_t *msgs_processed)
+{
+ struct failing_send_data *data = BIO_get_data(bio);
+ BIO *next = BIO_next(bio);
+
+ if (data == NULL || next == NULL)
+ return 0;
+
+ data->sends++;
+
+ if (data->fails_remaining > 0) {
+ data->fails_remaining--;
+ *msgs_processed = 0;
+ /*
+ * BIO_err_is_non_fatal() accepts this, so the record layer treats the
+ * send as one to be attempted again rather than as an error.
+ */
+ ERR_raise(ERR_LIB_BIO, BIO_R_NON_FATAL);
+ return 0;
+ }
+
+ return BIO_sendmmsg(next, msg, stride, num_msg, flags, msgs_processed);
+}
+
+/* Choose a sufficiently large type likely to be unused for this custom BIO */
+#define BIO_TYPE_FAILING_SEND_FILTER (0x83 | BIO_TYPE_FILTER)
+
+static BIO_METHOD *method_failing_send = NULL;
+
+/* Note: Not thread safe! */
+static const BIO_METHOD *bio_f_failing_send_filter(void)
+{
+ if (method_failing_send == NULL) {
+ method_failing_send = BIO_meth_new(BIO_TYPE_FAILING_SEND_FILTER,
+ "Failing datagram send filter");
+ if (method_failing_send == NULL
+ || !BIO_meth_set_ctrl(method_failing_send, failing_send_ctrl)
+ || !BIO_meth_set_sendmmsg(method_failing_send,
+ failing_send_sendmmsg))
+ return NULL;
+ }
+ return method_failing_send;
+}
+
+/*
+ * Test that a write on a blocking listener connection waits for the socket and
+ * sends again, rather than reporting that it needs to be retried.
+ *
+ * A datagram which cannot be sent is normally dropped, which is reasonable for
+ * an unreliable transport but is not what an application asking for blocking
+ * writes expects: it gets no data sent and a WANT_WRITE it did not ask to have
+ * to handle. The listener's socket is shared and always non-blocking, so there
+ * is nothing for such a write to block in by itself.
+ *
+ * A loopback socket's send buffer does not fill, so a filter BIO supplies the
+ * transient failure instead. Only one send is rejected: the retry then goes
+ * through, and the client is read to confirm the datagram was really sent
+ * rather than merely reported as sent.
+ *
+ * The handshake runs with the listener non-blocking, so this test drives both
+ * ends from the one thread as the others here do, and only the connection is
+ * switched to blocking, for the write.
+ */
+static int test_dtls_blocking_write(void)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *listener = NULL, *clientssl = NULL, *serverssl = NULL;
+ BIO_ADDR *server_addr = NULL;
+ BIO *sockbio = NULL, *filter = NULL;
+ struct failing_send_data data;
+ int server_fd = -1, client_fd = -1;
+ int testresult = 0;
+ char buf[256];
+ size_t written = 0, readbytes = 0;
+ int i, ret = -1;
+
+ memset(&data, 0, sizeof(data));
+
+ if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+ DTLS_client_method(), DTLS1_VERSION, 0, &sctx, &cctx, cert,
+ privkey)))
+ goto end;
+
+ if (!TEST_true(create_dtls_listener(sctx, SSL_LISTENER_FLAG_SINGLE_THREAD,
+ &listener, &server_addr, &server_fd)))
+ goto end;
+
+ /*
+ * Insert the filter in front of the listener's socket for writes only,
+ * leaving reads to reach the socket directly. The listener holds a
+ * reference for each direction, so the filter chain needs one of its own.
+ */
+ if (!TEST_ptr(sockbio = SSL_get_wbio(listener))
+ || !TEST_ptr(filter = BIO_new(bio_f_failing_send_filter())))
+ goto end;
+
+ BIO_set_data(filter, &data);
+ BIO_set_init(filter, 1);
+
+ if (!TEST_true(BIO_up_ref(sockbio))) {
+ BIO_free(filter);
+ goto end;
+ }
+
+ BIO_push(filter, sockbio);
+ SSL_set0_wbio(listener, filter); /* the listener owns the chain now */
+ filter = NULL;
+
+ if (!TEST_true(create_dtls_client_for_addr(cctx, server_addr, &clientssl,
+ &client_fd)))
+ goto end;
+
+ if (!drive_until_connection_queued(listener, clientssl)
+ || !TEST_ptr(serverssl = SSL_accept_connection(listener,
+ SSL_ACCEPT_CONNECTION_NO_BLOCK)))
+ goto end;
+
+ if (!TEST_true(create_ssl_connection(serverssl, clientssl, SSL_ERROR_NONE)))
+ goto end;
+
+ /*
+ * Everything up to here, including any post-handshake traffic, has gone
+ * through the filter untouched. Reject the next send only.
+ */
+ if (!TEST_true(SSL_set_blocking_mode(serverssl, 1))
+ || !TEST_int_eq(SSL_get_blocking_mode(serverssl), 1))
+ goto end;
+
+ data.sends = 0;
+ data.fails_remaining = 1;
+
+ if (!TEST_true(SSL_write_ex(serverssl, "msg", 3, &written))
+ || !TEST_size_t_eq(written, 3))
+ goto end;
+
+ /*
+ * The send really was rejected, and was retried rather than reported: the
+ * count proves a second attempt was made, which is the whole behaviour
+ * under test. Without it, a write which never reached the filter at all
+ * would look the same as one which was retried.
+ */
+ if (!TEST_int_eq(data.fails_remaining, 0)
+ || !TEST_int_ge(data.sends, 2))
+ goto end;
+
+ /* The datagram reached the client, so nothing was dropped on the way. */
+ for (i = 0; i < 20; i++) {
+ ret = SSL_read_ex(clientssl, buf, sizeof(buf), &readbytes);
+ if (ret == 1)
+ break;
+ if (!TEST_int_eq(SSL_get_error(clientssl, ret), SSL_ERROR_WANT_READ))
+ goto end;
+ OSSL_sleep(10);
+ }
+
+ if (!TEST_int_eq(ret, 1)
+ || !TEST_mem_eq(buf, readbytes, "msg", 3))
+ goto end;
+
+ testresult = 1;
+end:
+ SSL_free(serverssl);
+ SSL_free(clientssl);
+ SSL_free(listener);
+ BIO_ADDR_free(server_addr);
+ if (server_fd >= 0)
+ BIO_closesocket(server_fd);
+ if (client_fd >= 0)
+ BIO_closesocket(client_fd);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+ BIO_meth_free(method_failing_send);
+ method_failing_send = NULL;
+ return testresult;
+}
+
OPT_TEST_DECLARE_USAGE("certfile privkeyfile\n")
int setup_tests(void)
@@ -5804,6 +6007,7 @@ int setup_tests(void)
ADD_TEST(test_dtls_blocking_mode);
ADD_TEST(test_dtls_blocking_mode_failed_set_is_inert);
ADD_TEST(test_dtls_accept_wait_requires_mode_and_flag);
+ ADD_TEST(test_dtls_blocking_write);
/* SSL object ownership tests (run with ASAN to detect leaks/double-frees) */
ADD_TEST(test_ssl_ownership_pending_conn_leak);