Commit dc13cfb931 for openssl.org

commit dc13cfb931200f3f720e3f6f0769e7601dd99801
Author: Neil Horman <nhorman@openssl.org>
Date:   Tue Aug 25 15:04:16 2026 -0400

    Add a test to check for quic unvalidated credit

    Adds a test to ensure that, when sending a coalesced frame that should
    drive more than the available unvalidated credit that a server has, we
    stop sending when we reach that limit.

    Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Reviewed-by: Andrew Dinh <andrewd@openssl.org>
    Merge-date: Tue Sep 29 11:52:15 2026

diff --git a/test/quicapitest.c b/test/quicapitest.c
index ddaed7025c..726bdd7e19 100644
--- a/test/quicapitest.c
+++ b/test/quicapitest.c
@@ -9,6 +9,8 @@

 #include <stdio.h>
 #include <string.h>
+#include <fcntl.h>
+#include <stdint.h>

 #include <openssl/opensslconf.h>
 #include <openssl/quic.h>
@@ -23,6 +25,7 @@
 #include "internal/quic_error.h"
 #include "internal/quic_ssl.h"
 #include "internal/quic_port.h"
+#include "internal/quic_txp.h"

 static OSSL_LIB_CTX *libctx = NULL;
 static char *propq = NULL;
@@ -3398,6 +3401,344 @@ err:
     return testresult;
 }

+#define CLIENT_PORT 4080
+#define SERVER_PORT 8040
+#define QUIC_MDPL 1200
+#define EXTRA_CERTS 40
+
+static int wait_readable(BIO *b, int timeout_ms)
+{
+    uint64_t expire = ossl_time2ticks(ossl_ms2time(ossl_time2ms(ossl_time_now()) + timeout_ms));
+    uint64_t now;
+
+    for (;;) {
+        if (BIO_pending(b))
+            break;
+        now = ossl_time2ticks(ossl_time_now());
+        if (now > expire)
+            return 0;
+    }
+    return 1;
+}
+
+static int drain_server_output(BIO *b, uint64_t *total,
+    size_t *num_datagrams)
+{
+    unsigned char buf[65536];
+    BIO_MSG msg = { 0 };
+    size_t num_processed;
+    int ret = 0;
+
+    for (;;) {
+        msg.data = buf;
+        msg.data_len = 65535;
+        num_processed = 0;
+        if (!BIO_recvmmsg(b, &msg, 1, 1, 0, &num_processed)) {
+            return !!ret;
+        } else {
+            *total += msg.data_len;
+            *num_datagrams += num_processed;
+            ret++;
+            continue;
+        }
+    }
+}
+
+static int send_coalesced_initial(OSSL_QTX *qtx, QUIC_PKT_HDR *hdr,
+    const BIO_ADDR *peer, uint64_t first_pn)
+{
+    static const unsigned char one_padding[1];
+    static const unsigned char final_padding[23];
+    OSSL_QTX_IOVEC iovec = { 0 };
+    OSSL_QTX_PKT pkt = { 0 };
+    size_t i;
+
+    pkt.hdr = hdr;
+    pkt.iovec = &iovec;
+    pkt.num_iovec = 1;
+    pkt.peer = peer;
+
+    /* 30 * 38-byte packets + one 60-byte packet = one 1200-byte datagram. */
+    for (i = 0; i < 31; ++i) {
+        iovec.buf = i < 30 ? one_padding : final_padding;
+        iovec.buf_len = i < 30 ? sizeof(one_padding) : sizeof(final_padding);
+        pkt.pn = first_pn + i;
+        pkt.flags = i < 30 ? OSSL_QTX_PKT_FLAG_COALESCE : 0;
+        if (!ossl_qtx_write_pkt(qtx, &pkt))
+            return 0;
+    }
+
+    if (ossl_qtx_get_queue_len_datagrams(qtx) != 1
+        || ossl_qtx_get_queue_len_bytes(qtx) != QUIC_MDPL) {
+        TEST_info("crafted queue has %zu datagrams / %zu bytes, "
+                  "expected 1 / %d",
+            ossl_qtx_get_queue_len_datagrams(qtx),
+            ossl_qtx_get_queue_len_bytes(qtx), QUIC_MDPL);
+        return 0;
+    }
+
+    if (ossl_qtx_flush_net(qtx) != QTX_FLUSH_NET_RES_OK) {
+        TEST_info("failed to flush crafted datagram");
+        return 0;
+    }
+
+    return 1;
+}
+
+/*
+ * Test that we don't exceed our amplification limit when a peer
+ * sends coalesced frames
+ */
+static int test_quic_amplification_limit(void)
+{
+    SSL_CTX *sctx = NULL, *cctx = NULL;
+    SSL *listener = NULL, *client = NULL, *server = NULL;
+    QUIC_CHANNEL *cch = NULL, *sch = NULL;
+    OSSL_QTX *inject_qtx = NULL;
+    OSSL_QTX_ARGS qtx_args = { 0 };
+    QUIC_PKT_HDR hdr = { 0 };
+    BIO_ADDR *server_addr = NULL, *client_addr = NULL;
+    BIO_ADDR *shadow_server_addr = NULL;
+    int rc, ssl_err, i, ret = 0;
+    uint64_t server_bytes = 0;
+    size_t server_datagrams = 0;
+    uint64_t first_injected_pn;
+    const uint64_t client_bytes = 2 * QUIC_MDPL;
+    const uint64_t rfc_ceiling = 3 * client_bytes;
+    struct in_addr ina;
+    static const unsigned char alpn[] = { 8, 'o', 's', 's', 'l', 't', 'e', 's', 't' };
+    X509 *chain_cert = NULL;
+    BIO *c_bio = NULL, *s_bio = NULL;
+
+    sctx = create_server_ctx();
+    cctx = create_client_ctx();
+    if (!TEST_ptr(sctx) || !TEST_ptr(cctx))
+        goto err;
+
+    /*
+     * Ensure that we better know the form of the handshake messages sent
+     * we set the groups and ciphersuites so we know how big the client and server
+     * hello frames will be so the math for adding and draining credit is
+     * predictable
+     */
+    if (!TEST_true(SSL_CTX_set_options(cctx, SSL_OP_NO_RX_CERTIFICATE_COMPRESSION))
+        || !TEST_true(SSL_CTX_set_options(sctx, SSL_OP_NO_TX_CERTIFICATE_COMPRESSION)))
+        goto err;
+
+    if (!TEST_true(SSL_CTX_set1_groups_list(sctx, "X25519"))
+        || !TEST_true(SSL_CTX_set1_groups_list(cctx, "X25519"))) {
+        TEST_skip("Skipping amplification test due to lack of X25519 group");
+        ret = 1;
+        goto err;
+    }
+    if (!TEST_true(SSL_CTX_set_ciphersuites(sctx, "TLS_AES_128_GCM_SHA256"))
+        || !TEST_true(SSL_CTX_set_ciphersuites(cctx, "TLS_AES_128_GCM_SHA256"))) {
+        TEST_skip("Skipping amplification test due to lack of aes-128-gcm-sha256 cipher");
+        ret = 1;
+        goto err;
+    }
+
+    /*
+     * We're going to send a set of extra certs that don't create a valid
+     * verification chain, so don't bother verifying
+     */
+    SSL_CTX_set_verify(sctx, SSL_VERIFY_NONE, NULL);
+    SSL_CTX_set_verify(cctx, SSL_VERIFY_NONE, NULL);
+
+    if (!TEST_true(SSL_CTX_check_private_key(sctx)))
+        goto err;
+
+    /*
+     * Get out leaf certificate and add it 40 times as extra_certs.
+     * This makes our server hello large, so that we drain our unvalidated
+     * credit on the server in response to the initial client hello
+     */
+    chain_cert = SSL_CTX_get0_certificate(sctx);
+
+    for (i = 0; i < EXTRA_CERTS; ++i) {
+        if (!TEST_int_eq(X509_up_ref(chain_cert), 1))
+            goto err;
+        if (!TEST_int_eq(SSL_CTX_add_extra_chain_cert(sctx, chain_cert), 1))
+            goto err;
+    }
+
+    /*
+     * Create a bio dgram pair, and attach them to the client and server ssl objects.
+     * We do this so we have access to the bios and can inject and drain frames as needed.
+     * Also, its important to make the bio ring buffer sizes large enough so that we don't
+     * accidentally drop frames.
+     */
+    ina.s_addr = htonl(INADDR_LOOPBACK);
+    if (!TEST_true(BIO_new_bio_dgram_pair(&c_bio, 65535, &s_bio, 65535)))
+        goto err;
+    if (!TEST_ptr((server_addr = create_addr(&ina, SERVER_PORT)))
+        || (!TEST_ptr((client_addr = create_addr(&ina, CLIENT_PORT)))))
+        goto err;
+
+    if (!TEST_true(bio_addr_bind(c_bio, client_addr)))
+        goto err;
+    client_addr = NULL;
+
+    if (!TEST_true(bio_addr_bind(s_bio, server_addr)))
+        goto err;
+    shadow_server_addr = server_addr;
+    server_addr = NULL;
+
+    if (!TEST_ptr((listener = SSL_new_listener(sctx,
+                       SSL_LISTENER_FLAG_NO_VALIDATE)))
+        || (!TEST_true(SSL_listen(listener)))
+        || (!TEST_ptr((client = SSL_new(cctx))))
+        || (!TEST_true(SSL_set1_initial_peer_addr(client, shadow_server_addr)))
+        || (!TEST_int_eq(SSL_set_alpn_protos(client, alpn, sizeof(alpn)), 0))
+        || (!TEST_true(SSL_set_tlsext_host_name(client, "localhost")))
+        || (!TEST_ptr((cch = ossl_quic_conn_get_channel(client)))))
+        goto err;
+
+    SSL_set_bio(listener, s_bio, s_bio);
+    SSL_set_bio(client, c_bio, c_bio);
+
+    if (!TEST_true(SSL_set_blocking_mode(listener, 0)))
+        goto err;
+
+    if (!TEST_true(SSL_set_blocking_mode(client, 0)))
+        goto err;
+
+    /*
+     * Create a bogus qtx so that we can inject a crafted frame after the
+     * initial client and server hello are exchanged.
+     */
+    qtx_args.bio = SSL_get_wbio(client);
+    qtx_args.mdpl = QUIC_MDPL;
+    qtx_args.libctx = libctx;
+    inject_qtx = ossl_qtx_new(&qtx_args);
+    if (!TEST_ptr(inject_qtx))
+        goto err;
+
+    /*
+     * Install the initial secret to our bogus qtx so that our subsequent
+     * crafted frame gets accepted on the server channel.
+     */
+    hdr.type = QUIC_PKT_TYPE_INITIAL;
+    hdr.fixed = 1;
+    hdr.pn_len = 4;
+    hdr.version = QUIC_VERSION_1;
+    hdr.dst_conn_id = cch->init_dcid;
+    hdr.src_conn_id = cch->init_scid;
+    if (!TEST_true(ossl_quic_provide_initial_secret(libctx, NULL, &hdr.dst_conn_id,
+            0, NULL, inject_qtx)))
+        goto err;
+
+    /* This emits a valid, padded, one-datagram X25519 ClientHello. */
+    rc = SSL_connect(client);
+    if (rc > 0)
+        goto err;
+    ssl_err = SSL_get_error(client, rc);
+    if (ssl_err != SSL_ERROR_WANT_READ && ssl_err != SSL_ERROR_WANT_WRITE)
+        goto err;
+
+    /*
+     * Make sure that the client hello was received at the server
+     */
+    if (!wait_readable(s_bio, 1000)) {
+        TEST_info("timed out waiting for the ClientHello");
+        goto err;
+    }
+
+    /*
+     * Accept the new connection on the server
+     */
+    for (i = 0; i < 64 && server == NULL; ++i) {
+        ERR_clear_error();
+        if (!TEST_true(SSL_handle_events(listener)))
+            goto err;
+        server = SSL_accept_connection(listener, 0);
+        ERR_clear_error();
+    }
+    if (!TEST_ptr(server))
+        goto err;
+    sch = ossl_quic_conn_get_channel(server);
+    if (!TEST_ptr(sch))
+        goto err;
+
+    if (!TEST_true(SSL_set_blocking_mode(server, 0)))
+        goto err;
+
+    first_injected_pn = ossl_quic_tx_packetiser_get_next_pn(
+        cch->txp, QUIC_PN_SPACE_INITIAL);
+    TEST_info("next client Initial packet number: %llu",
+        (unsigned long long)first_injected_pn);
+
+    /* Count but never deliver the server flight to the QUIC client. */
+    if (!TEST_true(drain_server_output(c_bio, &server_bytes, &server_datagrams)))
+        goto err;
+
+    TEST_info("phase 1 complete: legitimate ClientHello credit exhausted");
+
+    /*
+     * At this point the client has sent 1200 bytes, and the server should respond
+     * with 3600 bytes of server hello/certificate data, which should drain
+     * out unvalidated credit on the server.
+     *
+     * Given that, send another 1200 byte packet from the client, containing
+     * a bunch of initial frames.  The server should respond to each of these
+     * with 3600 bytes of handshake data, just as it did above, but
+     * (if the server is honoring the 3x amplification limit, will stop after
+     * sending the first one.
+     */
+    if (!TEST_true(send_coalesced_initial(inject_qtx, &hdr, shadow_server_addr,
+            first_injected_pn)))
+        goto err;
+    TEST_info("phase 2: sent one 1200-byte datagram containing 31 Initials");
+
+    /*
+     * Tick our state machine, making the server send responses, and counting
+     * how many datagrams and bytes are received by the client
+     */
+    for (i = 0; i < 64; ++i) {
+        if (!TEST_true(SSL_handle_events(listener)))
+            goto err;
+        if (!TEST_true(SSL_handle_events(server)))
+            goto err;
+        drain_server_output(c_bio, &server_bytes, &server_datagrams);
+    }
+
+    TEST_info("client UDP payload:  %llu bytes in 2 datagrams",
+        (unsigned long long)client_bytes);
+    TEST_info("RFC 9000 ceiling:    %llu bytes",
+        (unsigned long long)rfc_ceiling);
+    TEST_info("server UDP payload:  %llu bytes in %llu datagrams",
+        (unsigned long long)server_bytes, (unsigned long long)server_datagrams);
+    TEST_info("amplification ratio: %.2fx",
+        (double)server_bytes / (double)client_bytes);
+    TEST_info("handshake complete:  %s",
+        sch->handshake_complete ? "yes" : "no");
+    TEST_info("bogus credit >100k:  %s",
+        ossl_quic_tx_packetiser_check_unvalidated_credit(sch->txp, 100000)
+            ? "yes"
+            : "no");
+
+    /*
+     * rfc_ceiling should be 7200 (3600 bytes for the first client hello
+     * and 3600 more for our bogus coalesced frame above).  Make sure we
+     * didn't receive more than that on the client
+     */
+    if (!TEST_uint64_t_le(server_bytes, rfc_ceiling))
+        goto err;
+
+    ret = 1;
+
+err:
+    ossl_qtx_free(inject_qtx);
+    SSL_free(server);
+    SSL_free(client);
+    SSL_free(listener);
+    SSL_CTX_free(cctx);
+    SSL_CTX_free(sctx);
+    BIO_ADDR_free(client_addr);
+    BIO_ADDR_free(server_addr);
+    return ret;
+}
+
 static SSL *quic_verify_ssl = NULL;

 static int quic_verify_cb(int ok, X509_STORE_CTX *ctx)
@@ -4485,6 +4826,7 @@ int setup_tests(void)
 #endif
     ADD_TEST(test_server_method_with_ssl_new);
     ADD_TEST(test_ssl_accept_connection);
+    ADD_TEST(test_quic_amplification_limit);
     ADD_TEST(test_ssl_set_verify);
     ADD_TEST(test_accept_stream);
     ADD_TEST(test_reject_stream_gc);