Commit 45e53df37e for openssl.org

commit 45e53df37e1b521c80c377cfee4c709ab832c605
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date:   Thu Aug 27 07:44:04 2026 +0900

    QUIC: complete SSL_listen_ex() hierarchy adoption

    Treat domain flags as a property of the QUIC object hierarchy so
    explicitly configured domains remain effective for listeners, accepted
    connections and their streams.

    Keep the queued channel in place until TLS attachment is committed,
    configure deferred qlog state before the channel runs, install the
    listener message callbacks, and reset the adopted shell to
    accepted-connection mode, blocking and event-handling defaults.

    Extend the public API regression to cover explicit-domain ancestry,
    differing context flags and modes, callback identity, stream
    inheritance, and listener lifetime.

    Assisted-by: Codex:gpt-5.6-sol
    Reviewed-by: Neil Horman <nhorman@openssl.org>
    Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
    Merge-date: Tue Sep  1 14:19:18 2026
    Merged-from: https://github.com/openssl/openssl/pull/32491

diff --git a/CHANGES.md b/CHANGES.md
index c8e3d7ebea..5ed4b32026 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -92,9 +92,16 @@ OpenSSL 4.1

    *Ryan Hooper*

- * Fixed `SSL_listen_ex()` to return a usable QUIC connection and to preserve
-   queued connections on allocation failure. Invalid arguments and internal
-   failures now return `-1`, reserving `0` for "no connection available".
+ * Fixed `SSL_listen_ex()` to correctly adopt a QUIC connection and to preserve
+   queued connections on allocation failure. Invalid arguments, including
+   non-QUIC SSL objects, and internal failures now return `-1`, reserving `0`
+   for "no connection available".
+
+   *Mounir IDRASSI*
+
+ * Fixed QUIC child objects to inherit the effective flags of their explicit
+   event domain. `SSL_get0_domain()` now reports that domain for connections
+   and streams in the hierarchy.

    *Mounir IDRASSI*

diff --git a/doc/man3/SSL_new_listener.pod b/doc/man3/SSL_new_listener.pod
index 138da996b7..eeef4a7b65 100644
--- a/doc/man3/SSL_new_listener.pod
+++ b/doc/man3/SSL_new_listener.pod
@@ -138,10 +138,11 @@ in that it polls a listening SSL object, and, if a new connection is available,
 writes that connection into the SSL object pointed to by B<new_conn>.  Note that
 once a connection is returned from a listener via this call, accepting
 connections via L<SSL_accept_connection(3)> is no longer permissible, and will
-result in an error.  Note that many calls into the QUIC api may trigger the quic reactor
-(such as L<SSL_poll(3)>), and will set the port into a mode in which L<SSL_accept_connection(3)>
-is assumed.  To avoid this, it is recommended that, if the use of SSL_listen_ex use is desired, that
-it is the first I/O call made to the SSL object to which the port is attached.
+result in an error. Many calls into the QUIC API may trigger the QUIC reactor
+(such as L<SSL_poll(3)>) and select L<SSL_accept_connection(3)> mode for the
+port. Applications intending to use SSL_listen_ex() must call it before any
+operation selects that mode; in practice, it should be the first I/O call made
+on the listener.

 Likewise, if a listener has accepted a connection via
 L<SSL_accept_connection(3)>, it is impermissible to accept connections via
@@ -149,14 +150,22 @@ B<SSL_listen_ex()>. The SSL object passed in the B<new_conn> parameter must be
 a fresh standalone connection returned by L<SSL_new(3)>.
 L<OSSL_QUIC_method(3)> is intended for this use. The connection must not use
 thread assisted mode, have been started or shut down, have streams or network
-BIOs configured on it, or belong to another QUIC object hierarchy.
+BIOs configured on it, or belong to another QUIC object hierarchy. The
+application must not access I<new_conn> concurrently while SSL_listen_ex() is
+executing.

 On success, I<new_conn> is adopted into the listener's object hierarchy and
 uses its shared network port and TLS configuration derived from the listener.
 Certificates, verification parameters, ciphersuites, ALPN configuration and
 other security policy must be configured on the listener's
-B<SSL_CTX>. Applications must not install a private connected BIO or file
-descriptor on the returned connection.
+B<SSL_CTX>. The connection is reset to accepted-connection defaults. TLS
+callbacks and QUIC message tracing callbacks are derived from the listener.
+Except for application ex data associated with the outer SSL object,
+applications must not rely on configuration applied to I<new_conn> before the
+call being retained; configure it on the listener or after SSL_listen_ex()
+succeeds. Applications must not call L<SSL_set_bio(3)> or L<SSL_set_fd(3)> to
+install a private connected BIO or file descriptor on the returned connection;
+it continues to use the listener's shared network port.

 Once B<SSL_listen_ex()> returns success, the connection written to I<new_conn>
 is already in the accept (server) state. Use L<SSL_do_handshake(3)> (or
diff --git a/include/internal/quic_channel.h b/include/internal/quic_channel.h
index fd600fb44c..47a6202164 100644
--- a/include/internal/quic_channel.h
+++ b/include/internal/quic_channel.h
@@ -332,7 +332,7 @@ OSSL_STATM *ossl_quic_channel_get_statm(QUIC_CHANNEL *ch);
 SSL *ossl_quic_channel_get0_tls(QUIC_CHANNEL *ch);

 /* Attaches the TLS layer to a deferred channel. */
-void ossl_quic_channel_set0_tls(QUIC_CHANNEL *ch, SSL *ssl);
+int ossl_quic_channel_set0_tls(QUIC_CHANNEL *ch, SSL *ssl);

 /* Gets the channels short header connection id length */
 size_t ossl_quic_channel_get_short_header_conn_id_len(QUIC_CHANNEL *ch);
diff --git a/include/internal/quic_port.h b/include/internal/quic_port.h
index 90f42e0e1c..bd590939fa 100644
--- a/include/internal/quic_port.h
+++ b/include/internal/quic_port.h
@@ -95,6 +95,9 @@ QUIC_CHANNEL *ossl_quic_port_create_incoming(QUIC_PORT *port, SSL *tls);
  */
 QUIC_CHANNEL *ossl_quic_port_pop_incoming(QUIC_PORT *port);

+/* Returns the first incoming channel without removing it, or NULL. */
+QUIC_CHANNEL *ossl_quic_port_peek_incoming(QUIC_PORT *port);
+
 /* Returns 1 if there is at least one connection incoming. */
 int ossl_quic_port_have_incoming(QUIC_PORT *port);

diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c
index dcb0974b49..8e7292f193 100644
--- a/ssl/quic/quic_channel.c
+++ b/ssl/quic/quic_channel.c
@@ -582,7 +582,7 @@ SSL *ossl_quic_channel_get0_tls(QUIC_CHANNEL *ch)
     return ch->tls;
 }

-void ossl_quic_channel_set0_tls(QUIC_CHANNEL *ch, SSL *ssl)
+int ossl_quic_channel_set0_tls(QUIC_CHANNEL *ch, SSL *ssl)
 {
     /*
      * Rebind the handshake layer first, so that a failure leaves the channel
@@ -591,19 +591,10 @@ void ossl_quic_channel_set0_tls(QUIC_CHANNEL *ch, SSL *ssl)
      */
     if (!ossl_assert(ch != NULL && ssl != NULL && ch->tls == NULL)
         || !ossl_quic_tls_set0_ssl(ch->qtls, ssl))
-        return;
+        return 0;

     ch->tls = ssl;
-#ifndef OPENSSL_NO_QLOG
-    /*
-     * If we're using qlog, make sure the tls gets further configured properly
-     */
-    ch->use_qlog = 1;
-    if (ch->tls->ctx->qlog_title != NULL) {
-        OPENSSL_free(ch->qlog_title);
-        ch->qlog_title = OPENSSL_strdup(ch->tls->ctx->qlog_title);
-    }
-#endif
+    return 1;
 }

 static void free_buf_mem(unsigned char *buf, size_t buf_len, void *arg)
diff --git a/ssl/quic/quic_impl.c b/ssl/quic/quic_impl.c
index 84d9ecdafe..6ea7734b09 100644
--- a/ssl/quic/quic_impl.c
+++ b/ssl/quic/quic_impl.c
@@ -307,8 +307,10 @@ static int expect_quic_as(const SSL *s, QCTX *ctx, uint32_t flags)
     case SSL_TYPE_QUIC_CONNECTION:
         qc = (QUIC_CONNECTION *)s;
         ctx->obj = &qc->obj;
-        ctx->qd = qc->domain;
-        ctx->ql = qc->listener; /* never changes, so can be read without lock */
+        ctx->qd = qc->domain != NULL
+            ? qc->domain
+            : (qc->listener != NULL ? qc->listener->domain : NULL);
+        ctx->ql = qc->listener;
         ctx->qc = qc;

         if ((flags & QCTX_AUTO_S) != 0) {
@@ -357,7 +359,11 @@ static int expect_quic_as(const SSL *s, QCTX *ctx, uint32_t flags)

         xso = (QUIC_XSO *)s;
         ctx->obj = &xso->obj;
-        ctx->qd = xso->conn->domain;
+        ctx->qd = xso->conn->domain != NULL
+            ? xso->conn->domain
+            : (xso->conn->listener != NULL
+                      ? xso->conn->listener->domain
+                      : NULL);
         ctx->ql = xso->conn->listener;
         ctx->qc = xso->conn;
         ctx->xso = xso;
@@ -5019,7 +5025,7 @@ int ossl_quic_peeloff_conn(SSL *listener, SSL *new_conn)
 {
     QCTX lctx;
     QCTX cctx;
-    QUIC_CHANNEL *new_ch, *old_ch;
+    QUIC_CHANNEL *new_ch, *old_ch, *popped_ch;
     QUIC_PORT *old_port;
     QUIC_ENGINE *old_engine;
 #if defined(OPENSSL_THREADS)
@@ -5071,7 +5077,8 @@ int ossl_quic_peeloff_conn(SSL *listener, SSL *new_conn)
     }

     /* Do all fallible setup before consuming the queued channel. */
-    if (!ossl_quic_port_have_incoming(lctx.ql->port))
+    new_ch = ossl_quic_port_peek_incoming(lctx.ql->port);
+    if (new_ch == NULL)
         goto out;

     qc = cctx.qc;
@@ -5105,10 +5112,24 @@ int ossl_quic_peeloff_conn(SSL *listener, SSL *new_conn)
         goto out;
     }

-    new_ch = ossl_quic_port_pop_incoming(ql->port);
-
     /* Bind TLS before adopting the deferred incoming channel. */
-    ossl_quic_channel_set0_tls(new_ch, tls);
+    if (!ossl_quic_channel_set0_tls(new_ch, tls)) {
+        QUIC_RAISE_NON_NORMAL_ERROR(NULL, ERR_R_INTERNAL_ERROR, NULL);
+        SSL_free(tls);
+        SSL_free(&ql->obj.ssl);
+        ret = -1;
+        goto out;
+    }
+
+    ossl_quic_channel_set_msg_callback(new_ch,
+        ql->obj.ssl.ctx->msg_callback, new_conn);
+    ossl_quic_channel_set_msg_callback_arg(new_ch,
+        ql->obj.ssl.ctx->msg_callback_arg);
+
+    /* The listener lock guarantees that the queue head has not changed. */
+    popped_ch = ossl_quic_port_pop_incoming(ql->port);
+    assert(popped_ch == new_ch);
+    (void)popped_ch;

     old_ch = qc->ch;
     old_port = qc->port;
@@ -5147,11 +5168,14 @@ int ossl_quic_peeloff_conn(SSL *listener, SSL *new_conn)
     qc->as_server_state = 1;
     /* Reinitialise configuration to the accepted-connection defaults. */
     qc->default_stream_mode = SSL_DEFAULT_STREAM_MODE_AUTO_BIDI;
+    qc->default_ssl_mode = ql->obj.ssl.ctx->mode;
     qc->default_ssl_options
         = ql->obj.ssl.ctx->options & OSSL_QUIC_PERMITTED_OPTIONS;
     qc->incoming_stream_policy = SSL_INCOMING_STREAM_POLICY_AUTO;
     qc->incoming_stream_aec = 0;
     qc->last_error = SSL_ERROR_NONE;
+    ossl_quic_obj_set_blocking_mode(&qc->obj, QUIC_BLOCKING_MODE_INHERIT);
+    qc->obj.event_handling_mode = SSL_VALUE_EVENT_HANDLING_MODE_INHERIT;
     qc_update_reject_policy(qc);
     ret = 1;

diff --git a/ssl/quic/quic_obj.c b/ssl/quic/quic_obj.c
index d2673b236b..2aba3fb4bd 100644
--- a/ssl/quic/quic_obj.c
+++ b/ssl/quic/quic_obj.c
@@ -20,11 +20,13 @@ int ossl_quic_obj_init(QUIC_OBJ *obj,
     QUIC_ENGINE *engine,
     QUIC_PORT *port)
 {
+    QUIC_OBJ *parent = (QUIC_OBJ *)parent_obj;
     int is_event_leader = (engine != NULL);
     int is_port_leader = (port != NULL);

     if (!ossl_assert(obj != NULL && !obj->init_done && SSL_TYPE_IS_QUIC(type)
-            && (parent_obj == NULL || IS_QUIC(parent_obj))))
+            && (parent_obj == NULL
+                || (IS_QUIC(parent_obj) && parent->init_done))))
         return 0;

     /* Event leader is always the root object. */
@@ -34,8 +36,9 @@ int ossl_quic_obj_init(QUIC_OBJ *obj,
     if (!ossl_ssl_init(&obj->ssl, ctx, ctx->method, type))
         goto err;

-    obj->domain_flags = ctx->domain_flags;
-    obj->parent_obj = (QUIC_OBJ *)parent_obj;
+    obj->domain_flags
+        = parent != NULL ? parent->domain_flags : ctx->domain_flags;
+    obj->parent_obj = parent;
     obj->is_event_leader = is_event_leader;
     obj->is_port_leader = is_port_leader;
     obj->engine = engine;
diff --git a/ssl/quic/quic_obj_local.h b/ssl/quic/quic_obj_local.h
index 75ca1eee37..0d7627c241 100644
--- a/ssl/quic/quic_obj_local.h
+++ b/ssl/quic/quic_obj_local.h
@@ -100,7 +100,7 @@ struct quic_obj_st {
      */
     QUIC_PORT *port;

-    /* SSL_DOMAIN_FLAG values taken from SSL_CTX at construction time. */
+    /* Effective SSL_DOMAIN_FLAG values inherited from the object hierarchy. */
     uint64_t domain_flags;

     unsigned int init_done : 1;
diff --git a/ssl/quic/quic_port.c b/ssl/quic/quic_port.c
index 0dfe4c6cfa..6c8bb17a77 100644
--- a/ssl/quic/quic_port.c
+++ b/ssl/quic/quic_port.c
@@ -591,6 +591,9 @@ static QUIC_CHANNEL *port_make_channel(QUIC_PORT *port, SSL *tls, OSSL_QRX *qrx,
     QUIC_CHANNEL_ARGS args = { 0 };
     QUIC_CHANNEL *ch;
     SSL *user_ssl = NULL;
+#ifndef OPENSSL_NO_QLOG
+    SSL_CTX *qlog_ctx;
+#endif

     args.port = port;
     args.is_server = is_server;
@@ -649,12 +652,15 @@ static QUIC_CHANNEL *port_make_channel(QUIC_PORT *port, SSL *tls, OSSL_QRX *qrx,
     }
 #ifndef OPENSSL_NO_QLOG
     /*
-     * If we're using qlog, make sure the tls get further configured properly
+     * A deferred SSL_listen_ex() channel does not have its TLS object yet, but
+     * it still uses the port's channel context. Configure its qlog title before
+     * the first packet can cause the qlog object to be instantiated.
      */
     ch->use_qlog = 1;
-    if (ch->tls != NULL && ch->tls->ctx->qlog_title != NULL) {
+    qlog_ctx = ch->tls != NULL ? ch->tls->ctx : port->channel_ctx;
+    if (qlog_ctx != NULL && qlog_ctx->qlog_title != NULL) {
         OPENSSL_free(ch->qlog_title);
-        if ((ch->qlog_title = OPENSSL_strdup(ch->tls->ctx->qlog_title)) == NULL)
+        if ((ch->qlog_title = OPENSSL_strdup(qlog_ctx->qlog_title)) == NULL)
             goto err;
     }
 #endif
@@ -713,9 +719,14 @@ QUIC_CHANNEL *ossl_quic_port_pop_incoming(QUIC_PORT *port)
     return ch;
 }

+QUIC_CHANNEL *ossl_quic_port_peek_incoming(QUIC_PORT *port)
+{
+    return ossl_list_incoming_ch_head(&port->incoming_channel_list);
+}
+
 int ossl_quic_port_have_incoming(QUIC_PORT *port)
 {
-    return ossl_list_incoming_ch_head(&port->incoming_channel_list) != NULL;
+    return ossl_quic_port_peek_incoming(port) != NULL;
 }

 void ossl_quic_port_drop_incoming(QUIC_PORT *port)
diff --git a/test/quicapitest.c b/test/quicapitest.c
index 8d8ce409e0..8dc36b8a22 100644
--- a/test/quicapitest.c
+++ b/test/quicapitest.c
@@ -2424,7 +2424,9 @@ static int test_domain_flags(void)
 {
     int testresult = 0;
     SSL_CTX *ctx = NULL;
-    SSL *domain = NULL, *listener = NULL, *other_conn = NULL;
+    SSL *inherited_domain = NULL, *domain = NULL, *listener = NULL;
+    SSL *listener_conn = NULL;
+    SSL *other_conn = NULL;
     uint64_t domain_flags = 0;

     if (!TEST_ptr(ctx = SSL_CTX_new_ex(libctx, NULL, OSSL_QUIC_client_method()))
@@ -2432,15 +2434,19 @@ static int test_domain_flags(void)
         || !TEST_uint64_t_ne(domain_flags, 0)
         || !TEST_uint64_t_ne(domain_flags & (SSL_DOMAIN_FLAG_SINGLE_THREAD | SSL_DOMAIN_FLAG_MULTI_THREAD), 0)
         || !TEST_uint64_t_ne(domain_flags & SSL_DOMAIN_FLAG_LEGACY_BLOCKING, 0)
-        || !TEST_true(SSL_CTX_set_domain_flags(ctx, SSL_DOMAIN_FLAG_SINGLE_THREAD))
+        || !TEST_true(SSL_CTX_set_domain_flags(ctx, SSL_DOMAIN_FLAG_MULTI_THREAD))
         || !TEST_true(SSL_CTX_get_domain_flags(ctx, &domain_flags))
-        || !TEST_uint64_t_eq(domain_flags, SSL_DOMAIN_FLAG_SINGLE_THREAD)
-        || !TEST_ptr(domain = SSL_new_domain(ctx, 0))
+        || !TEST_uint64_t_eq(domain_flags, SSL_DOMAIN_FLAG_MULTI_THREAD)
+        || !TEST_ptr(inherited_domain = SSL_new_domain(ctx, 0))
+        || !TEST_true(SSL_get_domain_flags(inherited_domain, &domain_flags))
+        || !TEST_uint64_t_eq(domain_flags, SSL_DOMAIN_FLAG_MULTI_THREAD)
+        || !TEST_ptr(domain = SSL_new_domain(ctx,
+                         SSL_DOMAIN_FLAG_SINGLE_THREAD))
         || !TEST_true(SSL_get_domain_flags(domain, &domain_flags))
         || !TEST_uint64_t_eq(domain_flags, SSL_DOMAIN_FLAG_SINGLE_THREAD)
         || !TEST_true(other_conn = SSL_new(ctx))
         || !TEST_true(SSL_get_domain_flags(other_conn, &domain_flags))
-        || !TEST_uint64_t_eq(domain_flags, SSL_DOMAIN_FLAG_SINGLE_THREAD)
+        || !TEST_uint64_t_eq(domain_flags, SSL_DOMAIN_FLAG_MULTI_THREAD)
         || !TEST_true(SSL_is_domain(domain))
         || !TEST_false(SSL_is_domain(other_conn))
         || !TEST_ptr_eq(SSL_get0_domain(domain), domain)
@@ -2449,13 +2455,22 @@ static int test_domain_flags(void)
         || !TEST_true(SSL_is_listener(listener))
         || !TEST_false(SSL_is_domain(listener))
         || !TEST_ptr_eq(SSL_get0_domain(listener), domain)
-        || !TEST_ptr_eq(SSL_get0_listener(listener), listener))
+        || !TEST_ptr_eq(SSL_get0_listener(listener), listener)
+        || !TEST_true(SSL_get_domain_flags(listener, &domain_flags))
+        || !TEST_uint64_t_eq(domain_flags, SSL_DOMAIN_FLAG_SINGLE_THREAD)
+        || !TEST_ptr(listener_conn = SSL_new_from_listener(listener, 0))
+        || !TEST_ptr_eq(SSL_get0_domain(listener_conn), domain)
+        || !TEST_ptr_eq(SSL_get0_listener(listener_conn), listener)
+        || !TEST_true(SSL_get_domain_flags(listener_conn, &domain_flags))
+        || !TEST_uint64_t_eq(domain_flags, SSL_DOMAIN_FLAG_SINGLE_THREAD))
         goto err;

     testresult = 1;
 err:
-    SSL_free(domain);
+    SSL_free(listener_conn);
     SSL_free(listener);
+    SSL_free(domain);
+    SSL_free(inherited_domain);
     SSL_free(other_conn);
     SSL_CTX_free(ctx);
     return testresult;
@@ -2807,7 +2822,7 @@ static OSSL_TIME fake_now_cb(void *arg)
 }

 static int create_quic_ssl_objects_ex(SSL_CTX *sctx, SSL_CTX *cctx,
-    SSL **lssl, SSL **cssl, int use_fake_time)
+    SSL *domain, SSL **lssl, SSL **cssl, int use_fake_time)
 {
     BIO_ADDR *addr = NULL;
     struct in_addr ina;
@@ -2827,10 +2842,19 @@ static int create_quic_ssl_objects_ex(SSL_CTX *sctx, SSL_CTX *cctx,
         goto err;
     addr = NULL;

-    *lssl = ql_create(sctx, sbio);
-    sbio = NULL;
-    if (!TEST_ptr(*lssl))
-        goto err;
+    if (domain == NULL) {
+        *lssl = ql_create(sctx, sbio);
+        sbio = NULL;
+        if (!TEST_ptr(*lssl))
+            goto err;
+    } else {
+        if (!TEST_ptr(*lssl = SSL_new_listener_from(domain, 0)))
+            goto err;
+        SSL_set_bio(*lssl, sbio, sbio);
+        sbio = NULL;
+        if (!TEST_true(SSL_listen(*lssl)))
+            goto err;
+    }

     if (!TEST_ptr(*cssl = SSL_new(cctx)))
         goto err;
@@ -2878,7 +2902,7 @@ err:
 static int create_quic_ssl_objects(SSL_CTX *sctx, SSL_CTX *cctx,
     SSL **lssl, SSL **cssl)
 {
-    return create_quic_ssl_objects_ex(sctx, cctx, lssl, cssl, 0);
+    return create_quic_ssl_objects_ex(sctx, cctx, NULL, lssl, cssl, 0);
 }

 static int queue_incoming_connection(SSL *qlistener, SSL *clientssl)
@@ -2899,6 +2923,39 @@ static int queue_incoming_connection(SSL *qlistener, SSL *clientssl)
     return TEST_size_t_eq(SSL_get_accept_connection_queue_len(qlistener), 1);
 }

+static SSL *listen_ex_msg_cb_ssl;
+static char listen_ex_msg_cb_arg;
+static unsigned int listen_ex_tls_msg_count;
+static unsigned int listen_ex_quic_msg_count;
+static int listen_ex_msg_cb_mismatch;
+
+static void listen_ex_msg_cb(int write_p, int version, int content_type,
+    const void *buf, size_t len, SSL *ssl, void *arg)
+{
+    (void)write_p;
+    (void)version;
+    (void)buf;
+    (void)len;
+
+    if (ssl != listen_ex_msg_cb_ssl || arg != &listen_ex_msg_cb_arg)
+        listen_ex_msg_cb_mismatch = 1;
+
+    if (content_type == SSL3_RT_HANDSHAKE)
+        ++listen_ex_tls_msg_count;
+
+    switch (content_type) {
+    case SSL3_RT_QUIC_DATAGRAM:
+    case SSL3_RT_QUIC_PACKET:
+    case SSL3_RT_QUIC_FRAME_FULL:
+    case SSL3_RT_QUIC_FRAME_HEADER:
+    case SSL3_RT_QUIC_FRAME_PADDING:
+        ++listen_ex_quic_msg_count;
+        break;
+    default:
+        break;
+    }
+}
+
 static int listen_ex_rejects_new_conn(SSL *qlistener, SSL *new_conn)
 {
     int ret = SSL_listen_ex(qlistener, new_conn);
@@ -2981,35 +3038,67 @@ static int test_ssl_listen_ex(void)
 {
     SSL_CTX *cctx = NULL, *sctx = NULL, *qmctx = NULL, *threadctx = NULL;
     SSL *clientssl = NULL, *serverssl = NULL, *qlistener = NULL;
+    SSL *domain = NULL;
     SSL *preconf = NULL, *prestream = NULL, *threadssl = NULL;
     SSL *cstream = NULL, *sstream = NULL;
     BIO *confbio = NULL;
     unsigned char buf[16], msg[] = "Hello, World!";
     size_t readbytes, written;
+    uint64_t listener_domain_flags, target_domain_flags;
+    uint64_t connection_domain_flags, stream_domain_flags;
+    uint64_t event_handling_mode;
+    long listener_mode, target_mode;
     int testresult = 0;
     int ret = 0, i;

     if (!TEST_ptr(sctx = create_server_ctx())
-        || !TEST_ptr(cctx = create_client_ctx()))
+        || !TEST_ptr(cctx = create_client_ctx())
+        || !TEST_true(SSL_CTX_set_domain_flags(sctx,
+            SSL_DOMAIN_FLAG_MULTI_THREAD)))
         goto err;

-    if (!create_quic_ssl_objects_ex(sctx, cctx,
+    listener_mode = SSL_CTX_set_mode(sctx, SSL_MODE_ENABLE_PARTIAL_WRITE);
+    SSL_CTX_set_msg_callback(sctx, listen_ex_msg_cb);
+    SSL_CTX_set_msg_callback_arg(sctx, &listen_ex_msg_cb_arg);
+
+    if (!TEST_ptr(domain = SSL_new_domain(sctx,
+                      SSL_DOMAIN_FLAG_MULTI_THREAD))
+        || !create_quic_ssl_objects_ex(sctx, cctx, domain,
             &qlistener, &clientssl, 1))
         goto err;

     qmctx = SSL_CTX_new_ex(libctx, NULL, OSSL_QUIC_method());
-    if (!TEST_ptr(qmctx))
+    if (!TEST_ptr(qmctx)
+        || !TEST_true(SSL_CTX_set_domain_flags(qmctx,
+            SSL_DOMAIN_FLAG_SINGLE_THREAD)))
         goto err;

+    SSL_CTX_set_mode(qmctx, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
     serverssl = SSL_new(qmctx);
     if (!TEST_ptr(serverssl))
         goto err;

+    SSL_clear_mode(serverssl, SSL_MODE_ENABLE_PARTIAL_WRITE);
+    if (!TEST_true(SSL_set_event_handling_mode(serverssl,
+            SSL_VALUE_EVENT_HANDLING_MODE_EXPLICIT)))
+        goto err;
+    target_mode = SSL_get_mode(serverssl);
+    listen_ex_msg_cb_ssl = serverssl;
+    listen_ex_tls_msg_count = 0;
+    listen_ex_quic_msg_count = 0;
+    listen_ex_msg_cb_mismatch = 0;
+
     if (!TEST_int_eq(SSL_listen_ex(NULL, serverssl), -1)
         || !TEST_true(ERR_GET_REASON(ERR_get_error())
             == ERR_R_PASSED_INVALID_ARGUMENT)
         || !TEST_int_eq(SSL_listen_ex(qlistener, serverssl), 0)
-        || !queue_incoming_connection(qlistener, clientssl))
+        || !queue_incoming_connection(qlistener, clientssl)
+        || !TEST_true(SSL_get_domain_flags(qlistener,
+            &listener_domain_flags))
+        || !TEST_true(SSL_get_domain_flags(serverssl,
+            &target_domain_flags))
+        || !TEST_uint64_t_ne(listener_domain_flags, target_domain_flags)
+        || !TEST_long_ne(listener_mode, target_mode))
         goto err;
     ERR_clear_error();

@@ -3068,9 +3157,32 @@ static int test_ssl_listen_ex(void)
     preconf = NULL;

     if (!TEST_int_eq(SSL_listen_ex(qlistener, serverssl), 1)
-        || !TEST_size_t_eq(SSL_get_accept_connection_queue_len(qlistener), 0))
+        || !TEST_size_t_eq(SSL_get_accept_connection_queue_len(qlistener), 0)
+        || !TEST_true(SSL_get_domain_flags(serverssl,
+            &connection_domain_flags))
+        || !TEST_uint64_t_eq(connection_domain_flags,
+            listener_domain_flags)
+        || !TEST_ptr_eq(SSL_get0_domain(serverssl), domain)
+        || !TEST_ptr_eq(SSL_get0_listener(serverssl), qlistener)
+        || !TEST_long_eq(SSL_get_mode(serverssl), listener_mode)
+        || !TEST_true(SSL_get_event_handling_mode(serverssl,
+            &event_handling_mode))
+        || !TEST_uint64_t_eq(event_handling_mode,
+            SSL_VALUE_EVENT_HANDLING_MODE_INHERIT))
         goto err;

+    if (!TEST_ptr_null(SSL_accept_connection(qlistener, 0))
+        || !TEST_true(ERR_GET_REASON(ERR_get_error())
+            == ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED))
+        goto err;
+    ERR_clear_error();
+
+    /* The adopted connection keeps the listener hierarchy alive. */
+    SSL_free(qlistener);
+    qlistener = NULL;
+    SSL_free(domain);
+    domain = NULL;
+
     for (i = 0; i < 10; i++) {
         ret = SSL_do_handshake(serverssl);
         if (ret != 1
@@ -3092,12 +3204,6 @@ static int test_ssl_listen_ex(void)
         || !TEST_true(SSL_is_init_finished(clientssl)))
         goto err;

-    if (!TEST_ptr_null(SSL_accept_connection(qlistener, 0))
-        || !TEST_true(ERR_GET_REASON(ERR_get_error())
-            == ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED))
-        goto err;
-    ERR_clear_error();
-
     if (!TEST_true(SSL_set_default_stream_mode(serverssl,
             SSL_DEFAULT_STREAM_MODE_NONE))
         || !TEST_true(SSL_set_default_stream_mode(clientssl,
@@ -3110,6 +3216,12 @@ static int test_ssl_listen_ex(void)

     sstream = SSL_accept_stream(serverssl, 0);
     if (!TEST_ptr(sstream)
+        || !TEST_true(SSL_get_domain_flags(sstream, &stream_domain_flags))
+        || !TEST_uint64_t_eq(stream_domain_flags, listener_domain_flags)
+        || !TEST_ptr_eq(SSL_get0_domain(sstream),
+            SSL_get0_domain(serverssl))
+        || !TEST_ptr_eq(SSL_get0_listener(sstream),
+            SSL_get0_listener(serverssl))
         || !TEST_true(SSL_read_ex(sstream, buf, sizeof(buf), &readbytes))
         || !TEST_size_t_eq(readbytes, sizeof(msg))
         || !TEST_mem_eq(buf, readbytes, msg, sizeof(msg))
@@ -3120,6 +3232,11 @@ static int test_ssl_listen_ex(void)
         || !TEST_mem_eq(buf, readbytes, msg, sizeof(msg)))
         goto err;

+    if (!TEST_uint_gt(listen_ex_tls_msg_count, 0)
+        || !TEST_uint_gt(listen_ex_quic_msg_count, 0)
+        || !TEST_false(listen_ex_msg_cb_mismatch))
+        goto err;
+
     testresult = 1;

 err:
@@ -3132,10 +3249,12 @@ err:
     SSL_free(qlistener);
     SSL_free(serverssl);
     SSL_free(clientssl);
+    SSL_free(domain);
     SSL_CTX_free(sctx);
     SSL_CTX_free(cctx);
     SSL_CTX_free(qmctx);
     SSL_CTX_free(threadctx);
+    listen_ex_msg_cb_ssl = NULL;

     return testresult;
 }
@@ -3149,7 +3268,7 @@ static int test_ssl_listen_ex_teardown(void)

     if (!TEST_ptr(sctx = create_server_ctx())
         || !TEST_ptr(cctx = create_client_ctx())
-        || !create_quic_ssl_objects_ex(sctx, cctx,
+        || !create_quic_ssl_objects_ex(sctx, cctx, NULL,
             &qlistener, &clientssl, 1)
         || !TEST_ptr(qmctx = SSL_CTX_new_ex(libctx, NULL, OSSL_QUIC_method()))
         || !TEST_ptr(serverssl = SSL_new(qmctx))
@@ -3180,7 +3299,7 @@ static int test_ssl_listen_ex_mfail(void)

     if (!TEST_ptr(sctx = create_server_ctx())
         || !TEST_ptr(cctx = create_client_ctx())
-        || !create_quic_ssl_objects_ex(sctx, cctx,
+        || !create_quic_ssl_objects_ex(sctx, cctx, NULL,
             &qlistener, &clientssl, 1)
         || !TEST_ptr(qmctx = SSL_CTX_new_ex(libctx, NULL, OSSL_QUIC_method()))
         || !TEST_ptr(serverssl = SSL_new(qmctx))
@@ -3200,6 +3319,14 @@ static int test_ssl_listen_ex_mfail(void)
             testresult = -1;
             goto err;
         }
+
+        ERR_clear_error();
+        if (!TEST_int_eq(SSL_listen_ex(qlistener, serverssl), 1)
+            || !TEST_size_t_eq(
+                SSL_get_accept_connection_queue_len(qlistener), 0)) {
+            testresult = -1;
+            goto err;
+        }
     } else if (!TEST_int_eq(ret, 1)
         || !TEST_size_t_eq(SSL_get_accept_connection_queue_len(qlistener), 0)) {
         goto err;
@@ -3764,7 +3891,8 @@ static int test_quic_handshake_multipkt_mfail(void)
         || !TEST_ptr(cctx = create_client_ctx()))
         goto err;

-    if (!create_quic_ssl_objects_ex(sctx, cctx, &qlistener, &clientssl, 1))
+    if (!create_quic_ssl_objects_ex(sctx, cctx, NULL,
+            &qlistener, &clientssl, 1))
         goto err;

     if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "localhost")))