]> git.ipfire.org Git - thirdparty/openssl.git/commitdiff
Make SSL_poll() and SSL_shutdown() better friends
authorAlexandr Nedvedicky <sashan@openssl.org>
Tue, 29 Jul 2025 14:45:25 +0000 (16:45 +0200)
committerAlexandr Nedvedicky <sashan@openssl.org>
Thu, 21 Aug 2025 12:43:03 +0000 (14:43 +0200)
Current QUIC stack may leave connection monitored by SSL_poll() to stale
during regular shutdown.  The issue is triggered when ACK for client's
FIN gets delayed. The sequeance of operations to trigger
the stale of QUIC connection at client goes as follows:

- application calls SSL_shutdown() on connection,
  the shutdown can not proceed, because bi-directional
  stream must be flushed. The client awaits ACK from
  server acknowledging reception of FIN on client's stream

- the stream object gets destroyed, because application
  received all data from server.

- application updates poll set and passes to SSL_poll()

- ssl poll ticks the engine. Engine receives delayed ACK
  and marks stream as flushed. At this point the SSL_shutdown()
  operation may proceed given the application calls the
  SSL_shutdown(). However there is no mechanism to make SSL_poll()
  return so application is unable to proceed with its event
  loop where SSL_shutdown() may get called.

This change introduces ossl_quic_channel_notify_flush_done() function
which notifies channel when all streams are flushed (all FINs got ACKed).

The first thing SSL_shudown() does it calls ossl_quic_stream_map_begin_shutdown_flush().
The function walks list of all streams attached to channel and notes how many
streams is missing ACK for their FIN. In our test case it finds one such stream.
Call to SSL_shutdown() returns and application destroys the SSL stream object
and updates a poll set.

SSL_poll() gets called. The QUIC stack (engine) gets ticked and reads data
from socket. It processes delayed ACK now. The ACK-manager updates the
stream notifying the server ACKs the FIN sent by client. The stream
is flushed now. Thw shutdown_flush_done() for stream gets called on
behalf of ACK manager.

The shutdown_flush_done() does two things:
- it marks stream as flushed
- it decrements the num_shutdown_flush counter initialized
  be earlier call to ossl_quic_stream_map_begin_shutdown_flush()
  called by SSL_shutdown()
The change here calls ossl_quic_channel_notify_flush_done() when
num_shutdown_flush reaches zero.

The ossl_quic_channel_notify_flush_done() then calls function
ossl_quic_channel_notify_flush_done(), which just moves the state
of the channel (connection) from active to terminating state.
The change of channel state is sufficent for SSL_poll() to
signal _EC event on connection.

Once application receives _EC event on connection it should
check the state of the channel/reason of error. In regular case
the error/channel state hints application to call SSL_shutdown()
so connection object can proceed with connection shutdown.
The SSL_shutdown() call done now moves channel to terminated
state. So the next call to SSL_poll() can signal _ECD which
tells application it's time to stop polling on SSL connection
object and destroy it.

Fixes openssl/project#1291

Reviewed-by: Neil Horman <nhorman@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/28116)

include/internal/quic_channel.h
include/internal/quic_stream_map.h
ssl/quic/quic_channel.c
ssl/quic/quic_impl.c
ssl/quic/quic_stream_map.c
test/quic_multistream_test.c
test/quic_txp_test.c

index 4476481fef9ba22f81e835045214bc849195487f..1d008abd6330a513c6aba4231dc2709f7de6dc9e 100644 (file)
@@ -345,6 +345,8 @@ int ossl_quic_channel_is_terminated(const QUIC_CHANNEL *ch);
 int ossl_quic_channel_is_active(const QUIC_CHANNEL *ch);
 int ossl_quic_channel_is_handshake_complete(const QUIC_CHANNEL *ch);
 int ossl_quic_channel_is_handshake_confirmed(const QUIC_CHANNEL *ch);
+int ossl_quic_channel_is_server(const QUIC_CHANNEL *ch);
+void ossl_quic_channel_notify_flush_done(QUIC_CHANNEL *ch);
 
 QUIC_PORT *ossl_quic_channel_get0_port(QUIC_CHANNEL *ch);
 QUIC_ENGINE *ossl_quic_channel_get0_engine(QUIC_CHANNEL *ch);
@@ -470,6 +472,8 @@ int ossl_quic_bind_channel(QUIC_CHANNEL *ch, const BIO_ADDR *peer,
                            const QUIC_CONN_ID *scid, const QUIC_CONN_ID *dcid,
                            const QUIC_CONN_ID *odcid);
 
+void ossl_quic_channel_set_tcause(QUIC_CHANNEL *ch, uint64_t app_error_code,
+                                  const char *app_reason);
 # endif
 
 #endif
index 0e34ae4b4cbff4730be7525f50c59a055fb12a3b..7b2c46669b520c191386973e6b338e97e83c3844 100644 (file)
@@ -554,6 +554,7 @@ static ossl_inline ossl_unused size_t ossl_quic_stream_recv_pending(const QUIC_S
  */
 struct quic_stream_map_st {
     LHASH_OF(QUIC_STREAM)   *map;
+    QUIC_CHANNEL            *ch;
     QUIC_STREAM_LIST_NODE   active_list;
     QUIC_STREAM_LIST_NODE   accept_list;
     QUIC_STREAM_LIST_NODE   ready_for_gc_list;
@@ -564,7 +565,6 @@ struct quic_stream_map_st {
     void                    *get_stream_limit_cb_arg;
     QUIC_RXFC               *max_streams_bidi_rxfc;
     QUIC_RXFC               *max_streams_uni_rxfc;
-    int                     is_server;
 };
 
 /*
@@ -585,7 +585,7 @@ int ossl_quic_stream_map_init(QUIC_STREAM_MAP *qsm,
                               void *get_stream_limit_cb_arg,
                               QUIC_RXFC *max_streams_bidi_rxfc,
                               QUIC_RXFC *max_streams_uni_rxfc,
-                              int is_server);
+                              QUIC_CHANNEL *ch);
 
 /*
  * Any streams still in the map will be released as though
index 12bda515e607c4510e2b38dff86d653d6bb614d8..47d2b94f92fca00d510e26baf593d1a16380480b 100644 (file)
@@ -249,7 +249,7 @@ static int ch_init(QUIC_CHANNEL *ch)
     if (!ossl_quic_stream_map_init(&ch->qsm, get_stream_limit, ch,
                                    &ch->max_streams_bidi_rxfc,
                                    &ch->max_streams_uni_rxfc,
-                                   ch->is_server))
+                                   ch))
         goto err;
 
     ch->have_qsm = 1;
@@ -398,8 +398,12 @@ static void ch_cleanup(QUIC_CHANNEL *ch)
              ++pn_space)
             ossl_ackm_on_pkt_space_discarded(ch->ackm, pn_space);
 
-    ossl_quic_lcidm_cull(ch->lcidm, ch);
-    ossl_quic_srtm_cull(ch->srtm, ch);
+    if (ch->lcidm != NULL)
+        ossl_quic_lcidm_cull(ch->lcidm, ch);
+
+    if (ch->srtm != NULL)
+        ossl_quic_srtm_cull(ch->srtm, ch);
+
     ossl_quic_tx_packetiser_free(ch->txp);
     ossl_quic_txpim_free(ch->txpim);
     ossl_quic_cfq_free(ch->cfq);
@@ -649,6 +653,45 @@ int ossl_quic_channel_is_term_any(const QUIC_CHANNEL *ch)
         || ossl_quic_channel_is_terminated(ch);
 }
 
+int ossl_quic_channel_is_server(const QUIC_CHANNEL *ch)
+{
+    return ch->is_server;
+}
+
+void ossl_quic_channel_notify_flush_done(QUIC_CHANNEL *ch)
+{
+    ch_record_state_transition(ch, ch->terminate_cause.remote
+                                   ? QUIC_CHANNEL_STATE_TERMINATING_DRAINING
+                                   : QUIC_CHANNEL_STATE_TERMINATING_CLOSING);
+    /*
+     * RFC 9000 s. 10.2 Immediate Close
+     *  These states SHOULD persist for at least three times
+     *  the current PTO interval as defined in [QUIC-RECOVERY].
+     */
+    ch->terminate_deadline
+        = ossl_time_add(get_time(ch),
+                        ossl_time_multiply(ossl_ackm_get_pto_duration(ch->ackm), 3));
+    if (!ch->terminate_cause.remote) {
+        OSSL_QUIC_FRAME_CONN_CLOSE f = {0};
+
+        /* best effort */
+        f.error_code = ch->terminate_cause.error_code;
+        f.frame_type = ch->terminate_cause.frame_type;
+        f.is_app     = ch->terminate_cause.app;
+        f.reason     = (char *)ch->terminate_cause.reason;
+        f.reason_len = ch->terminate_cause.reason_len;
+        ossl_quic_tx_packetiser_schedule_conn_close(ch->txp, &f);
+        /*
+         * RFC 9000 s. 10.2.2 Draining Connection State:
+         *  An endpoint that receives a CONNECTION_CLOSE frame MAY
+         *  send a single packet containing a CONNECTION_CLOSE
+         *  frame before entering the draining state, using a
+         *  NO_ERROR code if appropriate
+         */
+        ch->conn_close_queued = 1;
+    }
+}
+
 const QUIC_TERMINATE_CAUSE *
 ossl_quic_channel_get_terminate_cause(const QUIC_CHANNEL *ch)
 {
@@ -3156,14 +3199,17 @@ int ossl_quic_channel_on_handshake_confirmed(QUIC_CHANNEL *ch)
 static void copy_tcause(QUIC_TERMINATE_CAUSE *dst,
                         const QUIC_TERMINATE_CAUSE *src)
 {
+    /*
+     * do not override reason once it got set.
+     */
+    if (dst->reason != NULL)
+        return;
+
     dst->error_code = src->error_code;
     dst->frame_type = src->frame_type;
     dst->app        = src->app;
     dst->remote     = src->remote;
 
-    dst->reason     = NULL;
-    dst->reason_len = 0;
-
     if (src->reason != NULL && src->reason_len > 0) {
         size_t l = src->reason_len;
         char *r;
@@ -3184,6 +3230,18 @@ static void copy_tcause(QUIC_TERMINATE_CAUSE *dst,
     }
 }
 
+void ossl_quic_channel_set_tcause(QUIC_CHANNEL *ch, uint64_t app_error_code,
+                                  const char *app_reason)
+{
+    QUIC_TERMINATE_CAUSE tcause = {0};
+
+    tcause.app          = 1;
+    tcause.error_code   = app_error_code;
+    tcause.reason       = app_reason;
+    tcause.reason_len   = app_reason != NULL ? strlen(app_reason) : 0;
+    copy_tcause(&ch->terminate_cause, &tcause);
+}
+
 static void ch_start_terminating(QUIC_CHANNEL *ch,
                                  const QUIC_TERMINATE_CAUSE *tcause,
                                  int force_immediate)
@@ -3205,38 +3263,7 @@ static void ch_start_terminating(QUIC_CHANNEL *ch,
         ossl_qlog_event_connectivity_connection_closed(ch_get_qlog(ch), tcause);
 
         if (!force_immediate) {
-            ch_record_state_transition(ch, tcause->remote
-                                           ? QUIC_CHANNEL_STATE_TERMINATING_DRAINING
-                                           : QUIC_CHANNEL_STATE_TERMINATING_CLOSING);
-            /*
-             * RFC 9000 s. 10.2 Immediate Close
-             *  These states SHOULD persist for at least three times
-             *  the current PTO interval as defined in [QUIC-RECOVERY].
-             */
-            ch->terminate_deadline
-                = ossl_time_add(get_time(ch),
-                                ossl_time_multiply(ossl_ackm_get_pto_duration(ch->ackm),
-                                                   3));
-
-            if (!tcause->remote) {
-                OSSL_QUIC_FRAME_CONN_CLOSE f = {0};
-
-                /* best effort */
-                f.error_code = ch->terminate_cause.error_code;
-                f.frame_type = ch->terminate_cause.frame_type;
-                f.is_app     = ch->terminate_cause.app;
-                f.reason     = (char *)ch->terminate_cause.reason;
-                f.reason_len = ch->terminate_cause.reason_len;
-                ossl_quic_tx_packetiser_schedule_conn_close(ch->txp, &f);
-                /*
-                 * RFC 9000 s. 10.2.2 Draining Connection State:
-                 *  An endpoint that receives a CONNECTION_CLOSE frame MAY
-                 *  send a single packet containing a CONNECTION_CLOSE
-                 *  frame before entering the draining state, using a
-                 *  NO_ERROR code if appropriate
-                 */
-                ch->conn_close_queued = 1;
-            }
+            ossl_quic_channel_notify_flush_done(ch);
         } else {
             ch_on_terminating_timeout(ch);
         }
index baae2dddba82289e731104154e3f24be3ebe68c3..09c010ebba8406f0d5d68ef255700fbf3092f226 100644 (file)
@@ -1501,6 +1501,36 @@ static int quic_shutdown_peer_wait(void *arg)
     return ossl_quic_channel_is_term_any(qc->ch);
 }
 
+/*
+ * This function deals with local shutdown.
+ * Function must consider those scenarios:
+ *    - blocking mode (1)
+ *    - non-blocking mode (2)
+ *    - non-blocking mode with assistance from SSL_poll() (3)
+ * (1) The function completes shutdown then returns back to caller.
+ * To complete shutdown we must do:
+ *    - flush all streams, unless we got SSL_SHUTDOWN_FLAG_NO_STREAM_FLUSH,
+ *      which means the connection is closed without waiting for streams
+ *      to deliver data written by application.
+ *    - let remote peer know local application is going to close connection,
+ *      unless we got SSL_SHUTDOWN_FLAG_WAIT_PEER in which case we await
+ *      until remote peer closes the connection
+ *    - wait for peer to confirm connection close
+ *
+ * (2) The function does not block waiting for streams to be flushed
+ * nor for peer to close connection (when running with SSL_SHUTDOWN_FLAG_WAIT_PEER)
+ * Application is supposed to call SSL_shutdown() repeatedly as long as
+ * function returns 0 which indicates the operation is still in progress.
+ *
+ * (3) In this case application uses SSL_poll() to wait for completion
+ * of each step of shutdown process. Application calls SSL_shutdown()
+ * to start with connection shutdown. The function does not block.
+ * Application then uses SSL_poll() on connection object to monitor
+ * progress of shutdown. The SSL_poll() indicates progress by signaling
+ * SSL_POLL_EVENT_EC event. Application must check connection object
+ * for error. If no error is indicated, then application must call
+ * SSL_shutdown() to move to the next stop in shutdown process.
+ */
 QUIC_TAKES_LOCK
 int ossl_quic_conn_shutdown(SSL *s, uint64_t flags,
                             const SSL_SHUTDOWN_EX_ARGS *args,
@@ -1527,6 +1557,19 @@ int ossl_quic_conn_shutdown(SSL *s, uint64_t flags,
         return 1;
     }
 
+    if (!wait_peer) {
+        /*
+         * Set shutdown reason now when local application wants to do
+         * active close (does not waant to wait for peer to close th
+         * connection). The reason will be sent to peer with connection
+         * close notification as soon as streams will be flushed.
+         */
+        if (args != NULL) {
+            ossl_quic_channel_set_tcause(ctx.qc->ch, args->quic_error_code,
+                                         args->quic_reason);
+        }
+    }
+
     /* Phase 1: Stream Flushing */
     if (!wait_peer && stream_flush) {
         qc_shutdown_flush_init(ctx.qc);
index 7162a53fbd6db294f3a1afc48b89556358f2a201..506ecadab553bf627c7b1c6bf932e1aa25913de2 100644 (file)
@@ -9,6 +9,7 @@
 
 #include "internal/quic_stream_map.h"
 #include "internal/nelem.h"
+#include "internal/quic_channel.h"
 
 /*
  * QUIC Stream Map
@@ -92,7 +93,7 @@ int ossl_quic_stream_map_init(QUIC_STREAM_MAP *qsm,
                               void *get_stream_limit_cb_arg,
                               QUIC_RXFC *max_streams_bidi_rxfc,
                               QUIC_RXFC *max_streams_uni_rxfc,
-                              int is_server)
+                              QUIC_CHANNEL *ch)
 {
     qsm->map = lh_QUIC_STREAM_new(hash_stream, cmp_stream);
     qsm->active_list.prev = qsm->active_list.next = &qsm->active_list;
@@ -111,7 +112,7 @@ int ossl_quic_stream_map_init(QUIC_STREAM_MAP *qsm,
     qsm->get_stream_limit_cb_arg    = get_stream_limit_cb_arg;
     qsm->max_streams_bidi_rxfc      = max_streams_bidi_rxfc;
     qsm->max_streams_uni_rxfc       = max_streams_uni_rxfc;
-    qsm->is_server                  = is_server;
+    qsm->ch                         = ch;
     return 1;
 }
 
@@ -156,7 +157,7 @@ QUIC_STREAM *ossl_quic_stream_map_alloc(QUIC_STREAM_MAP *qsm,
 
     s->id           = stream_id;
     s->type         = type;
-    s->as_server    = qsm->is_server;
+    s->as_server    = ossl_quic_channel_is_server(qsm->ch);
     s->send_state   = (ossl_quic_stream_is_local_init(s)
                        || ossl_quic_stream_is_bidi(s))
         ? QUIC_SSTREAM_STATE_READY
@@ -329,7 +330,7 @@ void ossl_quic_stream_map_update_state(QUIC_STREAM_MAP *qsm, QUIC_STREAM *s)
 {
     int should_be_active, allowed_by_stream_limit = 1;
 
-    if (ossl_quic_stream_is_server_init(s) == qsm->is_server) {
+    if (ossl_quic_stream_is_server_init(s) == ossl_quic_channel_is_server(qsm->ch)) {
         int is_uni = !ossl_quic_stream_is_bidi(s);
         uint64_t stream_ordinal = s->id >> 2;
 
@@ -425,6 +426,14 @@ static void shutdown_flush_done(QUIC_STREAM_MAP *qsm, QUIC_STREAM *qs)
     assert(qsm->num_shutdown_flush > 0);
     qs->shutdown_flush = 0;
     --qsm->num_shutdown_flush;
+
+    /*
+     * when num_shutdown_flush becomes zero we need to poke
+     * SSL_poll() it's time to poke to SSL_shutdown() to proceed
+     * with shutdown process as all streams are gone (flushed).
+     */
+    if (qsm->num_shutdown_flush == 0)
+        ossl_quic_channel_notify_flush_done(qsm->ch);
 }
 
 int ossl_quic_stream_map_notify_totally_acked(QUIC_STREAM_MAP *qsm,
index 51a16db0ea4a619e4dd6aa57f6a80e07fa65b4c7..74b0d20cd74038682dbad0ac7609f11e5f11f648 100644 (file)
@@ -198,6 +198,7 @@ struct script_op {
 #define OPK_C_WRITE_EX2                             52
 #define OPK_SKIP_IF_BLOCKING                        53
 #define OPK_C_STREAM_RESET_FAIL                     54
+#define OPK_C_SHUTDOWN                              55
 
 #define EXPECT_CONN_CLOSE_APP       (1U << 0)
 #define EXPECT_CONN_CLOSE_REMOTE    (1U << 1)
@@ -270,6 +271,8 @@ struct script_op {
     {OPK_C_SET_DEFAULT_STREAM_MODE, NULL, (mode), NULL, NULL},
 #define OP_C_SET_INCOMING_STREAM_POLICY(policy) \
     {OPK_C_SET_INCOMING_STREAM_POLICY, NULL, (policy), NULL, NULL},
+#define OP_C_SHUTDOWN(reason, flags) \
+    {OPK_C_SHUTDOWN, (reason), (flags), NULL, NULL},
 #define OP_C_SHUTDOWN_WAIT(reason, flags) \
     {OPK_C_SHUTDOWN_WAIT, (reason), (flags), NULL, NULL},
 #define OP_C_EXPECT_CONN_CLOSE_INFO(ec, app, remote)                \
@@ -1658,6 +1661,25 @@ static int run_script_worker(struct helper *h, const struct script_op *script,
             }
             break;
 
+        case OPK_C_SHUTDOWN:
+            {
+                int ret;
+                QUIC_CHANNEL *ch = ossl_quic_conn_get_channel(h->c_conn);
+                SSL_SHUTDOWN_EX_ARGS args = {0};
+
+                ossl_quic_engine_set_inhibit_tick(ossl_quic_channel_get0_engine(ch), 0);
+
+                if (!TEST_ptr(c_tgt))
+                    goto out;
+
+                args.quic_reason = (const char *)op->arg0;
+
+                ret = SSL_shutdown_ex(c_tgt, op->arg1, &args, sizeof(args));
+                if (!TEST_int_ge(ret, 0))
+                    goto out;
+            }
+            break;
+
         case OPK_C_SHUTDOWN_WAIT:
             {
                 int ret;
@@ -5787,6 +5809,189 @@ static const struct script_op script_85[] = {
     OP_END
 };
 
+#define POLL_FMT "%s%s%s%s%s%s%s%s%s%s%s%s%s"
+#define POLL_PRINTA(_revents_) \
+    (_revents_) & SSL_POLL_EVENT_F ? "SSL_POLL_EVENT_F " : "", \
+    (_revents_) & SSL_POLL_EVENT_EL ? "SSL_POLL_EVENT_EL " : "", \
+    (_revents_) & SSL_POLL_EVENT_EC ? "SSL_POLL_EVENT_EC " : "", \
+    (_revents_) & SSL_POLL_EVENT_ECD ? "SSL_POLL_EVENT_ECD " : "", \
+    (_revents_) & SSL_POLL_EVENT_ER ? "SSL_POLL_EVENT_ER " : "", \
+    (_revents_) & SSL_POLL_EVENT_EW ? "SSL_POLL_EVENT_EW " : "", \
+    (_revents_) & SSL_POLL_EVENT_R ? "SSL_POLL_EVENT_R " : "", \
+    (_revents_) & SSL_POLL_EVENT_W ? "SSL_POLL_EVENT_W " : "", \
+    (_revents_) & SSL_POLL_EVENT_IC ? "SSL_POLL_EVENT_IC " : "", \
+    (_revents_) & SSL_POLL_EVENT_ISB ? "SSL_POLL_EVENT_ISB " : "", \
+    (_revents_) & SSL_POLL_EVENT_ISU ? "SSL_POLL_EVENT_ISU " : "", \
+    (_revents_) & SSL_POLL_EVENT_OSB ? "SSL_POLL_EVENT_OSB " : "", \
+    (_revents_) & SSL_POLL_EVENT_OSU ? "SSL_POLL_EVENT_OSU " : ""
+
+/* 88. Test SSL_poll (lite, non-blocking) */
+ossl_unused static int script_88_poll(struct helper *h, struct helper_local *hl)
+{
+    int ok = 1, ret, expected_ret = 1;
+    static const struct timeval timeout = {0};
+    size_t result_count, processed;
+    SSL_POLL_ITEM items[2] = {0}, *item = items;
+    SSL *c_a;
+    size_t i;
+    uint64_t mode, expected_revents[2] = {0};
+
+    if (!TEST_ptr(c_a = helper_local_get_c_stream(hl, "a")))
+        return 0;
+
+    item->desc    = SSL_as_poll_descriptor(c_a);
+    item->events  = UINT64_MAX;
+    item->revents = UINT64_MAX;
+    ++item;
+
+    item->desc    = SSL_as_poll_descriptor(h->c_conn);
+    item->events  = UINT64_MAX;
+    item->revents = UINT64_MAX;
+    ++item;
+
+    result_count = SIZE_MAX;
+    ret = SSL_poll(items, OSSL_NELEM(items), sizeof(SSL_POLL_ITEM),
+                   &timeout, 0,
+                   &result_count);
+
+    mode = hl->check_op->arg2;
+    switch (mode) {
+    case 0:
+        /* No incoming data yet */
+        expected_revents[0]     = SSL_POLL_EVENT_W;
+        expected_revents[1]     = SSL_POLL_EVENT_OS;
+        break;
+    case 1:
+        /* Expect more events */
+        expected_revents[0]     = SSL_POLL_EVENT_R;
+        expected_revents[1]     = SSL_POLL_EVENT_OS;
+        break;
+    default:
+        return 0;
+    }
+
+    if (!TEST_int_eq(ret, expected_ret))
+        ok = 0;
+
+    /*
+     * Unlike script 85 which always expects all objects
+     * get signaled in single call to SSL_poll() we must
+     * assume here we can get notification for only one.
+     */
+    processed = 0;
+    for (i = 0; i < OSSL_NELEM(items); ++i) {
+        if (items[i].revents == 0)
+            continue;
+
+        processed++;
+        if (!TEST_uint64_t_eq(items[i].revents, expected_revents[i])) {
+            TEST_info("wanted: " POLL_FMT " got: " POLL_FMT,
+                      POLL_PRINTA(expected_revents[i]),
+                      POLL_PRINTA(items[i].revents));
+            TEST_error("mismatch at index %zu in poll results, mode %d",
+                       i, (int)mode);
+            ok = 0;
+        }
+    }
+
+    if (!TEST_size_t_eq(processed, result_count))
+        ok = 0;
+
+    return ok;
+}
+
+ossl_unused static int script_88_poll_conly(struct helper *h, struct helper_local *hl)
+{
+    int ok = 1;
+    static const struct timeval timeout = {0};
+    size_t result_count;
+    SSL_POLL_ITEM items[1] = {0};
+    int done = 0;
+    OSSL_TIME t_limit;
+
+    result_count = SIZE_MAX;
+
+    items[0].desc    = SSL_as_poll_descriptor(h->c_conn);
+    items[0].events  = UINT64_MAX;
+    items[0].revents = UINT64_MAX;
+
+    t_limit = ossl_time_add(ossl_time_now(),
+                            ossl_ticks2time(5 * OSSL_TIME_SECOND));
+    while (done == 0 && ok == 1) {
+        ok = SSL_poll(items, OSSL_NELEM(items), sizeof(SSL_POLL_ITEM),
+                       &timeout, 0,
+                       &result_count);
+
+        if (!TEST_int_eq(ok, 1))
+            continue;
+
+        if (result_count == 0)
+            OSSL_sleep(10);
+
+        TEST_info("received event " POLL_FMT, POLL_PRINTA(items[0].revents));
+
+        if ((items[0].revents & SSL_POLL_EVENT_EC) == SSL_POLL_EVENT_EC)
+            SSL_shutdown(h->c_conn);
+        done =
+            ((items[0].revents & SSL_POLL_EVENT_ECD) == SSL_POLL_EVENT_ECD);
+
+        if (ossl_time_compare(ossl_time_now(), t_limit) == 1) {
+            TEST_error("shutdown time exceeded 5sec");
+            ok = 0;
+        }
+    }
+
+    return ok;
+}
+
+/*
+ * verify SSL_poll() signals SSL_POLL_EVENT_EC event
+ * to notify client it's time to call SSL_shutdown().
+ */
+static const struct script_op script_88[] = {
+    OP_SKIP_IF_BLOCKING     (16)
+    OP_C_SET_ALPN           ("ossltest")
+    OP_C_CONNECT_WAIT       ()
+
+    OP_C_SET_DEFAULT_STREAM_MODE(SSL_DEFAULT_STREAM_MODE_NONE)
+
+    OP_C_NEW_STREAM_BIDI    (a, C_BIDI_ID(0))
+    OP_S_BIND_STREAM_ID     (a, C_BIDI_ID(0))
+
+    /* Check nothing readable yet. */
+    OP_CHECK                (script_88_poll, 0 /* ->arg2 */)
+
+    OP_C_WRITE              (a, "flamingo", 8)
+    OP_C_CONCLUDE           (a)
+
+    /* Send something that will make client sockets readable. */
+    OP_S_READ_EXPECT        (a, "flamingo", 8)
+    OP_S_WRITE              (a, "flamingo", 8)
+    OP_S_CONCLUDE           (a)
+
+    OP_CHECK                (script_88_poll, 1 /* ->arg2 */)
+
+    OP_C_READ_EXPECT        (a, "flamingo", 8)
+
+    /*
+     * client calls non-blocking SSL_shutdown() and gives
+     * server chance to run by calling sleep.
+     */
+    OP_C_SHUTDOWN           (NULL, 0)
+    OP_SLEEP(100)
+
+    /*
+     * Here we call SSL_poll() and handle SSL_POLL_EVENT_EC
+     * and SSL_POLL_EVENT_ECD on connection object. Whenever
+     * _EC event comes we call SSL_shutdown() to keep connection
+     * draining. We keep calling SSL_poll()/SSL_shutdown() until
+     * SSL_poll() signals SSL_POLL_EVENT_ECD to let us know connection
+     * has dried out and con be closed.
+     */
+    OP_CHECK                (script_88_poll_conly, 0)
+
+    OP_END
+};
 /* 86. Event Handling Mode Configuration */
 static int set_event_handling_mode_conn(struct helper *h, struct helper_local *hl)
 {
@@ -5979,7 +6184,8 @@ static const struct script_op *const scripts[] = {
     script_84,
     script_85,
     script_86,
-    script_87
+    script_87,
+    script_88,
 };
 
 static int test_script(int idx)
@@ -6009,6 +6215,7 @@ static int test_script(int idx)
 
     TEST_info("Running script %d (order=%d, blocking=%d)", script_idx + 1,
               free_order, blocking);
+
     return run_script(scripts[script_idx], script_name, free_order, blocking);
 }
 
index c9ac68ef3917e6d97254908635e31b763530e084..172bd3f39385719488dfb8df409b9af2d4909791 100644 (file)
@@ -11,6 +11,7 @@
 #include "internal/quic_statm.h"
 #include "internal/quic_demux.h"
 #include "internal/quic_record_rx.h"
+#include "internal/quic_channel.h"
 #include "testutil.h"
 #include "quic_record_test_util.h"
 
@@ -74,6 +75,7 @@ struct helper {
         OSSL_QUIC_FRAME_CONN_CLOSE      conn_close;
     } frame;
     OSSL_QUIC_ACK_RANGE     ack_ranges[16];
+    QUIC_CHANNEL *client_ch;
 };
 
 static void helper_cleanup(struct helper *h)
@@ -106,6 +108,7 @@ static void helper_cleanup(struct helper *h)
     ossl_quic_demux_free(h->demux);
     BIO_free(h->bio1);
     BIO_free(h->bio2);
+    ossl_quic_channel_free(h->client_ch);
 }
 
 static void demux_default_handler(QUIC_URXE *e, void *arg,
@@ -123,8 +126,10 @@ static int helper_init(struct helper *h)
 {
     int rc = 0;
     size_t i;
+    QUIC_CHANNEL_ARGS client_ch_args;
 
     memset(h, 0, sizeof(*h));
+    memset(&client_ch_args, 0, sizeof(client_ch_args));
 
     /* Initialisation */
     if (!TEST_true(BIO_new_bio_dgram_pair(&h->bio1, 0, &h->bio2, 0)))
@@ -186,10 +191,13 @@ static int helper_init(struct helper *h)
                                                /* is_server */0)))
         goto err;
 
+    h->client_ch = ossl_quic_channel_alloc(&client_ch_args);
+    if (!TEST_ptr(h->client_ch))
+        goto err;
     if (!TEST_true(ossl_quic_stream_map_init(&h->qsm, NULL, NULL,
                                              &h->max_streams_bidi_rxfc,
                                              &h->max_streams_uni_rxfc,
-                                             /*is_server=*/0)))
+                                             h->client_ch)))
         goto err;
 
     h->have_qsm = 1;