Commit 8628bb9f21 for openssl.org
commit 8628bb9f21365f3c448b3e08cef337992027587c
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date: Mon Oct 12 12:24:08 2026 +0900
DTLS listener: make max_pending_conns a combined admission budget
The pending-connection cap only applied to pending_conns, the table
of connections still doing their handshake: the moment a connection passed
cookie validation it was moved onto the accept queue (incoming_connections)
with no length check and no expiry. A reachable peer completing
the ordinary cookie exchange from many source endpoints could accumulate
an unbounded, unexpiring queue of listener-owned SSL objects.
This is the same missing admission bound as the QUIC listener admission
check fixed in [1]; the DTLS listener is new in 4.1 and not in any final
release.
Mirror the QUIC fix: enforce the bound at admission, in the packet
handler, before any per-connection state is allocated.
SSL_VALUE_DTLS_LISTENER_MAX_PENDING_CONNS is now a single admission budget
covering both listener-owned tables, pending plus accept queue,
so listener-owned connections are bounded at N in total, including
under SSL_LISTENER_FLAG_NO_VALIDATE. Moving a connection from one table
to the other leaves the total unchanged, so a validated connection
can always be queued: none is discarded, and no handshake work (cookie
exchange, server flight) is spent on an attempt that cannot be admitted.
A peer whose datagram is dropped while the budget is exhausted retransmits
its ClientHello and is admitted once the application drains the queue;
measured on real UDP, such a client completes its handshake about one
retransmit interval after the drain.
This also aligns the DTLS knob's meaning with the QUIC one ("not yet
accepted"), which it was modelled on in [2].
Accept-queue entries still do not expire; a full queue stays full until
the application drains it. With the budget, an attacker can pin at most N
accept-queue slots, and only after proving return routability - spoofed
ClientHellos can still occupy pending slots until the pending timeout
reaps them, as before this change - so what remains is a bounded
availability concern, not memory exhaustion.
[1] https://github.com/openssl/openssl/pull/32052
[2] https://github.com/openssl/openssl/pull/31980
Resolves: https://github.com/openssl/openssl/issues/33172
Assisted-by: Claude Code:moonshotai/kimi-k3
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Reviewed-by: Ryan Hooper <ryanh@openssl.foundation>
Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org>
Merge-date: Wed Oct 14 10:54:55 2026
Merged-from: https://github.com/openssl/openssl/pull/33197
diff --git a/doc/man3/SSL_get_value_uint.pod b/doc/man3/SSL_get_value_uint.pod
index 09d686d9d6..8e953a5c69 100644
--- a/doc/man3/SSL_get_value_uint.pod
+++ b/doc/man3/SSL_get_value_uint.pod
@@ -401,20 +401,20 @@ macros). Using any other value class fails with an error.
=item B<SSL_VALUE_DTLS_LISTENER_MAX_PENDING_CONNS> (DTLS listener object)
-Generic value. Limits the number of pending
-(handshake-in-progress) connections a DTLS listener created by
-L<SSL_new_listener(3)> will track. When the limit is reached, datagrams
-from novel peers are silently dropped; existing pending connections may
-still complete their handshake and enter the accept queue, and stale
-entries age out via B<SSL_VALUE_DTLS_LISTENER_PENDING_TIMEOUT>.
-
-The default limit is 256. Setting the value to 0 is rejected: the limit is an
-upper bound on tracked pending connections, so a cap of 0 would reject every
-incoming connection and leave the listener unable to accept anything. Any
-nonzero value is accepted (values larger than B<SIZE_MAX> are clamped to
-B<SIZE_MAX>); a sufficiently large value therefore makes the limit effectively
-unbounded, so keeping it at a sensible value is what protects the listener
-against Denial of Service from spoofed-source UDP floods.
+Generic value. Bounds the number of connections a DTLS listener created by
+L<SSL_new_listener(3)> owns at any one time: pending (handshake-in-progress)
+connections plus validated connections waiting in the accept queue for
+L<SSL_accept_connection(3)>. When the limit is reached, datagrams from novel
+peers are silently discarded before any connection state is allocated; the
+peer retransmits its ClientHello and is admitted once a slot frees up. A
+connection returned by L<SSL_accept_connection(3)> is no longer counted.
+
+The default limit is 256. Setting the value to 0 is rejected: a cap of 0
+would reject every incoming connection and leave the listener unable to
+accept anything. Lowering the value does not evict connections the listener
+already owns; it only affects later admission. Stale pending entries age out
+via B<SSL_VALUE_DTLS_LISTENER_PENDING_TIMEOUT>; accept-queue entries do not
+expire.
=item B<SSL_VALUE_DTLS_LISTENER_PENDING_TIMEOUT> (DTLS listener object)
@@ -422,7 +422,8 @@ Generic value. Sets the maximum time in milliseconds that a
pending (handshake-in-progress) DTLS connection may remain in the
listener's pending table before being reaped. Expired pending
connections are silently discarded, freeing a slot in the
-B<SSL_VALUE_DTLS_LISTENER_MAX_PENDING_CONNS> quota.
+B<SSL_VALUE_DTLS_LISTENER_MAX_PENDING_CONNS> budget. The timeout applies
+only to the pending table; accept-queue entries do not expire.
The default is 30000 (30 seconds). Setting the value to 0 is rejected: a zero
timeout will remove all pending connections on the next tick, before the
diff --git a/doc/man3/SSL_new_listener.pod b/doc/man3/SSL_new_listener.pod
index 62e43b8ae3..95565603fc 100644
--- a/doc/man3/SSL_new_listener.pod
+++ b/doc/man3/SSL_new_listener.pod
@@ -199,12 +199,23 @@ ultimately be returned may be different.
For DTLS listeners, several per-listener tunables are configured through
L<SSL_get_value_uint(3)> / L<SSL_set_value_uint(3)>:
-B<SSL_VALUE_DTLS_LISTENER_MAX_PENDING_CONNS> (pending-connection cap, default
-256, cannot be disabled), B<SSL_VALUE_DTLS_LISTENER_PENDING_TIMEOUT> (millisecond
+B<SSL_VALUE_DTLS_LISTENER_MAX_PENDING_CONNS> (cap on the connections the
+listener owns at any one time - those still doing their handshake plus
+those waiting in the accept queue - default 256, cannot be disabled),
+B<SSL_VALUE_DTLS_LISTENER_PENDING_TIMEOUT> (millisecond
reap timeout, default 30000, B<UINT64_MAX> disables) and
B<SSL_VALUE_DTLS_LISTENER_MAX_DGRAM_SIZE> (maximum received datagram size in
bytes, default 2000). See L<SSL_get_value_uint(3)> for details.
+Independently of those tunables, a DTLS listener bounds the datagrams queued
+for a connection that the application is not reading: each connection's
+receive queue is limited to a fixed internal number of datagrams, and once
+it is full each further datagram evicts the oldest queued one. Every queued
+datagram holds a buffer of at least B<SSL_VALUE_DTLS_LISTENER_MAX_DGRAM_SIZE>
+bytes whatever its length, so the receive memory held per connection scales
+with that value. DTLS permits datagram loss, so the connection is not
+otherwise affected.
+
Currently, listener SSL objects are supported for QUIC server usage via
L<OSSL_QUIC_server_method(3)>, QUIC client-only usage via
L<OSSL_QUIC_client_method(3)> or L<OSSL_QUIC_client_thread_method(3)> (see
@@ -270,7 +281,12 @@ Disables all address validation. The listener will not send HelloVerifyRequest
faster but provides no protection against amplification attacks, and is not
recommended for use in untrusted network environments. If both this flag and
B<SSL_LISTENER_FLAG_ADDRESS_VALIDATION> are specified, the listener fails safe
-and performs address validation.
+and performs address validation. Note that in this mode an accept-queue entry
+is created from the connection's first datagram rather than from a
+cookie-validated handshake; the B<SSL_VALUE_DTLS_LISTENER_MAX_PENDING_CONNS>
+budget limits how many such entries can exist, but cannot stop the budget
+from being occupied by junk, so the flag remains dangerous in untrusted
+environments.
=item B<SSL_LISTENER_FLAG_SINGLE_THREAD>
diff --git a/doc/man7/ossl-guide-dtlsv13.pod b/doc/man7/ossl-guide-dtlsv13.pod
index fc054ff756..8a250a23cc 100644
--- a/doc/man7/ossl-guide-dtlsv13.pod
+++ b/doc/man7/ossl-guide-dtlsv13.pod
@@ -192,7 +192,8 @@ See L<SSL_set_blocking_mode(3)> for full details.
The following tunables are available via L<SSL_get_value_uint(3)> /
L<SSL_set_value_uint(3)>: B<SSL_VALUE_DTLS_LISTENER_MAX_PENDING_CONNS>
-(pending-connection cap, default 256), B<SSL_VALUE_DTLS_LISTENER_PENDING_TIMEOUT>
+(cap on listener-owned connections, default 256),
+B<SSL_VALUE_DTLS_LISTENER_PENDING_TIMEOUT>
(pending connection timeout in milliseconds, default 30000; pending connections
that do not complete their handshake within this period are automatically cleaned
up), and B<SSL_VALUE_DTLS_LISTENER_MAX_DGRAM_SIZE> (maximum received datagram size
diff --git a/ssl/d1_lib.c b/ssl/d1_lib.c
index f0247525c5..d42ad2e52c 100644
--- a/ssl/d1_lib.c
+++ b/ssl/d1_lib.c
@@ -1443,7 +1443,9 @@ static void dtls_listener_packet_handler(DGRAM_URXE *urxe, void *arg)
* is reached. The LHASH item count is O(1) and the check does not
* need a conn_ssl.
*/
- if (ossl_dgram_conn_lookup_num_items(dl->pending_conns) >= dl->max_pending_conns)
+ if (ossl_dgram_conn_lookup_num_items(dl->pending_conns)
+ + (size_t)sk_SSL_num(dl->incoming_connections)
+ >= dl->max_pending_conns)
goto release;
conn_ssl = dtls_listener_create_conn_ssl(dl, &urxe->peer);
@@ -2852,11 +2854,14 @@ int ossl_dtls_listener_set_override_now_cb(SSL *s,
*
* Supported (id) values:
* SSL_VALUE_DTLS_LISTENER_MAX_PENDING_CONNS
- * Current cap on the number of pending (handshake-in-progress)
- * connections the listener will track.
+ * Admission budget: the total connections the listener owns,
+ * counting both the pending (handshake-in-progress) table and the
+ * accept queue (validated connections awaiting
+ * SSL_accept_connection). Accepted connections are not counted.
* SSL_VALUE_DTLS_LISTENER_PENDING_TIMEOUT
* Current reap timeout for pending connections, in milliseconds.
- * UINT64_MAX means "infinite / disabled".
+ * UINT64_MAX means "infinite / disabled". Accept-queue entries do
+ * not expire.
* SSL_VALUE_DTLS_LISTENER_MAX_DGRAM_SIZE
* Maximum size in bytes of a datagram the listener will receive.
*
@@ -2931,7 +2936,9 @@ int ossl_dtls_get_value_uint(SSL *s, uint32_t class_, uint32_t id, uint64_t *val
* SSL_VALUE_DTLS_LISTENER_MAX_PENDING_CONNS
* value == 0 is rejected (a zero cap would reject every incoming
* connection). Values larger than SIZE_MAX are clamped to SIZE_MAX
- * to avoid silent truncation on 32-bit builds.
+ * to avoid silent truncation on 32-bit builds. Lowering the value
+ * below the current number of listener-owned connections does not
+ * evict any of them; it only affects later admission.
* SSL_VALUE_DTLS_LISTENER_PENDING_TIMEOUT
* Interpreted as milliseconds. value == 0 is rejected. UINT64_MAX is
* treated as "infinite / disabled".
diff --git a/ssl/ssl_local.h b/ssl/ssl_local.h
index d7d4723818..671f49ffc5 100644
--- a/ssl/ssl_local.h
+++ b/ssl/ssl_local.h
@@ -2457,7 +2457,8 @@ typedef struct dtls_listener_st {
OSSL_TIME pending_timeout;
/*
- * Maximum number of pending connections allowed.
+ * Maximum number of listener-owned connections allowed, counting both
+ * the pending table and the accept queue.
* When this limit is reached, new connection attempts are rejected.
*
* Default: DTLS_LISTENER_DEFAULT_MAX_PENDING_CONNS
diff --git a/test/dtlsssllistenertest.c b/test/dtlsssllistenertest.c
index 9e09c22d49..71ffa8dabe 100644
--- a/test/dtlsssllistenertest.c
+++ b/test/dtlsssllistenertest.c
@@ -6015,6 +6015,359 @@ end:
}
#endif /* OPENSSL_NO_DTLS1_2 */
+#if !defined(OPENSSL_NO_DTLS1_2) || !defined(OPENSSL_NO_DTLS1_3)
+/* Tick the listener's incoming-connection pump once, without blocking. */
+static void listener_tick(SSL *listener)
+{
+ SSL_POLL_ITEM poll_item;
+ struct timeval poll_timeout;
+ size_t poll_result = 0;
+
+ poll_item.desc.type = BIO_POLL_DESCRIPTOR_TYPE_SSL;
+ poll_item.desc.value.ssl = listener;
+ poll_item.events = SSL_POLL_EVENT_IC;
+ poll_item.revents = 0;
+ poll_timeout.tv_sec = 0;
+ poll_timeout.tv_usec = 0;
+ /* A failed poll means the tick did not run; treat it as no progress. */
+ if (!SSL_poll(&poll_item, 1, sizeof(poll_item), &poll_timeout, 0,
+ &poll_result))
+ ERR_clear_error();
+}
+
+/* Step one client's handshake, tolerating the expected WANT_READ/WANT_WRITE. */
+static int client_step(SSL *client)
+{
+ int ret = SSL_connect(client);
+ int err = SSL_get_error(client, ret);
+
+ if (ret <= 0 && err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE)
+ return 0;
+ ERR_clear_error();
+ return 1;
+}
+
+/*
+ * Message-callback observation harness: counts outgoing ClientHellos of
+ * the "active" client, so a test can confirm it kept retransmitting
+ * while its datagrams were refused.
+ */
+typedef struct {
+ SSL *active_client; /* client currently observed, or NULL */
+ int active_client_ch; /* outgoing ClientHellos seen for it */
+} QUEUE_CAP_OBS;
+
+static void queue_cap_msg_cb(int write_p, int version, int content_type,
+ const void *buf, size_t len, SSL *ssl, void *arg)
+{
+ QUEUE_CAP_OBS *obs = arg;
+
+ (void)version;
+ if (!write_p || content_type != SSL3_RT_HANDSHAKE || buf == NULL || len < 1)
+ return;
+
+ if (ssl == obs->active_client
+ && *(const unsigned char *)buf == SSL3_MT_CLIENT_HELLO)
+ obs->active_client_ch++;
+}
+
+/*
+ * Accelerated DTLS retransmit policy for these tests: 100 ms initial,
+ * doubling, capped at 6 s, so retransmit-driven phases stay fast.
+ */
+static unsigned int queue_cap_timer_cb(SSL *ssl, unsigned int us)
+{
+ if (us == 0)
+ return 100000;
+ return us * 2 > 6000000 ? 6000000 : us * 2;
+}
+
+/*
+ * Drive one client through the cookie exchange until the accept queue
+ * reaches |target| entries.
+ */
+static int drive_client_to_promotion(SSL *listener, SSL *client,
+ size_t target)
+{
+ int k;
+
+ for (k = 0; k < 600; k++) {
+ if (!client_step(client))
+ return 0;
+ listener_tick(listener);
+ if (SSL_get_accept_connection_queue_len(listener) >= target)
+ return 1;
+ OSSL_sleep(10);
+ }
+ TEST_error("client was not queued within the iteration budget");
+ return 0;
+}
+
+#define ACCEPT_CAP_TEST_LIMIT ((size_t)1)
+
+/*
+ * Regression test: SSL_VALUE_DTLS_LISTENER_MAX_PENDING_CONNS is a single
+ * admission budget over both listener-owned tables, so the accept queue
+ * is bounded. Client A fills the queue; client B, driven while full, must
+ * be refused at admission with no state spent on it; after a drain, B is
+ * admitted and completes its handshake: a full queue must not strand a
+ * legitimate client. Runs for both DTLS versions.
+ */
+static int test_dtls_listener_accept_queue_capped_int(int version)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *listener = NULL, *accepted = NULL;
+ SSL *client_a = NULL, *client_b = NULL;
+ int fd_a = -1, fd_b = -1;
+ BIO_ADDR *server_addr = NULL;
+ int server_fd = -1, k;
+ int testresult = 0;
+ DTLS_LISTENER *dl;
+ QUEUE_CAP_OBS obs;
+
+ memset(&obs, 0, sizeof(obs));
+
+ if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+ DTLS_client_method(), version, version,
+ &sctx, &cctx, cert, privkey)))
+ goto err;
+
+ SSL_CTX_set_msg_callback(cctx, queue_cap_msg_cb);
+ SSL_CTX_set_msg_callback_arg(cctx, &obs);
+
+ if (!TEST_true(create_dtls_listener(sctx,
+ SSL_LISTENER_FLAG_SINGLE_THREAD,
+ &listener, &server_addr, &server_fd)))
+ goto err;
+
+ /* One listener-owned connection at a time, pending or queued. */
+ if (!TEST_true(SSL_set_generic_value_uint(listener,
+ SSL_VALUE_DTLS_LISTENER_MAX_PENDING_CONNS, ACCEPT_CAP_TEST_LIMIT)))
+ goto err;
+
+ dl = (DTLS_LISTENER *)listener;
+
+ if (!TEST_true(create_dtls_client_for_addr(cctx, server_addr,
+ &client_a, &fd_a))
+ || !TEST_true(create_dtls_client_for_addr(cctx, server_addr,
+ &client_b, &fd_b)))
+ goto err;
+ /* Bound the runtime of the retransmit-driven phases below. */
+ DTLS_set_timer_cb(client_a, queue_cap_timer_cb);
+ DTLS_set_timer_cb(client_b, queue_cap_timer_cb);
+
+ /* Phase 1: client A fills the accept queue to the cap. */
+ if (!drive_client_to_promotion(listener, client_a, ACCEPT_CAP_TEST_LIMIT))
+ goto err;
+ /*
+ * The pending table has no public accessor by design (the cap is
+ * silent), so read its count directly, as the existing pending-cap
+ * tests do; safe in these single-threaded tests.
+ */
+ if (!TEST_size_t_eq(SSL_get_accept_connection_queue_len(listener),
+ ACCEPT_CAP_TEST_LIMIT)
+ || !TEST_size_t_eq(ossl_dgram_conn_lookup_num_items(
+ dl->pending_conns),
+ 0))
+ goto err;
+
+ /*
+ * Phase 2: with the queue full, B must be refused at admission.
+ * B keeps retransmitting on its accelerated timer; every tick must
+ * show no pending connection created and the queue still at the cap.
+ */
+ obs.active_client = client_b;
+ obs.active_client_ch = 0;
+ for (k = 0; k < 400 && obs.active_client_ch < 3; k++) {
+ if (!client_step(client_b))
+ goto err;
+ listener_tick(listener);
+ if (!TEST_size_t_eq(SSL_get_accept_connection_queue_len(listener),
+ ACCEPT_CAP_TEST_LIMIT)
+ || !TEST_size_t_eq(ossl_dgram_conn_lookup_num_items(
+ dl->pending_conns),
+ 0))
+ goto err;
+ OSSL_sleep(5);
+ }
+ obs.active_client = NULL;
+ if (!TEST_int_ge(obs.active_client_ch, 2)) {
+ TEST_error("client B did not retransmit its ClientHello");
+ goto err;
+ }
+
+ /*
+ * Phase 3: drain the queue. B's retransmitted ClientHello is now
+ * admitted, B completes the cookie exchange and is queued.
+ */
+ if (!TEST_ptr(accepted = SSL_accept_connection(listener,
+ SSL_ACCEPT_CONNECTION_NO_BLOCK)))
+ goto err;
+ SSL_free(accepted);
+ accepted = NULL;
+ if (!drive_client_to_promotion(listener, client_b, ACCEPT_CAP_TEST_LIMIT))
+ goto err;
+
+ /* Phase 4: the listener is usable afterwards; B's handshake completes. */
+ if (!TEST_ptr(accepted = SSL_accept_connection(listener,
+ SSL_ACCEPT_CONNECTION_NO_BLOCK)))
+ goto err;
+ if (!TEST_true(create_ssl_connection(accepted, client_b,
+ SSL_ERROR_NONE)))
+ goto err;
+ if (!TEST_int_eq(SSL_version(accepted), version))
+ goto err;
+
+ testresult = 1;
+
+err:
+ SSL_free(accepted);
+ SSL_free(client_a);
+ if (fd_a >= 0)
+ BIO_closesocket(fd_a);
+ SSL_free(client_b);
+ if (fd_b >= 0)
+ BIO_closesocket(fd_b);
+ SSL_free(listener);
+ BIO_ADDR_free(server_addr);
+ if (server_fd >= 0)
+ BIO_closesocket(server_fd);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+ return testresult;
+}
+
+static int test_dtls_listener_accept_queue_capped(void)
+{
+ int ok = 1;
+
+#ifndef OPENSSL_NO_DTLS1_2
+ if (!test_dtls_listener_accept_queue_capped_int(DTLS1_2_VERSION))
+ ok = 0;
+#endif
+#ifndef OPENSSL_NO_DTLS1_3
+ if (!test_dtls_listener_accept_queue_capped_int(DTLS1_3_VERSION))
+ ok = 0;
+#endif
+ return ok;
+}
+
+/*
+ * Regression test for SSL_LISTENER_FLAG_NO_VALIDATE: a connection is
+ * queued on its first datagram, so only the admission budget bounds the
+ * accept queue there. With the budget at ACCEPT_CAP_TEST_LIMIT, at most
+ * one listener-owned connection may exist at a time, and a drain must
+ * admit a waiting client.
+ */
+static int test_dtls_listener_accept_queue_novalidate_capped(void)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *listener = NULL, *accepted = NULL;
+ SSL *clients[3] = { NULL };
+ int client_fds[3] = { -1, -1, -1 };
+ BIO_ADDR *server_addr = NULL;
+ int server_fd = -1;
+ int testresult = 0;
+ int i, k;
+ DTLS_LISTENER *dl;
+#if !defined(OPENSSL_NO_DTLS1_3)
+ const int version = DTLS1_3_VERSION;
+#else
+ const int version = DTLS1_2_VERSION;
+#endif
+
+ if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+ DTLS_client_method(), version, version,
+ &sctx, &cctx, cert, privkey)))
+ goto err;
+
+ if (!TEST_true(create_dtls_listener(sctx,
+ SSL_LISTENER_FLAG_SINGLE_THREAD | SSL_LISTENER_FLAG_NO_VALIDATE,
+ &listener, &server_addr, &server_fd)))
+ goto err;
+
+ if (!TEST_true(SSL_set_generic_value_uint(listener,
+ SSL_VALUE_DTLS_LISTENER_MAX_PENDING_CONNS, ACCEPT_CAP_TEST_LIMIT)))
+ goto err;
+
+ dl = (DTLS_LISTENER *)listener;
+
+ for (i = 0; i < 3; i++) {
+ if (!TEST_true(create_dtls_client_for_addr(cctx, server_addr,
+ &clients[i], &client_fds[i])))
+ goto err;
+ DTLS_set_timer_cb(clients[i], queue_cap_timer_cb);
+ }
+
+ /*
+ * Drive all three clients: one's first datagram queues its connection,
+ * the other two are refused; the total must never exceed the budget.
+ */
+ for (k = 0; k < 100; k++) {
+ for (i = 0; i < 3; i++)
+ if (!client_step(clients[i]))
+ goto err;
+ listener_tick(listener);
+ if (!TEST_size_t_le(SSL_get_accept_connection_queue_len(listener)
+ + ossl_dgram_conn_lookup_num_items(dl->pending_conns),
+ ACCEPT_CAP_TEST_LIMIT))
+ goto err;
+ OSSL_sleep(2);
+ }
+ if (!TEST_size_t_eq(SSL_get_accept_connection_queue_len(listener),
+ ACCEPT_CAP_TEST_LIMIT)
+ || !TEST_size_t_eq(ossl_dgram_conn_lookup_num_items(
+ dl->pending_conns),
+ 0))
+ goto err;
+
+ /* Drain: the next retransmission from another client is admitted. */
+ if (!TEST_ptr(accepted = SSL_accept_connection(listener,
+ SSL_ACCEPT_CONNECTION_NO_BLOCK)))
+ goto err;
+ SSL_free(accepted);
+ accepted = NULL;
+
+ /*
+ * Client 0 was just accepted and freed; stepping only the other two
+ * must refill the queue without exceeding the budget.
+ */
+ for (k = 0; k < 200
+ && SSL_get_accept_connection_queue_len(listener) < ACCEPT_CAP_TEST_LIMIT;
+ k++) {
+ for (i = 1; i < 3; i++)
+ if (!client_step(clients[i]))
+ goto err;
+ listener_tick(listener);
+ if (!TEST_size_t_le(SSL_get_accept_connection_queue_len(listener)
+ + ossl_dgram_conn_lookup_num_items(dl->pending_conns),
+ ACCEPT_CAP_TEST_LIMIT))
+ goto err;
+ OSSL_sleep(2);
+ }
+ if (!TEST_size_t_eq(SSL_get_accept_connection_queue_len(listener),
+ ACCEPT_CAP_TEST_LIMIT))
+ goto err;
+
+ testresult = 1;
+
+err:
+ SSL_free(accepted);
+ for (i = 0; i < 3; i++) {
+ SSL_free(clients[i]);
+ if (client_fds[i] >= 0)
+ BIO_closesocket(client_fds[i]);
+ }
+ SSL_free(listener);
+ BIO_ADDR_free(server_addr);
+ if (server_fd >= 0)
+ BIO_closesocket(server_fd);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+ return testresult;
+}
+#endif /* !OPENSSL_NO_DTLS1_2 || !OPENSSL_NO_DTLS1_3 */
+
OPT_TEST_DECLARE_USAGE("certfile privkeyfile\n")
int setup_tests(void)
@@ -6151,6 +6504,14 @@ int setup_tests(void)
#ifndef OPENSSL_NO_DTLS1_2
ADD_TEST(test_dtls_listener_rx_queue_drops_oldest);
#endif
+#if !defined(OPENSSL_NO_DTLS1_2) || !defined(OPENSSL_NO_DTLS1_3)
+ /*
+ * Accept-queue bound regression tests; the first runs every enabled
+ * DTLS version, the NO_VALIDATE one runs a single version.
+ */
+ ADD_TEST(test_dtls_listener_accept_queue_capped);
+ ADD_TEST(test_dtls_listener_accept_queue_novalidate_capped);
+#endif
return 1;
}