Commit a87db42e46 for openssl.org

commit a87db42e4647d36fb82464d9493145ccc9bd21ed
Author: Nikolas Gauder <nikolas.gauder@tum.de>
Date:   Wed Oct 21 18:40:34 2026 +0200

    QUIC: release pseudo-lost packets from the congestion controller

    On a Retry, and after version negotiation, the client marks Initial
    packets it has sent as pseudo-lost, so that their CRYPTO data is sent
    again.  The ACK manager then stops counting them as in flight, but on
    purpose does not report them to the congestion controller as lost,
    since the network lost nothing.  The congestion controller is not told
    about them at all, so NewReno kept counting their bytes as in flight
    for the rest of the connection, and allowed that much less to be sent.

    Release them with on_data_invalidated(), as when a packet number space
    is discarded.  The congestion window is left alone.  This follows the
    approach proposed in #33099.

    Fixes: 90699176b074 "QUIC CC: Major revisions to CC abstract interface"
    Fixes #33079
    Assisted-by: Claude:claude-opus-5-5

    Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
    Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
    Merge-date: Fri Oct 23 12:53:54 2026
    Merged-from: https://github.com/openssl/openssl/pull/33298

diff --git a/ssl/quic/quic_ackm.c b/ssl/quic/quic_ackm.c
index a249d6db12..bd0fb966e3 100644
--- a/ssl/quic/quic_ackm.c
+++ b/ssl/quic/quic_ackm.c
@@ -980,6 +980,13 @@ static void ackm_on_pkts_lost(OSSL_ACKM *ackm, int pkt_space,
                 loss_info.tx_size = p->num_bytes;

                 ackm->cc_method->on_data_lost(ackm->cc_data, &loss_info);
+            } else {
+                /*
+                 * The packet is no longer in flight all the same, so the CC
+                 * must stop counting it.
+                 */
+                ackm->cc_method->on_data_invalidated(ackm->cc_data,
+                    p->num_bytes);
             }
         }

diff --git a/test/quic_ackm_test.c b/test/quic_ackm_test.c
index 79fa51b99f..13c35dcb29 100644
--- a/test/quic_ackm_test.c
+++ b/test/quic_ackm_test.c
@@ -1137,6 +1137,101 @@ err:
     return testresult;
 }

+/*
+ * Pseudo-loss
+ * ******************************************************************
+ */
+
+/*
+ * Pseudo-lost packets (e.g. the Initial packets of a ClientHello sent before a
+ * Retry) are no longer in flight, so the congestion controller must stop
+ * counting them. They were not lost on the network, so the congestion window
+ * must not shrink.
+ */
+static int test_tx_pseudo_loss(void)
+{
+    int testresult = 0, have_statm = 0;
+    OSSL_STATM statm;
+    OSSL_CC_DATA *ccdata = NULL;
+    OSSL_ACKM *ackm = NULL;
+    OSSL_ACKM_TX_PKT *tx[2] = { NULL, NULL };
+    struct pkt_info info[2] = { { 0 } };
+    OSSL_PARAM params[3], *p = params;
+    uint64_t cwnd = 0, bytes_in_flight = 0, init_cwnd;
+    size_t i;
+
+    fake_time = TIME_BASE;
+
+    if (!TEST_int_eq(ossl_statm_init(&statm), 1))
+        goto err;
+
+    have_statm = 1;
+
+    *p++ = OSSL_PARAM_construct_uint64(OSSL_CC_OPTION_CUR_BYTES_IN_FLIGHT,
+        &bytes_in_flight);
+    *p++ = OSSL_PARAM_construct_uint64(OSSL_CC_OPTION_CUR_CWND_SIZE,
+        &cwnd);
+    *p++ = OSSL_PARAM_construct_end();
+
+    ccdata = ossl_cc_newreno_method.new(fake_now, NULL);
+    if (!TEST_ptr(ccdata)
+        || !TEST_true(ossl_cc_newreno_method.bind_diagnostics(ccdata, params)))
+        goto err;
+
+    ackm = ossl_ackm_new(fake_now, NULL, &statm,
+        &ossl_cc_newreno_method, ccdata,
+        /* is_server */ 0);
+    if (!TEST_ptr(ackm))
+        goto err;
+
+    init_cwnd = cwnd;
+
+    for (i = 0; i < OSSL_NELEM(tx); ++i) {
+        if (!TEST_ptr(tx[i] = OPENSSL_zalloc(sizeof(*tx[i]))))
+            goto err;
+
+        tx[i]->pkt_num = i;
+        tx[i]->pkt_space = QUIC_PN_SPACE_INITIAL;
+        tx[i]->is_inflight = 1;
+        tx[i]->is_ack_eliciting = 1;
+        tx[i]->num_bytes = 1200;
+        tx[i]->largest_acked = QUIC_PN_INVALID;
+        tx[i]->on_lost = on_lost;
+        tx[i]->on_acked = on_acked;
+        tx[i]->on_discarded = on_discarded;
+        tx[i]->cb_arg = &info[i];
+        tx[i]->time = fake_time;
+
+        if (!TEST_int_eq(ossl_ackm_on_tx_packet(ackm, tx[i]), 1))
+            goto err;
+    }
+
+    if (!TEST_uint64_t_eq(bytes_in_flight, 2400))
+        goto err;
+
+    if (!TEST_true(ossl_ackm_mark_packet_pseudo_lost(ackm,
+            QUIC_PN_SPACE_INITIAL, 0))
+        || !TEST_uint64_t_eq(bytes_in_flight, 1200)
+        || !TEST_true(ossl_ackm_mark_packet_pseudo_lost(ackm,
+            QUIC_PN_SPACE_INITIAL, 1))
+        || !TEST_uint64_t_eq(bytes_in_flight, 0)
+        || !TEST_int_eq(info[0].lost, 1)
+        || !TEST_int_eq(info[1].lost, 1)
+        || !TEST_uint64_t_eq(cwnd, init_cwnd))
+        goto err;
+
+    testresult = 1;
+err:
+    ossl_ackm_free(ackm);
+    if (ccdata != NULL)
+        ossl_cc_newreno_method.free(ccdata);
+    if (have_statm)
+        ossl_statm_destroy(&statm);
+    for (i = 0; i < OSSL_NELEM(tx); ++i)
+        OPENSSL_free(tx[i]);
+    return testresult;
+}
+
 /*
  * Driver
  * ******************************************************************
@@ -1170,5 +1265,6 @@ int setup_tests(void)
         OSSL_NELEM(tx_ack_cases) * MODE_NUM * QUIC_PN_SPACE_NUM);
     ADD_ALL_TESTS(test_tx_ack_time_script, OSSL_NELEM(tx_ack_time_scripts));
     ADD_ALL_TESTS(test_rx_ack, OSSL_NELEM(rx_test_scripts) * QUIC_PN_SPACE_NUM);
+    ADD_TEST(test_tx_pseudo_loss);
     return 1;
 }