Resolve some of the TODO(QUIC) items
[openssl.git] / ssl / quic / quic_impl.c
index fbcc85e52a1b2ef2018f2649d4cb34fb6cdeae80..ca00fcd476b42cd0a845ae400a5f8c73087cbbc4 100644 (file)
 #include "internal/quic_error.h"
 #include "internal/time.h"
 
+typedef struct qctx_st QCTX;
+
 static void aon_write_finish(QUIC_XSO *xso);
 static int create_channel(QUIC_CONNECTION *qc);
 static QUIC_XSO *create_xso_from_stream(QUIC_CONNECTION *qc, QUIC_STREAM *qs);
-static int qc_try_create_default_xso_for_write(QUIC_CONNECTION *qc);
-static int qc_wait_for_default_xso_for_read(QUIC_CONNECTION *qc);
+static int qc_try_create_default_xso_for_write(QCTX *ctx);
+static int qc_wait_for_default_xso_for_read(QCTX *ctx);
 static void quic_lock(QUIC_CONNECTION *qc);
 static void quic_unlock(QUIC_CONNECTION *qc);
-static int quic_do_handshake(QUIC_CONNECTION *qc);
+static int quic_do_handshake(QCTX *ctx);
 static void qc_update_reject_policy(QUIC_CONNECTION *qc);
 static void qc_touch_default_xso(QUIC_CONNECTION *qc);
 static void qc_set_default_xso(QUIC_CONNECTION *qc, QUIC_XSO *xso, int touch);
-static SSL *quic_conn_stream_new(QUIC_CONNECTION *qc, uint64_t flags,
-                                 int need_lock);
+static void qc_set_default_xso_keep_ref(QUIC_CONNECTION *qc, QUIC_XSO *xso,
+                                        int touch, QUIC_XSO **old_xso);
+static SSL *quic_conn_stream_new(QCTX *ctx, uint64_t flags, int need_lock);
+static int quic_validate_for_write(QUIC_XSO *xso, int *err);
+static int quic_mutation_allowed(QUIC_CONNECTION *qc, int req_active);
 
 /*
  * QUIC Front-End I/O API: Common Utilities
@@ -51,19 +56,64 @@ static int block_until_pred(QUIC_CONNECTION *qc,
 
     assert(qc->ch != NULL);
 
+    /*
+     * Any attempt to block auto-disables tick inhibition as otherwise we will
+     * hang around forever.
+     */
+    ossl_quic_channel_set_inhibit_tick(qc->ch, 0);
+
     rtor = ossl_quic_channel_get_reactor(qc->ch);
     return ossl_quic_reactor_block_until_pred(rtor, pred, pred_arg, flags,
                                               qc->mutex);
 }
 
+static OSSL_TIME get_time(QUIC_CONNECTION *qc)
+{
+    if (qc->override_now_cb != NULL)
+        return qc->override_now_cb(qc->override_now_cb_arg);
+    else
+        return ossl_time_now();
+}
+
+static OSSL_TIME get_time_cb(void *arg)
+{
+    QUIC_CONNECTION *qc = arg;
+
+    return get_time(qc);
+}
+
+/*
+ * QCTX is a utility structure which provides information we commonly wish to
+ * unwrap upon an API call being dispatched to us, namely:
+ *
+ *   - a pointer to the QUIC_CONNECTION (regardless of whether a QCSO or QSSO
+ *     was passed);
+ *   - a pointer to any applicable QUIC_XSO (e.g. if a QSSO was passed, or if
+ *     a QCSO with a default stream was passed);
+ *   - whether a QSSO was passed (xso == NULL must not be used to determine this
+ *     because it may be non-NULL when a QCSO is passed if that QCSO has a
+ *     default stream).
+ */
+struct qctx_st {
+    QUIC_CONNECTION *qc;
+    QUIC_XSO        *xso;
+    int             is_stream;
+};
+
 /*
  * Raise a 'normal' error, meaning one that can be reported via SSL_get_error()
- * rather than via ERR.
+ * rather than via ERR. Note that normal errors must always be raised while
+ * holding a lock.
  */
-static int quic_raise_normal_error(QUIC_CONNECTION *qc,
+QUIC_NEEDS_LOCK
+static int quic_raise_normal_error(QCTX *ctx,
                                    int err)
 {
-    qc->last_error = err;
+    if (ctx->is_stream)
+        ctx->xso->last_error = err;
+    else
+        ctx->qc->last_error = err;
+
     return 0;
 }
 
@@ -75,8 +125,10 @@ static int quic_raise_normal_error(QUIC_CONNECTION *qc,
  * not known NULL may be passed. This should generally only happen when an
  * expect_...() function defined below fails, which generally indicates a
  * dispatch error or caller error.
+ *
+ * ctx should be NULL if the connection lock is not held.
  */
-static int quic_raise_non_normal_error(QUIC_CONNECTION *qc,
+static int quic_raise_non_normal_error(QCTX *ctx,
                                        const char *file,
                                        int line,
                                        const char *func,
@@ -86,6 +138,16 @@ static int quic_raise_non_normal_error(QUIC_CONNECTION *qc,
 {
     va_list args;
 
+    if (ctx != NULL) {
+        if (ctx->is_stream && ctx->xso != NULL)
+            ctx->xso->last_error = SSL_ERROR_SSL;
+        else if (!ctx->is_stream && ctx->qc != NULL)
+            ctx->qc->last_error = SSL_ERROR_SSL;
+
+        if (reason == SSL_R_PROTOCOL_IS_SHUTDOWN && ctx->qc != NULL)
+            ossl_quic_channel_restore_err_state(ctx->qc->ch);
+    }
+
     ERR_new();
     ERR_set_debug(file, line, func);
 
@@ -93,40 +155,19 @@ static int quic_raise_non_normal_error(QUIC_CONNECTION *qc,
     ERR_vset_error(ERR_LIB_SSL, reason, fmt, args);
     va_end(args);
 
-    if (qc != NULL)
-        qc->last_error = SSL_ERROR_SSL;
-
     return 0;
 }
 
-#define QUIC_RAISE_NORMAL_ERROR(qc, err)                        \
-    quic_raise_normal_error((qc), (err))
+#define QUIC_RAISE_NORMAL_ERROR(ctx, err)                       \
+    quic_raise_normal_error((ctx), (err))
 
-#define QUIC_RAISE_NON_NORMAL_ERROR(qc, reason, msg)            \
-    quic_raise_non_normal_error((qc),                           \
+#define QUIC_RAISE_NON_NORMAL_ERROR(ctx, reason, msg)           \
+    quic_raise_non_normal_error((ctx),                          \
                                 OPENSSL_FILE, OPENSSL_LINE,     \
                                 OPENSSL_FUNC,                   \
                                 (reason),                       \
                                 (msg))
 
-/*
- * QCTX is a utility structure which provides information we commonly wish to
- * unwrap upon an API call being dispatched to us, namely:
- *
- *   - a pointer to the QUIC_CONNECTION (regardless of whether a QCSO or QSSO
- *     was passed);
- *   - a pointer to any applicable QUIC_XSO (e.g. if a QSSO was passed, or if
- *     a QCSO with a default stream was passed);
- *   - whether a QSSO was passed (xso == NULL must not be used to determine this
- *     because it may be non-NULL when a QCSO is passed if that QCSO has a
- *     default stream).
- */
-typedef struct qctx_st {
-    QUIC_CONNECTION *qc;
-    QUIC_XSO        *xso;
-    int             is_stream;
-} QCTX;
-
 /*
  * Given a QCSO or QSSO, initialises a QCTX, determining the contextually
  * applicable QUIC_CONNECTION pointer and, if applicable, QUIC_XSO pointer.
@@ -187,21 +228,21 @@ static int ossl_unused expect_quic_with_stream_lock(const SSL *s, int remote_ini
     quic_lock(ctx->qc);
 
     if (ctx->xso == NULL && remote_init >= 0) {
-        if (ossl_quic_channel_is_term_any(ctx->qc->ch)) {
-            QUIC_RAISE_NON_NORMAL_ERROR(ctx->qc, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
+        if (!quic_mutation_allowed(ctx->qc, /*req_active=*/0)) {
+            QUIC_RAISE_NON_NORMAL_ERROR(ctx, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
             goto err;
         }
 
         /* If we haven't finished the handshake, try to advance it. */
-        if (quic_do_handshake(ctx->qc) < 1)
+        if (quic_do_handshake(ctx) < 1)
             /* ossl_quic_do_handshake raised error here */
             goto err;
 
         if (remote_init == 0) {
-            if (!qc_try_create_default_xso_for_write(ctx->qc))
+            if (!qc_try_create_default_xso_for_write(ctx))
                 goto err;
         } else {
-            if (!qc_wait_for_default_xso_for_read(ctx->qc))
+            if (!qc_wait_for_default_xso_for_read(ctx))
                 goto err;
         }
 
@@ -209,7 +250,7 @@ static int ossl_unused expect_quic_with_stream_lock(const SSL *s, int remote_ini
     }
 
     if (ctx->xso == NULL) {
-        QUIC_RAISE_NON_NORMAL_ERROR(ctx->qc, SSL_R_NO_STREAM, NULL);
+        QUIC_RAISE_NON_NORMAL_ERROR(ctx, SSL_R_NO_STREAM, NULL);
         goto err;
     }
 
@@ -230,7 +271,7 @@ static int ossl_unused expect_quic_conn_only(const SSL *s, QCTX *ctx)
         return 0;
 
     if (ctx->is_stream)
-        return QUIC_RAISE_NON_NORMAL_ERROR(ctx->qc, SSL_R_CONN_USE_ONLY, NULL);
+        return QUIC_RAISE_NON_NORMAL_ERROR(ctx, SSL_R_CONN_USE_ONLY, NULL);
 
     return 1;
 }
@@ -243,16 +284,40 @@ static int ossl_unused expect_quic_conn_only(const SSL *s, QCTX *ctx)
  */
 static void quic_lock(QUIC_CONNECTION *qc)
 {
+#if defined(OPENSSL_THREADS)
     ossl_crypto_mutex_lock(qc->mutex);
+#endif
 }
 
 /* Precondition: Channel mutex is held (unchecked) */
 QUIC_NEEDS_LOCK
 static void quic_unlock(QUIC_CONNECTION *qc)
 {
+#if defined(OPENSSL_THREADS)
     ossl_crypto_mutex_unlock(qc->mutex);
+#endif
 }
 
+/*
+ * This predicate is the criterion which should determine API call rejection for
+ * *most* mutating API calls, particularly stream-related operations for send
+ * parts.
+ *
+ * A call is rejected (this function returns 0) if shutdown is in progress
+ * (stream flushing), or we are in a TERMINATING or TERMINATED state. If
+ * req_active=1, the connection must be active (i.e., the IDLE state is also
+ * rejected).
+ */
+static int quic_mutation_allowed(QUIC_CONNECTION *qc, int req_active)
+{
+    if (qc->shutting_down || ossl_quic_channel_is_term_any(qc->ch))
+        return 0;
+
+    if (req_active && !ossl_quic_channel_is_active(qc->ch))
+        return 0;
+
+    return 1;
+}
 
 /*
  * QUIC Front-End I/O API: Initialization
@@ -265,6 +330,10 @@ static void quic_unlock(QUIC_CONNECTION *qc)
  *                                     ossl_quic_deinit
  *         SSL_free                 => ossl_quic_free
  *
+ *         SSL_set_options          => ossl_quic_set_options
+ *         SSL_get_options          => ossl_quic_get_options
+ *         SSL_clear_options        => ossl_quic_clear_options
+ *
  */
 
 /* SSL_new */
@@ -289,25 +358,40 @@ SSL *ossl_quic_new(SSL_CTX *ctx)
     if (qc->tls == NULL || (sc = SSL_CONNECTION_FROM_SSL(qc->tls)) == NULL)
          goto err;
 
+    /* override the user_ssl of the inner connection */
+    sc->s3.flags |= TLS1_FLAGS_QUIC;
+
+    /* Restrict options derived from the SSL_CTX. */
+    sc->options &= OSSL_QUIC_PERMITTED_OPTIONS_CONN;
+    sc->pha_enabled = 0;
+
+#if defined(OPENSSL_THREADS)
     if ((qc->mutex = ossl_crypto_mutex_new()) == NULL)
         goto err;
+#endif
 
+#if !defined(OPENSSL_NO_QUIC_THREAD_ASSIST)
     qc->is_thread_assisted
         = (ssl_base->method == OSSL_QUIC_client_thread_method());
+#endif
 
-    qc->as_server       = 0; /* TODO(QUIC): server support */
+    qc->as_server       = 0; /* TODO(QUIC SERVER): add server support */
     qc->as_server_state = qc->as_server;
 
     qc->default_stream_mode     = SSL_DEFAULT_STREAM_MODE_AUTO_BIDI;
     qc->default_ssl_mode        = qc->ssl.ctx->mode;
+    qc->default_ssl_options     = qc->ssl.ctx->options & OSSL_QUIC_PERMITTED_OPTIONS;
     qc->default_blocking        = 1;
-    qc->incoming_stream_reject_policy
-        = SSL_INCOMING_STREAM_REJECT_POLICY_AUTO;
+    qc->blocking                = 1;
+    qc->incoming_stream_policy  = SSL_INCOMING_STREAM_POLICY_AUTO;
     qc->last_error              = SSL_ERROR_NONE;
 
     if (!create_channel(qc))
         goto err;
 
+    ossl_quic_channel_set_msg_callback(qc->ch, ctx->msg_callback, ssl_base);
+    ossl_quic_channel_set_msg_callback_arg(qc->ch, ctx->msg_callback_arg);
+
     qc_update_reject_policy(qc);
 
     /*
@@ -324,6 +408,9 @@ SSL *ossl_quic_new(SSL_CTX *ctx)
 
 err:
     if (qc != NULL) {
+#if defined(OPENSSL_THREADS)
+        ossl_crypto_mutex_free(qc->mutex);
+#endif
         ossl_quic_channel_free(qc->ch);
         SSL_free(qc->tls);
     }
@@ -336,11 +423,14 @@ QUIC_TAKES_LOCK
 void ossl_quic_free(SSL *s)
 {
     QCTX ctx;
+    int is_default;
 
     /* We should never be called on anything but a QSO. */
     if (!expect_quic(s, &ctx))
         return;
 
+    quic_lock(ctx.qc);
+
     if (ctx.is_stream) {
         /*
          * When a QSSO is freed, the XSO is freed immediately, because the XSO
@@ -349,30 +439,44 @@ void ossl_quic_free(SSL *s)
          * as deleted for later collection.
          */
 
-        quic_lock(ctx.qc);
-
         assert(ctx.qc->num_xso > 0);
         --ctx.qc->num_xso;
 
-        ctx.xso->stream->deleted = 1;
+        /* If a stream's send part has not been finished, auto-reset it. */
+        if ((   ctx.xso->stream->send_state == QUIC_SSTREAM_STATE_READY
+             || ctx.xso->stream->send_state == QUIC_SSTREAM_STATE_SEND)
+            && !ossl_quic_sstream_get_final_size(ctx.xso->stream->sstream, NULL))
+            ossl_quic_stream_map_reset_stream_send_part(ossl_quic_channel_get_qsm(ctx.qc->ch),
+                                                        ctx.xso->stream, 0);
 
-        /* Auto-conclude stream. */
-        /* TODO(QUIC): Do RESET_STREAM here instead of auto-conclude */
-        if (ctx.xso->stream->sstream != NULL)
-            ossl_quic_sstream_fin(ctx.xso->stream->sstream);
+        /* Do STOP_SENDING for the receive part, if applicable. */
+        if (   ctx.xso->stream->recv_state == QUIC_RSTREAM_STATE_RECV
+            || ctx.xso->stream->recv_state == QUIC_RSTREAM_STATE_SIZE_KNOWN)
+            ossl_quic_stream_map_stop_sending_recv_part(ossl_quic_channel_get_qsm(ctx.qc->ch),
+                                                        ctx.xso->stream, 0);
 
         /* Update stream state. */
-        ossl_quic_stream_map_update_state(ossl_quic_channel_get_qsm(ctx.xso->conn->ch),
+        ctx.xso->stream->deleted = 1;
+        ossl_quic_stream_map_update_state(ossl_quic_channel_get_qsm(ctx.qc->ch),
                                           ctx.xso->stream);
 
+        is_default = (ctx.xso == ctx.qc->default_xso);
         quic_unlock(ctx.qc);
 
+        /*
+         * Unref the connection in most cases; the XSO has a ref to the QC and
+         * not vice versa. But for a default XSO, to avoid circular references,
+         * the QC refs the XSO but the XSO does not ref the QC. If we are the
+         * default XSO, we only get here when the QC is being torn down anyway,
+         * so don't call SSL_free(qc) as we are already in it.
+         */
+        if (!is_default)
+            SSL_free(&ctx.qc->ssl);
+
         /* Note: SSL_free calls OPENSSL_free(xso) for us */
         return;
     }
 
-    quic_lock(ctx.qc);
-
     /*
      * Free the default XSO, if any. The QUIC_STREAM is not deleted at this
      * stage, but is freed during the channel free when the whole QSM is freed.
@@ -383,15 +487,18 @@ void ossl_quic_free(SSL *s)
         quic_unlock(ctx.qc);
         SSL_free(&xso->ssl);
         quic_lock(ctx.qc);
+        ctx.qc->default_xso = NULL;
     }
 
     /* Ensure we have no remaining XSOs. */
     assert(ctx.qc->num_xso == 0);
 
+#if !defined(OPENSSL_NO_QUIC_THREAD_ASSIST)
     if (ctx.qc->is_thread_assisted && ctx.qc->started) {
         ossl_quic_thread_assist_wait_stopped(&ctx.qc->thread_assist);
         ossl_quic_thread_assist_cleanup(&ctx.qc->thread_assist);
     }
+#endif
 
     ossl_quic_channel_free(ctx.qc->ch);
 
@@ -401,7 +508,10 @@ void ossl_quic_free(SSL *s)
     /* Note: SSL_free calls OPENSSL_free(qc) for us */
 
     SSL_free(ctx.qc->tls);
-    ossl_crypto_mutex_free(&ctx.qc->mutex); /* freed while still locked */
+    quic_unlock(ctx.qc); /* tsan doesn't like freeing locked mutexes */
+#if defined(OPENSSL_THREADS)
+    ossl_crypto_mutex_free(&ctx.qc->mutex);
+#endif
 }
 
 /* SSL method init */
@@ -417,7 +527,7 @@ void ossl_quic_deinit(SSL *s)
     /* No-op. */
 }
 
-/* SSL_reset */
+/* SSL_clear (ssl_reset method) */
 int ossl_quic_reset(SSL *s)
 {
     QCTX ctx;
@@ -425,11 +535,11 @@ int ossl_quic_reset(SSL *s)
     if (!expect_quic(s, &ctx))
         return 0;
 
-    /* TODO(QUIC); Currently a no-op. */
-    return 1;
+    /* Not supported. */
+    return 0;
 }
 
-/* SSL_clear */
+/* ssl_clear method (unused) */
 int ossl_quic_clear(SSL *s)
 {
     QCTX ctx;
@@ -437,21 +547,26 @@ int ossl_quic_clear(SSL *s)
     if (!expect_quic(s, &ctx))
         return 0;
 
-    /* TODO(QUIC): Currently a no-op. */
+    /* TODO(QUIC FUTURE): Currently a no-op. */
     return 1;
 }
 
-void ossl_quic_conn_set_override_now_cb(SSL *s,
-                                        OSSL_TIME (*now_cb)(void *arg),
-                                        void *now_cb_arg)
+int ossl_quic_conn_set_override_now_cb(SSL *s,
+                                       OSSL_TIME (*now_cb)(void *arg),
+                                       void *now_cb_arg)
 {
     QCTX ctx;
 
     if (!expect_quic(s, &ctx))
-        return;
+        return 0;
+
+    quic_lock(ctx.qc);
 
     ctx.qc->override_now_cb     = now_cb;
     ctx.qc->override_now_cb_arg = now_cb_arg;
+
+    quic_unlock(ctx.qc);
+    return 1;
 }
 
 void ossl_quic_conn_force_assist_thread_wake(SSL *s)
@@ -461,8 +576,10 @@ void ossl_quic_conn_force_assist_thread_wake(SSL *s)
     if (!expect_quic(s, &ctx))
         return;
 
+#if !defined(OPENSSL_NO_QUIC_THREAD_ASSIST)
     if (ctx.qc->is_thread_assisted && ctx.qc->started)
         ossl_quic_thread_assist_notify_deadline_changed(&ctx.qc->thread_assist);
+#endif
 }
 
 QUIC_NEEDS_LOCK
@@ -472,14 +589,150 @@ static void qc_touch_default_xso(QUIC_CONNECTION *qc)
     qc_update_reject_policy(qc);
 }
 
+/*
+ * Changes default XSO. Allows caller to keep reference to the old default XSO
+ * (if any). Reference to new XSO is transferred from caller.
+ */
 QUIC_NEEDS_LOCK
-static void qc_set_default_xso(QUIC_CONNECTION *qc, QUIC_XSO *xso, int touch)
+static void qc_set_default_xso_keep_ref(QUIC_CONNECTION *qc, QUIC_XSO *xso,
+                                        int touch,
+                                        QUIC_XSO **old_xso)
 {
-    qc->default_xso = xso;
+    int refs;
+
+    *old_xso = NULL;
+
+    if (qc->default_xso != xso) {
+        *old_xso = qc->default_xso; /* transfer old XSO ref to caller */
+
+        qc->default_xso = xso;
+
+        if (xso == NULL) {
+            /*
+             * Changing to not having a default XSO. XSO becomes standalone and
+             * now has a ref to the QC.
+             */
+            if (!ossl_assert(SSL_up_ref(&qc->ssl)))
+                return;
+        } else {
+            /*
+             * Changing from not having a default XSO to having one. The new XSO
+             * will have had a reference to the QC we need to drop to avoid a
+             * circular reference.
+             *
+             * Currently we never change directly from one default XSO to
+             * another, though this function would also still be correct if this
+             * weren't the case.
+             */
+            assert(*old_xso == NULL);
+
+            CRYPTO_DOWN_REF(&qc->ssl.references, &refs);
+            assert(refs > 0);
+        }
+    }
+
     if (touch)
         qc_touch_default_xso(qc);
 }
 
+/*
+ * Changes default XSO, releasing the reference to any previous default XSO.
+ * Reference to new XSO is transferred from caller.
+ */
+QUIC_NEEDS_LOCK
+static void qc_set_default_xso(QUIC_CONNECTION *qc, QUIC_XSO *xso, int touch)
+{
+    QUIC_XSO *old_xso = NULL;
+
+    qc_set_default_xso_keep_ref(qc, xso, touch, &old_xso);
+
+    if (old_xso != NULL)
+        SSL_free(&old_xso->ssl);
+}
+
+QUIC_NEEDS_LOCK
+static void xso_update_options(QUIC_XSO *xso)
+{
+    int cleanse = ((xso->ssl_options & SSL_OP_CLEANSE_PLAINTEXT) != 0);
+
+    if (xso->stream->rstream != NULL)
+        ossl_quic_rstream_set_cleanse(xso->stream->rstream, cleanse);
+
+    if (xso->stream->sstream != NULL)
+        ossl_quic_sstream_set_cleanse(xso->stream->sstream, cleanse);
+}
+
+/*
+ * SSL_set_options
+ * ---------------
+ *
+ * Setting options on a QCSO
+ *   - configures the handshake-layer options;
+ *   - configures the default data-plane options for new streams;
+ *   - configures the data-plane options on the default XSO, if there is one.
+ *
+ * Setting options on a QSSO
+ *   - configures data-plane options for that stream only.
+ */
+QUIC_TAKES_LOCK
+static uint64_t quic_mask_or_options(SSL *ssl, uint64_t mask_value, uint64_t or_value)
+{
+    QCTX ctx;
+    uint64_t hs_mask_value, hs_or_value, ret;
+
+    if (!expect_quic(ssl, &ctx))
+        return 0;
+
+    quic_lock(ctx.qc);
+
+    if (!ctx.is_stream) {
+        /*
+         * If we were called on the connection, we apply any handshake option
+         * changes.
+         */
+        hs_mask_value = (mask_value & OSSL_QUIC_PERMITTED_OPTIONS_CONN);
+        hs_or_value   = (or_value   & OSSL_QUIC_PERMITTED_OPTIONS_CONN);
+
+        SSL_clear_options(ctx.qc->tls, hs_mask_value);
+        SSL_set_options(ctx.qc->tls, hs_or_value);
+
+        /* Update defaults for new streams. */
+        ctx.qc->default_ssl_options
+            = ((ctx.qc->default_ssl_options & ~mask_value) | or_value)
+              & OSSL_QUIC_PERMITTED_OPTIONS;
+    }
+
+    if (ctx.xso != NULL) {
+        ctx.xso->ssl_options
+            = ((ctx.xso->ssl_options & ~mask_value) | or_value)
+            & OSSL_QUIC_PERMITTED_OPTIONS_STREAM;
+
+        xso_update_options(ctx.xso);
+    }
+
+    ret = ctx.is_stream ? ctx.xso->ssl_options : ctx.qc->default_ssl_options;
+
+    quic_unlock(ctx.qc);
+    return ret;
+}
+
+uint64_t ossl_quic_set_options(SSL *ssl, uint64_t options)
+{
+    return quic_mask_or_options(ssl, 0, options);
+}
+
+/* SSL_clear_options */
+uint64_t ossl_quic_clear_options(SSL *ssl, uint64_t options)
+{
+    return quic_mask_or_options(ssl, options, 0);
+}
+
+/* SSL_get_options */
+uint64_t ossl_quic_get_options(const SSL *ssl)
+{
+    return quic_mask_or_options((SSL *)ssl, 0, 0);
+}
+
 /*
  * QUIC Front-End I/O API: Network BIO Configuration
  * =================================================
@@ -670,7 +923,7 @@ int ossl_quic_conn_set_blocking_mode(SSL *s, int blocking)
     /* Cannot enable blocking mode if we do not have pollable FDs. */
     if (blocking != 0 &&
         (!ctx.qc->can_poll_net_rbio || !ctx.qc->can_poll_net_wbio))
-        return QUIC_RAISE_NON_NORMAL_ERROR(ctx.qc, ERR_R_UNSUPPORTED, NULL);
+        return QUIC_RAISE_NON_NORMAL_ERROR(&ctx, ERR_R_UNSUPPORTED, NULL);
 
     if (!ctx.is_stream) {
         /*
@@ -700,7 +953,7 @@ int ossl_quic_conn_set_initial_peer_addr(SSL *s,
         return 0;
 
     if (ctx.qc->started)
-        return QUIC_RAISE_NON_NORMAL_ERROR(ctx.qc, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED,
+        return QUIC_RAISE_NON_NORMAL_ERROR(&ctx, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED,
                                            NULL);
 
     if (peer_addr == NULL) {
@@ -716,8 +969,8 @@ int ossl_quic_conn_set_initial_peer_addr(SSL *s,
  * QUIC Front-End I/O API: Asynchronous I/O Management
  * ===================================================
  *
- *   (BIO/)SSL_tick                 => ossl_quic_tick
- *   (BIO/)SSL_get_tick_timeout     => ossl_quic_get_tick_timeout
+ *   (BIO/)SSL_handle_events        => ossl_quic_handle_events
+ *   (BIO/)SSL_get_event_timeout    => ossl_quic_get_event_timeout
  *   (BIO/)SSL_get_poll_fd          => ossl_quic_get_poll_fd
  *
  */
@@ -735,9 +988,9 @@ static int xso_blocking_mode(const QUIC_XSO *xso)
         && xso->conn->can_poll_net_wbio;
 }
 
-/* SSL_tick; ticks the reactor. */
+/* SSL_handle_events; performs QUIC I/O and timeout processing. */
 QUIC_TAKES_LOCK
-int ossl_quic_tick(SSL *s)
+int ossl_quic_handle_events(SSL *s)
 {
     QCTX ctx;
 
@@ -751,13 +1004,14 @@ int ossl_quic_tick(SSL *s)
 }
 
 /*
- * SSL_get_tick_timeout. Get the time in milliseconds until the SSL object
- * should be ticked by the application by calling SSL_tick(). tv is set to 0 if
- * the object should be ticked immediately and tv->tv_sec is set to -1 if no
- * timeout is currently active.
+ * SSL_get_event_timeout. Get the time in milliseconds until the SSL object
+ * should next have events handled by the application by calling
+ * SSL_handle_events(). tv is set to 0 if the object should have events handled
+ * immediately. If no timeout is currently active, *is_infinite is set to 1 and
+ * the value of *tv is undefined.
  */
 QUIC_TAKES_LOCK
-int ossl_quic_get_tick_timeout(SSL *s, struct timeval *tv)
+int ossl_quic_get_event_timeout(SSL *s, struct timeval *tv, int *is_infinite)
 {
     QCTX ctx;
     OSSL_TIME deadline = ossl_time_infinite();
@@ -771,13 +1025,21 @@ int ossl_quic_get_tick_timeout(SSL *s, struct timeval *tv)
         = ossl_quic_reactor_get_tick_deadline(ossl_quic_channel_get_reactor(ctx.qc->ch));
 
     if (ossl_time_is_infinite(deadline)) {
-        tv->tv_sec  = -1;
+        *is_infinite = 1;
+
+        /*
+         * Robustness against faulty applications that don't check *is_infinite;
+         * harmless long timeout.
+         */
+        tv->tv_sec  = 1000000;
         tv->tv_usec = 0;
+
         quic_unlock(ctx.qc);
         return 1;
     }
 
-    *tv = ossl_time_to_timeval(ossl_time_subtract(deadline, ossl_time_now()));
+    *tv = ossl_time_to_timeval(ossl_time_subtract(deadline, get_time(ctx.qc)));
+    *is_infinite = 0;
     quic_unlock(ctx.qc);
     return 1;
 }
@@ -856,6 +1118,30 @@ int ossl_quic_get_net_write_desired(SSL *s)
  *
  */
 
+QUIC_NEEDS_LOCK
+static void qc_shutdown_flush_init(QUIC_CONNECTION *qc)
+{
+    QUIC_STREAM_MAP *qsm;
+
+    if (qc->shutting_down)
+        return;
+
+    qsm = ossl_quic_channel_get_qsm(qc->ch);
+
+    ossl_quic_stream_map_begin_shutdown_flush(qsm);
+    qc->shutting_down = 1;
+}
+
+/* Returns 1 if all shutdown-flush streams have been done with. */
+QUIC_NEEDS_LOCK
+static int qc_shutdown_flush_finished(QUIC_CONNECTION *qc)
+{
+    QUIC_STREAM_MAP *qsm = ossl_quic_channel_get_qsm(qc->ch);
+
+    return qc->shutting_down
+        && ossl_quic_stream_map_is_shutdown_flush_finished(qsm);
+}
+
 /* SSL_shutdown */
 static int quic_shutdown_wait(void *arg)
 {
@@ -864,6 +1150,15 @@ static int quic_shutdown_wait(void *arg)
     return ossl_quic_channel_is_terminated(qc->ch);
 }
 
+/* Returns 1 if shutdown flush process has finished or is inapplicable. */
+static int quic_shutdown_flush_wait(void *arg)
+{
+    QUIC_CONNECTION *qc = arg;
+
+    return ossl_quic_channel_is_term_any(qc->ch)
+        || qc_shutdown_flush_finished(qc);
+}
+
 QUIC_TAKES_LOCK
 int ossl_quic_conn_shutdown(SSL *s, uint64_t flags,
                             const SSL_SHUTDOWN_EX_ARGS *args,
@@ -871,26 +1166,50 @@ int ossl_quic_conn_shutdown(SSL *s, uint64_t flags,
 {
     int ret;
     QCTX ctx;
+    int stream_flush = ((flags & SSL_SHUTDOWN_FLAG_NO_STREAM_FLUSH) == 0);
 
     if (!expect_quic(s, &ctx))
-        return 0;
+        return -1;
 
     if (ctx.is_stream)
-        /* TODO(QUIC): Semantics currently undefined for QSSOs */
         return -1;
 
     quic_lock(ctx.qc);
 
+    if (ossl_quic_channel_is_terminated(ctx.qc->ch)) {
+        quic_unlock(ctx.qc);
+        return 1;
+    }
+
+    /* Phase 1: Stream Flushing */
+    if (stream_flush) {
+        qc_shutdown_flush_init(ctx.qc);
+
+        if (!qc_shutdown_flush_finished(ctx.qc)) {
+            if (qc_blocking_mode(ctx.qc))
+                block_until_pred(ctx.qc, quic_shutdown_flush_wait, ctx.qc, 0);
+            else
+                ossl_quic_reactor_tick(ossl_quic_channel_get_reactor(ctx.qc->ch), 0);
+        }
+
+        if (!qc_shutdown_flush_finished(ctx.qc)) {
+            quic_unlock(ctx.qc);
+            return 0; /* ongoing */
+        }
+    }
+
+    /* Phase 2: Connection Closure */
     ossl_quic_channel_local_close(ctx.qc->ch,
                                   args != NULL ? args->quic_error_code : 0);
 
-    /* TODO(QUIC): !SSL_SHUTDOWN_FLAG_NO_STREAM_FLUSH */
+    SSL_set_shutdown(ctx.qc->tls, SSL_SENT_SHUTDOWN);
 
     if (ossl_quic_channel_is_terminated(ctx.qc->ch)) {
         quic_unlock(ctx.qc);
         return 1;
     }
 
+    /* Phase 3: Terminating Wait Time */
     if (qc_blocking_mode(ctx.qc) && (flags & SSL_SHUTDOWN_FLAG_RAPID) == 0)
         block_until_pred(ctx.qc, quic_shutdown_wait, ctx.qc, 0);
     else
@@ -939,6 +1258,24 @@ long ossl_quic_ctrl(SSL *s, int cmd, long larg, void *parg)
         }
 
         return ctx.qc->default_ssl_mode;
+
+    case SSL_CTRL_SET_MSG_CALLBACK_ARG:
+        ossl_quic_channel_set_msg_callback_arg(ctx.qc->ch, parg);
+        /* This ctrl also needs to be passed to the internal SSL object */
+        return SSL_ctrl(ctx.qc->tls, cmd, larg, parg);
+
+    case DTLS_CTRL_GET_TIMEOUT: /* DTLSv1_get_timeout */
+        {
+            int is_infinite;
+
+            if (!ossl_quic_get_event_timeout(s, parg, &is_infinite))
+                return 0;
+
+            return !is_infinite;
+        }
+    case DTLS_CTRL_HANDLE_TIMEOUT: /* DTLSv1_handle_timeout */
+        /* For legacy compatibility with DTLS calls. */
+        return ossl_quic_handle_events(s) == 1 ? 1 : -1;
     default:
         /* Probably a TLS related ctrl. Defer to our internal SSL object */
         return SSL_ctrl(ctx.qc->tls, cmd, larg, parg);
@@ -984,7 +1321,7 @@ static int quic_handshake_wait(void *arg)
 {
     struct quic_handshake_wait_args *args = arg;
 
-    if (!ossl_quic_channel_is_active(args->qc->ch))
+    if (!quic_mutation_allowed(args->qc, /*req_active=*/1))
         return -1;
 
     if (ossl_quic_channel_is_handshake_complete(args->qc->ch))
@@ -1015,8 +1352,8 @@ static int create_channel(QUIC_CONNECTION *qc)
     args.is_server  = qc->as_server;
     args.tls        = qc->tls;
     args.mutex      = qc->mutex;
-    args.now_cb     = qc->override_now_cb;
-    args.now_cb_arg = qc->override_now_cb_arg;
+    args.now_cb     = get_time_cb;
+    args.now_cb_arg = qc;
 
     qc->ch = ossl_quic_channel_new(&args);
     if (qc->ch == NULL)
@@ -1038,9 +1375,11 @@ static int ensure_channel_started(QUIC_CONNECTION *qc)
             || !ossl_quic_channel_start(qc->ch))
             goto err;
 
+#if !defined(OPENSSL_NO_QUIC_THREAD_ASSIST)
         if (qc->is_thread_assisted)
             if (!ossl_quic_thread_assist_init_start(&qc->thread_assist, qc->ch))
                 goto err;
+#endif
     }
 
     qc->started = 1;
@@ -1053,32 +1392,32 @@ err:
 }
 
 QUIC_NEEDS_LOCK
-static int quic_do_handshake(QUIC_CONNECTION *qc)
+static int quic_do_handshake(QCTX *ctx)
 {
     int ret;
+    QUIC_CONNECTION *qc = ctx->qc;
 
     if (ossl_quic_channel_is_handshake_complete(qc->ch))
         /* Handshake already completed. */
         return 1;
 
-    if (ossl_quic_channel_is_term_any(qc->ch))
-        return QUIC_RAISE_NON_NORMAL_ERROR(qc, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
+    if (!quic_mutation_allowed(qc, /*req_active=*/0))
+        return QUIC_RAISE_NON_NORMAL_ERROR(ctx, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
 
     if (BIO_ADDR_family(&qc->init_peer_addr) == AF_UNSPEC) {
         /* Peer address must have been set. */
-        QUIC_RAISE_NON_NORMAL_ERROR(qc, SSL_R_REMOTE_PEER_ADDRESS_NOT_SET, NULL);
+        QUIC_RAISE_NON_NORMAL_ERROR(ctx, SSL_R_REMOTE_PEER_ADDRESS_NOT_SET, NULL);
         return -1; /* Non-protocol error */
     }
 
     if (qc->as_server != qc->as_server_state) {
-        /* TODO(QUIC): Must match the method used to create the QCSO */
-        QUIC_RAISE_NON_NORMAL_ERROR(qc, ERR_R_PASSED_INVALID_ARGUMENT, NULL);
+        QUIC_RAISE_NON_NORMAL_ERROR(ctx, ERR_R_PASSED_INVALID_ARGUMENT, NULL);
         return -1; /* Non-protocol error */
     }
 
     if (qc->net_rbio == NULL || qc->net_wbio == NULL) {
         /* Need read and write BIOs. */
-        QUIC_RAISE_NON_NORMAL_ERROR(qc, SSL_R_BIO_NOT_SET, NULL);
+        QUIC_RAISE_NON_NORMAL_ERROR(ctx, SSL_R_BIO_NOT_SET, NULL);
         return -1; /* Non-protocol error */
     }
 
@@ -1087,7 +1426,7 @@ static int quic_do_handshake(QUIC_CONNECTION *qc)
      * non-blocking mode, which is fine.
      */
     if (!ensure_channel_started(qc)) {
-        QUIC_RAISE_NON_NORMAL_ERROR(qc, ERR_R_INTERNAL_ERROR, NULL);
+        QUIC_RAISE_NON_NORMAL_ERROR(ctx, ERR_R_INTERNAL_ERROR, NULL);
         return -1; /* Non-protocol error */
     }
 
@@ -1102,11 +1441,11 @@ static int quic_do_handshake(QUIC_CONNECTION *qc)
         args.qc     = qc;
 
         ret = block_until_pred(qc, quic_handshake_wait, &args, 0);
-        if (!ossl_quic_channel_is_active(qc->ch)) {
-            QUIC_RAISE_NON_NORMAL_ERROR(qc, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
+        if (!quic_mutation_allowed(qc, /*req_active=*/1)) {
+            QUIC_RAISE_NON_NORMAL_ERROR(ctx, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
             return 0; /* Shutdown before completion */
         } else if (ret <= 0) {
-            QUIC_RAISE_NON_NORMAL_ERROR(qc, ERR_R_INTERNAL_ERROR, NULL);
+            QUIC_RAISE_NON_NORMAL_ERROR(ctx, ERR_R_INTERNAL_ERROR, NULL);
             return -1; /* Non-protocol error */
         }
 
@@ -1121,7 +1460,7 @@ static int quic_do_handshake(QUIC_CONNECTION *qc)
             return 1;
 
         /* Otherwise, indicate that the handshake isn't done yet. */
-        QUIC_RAISE_NORMAL_ERROR(qc, SSL_ERROR_WANT_READ);
+        QUIC_RAISE_NORMAL_ERROR(ctx, SSL_ERROR_WANT_READ);
         return -1; /* Non-protocol error */
     }
 }
@@ -1137,7 +1476,7 @@ int ossl_quic_do_handshake(SSL *s)
 
     quic_lock(ctx.qc);
 
-    ret = quic_do_handshake(ctx.qc);
+    ret = quic_do_handshake(&ctx);
     quic_unlock(ctx.qc);
     return ret;
 }
@@ -1176,9 +1515,10 @@ int ossl_quic_accept(SSL *s)
  * exists). Note that this is NOT an error condition.
  */
 QUIC_NEEDS_LOCK
-static int qc_try_create_default_xso_for_write(QUIC_CONNECTION *qc)
+static int qc_try_create_default_xso_for_write(QCTX *ctx)
 {
     uint64_t flags = 0;
+    QUIC_CONNECTION *qc = ctx->qc;
 
     if (qc->default_xso_created
         || qc->default_stream_mode == SSL_DEFAULT_STREAM_MODE_NONE)
@@ -1186,17 +1526,17 @@ static int qc_try_create_default_xso_for_write(QUIC_CONNECTION *qc)
          * We only do this once. If the user detaches a previously created
          * default XSO we don't auto-create another one.
          */
-        return QUIC_RAISE_NON_NORMAL_ERROR(qc, SSL_R_NO_STREAM, NULL);
+        return QUIC_RAISE_NON_NORMAL_ERROR(ctx, SSL_R_NO_STREAM, NULL);
 
     /* Create a locally-initiated stream. */
     if (qc->default_stream_mode == SSL_DEFAULT_STREAM_MODE_AUTO_UNI)
         flags |= SSL_STREAM_FLAG_UNI;
 
-    qc_set_default_xso(qc, (QUIC_XSO *)quic_conn_stream_new(qc, flags,
+    qc_set_default_xso(qc, (QUIC_XSO *)quic_conn_stream_new(ctx, flags,
                                                             /*needs_lock=*/0),
                        /*touch=*/0);
     if (qc->default_xso == NULL)
-        return QUIC_RAISE_NON_NORMAL_ERROR(qc, ERR_R_INTERNAL_ERROR, NULL);
+        return QUIC_RAISE_NON_NORMAL_ERROR(ctx, ERR_R_INTERNAL_ERROR, NULL);
 
     qc_touch_default_xso(qc);
     return 1;
@@ -1205,6 +1545,7 @@ static int qc_try_create_default_xso_for_write(QUIC_CONNECTION *qc)
 struct quic_wait_for_stream_args {
     QUIC_CONNECTION *qc;
     QUIC_STREAM     *qs;
+    QCTX            *ctx;
     uint64_t        expect_id;
 };
 
@@ -1213,14 +1554,18 @@ static int quic_wait_for_stream(void *arg)
 {
     struct quic_wait_for_stream_args *args = arg;
 
-    if (!ossl_quic_channel_is_active(args->qc->ch)) {
+    if (!quic_mutation_allowed(args->qc, /*req_active=*/1)) {
         /* If connection is torn down due to an error while blocking, stop. */
-        QUIC_RAISE_NON_NORMAL_ERROR(args->qc, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
+        QUIC_RAISE_NON_NORMAL_ERROR(args->ctx, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
         return -1;
     }
 
     args->qs = ossl_quic_stream_map_get_by_id(ossl_quic_channel_get_qsm(args->qc->ch),
-                                              args->expect_id);
+                                              args->expect_id | QUIC_STREAM_DIR_BIDI);
+    if (args->qs == NULL)
+        args->qs = ossl_quic_stream_map_get_by_id(ossl_quic_channel_get_qsm(args->qc->ch),
+                                                  args->expect_id | QUIC_STREAM_DIR_UNI);
+
     if (args->qs != NULL)
         return 1; /* stream now exists */
 
@@ -1228,10 +1573,11 @@ static int quic_wait_for_stream(void *arg)
 }
 
 QUIC_NEEDS_LOCK
-static int qc_wait_for_default_xso_for_read(QUIC_CONNECTION *qc)
+static int qc_wait_for_default_xso_for_read(QCTX *ctx)
 {
     /* Called on a QCSO and we don't currently have a default stream. */
     uint64_t expect_id;
+    QUIC_CONNECTION *qc = ctx->qc;
     QUIC_STREAM *qs;
     int res;
     struct quic_wait_for_stream_args wargs;
@@ -1242,7 +1588,7 @@ static int qc_wait_for_default_xso_for_read(QUIC_CONNECTION *qc)
      */
     if (qc->default_xso_created
         || qc->default_stream_mode == SSL_DEFAULT_STREAM_MODE_NONE)
-        return QUIC_RAISE_NON_NORMAL_ERROR(qc, SSL_R_NO_STREAM, NULL);
+        return QUIC_RAISE_NON_NORMAL_ERROR(ctx, SSL_R_NO_STREAM, NULL);
 
     /*
      * The peer may have opened a stream since we last ticked. So tick and
@@ -1254,12 +1600,12 @@ static int qc_wait_for_default_xso_for_read(QUIC_CONNECTION *qc)
         ? QUIC_STREAM_INITIATOR_CLIENT
         : QUIC_STREAM_INITIATOR_SERVER;
 
-    expect_id |= (qc->default_stream_mode == SSL_DEFAULT_STREAM_MODE_AUTO_UNI)
-        ? QUIC_STREAM_DIR_UNI
-        : QUIC_STREAM_DIR_BIDI;
-
     qs = ossl_quic_stream_map_get_by_id(ossl_quic_channel_get_qsm(qc->ch),
-                                        expect_id);
+                                        expect_id | QUIC_STREAM_DIR_BIDI);
+    if (qs == NULL)
+        qs = ossl_quic_stream_map_get_by_id(ossl_quic_channel_get_qsm(qc->ch),
+                                            expect_id | QUIC_STREAM_DIR_UNI);
+
     if (qs == NULL) {
         ossl_quic_reactor_tick(ossl_quic_channel_get_reactor(qc->ch), 0);
 
@@ -1270,16 +1616,17 @@ static int qc_wait_for_default_xso_for_read(QUIC_CONNECTION *qc)
     if (qs == NULL) {
         if (!qc_blocking_mode(qc))
             /* Non-blocking mode, so just bail immediately. */
-            return QUIC_RAISE_NORMAL_ERROR(qc, SSL_ERROR_WANT_READ);
+            return QUIC_RAISE_NORMAL_ERROR(ctx, SSL_ERROR_WANT_READ);
 
         /* Block until we have a stream. */
         wargs.qc        = qc;
         wargs.qs        = NULL;
+        wargs.ctx       = ctx;
         wargs.expect_id = expect_id;
 
         res = block_until_pred(qc, quic_wait_for_stream, &wargs, 0);
         if (res == 0)
-            return QUIC_RAISE_NON_NORMAL_ERROR(qc, ERR_R_INTERNAL_ERROR, NULL);
+            return QUIC_RAISE_NON_NORMAL_ERROR(ctx, ERR_R_INTERNAL_ERROR, NULL);
         else if (res < 0 || wargs.qs == NULL)
             /* quic_wait_for_stream raised error here */
             return 0;
@@ -1293,7 +1640,7 @@ static int qc_wait_for_default_xso_for_read(QUIC_CONNECTION *qc)
      */
     qc_set_default_xso(qc, create_xso_from_stream(qc, qs), /*touch=*/0);
     if (qc->default_xso == NULL)
-        return QUIC_RAISE_NON_NORMAL_ERROR(qc, ERR_R_INTERNAL_ERROR, NULL);
+        return QUIC_RAISE_NON_NORMAL_ERROR(ctx, ERR_R_INTERNAL_ERROR, NULL);
 
     qc_touch_default_xso(qc); /* inhibits default XSO */
     return 1;
@@ -1310,13 +1657,21 @@ static QUIC_XSO *create_xso_from_stream(QUIC_CONNECTION *qc, QUIC_STREAM *qs)
     if (!ossl_ssl_init(&xso->ssl, qc->ssl.ctx, qc->ssl.method, SSL_TYPE_QUIC_XSO))
         goto err;
 
+    /* XSO refs QC */
+    if (!SSL_up_ref(&qc->ssl))
+        goto err;
+
     xso->conn       = qc;
     xso->blocking   = qc->default_blocking;
     xso->ssl_mode   = qc->default_ssl_mode;
+    xso->ssl_options
+        = qc->default_ssl_options & OSSL_QUIC_PERMITTED_OPTIONS_STREAM;
+    xso->last_error = SSL_ERROR_NONE;
 
     xso->stream     = qs;
 
     ++qc->num_xso;
+    xso_update_options(xso);
     return xso;
 
 err:
@@ -1325,9 +1680,9 @@ err:
 }
 
 /* locking depends on need_lock */
-static SSL *quic_conn_stream_new(QUIC_CONNECTION *qc, uint64_t flags,
-                                 int need_lock)
+static SSL *quic_conn_stream_new(QCTX *ctx, uint64_t flags, int need_lock)
 {
+    QUIC_CONNECTION *qc = ctx->qc;
     QUIC_XSO *xso = NULL;
     QUIC_STREAM *qs = NULL;
     int is_uni = ((flags & SSL_STREAM_FLAG_UNI) != 0);
@@ -1335,8 +1690,8 @@ static SSL *quic_conn_stream_new(QUIC_CONNECTION *qc, uint64_t flags,
     if (need_lock)
         quic_lock(qc);
 
-    if (ossl_quic_channel_is_term_any(qc->ch)) {
-        QUIC_RAISE_NON_NORMAL_ERROR(qc, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
+    if (!quic_mutation_allowed(qc, /*req_active=*/0)) {
+        QUIC_RAISE_NON_NORMAL_ERROR(ctx, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
         goto err;
     }
 
@@ -1372,7 +1727,7 @@ SSL *ossl_quic_conn_stream_new(SSL *s, uint64_t flags)
     if (!expect_quic_conn_only(s, &ctx))
         return NULL;
 
-    return quic_conn_stream_new(ctx.qc, flags, /*need_lock=*/1);
+    return quic_conn_stream_new(&ctx, flags, /*need_lock=*/1);
 }
 
 /*
@@ -1391,17 +1746,27 @@ SSL *ossl_quic_conn_stream_new(SSL *s, uint64_t flags)
  *   (BIO/)SSL_write            => ossl_quic_write
  *         SSL_pending          => ossl_quic_pending
  *         SSL_stream_conclude  => ossl_quic_conn_stream_conclude
+ *         SSL_key_update       => ossl_quic_key_update
  */
 
 /* SSL_get_error */
 int ossl_quic_get_error(const SSL *s, int i)
 {
     QCTX ctx;
+    int net_error, last_error;
 
     if (!expect_quic(s, &ctx))
         return 0;
 
-    return ctx.qc->last_error;
+    quic_lock(ctx.qc);
+    net_error = ossl_quic_channel_net_error(ctx.qc->ch);
+    last_error = ctx.is_stream ? ctx.xso->last_error : ctx.qc->last_error;
+    quic_unlock(ctx.qc);
+
+    if (net_error)
+        return SSL_ERROR_SYSCALL;
+
+    return last_error;
 }
 
 /*
@@ -1433,8 +1798,8 @@ static void quic_post_write(QUIC_XSO *xso, int did_append, int do_tick)
     /*
      * Try and send.
      *
-     * TODO(QUIC): It is probably inefficient to try and do this immediately,
-     * plus we should eventually consider Nagle's algorithm.
+     * TODO(QUIC FUTURE): It is probably inefficient to try and do this
+     * immediately, plus we should eventually consider Nagle's algorithm.
      */
     if (do_tick)
         ossl_quic_reactor_tick(ossl_quic_channel_get_reactor(xso->conn->ch), 0);
@@ -1445,6 +1810,7 @@ struct quic_write_again_args {
     const unsigned char *buf;
     size_t              len;
     size_t              total_written;
+    int                 err;
 };
 
 QUIC_NEEDS_LOCK
@@ -1453,10 +1819,18 @@ static int quic_write_again(void *arg)
     struct quic_write_again_args *args = arg;
     size_t actual_written = 0;
 
-    if (!ossl_quic_channel_is_active(args->xso->conn->ch))
+    if (!quic_mutation_allowed(args->xso->conn, /*req_active=*/1))
         /* If connection is torn down due to an error while blocking, stop. */
         return -2;
 
+    if (!quic_validate_for_write(args->xso, &args->err))
+        /*
+         * Stream may have become invalid for write due to connection events
+         * while we blocked.
+         */
+        return -2;
+
+    args->err = ERR_R_INTERNAL_ERROR;
     if (!ossl_quic_sstream_append(args->xso->stream->sstream,
                                   args->buf, args->len, &actual_written))
         return -2;
@@ -1476,10 +1850,11 @@ static int quic_write_again(void *arg)
 }
 
 QUIC_NEEDS_LOCK
-static int quic_write_blocking(QUIC_XSO *xso, const void *buf, size_t len,
+static int quic_write_blocking(QCTX *ctx, const void *buf, size_t len,
                                size_t *written)
 {
     int res;
+    QUIC_XSO *xso = ctx->xso;
     struct quic_write_again_args args;
     size_t actual_written = 0;
 
@@ -1488,7 +1863,7 @@ static int quic_write_blocking(QUIC_XSO *xso, const void *buf, size_t len,
                                   &actual_written)) {
         /* Stream already finished or allocation error. */
         *written = 0;
-        return QUIC_RAISE_NON_NORMAL_ERROR(xso->conn, ERR_R_INTERNAL_ERROR, NULL);
+        return QUIC_RAISE_NON_NORMAL_ERROR(ctx, ERR_R_INTERNAL_ERROR, NULL);
     }
 
     quic_post_write(xso, actual_written > 0, 1);
@@ -1508,13 +1883,14 @@ static int quic_write_blocking(QUIC_XSO *xso, const void *buf, size_t len,
     args.buf            = (const unsigned char *)buf + actual_written;
     args.len            = len - actual_written;
     args.total_written  = 0;
+    args.err            = ERR_R_INTERNAL_ERROR;
 
     res = block_until_pred(xso->conn, quic_write_again, &args, 0);
     if (res <= 0) {
-        if (!ossl_quic_channel_is_active(xso->conn->ch))
-            return QUIC_RAISE_NON_NORMAL_ERROR(xso->conn, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
+        if (!quic_mutation_allowed(xso->conn, /*req_active=*/1))
+            return QUIC_RAISE_NON_NORMAL_ERROR(ctx, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
         else
-            return QUIC_RAISE_NON_NORMAL_ERROR(xso->conn, ERR_R_INTERNAL_ERROR, NULL);
+            return QUIC_RAISE_NON_NORMAL_ERROR(ctx, args.err, NULL);
     }
 
     *written = args.total_written;
@@ -1545,9 +1921,10 @@ static void aon_write_finish(QUIC_XSO *xso)
 }
 
 QUIC_NEEDS_LOCK
-static int quic_write_nonblocking_aon(QUIC_XSO *xso, const void *buf,
+static int quic_write_nonblocking_aon(QCTX *ctx, const void *buf,
                                       size_t len, size_t *written)
 {
+    QUIC_XSO *xso = ctx->xso;
     const void *actual_buf;
     size_t actual_len, actual_written = 0;
     int accept_moving_buffer
@@ -1565,7 +1942,7 @@ static int quic_write_nonblocking_aon(QUIC_XSO *xso, const void *buf,
              * Pointer must not have changed if we are not in accept moving
              * buffer mode. Length must never change.
              */
-            return QUIC_RAISE_NON_NORMAL_ERROR(xso->conn, SSL_R_BAD_WRITE_RETRY, NULL);
+            return QUIC_RAISE_NON_NORMAL_ERROR(ctx, SSL_R_BAD_WRITE_RETRY, NULL);
 
         actual_buf = (unsigned char *)buf + xso->aon_buf_pos;
         actual_len = len - xso->aon_buf_pos;
@@ -1580,7 +1957,7 @@ static int quic_write_nonblocking_aon(QUIC_XSO *xso, const void *buf,
                                   &actual_written)) {
         /* Stream already finished or allocation error. */
         *written = 0;
-        return QUIC_RAISE_NON_NORMAL_ERROR(xso->conn, ERR_R_INTERNAL_ERROR, NULL);
+        return QUIC_RAISE_NON_NORMAL_ERROR(ctx, ERR_R_INTERNAL_ERROR, NULL);
     }
 
     quic_post_write(xso, actual_written > 0, 1);
@@ -1612,7 +1989,7 @@ static int quic_write_nonblocking_aon(QUIC_XSO *xso, const void *buf,
          */
         xso->aon_buf_pos += actual_written;
         assert(xso->aon_buf_pos < xso->aon_buf_len);
-        return QUIC_RAISE_NORMAL_ERROR(xso->conn, SSL_ERROR_WANT_WRITE);
+        return QUIC_RAISE_NORMAL_ERROR(ctx, SSL_ERROR_WANT_WRITE);
     }
 
     /*
@@ -1628,43 +2005,84 @@ static int quic_write_nonblocking_aon(QUIC_XSO *xso, const void *buf,
      * completes.
      */
     *written = 0;
-    return QUIC_RAISE_NORMAL_ERROR(xso->conn, SSL_ERROR_WANT_WRITE);
+    return QUIC_RAISE_NORMAL_ERROR(ctx, SSL_ERROR_WANT_WRITE);
 }
 
 QUIC_NEEDS_LOCK
-static int quic_write_nonblocking_epw(QUIC_XSO *xso, const void *buf, size_t len,
+static int quic_write_nonblocking_epw(QCTX *ctx, const void *buf, size_t len,
                                       size_t *written)
 {
+    QUIC_XSO *xso = ctx->xso;
+
     /* Simple best effort operation. */
     if (!ossl_quic_sstream_append(xso->stream->sstream, buf, len, written)) {
         /* Stream already finished or allocation error. */
         *written = 0;
-        return QUIC_RAISE_NON_NORMAL_ERROR(xso->conn, ERR_R_INTERNAL_ERROR, NULL);
+        return QUIC_RAISE_NON_NORMAL_ERROR(ctx, ERR_R_INTERNAL_ERROR, NULL);
     }
 
     quic_post_write(xso, *written > 0, 1);
     return 1;
 }
 
+QUIC_NEEDS_LOCK
+static int quic_validate_for_write(QUIC_XSO *xso, int *err)
+{
+    QUIC_STREAM_MAP *qsm;
+
+    if (xso == NULL || xso->stream == NULL) {
+        *err = ERR_R_INTERNAL_ERROR;
+        return 0;
+    }
+
+    switch (xso->stream->send_state) {
+    default:
+    case QUIC_SSTREAM_STATE_NONE:
+        *err = SSL_R_STREAM_RECV_ONLY;
+        return 0;
+
+    case QUIC_SSTREAM_STATE_READY:
+        qsm = ossl_quic_channel_get_qsm(xso->conn->ch);
+
+        if (!ossl_quic_stream_map_ensure_send_part_id(qsm, xso->stream)) {
+            *err = ERR_R_INTERNAL_ERROR;
+            return 0;
+        }
+
+        /* FALLTHROUGH */
+    case QUIC_SSTREAM_STATE_SEND:
+    case QUIC_SSTREAM_STATE_DATA_SENT:
+    case QUIC_SSTREAM_STATE_DATA_RECVD:
+        if (ossl_quic_sstream_get_final_size(xso->stream->sstream, NULL)) {
+            *err = SSL_R_STREAM_FINISHED;
+            return 0;
+        }
+
+        return 1;
+
+    case QUIC_SSTREAM_STATE_RESET_SENT:
+    case QUIC_SSTREAM_STATE_RESET_RECVD:
+        *err = SSL_R_STREAM_RESET;
+        return 0;
+    }
+}
+
 QUIC_TAKES_LOCK
 int ossl_quic_write(SSL *s, const void *buf, size_t len, size_t *written)
 {
     int ret;
     QCTX ctx;
-    int partial_write;
+    int partial_write, err;
 
     *written = 0;
 
-    if (len == 0)
-        return 1;
-
     if (!expect_quic_with_stream_lock(s, /*remote_init=*/0, &ctx))
         return 0;
 
     partial_write = ((ctx.xso->ssl_mode & SSL_MODE_ENABLE_PARTIAL_WRITE) != 0);
 
-    if (ossl_quic_channel_is_term_any(ctx.qc->ch)) {
-        ret = QUIC_RAISE_NON_NORMAL_ERROR(ctx.qc, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
+    if (!quic_mutation_allowed(ctx.qc, /*req_active=*/0)) {
+        ret = QUIC_RAISE_NON_NORMAL_ERROR(&ctx, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
         goto out;
     }
 
@@ -1672,22 +2090,28 @@ int ossl_quic_write(SSL *s, const void *buf, size_t len, size_t *written)
      * If we haven't finished the handshake, try to advance it.
      * We don't accept writes until the handshake is completed.
      */
-    if (quic_do_handshake(ctx.qc) < 1) {
+    if (quic_do_handshake(&ctx) < 1) {
         ret = 0;
         goto out;
     }
 
-    if (ctx.xso->stream == NULL || ctx.xso->stream->sstream == NULL) {
-        ret = QUIC_RAISE_NON_NORMAL_ERROR(ctx.qc, ERR_R_INTERNAL_ERROR, NULL);
+    /* Ensure correct stream state, stream send part not concluded, etc. */
+    if (!quic_validate_for_write(ctx.xso, &err)) {
+        ret = QUIC_RAISE_NON_NORMAL_ERROR(&ctx, err, NULL);
+        goto out;
+    }
+
+    if (len == 0) {
+        ret = 1;
         goto out;
     }
 
     if (xso_blocking_mode(ctx.xso))
-        ret = quic_write_blocking(ctx.xso, buf, len, written);
+        ret = quic_write_blocking(&ctx, buf, len, written);
     else if (partial_write)
-        ret = quic_write_nonblocking_epw(ctx.xso, buf, len, written);
+        ret = quic_write_nonblocking_epw(&ctx, buf, len, written);
     else
-        ret = quic_write_nonblocking_aon(ctx.xso, buf, len, written);
+        ret = quic_write_nonblocking_aon(&ctx, buf, len, written);
 
 out:
     quic_unlock(ctx.qc);
@@ -1699,7 +2123,7 @@ out:
  * --------
  */
 struct quic_read_again_args {
-    QUIC_CONNECTION *qc;
+    QCTX            *ctx;
     QUIC_STREAM     *stream;
     void            *buf;
     size_t          len;
@@ -1708,30 +2132,69 @@ struct quic_read_again_args {
 };
 
 QUIC_NEEDS_LOCK
-static int quic_read_actual(QUIC_CONNECTION *qc,
+static int quic_validate_for_read(QUIC_XSO *xso, int *err, int *eos)
+{
+    QUIC_STREAM_MAP *qsm;
+
+    *eos = 0;
+
+    if (xso == NULL || xso->stream == NULL) {
+        *err = ERR_R_INTERNAL_ERROR;
+        return 0;
+    }
+
+    switch (xso->stream->recv_state) {
+    default:
+    case QUIC_RSTREAM_STATE_NONE:
+        *err = SSL_R_STREAM_SEND_ONLY;
+        return 0;
+
+    case QUIC_RSTREAM_STATE_RECV:
+    case QUIC_RSTREAM_STATE_SIZE_KNOWN:
+    case QUIC_RSTREAM_STATE_DATA_RECVD:
+        return 1;
+
+    case QUIC_RSTREAM_STATE_DATA_READ:
+        *eos = 1;
+        return 0;
+
+    case QUIC_RSTREAM_STATE_RESET_RECVD:
+        qsm = ossl_quic_channel_get_qsm(xso->conn->ch);
+        ossl_quic_stream_map_notify_app_read_reset_recv_part(qsm, xso->stream);
+
+        /* FALLTHROUGH */
+    case QUIC_RSTREAM_STATE_RESET_READ:
+        *err = SSL_R_STREAM_RESET;
+        return 0;
+    }
+}
+
+QUIC_NEEDS_LOCK
+static int quic_read_actual(QCTX *ctx,
                             QUIC_STREAM *stream,
                             void *buf, size_t buf_len,
                             size_t *bytes_read,
                             int peek)
 {
-    int is_fin = 0;
-
-    /* If the receive part of the stream is over, issue EOF. */
-    if (stream->recv_fin_retired)
-        return QUIC_RAISE_NORMAL_ERROR(qc, SSL_ERROR_ZERO_RETURN);
+    int is_fin = 0, err, eos;
+    QUIC_CONNECTION *qc = ctx->qc;
 
-    if (stream->rstream == NULL)
-        return QUIC_RAISE_NON_NORMAL_ERROR(qc, ERR_R_INTERNAL_ERROR, NULL);
+    if (!quic_validate_for_read(ctx->xso, &err, &eos)) {
+        if (eos)
+            return QUIC_RAISE_NORMAL_ERROR(ctx, SSL_ERROR_ZERO_RETURN);
+        else
+            return QUIC_RAISE_NON_NORMAL_ERROR(ctx, err, NULL);
+    }
 
     if (peek) {
         if (!ossl_quic_rstream_peek(stream->rstream, buf, buf_len,
                                     bytes_read, &is_fin))
-            return QUIC_RAISE_NON_NORMAL_ERROR(qc, ERR_R_INTERNAL_ERROR, NULL);
+            return QUIC_RAISE_NON_NORMAL_ERROR(ctx, ERR_R_INTERNAL_ERROR, NULL);
 
     } else {
         if (!ossl_quic_rstream_read(stream->rstream, buf, buf_len,
                                     bytes_read, &is_fin))
-            return QUIC_RAISE_NON_NORMAL_ERROR(qc, ERR_R_INTERNAL_ERROR, NULL);
+            return QUIC_RAISE_NON_NORMAL_ERROR(ctx, ERR_R_INTERNAL_ERROR, NULL);
     }
 
     if (!peek) {
@@ -1748,11 +2211,14 @@ static int quic_read_actual(QUIC_CONNECTION *qc,
 
             if (!ossl_quic_rxfc_on_retire(&stream->rxfc, *bytes_read,
                                           rtt_info.smoothed_rtt))
-                return QUIC_RAISE_NON_NORMAL_ERROR(qc, ERR_R_INTERNAL_ERROR, NULL);
+                return QUIC_RAISE_NON_NORMAL_ERROR(ctx, ERR_R_INTERNAL_ERROR, NULL);
         }
 
-        if (is_fin)
-            stream->recv_fin_retired = 1;
+        if (is_fin && !peek) {
+            QUIC_STREAM_MAP *qsm = ossl_quic_channel_get_qsm(ctx->qc->ch);
+
+            ossl_quic_stream_map_notify_totally_read(qsm, ctx->xso->stream);
+        }
 
         if (*bytes_read > 0)
             ossl_quic_stream_map_update_state(ossl_quic_channel_get_qsm(qc->ch),
@@ -1767,13 +2233,13 @@ static int quic_read_again(void *arg)
 {
     struct quic_read_again_args *args = arg;
 
-    if (!ossl_quic_channel_is_active(args->qc->ch)) {
+    if (!quic_mutation_allowed(args->ctx->qc, /*req_active=*/1)) {
         /* If connection is torn down due to an error while blocking, stop. */
-        QUIC_RAISE_NON_NORMAL_ERROR(args->qc, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
+        QUIC_RAISE_NON_NORMAL_ERROR(args->ctx, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
         return -1;
     }
 
-    if (!quic_read_actual(args->qc, args->stream,
+    if (!quic_read_actual(args->ctx, args->stream,
                           args->buf, args->len, args->bytes_read,
                           args->peek))
         return -1;
@@ -1799,13 +2265,13 @@ static int quic_read(SSL *s, void *buf, size_t len, size_t *bytes_read, int peek
 
     quic_lock(ctx.qc);
 
-    if (ossl_quic_channel_is_term_any(ctx.qc->ch)) {
-        ret = QUIC_RAISE_NON_NORMAL_ERROR(ctx.qc, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
+    if (!quic_mutation_allowed(ctx.qc, /*req_active=*/0)) {
+        ret = QUIC_RAISE_NON_NORMAL_ERROR(&ctx, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
         goto out;
     }
 
     /* If we haven't finished the handshake, try to advance it. */
-    if (quic_do_handshake(ctx.qc) < 1) {
+    if (quic_do_handshake(&ctx) < 1) {
         ret = 0; /* ossl_quic_do_handshake raised error here */
         goto out;
     }
@@ -1817,7 +2283,7 @@ static int quic_read(SSL *s, void *buf, size_t len, size_t *bytes_read, int peek
          * Wait until we get a stream initiated by the peer (blocking mode) or
          * fail if we don't have one yet (non-blocking mode).
          */
-        if (!qc_wait_for_default_xso_for_read(ctx.qc)) {
+        if (!qc_wait_for_default_xso_for_read(&ctx)) {
             ret = 0; /* error already raised here */
             goto out;
         }
@@ -1825,12 +2291,7 @@ static int quic_read(SSL *s, void *buf, size_t len, size_t *bytes_read, int peek
         ctx.xso = ctx.qc->default_xso;
     }
 
-    if (ctx.xso->stream == NULL) {
-        ret = QUIC_RAISE_NON_NORMAL_ERROR(ctx.qc, ERR_R_INTERNAL_ERROR, NULL);
-        goto out;
-    }
-
-    if (!quic_read_actual(ctx.qc, ctx.xso->stream, buf, len, bytes_read, peek)) {
+    if (!quic_read_actual(&ctx, ctx.xso->stream, buf, len, bytes_read, peek)) {
         ret = 0; /* quic_read_actual raised error here */
         goto out;
     }
@@ -1848,7 +2309,7 @@ static int quic_read(SSL *s, void *buf, size_t len, size_t *bytes_read, int peek
          * buffer is empty. This means we need to block until we get
          * at least one byte.
          */
-        args.qc         = ctx.qc;
+        args.ctx        = &ctx;
         args.stream     = ctx.xso->stream;
         args.buf        = buf;
         args.len        = len;
@@ -1857,7 +2318,7 @@ static int quic_read(SSL *s, void *buf, size_t len, size_t *bytes_read, int peek
 
         res = block_until_pred(ctx.qc, quic_read_again, &args, 0);
         if (res == 0) {
-            ret = QUIC_RAISE_NON_NORMAL_ERROR(ctx.qc, ERR_R_INTERNAL_ERROR, NULL);
+            ret = QUIC_RAISE_NON_NORMAL_ERROR(&ctx, ERR_R_INTERNAL_ERROR, NULL);
             goto out;
         } else if (res < 0) {
             ret = 0; /* quic_read_again raised error here */
@@ -1866,8 +2327,22 @@ static int quic_read(SSL *s, void *buf, size_t len, size_t *bytes_read, int peek
 
         ret = 1;
     } else {
-        /* We did not get any bytes and are not in blocking mode. */
-        ret = QUIC_RAISE_NORMAL_ERROR(ctx.qc, SSL_ERROR_WANT_READ);
+        /*
+         * We did not get any bytes and are not in blocking mode.
+         * Tick to see if this delivers any more.
+         */
+        ossl_quic_reactor_tick(ossl_quic_channel_get_reactor(ctx.qc->ch), 0);
+
+        /* Try the read again. */
+        if (!quic_read_actual(&ctx, ctx.xso->stream, buf, len, bytes_read, peek)) {
+            ret = 0; /* quic_read_actual raised error here */
+            goto out;
+        }
+
+        if (*bytes_read > 0)
+            ret = 1; /* Succeeded this time. */
+        else
+            ret = QUIC_RAISE_NORMAL_ERROR(&ctx, SSL_ERROR_WANT_READ);
     }
 
 out:
@@ -1889,23 +2364,34 @@ int ossl_quic_peek(SSL *s, void *buf, size_t len, size_t *bytes_read)
  * SSL_pending
  * -----------
  */
+
 QUIC_TAKES_LOCK
-static size_t ossl_quic_pending_int(const SSL *s)
+static size_t ossl_quic_pending_int(const SSL *s, int check_channel)
 {
     QCTX ctx;
     size_t avail = 0;
     int fin = 0;
 
-    if (!expect_quic_with_stream_lock(s, /*remote_init=*/-1, &ctx))
+
+    if (!expect_quic(s, &ctx))
         return 0;
 
-    if (ctx.xso->stream == NULL || ctx.xso->stream->rstream == NULL)
+    quic_lock(ctx.qc);
+
+    if (ctx.xso == NULL)
+        goto out;
+
+    if (ctx.xso->stream == NULL
+        || !ossl_quic_stream_has_recv_buffer(ctx.xso->stream))
         /* Cannot raise errors here because we are const, just fail. */
         goto out;
 
     if (!ossl_quic_rstream_available(ctx.xso->stream->rstream, &avail, &fin))
         avail = 0;
 
+    if (avail == 0 && check_channel && ossl_quic_channel_has_pending(ctx.qc->ch))
+        avail = 1;
+
 out:
     quic_unlock(ctx.qc);
     return avail;
@@ -1913,12 +2399,13 @@ out:
 
 size_t ossl_quic_pending(const SSL *s)
 {
-    return ossl_quic_pending_int(s);
+    return ossl_quic_pending_int(s, /*check_channel=*/0);
 }
 
 int ossl_quic_has_pending(const SSL *s)
 {
-    return ossl_quic_pending_int(s) > 0;
+    /* Do we have app-side pending data or pending URXEs or RXEs? */
+    return ossl_quic_pending_int(s, /*check_channel=*/1) > 0;
 }
 
 /*
@@ -1930,19 +2417,24 @@ int ossl_quic_conn_stream_conclude(SSL *s)
 {
     QCTX ctx;
     QUIC_STREAM *qs;
+    int err;
 
     if (!expect_quic_with_stream_lock(s, /*remote_init=*/0, &ctx))
         return 0;
 
     qs = ctx.xso->stream;
 
-    if (qs == NULL || qs->sstream == NULL) {
+    if (!quic_mutation_allowed(ctx.qc, /*req_active=*/1)) {
         quic_unlock(ctx.qc);
-        return 0;
+        return QUIC_RAISE_NON_NORMAL_ERROR(&ctx, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
+    }
+
+    if (!quic_validate_for_write(ctx.xso, &err)) {
+        quic_unlock(ctx.qc);
+        return QUIC_RAISE_NON_NORMAL_ERROR(&ctx, err, NULL);
     }
 
-    if (!ossl_quic_channel_is_active(ctx.qc->ch)
-        || ossl_quic_sstream_get_final_size(qs->sstream, NULL)) {
+    if (ossl_quic_sstream_get_final_size(qs->sstream, NULL)) {
         quic_unlock(ctx.qc);
         return 1;
     }
@@ -2002,18 +2494,21 @@ int ossl_quic_get_stream_type(SSL *s)
     QCTX ctx;
 
     if (!expect_quic(s, &ctx))
-        return SSL_STREAM_TYPE_NONE;
+        return SSL_STREAM_TYPE_BIDI;
 
     if (ctx.xso == NULL) {
         /*
-         * If we are deferring XSO creation, assume single stream mode and
-         * default to BIDI, as the deferred XSO which will be created will be
-         * bidirectional.
+         * If deferred XSO creation has yet to occur, proceed according to the
+         * default stream mode. If AUTO_BIDI or AUTO_UNI is set, we cannot know
+         * what kind of stream will be created yet, so return BIDI on the basis
+         * that at this time, the client still has the option of calling
+         * SSL_read() or SSL_write() first.
          */
-        if (!ctx.qc->default_xso_created)
-            return SSL_STREAM_TYPE_BIDI;
-        else
+        if (ctx.qc->default_xso_created
+            || ctx.qc->default_stream_mode == SSL_DEFAULT_STREAM_MODE_NONE)
             return SSL_STREAM_TYPE_NONE;
+        else
+            return SSL_STREAM_TYPE_BIDI;
     }
 
     if (ossl_quic_stream_is_bidi(ctx.xso->stream))
@@ -2059,7 +2554,7 @@ int ossl_quic_set_default_stream_mode(SSL *s, uint32_t mode)
     quic_lock(ctx.qc);
 
     if (ctx.qc->default_xso_created)
-        return QUIC_RAISE_NON_NORMAL_ERROR(ctx.qc, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED,
+        return QUIC_RAISE_NON_NORMAL_ERROR(&ctx, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED,
                                            "too late to change default stream mode");
 
     switch (mode) {
@@ -2070,7 +2565,7 @@ int ossl_quic_set_default_stream_mode(SSL *s, uint32_t mode)
         break;
     default:
         quic_unlock(ctx.qc);
-        return QUIC_RAISE_NON_NORMAL_ERROR(ctx.qc, ERR_R_PASSED_INVALID_ARGUMENT,
+        return QUIC_RAISE_NON_NORMAL_ERROR(&ctx, ERR_R_PASSED_INVALID_ARGUMENT,
                                            "bad default stream type");
     }
 
@@ -2086,7 +2581,7 @@ QUIC_TAKES_LOCK
 SSL *ossl_quic_detach_stream(SSL *s)
 {
     QCTX ctx;
-    QUIC_XSO *xso;
+    QUIC_XSO *xso = NULL;
 
     if (!expect_quic_conn_only(s, &ctx))
         return NULL;
@@ -2094,12 +2589,12 @@ SSL *ossl_quic_detach_stream(SSL *s)
     quic_lock(ctx.qc);
 
     /* Calling this function inhibits default XSO autocreation. */
-    xso = ctx.qc->default_xso;
-    qc_set_default_xso(ctx.qc, NULL, /*touch=*/1);
+    /* QC ref to any default XSO is transferred to us and to caller. */
+    qc_set_default_xso_keep_ref(ctx.qc, NULL, /*touch=*/1, &xso);
 
     quic_unlock(ctx.qc);
 
-    return &xso->ssl;
+    return xso != NULL ? &xso->ssl : NULL;
 }
 
 /*
@@ -2110,63 +2605,85 @@ QUIC_TAKES_LOCK
 int ossl_quic_attach_stream(SSL *conn, SSL *stream)
 {
     QCTX ctx;
+    QUIC_XSO *xso;
+    int nref;
 
     if (!expect_quic_conn_only(conn, &ctx))
         return 0;
 
     if (stream == NULL || stream->type != SSL_TYPE_QUIC_XSO)
-        return QUIC_RAISE_NON_NORMAL_ERROR(ctx.qc, ERR_R_PASSED_NULL_PARAMETER,
+        return QUIC_RAISE_NON_NORMAL_ERROR(&ctx, ERR_R_PASSED_NULL_PARAMETER,
                                            "stream to attach must be a valid QUIC stream");
 
+    xso = (QUIC_XSO *)stream;
+
     quic_lock(ctx.qc);
 
     if (ctx.qc->default_xso != NULL) {
         quic_unlock(ctx.qc);
-        return QUIC_RAISE_NON_NORMAL_ERROR(ctx.qc, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED,
+        return QUIC_RAISE_NON_NORMAL_ERROR(&ctx, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED,
                                            "connection already has a default stream");
     }
 
+    /*
+     * It is a caller error for the XSO being attached as a default XSO to have
+     * more than one ref.
+     */
+    if (!CRYPTO_GET_REF(&xso->ssl.references, &nref)) {
+        quic_unlock(ctx.qc);
+        return QUIC_RAISE_NON_NORMAL_ERROR(&ctx, ERR_R_INTERNAL_ERROR,
+                                           "ref");
+    }
+
+    if (nref != 1) {
+        quic_unlock(ctx.qc);
+        return QUIC_RAISE_NON_NORMAL_ERROR(&ctx, ERR_R_PASSED_INVALID_ARGUMENT,
+                                           "stream being attached must have "
+                                           "only 1 reference");
+    }
+
+    /* Caller's reference to the XSO is transferred to us. */
     /* Calling this function inhibits default XSO autocreation. */
-    qc_set_default_xso(ctx.qc, (QUIC_XSO *)stream, /*touch=*/1);
+    qc_set_default_xso(ctx.qc, xso, /*touch=*/1);
 
     quic_unlock(ctx.qc);
     return 1;
 }
 
 /*
- * SSL_set_incoming_stream_reject_policy
- * -------------------------------------
+ * SSL_set_incoming_stream_policy
+ * ------------------------------
  */
 QUIC_NEEDS_LOCK
-static int qc_get_effective_incoming_stream_reject_policy(QUIC_CONNECTION *qc)
+static int qc_get_effective_incoming_stream_policy(QUIC_CONNECTION *qc)
 {
-    switch (qc->incoming_stream_reject_policy) {
-        case SSL_INCOMING_STREAM_REJECT_POLICY_AUTO:
+    switch (qc->incoming_stream_policy) {
+        case SSL_INCOMING_STREAM_POLICY_AUTO:
             if ((qc->default_xso == NULL && !qc->default_xso_created)
                 || qc->default_stream_mode == SSL_DEFAULT_STREAM_MODE_NONE)
-                return SSL_INCOMING_STREAM_REJECT_POLICY_ACCEPT;
+                return SSL_INCOMING_STREAM_POLICY_ACCEPT;
             else
-                return SSL_INCOMING_STREAM_REJECT_POLICY_REJECT;
+                return SSL_INCOMING_STREAM_POLICY_REJECT;
 
         default:
-            return qc->incoming_stream_reject_policy;
+            return qc->incoming_stream_policy;
     }
 }
 
 QUIC_NEEDS_LOCK
 static void qc_update_reject_policy(QUIC_CONNECTION *qc)
 {
-    int policy = qc_get_effective_incoming_stream_reject_policy(qc);
-    int enable_reject = (policy == SSL_INCOMING_STREAM_REJECT_POLICY_REJECT);
+    int policy = qc_get_effective_incoming_stream_policy(qc);
+    int enable_reject = (policy == SSL_INCOMING_STREAM_POLICY_REJECT);
 
     ossl_quic_channel_set_incoming_stream_auto_reject(qc->ch,
                                                       enable_reject,
-                                                      qc->incoming_stream_reject_aec);
+                                                      qc->incoming_stream_aec);
 }
 
 QUIC_TAKES_LOCK
-int ossl_quic_set_incoming_stream_reject_policy(SSL *s, int policy,
-                                                uint64_t aec)
+int ossl_quic_set_incoming_stream_policy(SSL *s, int policy,
+                                         uint64_t aec)
 {
     int ret = 1;
     QCTX ctx;
@@ -2177,11 +2694,11 @@ int ossl_quic_set_incoming_stream_reject_policy(SSL *s, int policy,
     quic_lock(ctx.qc);
 
     switch (policy) {
-    case SSL_INCOMING_STREAM_REJECT_POLICY_AUTO:
-    case SSL_INCOMING_STREAM_REJECT_POLICY_ACCEPT:
-    case SSL_INCOMING_STREAM_REJECT_POLICY_REJECT:
-        ctx.qc->incoming_stream_reject_policy = policy;
-        ctx.qc->incoming_stream_reject_aec    = aec;
+    case SSL_INCOMING_STREAM_POLICY_AUTO:
+    case SSL_INCOMING_STREAM_POLICY_ACCEPT:
+    case SSL_INCOMING_STREAM_POLICY_REJECT:
+        ctx.qc->incoming_stream_policy = policy;
+        ctx.qc->incoming_stream_aec    = aec;
         break;
 
     default:
@@ -2199,7 +2716,7 @@ int ossl_quic_set_incoming_stream_reject_policy(SSL *s, int policy,
  * -----------------
  */
 struct wait_for_incoming_stream_args {
-    QUIC_CONNECTION *qc;
+    QCTX            *ctx;
     QUIC_STREAM     *qs;
 };
 
@@ -2207,11 +2724,12 @@ QUIC_NEEDS_LOCK
 static int wait_for_incoming_stream(void *arg)
 {
     struct wait_for_incoming_stream_args *args = arg;
-    QUIC_STREAM_MAP *qsm = ossl_quic_channel_get_qsm(args->qc->ch);
+    QUIC_CONNECTION *qc = args->ctx->qc;
+    QUIC_STREAM_MAP *qsm = ossl_quic_channel_get_qsm(qc->ch);
 
-    if (!ossl_quic_channel_is_active(args->qc->ch)) {
+    if (!quic_mutation_allowed(qc, /*req_active=*/1)) {
         /* If connection is torn down due to an error while blocking, stop. */
-        QUIC_RAISE_NON_NORMAL_ERROR(args->qc, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
+        QUIC_RAISE_NON_NORMAL_ERROR(args->ctx, SSL_R_PROTOCOL_IS_SHUTDOWN, NULL);
         return -1;
     }
 
@@ -2238,8 +2756,8 @@ SSL *ossl_quic_accept_stream(SSL *s, uint64_t flags)
 
     quic_lock(ctx.qc);
 
-    if (qc_get_effective_incoming_stream_reject_policy(ctx.qc)
-        == SSL_INCOMING_STREAM_REJECT_POLICY_REJECT)
+    if (qc_get_effective_incoming_stream_policy(ctx.qc)
+        == SSL_INCOMING_STREAM_POLICY_REJECT)
         goto out;
 
     qsm = ossl_quic_channel_get_qsm(ctx.qc->ch);
@@ -2250,12 +2768,12 @@ SSL *ossl_quic_accept_stream(SSL *s, uint64_t flags)
             && (flags & SSL_ACCEPT_STREAM_NO_BLOCK) == 0) {
             struct wait_for_incoming_stream_args args;
 
-            args.qc = ctx.qc;
+            args.ctx = &ctx;
             args.qs = NULL;
 
             ret = block_until_pred(ctx.qc, wait_for_incoming_stream, &args, 0);
             if (ret == 0) {
-                QUIC_RAISE_NON_NORMAL_ERROR(ctx.qc, ERR_R_INTERNAL_ERROR, NULL);
+                QUIC_RAISE_NON_NORMAL_ERROR(&ctx, ERR_R_INTERNAL_ERROR, NULL);
                 goto out;
             } else if (ret < 0 || args.qs == NULL) {
                 goto out;
@@ -2317,6 +2835,7 @@ int ossl_quic_stream_reset(SSL *ssl,
     QUIC_STREAM_MAP *qsm;
     QUIC_STREAM *qs;
     uint64_t error_code;
+    int ok, err;
 
     if (!expect_quic_with_stream_lock(ssl, /*remote_init=*/0, &ctx))
         return 0;
@@ -2325,10 +2844,16 @@ int ossl_quic_stream_reset(SSL *ssl,
     qs          = ctx.xso->stream;
     error_code  = (args != NULL ? args->quic_error_code : 0);
 
-    ossl_quic_stream_map_reset_stream_send_part(qsm, qs, error_code);
+    if (!quic_validate_for_write(ctx.xso, &err)) {
+        ok = QUIC_RAISE_NON_NORMAL_ERROR(&ctx, err, NULL);
+        goto err;
+    }
+
+    ok = ossl_quic_stream_map_reset_stream_send_part(qsm, qs, error_code);
 
+err:
     quic_unlock(ctx.qc);
-    return 1;
+    return ok;
 }
 
 /*
@@ -2360,11 +2885,11 @@ static void quic_classify_stream(QUIC_CONNECTION *qc,
     } else if (ossl_quic_channel_is_term_any(qc->ch)) {
         /* Connection already closed. */
         *state = SSL_STREAM_STATE_CONN_CLOSED;
-    } else if (!is_write && qs->recv_fin_retired) {
+    } else if (!is_write && qs->recv_state == QUIC_RSTREAM_STATE_DATA_READ) {
         /* Application has read a FIN. */
         *state = SSL_STREAM_STATE_FINISHED;
     } else if ((!is_write && qs->stop_sending)
-               || (is_write && qs->reset_stream)) {
+               || (is_write && ossl_quic_stream_send_is_reset(qs))) {
         /*
          * Stream has been reset locally. FIN takes precedence over this for the
          * read case as the application need not care if the stream is reset
@@ -2374,7 +2899,7 @@ static void quic_classify_stream(QUIC_CONNECTION *qc,
         *app_error_code = !is_write
             ? qs->stop_sending_aec
             : qs->reset_stream_aec;
-    } else if ((!is_write && qs->peer_reset_stream)
+    } else if ((!is_write && ossl_quic_stream_recv_is_reset(qs))
                || (is_write && qs->peer_stop_sending)) {
         /*
          * Stream has been reset remotely. */
@@ -2464,6 +2989,41 @@ int ossl_quic_get_stream_write_error_code(SSL *ssl, uint64_t *app_error_code)
     return quic_get_stream_error_code(ssl, /*is_write=*/1, app_error_code);
 }
 
+/*
+ * Write buffer size mutation
+ * --------------------------
+ */
+int ossl_quic_set_write_buffer_size(SSL *ssl, size_t size)
+{
+    int ret = 0;
+    QCTX ctx;
+
+    if (!expect_quic_with_stream_lock(ssl, /*remote_init=*/-1, &ctx))
+        return 0;
+
+    if (!ossl_quic_stream_has_send(ctx.xso->stream))
+        /* Called on a unidirectional receive-only stream - error. */
+        goto out;
+
+    if (!ossl_quic_stream_has_send_buffer(ctx.xso->stream)) {
+        /*
+         * If the stream has a send part but we have disposed of it because we
+         * no longer need it, this is a no-op.
+         */
+        ret = 1;
+        goto out;
+    }
+
+    if (!ossl_quic_sstream_set_buffer_size(ctx.xso->stream->sstream, size))
+        goto out;
+
+    ret = 1;
+
+out:
+    quic_unlock(ctx.qc);
+    return ret;
+}
+
 /*
  * SSL_get_conn_close_info
  * -----------------------
@@ -2490,6 +3050,56 @@ int ossl_quic_get_conn_close_info(SSL *ssl,
     return 1;
 }
 
+/*
+ * SSL_key_update
+ * --------------
+ */
+int ossl_quic_key_update(SSL *ssl, int update_type)
+{
+    QCTX ctx;
+
+    if (!expect_quic_conn_only(ssl, &ctx))
+        return 0;
+
+    switch (update_type) {
+    case SSL_KEY_UPDATE_NOT_REQUESTED:
+        /*
+         * QUIC signals peer key update implicily by triggering a local
+         * spontaneous TXKU. Silently upgrade this to SSL_KEY_UPDATE_REQUESTED.
+         */
+    case SSL_KEY_UPDATE_REQUESTED:
+        break;
+
+    default:
+        /* Unknown type - error. */
+        return 0;
+    }
+
+    quic_lock(ctx.qc);
+
+    /* Attempt to perform a TXKU. */
+    if (!ossl_quic_channel_trigger_txku(ctx.qc->ch)) {
+        quic_unlock(ctx.qc);
+        return 0;
+    }
+
+    quic_unlock(ctx.qc);
+    return 1;
+}
+
+/*
+ * SSL_get_key_update_type
+ * -----------------------
+ */
+int ossl_quic_get_key_update_type(const SSL *s)
+{
+    /*
+     * We always handle key updates immediately so a key update is never
+     * pending.
+     */
+    return SSL_KEY_UPDATE_NONE;
+}
+
 /*
  * QUIC Front-End I/O API: SSL_CTX Management
  * ==========================================
@@ -2505,7 +3115,22 @@ long ossl_quic_ctx_ctrl(SSL_CTX *ctx, int cmd, long larg, void *parg)
 
 long ossl_quic_callback_ctrl(SSL *s, int cmd, void (*fp) (void))
 {
-    return ssl3_callback_ctrl(s, cmd, fp);
+    QCTX ctx;
+
+    if (!expect_quic_conn_only(s, &ctx))
+        return 0;
+
+    switch (cmd) {
+    case SSL_CTRL_SET_MSG_CALLBACK:
+        ossl_quic_channel_set_msg_callback(ctx.qc->ch, (ossl_msg_cb)fp,
+                                           &ctx.qc->ssl);
+        /* This callback also needs to be set on the internal SSL object */
+        return ssl3_callback_ctrl(ctx.qc->tls, cmd, fp);;
+
+    default:
+        /* Probably a TLS related ctrl. Defer to our internal SSL object */
+        return ssl3_callback_ctrl(ctx.qc->tls, cmd, fp);
+    }
 }
 
 long ossl_quic_ctx_callback_ctrl(SSL_CTX *ctx, int cmd, void (*fp) (void))
@@ -2533,3 +3158,18 @@ const SSL_CIPHER *ossl_quic_get_cipher(unsigned int u)
 {
     return NULL;
 }
+
+/*
+ * Internal Testing APIs
+ * =====================
+ */
+
+QUIC_CHANNEL *ossl_quic_conn_get_channel(SSL *s)
+{
+    QCTX ctx;
+
+    if (!expect_quic_conn_only(s, &ctx))
+        return NULL;
+
+    return ctx.qc->ch;
+}