Commit 1a97d41789 for openssl.org

commit 1a97d4178935b7d9ef4b905f889cdee412f8109a
Author: Nikola Pajkovsky <nikolap@openssl.org>
Date:   Sat Oct 17 17:25:13 2026 +0200

    Fix use-after-free of PSK binder pointer on packet growth

    tls_construct_ctos_psk() reserved the resumption binder
    with WPACKET_sub_allocate_bytes_u8() and kept the returned pointer.
    Reserving the external PSK binder afterwards can grow the packet
    and relocate its backing buffer, leaving the resumption binder pointer
    dangling.  tls_psk_do_binder() then wrote the binder into freed memory.

    Record the binder end offsets instead and convert them to pointers
    only after both binders and their enclosing lengths have been allocated.

    Add test_psk_binder_growth to sslapitest for SHA-256 and SHA-384.
    It offers both a resumption ticket and an external PSK, sizes the packet
    so the first binder ends exactly at the buffer end, and uses a secure
    BUF_MEM so growth always relocates.  Both binders are then verified.
    Without the fix, ASan reports a heap-use-after-free.

    Fixes: 9368f86526d9 "Add TLSv1.3 client side external PSK support"
    Assisted-by: pi:GLM-5.3-Flash-EXL3
    Assisted-by: OpenAI:gpt-6.1-sol
    Signed-off-by: Nikola Pajkovsky <nikolap@openssl.org>
    Reviewed-by: Bob Beck <beck@openssl.org>
    Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Merge-date: Tue Oct 27 17:47:14 2026
    Merged-from: https://github.com/openssl/openssl/pull/33272

diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c
index 57c6166c16..a5fef5c1f2 100644
--- a/ssl/statem/extensions_clnt.c
+++ b/ssl/statem/extensions_clnt.c
@@ -1463,7 +1463,7 @@ EXT_RETURN tls_construct_ctos_psk(SSL_CONNECTION *s, WPACKET *pkt,
 {
 #if !(defined(OPENSSL_NO_TLS1_3) && defined(OPENSSL_NO_DTLS1_3))
     uint32_t agems = 0;
-    size_t binderoffset, msglen;
+    size_t binderoffset, msglen, resbinder_end_offset = 0, pskbinder_end_offset = 0;
     int reshashsize = 0, pskhashsize = 0;
     unsigned char *resbinder = NULL, *pskbinder = NULL, *msgstart = NULL;
     const EVP_MD *handmd = NULL, *mdres = NULL, *mdpsk = NULL;
@@ -1694,9 +1694,11 @@ dopsksess:
         || !WPACKET_get_total_written(pkt, &binderoffset)
         || !WPACKET_start_sub_packet_u16(pkt)
         || (dores
-            && !WPACKET_sub_allocate_bytes_u8(pkt, reshashsize, &resbinder))
+            && (!WPACKET_sub_allocate_bytes_u8(pkt, reshashsize, NULL)
+                || !WPACKET_get_total_written(pkt, &resbinder_end_offset)))
         || (s->psksession != NULL
-            && !WPACKET_sub_allocate_bytes_u8(pkt, pskhashsize, &pskbinder))
+            && (!WPACKET_sub_allocate_bytes_u8(pkt, pskhashsize, NULL)
+                || !WPACKET_get_total_written(pkt, &pskbinder_end_offset)))
         || !WPACKET_close(pkt)
         || !WPACKET_close(pkt)
         || !WPACKET_get_total_written(pkt, &msglen)
@@ -1710,6 +1712,11 @@ dopsksess:
     }

     msgstart = WPACKET_get_curr(pkt) - msglen;
+    /* Allocating either binder may relocate the packet's backing buffer. */
+    if (dores)
+        resbinder = msgstart + resbinder_end_offset - reshashsize;
+    if (s->psksession != NULL)
+        pskbinder = msgstart + pskbinder_end_offset - pskhashsize;

     if (dores && tls_psk_do_binder(s, mdres, msgstart, binderoffset, NULL, resbinder, s->session, 1, 0) != 1) {
         /* SSLfatal() already called */
diff --git a/test/sslapitest.c b/test/sslapitest.c
index 701a707991..d2eaa9fb83 100644
--- a/test/sslapitest.c
+++ b/test/sslapitest.c
@@ -46,6 +46,29 @@
 #include "../ssl/record/methods/recmethod_local.h"
 #include "filterprov.h"

+/*
+ * PSK pre binder overhead =
+ *  2 bytes for TLSEXT_TYPE_psk
+ *  2 bytes for extension length
+ *  2 bytes for identities list length
+ *  2 bytes for identity length
+ *  4 bytes for obfuscated_ticket_age
+ *  2 bytes for binder list length
+ *  1 byte for binder length
+ * The above excludes the number of bytes for the identity itself and the
+ * subsequent binder bytes
+ */
+#define PSK_PRE_BINDER_OVERHEAD (2 + 2 + 2 + 2 + 4 + 2 + 1)
+/* Each further identity adds its identity length and obfuscated_ticket_age */
+#define PSK_IDENTITY_OVERHEAD (2 + 4)
+#define PSK_BINDERS_LIST_LEN_BYTES 2
+#define PSK_BINDER_LEN_BYTES 1
+/*
+ * Initial packet buffer length in test_psk_binder_growth(): the first binder
+ * ends exactly here, so allocating the second one must grow the buffer.
+ */
+#define PSK_GROWTH_BUFLEN 256
+
 #undef OSSL_NO_USABLE_TLS1_3
 #if defined(OPENSSL_NO_TLS1_3) \
     || (defined(OPENSSL_NO_EC) && defined(OPENSSL_NO_DH))
@@ -4613,6 +4636,97 @@ end:
     return testresult;
 }

+/* Both PSK binders must remain valid if allocating the second grows the packet. */
+static int test_psk_binder_growth(int idx)
+{
+#ifdef OSSL_NO_USABLE_TLS1_3
+    return TEST_skip("TLSv1.3 is not usable");
+#else
+    SSL_CTX *ctx = NULL;
+    SSL *ssl = NULL;
+    SSL_CONNECTION *sc;
+    SSL_SESSION *sess = NULL;
+    BUF_MEM *buf = NULL;
+    WPACKET pkt = { 0 };
+    EVP_MD *md = NULL;
+    const char *tick = "a ticket";
+    size_t tick_len = strlen(tick);
+    const char *psksession_id = "Identity";
+    size_t psksession_id_len = strlen(psksession_id);
+    size_t hashsize = idx == 0 ? SHA256_DIGEST_LENGTH : SHA384_DIGEST_LENGTH;
+    /* Bytes before the extension, chosen so the first binder ends at the buffer end */
+    size_t prefix = PSK_GROWTH_BUFLEN
+        - (PSK_PRE_BINDER_OVERHEAD + PSK_IDENTITY_OVERHEAD + tick_len
+            + psksession_id_len + hashsize);
+    /* Offset of the binders list, i.e. the end of the data the binders cover */
+    size_t binderoffset = PSK_GROWTH_BUFLEN - hashsize - PSK_BINDER_LEN_BYTES
+        - PSK_BINDERS_LIST_LEN_BYTES;
+    unsigned char *resbinder, *pskbinder;
+    size_t written;
+    uintptr_t original;
+    int ret = 0;
+
+    if (!TEST_ptr(ctx = SSL_CTX_new_ex(libctx, NULL, TLS_client_method()))
+        || !TEST_ptr(ssl = SSL_new(ctx))
+        || !TEST_ptr(sess = create_a_psk(ssl, hashsize))
+        || !TEST_ptr(sess->ext.tick = OPENSSL_memdup(tick, tick_len)))
+        goto end;
+    sess->ext.ticklen = tick_len;
+    sess->ext.tick_lifetime_hint = 7200;
+    if (!TEST_true(SSL_set_session(ssl, sess)))
+        goto end;
+    SSL_set_connect_state(ssl);
+    sc = SSL_CONNECTION_FROM_SSL(ssl);
+    sc->version = sc->negotiated_version = TLS1_3_VERSION;
+    sc->ext.psk_resumption_offered = 1;
+    /* Secure-buffer growth allocates before freeing, forcing relocation. */
+    if (!TEST_ptr(sc->psksession = create_a_psk(ssl, hashsize))
+        || !TEST_ptr(sc->psksession_id = OPENSSL_memdup(psksession_id, psksession_id_len))
+        || !TEST_ptr(md = EVP_MD_fetch(libctx, idx == 0 ? "SHA256" : "SHA384", NULL))
+        || !TEST_ptr(buf = BUF_MEM_new_ex(BUF_MEM_FLAG_SECURE))
+        || !TEST_size_t_eq(BUF_MEM_grow(buf, PSK_GROWTH_BUFLEN), PSK_GROWTH_BUFLEN))
+        goto end;
+    sc->psksession_id_len = psksession_id_len;
+    original = (uintptr_t)buf->data;
+    if (!TEST_true(WPACKET_init(&pkt, buf)))
+        goto end;
+    /* The first binder ends at the buffer end; the second must grow the packet. */
+    if (!TEST_true(WPACKET_put_bytes_u8(&pkt, SSL3_MT_CLIENT_HELLO))
+        || !TEST_true(WPACKET_start_sub_packet_u24(&pkt))
+        || !TEST_true(WPACKET_memset(&pkt, 0, prefix - SSL3_HM_HEADER_LENGTH))
+        || !TEST_int_eq(tls_construct_ctos_psk(sc, &pkt, SSL_EXT_CLIENT_HELLO,
+                            NULL, 0),
+            EXT_RETURN_SENT)
+        || !TEST_true(WPACKET_close(&pkt))
+        || !TEST_true(WPACKET_finish(&pkt))
+        || !TEST_true(WPACKET_get_total_written(&pkt, &written))
+        || !TEST_size_t_eq(written,
+            PSK_GROWTH_BUFLEN + PSK_BINDER_LEN_BYTES + hashsize)
+        || !TEST_true(original != (uintptr_t)buf->data))
+        goto end;
+    resbinder = (unsigned char *)buf->data + PSK_GROWTH_BUFLEN - hashsize;
+    pskbinder = (unsigned char *)buf->data + PSK_GROWTH_BUFLEN + PSK_BINDER_LEN_BYTES;
+
+    if (!TEST_int_eq(tls_psk_do_binder(sc, md, (unsigned char *)buf->data,
+                         binderoffset, resbinder, NULL, sess, 0, 0),
+            1)
+        || !TEST_int_eq(tls_psk_do_binder(sc, md, (unsigned char *)buf->data,
+                            binderoffset, pskbinder, NULL, sc->psksession,
+                            0, 1),
+            1))
+        goto end;
+    ret = 1;
+end:
+    WPACKET_cleanup(&pkt);
+    EVP_MD_free(md);
+    BUF_MEM_free(buf);
+    SSL_SESSION_free(sess);
+    SSL_free(ssl);
+    SSL_CTX_free(ctx);
+    return ret;
+#endif
+}
+
 static int test_extra_tickets(int idx)
 {
     SSL_CTX *sctx = NULL, *cctx = NULL;
@@ -19360,6 +19474,7 @@ int setup_tests(void)
     ADD_ALL_TESTS(test_stateless_tickets, 6);
     ADD_ALL_TESTS(test_psk_tickets, 2);
     ADD_ALL_TESTS(test_extra_tickets, 12);
+    ADD_ALL_TESTS(test_psk_binder_growth, 2);
 #endif
     ADD_ALL_TESTS(test_ssl_set_bio, TOTAL_SSL_SET_BIO_TESTS);
     ADD_TEST(test_ssl_bio_pop_next_bio);