Version in base suite: 3.5.7-1~deb13u2 Base version: openssl_3.5.7-1~deb13u2 Target version: openssl_3.5.7-1~deb13u3 Base file: /srv/ftp-master.debian.org/ftp/pool/main/o/openssl/openssl_3.5.7-1~deb13u2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/o/openssl/openssl_3.5.7-1~deb13u3.dsc changelog | 26 patches/Add-a-red-black-tree-implementation.patch | 1572 ++++++ patches/Add-a-test-to-check-for-quic-unvalidated-credit.patch | 371 + patches/Add-explicit-tests-to-check-some-typical-stream-reassembl.patch | 289 + patches/Add-ossl_list_TYPE_join-head-tail-function.patch | 161 patches/Add-regression-tests-for-the-SSL_set_SSL_CTX-sigalg-state.patch | 484 ++ patches/Add-test-for-CVE-2026-75805.patch | 127 patches/CVE-2026-75804-QUIC-connection-level-flow-control-not-enf.patch | 37 patches/CVE-2026-84784-QUIC-unbounded-RETIRE_CONNECTION_ID-backlo-1.patch | 322 + patches/CVE-2026-84784-QUIC-unbounded-RETIRE_CONNECTION_ID-backlo.patch | 108 patches/Defer-computation-of-relative-CRLDP-names.patch | 293 + patches/Enforce-final-size-for-streams.patch | 1478 ++++++ patches/Fix-out-of-bounds-valid_flags-access-after-SSL_set_SSL_CT.patch | 113 patches/Guard-comparision-when-values-are-NULL.patch | 43 patches/Limit-packet-buffer-overhead-to-64kB-per-stream.patch | 1777 +++++++ patches/Make-the-ecp_sm2p256-scalar-multiplication-constant-time.patch | 339 + patches/New-implementation-of-stream-reassembly-for-QUIC.patch | 2408 ++++++++++ patches/TLS-Reject-undersized-TLS-1.2-AEAD-records-before-AEAD-pr.patch | 297 + patches/don-t-double-count-full-databgram-length-on-unvalidated-c.patch | 53 patches/dtls-reset-init_off-before-retransmitting-a-message.patch | 535 ++ patches/ec-make-ossl_ec_scalar_mul_ladder-scalar-padding-constant.patch | 141 patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-1.patch | 40 patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-2.patch | 21 patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-3.patch | 25 patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-4.patch | 25 patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-5.patch | 63 patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-6.patch | 31 patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit.patch | 30 patches/fixup-Fix-out-of-bounds-valid_flags-access-after-SSL_set_.patch | 22 patches/fixup-don-t-double-count-full-databgram-length-on-unvalid.patch | 21 patches/make-ch_cleanup-just-safe-enough-to-be-called-by-quic_str.patch | 42 patches/quic-move-the-RXE-definition-to-a-local-header.patch | 191 patches/series | 41 patches/sm2-make-sm2_sig_gen-constant-time.patch | 156 patches/test-cover-FIN-final-size-against-buffered-data.patch | 193 patches/test-cover-a-long-lagging-read-of-packet-backed-stream-da.patch | 118 patches/test-cover-cleansing-of-dropped-duplicate-bytes.patch | 158 patches/test-cover-sc_data_trim_right-function.patch | 244 + patches/test-cover-the-packet-pinning-path-of-QUIC-stream-reassem.patch | 374 + patches/test-cover-two-sided-overlap-of-direct-tail-chunk.patch | 271 + patches/test-cover-zero-length-read-of-a-QUIC-rstream.patch | 106 patches/test-reassemble-small-out-of-order-frames-and-check-the-b.patch | 161 patches/test-verifies-the-connection-level-RX-flow-control.patch | 45 43 files changed, 13352 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp10o4okbd/openssl_3.5.7-1~deb13u2.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp10o4okbd/openssl_3.5.7-1~deb13u3.dsc: no acceptable signature found diff -Nru openssl-3.5.7/debian/changelog openssl-3.5.7/debian/changelog --- openssl-3.5.7/debian/changelog 2026-08-23 15:26:22.000000000 +0000 +++ openssl-3.5.7/debian/changelog 2026-09-29 20:10:28.000000000 +0000 @@ -1,3 +1,29 @@ +openssl (3.5.7-1~deb13u3) trixie-security; urgency=medium + + * CVE-2026-84782 ("DTLS Retransmits Handshake Messages From a Stale Buffer + Offset") + * CVE-2026-35189 ("Memory Allocation in Relative CRLDP Processing") + * CVE-2026-35191 ("QUIC Unvalidated Amplification Credit may be Over + Accounted") + * CVE-2026-54872 ("Timing Side-Channel in Scalar Multiplication for Non-NIST + EC Curves") + * CVE-2026-54875 ("Non-Constant-Time SM2 Scalar Multiplication on ARM64 and + RISC-V") + * CVE-2026-72897 ("Out-of-Bounds Access After SSL_set_SSL_CTX() During a + Handshake") + * CVE-2026-75804 ("QUIC Connection-Level Flow Control is Not Enforced for + Streams") + * CVE-2026-75805 ("NULL Pointer Dereference in CMP Client Revocation + Response Handling") + * CVE-2026-75806 ("Unauthenticated and Undersized DTLS 1.2 AEAD Record + Causes DoS") + * CVE-2026-77696 ("Timing Side-Channel in SM2 Signature Generation") + * CVE-2026-84784 ("QUIC: Unbounded RETIRE_CONNECTION_ID Backlog") + * CVE-2026-42772 and CVE-2026-54873 ("improved QUIC + stream reassembly implementation") + + -- Sebastian Andrzej Siewior Tue, 29 Sep 2026 22:10:28 +0200 + openssl (3.5.7-1~deb13u2) trixie-security; urgency=medium * CVE-2026-18798 ("QUIC Server May Trigger Double Free When Processing diff -Nru openssl-3.5.7/debian/patches/Add-a-red-black-tree-implementation.patch openssl-3.5.7/debian/patches/Add-a-red-black-tree-implementation.patch --- openssl-3.5.7/debian/patches/Add-a-red-black-tree-implementation.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/Add-a-red-black-tree-implementation.patch 2026-09-29 20:10:25.000000000 +0000 @@ -0,0 +1,1572 @@ +From: Alexandr Nedvedicky +Date: Tue, 21 Jul 2026 00:33:18 +0200 +Subject: Add a red-black tree implementation + +The code comes from David Gwynne . +The same implmentation can be found in OpenBSD. + +Several minor adjustments have been done to include the code +into OpenSSL: + * the prefix has been changed to OSSL_RBT_, + * the support augment is removed, + * _RBT_CHECK()/_RBT_POISON() got removed too, + * _RB_REMOVE() resets link pointer to NULL in debug build, + +The documentation is based on tree(3) from OpenBSD, originally authored +by Niels Provos . + +Co-Authoerd-by: David Gwynne +Co-Authored-by: Niels Provos +Co-Authored-by: Andrew Dinh + +Reviewed-by: Eugene Syromiatnikov +Reviewed-by: Bob Beck +Reviewed-by: Norbert Pocs +MergeDate: Tue Aug 11 19:15:59 2026 +Reviewed-by: Tomas Mraz +Reviewed-by: Milan Broz +Merged-from: https://github.com/openssl/openssl/pull/32769 +--- + crypto/build.info | 2 +- + crypto/{aria => rbtree}/build.info | 3 +- + crypto/rbtree/rbtree.c | 561 ++++++++++++++++++++++ + doc/internal/man7/ossl_rbtree.pod | 303 ++++++++++++ + include/internal/ossl_rbtree.h | 265 ++++++++++ + ssl/build.info | 3 +- + test/build.info | 7 +- + test/ossl_rbtree_test.c | 276 +++++++++++ + test/recipes/{02-test_time.t => 02-test_rbtree.t} | 5 +- + util/missingcrypto-internal.txt | 1 + + 10 files changed, 1418 insertions(+), 8 deletions(-) + copy crypto/{aria => rbtree}/build.info (73%) + create mode 100644 crypto/rbtree/rbtree.c + create mode 100644 doc/internal/man7/ossl_rbtree.pod + create mode 100644 include/internal/ossl_rbtree.h + create mode 100644 test/ossl_rbtree_test.c + copy test/recipes/{02-test_time.t => 02-test_rbtree.t} (72%) + +diff --git a/crypto/build.info b/crypto/build.info +index aee5c467668a..058bcc5c304d 100644 +--- a/crypto/build.info ++++ b/crypto/build.info +@@ -6,7 +6,7 @@ SUBDIRS=objects buffer bio stack lhash hashtable rand evp asn1 pem x509 conf \ + siphash sm3 des aes rc2 rc4 rc5 idea aria bf cast camellia \ + seed sm4 chacha modes bn ec rsa dsa dh sm2 dso engine \ + err comp http ocsp cms ts srp cmac ct async ess crmf cmp encode_decode \ +- ffc hpke thread ml_dsa slh_dsa ++ ffc hpke thread ml_dsa slh_dsa rbtree + + LIBS=../libcrypto + +diff --git a/crypto/aria/build.info b/crypto/rbtree/build.info +similarity index 73% +copy from crypto/aria/build.info +copy to crypto/rbtree/build.info +index 218d0612f747..79f9ff01e679 100644 +--- a/crypto/aria/build.info ++++ b/crypto/rbtree/build.info +@@ -1,4 +1,3 @@ + LIBS=../../libcrypto + SOURCE[../../libcrypto]=\ +- aria.c +- ++ rbtree.c +diff --git a/crypto/rbtree/rbtree.c b/crypto/rbtree/rbtree.c +new file mode 100644 +index 000000000000..f1d89166e560 +--- /dev/null ++++ b/crypto/rbtree/rbtree.c +@@ -0,0 +1,561 @@ ++/* ++ * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved. ++ * Copyright (c) 2016 David Gwynne ++ * ++ * Licensed under the Apache License 2.0 (the "License"). You may not use ++ * this file except in compliance with the License. You can obtain a copy ++ * in the file LICENSE in the source distribution or at ++ * https://www.openssl.org/source/license.html ++ */ ++ ++/* ++ * The code here comes from David Gwynne . The original ++ * version can be found: ++ * https://github.com/dgwynne/data-structures/ ++ * file bst.h. The same code is also part of OpenBSD OS where it is shipped ++ * under BSD license. ++ * ++ * David Gwynne agrees to include modified version to OpenSSL and ship it ++ * under OpenSSL Apache 2.0 license. ++ */ ++ ++#include "internal/ossl_rbtree.h" ++ ++#ifndef NDEBUG ++#include ++#endif ++ ++#define OSSL_RBT_BLACK 0 ++#define OSSL_RBT_RED 1 ++ ++static struct ossl_rbt_entry * ++rbt_n2e(const struct ossl_rbt_type *t, void *node) ++{ ++ uintptr_t addr = (uintptr_t)node; ++ ++ return (struct ossl_rbt_entry *)(addr + t->t_offset); ++} ++ ++static void * ++rbt_e2n(const struct ossl_rbt_type *t, struct ossl_rbt_entry *rbe) ++{ ++ uintptr_t addr = (uintptr_t)rbe; ++ ++ return (void *)(addr - t->t_offset); ++} ++ ++#define OSSL_RBE_LEFT(_rbe) (_rbe)->rb_left ++#define OSSL_RBE_RIGHT(_rbe) (_rbe)->rb_right ++#define OSSL_RBE_PARENT(_rbe) (_rbe)->rb_parent ++#define OSSL_RBE_COLOR(_rbe) (_rbe)->rb_color ++ ++#define OSSL_RBH_ROOT(_rbt) (_rbt)->rb_root ++ ++static void ++rbe_set(struct ossl_rbt_entry *rbe, struct ossl_rbt_entry *parent) ++{ ++ OSSL_RBE_PARENT(rbe) = parent; ++ OSSL_RBE_LEFT(rbe) = OSSL_RBE_RIGHT(rbe) = NULL; ++ OSSL_RBE_COLOR(rbe) = OSSL_RBT_RED; ++} ++ ++static void ++rbe_set_blackred(struct ossl_rbt_entry *black, struct ossl_rbt_entry *red) ++{ ++ OSSL_RBE_COLOR(black) = OSSL_RBT_BLACK; ++ OSSL_RBE_COLOR(red) = OSSL_RBT_RED; ++} ++ ++static void ++rbe_rotate_left(struct ossl_rbt_tree *rbt, struct ossl_rbt_entry *rbe) ++{ ++ struct ossl_rbt_entry *parent; ++ struct ossl_rbt_entry *tmp; ++ ++ tmp = OSSL_RBE_RIGHT(rbe); ++ OSSL_RBE_RIGHT(rbe) = OSSL_RBE_LEFT(tmp); ++ if (OSSL_RBE_RIGHT(rbe) != NULL) ++ OSSL_RBE_PARENT(OSSL_RBE_LEFT(tmp)) = rbe; ++ ++ parent = OSSL_RBE_PARENT(rbe); ++ OSSL_RBE_PARENT(tmp) = parent; ++ if (parent != NULL) { ++ if (rbe == OSSL_RBE_LEFT(parent)) ++ OSSL_RBE_LEFT(parent) = tmp; ++ else ++ OSSL_RBE_RIGHT(parent) = tmp; ++ } else ++ OSSL_RBH_ROOT(rbt) = tmp; ++ ++ OSSL_RBE_LEFT(tmp) = rbe; ++ OSSL_RBE_PARENT(rbe) = tmp; ++} ++ ++static void ++rbe_rotate_right(struct ossl_rbt_tree *rbt, struct ossl_rbt_entry *rbe) ++{ ++ struct ossl_rbt_entry *parent; ++ struct ossl_rbt_entry *tmp; ++ ++ tmp = OSSL_RBE_LEFT(rbe); ++ OSSL_RBE_LEFT(rbe) = OSSL_RBE_RIGHT(tmp); ++ if (OSSL_RBE_LEFT(rbe) != NULL) ++ OSSL_RBE_PARENT(OSSL_RBE_RIGHT(tmp)) = rbe; ++ ++ parent = OSSL_RBE_PARENT(rbe); ++ OSSL_RBE_PARENT(tmp) = parent; ++ if (parent != NULL) { ++ if (rbe == OSSL_RBE_LEFT(parent)) ++ OSSL_RBE_LEFT(parent) = tmp; ++ else ++ OSSL_RBE_RIGHT(parent) = tmp; ++ } else ++ OSSL_RBH_ROOT(rbt) = tmp; ++ ++ OSSL_RBE_RIGHT(tmp) = rbe; ++ OSSL_RBE_PARENT(rbe) = tmp; ++} ++ ++static void ++rbe_insert_color(struct ossl_rbt_tree *rbt, struct ossl_rbt_entry *rbe) ++{ ++ struct ossl_rbt_entry *parent, *gparent, *tmp; ++ ++ while ((parent = OSSL_RBE_PARENT(rbe)) != NULL && OSSL_RBE_COLOR(parent) == OSSL_RBT_RED) { ++ gparent = OSSL_RBE_PARENT(parent); ++ ++ if (parent == OSSL_RBE_LEFT(gparent)) { ++ tmp = OSSL_RBE_RIGHT(gparent); ++ if (tmp != NULL && OSSL_RBE_COLOR(tmp) == OSSL_RBT_RED) { ++ OSSL_RBE_COLOR(tmp) = OSSL_RBT_BLACK; ++ rbe_set_blackred(parent, gparent); ++ rbe = gparent; ++ continue; ++ } ++ ++ if (OSSL_RBE_RIGHT(parent) == rbe) { ++ rbe_rotate_left(rbt, parent); ++ tmp = parent; ++ parent = rbe; ++ rbe = tmp; ++ } ++ ++ rbe_set_blackred(parent, gparent); ++ rbe_rotate_right(rbt, gparent); ++ } else { ++ tmp = OSSL_RBE_LEFT(gparent); ++ if (tmp != NULL && OSSL_RBE_COLOR(tmp) == OSSL_RBT_RED) { ++ OSSL_RBE_COLOR(tmp) = OSSL_RBT_BLACK; ++ rbe_set_blackred(parent, gparent); ++ rbe = gparent; ++ continue; ++ } ++ ++ if (OSSL_RBE_LEFT(parent) == rbe) { ++ rbe_rotate_right(rbt, parent); ++ tmp = parent; ++ parent = rbe; ++ rbe = tmp; ++ } ++ ++ rbe_set_blackred(parent, gparent); ++ rbe_rotate_left(rbt, gparent); ++ } ++ } ++ ++ OSSL_RBE_COLOR(OSSL_RBH_ROOT(rbt)) = OSSL_RBT_BLACK; ++} ++ ++static void ++rbe_remove_color(struct ossl_rbt_tree *rbt, ++ struct ossl_rbt_entry *parent, struct ossl_rbt_entry *rbe) ++{ ++ struct ossl_rbt_entry *tmp; ++ ++ while ((rbe == NULL || OSSL_RBE_COLOR(rbe) == OSSL_RBT_BLACK) && rbe != OSSL_RBH_ROOT(rbt)) { ++ if (OSSL_RBE_LEFT(parent) == rbe) { ++ tmp = OSSL_RBE_RIGHT(parent); ++ if (OSSL_RBE_COLOR(tmp) == OSSL_RBT_RED) { ++ rbe_set_blackred(tmp, parent); ++ rbe_rotate_left(rbt, parent); ++ tmp = OSSL_RBE_RIGHT(parent); ++ } ++ if ((OSSL_RBE_LEFT(tmp) == NULL || OSSL_RBE_COLOR(OSSL_RBE_LEFT(tmp)) == OSSL_RBT_BLACK) && (OSSL_RBE_RIGHT(tmp) == NULL || OSSL_RBE_COLOR(OSSL_RBE_RIGHT(tmp)) == OSSL_RBT_BLACK)) { ++ OSSL_RBE_COLOR(tmp) = OSSL_RBT_RED; ++ rbe = parent; ++ parent = OSSL_RBE_PARENT(rbe); ++ } else { ++ if (OSSL_RBE_RIGHT(tmp) == NULL || OSSL_RBE_COLOR(OSSL_RBE_RIGHT(tmp)) == OSSL_RBT_BLACK) { ++ struct ossl_rbt_entry *oleft; ++ ++ oleft = OSSL_RBE_LEFT(tmp); ++ if (oleft != NULL) ++ OSSL_RBE_COLOR(oleft) = OSSL_RBT_BLACK; ++ ++ OSSL_RBE_COLOR(tmp) = OSSL_RBT_RED; ++ rbe_rotate_right(rbt, tmp); ++ tmp = OSSL_RBE_RIGHT(parent); ++ } ++ ++ OSSL_RBE_COLOR(tmp) = OSSL_RBE_COLOR(parent); ++ OSSL_RBE_COLOR(parent) = OSSL_RBT_BLACK; ++ if (OSSL_RBE_RIGHT(tmp)) ++ OSSL_RBE_COLOR(OSSL_RBE_RIGHT(tmp)) = OSSL_RBT_BLACK; ++ ++ rbe_rotate_left(rbt, parent); ++ rbe = OSSL_RBH_ROOT(rbt); ++ break; ++ } ++ } else { ++ tmp = OSSL_RBE_LEFT(parent); ++ if (OSSL_RBE_COLOR(tmp) == OSSL_RBT_RED) { ++ rbe_set_blackred(tmp, parent); ++ rbe_rotate_right(rbt, parent); ++ tmp = OSSL_RBE_LEFT(parent); ++ } ++ ++ if ((OSSL_RBE_LEFT(tmp) == NULL || OSSL_RBE_COLOR(OSSL_RBE_LEFT(tmp)) == OSSL_RBT_BLACK) && (OSSL_RBE_RIGHT(tmp) == NULL || OSSL_RBE_COLOR(OSSL_RBE_RIGHT(tmp)) == OSSL_RBT_BLACK)) { ++ OSSL_RBE_COLOR(tmp) = OSSL_RBT_RED; ++ rbe = parent; ++ parent = OSSL_RBE_PARENT(rbe); ++ } else { ++ if (OSSL_RBE_LEFT(tmp) == NULL || OSSL_RBE_COLOR(OSSL_RBE_LEFT(tmp)) == OSSL_RBT_BLACK) { ++ struct ossl_rbt_entry *oright; ++ ++ oright = OSSL_RBE_RIGHT(tmp); ++ if (oright != NULL) ++ OSSL_RBE_COLOR(oright) = OSSL_RBT_BLACK; ++ ++ OSSL_RBE_COLOR(tmp) = OSSL_RBT_RED; ++ rbe_rotate_left(rbt, tmp); ++ tmp = OSSL_RBE_LEFT(parent); ++ } ++ ++ OSSL_RBE_COLOR(tmp) = OSSL_RBE_COLOR(parent); ++ OSSL_RBE_COLOR(parent) = OSSL_RBT_BLACK; ++ if (OSSL_RBE_LEFT(tmp) != NULL) ++ OSSL_RBE_COLOR(OSSL_RBE_LEFT(tmp)) = OSSL_RBT_BLACK; ++ ++ rbe_rotate_right(rbt, parent); ++ rbe = OSSL_RBH_ROOT(rbt); ++ break; ++ } ++ } ++ } ++ ++ if (rbe != NULL) ++ OSSL_RBE_COLOR(rbe) = OSSL_RBT_BLACK; ++} ++ ++static struct ossl_rbt_entry * ++rbe_remove(struct ossl_rbt_tree *rbt, struct ossl_rbt_entry *rbe) ++{ ++ struct ossl_rbt_entry *child, *parent, *old = rbe; ++ unsigned int color; ++ ++ if (OSSL_RBE_LEFT(rbe) == NULL) ++ child = OSSL_RBE_RIGHT(rbe); ++ else if (OSSL_RBE_RIGHT(rbe) == NULL) ++ child = OSSL_RBE_LEFT(rbe); ++ else { ++ struct ossl_rbt_entry *tmp; ++ ++ rbe = OSSL_RBE_RIGHT(rbe); ++ while ((tmp = OSSL_RBE_LEFT(rbe)) != NULL) ++ rbe = tmp; ++ ++ child = OSSL_RBE_RIGHT(rbe); ++ parent = OSSL_RBE_PARENT(rbe); ++ color = OSSL_RBE_COLOR(rbe); ++ if (child != NULL) ++ OSSL_RBE_PARENT(child) = parent; ++ if (parent != NULL) { ++ if (OSSL_RBE_LEFT(parent) == rbe) ++ OSSL_RBE_LEFT(parent) = child; ++ else ++ OSSL_RBE_RIGHT(parent) = child; ++ } else ++ OSSL_RBH_ROOT(rbt) = child; ++ if (OSSL_RBE_PARENT(rbe) == old) ++ parent = rbe; ++ *rbe = *old; ++ ++ tmp = OSSL_RBE_PARENT(old); ++ if (tmp != NULL) { ++ if (OSSL_RBE_LEFT(tmp) == old) ++ OSSL_RBE_LEFT(tmp) = rbe; ++ else ++ OSSL_RBE_RIGHT(tmp) = rbe; ++ } else ++ OSSL_RBH_ROOT(rbt) = rbe; ++ ++ OSSL_RBE_PARENT(OSSL_RBE_LEFT(old)) = rbe; ++ if (OSSL_RBE_RIGHT(old)) ++ OSSL_RBE_PARENT(OSSL_RBE_RIGHT(old)) = rbe; ++ goto color; ++ } ++ ++ parent = OSSL_RBE_PARENT(rbe); ++ color = OSSL_RBE_COLOR(rbe); ++ ++ if (child != NULL) ++ OSSL_RBE_PARENT(child) = parent; ++ if (parent != NULL) { ++ if (OSSL_RBE_LEFT(parent) == rbe) ++ OSSL_RBE_LEFT(parent) = child; ++ else ++ OSSL_RBE_RIGHT(parent) = child; ++ } else ++ OSSL_RBH_ROOT(rbt) = child; ++color: ++ if (color == OSSL_RBT_BLACK) ++ rbe_remove_color(rbt, parent, child); ++ ++#ifndef NDEBUG ++ if (old != NULL) { ++ OSSL_RBE_PARENT(old) = NULL; ++ OSSL_RBE_LEFT(old) = NULL; ++ OSSL_RBE_RIGHT(old) = NULL; ++ } ++#endif ++ ++ return old; ++} ++ ++void * ++ossl_rbt_remove(const struct ossl_rbt_type *t, struct ossl_rbt_tree *rbt, void *elm) ++{ ++ struct ossl_rbt_entry *rbe = rbt_n2e(t, elm); ++ struct ossl_rbt_entry *old; ++ ++ old = rbe_remove(rbt, rbe); ++ ++ return old == NULL ? NULL : rbt_e2n(t, old); ++} ++ ++void * ++ossl_rbt_insert(const struct ossl_rbt_type *t, struct ossl_rbt_tree *rbt, void *elm) ++{ ++ struct ossl_rbt_entry *rbe = rbt_n2e(t, elm); ++ struct ossl_rbt_entry *tmp; ++ struct ossl_rbt_entry *parent = NULL; ++ void *node; ++ int comp = 0; ++ ++#ifndef NDEBUG ++ assert(rbe->rb_parent == NULL); ++ assert(rbe->rb_left == NULL); ++ assert(rbe->rb_right == NULL); ++#endif ++ ++ tmp = OSSL_RBH_ROOT(rbt); ++ while (tmp != NULL) { ++ parent = tmp; ++ ++ node = rbt_e2n(t, tmp); ++ comp = (*t->t_compare)(elm, node); ++ if (comp < 0) ++ tmp = OSSL_RBE_LEFT(tmp); ++ else if (comp > 0) ++ tmp = OSSL_RBE_RIGHT(tmp); ++ else ++ return node; ++ } ++ ++ rbe_set(rbe, parent); ++ ++ if (parent != NULL) { ++ if (comp < 0) ++ OSSL_RBE_LEFT(parent) = rbe; ++ else ++ OSSL_RBE_RIGHT(parent) = rbe; ++ } else ++ OSSL_RBH_ROOT(rbt) = rbe; ++ ++ rbe_insert_color(rbt, rbe); ++ ++ return NULL; ++} ++ ++/* Finds the node with the same key as elm */ ++void * ++ossl_rbt_find(const struct ossl_rbt_type *t, struct ossl_rbt_tree *rbt, const void *key) ++{ ++ struct ossl_rbt_entry *tmp = OSSL_RBH_ROOT(rbt); ++ void *node; ++ int comp; ++ ++ while (tmp != NULL) { ++ node = rbt_e2n(t, tmp); ++ comp = (*t->t_compare)(key, node); ++ if (comp < 0) ++ tmp = OSSL_RBE_LEFT(tmp); ++ else if (comp > 0) ++ tmp = OSSL_RBE_RIGHT(tmp); ++ else ++ return node; ++ } ++ ++ return NULL; ++} ++ ++/* Finds the first node greater than or equal to the search key */ ++void * ++ossl_rbt_nfind(const struct ossl_rbt_type *t, struct ossl_rbt_tree *rbt, const void *key) ++{ ++ struct ossl_rbt_entry *tmp = OSSL_RBH_ROOT(rbt); ++ void *node; ++ void *res = NULL; ++ int comp; ++ ++ while (tmp != NULL) { ++ node = rbt_e2n(t, tmp); ++ comp = (*t->t_compare)(key, node); ++ if (comp < 0) { ++ res = node; ++ tmp = OSSL_RBE_LEFT(tmp); ++ } else if (comp > 0) ++ tmp = OSSL_RBE_RIGHT(tmp); ++ else ++ return node; ++ } ++ ++ return res; ++} ++ ++void * ++ossl_rbt_next(const struct ossl_rbt_type *t, void *elm) ++{ ++ struct ossl_rbt_entry *rbe = rbt_n2e(t, elm); ++ ++ if (OSSL_RBE_RIGHT(rbe) != NULL) { ++ rbe = OSSL_RBE_RIGHT(rbe); ++ while (OSSL_RBE_LEFT(rbe) != NULL) ++ rbe = OSSL_RBE_LEFT(rbe); ++ } else { ++ if (OSSL_RBE_PARENT(rbe) && (rbe == OSSL_RBE_LEFT(OSSL_RBE_PARENT(rbe)))) ++ rbe = OSSL_RBE_PARENT(rbe); ++ else { ++ while (OSSL_RBE_PARENT(rbe) && (rbe == OSSL_RBE_RIGHT(OSSL_RBE_PARENT(rbe)))) ++ rbe = OSSL_RBE_PARENT(rbe); ++ rbe = OSSL_RBE_PARENT(rbe); ++ } ++ } ++ ++ return rbe == NULL ? NULL : rbt_e2n(t, rbe); ++} ++ ++void * ++ossl_rbt_prev(const struct ossl_rbt_type *t, void *elm) ++{ ++ struct ossl_rbt_entry *rbe = rbt_n2e(t, elm); ++ ++ if (OSSL_RBE_LEFT(rbe)) { ++ rbe = OSSL_RBE_LEFT(rbe); ++ while (OSSL_RBE_RIGHT(rbe)) ++ rbe = OSSL_RBE_RIGHT(rbe); ++ } else { ++ if (OSSL_RBE_PARENT(rbe) && (rbe == OSSL_RBE_RIGHT(OSSL_RBE_PARENT(rbe)))) ++ rbe = OSSL_RBE_PARENT(rbe); ++ else { ++ while (OSSL_RBE_PARENT(rbe) && (rbe == OSSL_RBE_LEFT(OSSL_RBE_PARENT(rbe)))) ++ rbe = OSSL_RBE_PARENT(rbe); ++ rbe = OSSL_RBE_PARENT(rbe); ++ } ++ } ++ ++ return rbe == NULL ? NULL : rbt_e2n(t, rbe); ++} ++ ++void * ++ossl_rbt_root(const struct ossl_rbt_type *t, struct ossl_rbt_tree *rbt) ++{ ++ struct ossl_rbt_entry *rbe = OSSL_RBH_ROOT(rbt); ++ ++ return rbe == NULL ? rbe : rbt_e2n(t, rbe); ++} ++ ++void * ++ossl_rbt_min(const struct ossl_rbt_type *t, struct ossl_rbt_tree *rbt) ++{ ++ struct ossl_rbt_entry *rbe = OSSL_RBH_ROOT(rbt); ++ struct ossl_rbt_entry *parent = NULL; ++ ++ while (rbe != NULL) { ++ parent = rbe; ++ rbe = OSSL_RBE_LEFT(rbe); ++ } ++ ++ return parent == NULL ? NULL : rbt_e2n(t, parent); ++} ++ ++void * ++ossl_rbt_max(const struct ossl_rbt_type *t, struct ossl_rbt_tree *rbt) ++{ ++ struct ossl_rbt_entry *rbe = OSSL_RBH_ROOT(rbt); ++ struct ossl_rbt_entry *parent = NULL; ++ ++ while (rbe != NULL) { ++ parent = rbe; ++ rbe = OSSL_RBE_RIGHT(rbe); ++ } ++ ++ return parent == NULL ? NULL : rbt_e2n(t, parent); ++} ++ ++void * ++ossl_rbt_left(const struct ossl_rbt_type *t, void *node) ++{ ++ struct ossl_rbt_entry *rbe = rbt_n2e(t, node); ++ rbe = OSSL_RBE_LEFT(rbe); ++ return rbe == NULL ? NULL : rbt_e2n(t, rbe); ++} ++ ++void * ++ossl_rbt_right(const struct ossl_rbt_type *t, void *node) ++{ ++ struct ossl_rbt_entry *rbe = rbt_n2e(t, node); ++ rbe = OSSL_RBE_RIGHT(rbe); ++ return rbe == NULL ? NULL : rbt_e2n(t, rbe); ++} ++ ++void * ++ossl_rbt_parent(const struct ossl_rbt_type *t, void *node) ++{ ++ struct ossl_rbt_entry *rbe = rbt_n2e(t, node); ++ rbe = OSSL_RBE_PARENT(rbe); ++ return rbe == NULL ? NULL : rbt_e2n(t, rbe); ++} ++ ++void ossl_rbt_set_left(const struct ossl_rbt_type *t, void *node, void *left) ++{ ++ struct ossl_rbt_entry *rbe = rbt_n2e(t, node); ++ struct ossl_rbt_entry *rbl = (left == NULL) ? NULL : rbt_n2e(t, left); ++ ++ OSSL_RBE_LEFT(rbe) = rbl; ++} ++ ++void ossl_rbt_set_right(const struct ossl_rbt_type *t, void *node, void *right) ++{ ++ struct ossl_rbt_entry *rbe = rbt_n2e(t, node); ++ struct ossl_rbt_entry *rbr = (right == NULL) ? NULL : rbt_n2e(t, right); ++ ++ OSSL_RBE_RIGHT(rbe) = rbr; ++} ++ ++void ossl_rbt_set_parent(const struct ossl_rbt_type *t, void *node, void *parent) ++{ ++ struct ossl_rbt_entry *rbe = rbt_n2e(t, node); ++ struct ossl_rbt_entry *rbp = (parent == NULL) ? NULL : rbt_n2e(t, parent); ++ ++ OSSL_RBE_PARENT(rbe) = rbp; ++} ++ ++void ossl_rbt_init_rbe(const struct ossl_rbt_type *t, void *node) ++{ ++ struct ossl_rbt_entry *rbe = rbt_n2e(t, node); ++ ++ OSSL_RBE_PARENT(rbe) = NULL; ++ OSSL_RBE_LEFT(rbe) = NULL; ++ OSSL_RBE_RIGHT(rbe) = NULL; ++} +diff --git a/doc/internal/man7/ossl_rbtree.pod b/doc/internal/man7/ossl_rbtree.pod +new file mode 100644 +index 000000000000..ce05729798ed +--- /dev/null ++++ b/doc/internal/man7/ossl_rbtree.pod +@@ -0,0 +1,303 @@ ++=pod ++ ++=head1 NAME ++ ++OSSL_RBT_PROTOTYPE, OSSL_RBT_GENERATE, OSSL_RBT_ENTRY, OSSL_RBT_HEAD, ++OSSL_RBT_INITIALIZER, OSSL_RBT_ROOT, OSSL_RBT_EMPTY, OSSL_RBT_NEXT, ++OSSL_RBT_PREV, OSSL_RBT_MIN, OSSL_RBT_MAX, OSSL_RBT_FIND, OSSL_RBT_NFIND, ++OSSL_RBT_LEFT, OSSL_RBT_RIGHT, OSSL_RBT_PARENT, OSSL_RBT_SET_LEFT, ++OSSL_RBT_SET_RIGHT, OSSL_RBT_SET_PARENT, OSSL_RBT_FOREACH, ++OSSL_RBT_FOREACH_SAFE, OSSL_RBT_FOREACH_REVERSE, OSSL_RBT_FOREACH_REVERSE_SAFE, ++OSSL_RBT_INIT, OSSL_RBT_INSERT, OSSL_RBT_REMOVE, ++ossl_rbtree - implementation of red-black tree ++ ++=head1 SYNOPSIS ++ ++ #include "internal/ossl_rbtree.h" ++ ++ /* int (*CMP)(const NODE_TYPE *, const NODE_TYPE *); */ ++ ++ OSSL_RBT_PROTOTYPE(NAME, NODE_TYPE, FIELD, CMP) ++ ++ OSSL_RBT_GENERATE(NAME, NODE_TYPE, FIELD, CMP); ++ ++ OSSL_RBT_ENTRY(NODE_TYPE) ++ ++ OSSL_RBT_HEAD(HEADNAME, NODE_TYPE) ++ ++ OSSL_RBT_INITIALIZER(OSSL_RBT_HEAD *head); ++ ++ struct NODE_TYPE * OSSL_RBT_ROOT(NAME, OSSL_RBT_HEAD *head); ++ ++ int OSSL_RBT_EMPTY(NAME, OSSL_RBT_HEAD *head); ++ ++ struct NODE_TYPE * OSSL_RBT_NEXT(NAME, struct NODE_TYPE *elm); ++ ++ struct NODE_TYPE * OSSL_RBT_PREV(NAME, struct NODE_TYPE *elm); ++ ++ struct NODE_TYPE * OSSL_RBT_MIN(NAME, OSSL_RBT_HEAD *head); ++ ++ struct NODE_TYPE * OSSL_RBT_MAX(NAME, OSSL_RBT_HEAD *head); ++ ++ struct NODE_TYPE * OSSL_RBT_FIND(NAME, OSSL_RBT_HEAD *head, struct NODE_TYPE *elm); ++ ++ struct NODE_TYPE * OSSL_RBT_NFIND(NAME, OSSL_RBT_HEAD *head, struct NODE_TYPE *elm); ++ ++ struct NODE_TYPE * OSSL_RBT_LEFT(NAME, struct NODE_TYPE *elm); ++ ++ struct NODE_TYPE * OSSL_RBT_RIGHT(NAME, struct NODE_TYPE *elm); ++ ++ struct NODE_TYPE * OSSL_RBT_PARENT(NAME, struct NODE_TYPE *elm); ++ ++ void OSSL_RBT_SET_LEFT(NAME, struct NODE_TYPE *elm, struct NODE_TYPE *left); ++ ++ void OSSL_RBT_SET_RIGHT(NAME, struct NODE_TYPE *elm, struct NODE_TYPE *right); ++ ++ void OSSL_RBT_SET_PARENT(NAME, struct NODE_TYPE *elm, struct NODE_TYPE *parent); ++ ++ OSSL_RBT_FOREACH(VARNAME, NAME, OSSL_RBT_HEAD *head); ++ ++ OSSL_RBT_FOREACH_SAFE(VARNAME, NAME, OSSL_RBT_HEAD *head, TEMP_VARNAME); ++ ++ OSSL_RBT_FOREACH_REVERSE(VARNAME, NAME, OSSL_RBT_HEAD *head); ++ ++ OSSL_RBT_FOREACH_REVERSE_SAFE(VARNAME, NAME, OSSL_RBT_HEAD *head, TEMP_VARNAME); ++ ++ void OSSL_RBT_INIT(NAME, OSSL_RBT_HEAD *head); ++ ++ struct NODE_TYPE * OSSL_RBT_INSERT(NAME, OSSL_RBT_HEAD *head, struct NODE_TYPE *elm); ++ ++ struct NODE_TYPE * OSSL_RBT_REMOVE(NAME, OSSL_RBT_HEAD *head, struct NODE_TYPE *elm); ++ ++=head1 DESCRIPTION ++ ++These macros define data structures for a red-black tree. ++Every operation on a red-black tree is bounded as O(lg n). The maximum ++height of a red-black tree is 2lg (n+1). ++ ++In the macro definitions, B is the name tag of a user defined ++structure that must contain a field named B, of type B. ++The argument B is the name tag of a user defined ++structure that must be declared using the macro OSSL_RBT_HEAD(). ++The argument B has to be a unique name prefix for every ++tree that is defined. ++ ++The function prototypes are declared with B, ++B. See the examples below for further ++explanation of how these macros are used. ++ ++A red-black tree is a binary search tree with the node color as an extra ++attribute. It fulfills a set of conditions: ++ ++=over 4 ++ ++=item 1. ++every search path from the root to a leaf consists of the same ++number of black nodes, ++ ++=item 2. ++each red node (except for the root) has a black parent, ++ ++=item 3. ++each leaf node is black. ++ ++=back ++ ++A red-black tree is headed by a structure defined by the OSSL_RBT_HEAD macro. ++An OSSL_RBT_HEAD structure is declared as follows: ++ ++ OSSL_RBT_HEAD(HEADNAME, NODE_TYPE) head; ++ ++where B is the name of the structure to be defined, and B is the type of the elements to be inserted into the tree. ++ ++The OSSL_RBT_ENTRY macro declares a structure that allows elements to be ++connected in the tree. ++ ++In order to use the functions that manipulate the tree structure, their ++prototypes need to be declared with the OSSL_RBT_PROTOTYPE() macro, ++where B is a unique identifier for this ++particular tree. The B argument is the type of the structure that is ++being managed by the tree. The B argument is the name of the element ++defined by OSSL_RBT_ENTRY(). ++ ++The function bodies are generated with the OSSL_RBT_GENERATE() macro. ++These macros take the same arguments as the ++OSSL_RBT_PROTOTYPE() macro, but should be used only once. ++ ++Finally, the B argument is the name of a function used to compare ++trees' nodes with each other. The function takes two arguments of type ++B. If the first argument is smaller than the second, ++the function returns a value smaller than zero. If they are equal, the ++function returns zero. Otherwise, it should return a value greater than ++zero. The compare function defines the order of the tree elements. ++ ++The OSSL_RBT_INIT() macro initializes the tree referenced by B. ++ ++The red-black tree can also be initialized statically by using the ++OSSL_RBT_INITIALIZER() macro like this: ++ ++ OSSL_RBT_HEAD(HEADNAME, NODE_TYPE) head = OSSL_RBT_INITIALIZER(&head); ++ ++The OSSL_RBT_INSERT() macro inserts the new element B into the tree pointed by B. ++Upon success, NULL is returned. If a matching element already exists in the ++tree, the insertion is aborted, and a pointer to the existing element is ++returned. ++ ++The OSSL_RBT_REMOVE() macro removes the element B from the tree pointed by ++B. OSSL_RBT_REMOVE() returns B. ++ ++The OSSL_RBT_LEFT() macro returns a pointer to the left child element of B ++in a red-black tree of type B. ++ ++The OSSL_RBT_RIGHT() macro returns a pointer to the right child element ++of B in a red-black tree of type B. ++ ++The OSSL_RBT_PARENT() macro returns a pointer to the parent element of B ++in a red-black tree of type B. ++ ++The OSSL_RBT_SET_LEFT() macro sets the left child pointer of element B ++to B in a red-black tree of type B. ++ ++The OSSL_RBT_SET_RIGHT() macro sets the right child pointer of element B ++to B in a red-black tree of type B. ++ ++The OSSL_RBT_SET_PARENT() macro sets the parent pointer of element B ++to B in a red-black tree of type B. ++ ++The OSSL_RBT_FIND() and OSSL_RBT_NFIND() macros can be used to find a particular ++element in the tree. OSSL_RBT_FIND() finds the node with the same key as B. ++OSSL_RBT_NFIND() finds the first node greater than or equal to the search key. ++ ++ struct NODE_TYPE find, *res; ++ find.key = 30; ++ res = OSSL_RBT_FIND(NAME, &head, &find); ++ ++The OSSL_RBT_ROOT(), OSSL_RBT_MIN(), OSSL_RBT_MAX(), OSSL_RBT_NEXT(), and ++OSSL_RBT_PREV() macros can be used to traverse the tree: ++ ++ for (np = OSSL_RBT_MIN(NAME, &head); np != NULL; np = OSSL_RBT_NEXT(NAME, &head, np)) ++ ++Or, for simplicity, one can use the OSSL_RBT_FOREACH() or OSSL_RBT_FOREACH_REVERSE() ++macros: ++ ++ OSSL_RBT_FOREACH(np, NAME, &head) ++ ++The macros OSSL_RBT_FOREACH_SAFE() and OSSL_RBT_FOREACH_REVERSE_SAFE() traverse the ++tree referenced by head in a forward or reverse direction respectively, ++assigning each element in turn to B. However, unlike their unsafe ++counterparts, they permit both the removal of B as well as freeing it ++from within the loop safely without interfering with the traversal. ++ ++The OSSL_RBT_EMPTY() macro should be used to check whether a red-black tree is ++empty. ++ ++=head1 EXAMPLES ++ ++The following example demonstrates how to declare a red-black tree ++holding integers. Values are inserted into it and the contents of the ++tree are printed in order. Lastly, the internal structure of the tree ++is printed. ++ ++ #include ++ #include ++ #include ++ ++ #include "internal/nelem.h" ++ #include "internal/ossl_rbtree.h" ++ ++ struct node { ++ OSSL_RBT_ENTRY(node) entry; ++ int i; ++ }; ++ ++ static int intcmp(const struct node *, const struct node *); ++ ++ OSSL_RBT_HEAD(inttree, node) head = OSSL_RBT_INITIALIZER(&head); ++ OSSL_RBT_PROTOTYPE(inttree, node, entry, intcmp) ++ OSSL_RBT_GENERATE(inttree, node, entry, intcmp); ++ ++ static const int testdata[] = { ++ 20, 16, 17, 13, 3, 6, 1, 8, 2, 4, ++ 10, 19, 5, 9, 12, 15, 18, 7, 11, 14 ++ }; ++ ++ static int intcmp(const struct node *e1, const struct node *e2) ++ { ++ return e1->i < e2->i ? -1 : e1->i > e2->i; ++ } ++ ++ static void print_tree(struct node *n) ++ { ++ struct node *left, *right; ++ ++ if (n == NULL) { ++ printf("nil"); ++ return; ++ } ++ ++ left = OSSL_RBT_LEFT(inttree, n); ++ right = OSSL_RBT_RIGHT(inttree, n); ++ ++ if (left == NULL && right == NULL) { ++ printf("%d", n->i); ++ } else { ++ printf("%d(", n->i); ++ print_tree(left); ++ printf(","); ++ print_tree(right); ++ printf(")"); ++ } ++ } ++ ++ int main(void) ++ { ++ size_t i; ++ struct node *n; ++ ++ for (i = 0; i < OSSL_NELEM(testdata); i++) { ++ if ((n = OPENSSL_malloc(sizeof(struct node))) == NULL) ++ err(1, NULL); ++ n->i = testdata[i]; ++ OSSL_RBT_INSERT(inttree, &head, n); ++ } ++ ++ OSSL_RBT_FOREACH (n, inttree, &head) { ++ printf("%d\n", n->i); ++ } ++ ++ print_tree(OSSL_RBT_ROOT(inttree, &head)); ++ printf("\n"); ++ ++ return 0; ++ } ++ ++=head1 HISTORY ++ ++The red-black tree implementation comes from David Gwynne. It can be found ++here: L. OpenSSL project ++obtained a permission from David Gwynne to license his work under ++Apache License 2.0 ++ ++Apart from B prefix, the API is compatible with implementation ++done by Neils Provos for OpenBSD. ++ ++The text in this manual page comes from C in OpenBSD, the text was ++authored by Neils Provos (according to the commit history) ++ ++The red-black tree implementation was added in OpenSSL 4.1. ++ ++=head1 COPYRIGHT ++ ++Copyright 2002 Niels Provos ++All rights reserved. ++ ++Copyright 2026 The OpenSSL Project Authors. All Rights Reserved. ++ ++ ++Licensed under the Apache License 2.0 (the "License"). You may not use ++this file except in compliance with the License. You can obtain a copy ++in the file LICENSE in the source distribution or at ++L. ++ ++=cut +diff --git a/include/internal/ossl_rbtree.h b/include/internal/ossl_rbtree.h +new file mode 100644 +index 000000000000..053ed99478bb +--- /dev/null ++++ b/include/internal/ossl_rbtree.h +@@ -0,0 +1,265 @@ ++/* ++ * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved. ++ * Copyright (c) 2016 David Gwynne ++ * ++ * Licensed under the Apache License 2.0 (the "License"). You may not use ++ * this file except in compliance with the License. You can obtain a copy ++ * in the file LICENSE in the source distribution or at ++ * https://www.openssl.org/source/license.html ++ */ ++ ++/* ++ * The code here comes from David Gwynne . The original ++ * version can be found: ++ * https://github.com/dgwynne/data-structures/ ++ * file bst.h. The same code is also part of OpenBSD OS where it is shipped ++ * under BSD license. ++ * ++ * David Gwynne agrees to include modified version to OpenSSL and ship it ++ * under OpenSSL Apache 2.0 license. ++ */ ++#ifndef _OSSL_INTERNAL_RBTREE_H_ ++#define _OSSL_INTERNAL_RBTREE_H_ ++ ++#include "internal/e_os.h" ++ ++/* ++ * List of changes against upstream version: ++ * augmentation mechanism is removed in OpenSSL as there is no demand for it ++ * ++ * prefix changed from rb/rbt to ossl_rbt ++ * ++ * debug version of OSSL_RBT_REMOVE() sets parent, left, right members ++ * to NULL ++ * ++ * cstyle is changed to match OpenSSL. ++ */ ++struct ossl_rbt_type { ++ int (*t_compare)(const void *, const void *); ++ uintptr_t t_offset; /* offset of ossl_rbt_entry in type */ ++}; ++ ++struct ossl_rbt_tree { ++ struct ossl_rbt_entry *rb_root; ++}; ++ ++struct ossl_rbt_entry { ++ struct ossl_rbt_entry *rb_parent; ++ struct ossl_rbt_entry *rb_left; ++ struct ossl_rbt_entry *rb_right; ++ unsigned int rb_color; ++}; ++ ++#define OSSL_RBT_HEAD(_name, _type) \ ++ struct _name { \ ++ struct ossl_rbt_tree rbh_root; \ ++ } ++ ++#define OSSL_RBT_ENTRY(_type) struct ossl_rbt_entry ++ ++static ossl_inline void ++ossl_rbt_init(struct ossl_rbt_tree *rb) ++{ ++ rb->rb_root = NULL; ++} ++ ++static ossl_inline int ++ossl_rbt_empty(struct ossl_rbt_tree *rb) ++{ ++ return rb->rb_root == NULL; ++} ++ ++void *ossl_rbt_insert(const struct ossl_rbt_type *, struct ossl_rbt_tree *, void *); ++void *ossl_rbt_remove(const struct ossl_rbt_type *, struct ossl_rbt_tree *, void *); ++void *ossl_rbt_find(const struct ossl_rbt_type *, struct ossl_rbt_tree *, const void *); ++void *ossl_rbt_nfind(const struct ossl_rbt_type *, struct ossl_rbt_tree *, const void *); ++void *ossl_rbt_root(const struct ossl_rbt_type *, struct ossl_rbt_tree *); ++void *ossl_rbt_min(const struct ossl_rbt_type *, struct ossl_rbt_tree *); ++void *ossl_rbt_max(const struct ossl_rbt_type *, struct ossl_rbt_tree *); ++void *ossl_rbt_next(const struct ossl_rbt_type *, void *); ++void *ossl_rbt_prev(const struct ossl_rbt_type *, void *); ++void *ossl_rbt_left(const struct ossl_rbt_type *, void *); ++void *ossl_rbt_right(const struct ossl_rbt_type *, void *); ++void *ossl_rbt_parent(const struct ossl_rbt_type *, void *); ++void ossl_rbt_set_left(const struct ossl_rbt_type *, void *, void *); ++void ossl_rbt_set_right(const struct ossl_rbt_type *, void *, void *); ++void ossl_rbt_set_parent(const struct ossl_rbt_type *, void *, void *); ++void ossl_rbt_init_rbe(const struct ossl_rbt_type *, void *); ++ ++#define OSSL_RBT_INITIALIZER(_head) \ ++ { \ ++ { \ ++ NULL \ ++ } \ ++ } ++ ++#define OSSL_RBT_PROTOTYPE(_name, _type, _field, _cmp) \ ++ extern const struct ossl_rbt_type *const _name##_OSSL_RBT_TYPE; \ ++ \ ++ ossl_unused static ossl_inline void \ ++ _name##_OSSL_RBT_INIT(struct _name *head) \ ++ { \ ++ ossl_rbt_init(&head->rbh_root); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline struct _type * \ ++ _name##_OSSL_RBT_INSERT(struct _name *head, struct _type *elm) \ ++ { \ ++ return ossl_rbt_insert(_name##_OSSL_RBT_TYPE, &head->rbh_root, elm); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline struct _type * \ ++ _name##_OSSL_RBT_REMOVE(struct _name *head, struct _type *elm) \ ++ { \ ++ return ossl_rbt_remove(_name##_OSSL_RBT_TYPE, &head->rbh_root, elm); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline struct _type * \ ++ _name##_OSSL_RBT_FIND(struct _name *head, const struct _type *key) \ ++ { \ ++ return ossl_rbt_find(_name##_OSSL_RBT_TYPE, &head->rbh_root, key); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline struct _type * \ ++ _name##_OSSL_RBT_NFIND(struct _name *head, const struct _type *key) \ ++ { \ ++ return ossl_rbt_nfind(_name##_OSSL_RBT_TYPE, &head->rbh_root, key); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline struct _type * \ ++ _name##_OSSL_RBT_ROOT(struct _name *head) \ ++ { \ ++ return ossl_rbt_root(_name##_OSSL_RBT_TYPE, &head->rbh_root); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline int \ ++ _name##_OSSL_RBT_EMPTY(struct _name *head) \ ++ { \ ++ return ossl_rbt_empty(&head->rbh_root); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline struct _type * \ ++ _name##_OSSL_RBT_MIN(struct _name *head) \ ++ { \ ++ return ossl_rbt_min(_name##_OSSL_RBT_TYPE, &head->rbh_root); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline struct _type * \ ++ _name##_OSSL_RBT_MAX(struct _name *head) \ ++ { \ ++ return ossl_rbt_max(_name##_OSSL_RBT_TYPE, &head->rbh_root); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline struct _type * \ ++ _name##_OSSL_RBT_NEXT(struct _type *elm) \ ++ { \ ++ return ossl_rbt_next(_name##_OSSL_RBT_TYPE, elm); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline struct _type * \ ++ _name##_OSSL_RBT_PREV(struct _type *elm) \ ++ { \ ++ return ossl_rbt_prev(_name##_OSSL_RBT_TYPE, elm); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline struct _type * \ ++ _name##_OSSL_RBT_LEFT(struct _type *elm) \ ++ { \ ++ return ossl_rbt_left(_name##_OSSL_RBT_TYPE, elm); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline struct _type * \ ++ _name##_OSSL_RBT_RIGHT(struct _type *elm) \ ++ { \ ++ return ossl_rbt_right(_name##_OSSL_RBT_TYPE, elm); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline struct _type * \ ++ _name##_OSSL_RBT_PARENT(struct _type *elm) \ ++ { \ ++ return ossl_rbt_parent(_name##_OSSL_RBT_TYPE, elm); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline void \ ++ _name##_OSSL_RBT_SET_LEFT(struct _type *elm, struct _type *left) \ ++ { \ ++ ossl_rbt_set_left(_name##_OSSL_RBT_TYPE, elm, left); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline void \ ++ _name##_OSSL_RBT_SET_RIGHT(struct _type *elm, struct _type *right) \ ++ { \ ++ ossl_rbt_set_right(_name##_OSSL_RBT_TYPE, elm, right); \ ++ } \ ++ \ ++ ossl_unused static ossl_inline void \ ++ _name##_OSSL_RBT_SET_PARENT(struct _type *elm, struct _type *parent) \ ++ { \ ++ ossl_rbt_set_parent(_name##_OSSL_RBT_TYPE, elm, parent); \ ++ } \ ++ ossl_unused static ossl_inline void \ ++ _name##_OSSL_RBT_INIT_RBE(struct _type *elm) \ ++ { \ ++ ossl_rbt_init_rbe(_name##_OSSL_RBT_TYPE, elm); \ ++ } ++ ++#define OSSL_RBT_GENERATE_INTERNAL(_name, _type, _field, _cmp) \ ++ static int \ ++ _name##_OSSL_RBT_COMPARE(const void *lptr, const void *rptr) \ ++ { \ ++ const struct _type *l = lptr, *r = rptr; \ ++ return _cmp(l, r); \ ++ } \ ++ static const struct ossl_rbt_type _name##_OSSL_RBT_INFO = { \ ++ _name##_OSSL_RBT_COMPARE, \ ++ offsetof(struct _type, _field), \ ++ }; \ ++ const struct ossl_rbt_type *const _name##_OSSL_RBT_TYPE = &_name##_OSSL_RBT_INFO ++ ++#define OSSL_RBT_GENERATE(_name, _type, _field, _cmp) \ ++ OSSL_RBT_GENERATE_INTERNAL(_name, _type, _field, _cmp) ++ ++#define OSSL_RBT_INIT(_name, _head) _name##_OSSL_RBT_INIT(_head) ++#define OSSL_RBT_INSERT(_name, _head, _elm) _name##_OSSL_RBT_INSERT(_head, _elm) ++#define OSSL_RBT_REMOVE(_name, _head, _elm) _name##_OSSL_RBT_REMOVE(_head, _elm) ++#define OSSL_RBT_FIND(_name, _head, _key) _name##_OSSL_RBT_FIND(_head, _key) ++#define OSSL_RBT_NFIND(_name, _head, _key) _name##_OSSL_RBT_NFIND(_head, _key) ++#define OSSL_RBT_ROOT(_name, _head) _name##_OSSL_RBT_ROOT(_head) ++#define OSSL_RBT_EMPTY(_name, _head) _name##_OSSL_RBT_EMPTY(_head) ++#define OSSL_RBT_MIN(_name, _head) _name##_OSSL_RBT_MIN(_head) ++#define OSSL_RBT_MAX(_name, _head) _name##_OSSL_RBT_MAX(_head) ++#define OSSL_RBT_NEXT(_name, _elm) _name##_OSSL_RBT_NEXT(_elm) ++#define OSSL_RBT_PREV(_name, _elm) _name##_OSSL_RBT_PREV(_elm) ++#define OSSL_RBT_LEFT(_name, _elm) _name##_OSSL_RBT_LEFT(_elm) ++#define OSSL_RBT_RIGHT(_name, _elm) _name##_OSSL_RBT_RIGHT(_elm) ++#define OSSL_RBT_PARENT(_name, _elm) _name##_OSSL_RBT_PARENT(_elm) ++#define OSSL_RBT_SET_LEFT(_name, _elm, _l) _name##_OSSL_RBT_SET_LEFT(_elm, _l) ++#define OSSL_RBT_SET_RIGHT(_name, _elm, _r) _name##_OSSL_RBT_SET_RIGHT(_elm, _r) ++#define OSSL_RBT_SET_PARENT(_name, _elm, _p) _name##_OSSL_RBT_SET_PARENT(_elm, _p) ++#ifndef NDEBUG ++#define OSSL_RBT_INIT_RBE(_name, _elm) _name##_OSSL_RBT_INIT_RBE(_elm) ++#else ++#define OSSL_RBT_INIT_RBE(_name, _elm) (void)(0) ++#endif ++ ++#define OSSL_RBT_FOREACH(_e, _name, _head) \ ++ for ((_e) = OSSL_RBT_MIN(_name, (_head)); \ ++ (_e) != NULL; \ ++ (_e) = OSSL_RBT_NEXT(_name, (_e))) ++ ++#define OSSL_RBT_FOREACH_SAFE(_e, _name, _head, _n) \ ++ for ((_e) = OSSL_RBT_MIN(_name, (_head)); \ ++ (_e) != NULL && ((_n) = OSSL_RBT_NEXT(_name, (_e)), 1); \ ++ (_e) = (_n)) ++ ++#define OSSL_RBT_FOREACH_REVERSE(_e, _name, _head) \ ++ for ((_e) = OSSL_RBT_MAX(_name, (_head)); \ ++ (_e) != NULL; \ ++ (_e) = OSSL_RBT_PREV(_name, (_e))) ++ ++#define OSSL_RBT_FOREACH_REVERSE_SAFE(_e, _name, _head, _n) \ ++ for ((_e) = OSSL_RBT_MAX(_name, (_head)); \ ++ (_e) != NULL && ((_n) = OSSL_RBT_PREV(_name, (_e)), 1); \ ++ (_e) = (_n)) ++ ++#endif /* _OSSL_INTERNAL_RBTREE_H_ */ +diff --git a/ssl/build.info b/ssl/build.info +index 7f4ecaa68f50..515d5db5d403 100644 +--- a/ssl/build.info ++++ b/ssl/build.info +@@ -21,7 +21,8 @@ SOURCE[../libssl]=\ + # For shared builds we need to include the libcrypto packet.c and quic_vlint.c + # in libssl as well. + SHARED_SOURCE[../libssl]=\ +- ../crypto/packet.c ../crypto/quic_vlint.c ../crypto/time.c ++ ../crypto/packet.c ../crypto/quic_vlint.c ../crypto/time.c \ ++ ../crypto/rbtree/rbtree.c + + IF[{- !$disabled{'deprecated-3.0'} -}] + SOURCE[../libssl]=ssl_rsa_legacy.c +diff --git a/test/build.info b/test/build.info +index 830bfd1fa750..8b1198d2837d 100644 +--- a/test/build.info ++++ b/test/build.info +@@ -70,7 +70,8 @@ IF[{- !$disabled{tests} -}] + ca_internals_test bio_tfo_test membio_test bio_dgram_test list_test \ + fips_version_test x509_test hpke_test pairwise_fail_test \ + nodefltctxtest evp_xof_test x509_load_cert_file_test bio_meth_test \ +- x509_acert_test x509_req_test strtoultest bio_pw_callback_test ++ x509_acert_test x509_req_test strtoultest bio_pw_callback_test \ ++ ossl_rbtree_test + + IF[{- !$disabled{'rpk'} -}] + PROGRAMS{noinst}=rpktest +@@ -1295,6 +1296,10 @@ ENDIF + INCLUDE[bio_pw_callback_test]=../include ../apps/include + DEPEND[bio_pw_callback_test]=../libcrypto libtestutil.a + ++ SOURCE[ossl_rbtree_test]=ossl_rbtree_test.c ++ INCLUDE[ossl_rbtree_test]=../include ../apps/include ++ DEPEND[ossl_rbtree_test]=../libcrypto.a libtestutil.a ++ + {- + use File::Spec::Functions; + use File::Basename; +diff --git a/test/ossl_rbtree_test.c b/test/ossl_rbtree_test.c +new file mode 100644 +index 000000000000..ef6f1d416380 +--- /dev/null ++++ b/test/ossl_rbtree_test.c +@@ -0,0 +1,276 @@ ++/* ++ * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved. ++ * ++ * Licensed under the Apache License 2.0 (the "License"). You may not use ++ * this file except in compliance with the License. You can obtain a copy ++ * in the file LICENSE in the source distribution or at ++ * https://www.openssl.org/source/license.html ++ */ ++ ++#include ++ ++#include "testutil.h" ++#include "internal/nelem.h" ++#include "internal/ossl_rbtree.h" ++ ++static const char *test_data[] = { ++ "alpha", ++ "bravo", ++ "charlie", ++ "delta", ++ "echo", ++ "foxtrot", ++ "golf", ++ "hotel", ++ "india", ++ "juliet", ++ "kilo", ++ "lima", ++ "mike", ++ "november", ++ "oscar", ++ "papa", ++ "quebec", ++ "romeo", ++ "sierra", ++ "tango", ++ "uniform", ++ "victor", ++ "whiskey", ++ "x-ray", ++ "yankey", ++ "zulu", ++}; ++ ++typedef struct test_rbt { ++ OSSL_RBT_ENTRY(test_rbt) ++ rbt_entry; ++ const char *rbt_data; ++} TEST_RBT_T; ++ ++static OSSL_RBT_HEAD(ossl_rbt, test_rbt) ++ rbt_head; ++ ++static TEST_RBT_T nodes_rbt[26]; ++ ++static int cmp(const TEST_RBT_T *a, const TEST_RBT_T *b); ++ ++OSSL_RBT_PROTOTYPE(ossl_rbt, test_rbt, rbt_entry, cmp) ++ ++OSSL_RBT_GENERATE(ossl_rbt, test_rbt, rbt_entry, cmp); ++ ++static int cmp(const TEST_RBT_T *a_rbt, const TEST_RBT_T *b_rbt) ++{ ++ return strcmp(a_rbt->rbt_data, b_rbt->rbt_data); ++} ++ ++static int test_rbt_insert(void) ++{ ++ unsigned int i; ++ TEST_RBT_T *found_rbt, *node_rbt; ++ ++ OSSL_RBT_INIT(ossl_rbt, &rbt_head); ++ ++ for (i = OSSL_NELEM(test_data); i != 0; i--) { ++ node_rbt = &nodes_rbt[i - 1]; ++ OSSL_RBT_INIT_RBE(ossl_rbt, node_rbt); ++ node_rbt->rbt_data = test_data[i - 1]; ++ found_rbt = OSSL_RBT_INSERT(ossl_rbt, &rbt_head, node_rbt); ++ if (!TEST_ptr_eq(found_rbt, NULL)) { ++ TEST_info("%s %p(%s) found already %p(%s) @ %u\n", OPENSSL_FUNC, ++ (void *)node_rbt, node_rbt->rbt_data, ++ (void *)found_rbt, found_rbt->rbt_data, i); ++ return 0; ++ } ++ } ++ ++ return 1; ++} ++ ++static int test_rbt_min(void) ++{ ++ unsigned int i; ++ int match; ++ TEST_RBT_T *node_rbt; ++ ++ if (test_rbt_insert() == 0) ++ return 0; ++ ++ node_rbt = OSSL_RBT_MIN(ossl_rbt, &rbt_head); ++ if (!TEST_ptr(node_rbt)) { ++ TEST_info("%s OSSL_RBT_MIN() returns NULL", OPENSSL_FUNC); ++ return 0; ++ } ++ ++ for (i = 0; i < OSSL_NELEM(test_data); i++) { ++ match = strcmp(node_rbt->rbt_data, test_data[i]); ++ if (!TEST_int_eq(match, 0)) { ++ TEST_info("%s %s != %s @ %u", OPENSSL_FUNC, ++ node_rbt->rbt_data, test_data[i], i); ++ return 0; ++ } ++ node_rbt = OSSL_RBT_NEXT(ossl_rbt, node_rbt); ++ } ++ ++ if (!TEST_ptr_eq(node_rbt, NULL)) { ++ TEST_info("%s OSSL_RBT_NEXT() is not NULL", OPENSSL_FUNC); ++ return 0; ++ } ++ ++ return 1; ++} ++ ++static int test_rbt_max(void) ++{ ++ unsigned int i; ++ int match; ++ TEST_RBT_T *node_rbt; ++ ++ if (test_rbt_insert() == 0) ++ return 0; ++ ++ node_rbt = OSSL_RBT_MAX(ossl_rbt, &rbt_head); ++ if (!TEST_ptr(node_rbt)) { ++ TEST_info("%s OSSL_RBT_MIN() returns NULL", OPENSSL_FUNC); ++ return 0; ++ } ++ ++ for (i = OSSL_NELEM(test_data); i > 0; i--) { ++ match = strcmp(node_rbt->rbt_data, test_data[i - 1]); ++ if (!TEST_int_eq(match, 0)) { ++ TEST_info("%s %s != %s @ %u", OPENSSL_FUNC, ++ node_rbt->rbt_data, test_data[i - 1], i); ++ return 0; ++ } ++ node_rbt = OSSL_RBT_PREV(ossl_rbt, node_rbt); ++ } ++ ++ if (!TEST_ptr_eq(node_rbt, NULL)) { ++ TEST_info("%s OSSL_RBT_PREV() is not NULL", OPENSSL_FUNC); ++ return 0; ++ } ++ ++ return 1; ++} ++ ++static int test_rbt_find_remove(void) ++{ ++ unsigned int i; ++ TEST_RBT_T *node_rbt, *removed_rbt; ++ TEST_RBT_T key_rbt; ++ ++ if (test_rbt_insert() == 0) ++ return 0; ++ ++ for (i = 0; i < OSSL_NELEM(test_data); i++) { ++ key_rbt.rbt_data = test_data[i]; ++ node_rbt = OSSL_RBT_FIND(ossl_rbt, &rbt_head, &key_rbt); ++ if (!TEST_ptr(node_rbt)) { ++ TEST_info("%s %s not found in tree @ %u", OPENSSL_FUNC, ++ key_rbt.rbt_data, i); ++ return 0; ++ } ++ removed_rbt = OSSL_RBT_REMOVE(ossl_rbt, &rbt_head, node_rbt); ++ if (!TEST_ptr_eq(node_rbt, removed_rbt)) { ++ TEST_info("%s node_rbt(%p) != removed_rbt(%p) @ %u", ++ OPENSSL_FUNC, (void *)node_rbt, (void *)removed_rbt, i); ++ return 0; ++ } ++ ++ node_rbt = OSSL_RBT_FIND(ossl_rbt, &rbt_head, &key_rbt); ++ if (!TEST_ptr_eq(node_rbt, NULL)) { ++ TEST_info("%s %s(%p) still found after being removed @ %u", ++ OPENSSL_FUNC, node_rbt->rbt_data, (void *)node_rbt, i); ++ return 0; ++ } ++ } ++ ++ if (!TEST_int_ne(OSSL_RBT_EMPTY(ossl_rbt, &rbt_head), 0)) { ++ TEST_info("%s rbt is not empty", OPENSSL_FUNC); ++ return 0; ++ } ++ ++ return 1; ++} ++ ++static int test_rbt_dup_insert(void) ++{ ++ unsigned int i; ++ int match; ++ TEST_RBT_T *conflict_rbt; ++ TEST_RBT_T insert_rbt; ++ ++ if (test_rbt_insert() == 0) ++ return 0; ++ ++ for (i = 0; i < OSSL_NELEM(test_data); i++) { ++ OSSL_RBT_INIT_RBE(ossl_rbt, &insert_rbt); ++ insert_rbt.rbt_data = test_data[i]; ++ conflict_rbt = OSSL_RBT_INSERT(ossl_rbt, &rbt_head, &insert_rbt); ++ if (!TEST_ptr(conflict_rbt)) { ++ TEST_info("%s %s not found in tree @ %u", OPENSSL_FUNC, ++ insert_rbt.rbt_data, i); ++ return 0; ++ } ++ match = strcmp(conflict_rbt->rbt_data, insert_rbt.rbt_data); ++ if (!TEST_int_eq(match, 0)) { ++ TEST_info("%s insert(%s) != conflict(%s) @ %u", ++ OPENSSL_FUNC, insert_rbt.rbt_data, conflict_rbt->rbt_data, i); ++ return 0; ++ } ++ } ++ ++ return 1; ++} ++ ++static int test_rbt_foreach(void) ++{ ++ unsigned int i; ++ int match; ++ TEST_RBT_T *walk_rbt, *save_rbt; ++ ++ if (test_rbt_insert() == 0) ++ return 0; ++ ++ i = 0; ++ OSSL_RBT_FOREACH (walk_rbt, ossl_rbt, &rbt_head) { ++ match = strcmp(walk_rbt->rbt_data, test_data[i]); ++ if (!TEST_int_eq(match, 0)) { ++ TEST_info("%s expected: %s got: %s @ %u", ++ OPENSSL_FUNC, walk_rbt->rbt_data, test_data[i], i); ++ return 0; ++ } ++ i++; ++ } ++ ++ i = 0; ++ OSSL_RBT_FOREACH_SAFE (walk_rbt, ossl_rbt, &rbt_head, save_rbt) { ++ match = strcmp(walk_rbt->rbt_data, test_data[i]); ++ if (!TEST_int_eq(match, 0)) { ++ TEST_info("%s expected: %s got: %s @ %u", ++ OPENSSL_FUNC, walk_rbt->rbt_data, test_data[i], i); ++ return 0; ++ } ++ OSSL_RBT_REMOVE(ossl_rbt, &rbt_head, walk_rbt); ++ i++; ++ } ++ ++ if (!TEST_int_ne(OSSL_RBT_EMPTY(ossl_rbt, &rbt_head), 0)) { ++ TEST_info("%s rbt is not empty", OPENSSL_FUNC); ++ return 0; ++ } ++ ++ return 1; ++} ++ ++int setup_tests(void) ++{ ++ ADD_TEST(test_rbt_insert); ++ ADD_TEST(test_rbt_min); ++ ADD_TEST(test_rbt_max); ++ ADD_TEST(test_rbt_find_remove); ++ ADD_TEST(test_rbt_dup_insert); ++ ADD_TEST(test_rbt_foreach); ++ ++ return 1; ++} +diff --git a/test/recipes/02-test_time.t b/test/recipes/02-test_rbtree.t +similarity index 72% +copy from test/recipes/02-test_time.t +copy to test/recipes/02-test_rbtree.t +index c4534034fcfd..fb92affda138 100644 +--- a/test/recipes/02-test_time.t ++++ b/test/recipes/02-test_rbtree.t +@@ -1,12 +1,11 @@ + #! /usr/bin/env perl +-# Copyright 2023 The OpenSSL Project Authors. All Rights Reserved. ++# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved. + # + # Licensed under the Apache License 2.0 (the "License"). You may not use + # this file except in compliance with the License. You can obtain a copy + # in the file LICENSE in the source distribution or at + # https://www.openssl.org/source/license.html + +- + use OpenSSL::Test::Simple; + +-simple_test("test_time", "time_test"); ++simple_test("ossl_rbtree_test", "ossl_rbtree_test"); +diff --git a/util/missingcrypto-internal.txt b/util/missingcrypto-internal.txt +index 54e1bc9ba7dd..41115bbec3b1 100644 +--- a/util/missingcrypto-internal.txt ++++ b/util/missingcrypto-internal.txt +@@ -6,3 +6,4 @@ ossl_do_PVK_header(3) + ossl_do_blob_header(3) + ossl_b2i(3) + ossl_b2i_bio(3) ++ossl_rbtree(3) diff -Nru openssl-3.5.7/debian/patches/Add-a-test-to-check-for-quic-unvalidated-credit.patch openssl-3.5.7/debian/patches/Add-a-test-to-check-for-quic-unvalidated-credit.patch --- openssl-3.5.7/debian/patches/Add-a-test-to-check-for-quic-unvalidated-credit.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/Add-a-test-to-check-for-quic-unvalidated-credit.patch 2026-09-29 20:10:10.000000000 +0000 @@ -0,0 +1,371 @@ +From: Neil Horman +Date: Tue, 25 Aug 2026 15:04:16 -0400 +Subject: 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. +--- + test/quicapitest.c | 328 +++++++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 328 insertions(+) + +diff --git a/test/quicapitest.c b/test/quicapitest.c +index d40413898a6f..164154e13517 100644 +--- a/test/quicapitest.c ++++ b/test/quicapitest.c +@@ -10,6 +10,14 @@ + #include + #include + ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ + #include + #include + #include +@@ -21,6 +29,9 @@ + #include "../ssl/ssl_local.h" + #include "../ssl/quic/quic_channel_local.h" + #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 OSSL_PROVIDER *defctxnull = NULL; +@@ -2908,6 +2919,322 @@ static int test_ssl_accept_connection(void) + 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; ++ 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) { ++ fprintf(stderr, "crafted queue has %zu datagrams / %zu bytes, " ++ "expected 1 / %d\n", ++ 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) { ++ fprintf(stderr, "failed to flush crafted datagram\n"); ++ 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; ++ 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)) ++ || !TEST_true(SSL_CTX_set1_groups_list(sctx, "X25519")) ++ || !TEST_true(SSL_CTX_set_ciphersuites(sctx, "TLS_AES_128_GCM_SHA256")) ++ || !TEST_true(SSL_CTX_set1_groups_list(cctx, "X25519")) ++ || !TEST_true(SSL_CTX_set_ciphersuites(cctx, "TLS_AES_128_GCM_SHA256"))) ++ 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 ++ * acidentally drop frames. ++ */ ++ ina.s_addr = htonl(INADDR_LOOPBACK); ++ if (!TEST_ptr((server_addr = create_addr(&ina, SERVER_PORT))) ++ || (!TEST_ptr((client_addr = create_addr(&ina, CLIENT_PORT)))) ++ || (!TEST_true(BIO_new_bio_dgram_pair(&c_bio, 65535, &s_bio, 65535))) ++ || (!TEST_true(bio_addr_bind(c_bio, client_addr))) ++ || (!TEST_true(bio_addr_bind(s_bio, server_addr))) ++ || (!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, 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, jsut 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, 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); ++ return ret; ++} ++ + static SSL *quic_verify_ssl = NULL; + + static int quic_verify_cb(int ok, X509_STORE_CTX *ctx) +@@ -3189,6 +3516,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_client_hello_retry); + ADD_TEST(test_quic_resize_txe); diff -Nru openssl-3.5.7/debian/patches/Add-explicit-tests-to-check-some-typical-stream-reassembl.patch openssl-3.5.7/debian/patches/Add-explicit-tests-to-check-some-typical-stream-reassembl.patch --- openssl-3.5.7/debian/patches/Add-explicit-tests-to-check-some-typical-stream-reassembl.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/Add-explicit-tests-to-check-some-typical-stream-reassembl.patch 2026-09-29 20:10:12.000000000 +0000 @@ -0,0 +1,289 @@ +From: Alexandr Nedvedicky +Date: Mon, 10 Aug 2026 12:34:32 +0200 +Subject: Add explicit tests to check some typical stream reassembly situation + +the list of tests is as follows:s + - partial overlap between newly arriving chunk and existing range + (there is intersection between existing range and newly arriving chunk) + - full overlap: existing range is subset of newly arriving stream chunk + - newly arriving chunk is superset of two existing ranges. + - reassembly of 1-byte chunks which are kept in stream buffers + - we need to also test handling of short chunks in directo storage + +Co-authored-by: Jakub Zelenka + +Assisted-by: Claude:claude-fable-5 +Reviewed-by: Tomas Mraz +Reviewed-by: Milan Broz +Merge-date: Sat Sep 26 11:53:42 2026 +Merged-from: https://github.com/openssl/openssl/pull/32769 +--- + test/quic_stream_test.c | 160 +++++++++++++++++++++++++++++++++++++++++------- + 1 file changed, 137 insertions(+), 23 deletions(-) + +diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c +index 41bc5d3b2bfc..23a2ec85b460 100644 +--- a/test/quic_stream_test.c ++++ b/test/quic_stream_test.c +@@ -407,6 +407,128 @@ static int test_single_copy_read(QUIC_RSTREAM *qrs, + return 1; + } + ++static const unsigned char simple_data[] = "Hello world! And thank you for all the fish!"; ++ ++/* ++ * Walks the basic sequential contract of the receive stream: a gap at ++ * the head reads zero bytes without signalling the end of stream even ++ * when FIN is already known, duplicate FIN frames with a matching final ++ * size are accepted, available() reports the final size together with ++ * FIN, partial reads flip the FIN flag exactly with the last byte and ++ * reading at the end of stream stays successful. Runs once with the ++ * plain read and once with the single copy record path. ++ */ ++static int test_rstream_simple(int idx) ++{ ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ OSSL_QRX_PKT *pkt[8] = { NULL }; ++ int ret = 0; ++ unsigned char buf[sizeof(simple_data)]; ++ size_t readbytes = 0, avail = 0, i; ++ int fin = 0; ++ int use_sc = (idx & 1) != 0; ++ int (*read_fn)(QUIC_RSTREAM *, unsigned char *, size_t, size_t *, ++ int *) ++ = use_sc ? test_single_copy_read ++ : ossl_quic_rstream_read; ++ ++ /* every frame arrives in a packet, as it does in production */ ++ for (i = 0; i < OSSL_NELEM(pkt); ++i) ++ if (!TEST_ptr(pkt[i] = pkt_test_new(1200))) ++ goto err; ++ ++ if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) ++ goto err; ++ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[0], 5, ++ simple_data + 5, 10, 0)) ++ || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[1], ++ sizeof(simple_data) - 1, ++ simple_data + sizeof(simple_data) - 1, ++ 1, 1)) ++ || !TEST_true(ossl_quic_rstream_peek(rstream, buf, sizeof(buf), ++ &readbytes, &fin)) ++ || !TEST_false(fin) ++ || !TEST_size_t_eq(readbytes, 0) ++ || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[2], ++ sizeof(simple_data) - 10, ++ simple_data + sizeof(simple_data) - 10, ++ 10, 1)) ++ || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[3], 0, ++ simple_data, 1, 0)) ++ || !TEST_true(ossl_quic_rstream_peek(rstream, buf, sizeof(buf), ++ &readbytes, &fin)) ++ || !TEST_false(fin) ++ || !TEST_size_t_eq(readbytes, 1) ++ || !TEST_mem_eq(buf, 1, simple_data, 1) ++ || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[4], ++ 0, simple_data, ++ 10, 0)) ++ || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[5], ++ sizeof(simple_data), ++ NULL, ++ 0, 1)) ++ || !TEST_true(ossl_quic_rstream_peek(rstream, buf, sizeof(buf), ++ &readbytes, &fin)) ++ || !TEST_false(fin) ++ || !TEST_size_t_eq(readbytes, 15) ++ || !TEST_mem_eq(buf, 15, simple_data, 15) ++ || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[6], ++ 15, ++ simple_data + 15, ++ sizeof(simple_data) - 15, 1)) ++ || !TEST_true(ossl_quic_rstream_available(rstream, &avail, &fin)) ++ || !TEST_true(fin) ++ || !TEST_size_t_eq(avail, sizeof(simple_data)) ++ || !TEST_true(read_fn(rstream, buf, 2, &readbytes, &fin)) ++ || !TEST_false(fin) ++ || !TEST_size_t_eq(readbytes, 2) ++ || !TEST_mem_eq(buf, 2, simple_data, 2) ++ || !TEST_true(read_fn(rstream, buf + 2, 12, &readbytes, &fin)) ++ || !TEST_false(fin) ++ || !TEST_size_t_eq(readbytes, 12) ++ || !TEST_mem_eq(buf + 2, 12, simple_data + 2, 12) ++ || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[7], ++ sizeof(simple_data), ++ NULL, ++ 0, 1)) ++ || !TEST_true(read_fn(rstream, buf + 14, 5, &readbytes, &fin)) ++ || !TEST_false(fin) ++ || !TEST_size_t_eq(readbytes, 5) ++ || !TEST_mem_eq(buf, 14 + 5, simple_data, 14 + 5) ++ || !TEST_true(read_fn(rstream, buf + 14 + 5, sizeof(buf) - 14 - 5, ++ &readbytes, &fin)) ++ || !TEST_true(fin) ++ || !TEST_size_t_eq(readbytes, sizeof(buf) - 14 - 5) ++ || !TEST_mem_eq(buf, sizeof(buf), simple_data, sizeof(simple_data)) ++ || !TEST_true(read_fn(rstream, buf, sizeof(buf), &readbytes, &fin)) ++ || !TEST_true(fin) ++ || !TEST_size_t_eq(readbytes, 0)) ++ goto err; ++ ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ ++ ret = 1; ++ ++err: ++ ossl_quic_rstream_free(rstream); ++ /* All the references held by the stream must have been released */ ++ for (i = 0; i < OSSL_NELEM(pkt); ++i) { ++ if (pkt[i] != NULL ++ && !TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) ++ ret = 0; ++ pkt_test_free(pkt[i]); ++ } ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ ossl_quic_channel_free(ch); ++ return ret; ++} ++ + static int test_rstream_random(int idx) + { + unsigned char *bulk_data = NULL; +@@ -608,7 +730,7 @@ static int test_rstream_pkt(void) + */ + if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_c, 0, + pdata, 15, 0)) +- || !TEST_size_t_eq(pkt_test_refcount(pkt_a), 2) ++ || !TEST_size_t_eq(pkt_test_refcount(pkt_a), 3) + || !TEST_size_t_eq(pkt_test_refcount(pkt_c), 2)) + goto err; + +@@ -627,10 +749,7 @@ static int test_rstream_pkt(void) + if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_b, 15, + pdata + 15, 5, 0)) + || !TEST_size_t_eq(pkt_test_refcount(pkt_b), 2) +- || !TEST_true(ossl_quic_rstream_resize_rbuf(rstream, sizeof(pdata))) +- || !TEST_true(ossl_quic_rstream_move_to_rbuf(rstream)) +- || !TEST_size_t_eq(pkt_test_refcount(pkt_a), 1) +- || !TEST_size_t_eq(pkt_test_refcount(pkt_b), 1)) ++ || !TEST_size_t_eq(pkt_test_refcount(pkt_a), 2)) + goto err; + + /* The moved data is still readable from the ring buffer */ +@@ -1195,10 +1314,11 @@ static int test_rstream_chunk_partial_overlap(void) + + ok = 1; + err: ++ ossl_quic_rstream_free(rstream); ++ + for (i = 0; i < OSSL_NELEM(tsc_buf); i++) + pkt_test_free(pkt[i]); + +- ossl_quic_rstream_free(rstream); + ossl_quic_rstream_qparm_destroy(rsqp); + ossl_quic_channel_free(ch); + +@@ -1339,10 +1459,11 @@ static int test_rstream_chunk_full_overlap(void) + + ok = 1; + err: ++ ossl_quic_rstream_free(rstream); ++ + for (i = 0; i < OSSL_NELEM(tsc_buf); i++) + pkt_test_free(pkt[i]); + +- ossl_quic_rstream_free(rstream); + ossl_quic_rstream_qparm_destroy(rsqp); + ossl_quic_channel_free(ch); + +@@ -1538,10 +1659,11 @@ static int test_rstream_range_overlap(void) + + ok = 1; + err: ++ ossl_quic_rstream_free(rstream); ++ + for (i = 0; i < OSSL_NELEM(tsc_buf); i++) + pkt_test_free(pkt[i]); + +- ossl_quic_rstream_free(rstream); + ossl_quic_rstream_qparm_destroy(rsqp); + ossl_quic_channel_free(ch); + +@@ -1692,10 +1814,11 @@ static int test_rstream_prepend_byte_chunks(void) + + ok = 1; + err: ++ ossl_quic_rstream_free(rstream); ++ + for (i = 0; i < OSSL_NELEM(tsc_buf); i++) + pkt_test_free(pkt[i]); + +- ossl_quic_rstream_free(rstream); + ossl_quic_rstream_qparm_destroy(rsqp); + ossl_quic_channel_free(ch); + +@@ -1846,10 +1969,11 @@ static int test_rstream_append_byte_chunks(void) + + ok = 1; + err: ++ ossl_quic_rstream_free(rstream); ++ + for (i = 0; i < OSSL_NELEM(tsc_buf); i++) + pkt_test_free(pkt[i]); + +- ossl_quic_rstream_free(rstream); + ossl_quic_rstream_qparm_destroy(rsqp); + ossl_quic_channel_free(ch); + +@@ -2026,10 +2150,11 @@ static int test_rstream_mix_chunks(void) + + ok = 1; + err: ++ ossl_quic_rstream_free(rstream); ++ + for (i = 0; i < OSSL_NELEM(tsc_buf); i++) + pkt_test_free(pkt[i]); + +- ossl_quic_rstream_free(rstream); + ossl_quic_rstream_qparm_destroy(rsqp); + ossl_quic_channel_free(ch); + +@@ -2153,12 +2278,6 @@ static int test_final_size_violation_fin_first(void) + + ok = 1; + err: +- /* +- * the data from rstream has not been consumed, +- * references to packets are still retained there. +- * therefore we need to free rstream before freeing +- * pkckets. +- */ + ossl_quic_rstream_free(rstream); + + for (i = 0; i < OSSL_NELEM(tsc_buf); i++) +@@ -2272,12 +2391,6 @@ static int test_final_size_violation_data_first(void) + + ok = 1; + err: +- /* +- * the data from rstream has not been consumed, +- * references to packets are still retained there. +- * therefore we need to free rstream before freeing +- * pkckets. +- */ + ossl_quic_rstream_free(rstream); + + for (i = 0; i < OSSL_NELEM(tsc_buf); i++) +@@ -2293,6 +2406,7 @@ int setup_tests(void) + { + ADD_TEST(test_sstream_simple); + ADD_ALL_TESTS(test_sstream_bulk, 100); ++ ADD_ALL_TESTS(test_rstream_simple, 2); + ADD_ALL_TESTS(test_rstream_random, 100); + ADD_TEST(test_rstream_pkt); + ADD_TEST(test_rstream_pkt_overhead); diff -Nru openssl-3.5.7/debian/patches/Add-ossl_list_TYPE_join-head-tail-function.patch openssl-3.5.7/debian/patches/Add-ossl_list_TYPE_join-head-tail-function.patch --- openssl-3.5.7/debian/patches/Add-ossl_list_TYPE_join-head-tail-function.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/Add-ossl_list_TYPE_join-head-tail-function.patch 2026-09-29 20:10:12.000000000 +0000 @@ -0,0 +1,161 @@ +From: Alexandr Nedvedicky +Date: Mon, 20 Jul 2026 09:44:53 +0200 +Subject: Add ossl_list_TYPE_join(head, tail) function + +The function appends list tail to list head. List tail becomes +empty after the function returns. + +Reviewed-by: Eugene Syromiatnikov +Reviewed-by: Nikola Pajkovsky +Reviewed-by: Norbert Pocs +MergeDate: Thu Aug 6 11:58:58 2026 +(Merged from https://github.com/openssl/openssl/pull/32031) +--- + doc/internal/man3/DEFINE_LIST_OF.pod | 11 +++++++-- + include/internal/list.h | 29 +++++++++++++++++++++++ + test/list_test.c | 46 ++++++++++++++++++++++++++++++++++++ + 3 files changed, 84 insertions(+), 2 deletions(-) + +diff --git a/doc/internal/man3/DEFINE_LIST_OF.pod b/doc/internal/man3/DEFINE_LIST_OF.pod +index d886defc43fb..6291a2d91062 100644 +--- a/doc/internal/man3/DEFINE_LIST_OF.pod ++++ b/doc/internal/man3/DEFINE_LIST_OF.pod +@@ -8,7 +8,7 @@ ossl_list_TYPE_is_empty, ossl_list_TYPE_num, + ossl_list_TYPE_head, ossl_list_TYPE_tail, + ossl_list_TYPE_next, ossl_list_TYPE_prev, + ossl_list_TYPE_remove, ossl_list_TYPE_insert_head, ossl_list_TYPE_insert_tail, +-ossl_list_TYPE_insert_before, ossl_list_TYPE_after ++ossl_list_TYPE_insert_before, ossl_list_TYPE_after, ossl_list_TYPE_join + - doubly linked list + + =head1 SYNOPSIS +@@ -38,6 +38,7 @@ ossl_list_TYPE_insert_before, ossl_list_TYPE_after + void ossl_list_TYPE_insert_before(OSSL_LIST(name) *list, type *existing, + type *elem); + void ossl_list_TYPE_insert_after(OSSL_LIST(name) *list, type *existing, type *elem); ++ void ossl_list_TYPE_join(OSSL_LIST(name) *lh, OSSL_LIST(name) *lt); + + =head1 DESCRIPTION + +@@ -90,6 +91,10 @@ B_insert_after>() inserts the element I, + which must not be in the list, into the I immediately after the + I element. + ++B_join<()> joins list B with list B. ++List B is appended to list B. The list B becomes empty, ++as all its members are part of B after the function returns. ++ + =head1 RETURN VALUES + + B_is_empty>() returns nonzero if the list is empty and zero +@@ -124,7 +129,9 @@ the specified element in the list. + + =head1 HISTORY + +-The functions described here were all added in OpenSSL 3.2. ++ossl_list_TYPE_join() was added in OpenSSL 4.1, 4.0.2, 3.6.4, 3.5.8, and 3.4.7. ++ ++The rest of the functions described here was added in OpenSSL 3.2. + + =head1 COPYRIGHT + +diff --git a/include/internal/list.h b/include/internal/list.h +index 8bb0b741bed1..cd43471409f5 100644 +--- a/include/internal/list.h ++++ b/include/internal/list.h +@@ -194,6 +194,35 @@ + list->omega = elem; \ + list->num_elems++; \ + } \ ++ static ossl_unused ossl_inline void \ ++ ossl_list_##name##_join(OSSL_LIST(name) * lh, OSSL_LIST(name) * lt) \ ++ { \ ++ OSSL_LIST_DBG(type * _p); /* local variable '_p' when debug */ \ ++ if (lt == NULL || lh == NULL || lt->num_elems == 0 || lh == lt) \ ++ return; \ ++ /* \ ++ * let's be optimistic about size_t overflow here: it can not happen. \ ++ */ \ ++ lh->num_elems += lt->num_elems; \ ++ if (lh->omega == NULL) { \ ++ assert(lh->alpha == NULL); \ ++ lh->omega = lt->omega; \ ++ lh->alpha = lt->alpha; \ ++ } else { \ ++ if (lt->alpha != NULL) \ ++ ((type *)lt->alpha)->ossl_list_##name.prev = lh->omega; \ ++ ((type *)lh->omega)->ossl_list_##name.next = lt->alpha; \ ++ } \ ++ OSSL_LIST_DBG(for (_p = (type *)lt->alpha; \ ++ assert(_p == NULL || _p->ossl_list_##name.list == lt), _p != NULL; \ ++ _p = _p->ossl_list_##name.next) \ ++ _p->ossl_list_##name.list \ ++ = lh); \ ++ lh->omega = lt->omega; \ ++ lt->alpha = NULL; \ ++ lt->omega = NULL; \ ++ lt->num_elems = 0; \ ++ } \ + struct ossl_list_st_##name + + #define DEFINE_LIST_OF(name, type) \ +diff --git a/test/list_test.c b/test/list_test.c +index 9deb57c4b91a..7798ea68348e 100644 +--- a/test/list_test.c ++++ b/test/list_test.c +@@ -175,9 +175,55 @@ static int test_insert(void) + return 1; + } + ++static int test_join(void) ++{ ++ OSSL_LIST(int) ++ l_h, l_t; ++ INTL elem_h[20]; ++ INTL elem_t[20]; ++ int i; ++ ++ ossl_list_int_init(&l_h); ++ ossl_list_int_init(&l_t); ++ ossl_list_int_join(&l_h, &l_t); ++ if (!TEST_size_t_eq(ossl_list_int_num(&l_t), 0)) ++ return 0; ++ ++ for (i = 0; i < (int)OSSL_NELEM(elem_h); i++) { ++ ossl_list_int_init_elem(&elem_h[i]); ++ elem_h[i].n = i; ++ ossl_list_int_insert_head(&l_h, &elem_h[i]); ++ } ++ ++ for (i = 0; i < (int)OSSL_NELEM(elem_t); i++) { ++ ossl_list_int_init_elem(&elem_t[i]); ++ elem_t[i].n = i + 10; ++ ossl_list_int_insert_head(&l_t, &elem_t[i]); ++ } ++ ++ ossl_list_int_join(NULL, NULL); ++ ++ ossl_list_int_join(NULL, &l_t); ++ if (!TEST_size_t_eq(ossl_list_int_num(&l_t), OSSL_NELEM(elem_t))) ++ return 0; ++ ++ ossl_list_int_join(&l_h, NULL); ++ if (!TEST_size_t_eq(ossl_list_int_num(&l_h), OSSL_NELEM(elem_h))) ++ return 0; ++ ++ ossl_list_int_join(&l_h, &l_t); ++ if (!TEST_size_t_eq(ossl_list_int_num(&l_h), OSSL_NELEM(elem_h) + OSSL_NELEM(elem_t))) ++ return 0; ++ ++ if (!TEST_true(ossl_list_int_is_empty(&l_t))) ++ return 0; ++ ++ return 1; ++} + int setup_tests(void) + { + ADD_TEST(test_fizzbuzz); + ADD_TEST(test_insert); ++ ADD_TEST(test_join); + return 1; + } diff -Nru openssl-3.5.7/debian/patches/Add-regression-tests-for-the-SSL_set_SSL_CTX-sigalg-state.patch openssl-3.5.7/debian/patches/Add-regression-tests-for-the-SSL_set_SSL_CTX-sigalg-state.patch --- openssl-3.5.7/debian/patches/Add-regression-tests-for-the-SSL_set_SSL_CTX-sigalg-state.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/Add-regression-tests-for-the-SSL_set_SSL_CTX-sigalg-state.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,484 @@ +From: Matt Caswell +Date: Wed, 9 Sep 2026 11:02:16 +0100 +Subject: Add regression tests for the SSL_set_SSL_CTX() sigalg state + +The fix for CVE-2026-72897 made SSL_set_SSL_CTX() refresh the +connection's signature algorithm state so that it describes the context +being installed. Cover it. + +That state is sized from the SSL_CTX SSL_new() was called on, and a +provider sigalg's sig_idx is its position in the list of whichever +SSL_CTX a peer codepoint is resolved against, so the two have to +describe the same context. The test provider makes them differ: a +context created before it is loaded lacks the two slots it adds, and one +created afterwards has them. + +Three routes an application can switch context from are covered, and +they do not behave alike. + +test_sigalg_ctx_switch() switches from the servername and client_hello +callbacks, both of which run before tls1_set_server_sigalgs() allocates +valid_flags. A stale slot count undersized that allocation, and +tls1_process_sigalgs() then read and wrote past its end. The two +callbacks run at different points - the client_hello callback before the +ClientHello extensions are parsed at all, the servername callback from +the final pass over them - so they are checked separately. The extra +provider sigalgs are TLSv1.3 only, so only those cases reached past the +end of the buffer; the TLSv1.2 cases pin the invariant. + +test_sigalg_ctx_switch_cert_cb() switches from the certificate callback, +which runs after valid_flags has been sized and filled in. Nothing +recomputes the shared sigalgs later in the same handshake, so nothing +read out of bounds on that route, but the state left behind was +inconsistent. It also checks the built-in slots survive the switch, +which is the only coverage of that part of the change. + +test_sigalg_ctx_switch_reneg() switches during a TLSv1.2 renegotiation +with a changed servername. valid_flags is allocated by the initial +handshake and not freed in between, so this is the case where the buffer +has to be replaced rather than merely sized correctly later. + +Each test checks the premise it depends on rather than risk becoming +vacuous: that the replacement context really does have more slots, that +the renegotiation really was a full handshake, and that the flags being +compared across the switch were non-zero to begin with. + +Assisted-by: Claude Code:claude-opus-5[1m] +--- + test/sslapitest.c | 416 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 416 insertions(+) + +diff --git a/test/sslapitest.c b/test/sslapitest.c +index 0cdeb473cdfb..b068fa2eeb37 100644 +--- a/test/sslapitest.c ++++ b/test/sslapitest.c +@@ -10820,6 +10820,415 @@ static int test_pluggable_signature(int idx) + } + #endif + ++#if !defined(OPENSSL_NO_TLS1_2) || !defined(OSSL_NO_USABLE_TLS1_3) ++/* ++ * Regression tests for CVE-2026-72897: SSL_set_SSL_CTX() must refresh the ++ * connection's signature algorithm state to describe the context it installs. ++ * ++ * |ssl_pkey_num| sizes |s3.tmp.valid_flags| and bounds the loops over ++ * |cert->pkeys|, while a provider sigalg's |sig_idx| is its position in the ++ * list of whichever context a peer codepoint is resolved against, so the two ++ * must describe the same one. The test provider is what makes them differ: a ++ * context created before it is loaded lacks the two slots it adds. ++ */ ++ ++struct sigalg_ctx_switch_st { ++ SSL_CTX *newctx; ++ int switches; /* Number of times the callback switched context */ ++ int err; /* Set if SSL_set_SSL_CTX() itself failed */ ++ uint32_t preflags; /* valid_flags[SSL_PKEY_RSA] before the switch */ ++ uint32_t postflags; /* ...and after it */ ++}; ++ ++static int sigalg_switch_ctx(SSL *s, struct sigalg_ctx_switch_st *data) ++{ ++ SSL_CONNECTION *sc = SSL_CONNECTION_FROM_SSL_ONLY(s); ++ ++ if (sc == NULL) ++ return 0; ++ ++ if (sc->s3.tmp.valid_flags != NULL) ++ data->preflags = sc->s3.tmp.valid_flags[SSL_PKEY_RSA]; ++ if (SSL_set_SSL_CTX(s, data->newctx) == NULL) ++ return 0; ++ if (sc->s3.tmp.valid_flags != NULL) ++ data->postflags = sc->s3.tmp.valid_flags[SSL_PKEY_RSA]; ++ ++ data->switches++; ++ return 1; ++} ++ ++static int sigalg_switch_sni_cb(SSL *s, int *al, void *arg) ++{ ++ struct sigalg_ctx_switch_st *data = arg; ++ ++ if (SSL_get_SSL_CTX(s) != data->newctx && !sigalg_switch_ctx(s, data)) { ++ data->err = 1; ++ *al = SSL_AD_INTERNAL_ERROR; ++ return SSL_TLSEXT_ERR_ALERT_FATAL; ++ } ++ return SSL_TLSEXT_ERR_OK; ++} ++ ++static int sigalg_switch_clienthello_cb(SSL *s, int *al, void *arg) ++{ ++ struct sigalg_ctx_switch_st *data = arg; ++ ++ if (SSL_get_SSL_CTX(s) != data->newctx && !sigalg_switch_ctx(s, data)) { ++ data->err = 1; ++ *al = SSL_AD_INTERNAL_ERROR; ++ return SSL_CLIENT_HELLO_ERROR; ++ } ++ return SSL_CLIENT_HELLO_SUCCESS; ++} ++ ++static int sigalg_switch_cert_cb(SSL *s, void *arg) ++{ ++ struct sigalg_ctx_switch_st *data = arg; ++ ++ if (SSL_get_SSL_CTX(s) != data->newctx && !sigalg_switch_ctx(s, data)) { ++ data->err = 1; ++ return 0; ++ } ++ return 1; ++} ++ ++/* ++ * Build the three contexts these tests need. An SSL_CTX takes its snapshot of ++ * the provider sigalgs when it is created, so |*sctx| is created while the test ++ * provider is not loaded and does not have its sigalgs, and |*newsctx| and ++ * |*cctx| are created after it is loaded and do have them. The client needs ++ * them too, so that it actually offers the codepoints which resolve to the ++ * slots the original context does not have. ++ */ ++static int sigalg_ctx_switch_setup(OSSL_PROVIDER **tlsprov, int version, ++ SSL_CTX **sctx, SSL_CTX **newsctx, SSL_CTX **cctx) ++{ ++ if (!TEST_true(create_ssl_ctx_pair(libctx, TLS_server_method(), NULL, ++ version, version, sctx, NULL, cert, privkey))) ++ return 0; ++ ++ if (!TEST_ptr(*tlsprov = OSSL_PROVIDER_load(libctx, "tls-provider"))) ++ return 0; ++ ++ if (!TEST_true(create_ssl_ctx_pair(libctx, TLS_server_method(), ++ TLS_client_method(), version, version, newsctx, cctx, cert, ++ privkey))) ++ return 0; ++ ++ /* ++ * The premise of the test is that the replacement context describes more ++ * signature algorithm slots than the original. Check that, rather than let ++ * the test quietly become vacuous if that ever stops being true. ++ */ ++ if (!TEST_size_t_lt((*sctx)->cert->ssl_pkey_num, ++ (*newsctx)->cert->ssl_pkey_num)) ++ return 0; ++ ++ return 1; ++} ++ ++/* ++ * |ssl_pkey_num| bounds the loops which index |cert->pkeys| and sizes ++ * |valid_flags|, so it must describe the CERT the connection is actually ++ * using, not the one it was created from. ++ */ ++static int sigalg_ctx_switch_check(SSL *serverssl) ++{ ++ SSL_CONNECTION *sc; ++ ++ if (!TEST_ptr(sc = SSL_CONNECTION_FROM_SSL_ONLY(serverssl))) ++ return 0; ++ ++ return TEST_size_t_eq(sc->ssl_pkey_num, sc->cert->ssl_pkey_num); ++} ++ ++/* ++ * Switch SSL_CTX from a callback which runs *before* tls1_set_server_sigalgs() ++ * allocates |valid_flags|. This is the route CVE-2026-72897 describes: the ++ * stale |ssl_pkey_num| undersizes that allocation, and tls1_process_sigalgs() ++ * reads and writes past its end for each peer codepoint occupying one of the ++ * replacement context's excess slots. The peer chooses how many such accesses ++ * happen, and where, by choosing which codepoints to offer. ++ * ++ * Both callbacks tested here run before that allocation, but not at the same ++ * point: the client_hello callback runs before the ClientHello extensions have ++ * been parsed at all, whereas the servername callback runs from the "final" ++ * pass over them, after the sigalgs extension has been saved and after session ++ * resumption has been decided. They are therefore checked separately. ++ * ++ * The two slots the test provider adds are TLSv1.3 only, so only the TLSv1.3 ++ * cases can index past the end of the buffer; the TLSv1.2 cases pin the ++ * invariant for a version which processes signature algorithms identically. ++ * ++ * Test 0: servername callback, TLSv1.3 ++ * Test 1: client_hello callback, TLSv1.3 ++ * Test 2: servername callback, TLSv1.2 ++ * Test 3: client_hello callback, TLSv1.2 ++ */ ++static int test_sigalg_ctx_switch(int idx) ++{ ++ OSSL_PROVIDER *tlsprov = NULL; ++ SSL_CTX *sctx = NULL, *newsctx = NULL, *cctx = NULL; ++ SSL *serverssl = NULL, *clientssl = NULL; ++ struct sigalg_ctx_switch_st cbdata; ++ int tls13 = idx < 2; ++ int ver, testresult = 0; ++ ++#ifdef OSSL_NO_USABLE_TLS1_3 ++ if (tls13) ++ return TEST_skip("TLSv1.3 is not available"); ++#endif ++#ifdef OPENSSL_NO_TLS1_2 ++ if (!tls13) ++ return TEST_skip("TLSv1.2 is not available"); ++#endif ++ ver = tls13 ? TLS1_3_VERSION : TLS1_2_VERSION; ++ ++ memset(&cbdata, 0, sizeof(cbdata)); ++ ++ if (!sigalg_ctx_switch_setup(&tlsprov, ver, &sctx, &newsctx, &cctx)) ++ goto end; ++ ++ cbdata.newctx = newsctx; ++ ++ if ((idx & 1) == 0) { ++ if (!TEST_true(SSL_CTX_set_tlsext_servername_callback(sctx, ++ sigalg_switch_sni_cb)) ++ || !TEST_true(SSL_CTX_set_tlsext_servername_arg(sctx, &cbdata))) ++ goto end; ++ } else { ++ SSL_CTX_set_client_hello_cb(sctx, sigalg_switch_clienthello_cb, ++ &cbdata); ++ } ++ ++ if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl, ++ NULL, NULL))) ++ goto end; ++ ++ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "server.example"))) ++ goto end; ++ ++ if (!TEST_true(create_ssl_connection(serverssl, clientssl, SSL_ERROR_NONE))) ++ goto end; ++ ++ if (!TEST_int_eq(cbdata.err, 0) ++ || !TEST_int_eq(cbdata.switches, 1) ++ || !sigalg_ctx_switch_check(serverssl)) ++ goto end; ++ ++ testresult = 1; ++ ++end: ++ SSL_free(serverssl); ++ SSL_free(clientssl); ++ SSL_CTX_free(sctx); ++ SSL_CTX_free(newsctx); ++ SSL_CTX_free(cctx); ++ OSSL_PROVIDER_unload(tlsprov); ++ ++ return testresult; ++} ++ ++/* ++ * Switch SSL_CTX from the certificate callback. Nothing prevents an ++ * application doing this, so it has to keep working, and unlike the ++ * servername and client_hello callbacks it runs *after* ++ * tls1_set_server_sigalgs() has sized and filled in |valid_flags| for the ++ * original context. Nothing recomputes the shared signature algorithms later ++ * in the same handshake, so what this checks is that the switch leaves both ++ * the slot count and the flags already derived from the peer consistent with ++ * the context now installed. ++ * ++ * Test 0: TLSv1.3 ++ * Test 1: TLSv1.2 ++ */ ++static int test_sigalg_ctx_switch_cert_cb(int idx) ++{ ++ OSSL_PROVIDER *tlsprov = NULL; ++ SSL_CTX *sctx = NULL, *newsctx = NULL, *cctx = NULL; ++ SSL *serverssl = NULL, *clientssl = NULL; ++ struct sigalg_ctx_switch_st cbdata; ++ int tls13 = idx == 0; ++ int ver, testresult = 0; ++ ++#ifdef OSSL_NO_USABLE_TLS1_3 ++ if (tls13) ++ return TEST_skip("TLSv1.3 is not available"); ++#endif ++#ifdef OPENSSL_NO_TLS1_2 ++ if (!tls13) ++ return TEST_skip("TLSv1.2 is not available"); ++#endif ++ ver = tls13 ? TLS1_3_VERSION : TLS1_2_VERSION; ++ ++ memset(&cbdata, 0, sizeof(cbdata)); ++ ++ if (!sigalg_ctx_switch_setup(&tlsprov, ver, &sctx, &newsctx, &cctx)) ++ goto end; ++ ++ cbdata.newctx = newsctx; ++ SSL_CTX_set_cert_cb(sctx, sigalg_switch_cert_cb, &cbdata); ++ ++ if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl, ++ NULL, NULL)) ++ || !TEST_true(create_ssl_connection(serverssl, clientssl, ++ SSL_ERROR_NONE))) ++ goto end; ++ ++ if (!TEST_int_eq(cbdata.err, 0) ++ || !TEST_int_eq(cbdata.switches, 1) ++ || !sigalg_ctx_switch_check(serverssl)) ++ goto end; ++ ++ /* ++ * The switch happened after |valid_flags| had been populated, so the ++ * built-in slots must have survived it. Nothing recomputes them after a ++ * context switch, and at TLSv1.2 and above ssl_set_masks() needs them to ++ * enable ECDSA, Ed25519 and Ed448. ++ */ ++ if (!TEST_uint_ne(cbdata.preflags, 0) ++ || !TEST_uint_eq(cbdata.postflags, cbdata.preflags)) ++ goto end; ++ ++ testresult = 1; ++ ++end: ++ SSL_free(serverssl); ++ SSL_free(clientssl); ++ SSL_CTX_free(sctx); ++ SSL_CTX_free(newsctx); ++ SSL_CTX_free(cctx); ++ OSSL_PROVIDER_unload(tlsprov); ++ ++ return testresult; ++} ++ ++#ifndef OPENSSL_NO_TLS1_2 ++#define SIGALG_SWITCH_SNI1 "first.example" ++#define SIGALG_SWITCH_SNI2 "second.example" ++ ++static int sigalg_switch_reneg_sni_cb(SSL *s, int *al, void *arg) ++{ ++ const char *name = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name); ++ ++ /* Model an application which picks its SSL_CTX from the name offered */ ++ if (name == NULL || strcmp(name, SIGALG_SWITCH_SNI2) != 0) ++ return SSL_TLSEXT_ERR_OK; ++ ++ return sigalg_switch_sni_cb(s, al, arg); ++} ++ ++/* ++ * Switch SSL_CTX from the servername callback during a *renegotiation* ++ * handshake, the client having offered a different name the second time round. ++ * |valid_flags| is allocated during the initial handshake and is not freed in ++ * between, so this is the case where SSL_set_SSL_CTX() has to replace an ++ * existing buffer rather than merely correct the count a later allocation will ++ * use. Renegotiation is TLSv1.2 and below only. ++ * ++ * The slot count is the only signal here: the two slots the test provider adds ++ * are TLSv1.3 only, so nothing indexes past the end of the reused buffer. ++ * Under a sanitiser this also catches replacing that buffer wrongly - ++ * refreshing |ssl_pkey_num| while keeping the old allocation would make the ++ * memset() in tls1_set_server_sigalgs() overrun it on the renegotiation. ++ */ ++static int test_sigalg_ctx_switch_reneg(void) ++{ ++ OSSL_PROVIDER *tlsprov = NULL; ++ SSL_CTX *sctx = NULL, *newsctx = NULL, *cctx = NULL; ++ SSL *serverssl = NULL, *clientssl = NULL; ++ SSL_CONNECTION *sc; ++ struct sigalg_ctx_switch_st cbdata; ++ size_t readbytes; ++ char buf[80]; ++ int i, testresult = 0; ++ ++ memset(&cbdata, 0, sizeof(cbdata)); ++ ++ if (!sigalg_ctx_switch_setup(&tlsprov, TLS1_2_VERSION, &sctx, &newsctx, ++ &cctx)) ++ goto end; ++ ++ cbdata.newctx = newsctx; ++ ++ if (!TEST_true(SSL_CTX_set_tlsext_servername_callback(sctx, ++ sigalg_switch_reneg_sni_cb)) ++ || !TEST_true(SSL_CTX_set_tlsext_servername_arg(sctx, &cbdata))) ++ goto end; ++ ++ if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl, ++ NULL, NULL))) ++ goto end; ++ ++ /* ++ * The first handshake must not switch, so that |valid_flags| is allocated ++ * and populated for the original context. ++ */ ++ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, SIGALG_SWITCH_SNI1)) ++ || !TEST_true(create_ssl_connection(serverssl, clientssl, ++ SSL_ERROR_NONE)) ++ || !TEST_int_eq(cbdata.switches, 0)) ++ goto end; ++ ++ if (!TEST_ptr(sc = SSL_CONNECTION_FROM_SSL_ONLY(serverssl)) ++ || !TEST_ptr(sc->s3.tmp.valid_flags)) ++ goto end; ++ ++ /* Now renegotiate, offering the name which does select the other context */ ++ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, SIGALG_SWITCH_SNI2)) ++ || !TEST_true(SSL_renegotiate(clientssl)) ++ || !TEST_true(SSL_renegotiate_pending(clientssl))) ++ goto end; ++ ++ for (i = 0; i < 3; i++) { ++ if (SSL_read_ex(clientssl, buf, sizeof(buf), &readbytes) > 0) { ++ if (!TEST_size_t_eq(readbytes, 0)) ++ goto end; ++ } else if (!TEST_int_eq(SSL_get_error(clientssl, 0), ++ SSL_ERROR_WANT_READ)) { ++ goto end; ++ } ++ if (SSL_read_ex(serverssl, buf, sizeof(buf), &readbytes) > 0) { ++ if (!TEST_size_t_eq(readbytes, 0)) ++ goto end; ++ } else if (!TEST_int_eq(SSL_get_error(serverssl, 0), ++ SSL_ERROR_WANT_READ)) { ++ goto end; ++ } ++ } ++ ++ /* ++ * A resumed renegotiation would skip tls1_set_server_sigalgs() altogether, ++ * so check the renegotiation really was a full handshake. ++ */ ++ if (!TEST_false(SSL_renegotiate_pending(clientssl)) ++ || !TEST_false(SSL_session_reused(serverssl)) ++ || !TEST_int_eq(cbdata.err, 0) ++ || !TEST_int_eq(cbdata.switches, 1) ++ || !sigalg_ctx_switch_check(serverssl)) ++ goto end; ++ ++ /* The built-in slots must survive the buffer being replaced */ ++ if (!TEST_uint_ne(cbdata.preflags, 0) ++ || !TEST_uint_eq(cbdata.postflags, cbdata.preflags)) ++ goto end; ++ ++ testresult = 1; ++ ++end: ++ SSL_free(serverssl); ++ SSL_free(clientssl); ++ SSL_CTX_free(sctx); ++ SSL_CTX_free(newsctx); ++ SSL_CTX_free(cctx); ++ OSSL_PROVIDER_unload(tlsprov); ++ ++ return testresult; ++} ++#endif /* OPENSSL_NO_TLS1_2 */ ++#endif ++ + #ifndef OPENSSL_NO_TLS1_2 + static int test_ssl_dup(void) + { +@@ -14288,6 +14697,13 @@ int setup_tests(void) + ADD_ALL_TESTS(test_pluggable_group, 2); + ADD_ALL_TESTS(test_pluggable_signature, 6); + #endif ++#if !defined(OPENSSL_NO_TLS1_2) || !defined(OSSL_NO_USABLE_TLS1_3) ++ ADD_ALL_TESTS(test_sigalg_ctx_switch, 4); ++ ADD_ALL_TESTS(test_sigalg_ctx_switch_cert_cb, 2); ++#ifndef OPENSSL_NO_TLS1_2 ++ ADD_TEST(test_sigalg_ctx_switch_reneg); ++#endif ++#endif + #ifndef OPENSSL_NO_TLS1_2 + ADD_TEST(test_ssl_dup); + ADD_ALL_TESTS(test_session_secret_cb, 2); diff -Nru openssl-3.5.7/debian/patches/Add-test-for-CVE-2026-75805.patch openssl-3.5.7/debian/patches/Add-test-for-CVE-2026-75805.patch --- openssl-3.5.7/debian/patches/Add-test-for-CVE-2026-75805.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/Add-test-for-CVE-2026-75805.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,127 @@ +From: Norbert Pocs +Date: Thu, 27 Aug 2026 13:59:30 +0200 +Subject: Add test for CVE-2026-75805 + +Signed-off-by: Norbert Pocs +--- + test/cmp_client_test.c | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 93 insertions(+) + +diff --git a/test/cmp_client_test.c b/test/cmp_client_test.c +index c2072c1be25c..b64a3fa63fe9 100644 +--- a/test/cmp_client_test.c ++++ b/test/cmp_client_test.c +@@ -10,6 +10,7 @@ + */ + + #include "helpers/cmp_testlib.h" ++#include "../crypto/crmf/crmf_local.h" /* for manipulating the CertId issuer */ + + #include "cmp_mock_srv.h" + +@@ -203,6 +204,97 @@ static int test_exec_RR_ses_receive_error(void) + return result; + } + ++/* ++ * Create a CertId the issuer of which is not a directoryName, such that ++ * OSSL_CRMF_CERTID_get0_issuer() yields NULL for it, while ++ * OSSL_CRMF_CERTID_get0_serialNumber() yields the serial number as usual. ++ */ ++static OSSL_CRMF_CERTID *certid_new_non_dirName_issuer(void) ++{ ++ OSSL_CRMF_CERTID *cid = OSSL_CRMF_CERTID_new(); ++ ASN1_IA5STRING *dns = ASN1_IA5STRING_new(); ++ ++ if (cid == NULL || dns == NULL) ++ goto err; ++ if (!ASN1_STRING_set(dns, "server.example", -1)) ++ goto err; ++ GENERAL_NAME_set0_value(cid->issuer, GEN_DNS, dns); ++ dns = NULL; /* ownership transferred to cid->issuer */ ++ if (!ASN1_INTEGER_set(cid->serialNumber, 1)) ++ goto err; ++ return cid; ++ ++err: ++ ASN1_IA5STRING_free(dns); ++ OSSL_CRMF_CERTID_free(cid); ++ return NULL; ++} ++ ++/* ++ * Transfer callback wrapping the mock server: add a CertId to the revCerts ++ * field of the RP, which the server side omits for an RR request derived from ++ * a PKCS#10 CSR because such a request contains no issuer and serial number. ++ */ ++static OSSL_CMP_MSG *transfer_add_revCerts(OSSL_CMP_CTX *ctx, ++ const OSSL_CMP_MSG *req) ++{ ++ OSSL_CMP_SRV_CTX *srv_ctx = OSSL_CMP_CTX_get_transfer_cb_arg(ctx); ++ OSSL_CMP_MSG *rp = ossl_cmp_mock_server_perform(ctx, req); ++ OSSL_CRMF_CERTID *cid; ++ ++ if (rp == NULL || OSSL_CMP_MSG_get_bodytype(rp) != OSSL_CMP_PKIBODY_RP) ++ return rp; ++ ++ if ((cid = certid_new_non_dirName_issuer()) == NULL) ++ goto err; ++ if (!sk_OSSL_CRMF_CERTID_push(rp->body->value.rp->revCerts, cid)) { ++ OSSL_CRMF_CERTID_free(cid); ++ goto err; ++ } ++ /* the body has been modified after the server protected the message */ ++ if (!ossl_cmp_msg_protect(OSSL_CMP_SRV_CTX_get0_cmp_ctx(srv_ctx), rp)) ++ goto err; ++ return rp; ++ ++err: ++ OSSL_CMP_MSG_free(rp); ++ return NULL; ++} ++ ++/* ++ * The certificate to be revoked is given by a PKCS#10 CSR, so the RR contains ++ * neither issuer nor serial number, yet the RP contains a CertId in revCerts. ++ * The client cannot compare the CertId with what it did not send and thus ++ * must not reject the response. ++ * The CertId issuer is not a directoryName, such that the issuer comparison ++ * compares NULL with NULL and succeeds, which makes the client go on ++ * comparing the serial numbers with the one it did not send being NULL. ++ */ ++static int test_exec_RR_ses_p10CSR_revCerts(void) ++{ ++ OSSL_CMP_CTX *ctx; ++ X509_REQ *csr = NULL; ++ ++ SETUP_TEST_FIXTURE(CMP_SES_TEST_FIXTURE, set_up); ++ ctx = fixture->cmp_ctx; ++ fixture->expected = OSSL_CMP_PKISTATUS_accepted; ++ if (!TEST_ptr(csr = load_csr_der(pkcs10_f, libctx)) ++ /* drop the reference cert such that the CSR is used instead */ ++ || !TEST_true(OSSL_CMP_CTX_set1_oldCert(ctx, NULL)) ++ || !TEST_true(OSSL_CMP_CTX_set1_p10CSR(ctx, csr)) ++ /* no recipient can be derived from just a CSR */ ++ || !TEST_true(OSSL_CMP_CTX_set1_recipient(ctx, ++ X509_get_subject_name(server_cert))) ++ || !TEST_true(OSSL_CMP_CTX_set_transfer_cb(ctx, ++ transfer_add_revCerts))) { ++ tear_down(fixture); ++ fixture = NULL; ++ } ++ X509_REQ_free(csr); ++ EXECUTE_TEST(execute_exec_RR_ses_test, tear_down); ++ return result; ++} ++ + static int test_exec_IR_ses(void) + { + SETUP_TEST_FIXTURE(CMP_SES_TEST_FIXTURE, set_up); +@@ -586,6 +678,7 @@ int setup_tests(void) + ADD_TEST(test_exec_RR_ses_ok); + ADD_TEST(test_exec_RR_ses_request_error); + ADD_TEST(test_exec_RR_ses_receive_error); ++ ADD_TEST(test_exec_RR_ses_p10CSR_revCerts); + ADD_TEST(test_exec_CR_ses_explicit_confirm); + ADD_TEST(test_exec_CR_ses_implicit_confirm); + ADD_TEST(test_exec_IR_ses); diff -Nru openssl-3.5.7/debian/patches/CVE-2026-75804-QUIC-connection-level-flow-control-not-enf.patch openssl-3.5.7/debian/patches/CVE-2026-75804-QUIC-connection-level-flow-control-not-enf.patch --- openssl-3.5.7/debian/patches/CVE-2026-75804-QUIC-connection-level-flow-control-not-enf.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/CVE-2026-75804-QUIC-connection-level-flow-control-not-enf.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,37 @@ +From: Alexandr Nedvedicky +Date: Thu, 3 Sep 2026 14:22:40 +0200 +Subject: CVE-2026-75804 QUIC connection-level flow control not enforced, + remote memory exhaustion + +Function ossl_quic_rxfc_get_error() must also report flow control violation +error for connection level not just for stream level. Ignoring connection +level error prevents QUIC stack to enforce flow control. + +Fixes CVE-2026-75804 +--- + ssl/quic/quic_fc.c | 11 ++++++++++- + 1 file changed, 10 insertions(+), 1 deletion(-) + +diff --git a/ssl/quic/quic_fc.c b/ssl/quic/quic_fc.c +index 1691d4d69efa..bda6e5b5db57 100644 +--- a/ssl/quic/quic_fc.c ++++ b/ssl/quic/quic_fc.c +@@ -393,8 +393,17 @@ int ossl_quic_rxfc_get_error(QUIC_RXFC *rxfc, int clear) + { + int r = rxfc->error_code; + +- if (clear) ++ if (r == OSSL_QUIC_ERR_NO_ERROR && rxfc->parent != NULL) ++ r = rxfc->parent->error_code; ++ ++ /* ++ * The clear argument is used for testing only. ++ */ ++ if (clear) { + rxfc->error_code = 0; ++ if (rxfc->parent != NULL) ++ rxfc->parent->error_code = 0; ++ } + + return r; + } diff -Nru openssl-3.5.7/debian/patches/CVE-2026-84784-QUIC-unbounded-RETIRE_CONNECTION_ID-backlo-1.patch openssl-3.5.7/debian/patches/CVE-2026-84784-QUIC-unbounded-RETIRE_CONNECTION_ID-backlo-1.patch --- openssl-3.5.7/debian/patches/CVE-2026-84784-QUIC-unbounded-RETIRE_CONNECTION_ID-backlo-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/CVE-2026-84784-QUIC-unbounded-RETIRE_CONNECTION_ID-backlo-1.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,322 @@ +From: Alexandr Nedvedicky +Date: Sat, 5 Sep 2026 22:42:40 +0200 +Subject: CVE-2026-84784 QUIC: unbounded RETIRE_CONNECTION_ID backlog (memory + DoS) + +connection must fail when remote peer attempts to retire more than +10 connection IDs. + +The point is to send more than 10 NEW_CONNECTION_ID frames. Each +frame is retiring earlier connection id. +--- + test/radix/quic_ops.c | 204 ++++++++++++++++++++++++++++++++++++++++++++++++ + test/radix/quic_tests.c | 72 +++++++++++++++++ + 2 files changed, 276 insertions(+) + +diff --git a/test/radix/quic_ops.c b/test/radix/quic_ops.c +index 4475f9180323..0f9794ac1c87 100644 +--- a/test/radix/quic_ops.c ++++ b/test/radix/quic_ops.c +@@ -1031,6 +1031,200 @@ DEF_FUNC(hf_sleep) + return ok; + } + ++/* ++ * Fault injection: intercepts a QUIC channel's outgoing packet in plaintext, ++ * before it is encrypted, so a script can tamper with its frames. ++ */ ++typedef struct radix_fault_st RADIX_FAULT; ++ ++typedef int (*radix_fault_plain_cb)(RADIX_FAULT *fault, QUIC_PKT_HDR *hdr, ++ unsigned char *buf, size_t len); ++ ++static ossl_inline void *radix_fault_plain_cb_to_ptr(radix_fault_plain_cb cb) ++{ ++ union { ++ radix_fault_plain_cb cb; ++ void *ptr; ++ } u; ++ ++ u.cb = cb; ++ return u.ptr; ++} ++ ++static ossl_inline radix_fault_plain_cb radix_fault_ptr_to_plain_cb(void *ptr) ++{ ++ union { ++ radix_fault_plain_cb cb; ++ void *ptr; ++ } u; ++ ++ u.ptr = ptr; ++ return u.cb; ++} ++ ++struct radix_fault_st { ++ QUIC_PKT_HDR hdr; ++ OSSL_QTX_IOVEC io; ++ size_t buf_alloc; ++ radix_fault_plain_cb cb; ++ QUIC_CHANNEL *ch; ++ uint64_t word0, word1; ++}; ++ ++/* Fault injection against one channel at a time. */ ++static RADIX_FAULT radix_fault; ++ ++static int radix_fault_mutate(const QUIC_PKT_HDR *hdrin, ++ const OSSL_QTX_IOVEC *iovecin, size_t numin, ++ QUIC_PKT_HDR **hdrout, ++ const OSSL_QTX_IOVEC **iovecout, ++ size_t *numout, ++ void *arg) ++{ ++ RADIX_FAULT *fault = arg; ++ size_t i, bufsz = 0; ++ unsigned char *cur; ++ int grow_allowance; ++ ++ for (i = 0; i < numin; i++) ++ bufsz += iovecin[i].buf_len; ++ ++ fault->io.buf_len = bufsz; ++ ++ /* ++ * 1200 is the length of the QUIC payload used by the record layer, bufsz is ++ * what we got from the txp, 16 is the AEAD tag length and 14 is the ++ * long header allowance (assume zero token length). ++ */ ++ grow_allowance = 1200 - (int)bufsz - 16 - 14; ++ grow_allowance -= hdrin->dst_conn_id.id_len; ++ grow_allowance -= hdrin->src_conn_id.id_len; ++ if (!TEST_int_ge(grow_allowance, 0)) ++ return 0; ++ bufsz += grow_allowance; ++ ++ OPENSSL_free((unsigned char *)fault->io.buf); ++ fault->io.buf = cur = OPENSSL_malloc(bufsz); ++ if (cur == NULL) { ++ fault->io.buf_len = 0; ++ fault->buf_alloc = 0; ++ return 0; ++ } ++ fault->buf_alloc = bufsz; ++ ++ for (i = 0; i < numin; i++) { ++ memcpy(cur, iovecin[i].buf, iovecin[i].buf_len); ++ cur += iovecin[i].buf_len; ++ } ++ ++ fault->hdr = *hdrin; ++ ++ if (fault->cb != NULL ++ && !fault->cb(fault, &fault->hdr, (unsigned char *)fault->io.buf, ++ fault->io.buf_len)) ++ return 0; ++ ++ *hdrout = &fault->hdr; ++ *iovecout = &fault->io; ++ *numout = 1; ++ ++ return 1; ++} ++ ++static void radix_fault_finish(void *arg) ++{ ++ RADIX_FAULT *fault = arg; ++ ++ OPENSSL_free((unsigned char *)fault->io.buf); ++ fault->io.buf = NULL; ++ fault->io.buf_len = 0; ++ fault->buf_alloc = 0; ++} ++ ++/* To be called from a radix_fault_plain_cb callback. */ ++static int radix_fault_resize_plain_packet(RADIX_FAULT *fault, size_t newlen) ++{ ++ unsigned char *buf; ++ size_t oldlen = fault->io.buf_len; ++ ++ if (fault->buf_alloc == 0 || newlen > fault->buf_alloc) ++ return 0; ++ ++ buf = (unsigned char *)fault->io.buf; ++ ++ if (newlen > oldlen) ++ memset(buf + oldlen, 0, newlen - oldlen); ++ ++ fault->io.buf_len = newlen; ++ fault->hdr.len = newlen; ++ ++ return 1; ++} ++ ++/* ++ * Prepend frame data into a packet. To be called from a ++ * radix_fault_plain_cb callback. ++ */ ++static int radix_fault_prepend_frame(RADIX_FAULT *fault, ++ const unsigned char *frame, size_t frame_len) ++{ ++ unsigned char *buf; ++ size_t old_len; ++ ++ if (fault->buf_alloc == 0) ++ return 0; ++ ++ /* Cast below is safe because we allocated the buffer. */ ++ buf = (unsigned char *)fault->io.buf; ++ old_len = fault->io.buf_len; ++ ++ if (!radix_fault_resize_plain_packet(fault, old_len + frame_len)) ++ return 0; ++ ++ memmove(buf + frame_len, buf, old_len); ++ memcpy(buf, frame, frame_len); ++ ++ return 1; ++} ++ ++DEF_FUNC(hf_set_inject_plain) ++{ ++ int ok = 0; ++ SSL *ssl; ++ void *cbptr; ++ QUIC_CHANNEL *ch; ++ ++ F_POP(cbptr); ++ REQUIRE_SSL(ssl); ++ ++ if (!TEST_ptr(ch = ossl_quic_conn_get_channel(ssl))) ++ goto err; ++ ++ OPENSSL_free((unsigned char *)radix_fault.io.buf); ++ memset(&radix_fault, 0, sizeof(radix_fault)); ++ radix_fault.cb = radix_fault_ptr_to_plain_cb(cbptr); ++ radix_fault.ch = ch; ++ ++ if (!TEST_true(ossl_quic_channel_set_mutator(ch, radix_fault_mutate, ++ radix_fault_finish, &radix_fault))) ++ goto err; ++ ++ ok = 1; ++err: ++ return ok; ++} ++ ++DEF_FUNC(hf_set_inject_word) ++{ ++ int ok = 0; ++ ++ F_POP2(radix_fault.word0, radix_fault.word1); ++ ++ ok = 1; ++err: ++ return ok; ++} ++ + #define OP_UNBIND(name) \ + (OP_PUSH_PZ(#name), \ + OP_FUNC(hf_unbind)) +@@ -1261,3 +1455,13 @@ DEF_FUNC(hf_sleep) + #define OP_SLEEP(ms) \ + (OP_PUSH_U64(ms), \ + OP_FUNC(hf_sleep)) ++ ++#define OP_SET_INJECT_PLAIN(name, cb) \ ++ (OP_SELECT_SSL(0, name), \ ++ OP_PUSH_P(radix_fault_plain_cb_to_ptr(cb)), \ ++ OP_FUNC(hf_set_inject_plain)) ++ ++#define OP_SET_INJECT_WORD(word0, word1) \ ++ (OP_PUSH_U64(word0), \ ++ OP_PUSH_U64(word1), \ ++ OP_FUNC(hf_set_inject_word)) +diff --git a/test/radix/quic_tests.c b/test/radix/quic_tests.c +index d6f0a19077a2..fdca80113c4b 100644 +--- a/test/radix/quic_tests.c ++++ b/test/radix/quic_tests.c +@@ -476,6 +476,77 @@ DEF_SCRIPT(check_pc_flood, "check path challenge flood") + OP_FUNC(check_flood_stats); + } + ++static int inject_new_cids(RADIX_FAULT *fault, QUIC_PKT_HDR *hdr, ++ unsigned char *buf, size_t len) ++{ ++ int ok = 0; ++ WPACKET wpkt; ++ unsigned char frame_buf[1000]; ++ size_t i, j, written; ++ uint64_t seq_no = 2, retire_prior_to = seq_no - 1; ++ QUIC_CONN_ID new_cid = { 0 }; ++ ++ if (hdr->type != QUIC_PKT_TYPE_1RTT) ++ return 1; ++ ++ if (!TEST_true(WPACKET_init_static_len(&wpkt, frame_buf, ++ sizeof(frame_buf), 0))) ++ return 0; ++ ++ ossl_quic_channel_get_diag_local_cid(fault->ch, &new_cid); ++ ++ for (i = 0; i < 20; i++) { ++ if (!TEST_true(WPACKET_quic_write_vlint(&wpkt, OSSL_QUIC_FRAME_TYPE_NEW_CONN_ID)) ++ || !TEST_true(WPACKET_quic_write_vlint(&wpkt, seq_no)) /* seq no */ ++ || !TEST_true(WPACKET_quic_write_vlint(&wpkt, retire_prior_to)) /* retire prior to */ ++ || !TEST_true(WPACKET_put_bytes_u8(&wpkt, new_cid.id_len))) /* len */ ++ goto err; ++ seq_no++; ++ retire_prior_to++; ++ ++ for (j = 0; j < new_cid.id_len && i < OSSL_NELEM(new_cid.id); ++j) ++ if (!TEST_true(WPACKET_put_bytes_u8(&wpkt, new_cid.id[i]))) ++ goto err; ++ ++ for (; j < new_cid.id_len; ++j) ++ if (!TEST_true(WPACKET_put_bytes_u8(&wpkt, 0x55))) ++ goto err; ++ ++ for (j = 0; j < QUIC_STATELESS_RESET_TOKEN_LEN; ++j) ++ if (!TEST_true(WPACKET_put_bytes_u8(&wpkt, 0x42))) ++ goto err; ++ } ++ ++ if (!TEST_true(WPACKET_get_total_written(&wpkt, &written)) ++ || !radix_fault_prepend_frame(fault, frame_buf, written)) ++ goto err; ++ ++ ok = 1; ++err: ++ if (ok) ++ WPACKET_finish(&wpkt); ++ else ++ WPACKET_cleanup(&wpkt); ++ return ok; ++} ++ ++DEF_SCRIPT(new_connid, "verify remote peer does not send excessive amount of NEW_CONNID frames") ++{ ++ OP_SIMPLE_PAIR_CONN(); ++ OP_WRITE_B(C, "apple"); ++ OP_ACCEPT_CONN_WAIT(L, S, 0); ++ OP_SET_INCOMING_STREAM_POLICY(C, SSL_INCOMING_STREAM_POLICY_ACCEPT, 42 /* error code */); ++ OP_SET_INCOMING_STREAM_POLICY(S, SSL_INCOMING_STREAM_POLICY_ACCEPT, 42 /* error code */); ++ OP_READ_EXPECT_B(S, "apple"); ++ ++ OP_WRITE_B(S, "orange"); ++ OP_READ_EXPECT_B(C, "orange"); ++ ++ OP_SET_INJECT_PLAIN(S, inject_new_cids); ++ ++ OP_WRITE_B(S, "banana"); ++ OP_EXPECT_CONN_CLOSE_INFO(C, OSSL_QUIC_ERR_CONNECTION_ID_LIMIT_ERROR, 0, 0); ++} + /* + * List of Test Scripts + * ============================================================================ +@@ -486,4 +557,5 @@ static SCRIPT_INFO *const scripts[] = { + USE(ssl_poll), + USE(check_cwm), + USE(check_pc_flood), ++ USE(new_connid), + }; diff -Nru openssl-3.5.7/debian/patches/CVE-2026-84784-QUIC-unbounded-RETIRE_CONNECTION_ID-backlo.patch openssl-3.5.7/debian/patches/CVE-2026-84784-QUIC-unbounded-RETIRE_CONNECTION_ID-backlo.patch --- openssl-3.5.7/debian/patches/CVE-2026-84784-QUIC-unbounded-RETIRE_CONNECTION_ID-backlo.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/CVE-2026-84784-QUIC-unbounded-RETIRE_CONNECTION_ID-backlo.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,108 @@ +From: Alexandr Nedvedicky +Date: Sat, 5 Sep 2026 18:11:05 +0200 +Subject: CVE-2026-84784 QUIC: unbounded RETIRE_CONNECTION_ID backlog (memory + DoS) + +Currently the cur_retire_prior_to counter is updated when RETIRE_CONNECTION_ID +is dispatched to control frame queue. However to follow protocol state +at both connection ends the counter must be updated after seeing an ACK +from remote peer indicating it's received RETIRE_CONNECTION_ID frame. + +Diverging from protocol spec here allows malicious remote peer to queue +excessive amount of RETIRE_CONNECTION_ID frames to local control +frame queue (CFQ). +--- + ssl/quic/quic_channel.c | 33 +++++++++++++++++++++++++++------ + 1 file changed, 27 insertions(+), 6 deletions(-) + +diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c +index 9534cbcaf7f9..c63272b511fa 100644 +--- a/ssl/quic/quic_channel.c ++++ b/ssl/quic/quic_channel.c +@@ -101,6 +101,11 @@ static void ch_record_state_transition(QUIC_CHANNEL *ch, uint32_t new_state); + + DEFINE_LHASH_OF_EX(QUIC_SRT_ELEM); + ++typedef struct cfq_data_retire_cid { ++ uint64_t rtcid_seq; ++ QUIC_CHANNEL *rtcid_ch; ++} CFQ_DATA_RETIRE_CID_T; ++ + QUIC_NEEDS_LOCK + static QLOG *ch_get_qlog(QUIC_CHANNEL *ch) + { +@@ -3230,19 +3235,33 @@ void ossl_quic_channel_on_remote_conn_close(QUIC_CHANNEL *ch, + ch_start_terminating(ch, &tcause, 0); + } + +-static void free_frame_data(unsigned char *buf, size_t buf_len, void *arg) ++static void free_frame_rtcid(unsigned char *buf, size_t buf_len, void *arg) + { ++ CFQ_DATA_RETIRE_CID_T *cfq_data_rtcid = (CFQ_DATA_RETIRE_CID_T *)arg; ++ QUIC_CHANNEL *ch = cfq_data_rtcid->rtcid_ch; ++ ++ if (ch->cur_retire_prior_to < cfq_data_rtcid->rtcid_seq) ++ ch->cur_retire_prior_to = cfq_data_rtcid->rtcid_seq; ++ + OPENSSL_free(buf); ++ OPENSSL_free(cfq_data_rtcid); + } + + static int ch_enqueue_retire_conn_id(QUIC_CHANNEL *ch, uint64_t seq_num) + { ++ CFQ_DATA_RETIRE_CID_T *cfq_data_rtcid = NULL; + BUF_MEM *buf_mem = NULL; + WPACKET wpkt; + size_t l; + + ossl_quic_srtm_remove(ch->srtm, ch, seq_num); + ++ cfq_data_rtcid = OPENSSL_malloc(sizeof(CFQ_DATA_RETIRE_CID_T)); ++ if (cfq_data_rtcid == NULL) ++ goto err; ++ cfq_data_rtcid->rtcid_seq = seq_num; ++ cfq_data_rtcid->rtcid_ch = ch; ++ + if ((buf_mem = BUF_MEM_new()) == NULL) + goto err; + +@@ -3261,7 +3280,7 @@ static int ch_enqueue_retire_conn_id(QUIC_CHANNEL *ch, uint64_t seq_num) + if (ossl_quic_cfq_add_frame(ch->cfq, 1, QUIC_PN_SPACE_APP, + OSSL_QUIC_FRAME_TYPE_RETIRE_CONN_ID, 0, + (unsigned char *)buf_mem->data, l, +- free_frame_data, NULL) ++ free_frame_rtcid, cfq_data_rtcid) + == NULL) + goto err; + +@@ -3275,6 +3294,7 @@ static int ch_enqueue_retire_conn_id(QUIC_CHANNEL *ch, uint64_t seq_num) + OSSL_QUIC_FRAME_TYPE_NEW_CONN_ID, + "internal error enqueueing retire conn id"); + BUF_MEM_free(buf_mem); ++ OPENSSL_free(cfq_data_rtcid); + return 0; + } + +@@ -3283,6 +3303,7 @@ void ossl_quic_channel_on_new_conn_id(QUIC_CHANNEL *ch, + { + uint64_t new_remote_seq_num = ch->cur_remote_seq_num; + uint64_t new_retire_prior_to = ch->cur_retire_prior_to; ++ uint64_t retire_prior_to; + + if (!ossl_quic_channel_is_active(ch)) + return; +@@ -3381,10 +3402,10 @@ void ossl_quic_channel_on_new_conn_id(QUIC_CHANNEL *ch, + * that NEW_CONNECTION_ID frame, by definition this will always be met. + * This may change in future when we change our CID handling. + */ +- while (new_retire_prior_to > ch->cur_retire_prior_to) { +- if (!ch_enqueue_retire_conn_id(ch, ch->cur_retire_prior_to)) +- break; +- ++ch->cur_retire_prior_to; ++ retire_prior_to = ch->cur_retire_prior_to; ++ while (new_retire_prior_to > retire_prior_to) { ++ ch_enqueue_retire_conn_id(ch, retire_prior_to); ++ retire_prior_to++; + } + } + diff -Nru openssl-3.5.7/debian/patches/Defer-computation-of-relative-CRLDP-names.patch openssl-3.5.7/debian/patches/Defer-computation-of-relative-CRLDP-names.patch --- openssl-3.5.7/debian/patches/Defer-computation-of-relative-CRLDP-names.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/Defer-computation-of-relative-CRLDP-names.patch 2026-09-29 20:10:10.000000000 +0000 @@ -0,0 +1,293 @@ +From: Viktor Dukhovni +Date: Wed, 2 Sep 2026 18:53:50 +1000 +Subject: Defer computation of relative CRLDP names + +These are derived just-in-time, during any actual CRL processing. + +Fixes CVE-2026-35189 +--- + crypto/x509/v3_crld.c | 42 ++++++++++++++++---------- + crypto/x509/v3_purp.c | 45 +++++++++------------------- + crypto/x509/x509_vfy.c | 80 +++++++++++++++++++++++++++++++++++++++++++------- + include/crypto/x509.h | 3 ++ + 4 files changed, 112 insertions(+), 58 deletions(-) + +diff --git a/crypto/x509/v3_crld.c b/crypto/x509/v3_crld.c +index 56715439a4a4..0a5daae7475e 100644 +--- a/crypto/x509/v3_crld.c ++++ b/crypto/x509/v3_crld.c +@@ -522,33 +522,43 @@ static int i2r_object(const X509V3_EXT_METHOD *method, void *oid, BIO *bp, + return 1; + } + +-/* Append any nameRelativeToCRLIssuer in dpn to iname, set in dpn->dpname */ +-int DIST_POINT_set_dpname(DIST_POINT_NAME *dpn, const X509_NAME *iname) ++/* ++ * Return a new X509_NAME consisting of iname with the nameRelativeToCRLIssuer ++ * fragment of dpn appended, with its DER encoding already cached. ++ * dpn must be a relative name (type 1). Returns NULL on error. ++ */ ++X509_NAME *ossl_dist_point_name_full(const DIST_POINT_NAME *dpn, ++ const X509_NAME *iname) + { + int i; +- STACK_OF(X509_NAME_ENTRY) *frag; ++ STACK_OF(X509_NAME_ENTRY) *frag = dpn->name.relativename; + X509_NAME_ENTRY *ne; ++ X509_NAME *dpname = X509_NAME_dup(iname); + +- if (dpn == NULL || dpn->type != 1) +- return 1; +- frag = dpn->name.relativename; +- X509_NAME_free(dpn->dpname); /* just in case it was already set */ +- dpn->dpname = X509_NAME_dup(iname); +- if (dpn->dpname == NULL) +- return 0; ++ if (dpname == NULL) ++ return NULL; + for (i = 0; i < sk_X509_NAME_ENTRY_num(frag); i++) { + ne = sk_X509_NAME_ENTRY_value(frag, i); +- if (!X509_NAME_add_entry(dpn->dpname, ne, -1, i ? 0 : 1)) ++ if (!X509_NAME_add_entry(dpname, ne, -1, i ? 0 : 1)) + goto err; + } + /* generate cached encoding of name */ +- if (i2d_X509_NAME(dpn->dpname, NULL) >= 0) +- return 1; ++ if (i2d_X509_NAME(dpname, NULL) >= 0) ++ return dpname; + + err: +- X509_NAME_free(dpn->dpname); +- dpn->dpname = NULL; +- return 0; ++ X509_NAME_free(dpname); ++ return NULL; ++} ++ ++/* Append any nameRelativeToCRLIssuer in dpn to iname, set in dpn->dpname */ ++int DIST_POINT_set_dpname(DIST_POINT_NAME *dpn, const X509_NAME *iname) ++{ ++ if (dpn == NULL || dpn->type != 1) ++ return 1; ++ X509_NAME_free(dpn->dpname); /* just in case it was already set */ ++ dpn->dpname = ossl_dist_point_name_full(dpn, iname); ++ return dpn->dpname != NULL; + } + + ASN1_SEQUENCE(OSSL_AA_DIST_POINT) = { +diff --git a/crypto/x509/v3_purp.c b/crypto/x509/v3_purp.c +index 3c1bdd84a7e9..2763feb423af 100644 +--- a/crypto/x509/v3_purp.c ++++ b/crypto/x509/v3_purp.c +@@ -345,12 +345,17 @@ int X509_supported_extension(X509_EXTENSION *ex) + return 0; + } + +-/* Returns 1 on success, 0 if x is invalid, -1 on (internal) error. */ +-static int setup_dp(const X509 *x, DIST_POINT *dp) ++/* ++ * The full name of a nameRelativeToCRLIssuer distribution point is not ++ * computed here. Doing so for every parsed certificate cost an ++ * X509_NAME_dup() of the issuer name per relative distribution point, ++ * which a certificate with many such entries could turn into hundreds of ++ * megabytes of heap on a plain TLS handshake, while the result is only ++ * needed when a CRL is actually being matched against the certificate. ++ * That name is now built on demand in the CRL checking code instead. ++ */ ++static int setup_dp(DIST_POINT *dp) + { +- const X509_NAME *iname = NULL; +- int i; +- + if (dp->distpoint == NULL && sk_GENERAL_NAME_num(dp->CRLissuer) <= 0) { + ERR_raise(ERR_LIB_X509, X509_R_INVALID_DISTPOINT); + return 0; +@@ -364,30 +369,10 @@ static int setup_dp(const X509 *x, DIST_POINT *dp) + } else { + dp->dp_reasons = CRLDP_ALL_REASONS; + } +- if (dp->distpoint == NULL || dp->distpoint->type != 1) +- return 1; +- +- /* Handle name fragment given by nameRelativeToCRLIssuer */ +- /* +- * Note that the below way of determining iname is not really compliant +- * with https://tools.ietf.org/html/rfc5280#section-4.2.1.13 +- * According to it, sk_GENERAL_NAME_num(dp->CRLissuer) MUST be <= 1 +- * and any CRLissuer could be of type different to GEN_DIRNAME. +- */ +- for (i = 0; i < sk_GENERAL_NAME_num(dp->CRLissuer); i++) { +- GENERAL_NAME *gen = sk_GENERAL_NAME_value(dp->CRLissuer, i); +- +- if (gen->type == GEN_DIRNAME) { +- iname = gen->d.directoryName; +- break; +- } +- } +- if (iname == NULL) +- iname = X509_get_issuer_name(x); +- return DIST_POINT_set_dpname(dp->distpoint, iname) ? 1 : -1; ++ return 1; + } + +-/* Return 1 on success, 0 if x is invalid, -1 on (internal) error. */ ++/* Return 1 on success, 0 on error. */ + static int setup_crldp(X509 *x) + { + int i; +@@ -397,10 +382,8 @@ static int setup_crldp(X509 *x) + return 0; + + for (i = 0; i < sk_DIST_POINT_num(x->crldp); i++) { +- int res = setup_dp(x, sk_DIST_POINT_value(x->crldp, i)); +- +- if (res < 1) +- return res; ++ if (!setup_dp(sk_DIST_POINT_value(x->crldp, i))) ++ return 0; + } + return 1; + } +diff --git a/crypto/x509/x509_vfy.c b/crypto/x509/x509_vfy.c +index 0994c32ca533..26a1c964c86e 100644 +--- a/crypto/x509/x509_vfy.c ++++ b/crypto/x509/x509_vfy.c +@@ -1518,16 +1518,59 @@ static int check_crl_chain(X509_STORE_CTX *ctx, + return X509_cmp(cert_ta, crl_ta) == 0; + } + ++/* ++ * Return the full name of the certificate CRL distribution point dp, whose ++ * distpoint is a nameRelativeToCRLIssuer fragment: the CRL issuer name with ++ * the fragment appended. The CRL issuer is the directoryName in ++ * dp->CRLissuer if there is one, else the issuer of the certificate. ++ * ++ * The result is a fresh X509_NAME owned by the caller. It is deliberately ++ * not stored in dp->distpoint->dpname: once its extension cache has been ++ * published a certificate is shared between threads without locking, and ++ * computing the name here rather than when the certificate is parsed keeps ++ * a certificate with many relative distribution points from costing a copy ++ * of the issuer name per entry on every parse. Returns NULL on error. ++ */ ++static X509_NAME *crldp_full_name(const X509 *x, const DIST_POINT *dp) ++{ ++ const X509_NAME *iname = NULL; ++ int i; ++ ++ /* ++ * Note that the below way of determining iname is not really compliant ++ * with https://tools.ietf.org/html/rfc5280#section-4.2.1.13 ++ * According to it, sk_GENERAL_NAME_num(dp->CRLissuer) MUST be <= 1 ++ * and any CRLissuer could be of type different to GEN_DIRNAME. ++ */ ++ for (i = 0; i < sk_GENERAL_NAME_num(dp->CRLissuer); i++) { ++ GENERAL_NAME *gen = sk_GENERAL_NAME_value(dp->CRLissuer, i); ++ ++ if (gen->type == GEN_DIRNAME) { ++ iname = gen->d.directoryName; ++ break; ++ } ++ } ++ if (iname == NULL) ++ iname = X509_get_issuer_name(x); ++ return ossl_dist_point_name_full(dp->distpoint, iname); ++} ++ + /*- + * Check for match between two dist point names: three separate cases. + * 1. Both are relative names and compare X509_NAME types. + * 2. One full, one relative. Compare X509_NAME to GENERAL_NAMES. + * 3. Both are full names and compare two GENERAL_NAMES. + * 4. One is NULL: automatic match. ++ * ++ * a is the certificate's distribution point name and b the CRL's issuing ++ * distribution point name. When a is a relative name, aname is its full ++ * name as built by crldp_full_name(); a->dpname itself is not consulted. ++ * For b the full name is the cached b->dpname set when the CRL was parsed. + */ +-static int idp_check_dp(DIST_POINT_NAME *a, DIST_POINT_NAME *b) ++static int idp_check_dp(DIST_POINT_NAME *a, const X509_NAME *aname, ++ DIST_POINT_NAME *b) + { +- X509_NAME *nm = NULL; ++ const X509_NAME *nm = NULL; + GENERAL_NAMES *gens = NULL; + GENERAL_NAME *gena, *genb; + int i, j; +@@ -1535,16 +1578,16 @@ static int idp_check_dp(DIST_POINT_NAME *a, DIST_POINT_NAME *b) + if (a == NULL || b == NULL) + return 1; + if (a->type == 1) { +- if (a->dpname == NULL) ++ if (aname == NULL) + return 0; + /* Case 1: two X509_NAME */ + if (b->type == 1) { + if (b->dpname == NULL) + return 0; +- return X509_NAME_cmp(a->dpname, b->dpname) == 0; ++ return X509_NAME_cmp(aname, b->dpname) == 0; + } + /* Case 2: set name and GENERAL_NAMES appropriately */ +- nm = a->dpname; ++ nm = aname; + gens = b->name.fullname; + } else if (b->type == 1) { + if (b->dpname == NULL) +@@ -1617,13 +1660,28 @@ static int crl_crldp_check(X509 *x, X509_CRL *crl, int crl_score, + *preasons = crl->idp_reasons; + for (i = 0; i < sk_DIST_POINT_num(x->crldp); i++) { + DIST_POINT *dp = sk_DIST_POINT_value(x->crldp, i); ++ X509_NAME *dpname = NULL; ++ int match; + +- if (crldp_check_crlissuer(dp, crl, crl_score)) { +- if (crl->idp == NULL +- || idp_check_dp(dp->distpoint, crl->idp->distpoint)) { +- *preasons &= dp->dp_reasons; +- return 1; +- } ++ if (!crldp_check_crlissuer(dp, crl, crl_score)) ++ continue; ++ if (crl->idp == NULL) { ++ match = 1; ++ } else { ++ /* ++ * A relative distribution point name is only comparable in ++ * full form. Build it for this one comparison and discard it; ++ * if that fails the entry simply does not match. ++ */ ++ if (dp->distpoint != NULL && dp->distpoint->type == 1 ++ && (dpname = crldp_full_name(x, dp)) == NULL) ++ continue; ++ match = idp_check_dp(dp->distpoint, dpname, crl->idp->distpoint); ++ X509_NAME_free(dpname); ++ } ++ if (match) { ++ *preasons &= dp->dp_reasons; ++ return 1; + } + } + return (crl->idp == NULL || crl->idp->distpoint == NULL) +diff --git a/include/crypto/x509.h b/include/crypto/x509.h +index fa085edeb8a2..b548259b9635 100644 +--- a/include/crypto/x509.h ++++ b/include/crypto/x509.h +@@ -14,6 +14,7 @@ + #include "internal/refcount.h" + #include + #include ++#include + #include + #include "crypto/types.h" + +@@ -313,6 +314,8 @@ int ossl_a2i_ipadd(unsigned char *ipout, const char *ipasc); + int ossl_x509_set1_time(int *modified, ASN1_TIME **ptm, const ASN1_TIME *tm); + int ossl_x509_print_ex_brief(BIO *bio, X509 *cert, unsigned long neg_cflags); + int ossl_x509v3_cache_extensions(X509 *x); ++X509_NAME *ossl_dist_point_name_full(const struct DIST_POINT_NAME_st *dpn, ++ const X509_NAME *iname); + int ossl_x509_init_sig_info(X509 *x); + + int ossl_x509_set0_libctx(X509 *x, OSSL_LIB_CTX *libctx, const char *propq); diff -Nru openssl-3.5.7/debian/patches/Enforce-final-size-for-streams.patch openssl-3.5.7/debian/patches/Enforce-final-size-for-streams.patch --- openssl-3.5.7/debian/patches/Enforce-final-size-for-streams.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/Enforce-final-size-for-streams.patch 2026-09-29 20:10:12.000000000 +0000 @@ -0,0 +1,1478 @@ +From: Alexandr Nedvedicky +Date: Wed, 9 Sep 2026 11:09:19 +0200 +Subject: Enforce final size for streams. + +When FIN stream frame is received at particular offset, +then the FIN's offset must be the largest offset kept +in stream reassembly buffer. + +Receiving stream frame with offset larger than FIN frame +must be reported as protocol violation and connection +must be closed. + +In order to report protocol violation error the stream +reassembly module must have a reference to QUIC_CHANNEL +object where particular stream belongs to. The refernce +to channel is part of QUIC_RSTREAM_QPARAM structure. +This change makes stream quality parameter mandatory. + +Co-authored-by: Jakub Zelenka +Reviewed-by: Tomas Mraz +Reviewed-by: Milan Broz +Merge-date: Sat Sep 26 11:53:41 2026 +Merged-from: https://github.com/openssl/openssl/pull/32769 +--- + include/internal/quic_stream.h | 2 +- + ssl/quic/quic_channel.c | 3 +- + ssl/quic/quic_rstream.c | 23 +- + ssl/quic/quic_strm_reas.c | 217 ++++++++++--------- + test/quic_stream_test.c | 463 +++++++++++++++++++++++++++++++++++++++-- + 5 files changed, 571 insertions(+), 137 deletions(-) + +diff --git a/include/internal/quic_stream.h b/include/internal/quic_stream.h +index 2d0d7d947503..1c0fd3f86e87 100644 +--- a/include/internal/quic_stream.h ++++ b/include/internal/quic_stream.h +@@ -408,7 +408,7 @@ size_t ossl_quic_rstream_get_chunk_count(QUIC_RSTREAM *qrs); + */ + size_t ossl_quic_rstream_get_range_count(QUIC_RSTREAM *qrs); + +-QUIC_RSTREAM_QPARM *ossl_quic_rstream_qparm_new(void); ++QUIC_RSTREAM_QPARM *ossl_quic_rstream_qparm_new(QUIC_CHANNEL *ch); + void ossl_quic_rstream_qparm_destroy(QUIC_RSTREAM_QPARM *rsqp); + #endif + +diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c +index f8851790ab2f..b5a19d389ccb 100644 +--- a/ssl/quic/quic_channel.c ++++ b/ssl/quic/quic_channel.c +@@ -337,12 +337,11 @@ static int ch_init(QUIC_CHANNEL *ch) + goto err; + } + +- ch->rsqp = ossl_quic_rstream_qparm_new(); ++ ch->rsqp = ossl_quic_rstream_qparm_new(ch); + if (ch->rsqp == NULL) + goto err; + + for (pn_space = QUIC_PN_SPACE_INITIAL; pn_space < QUIC_PN_SPACE_NUM; ++pn_space) { +- /* no quality control for crypto stream. */ + ch->crypto_recv[pn_space] = ossl_quic_rstream_new(NULL, NULL, ch->rsqp); + if (ch->crypto_recv[pn_space] == NULL) + goto err; +diff --git a/ssl/quic/quic_rstream.c b/ssl/quic/quic_rstream.c +index 1ac496858b07..23ecdd0a23aa 100644 +--- a/ssl/quic/quic_rstream.c ++++ b/ssl/quic/quic_rstream.c +@@ -20,12 +20,13 @@ struct quic_rstream_st { + UINT_RANGE head_range; + }; + +-#if !defined(NDEBUG) && defined(WITH_RSTREAM_DEBUG) +-#include +-#define DEBUG_PRINT(...) fprintf(__VA_ARGS__) +-#else +-#define DEBUG_PRINT(...) (void)(0) +-#endif ++/* ARGSUSED */ ++#define STDERR NULL ++static void print_foo(void *f, ...) ++{ ++} ++ ++#define DEBUG_PRINT print_foo + + QUIC_RSTREAM *ossl_quic_rstream_new(QUIC_RXFC *rxfc, + OSSL_STATM *statm, QUIC_RSTREAM_QPARM *rsqp) +@@ -79,11 +80,11 @@ static int read_internal(QUIC_RSTREAM *qrs, unsigned char *buf, size_t size, + size_t readbytes_ = 0; + int fin_ = 0, ret = 1; + +- DEBUG_PRINT(stderr, "%s want: %zu\n", OPENSSL_FUNC, size); ++ DEBUG_PRINT(STDERR, "%s want: %zu\n", OPENSSL_FUNC, size); + while (ossl_sframe_set_peek(&qrs->fs, &iter, &range, &data, &fin_)) { + size_t l = (size_t)(range.end - range.start); + +- DEBUG_PRINT(stderr, "\t[ %llu, %llu ]\n", range.start, range.end); ++ DEBUG_PRINT(STDERR, "\t[ %llu, %llu ]\n", range.start, range.end); + if (l > size) { + l = size; + fin_ = 0; +@@ -104,11 +105,11 @@ static int read_internal(QUIC_RSTREAM *qrs, unsigned char *buf, size_t size, + ret = ossl_sframe_set_move_offset(&qrs->fs, offset); + + if (ret) { +- DEBUG_PRINT(stderr, "%s got: %zu\n", OPENSSL_FUNC, readbytes_); ++ DEBUG_PRINT(STDERR, "%s got: %zu\n", OPENSSL_FUNC, readbytes_); + *readbytes = readbytes_; + *fin = fin_; + } else { +- DEBUG_PRINT(stderr, "%s got: nothing\n", OPENSSL_FUNC); ++ DEBUG_PRINT(STDERR, "%s got: nothing\n", OPENSSL_FUNC); + } + + return ret; +@@ -181,7 +182,7 @@ int ossl_quic_rstream_get_record(QUIC_RSTREAM *qrs, + return 1; + } + +- DEBUG_PRINT(stderr, "%s head: [ %llu, %llu ]\n", OPENSSL_FUNC, ++ DEBUG_PRINT(STDERR, "%s head: [ %llu, %llu ]\n", OPENSSL_FUNC, + qrs->head_range.start, qrs->head_range.end); + /* if final empty frame, we drop it immediately */ + if (qrs->head_range.end == qrs->head_range.start) { +diff --git a/ssl/quic/quic_strm_reas.c b/ssl/quic/quic_strm_reas.c +index 033c58ed6621..e54fefe433f3 100644 +--- a/ssl/quic/quic_strm_reas.c ++++ b/ssl/quic/quic_strm_reas.c +@@ -12,13 +12,15 @@ + #include "internal/quic_stream.h" + #include "internal/quic_strm_reas.h" + #include "internal/list.h" ++#include "internal/quic_channel.h" + +-#if !defined(NDEBUG) && defined(WITH_STRM_REAS_DEBUG) +-#include +-#define DEBUG_PRINT(...) fprintf(__VA_ARGS__) +-#else +-#define DEBUG_PRINT(...) (void)(0) +-#endif ++#define STDERR NULL ++/* ARGSUSED */ ++static void print_foo(void *f, ...) ++{ ++} ++ ++#define DEBUG_PRINT print_foo + + #define DIRECT_STORAGE_SZ (2 * sizeof(void *)) + +@@ -61,6 +63,7 @@ struct stream_chunk_t { + struct quic_rstream_qparm_st { + size_t rsqp_pkt_overhead_treshold; + size_t rsqp_pkt_overhead_sz; ++ QUIC_CHANNEL *rsqp_ch; + }; + + #define sc_data sc_data_u.u_data +@@ -104,27 +107,25 @@ OSSL_RBT_GENERATE(srange, stream_range_t, sr_rbe, srange_cmp); + + static void rsqp_add_overhead(QUIC_RSTREAM_QPARM *rsqp, size_t sc_overhead) + { +- if (rsqp != NULL) +- rsqp->rsqp_pkt_overhead_sz += sc_overhead; ++ rsqp->rsqp_pkt_overhead_sz += sc_overhead; + } + + static void rsqp_sub_overhead(QUIC_RSTREAM_QPARM *rsqp, size_t sc_overhead) + { +- if (rsqp != NULL) +- rsqp->rsqp_pkt_overhead_sz -= sc_overhead; ++ rsqp->rsqp_pkt_overhead_sz -= sc_overhead; + } + + /* +- * Cleansing (SSL_OP_CLEANSE_PLAINTEXT) must write through the const +- * data pointers received from ossl_sframe_set_insert(), which may +- * point into a shared packet buffer. That is safe: each chunk +- * references the disjoint payload slice of its own frame and a +- * processed packet is kept alive only by the chunks stored on it, +- * so nobody else reads the wiped bytes. +- * +- * The const should eventually be dropped from the prototypes +- * instead; until then deconst() is used. +- */ ++ * Cleansing (SSL_OP_CLEANSE_PLAINTEXT) must write through the const ++ * data pointers received from ossl_sframe_set_insert(), which may ++ * point into a shared packet buffer. That is safe: each chunk ++ * references the disjoint payload slice of its own frame and a ++ * processed packet is kept alive only by the chunks stored on it, ++ * so nobody else reads the wiped bytes. ++ * ++ * The const should eventually be dropped from the prototypes ++ * instead; until then deconst() is used. ++ */ + static unsigned char *deconst(const unsigned char *data) + { + union { +@@ -216,10 +217,9 @@ static int srange_cmp(const struct stream_range_t *a_sr, + } + + static int keep_schunk_data_on_packet(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, +- UINT_RANGE *r) ++ size_t overhead) + { +- if (fs->rsqp != NULL +- && fs->rsqp->rsqp_pkt_overhead_sz >= fs->rsqp->rsqp_pkt_overhead_treshold) ++ if ((fs->rsqp->rsqp_pkt_overhead_sz + overhead) >= fs->rsqp->rsqp_pkt_overhead_treshold) + return 0; + + return 1; +@@ -242,33 +242,27 @@ static struct stream_chunk_t *new_schunk(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, + rsize = r->end - r->start; + assert(rsize <= pkt->datagram_len); + overhead = UINT64_TO_SIZE_T(pkt->datagram_len - rsize); +- rsqp_add_overhead(fs->rsqp, overhead); + +- if (keep_schunk_data_on_packet(fs, pkt, r) == 1) { ++ if (keep_schunk_data_on_packet(fs, pkt, overhead) == 1) { + sc->sc_st = ST_TYPE_PKT; + sc->sc_pkt = pkt; + ossl_qrx_pkt_up_ref(pkt); ++ rsqp_add_overhead(fs->rsqp, overhead); + sc->sc_data = data; + sc->sc_range = *r; +- if (fs->rsqp != NULL) +- DEBUG_PRINT(stderr, +- "%s sc: %p sc overhead: %d pkt_buf_overhead_sz: %zu -> %zu\n", +- OPENSSL_FUNC, (void *)sc, SCHUNK_OVERHEAD(pkt, sc), +- fs->rsqp->rsqp_pkt_overhead_sz - SCHUNK_OVERHEAD(pkt, sc), +- fs->rsqp->rsqp_pkt_overhead_sz); ++ DEBUG_PRINT(STDERR, ++ "%s sc: %p sc overhead: %llu pkt_buf_overhead_sz: %llu -> %zu\n", ++ OPENSSL_FUNC, (void *)sc, SCHUNK_OVERHEAD(pkt, sc), ++ fs->rsqp->rsqp_pkt_overhead_sz - SCHUNK_OVERHEAD(pkt, sc), ++ fs->rsqp->rsqp_pkt_overhead_sz); + } else { +- /* +- * Only data which stay on packet must be accounted as overhead. +- */ +- rsqp_sub_overhead(fs->rsqp, overhead); +- + if (rsize <= DIRECT_STORAGE_SZ) { +- DEBUG_PRINT(stderr, "%s ST_TYPE_DIRECT sc: %p %llu\n", OPENSSL_FUNC, ++ DEBUG_PRINT(STDERR, "%s ST_TYPE_DIRECT sc: %p %llu\n", OPENSSL_FUNC, + (void *)sc, rsize); + sc->sc_st = ST_TYPE_DIRECT; + sc->sc_data_w = sc->sc_dstorage; + } else { +- DEBUG_PRINT(stderr, "%s ST_TYPE_HEAP sc: %p %llu\n", OPENSSL_FUNC, ++ DEBUG_PRINT(STDERR, "%s ST_TYPE_HEAP sc: %p %llu\n", OPENSSL_FUNC, + (void *)sc, rsize); + sc->sc_st = ST_TYPE_HEAP; + sc->sc_buf = OPENSSL_malloc(UINT64_TO_SIZE_T(rsize)); +@@ -301,14 +295,12 @@ static void destroy_schunk(SFRAME_SET *fs, struct stream_chunk_t *sc) + + switch (sc->sc_st) { + case ST_TYPE_PKT: +- assert(fs->rsqp == NULL +- || fs->rsqp->rsqp_pkt_overhead_sz >= SCHUNK_OVERHEAD(sc->sc_pkt, sc)); +- if (fs->rsqp != NULL) +- DEBUG_PRINT(stderr, +- "%s sc: %p sc overhead: %d pkt_buf_overhead_sz: %zu -> %zu\n", +- OPENSSL_FUNC, (void *)sc, SCHUNK_OVERHEAD(sc->sc_pkt, sc), +- fs->rsqp->rsqp_pkt_overhead_sz, +- fs->rsqp->rsqp_pkt_overhead_sz - SCHUNK_OVERHEAD(sc->sc_pkt, sc)); ++ assert(fs->rsqp->rsqp_pkt_overhead_sz >= SCHUNK_OVERHEAD(sc->sc_pkt, sc)); ++ DEBUG_PRINT(STDERR, ++ "%s sc: %p sc overhead: %llu pkt_buf_overhead_sz: %zu -> %llu\n", ++ OPENSSL_FUNC, (void *)sc, SCHUNK_OVERHEAD(sc->sc_pkt, sc), ++ fs->rsqp->rsqp_pkt_overhead_sz, ++ fs->rsqp->rsqp_pkt_overhead_sz - SCHUNK_OVERHEAD(sc->sc_pkt, sc)); + rsqp_sub_overhead(fs->rsqp, + UINT64_TO_SIZE_T(SCHUNK_OVERHEAD(sc->sc_pkt, sc))); + ossl_qrx_pkt_release(sc->sc_pkt); +@@ -374,6 +366,8 @@ static struct stream_range_t *create_range(SFRAME_SET *fs, + + void ossl_sframe_set_init(SFRAME_SET *fs, QUIC_RSTREAM_QPARM *rsqp) + { ++ assert(rsqp != NULL); ++ + memset(fs, 0, sizeof(*fs)); + OSSL_RBT_INIT(srange, &fs->ranges); + fs->rsqp = rsqp; +@@ -403,7 +397,7 @@ static int try_dstorage(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, + */ + rsize = r->end - r->start; + if (r->start < sr->sr_range.start && r->end > sr->sr_range.end +- && rsize > DIRECT_STORAGE_SZ && ossl_list_sc_num(&sr->sr_chunks) > 1) ++ && ossl_list_sc_num(&sr->sr_chunks) > 1) + return 0; + + head_sc = ossl_list_sc_head(&sr->sr_chunks); +@@ -424,14 +418,14 @@ static int try_dstorage(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, + if (head_sc->sc_st != ST_TYPE_DIRECT) + return 0; + +- DEBUG_PRINT(stderr, "%s @in %p [ %llu, %llu ]\n", ++ DEBUG_PRINT(STDERR, "%s @in %p [ %llu, %llu ]\n", + OPENSSL_FUNC, *data, r->start, r->end); + rsize = r->end - r->start; + if (rsize <= DIRECT_STORAGE_SZ) { + /* + * update existing chunk + */ +- DEBUG_PRINT(stderr, ++ DEBUG_PRINT(STDERR, + "%s overwrite dstorage %p [ %llu, %llu ] -> [ %llu, %llu ] " + "sr: %p [ %llu, %llu ]\n", + OPENSSL_FUNC, (void *)head_sc, +@@ -452,12 +446,12 @@ static int try_dstorage(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, + */ + new_sc = new_schunk(fs, pkt, r, *data); + if (new_sc == NULL) { +- DEBUG_PRINT(stderr, "%s new_chunk() alloc failed\n", ++ DEBUG_PRINT(STDERR, "%s new_chunk() alloc failed\n", + OPENSSL_FUNC); + return -1; + } + +- DEBUG_PRINT(stderr, ++ DEBUG_PRINT(STDERR, + "%s replace chunk %p [ %llu, %llu ] -> %p [ %llu, %llu ]\n", + OPENSSL_FUNC, + (void *)head_sc, head_sc->sc_range.start, head_sc->sc_range.end, +@@ -466,7 +460,7 @@ static int try_dstorage(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, + ossl_list_sc_insert_head(&sr->sr_chunks, new_sc); + destroy_schunk(fs, head_sc); + } +- DEBUG_PRINT(stderr, "\trange: %p [ %llu, %llu ] -> [ %llu, %llu ]\n", ++ DEBUG_PRINT(STDERR, "\trange: %p [ %llu, %llu ] -> [ %llu, %llu ]\n", + (void *)sr, sr->sr_range.start, sr->sr_range.end, r->start, r->end); + sr->sr_range.start = r->start; + sr->sr_range.end = r->end; +@@ -486,7 +480,7 @@ static int try_dstorage(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, + if (dsize == 0) + return 0; + +- DEBUG_PRINT(stderr, "%s append: @in %p [ %llu, %llu ] dsize: %llu " ++ DEBUG_PRINT(STDERR, "%s append: @in %p [ %llu, %llu ] dsize: %llu " + "tail_sc: %p [ %llu, %llu] sr: %p [ %llu, %llu ]\n", + OPENSSL_FUNC, *data, r->start, r->end, dsize, + (void *)tail_sc, tail_sc->sc_range.start, tail_sc->sc_range.end, +@@ -512,17 +506,17 @@ static int try_dstorage(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, + rsize = (rsize < dsize) ? rsize : dsize; + offset = SCHUNK_SIZE(tail_sc); + memcpy(&tail_sc->sc_data_w[offset], *data, UINT64_TO_SIZE_T(rsize)); +- DEBUG_PRINT(stderr, "%s append tail_sc: %p [ %llu, %llu ] -> ", ++ DEBUG_PRINT(STDERR, "%s append tail_sc: %p [ %llu, %llu ] -> ", + OPENSSL_FUNC, (void *)tail_sc, + tail_sc->sc_range.start, tail_sc->sc_range.end); + tail_sc->sc_range.end += rsize; +- DEBUG_PRINT(stderr, "[ %llu, %llu ]\n", ++ DEBUG_PRINT(STDERR, "[ %llu, %llu ]\n", + tail_sc->sc_range.start, tail_sc->sc_range.end); + assert(SCHUNK_SIZE(tail_sc) <= DIRECT_STORAGE_SZ); +- DEBUG_PRINT(stderr, "\trange: %p [ %llu, %llu ] -> ", ++ DEBUG_PRINT(STDERR, "\trange: %p [ %llu, %llu ] -> ", + (void *)sr, sr->sr_range.start, sr->sr_range.end); + sr->sr_range.end = tail_sc->sc_range.end; +- DEBUG_PRINT(stderr, "[ %llu, %llu ]\n", ++ DEBUG_PRINT(STDERR, "[ %llu, %llu ]\n", + sr->sr_range.start, sr->sr_range.end); + + if (fs->cleanse) +@@ -542,7 +536,7 @@ static int try_dstorage(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, + if (dsize == 0) + return 0; + +- DEBUG_PRINT(stderr, "%s prepend: @in %p [ %llu, %llu ] dsize: %llu " ++ DEBUG_PRINT(STDERR, "%s prepend: @in %p [ %llu, %llu ] dsize: %llu " + "sr: %p [ %llu, %llu ]\n", + OPENSSL_FUNC, *data, r->start, r->end, dsize, + (void *)sr, sr->sr_range.start, sr->sr_range.end); +@@ -568,17 +562,17 @@ static int try_dstorage(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, + assert(offset < r->end - r->start); + data_buf = *data; + memcpy(head_sc->sc_data_w, &data_buf[offset], UINT64_TO_SIZE_T(rsize)); +- DEBUG_PRINT(stderr, "%s prepend head_sc: %p [ %llu, %llu ] -> ", ++ DEBUG_PRINT(STDERR, "%s prepend head_sc: %p [ %llu, %llu ] -> ", + OPENSSL_FUNC, (void *)head_sc, + head_sc->sc_range.start, head_sc->sc_range.end); + head_sc->sc_range.start -= rsize; +- DEBUG_PRINT(stderr, "[ %llu, %llu ]\n", ++ DEBUG_PRINT(STDERR, "[ %llu, %llu ]\n", + head_sc->sc_range.start, head_sc->sc_range.end); + assert(SCHUNK_SIZE(head_sc) <= DIRECT_STORAGE_SZ); +- DEBUG_PRINT(stderr, "\trange: %p [ %llu, %llu ] -> ", ++ DEBUG_PRINT(STDERR, "\trange: %p [ %llu, %llu ] -> ", + (void *)sr, sr->sr_range.start, sr->sr_range.end); + sr->sr_range.start = head_sc->sc_range.start; +- DEBUG_PRINT(stderr, "[ %llu, %llu ]\n", ++ DEBUG_PRINT(STDERR, "[ %llu, %llu ]\n", + sr->sr_range.start, sr->sr_range.end); + assert(r->end - r->start >= rsize); + +@@ -596,7 +590,7 @@ static int try_dstorage(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, + * returns 1 if all data were consumed + */ + assert(r->end >= r->start); +- DEBUG_PRINT(stderr, "%s @out %p [ %llu, %llu ]\n", ++ DEBUG_PRINT(STDERR, "%s @out %p [ %llu, %llu ]\n", + OPENSSL_FUNC, *data, r->start, r->end); + + return ((r->end - r->start) == 0) ? 1 : 0; +@@ -625,7 +619,7 @@ static const unsigned char *trim_partial_overlap(SFRAME_SET *fs, + if (fs->cleanse) + OPENSSL_cleanse(deconst(data), UINT64_TO_SIZE_T(unused_sz)); + +- DEBUG_PRINT(stderr, "%s right overlap %p [ %llu, %llu ]:\n\t" ++ DEBUG_PRINT(STDERR, "%s right overlap %p [ %llu, %llu ]:\n\t" + "r: [ %llu, %llu ] -> [ %llu, %llu ]\n", + OPENSSL_FUNC, + (void *)sr, sr->sr_range.start, sr->sr_range.end, +@@ -642,7 +636,7 @@ static const unsigned char *trim_partial_overlap(SFRAME_SET *fs, + + (sr->sr_range.start - r->start)), + UINT64_TO_SIZE_T(unused_sz)); + +- DEBUG_PRINT(stderr, "%s left overlap %p [ %llu, %llu]:\n\t" ++ DEBUG_PRINT(STDERR, "%s left overlap %p [ %llu, %llu]:\n\t" + "r: [ %llu, %llu ] -> [ %llu, %llu ]\n", + OPENSSL_FUNC, + (void *)sr, sr->sr_range.start, sr->sr_range.end, +@@ -661,7 +655,7 @@ static const unsigned char *trim_partial_overlap(SFRAME_SET *fs, + static void prepend_chunk(SFRAME_SET *fs, struct stream_range_t *sr, + struct stream_chunk_t *sc) + { +- DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] add to head %p [ %llu, %llu ] " ++ DEBUG_PRINT(STDERR, "%s %p [ %llu, %llu ] add to head %p [ %llu, %llu ] " + "-> [ %llu, %llu ]\n", + OPENSSL_FUNC, + (void *)sc, sc->sc_range.start, sc->sc_range.end, +@@ -688,7 +682,7 @@ static void prepend_chunk(SFRAME_SET *fs, struct stream_range_t *sr, + static void append_chunk(SFRAME_SET *fs, struct stream_range_t *sr, + struct stream_chunk_t *sc) + { +- DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] add to tail %p [ %llu, %llu ] " ++ DEBUG_PRINT(STDERR, "%s %p [ %llu, %llu ] add to tail %p [ %llu, %llu ] " + "-> [ %llu, %llu ]\n", + OPENSSL_FUNC, + (void *)sc, sc->sc_range.start, sc->sc_range.end, +@@ -722,7 +716,7 @@ static void replace_chunks_in_range(SFRAME_SET *fs, struct stream_range_t *sr, + + ossl_list_sc_insert_head(&sr->sr_chunks, sc); + fs->stream_chunks++; +- DEBUG_PRINT(stderr, "%s range: %p [ %llu, %llu ] -> [ %llu, %llu ]\n", ++ DEBUG_PRINT(STDERR, "%s range: %p [ %llu, %llu ] -> [ %llu, %llu ]\n", + OPENSSL_FUNC, (void *)sr, sr->sr_range.start, sr->sr_range.end, + sc->sc_range.start, sc->sc_range.end); + sr->sr_range.start = sc->sc_range.start; +@@ -782,11 +776,10 @@ static int chop_range(SFRAME_SET *fs, struct stream_range_t *sr, + + if (sc->sc_st == ST_TYPE_PKT) { + rsqp_add_overhead(fs->rsqp, unused_sz); +- if (fs->rsqp != NULL) +- DEBUG_PRINT(stderr, "%s sc: %p unused_sz: %zu %zu -> %zu\n", +- OPENSSL_FUNC, (void *)sc, unused_sz, +- fs->rsqp->rsqp_pkt_overhead_sz - unused_sz, +- fs->rsqp->rsqp_pkt_overhead_sz); ++ DEBUG_PRINT(STDERR, "%s sc: %p unused_sz: %zu %zu -> %zu\n", ++ OPENSSL_FUNC, (void *)sc, unused_sz, ++ fs->rsqp->rsqp_pkt_overhead_sz - unused_sz, ++ fs->rsqp->rsqp_pkt_overhead_sz); + } + + return 1; +@@ -812,7 +805,7 @@ static struct stream_range_t *merge_ranges(SFRAME_SET *fs, + assert(super_sr->sr_range.start <= sub_sr->sr_range.start + && super_sr->sr_range.end >= sub_sr->sr_range.end); + +- DEBUG_PRINT(stderr, "%s super: %p [ %llu, %llu ], sub: %p [ %llu, %llu]\n", ++ DEBUG_PRINT(STDERR, "%s super: %p [ %llu, %llu ], sub: %p [ %llu, %llu]\n", + OPENSSL_FUNC, (void *)super_sr, super_sr->sr_range.start, + super_sr->sr_range.end, (void *)sub_sr, sub_sr->sr_range.start, + sub_sr->sr_range.end); +@@ -842,7 +835,7 @@ static struct stream_range_t *append_range(SFRAME_SET *fs, + */ + assert(left_sr->sr_range.end >= right_sr->sr_range.start); + +- DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] + %p [ %llu, %llu ] = %p " ++ DEBUG_PRINT(STDERR, "%s %p [ %llu, %llu ] + %p [ %llu, %llu ] = %p " + "[ %llu, %llu ]\n", + OPENSSL_FUNC, (void *)left_sr, left_sr->sr_range.start, + left_sr->sr_range.end, (void *)right_sr, right_sr->sr_range.start, +@@ -866,6 +859,7 @@ static struct stream_range_t *append_range(SFRAME_SET *fs, + + /* + * receives a chunk of data from stream frame. ++ * note there is a tri-state return value: + */ + int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *r, OSSL_QRX_PKT *pkt, + const unsigned char *data, int fin) +@@ -876,35 +870,54 @@ int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *r, OSSL_QRX_PKT *pkt, + struct stream_chunk_t *sc = NULL; + struct stream_range_t key_sr = { 0 }; + ++ assert(r->start <= r->end); ++ + /* +- * receive the FIN frame if FIN frame. If FIN was not seen yet, +- * then record FIN's offset (r->end). If FIN was received then +- * verify FIN's offset match, error out on mismatch. ++ * receive the FIN frame. If FIN was not seen yet, then record ++ * FIN's offset (r->end). If FIN was received then verify FIN's ++ * offset match, error out on mismatch. + */ + if (fin != 0) { + if (fs->fin == 0) { ++ sr = OSSL_RBT_MAX(srange, &fs->ranges); ++ if (r->end < fs->offset ++ || (sr != NULL && sr->sr_range.end > r->end)) { ++ ossl_quic_channel_raise_protocol_error(fs->rsqp->rsqp_ch, ++ OSSL_QUIC_ERR_FINAL_SIZE_ERROR, ++ OSSL_QUIC_FRAME_TYPE_STREAM_FIN, ++ "stream final size error"); ++ return 0; ++ } + fs->fin = 1; + fs->fin_off = r->end; + } else if (fs->fin_off != r->end) { ++ ossl_quic_channel_raise_protocol_error(fs->rsqp->rsqp_ch, ++ OSSL_QUIC_ERR_FINAL_SIZE_ERROR, ++ OSSL_QUIC_FRAME_TYPE_STREAM_FIN, ++ "stream final size error"); + return 0; + } + } + + /* +- * discard any data past FIN offset (of FIN offset is set). ++ * reject any data at or past the FIN offset (if FIN offset is set). + */ + if (fs->fin != 0) { +- if (fs->fin_off < r->end) +- r->end = fs->fin_off; /* truncate bytes beyond FIN */ +- if (fs->fin_off < r->start) ++ if (fs->fin_off < r->end) { ++ ossl_quic_channel_raise_protocol_error(fs->rsqp->rsqp_ch, ++ OSSL_QUIC_ERR_FINAL_SIZE_ERROR, ++ (fin == 0) ? OSSL_QUIC_FRAME_TYPE_STREAM ++ : OSSL_QUIC_FRAME_TYPE_STREAM_FIN, ++ "stream final size error"); + return 0; ++ } + } + + if (r->end <= fs->offset) { + /* + * retransmitted range got consumed already. + */ +- DEBUG_PRINT(stderr, "%s [ %llu, %llu ] <= %llu\n", OPENSSL_FUNC, ++ DEBUG_PRINT(STDERR, "%s [ %llu, %llu ] <= %llu\n", OPENSSL_FUNC, + r->start, r->end, fs->offset); + if (fs->cleanse && data != NULL) + OPENSSL_cleanse(deconst(data), UINT64_TO_SIZE_T(r->end - r->start)); +@@ -918,7 +931,7 @@ int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *r, OSSL_QRX_PKT *pkt, + * Make sure retransmitted chunk does not reintroduce + * bytes which were consumed already. + */ +- DEBUG_PRINT(stderr, "%s [ %llu, %llu ] -> [ %llu, %llu ]\n", OPENSSL_FUNC, ++ DEBUG_PRINT(STDERR, "%s [ %llu, %llu ] -> [ %llu, %llu ]\n", OPENSSL_FUNC, + r->start, r->end, fs->offset, r->end); + if (fs->cleanse) + OPENSSL_cleanse(deconst(data), +@@ -946,7 +959,7 @@ int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *r, OSSL_QRX_PKT *pkt, + sr = create_range(fs, sc); + if (sr == NULL) + goto err; +- DEBUG_PRINT(stderr, "%s chunk: %p [ %llu, %llu ] new range: %p\n", ++ DEBUG_PRINT(STDERR, "%s chunk: %p [ %llu, %llu ] new range: %p\n", + OPENSSL_FUNC, (void *)sc, sc->sc_range.start, sc->sc_range.end, + (void *)sr); + sc = NULL; +@@ -957,7 +970,7 @@ int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *r, OSSL_QRX_PKT *pkt, + * retransmission, the whole chunk is found in existing range already + */ + if (r->start >= sr->sr_range.start && r->end <= sr->sr_range.end) { +- DEBUG_PRINT(stderr, ++ DEBUG_PRINT(STDERR, + "%s [ %llu, %llu ] found in %p [ %llu, %llu ]\n", OPENSSL_FUNC, + r->start, r->end, (void *)sr, sr->sr_range.start, + sr->sr_range.end); +@@ -994,7 +1007,7 @@ int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *r, OSSL_QRX_PKT *pkt, + sr = NULL; + goto err; + } +- DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] -> %p [ %llu, %llu ]\n", ++ DEBUG_PRINT(STDERR, "%s %p [ %llu, %llu ] -> %p [ %llu, %llu ]\n", + OPENSSL_FUNC, (void *)sc, sc->sc_range.start, sc->sc_range.end, + (void *)sr, sr->sr_range.start, sr->sr_range.end); + +@@ -1046,7 +1059,7 @@ int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *r, OSSL_QRX_PKT *pkt, + + while (adjacent_sr != NULL) { + OSSL_RBT_REMOVE(srange, &fs->ranges, adjacent_sr); +- DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] >< %p [ %llu, %llu ]\n", ++ DEBUG_PRINT(STDERR, "%s %p [ %llu, %llu ] >< %p [ %llu, %llu ]\n", + OPENSSL_FUNC, (void *)sr, sr->sr_range.start, sr->sr_range.end, + (void *)adjacent_sr, adjacent_sr->sr_range.start, + adjacent_sr->sr_range.end); +@@ -1139,7 +1152,7 @@ int ossl_sframe_set_peek(SFRAME_SET *fs, void **iterator, + * no chunks are ready to be consumed, if there is a gap. + */ + if (sc != NULL && sc->sc_range.start > start) { +- DEBUG_PRINT(stderr, "%s sc: %p sr: %p sc->start %llu, fs->offset: %llu\n", ++ DEBUG_PRINT(STDERR, "%s sc: %p sr: %p sc->start %llu, fs->offset: %llu\n", + OPENSSL_FUNC, (void *)sc, (void *)sr, sc->sc_range.start, start); + sc = NULL; + } +@@ -1161,7 +1174,7 @@ int ossl_sframe_set_peek(SFRAME_SET *fs, void **iterator, + assert(sc == NULL || sc->sc_range.start < sc->sc_range.end); + } else { + /* iterator got invalidated by move/flatten operation on range */ +- DEBUG_PRINT(stderr, "%s iterator got invalidated\n", OPENSSL_FUNC); ++ DEBUG_PRINT(STDERR, "%s iterator got invalidated\n", OPENSSL_FUNC); + return 0; + } + +@@ -1182,7 +1195,7 @@ int ossl_sframe_set_peek(SFRAME_SET *fs, void **iterator, + else + *fin = 0; + +- DEBUG_PRINT(stderr, "%s no more chunks\n", OPENSSL_FUNC); ++ DEBUG_PRINT(STDERR, "%s no more chunks\n", OPENSSL_FUNC); + + return 0; + } +@@ -1195,7 +1208,7 @@ int ossl_sframe_set_peek(SFRAME_SET *fs, void **iterator, + *fin = fs->fin && sc->sc_range.end == fs->fin_off; + + if (sr->sr_it_sc != NULL) +- DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] %p [ %llu, %llu ]\n", ++ DEBUG_PRINT(STDERR, "%s %p [ %llu, %llu ] %p [ %llu, %llu ]\n", + OPENSSL_FUNC, (void *)sr->sr_it_sc, + sr->sr_it_sc->sc_range.start, sr->sr_it_sc->sc_range.end, + (void *)sc, sc->sc_range.start, sc->sc_range.end); +@@ -1207,7 +1220,7 @@ int ossl_sframe_set_peek(SFRAME_SET *fs, void **iterator, + * peek operation indicates error if there are no data to read + * in range. + */ +- DEBUG_PRINT(stderr, ++ DEBUG_PRINT(STDERR, + "%s peek range: [ %llu, %llu ] range: %p [ %llu, %llu ]\n", OPENSSL_FUNC, + range->start, range->end, (void *)sr, sr->sr_range.start, + sr->sr_range.end); +@@ -1262,7 +1275,7 @@ int ossl_sframe_set_move_offset(SFRAME_SET *fs, uint64_t new_offset) + } + } + +- DEBUG_PRINT(stderr, "%s offset: %llu -> %llu range: %p [ %llu, %llu ] -> ", ++ DEBUG_PRINT(STDERR, "%s offset: %llu -> %llu range: %p [ %llu, %llu ] -> ", + OPENSSL_FUNC, fs->offset - new_offset, new_offset, + (void *)sr, sr->sr_range.start, sr->sr_range.end); + +@@ -1274,22 +1287,21 @@ int ossl_sframe_set_move_offset(SFRAME_SET *fs, uint64_t new_offset) + OSSL_RBT_REMOVE(srange, &fs->ranges, sr); + destroy_srange(fs, sr); + fs->stream_ranges--; +- DEBUG_PRINT(stderr, "[ NULL ]\n"); ++ DEBUG_PRINT(STDERR, "[ NULL ]\n"); + } else { + unused_sz = UINT64_TO_SIZE_T(new_offset - sc->sc_range.start); + sc_data_trim_left(sc, unused_sz, fs->cleanse); + sc->sc_range.start = new_offset; + sr->sr_range.start = new_offset; +- DEBUG_PRINT(stderr, "[ %lli, %llu ]\n", ++ DEBUG_PRINT(STDERR, "[ %lli, %llu ]\n", + sr->sr_range.start, sr->sr_range.end); + + if (sc->sc_st == ST_TYPE_PKT) { + rsqp_add_overhead(fs->rsqp, unused_sz); +- if (fs->rsqp != NULL) +- DEBUG_PRINT(stderr, "%s sc: %p unused_sz: %zu %zu -> %zu\n", +- OPENSSL_FUNC, (void *)sc, unused_sz, +- fs->rsqp->rsqp_pkt_overhead_sz - unused_sz, +- fs->rsqp->rsqp_pkt_overhead_sz); ++ DEBUG_PRINT(STDERR, "%s sc: %p unused_sz: %zu %zu -> %zu\n", ++ OPENSSL_FUNC, (void *)sc, unused_sz, ++ fs->rsqp->rsqp_pkt_overhead_sz - unused_sz, ++ fs->rsqp->rsqp_pkt_overhead_sz); + } + } + +@@ -1310,7 +1322,7 @@ int ossl_sframe_set_avail(SFRAME_SET *fs, uint64_t *avail, int *fin) + return 1; + } + +-QUIC_RSTREAM_QPARM *ossl_quic_rstream_qparm_new(void) ++QUIC_RSTREAM_QPARM *ossl_quic_rstream_qparm_new(QUIC_CHANNEL *ch) + { + QUIC_RSTREAM_QPARM *rsqp; + +@@ -1318,6 +1330,7 @@ QUIC_RSTREAM_QPARM *ossl_quic_rstream_qparm_new(void) + if (rsqp != NULL) { + rsqp->rsqp_pkt_overhead_treshold = PKT_BUFFER_OVERHEAD_TRESHOLD; + rsqp->rsqp_pkt_overhead_sz = 0; ++ rsqp->rsqp_ch = ch; + } + + return rsqp; +diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c +index 93a745eaff86..41bc5d3b2bfc 100644 +--- a/test/quic_stream_test.c ++++ b/test/quic_stream_test.c +@@ -12,6 +12,7 @@ + #include "internal/quic_record_rx.h" + #include "internal/quic_stream.h" + #include "../ssl/quic/quic_record_rx_local.h" ++#include "../ssl/quic/quic_channel_local.h" + #include "internal/nelem.h" + #include "testutil.h" + +@@ -411,6 +412,7 @@ static int test_rstream_random(int idx) + unsigned char *bulk_data = NULL; + unsigned char *read_buf = NULL; + QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; + OSSL_QRX_PKT **pkts = NULL; + QUIC_RSTREAM_QPARM *rsqp = NULL; + size_t i, read_off, queued_min, queued_max, num_pkts = 0; +@@ -424,7 +426,8 @@ static int test_rstream_random(int idx) + if (!TEST_ptr(bulk_data = OPENSSL_malloc(data_size)) + || !TEST_ptr(read_buf = OPENSSL_malloc(data_size)) + || !TEST_ptr(pkts = OPENSSL_zalloc(sizeof(*pkts) * max_pkts)) +- || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()) ++ || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)) + || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) + goto err; + +@@ -532,6 +535,9 @@ static int test_rstream_random(int idx) + goto err; + } + ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ + ret = 1; + + err: +@@ -548,6 +554,7 @@ static int test_rstream_random(int idx) + ossl_quic_rstream_qparm_destroy(rsqp); + OPENSSL_free(bulk_data); + OPENSSL_free(read_buf); ++ ossl_quic_channel_free(ch); + return ret; + } + +@@ -558,6 +565,7 @@ static int test_rstream_random(int idx) + static int test_rstream_pkt(void) + { + QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; + QUIC_RSTREAM_QPARM *rsqp = NULL; + OSSL_QRX_PKT *pkt_a = NULL, *pkt_b = NULL, *pkt_c = NULL; + unsigned char pdata[64], cbuf[64], buf[64]; +@@ -571,7 +579,8 @@ static int test_rstream_pkt(void) + if (!TEST_ptr(pkt_a = pkt_test_new(1200)) + || !TEST_ptr(pkt_b = pkt_test_new(1200)) + || !TEST_ptr(pkt_c = pkt_test_new(1200)) +- || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()) ++ || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)) + || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) + goto err; + +@@ -661,6 +670,9 @@ static int test_rstream_pkt(void) + if (!TEST_uchar_eq(cbuf[i], i >= 8 && i < 56 ? 0 : 0xAA)) + goto err; + ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ + ret = 1; + + err: +@@ -669,6 +681,7 @@ static int test_rstream_pkt(void) + pkt_test_free(pkt_a); + pkt_test_free(pkt_b); + pkt_test_free(pkt_c); ++ ossl_quic_channel_free(ch); + return ret; + } + +@@ -683,6 +696,7 @@ static int test_rstream_pkt_overhead(void) + QUIC_RSTREAM *rstream = NULL; + OSSL_QRX_PKT **pkt = NULL; + QUIC_RSTREAM_QPARM *rsqp = NULL; ++ QUIC_CHANNEL *ch = NULL; + unsigned char *data = NULL, *buf = NULL; + const size_t framesz = 8; + const size_t nframes = 4096; /* far past a 64 KiB overhead limit */ +@@ -694,7 +708,8 @@ static int test_rstream_pkt_overhead(void) + if (!TEST_ptr(data = OPENSSL_malloc(total)) + || !TEST_ptr(buf = OPENSSL_malloc(total)) + || !TEST_ptr(pkt = OPENSSL_zalloc(nframes * sizeof(*pkt))) +- || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()) ++ || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)) + || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) + goto err; + +@@ -736,6 +751,9 @@ static int test_rstream_pkt_overhead(void) + if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) + goto err; + ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ + ret = 1; + + err: +@@ -747,6 +765,7 @@ static int test_rstream_pkt_overhead(void) + OPENSSL_free(pkt); + OPENSSL_free(data); + OPENSSL_free(buf); ++ ossl_quic_channel_free(ch); + return ret; + } + +@@ -764,6 +783,7 @@ static int test_rstream_reorder(int idx) + { + unsigned char *data = NULL, *buf = NULL, *arena = NULL, *ap; + QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; + OSSL_QRX_PKT **pkts = NULL; + QUIC_RSTREAM_QPARM *rsqp = NULL; + const size_t data_size = 4096; +@@ -779,7 +799,8 @@ static int test_rstream_reorder(int idx) + || !TEST_ptr(arena = OPENSSL_malloc(3 * data_size)) + || !TEST_ptr(order = OPENSSL_malloc(nframes * sizeof(*order))) + || !TEST_ptr(pkts = OPENSSL_zalloc(2 * nframes * sizeof(*pkts))) +- || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()) ++ || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)) + || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) + goto err; + +@@ -849,6 +870,9 @@ static int test_rstream_reorder(int idx) + || !TEST_mem_eq(buf, got, data, data_size)) + goto err; + ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ + ret = 1; + + err: +@@ -866,6 +890,99 @@ static int test_rstream_reorder(int idx) + OPENSSL_free(arena); + OPENSSL_free(data); + OPENSSL_free(buf); ++ ossl_quic_channel_free(ch); ++ return ret; ++} ++ ++/* ++ * A short retransmit which starts below an existing range and ends past its ++ * direct storage tail chunk is small enough to slip past the full overlap ++ * guard in try_dstorage(), whose append path then treats every byte below ++ * the tail chunk end as duplicate. The genuinely new bytes below the range ++ * start must not be dropped when the insert reports success, otherwise the ++ * frame is acked, never retransmitted and the gap in the stream is permanent. ++ */ ++static int test_rstream_dstorage_two_sided_overlap(void) ++{ ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ OSSL_QRX_PKT *pkts[59] = { NULL }; ++ unsigned char pdata[64], buf[64], fill = 0xFF; ++ size_t num_pkts = 0, readbytes = 0, avail = 0, i; ++ int fin = 0; ++ int ret = 0; ++ ++ for (i = 0; i < sizeof(pdata); ++i) ++ pdata[i] = (unsigned char)(0x40 + i); ++ ++ if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) ++ goto err; ++ ++ for (i = 0; i < OSSL_NELEM(pkts); ++i) ++ if (!TEST_ptr(pkts[num_pkts++] = pkt_test_new(1200))) ++ goto err; ++ ++ /* a packet backed chunk [10, 11) while below the overhead limit */ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[0], 10, ++ pdata + 10, 1, 0)) ++ || !TEST_size_t_eq(pkt_test_refcount(pkts[0]), 2)) ++ goto err; ++ ++ /* ++ * disjoint 1-byte frames on 1200 byte datagrams, each accounting ++ * 1199 bytes of overhead, take the stream past the 64kB overhead ++ * limit so that short chunks switch to direct storage ++ */ ++ for (i = 0; i < 55; ++i) ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[1 + i], ++ 100 + 2 * i, &fill, 1, 0))) ++ goto err; ++ ++ /* ++ * [11, 12) goes to the direct storage of a new tail chunk appended ++ * to the range, holding no reference to its packet ++ */ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[56], 11, ++ pdata + 11, 1, 0)) ++ || !TEST_size_t_eq(pkt_test_refcount(pkts[56]), 1)) ++ goto err; ++ ++ /* ++ * a 6 byte retransmit [8, 14) starts below the range [10, 12) and ++ * ends past its direct storage tail chunk, the bytes [8, 10) are ++ * new and must be kept ++ */ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[57], 8, ++ pdata + 8, 6, 0))) ++ goto err; ++ ++ /* deliver the head [0, 8) so everything up to 14 is contiguous */ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[58], 0, ++ pdata, 8, 0))) ++ goto err; ++ ++ if (!TEST_true(ossl_quic_rstream_available(rstream, &avail, &fin)) ++ || !TEST_size_t_eq(avail, 14) ++ || !TEST_true(ossl_quic_rstream_read(rstream, buf, sizeof(buf), ++ &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 14) ++ || !TEST_mem_eq(buf, readbytes, pdata, 14)) ++ goto err; ++ ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ ++ ret = 1; ++ ++err: ++ ossl_quic_rstream_free(rstream); ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ for (i = 0; i < num_pkts; ++i) ++ pkt_test_free(pkts[i]); ++ ossl_quic_channel_free(ch); + return ret; + } + +@@ -889,16 +1006,20 @@ static int test_rstream_chunk_partial_overlap(void) + OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 }; + QUIC_RSTREAM_QPARM *rsqp = NULL; + TEST_STREAM_CHUNK_T *tsc; +- QUIC_RSTREAM *rstream; ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; + size_t readbytes; + unsigned int i; +- unsigned int send_order[7]; ++ unsigned int send_order[OSSL_NELEM(tsc_buf)]; + int fin = 0; + int ok = 0; + +- if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())) ++ if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))) + return 0; + ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))) ++ goto err; ++ + rstream = ossl_quic_rstream_new(NULL, NULL, rsqp); + if (!TEST_ptr(rstream)) + goto err; +@@ -1069,6 +1190,9 @@ static int test_rstream_chunk_partial_overlap(void) + if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) + goto err; + ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ + ok = 1; + err: + for (i = 0; i < OSSL_NELEM(tsc_buf); i++) +@@ -1076,6 +1200,7 @@ static int test_rstream_chunk_partial_overlap(void) + + ossl_quic_rstream_free(rstream); + ossl_quic_rstream_qparm_destroy(rsqp); ++ ossl_quic_channel_free(ch); + + return ok; + } +@@ -1095,16 +1220,20 @@ static int test_rstream_chunk_full_overlap(void) + OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 }; + QUIC_RSTREAM_QPARM *rsqp = NULL; + TEST_STREAM_CHUNK_T *tsc; +- QUIC_RSTREAM *rstream; ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; + size_t readbytes; + unsigned int i; +- unsigned int send_order[5]; ++ unsigned int send_order[OSSL_NELEM(tsc_buf)]; + int fin; + int ok = 0; + +- if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())) ++ if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))) + return 0; + ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))) ++ goto err; ++ + rstream = ossl_quic_rstream_new(NULL, NULL, rsqp); + if (!TEST_ptr(rstream)) + goto err; +@@ -1205,6 +1334,9 @@ static int test_rstream_chunk_full_overlap(void) + if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) + goto err; + ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ + ok = 1; + err: + for (i = 0; i < OSSL_NELEM(tsc_buf); i++) +@@ -1212,6 +1344,7 @@ static int test_rstream_chunk_full_overlap(void) + + ossl_quic_rstream_free(rstream); + ossl_quic_rstream_qparm_destroy(rsqp); ++ ossl_quic_channel_free(ch); + + return ok; + } +@@ -1224,15 +1357,19 @@ static int test_rstream_range_overlap(void) + OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 }; + QUIC_RSTREAM_QPARM *rsqp = NULL; + TEST_STREAM_CHUNK_T *tsc; +- QUIC_RSTREAM *rstream; ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; + size_t readbytes; + unsigned int i; + int fin; + int ok = 0; + +- if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())) ++ if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))) + return 0; + ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))) ++ goto err; ++ + rstream = ossl_quic_rstream_new(NULL, NULL, rsqp); + if (!TEST_ptr(rstream)) + goto err; +@@ -1396,6 +1533,9 @@ static int test_rstream_range_overlap(void) + if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) + goto err; + ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ + ok = 1; + err: + for (i = 0; i < OSSL_NELEM(tsc_buf); i++) +@@ -1403,6 +1543,7 @@ static int test_rstream_range_overlap(void) + + ossl_quic_rstream_free(rstream); + ossl_quic_rstream_qparm_destroy(rsqp); ++ ossl_quic_channel_free(ch); + + return ok; + } +@@ -1415,10 +1556,11 @@ static int test_rstream_prepend_byte_chunks(void) + OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 }; + QUIC_RSTREAM_QPARM *rsqp = NULL; + TEST_STREAM_CHUNK_T *tsc; +- QUIC_RSTREAM *rstream; ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; + size_t readbytes; + unsigned int i; +- unsigned int send_order[6]; ++ unsigned int send_order[OSSL_NELEM(tsc_buf)]; + int fin; + int ok = 0; + +@@ -1427,9 +1569,12 @@ static int test_rstream_prepend_byte_chunks(void) + return 1; + } + +- if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())) ++ if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))) + return 0; + ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))) ++ goto err; ++ + rstream = ossl_quic_rstream_new(NULL, NULL, rsqp); + if (!TEST_ptr(rstream)) + goto err; +@@ -1542,6 +1687,9 @@ static int test_rstream_prepend_byte_chunks(void) + if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) + goto err; + ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ + ok = 1; + err: + for (i = 0; i < OSSL_NELEM(tsc_buf); i++) +@@ -1549,6 +1697,7 @@ static int test_rstream_prepend_byte_chunks(void) + + ossl_quic_rstream_free(rstream); + ossl_quic_rstream_qparm_destroy(rsqp); ++ ossl_quic_channel_free(ch); + + return ok; + } +@@ -1561,10 +1710,11 @@ static int test_rstream_append_byte_chunks(void) + OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 }; + QUIC_RSTREAM_QPARM *rsqp = NULL; + TEST_STREAM_CHUNK_T *tsc; +- QUIC_RSTREAM *rstream; ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; + size_t readbytes; + unsigned int i; +- unsigned int send_order[6]; ++ unsigned int send_order[OSSL_NELEM(tsc_buf)]; + int fin; + int ok = 0; + +@@ -1573,9 +1723,12 @@ static int test_rstream_append_byte_chunks(void) + return 1; + } + +- if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())) ++ if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))) + return 0; + ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))) ++ goto err; ++ + rstream = ossl_quic_rstream_new(NULL, NULL, rsqp); + if (!TEST_ptr(rstream)) + goto err; +@@ -1688,6 +1841,9 @@ static int test_rstream_append_byte_chunks(void) + if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) + goto err; + ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ + ok = 1; + err: + for (i = 0; i < OSSL_NELEM(tsc_buf); i++) +@@ -1695,6 +1851,7 @@ static int test_rstream_append_byte_chunks(void) + + ossl_quic_rstream_free(rstream); + ossl_quic_rstream_qparm_destroy(rsqp); ++ ossl_quic_channel_free(ch); + + return ok; + } +@@ -1707,10 +1864,11 @@ static int test_rstream_mix_chunks(void) + OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 }; + QUIC_RSTREAM_QPARM *rsqp = NULL; + TEST_STREAM_CHUNK_T *tsc; +- QUIC_RSTREAM *rstream; ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; + size_t readbytes; + unsigned int i; +- unsigned int send_order[7]; ++ unsigned int send_order[OSSL_NELEM(tsc_buf)]; + int fin; + int ok = 0; + +@@ -1719,9 +1877,12 @@ static int test_rstream_mix_chunks(void) + return 1; + } + +- if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())) ++ if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))) + return 0; + ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))) ++ goto err; ++ + rstream = ossl_quic_rstream_new(NULL, NULL, rsqp); + if (!TEST_ptr(rstream)) + goto err; +@@ -1860,13 +2021,270 @@ static int test_rstream_mix_chunks(void) + if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) + goto err; + ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ ++ ok = 1; ++err: ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) ++ pkt_test_free(pkt[i]); ++ ++ ossl_quic_rstream_free(rstream); ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ ossl_quic_channel_free(ch); ++ ++ return ok; ++} ++ ++static int test_final_size_violation_fin_first(void) ++{ ++ unsigned char data[4096]; ++ unsigned char read_buf[4096]; ++ TEST_STREAM_CHUNK_T tsc_buf[3]; ++ OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 }; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ TEST_STREAM_CHUNK_T *tsc; ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; ++ unsigned int i; ++ unsigned int send_order[OSSL_NELEM(tsc_buf)]; ++ int ok = 0; ++ ++ if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))) ++ return 0; ++ ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))) ++ goto err; ++ ++ rstream = ossl_quic_rstream_new(NULL, NULL, rsqp); ++ if (!TEST_ptr(rstream)) ++ goto err; ++ ++ for (i = 0; i < sizeof(data); i++) ++ data[i] = FILL_PATTERN[i % (sizeof(FILL_PATTERN) - 1)]; ++ ++ memset(tsc_buf, 0, sizeof(tsc_buf)); ++ memset(read_buf, 0, sizeof(read_buf)); ++ ++ /* ++ * we start with 4-byte nibble which at offset 8. ++ */ ++ tsc = &tsc_buf[0]; ++ tsc->tsc_data = &data[10]; ++ tsc->tsc_off = 10; ++ tsc->tsc_len = 10; ++ tsc->tsc_fin = 1; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[0] = 0; ++ ++ tsc = &tsc_buf[1]; ++ tsc->tsc_data = &data[5]; ++ tsc->tsc_off = 5; ++ tsc->tsc_len = 5; ++ tsc->tsc_fin = 0; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 2; ++ send_order[1] = 1; ++ ++ tsc = &tsc_buf[2]; ++ tsc->tsc_data = &data[50]; ++ tsc->tsc_off = 20; ++ tsc->tsc_len = 10; ++ tsc->tsc_fin = 0; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 2; ++ send_order[2] = 2; ++ ++ for (i = 0; i < OSSL_NELEM(tsc_buf) - 1; i++) { ++ pkt[i] = pkt_test_new(1200); ++ if (!TEST_ptr(pkt[i])) ++ goto err; ++ tsc = &tsc_buf[send_order[i]]; ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[i], ++ tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, tsc->tsc_fin))) ++ goto err; ++ ++ /* ++ * check our assumptions about about reassemble process internals. ++ */ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream), ++ tsc->tsc_ranges_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream), ++ tsc->tsc_chunks_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ } ++ ++ pkt[i] = pkt_test_new(1200); ++ if (!TEST_ptr(pkt[i])) ++ goto err; ++ tsc = &tsc_buf[send_order[i]]; ++ if (!TEST_false(ossl_quic_rstream_queue_data(rstream, pkt[i], ++ tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, tsc->tsc_fin))) ++ goto err; ++ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream), ++ tsc->tsc_ranges_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream), ++ tsc->tsc_chunks_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ for (i = 0; i < OSSL_NELEM(tsc_buf) - 1; i++) ++ if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 2)) ++ goto err; ++ ++ if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) ++ goto err; ++ ++ if (!TEST_int_eq(ch->protocol_error, 1)) ++ goto err; ++ + ok = 1; + err: ++ /* ++ * the data from rstream has not been consumed, ++ * references to packets are still retained there. ++ * therefore we need to free rstream before freeing ++ * pkckets. ++ */ ++ ossl_quic_rstream_free(rstream); ++ + for (i = 0; i < OSSL_NELEM(tsc_buf); i++) + pkt_test_free(pkt[i]); + ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ ossl_quic_channel_free(ch); ++ ++ return ok; ++} ++ ++static int test_final_size_violation_data_first(void) ++{ ++ unsigned char data[4096]; ++ unsigned char read_buf[4096]; ++ TEST_STREAM_CHUNK_T tsc_buf[3]; ++ OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 }; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ TEST_STREAM_CHUNK_T *tsc; ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; ++ unsigned int i; ++ int ok = 0; ++ ++ if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))) ++ return 0; ++ ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))) ++ goto err; ++ ++ rstream = ossl_quic_rstream_new(NULL, NULL, rsqp); ++ if (!TEST_ptr(rstream)) ++ goto err; ++ ++ for (i = 0; i < sizeof(data); i++) ++ data[i] = FILL_PATTERN[i % (sizeof(FILL_PATTERN) - 1)]; ++ ++ memset(tsc_buf, 0, sizeof(tsc_buf)); ++ memset(read_buf, 0, sizeof(read_buf)); ++ ++ /* ++ * we start with 4-byte nibble which at offset 8. ++ */ ++ tsc = &tsc_buf[0]; ++ tsc->tsc_data = &data[10]; ++ tsc->tsc_off = 10; ++ tsc->tsc_len = 10; ++ tsc->tsc_fin = 0; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ ++ tsc = &tsc_buf[1]; ++ tsc->tsc_data = &data[5]; ++ tsc->tsc_off = 5; ++ tsc->tsc_len = 5; ++ tsc->tsc_fin = 1; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ ++ pkt[0] = pkt_test_new(1200); ++ if (!TEST_ptr(pkt[0])) ++ goto err; ++ tsc = &tsc_buf[0]; ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[0], ++ tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, tsc->tsc_fin))) ++ goto err; ++ ++ /* ++ * check our assumptions about about reassemble process internals. ++ */ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream), ++ tsc->tsc_ranges_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream), ++ tsc->tsc_chunks_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ pkt[1] = pkt_test_new(1200); ++ if (!TEST_ptr(pkt[1])) ++ goto err; ++ tsc = &tsc_buf[1]; ++ if (!TEST_false(ossl_quic_rstream_queue_data(rstream, pkt[1], ++ tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, tsc->tsc_fin))) ++ goto err; ++ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream), ++ tsc->tsc_ranges_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream), ++ tsc->tsc_chunks_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_size_t_eq(pkt_test_refcount(pkt[0]), 2)) ++ goto err; ++ ++ if (!TEST_size_t_eq(pkt_test_refcount(pkt[1]), 1)) ++ goto err; ++ ++ if (!TEST_int_eq(ch->protocol_error, 1)) ++ goto err; ++ ++ ok = 1; ++err: ++ /* ++ * the data from rstream has not been consumed, ++ * references to packets are still retained there. ++ * therefore we need to free rstream before freeing ++ * pkckets. ++ */ + ossl_quic_rstream_free(rstream); ++ ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) ++ pkt_test_free(pkt[i]); ++ + ossl_quic_rstream_qparm_destroy(rsqp); ++ ossl_quic_channel_free(ch); + + return ok; + } +@@ -1879,12 +2297,15 @@ int setup_tests(void) + ADD_TEST(test_rstream_pkt); + ADD_TEST(test_rstream_pkt_overhead); + ADD_ALL_TESTS(test_rstream_reorder, 40); ++ ADD_TEST(test_rstream_dstorage_two_sided_overlap); + ADD_TEST(test_rstream_chunk_partial_overlap); + ADD_TEST(test_rstream_chunk_full_overlap); + ADD_TEST(test_rstream_range_overlap); + ADD_TEST(test_rstream_prepend_byte_chunks); + ADD_TEST(test_rstream_append_byte_chunks); + ADD_TEST(test_rstream_mix_chunks); ++ ADD_TEST(test_final_size_violation_fin_first); ++ ADD_TEST(test_final_size_violation_data_first); + + return 1; + } diff -Nru openssl-3.5.7/debian/patches/Fix-out-of-bounds-valid_flags-access-after-SSL_set_SSL_CT.patch openssl-3.5.7/debian/patches/Fix-out-of-bounds-valid_flags-access-after-SSL_set_SSL_CT.patch --- openssl-3.5.7/debian/patches/Fix-out-of-bounds-valid_flags-access-after-SSL_set_SSL_CT.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/Fix-out-of-bounds-valid_flags-access-after-SSL_set_SSL_CT.patch 2026-09-29 20:10:10.000000000 +0000 @@ -0,0 +1,113 @@ +From: Matt Caswell +Date: Tue, 8 Sep 2026 13:34:53 +0100 +Subject: Fix out-of-bounds valid_flags access after SSL_set_SSL_CTX() + +SSL_new() sizes the connection-local signature algorithm state from the +SSL_CTX the connection was created from: sc->ssl_pkey_num counts the +built-in certificate slots plus one for each of that context's provider +TLS-SIGALG entries, and s3.tmp.valid_flags is later allocated to match. + +SSL_set_SSL_CTX() installs a duplicate of the replacement context's CERT +but leaves both of those describing the original context. Peer signature +algorithm codepoints are subsequently resolved against the replacement +context, where a provider sigalg's sig_idx is simply its position in +that context's list. If the replacement context advertises more provider +sigalgs than the original, a codepoint occupying one of the excess slots +yields an index past the end of valid_flags. + +The usual route is a switch from the servername callback, which runs +before tls1_set_server_sigalgs() allocates the buffer: the stale +ssl_pkey_num undersizes the allocation, and tls1_process_sigalgs() then +reads one 4-byte word past the end for each such codepoint the peer +offered, and writes CERT_PKEY_EXPLICIT_SIGN|CERT_PKEY_SIGN where that +word already reads zero. The peer chooses how many of these accesses +occur, and at which offsets, by selecting which codepoints to send. + +Refresh ssl_pkey_num from the newly installed CERT and, where a +valid_flags buffer already exists, replace it with one sized for that +context. A count which is too large is wrong in the same way as one that +is too small: loops bounded by ssl_pkey_num index cert->pkeys, which the +replacement context sizes. The replacement buffer is allocated before +the point at which the switch is committed, so that a failure can still +be reported rather than leaving the connection half switched. + +The built-in slots are copied across rather than zeroed. They may +already hold peer signature algorithm state which is not derived from +the SSL_CTX, and nothing recomputes it after a context switch: at TLS +1.2 and above tls1_check_chain() only ORs CERT_PKEY_SIGN and +CERT_PKEY_EXPLICIT_SIGN in from the existing value, and ssl_set_masks() +needs them to enable ECDSA, Ed25519 and Ed448. Zeroing them makes a +TLSv1.2 handshake with an ECDSA certificate fail with "no shared +cipher". The provider slots are positional and context specific, so they +are reset. + +Fixes CVE-2026-72897 + +Assisted-by: Claude Code:claude-opus-5[1m] +--- + ssl/ssl_lib.c | 32 ++++++++++++++++++++++++++++++++ + 1 file changed, 32 insertions(+) + +diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c +index 05b0209a76b3..73395b36ff25 100644 +--- a/ssl/ssl_lib.c ++++ b/ssl/ssl_lib.c +@@ -5460,6 +5460,7 @@ SSL_CTX *SSL_get_SSL_CTX(const SSL *ssl) + SSL_CTX *SSL_set_SSL_CTX(SSL *ssl, SSL_CTX *ctx) + { + CERT *new_cert; ++ uint32_t *new_valid_flags = NULL; + SSL_CONNECTION *sc = SSL_CONNECTION_FROM_SSL_ONLY(ssl); + + /* TODO(QUIC FUTURE): Add support for QUIC */ +@@ -5484,6 +5485,31 @@ SSL_CTX *SSL_set_SSL_CTX(SSL *ssl, SSL_CTX *ctx) + */ + if (!ossl_assert(sc->sid_ctx_length <= sizeof(sc->sid_ctx))) + goto err; ++ ++ /* ++ * |valid_flags| is sized from the number of signature algorithm slots of ++ * the SSL_CTX the connection was created from, so it must be resized for ++ * the replacement context. ++ * ++ * The built-in slots are indexed by the fixed SSL_PKEY_* constants and so ++ * mean the same thing in either context. They are preserved because they ++ * may already hold peer signature algorithm state which does not depend ++ * on the SSL_CTX. A provider slot index is instead a position in one ++ * context's provider list, so the same index denotes a different ++ * algorithm here and the old value cannot be carried over. They are reset ++ * rather than recomputed: recomputing them means recomputing the shared ++ * signature algorithms against the replacement context, which would let ++ * its preferences take effect on an established connection. ++ */ ++ if (sc->s3.tmp.valid_flags != NULL) { ++ new_valid_flags = OPENSSL_zalloc(new_cert->ssl_pkey_num ++ * sizeof(*new_valid_flags)); ++ if (new_valid_flags == NULL) ++ goto err; ++ memcpy(new_valid_flags, sc->s3.tmp.valid_flags, ++ SSL_PKEY_NUM * sizeof(*new_valid_flags)); ++ } ++ + if (!SSL_CTX_up_ref(ctx)) + goto err; + +@@ -5500,12 +5526,18 @@ SSL_CTX *SSL_set_SSL_CTX(SSL *ssl, SSL_CTX *ctx) + + ssl_cert_free(sc->cert); + sc->cert = new_cert; ++ sc->ssl_pkey_num = new_cert->ssl_pkey_num; ++ if (new_valid_flags != NULL) { ++ OPENSSL_free(sc->s3.tmp.valid_flags); ++ sc->s3.tmp.valid_flags = new_valid_flags; ++ } + SSL_CTX_free(ssl->ctx); /* decrement reference count */ + ssl->ctx = ctx; + + return ssl->ctx; + + err: ++ OPENSSL_free(new_valid_flags); + ssl_cert_free(new_cert); + return NULL; + } diff -Nru openssl-3.5.7/debian/patches/Guard-comparision-when-values-are-NULL.patch openssl-3.5.7/debian/patches/Guard-comparision-when-values-are-NULL.patch --- openssl-3.5.7/debian/patches/Guard-comparision-when-values-are-NULL.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/Guard-comparision-when-values-are-NULL.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,43 @@ +From: Norbert Pocs +Date: Thu, 27 Aug 2026 13:58:20 +0200 +Subject: Guard comparision when values are NULL + +Fixes: CVE-2026-75805 + +Signed-off-by: Norbert Pocs +--- + crypto/cmp/cmp_client.c | 15 +++++++++++---- + 1 file changed, 11 insertions(+), 4 deletions(-) + +diff --git a/crypto/cmp/cmp_client.c b/crypto/cmp/cmp_client.c +index d6a4230d243e..0e0bff09f8ff 100644 +--- a/crypto/cmp/cmp_client.c ++++ b/crypto/cmp/cmp_client.c +@@ -999,16 +999,23 @@ int OSSL_CMP_exec_RR_ses(OSSL_CMP_CTX *ctx) + ret = 0; + goto err; + } +- if (X509_NAME_cmp(issuer, OSSL_CRMF_CERTID_get0_issuer(cid)) != 0) { ++ /* ++ * The issuer and serial number are absent if the certificate to be ++ * revoked was given as a PKCS#10 CSR, in which case there is nothing ++ * to check the CertId of the response against. ++ */ ++ if (issuer != NULL ++ && X509_NAME_cmp(issuer, OSSL_CRMF_CERTID_get0_issuer(cid)) != 0) { + #ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + ERR_raise(ERR_LIB_CMP, CMP_R_WRONG_CERTID_IN_RP); + ret = 0; + goto err; + #endif + } +- if (ASN1_INTEGER_cmp(serial, +- OSSL_CRMF_CERTID_get0_serialNumber(cid)) +- != 0) { ++ if (serial != NULL ++ && ASN1_INTEGER_cmp(serial, ++ OSSL_CRMF_CERTID_get0_serialNumber(cid)) ++ != 0) { + #ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + ERR_raise(ERR_LIB_CMP, CMP_R_WRONG_SERIAL_IN_RP); + ret = 0; diff -Nru openssl-3.5.7/debian/patches/Limit-packet-buffer-overhead-to-64kB-per-stream.patch openssl-3.5.7/debian/patches/Limit-packet-buffer-overhead-to-64kB-per-stream.patch --- openssl-3.5.7/debian/patches/Limit-packet-buffer-overhead-to-64kB-per-stream.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/Limit-packet-buffer-overhead-to-64kB-per-stream.patch 2026-09-29 20:10:12.000000000 +0000 @@ -0,0 +1,1777 @@ +From: Alexandr Nedvedicky +Date: Mon, 27 Jul 2026 09:54:00 +0200 +Subject: Limit packet buffer overhead to ~64kB per stream. + +There is currently no limit on how many bytes of packet +buffers each stream can hold in memory. To monitor and limit +the size of packet buffers used by stream the change accounts +so called stream chunk overhead which is a delta between +actual datagram size of packet delivering the particular +stream chunk and chunk itself. As soon as the chunk overhead +exceeds ~64kB for all stream chunks kept in receive buffer, +the newly received chunks will be moved from packet buffer +to stream buffer. + +The packet buffer overhead limit is held in newly introduced +structure QUIC_RSTREAM_QPARAM (RX stream quality parameter). +If new additional quality parameters are added, then those +should be part of QUIC_RSTREAM_QPARAM structure. + +this changeset introduces QUIC_RSTREAM_QPARAM + +the the rsqp (reead stream quality parameter) enables +channel to control quality of RX stream. quality currently +means the packet buffer overhead. more parameters +may be added later. + +Fixes: CVE-2026-54873 +Reviewed-by: Tomas Mraz +Reviewed-by: Milan Broz +Merge-date: Sat Sep 26 11:53:40 2026 +Merged-from: https://github.com/openssl/openssl/pull/32769 +--- + include/internal/quic_predef.h | 1 + + include/internal/quic_stream.h | 5 +- + include/internal/quic_strm_reas.h | 3 +- + ssl/quic/quic_channel.c | 12 +- + ssl/quic/quic_channel_local.h | 6 + + ssl/quic/quic_rstream.c | 4 +- + ssl/quic/quic_strm_reas.c | 139 ++++- + test/quic_stream_test.c | 1139 +++++++++++++++++++++++++++++++++---- + test/quic_txp_test.c | 14 +- + 9 files changed, 1177 insertions(+), 146 deletions(-) + +diff --git a/include/internal/quic_predef.h b/include/internal/quic_predef.h +index c8d4ad470f58..92a1de534baa 100644 +--- a/include/internal/quic_predef.h ++++ b/include/internal/quic_predef.h +@@ -44,6 +44,7 @@ typedef struct quic_conn_st QUIC_CONNECTION; + typedef struct quic_xso_st QUIC_XSO; + typedef struct quic_listener_st QUIC_LISTENER; + typedef struct quic_domain_st QUIC_DOMAIN; ++typedef struct quic_rstream_qparm_st QUIC_RSTREAM_QPARM; + + #endif + +diff --git a/include/internal/quic_stream.h b/include/internal/quic_stream.h +index b9431831d054..2d0d7d947503 100644 +--- a/include/internal/quic_stream.h ++++ b/include/internal/quic_stream.h +@@ -320,7 +320,7 @@ void ossl_quic_sstream_set_cleanse(QUIC_SSTREAM *qss, int cleanse); + * is read by application. `statm` is queried for current rtt. + */ + QUIC_RSTREAM *ossl_quic_rstream_new(QUIC_RXFC *rxfc, +- OSSL_STATM *statm); ++ OSSL_STATM *statm, QUIC_RSTREAM_QPARM *rsqp); + + /* + * Frees a QUIC_RSTREAM and any associated storage. +@@ -407,6 +407,9 @@ size_t ossl_quic_rstream_get_chunk_count(QUIC_RSTREAM *qrs); + * returns the number of stream ranges kept in rstream + */ + size_t ossl_quic_rstream_get_range_count(QUIC_RSTREAM *qrs); ++ ++QUIC_RSTREAM_QPARM *ossl_quic_rstream_qparm_new(void); ++void ossl_quic_rstream_qparm_destroy(QUIC_RSTREAM_QPARM *rsqp); + #endif + + #endif +diff --git a/include/internal/quic_strm_reas.h b/include/internal/quic_strm_reas.h +index 1d1af12c74eb..a2d958a7adc6 100644 +--- a/include/internal/quic_strm_reas.h ++++ b/include/internal/quic_strm_reas.h +@@ -31,12 +31,13 @@ typedef struct sframe_set_t { + /* Cleanse data on release? */ + int cleanse; + int move_buffers; ++ QUIC_RSTREAM_QPARM *rsqp; + } SFRAME_SET; + + /* + * Initializes the stream frame list fs. + */ +-void ossl_sframe_set_init(SFRAME_SET *fs); ++void ossl_sframe_set_init(SFRAME_SET *fs, QUIC_RSTREAM_QPARM *rsqp); + + /* + * Destroys the stream frame list fs releasing any data +diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c +index 15b2ed22e900..f8851790ab2f 100644 +--- a/ssl/quic/quic_channel.c ++++ b/ssl/quic/quic_channel.c +@@ -337,8 +337,13 @@ static int ch_init(QUIC_CHANNEL *ch) + goto err; + } + ++ ch->rsqp = ossl_quic_rstream_qparm_new(); ++ if (ch->rsqp == NULL) ++ goto err; ++ + for (pn_space = QUIC_PN_SPACE_INITIAL; pn_space < QUIC_PN_SPACE_NUM; ++pn_space) { +- ch->crypto_recv[pn_space] = ossl_quic_rstream_new(NULL, NULL); ++ /* no quality control for crypto stream. */ ++ ch->crypto_recv[pn_space] = ossl_quic_rstream_new(NULL, NULL, ch->rsqp); + if (ch->crypto_recv[pn_space] == NULL) + goto err; + } +@@ -420,6 +425,9 @@ static void ch_cleanup(QUIC_CHANNEL *ch) + ossl_quic_rstream_free(ch->crypto_recv[pn_space]); + } + ++ ossl_quic_rstream_qparm_destroy(ch->rsqp); ++ ch->rsqp = NULL; ++ + ossl_qrx_pkt_release(ch->qrx_pkt); + ch->qrx_pkt = NULL; + +@@ -3774,7 +3782,7 @@ static int ch_init_new_stream(QUIC_CHANNEL *ch, QUIC_STREAM *qs, + goto err; + + if (can_recv) +- if ((qs->rstream = ossl_quic_rstream_new(NULL, NULL)) == NULL) ++ if ((qs->rstream = ossl_quic_rstream_new(NULL, NULL, ch->rsqp)) == NULL) + goto err; + + /* TXFC */ +diff --git a/ssl/quic/quic_channel_local.h b/ssl/quic/quic_channel_local.h +index eb082d6cea7a..385dbfb2c409 100644 +--- a/ssl/quic/quic_channel_local.h ++++ b/ssl/quic/quic_channel_local.h +@@ -501,6 +501,12 @@ struct quic_channel_st { + + /* Title for qlog purposes. We own this copy. */ + char *qlog_title; ++ ++ /* ++ * RX stream quality parameter. ++ */ ++ QUIC_RSTREAM_QPARM *rsqp; ++ + /* + * number of path responses waiting to be dispatched + * from control frame queue (CFQ) +diff --git a/ssl/quic/quic_rstream.c b/ssl/quic/quic_rstream.c +index a2dc038e4fe2..1ac496858b07 100644 +--- a/ssl/quic/quic_rstream.c ++++ b/ssl/quic/quic_rstream.c +@@ -28,14 +28,14 @@ struct quic_rstream_st { + #endif + + QUIC_RSTREAM *ossl_quic_rstream_new(QUIC_RXFC *rxfc, +- OSSL_STATM *statm) ++ OSSL_STATM *statm, QUIC_RSTREAM_QPARM *rsqp) + { + QUIC_RSTREAM *ret = OPENSSL_zalloc(sizeof(*ret)); + + if (ret == NULL) + return NULL; + +- ossl_sframe_set_init(&ret->fs); ++ ossl_sframe_set_init(&ret->fs, rsqp); + ret->rxfc = rxfc; + ret->statm = statm; + return ret; +diff --git a/ssl/quic/quic_strm_reas.c b/ssl/quic/quic_strm_reas.c +index 9e6bbe285a07..033c58ed6621 100644 +--- a/ssl/quic/quic_strm_reas.c ++++ b/ssl/quic/quic_strm_reas.c +@@ -9,6 +9,7 @@ + + #include "internal/uint_set.h" + #include "internal/common.h" ++#include "internal/quic_stream.h" + #include "internal/quic_strm_reas.h" + #include "internal/list.h" + +@@ -21,6 +22,13 @@ + + #define DIRECT_STORAGE_SZ (2 * sizeof(void *)) + ++/* ++ * Maximal allocation overhead in packet buffers is ~64kB for ++ * connection. If ~64kB limit is exceeded, then the newly received ++ * chunks are moved from the packet to the stream buffer. ++ */ ++#define PKT_BUFFER_OVERHEAD_TRESHOLD (65535) ++ + /* + * storage type indicates where stream data bytes + * are stored. +@@ -50,6 +58,11 @@ struct stream_chunk_t { + } sc_storage_u; + }; + ++struct quic_rstream_qparm_st { ++ size_t rsqp_pkt_overhead_treshold; ++ size_t rsqp_pkt_overhead_sz; ++}; ++ + #define sc_data sc_data_u.u_data + #define sc_data_w sc_data_u.u_data_w + +@@ -61,6 +74,7 @@ DEFINE_LIST_OF(sc, struct stream_chunk_t); + + #define SCHUNK_SIZE(_sc) ((_sc)->sc_range.end - (_sc)->sc_range.start) + #define SRANGE_SIZE(_sr) ((_sr)->sr_range.end - (_sr)->sr_range.start) ++#define SCHUNK_OVERHEAD(_pkt, _sc) ((_pkt)->datagram_len - SCHUNK_SIZE(_sc)) + + /* + * Stream range keeps list of continuous stream chunks. The range +@@ -88,6 +102,18 @@ OSSL_RBT_GENERATE(srange, stream_range_t, sr_rbe, srange_cmp); + + #define UINT64_TO_SIZE_T(_x) ((size_t)(((_x) > SIZE_MAX) ? SIZE_MAX : (_x))) + ++static void rsqp_add_overhead(QUIC_RSTREAM_QPARM *rsqp, size_t sc_overhead) ++{ ++ if (rsqp != NULL) ++ rsqp->rsqp_pkt_overhead_sz += sc_overhead; ++} ++ ++static void rsqp_sub_overhead(QUIC_RSTREAM_QPARM *rsqp, size_t sc_overhead) ++{ ++ if (rsqp != NULL) ++ rsqp->rsqp_pkt_overhead_sz -= sc_overhead; ++} ++ + /* + * Cleansing (SSL_OP_CLEANSE_PLAINTEXT) must write through the const + * data pointers received from ossl_sframe_set_insert(), which may +@@ -192,27 +218,9 @@ static int srange_cmp(const struct stream_range_t *a_sr, + static int keep_schunk_data_on_packet(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, + UINT_RANGE *r) + { +- /* +- * the function decides whether stream data should be moved +- * from packet buffer to stream buffer or if data can stay +- * at packet buffer. +- * +- * Keeping the data at packet saves yet another buffer +- * allocation at heap (+ data transfer). On the other hand +- * it opens door to malicious peer to force stack to use more +- * memory than necessary. +- * +- * The function here should asses a current stream quality: +- * how many stream chunks are there +- * the time elapsed since the arrival of earlier chunk +- * the time elapsed since the application consumed the data +- * the size of the chunk compared with the whole packet size +- * the size of chunk with respect to DIRECT_STORAGE_SZ +- * ... +- * the code to collect those parameters is still missing, once +- * this gap will be filled this function will be able to +- * make the decision. +- */ ++ if (fs->rsqp != NULL ++ && fs->rsqp->rsqp_pkt_overhead_sz >= fs->rsqp->rsqp_pkt_overhead_treshold) ++ return 0; + + return 1; + } +@@ -222,6 +230,7 @@ static struct stream_chunk_t *new_schunk(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, + { + struct stream_chunk_t *sc; + uint64_t rsize; ++ size_t overhead; + + if (pkt == NULL) + return NULL; +@@ -230,14 +239,29 @@ static struct stream_chunk_t *new_schunk(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, + if (sc == NULL) + return NULL; + ++ rsize = r->end - r->start; ++ assert(rsize <= pkt->datagram_len); ++ overhead = UINT64_TO_SIZE_T(pkt->datagram_len - rsize); ++ rsqp_add_overhead(fs->rsqp, overhead); ++ + if (keep_schunk_data_on_packet(fs, pkt, r) == 1) { + sc->sc_st = ST_TYPE_PKT; + sc->sc_pkt = pkt; + ossl_qrx_pkt_up_ref(pkt); + sc->sc_data = data; + sc->sc_range = *r; ++ if (fs->rsqp != NULL) ++ DEBUG_PRINT(stderr, ++ "%s sc: %p sc overhead: %d pkt_buf_overhead_sz: %zu -> %zu\n", ++ OPENSSL_FUNC, (void *)sc, SCHUNK_OVERHEAD(pkt, sc), ++ fs->rsqp->rsqp_pkt_overhead_sz - SCHUNK_OVERHEAD(pkt, sc), ++ fs->rsqp->rsqp_pkt_overhead_sz); + } else { +- rsize = r->end - r->start; ++ /* ++ * Only data which stay on packet must be accounted as overhead. ++ */ ++ rsqp_sub_overhead(fs->rsqp, overhead); ++ + if (rsize <= DIRECT_STORAGE_SZ) { + DEBUG_PRINT(stderr, "%s ST_TYPE_DIRECT sc: %p %llu\n", OPENSSL_FUNC, + (void *)sc, rsize); +@@ -277,6 +301,16 @@ static void destroy_schunk(SFRAME_SET *fs, struct stream_chunk_t *sc) + + switch (sc->sc_st) { + case ST_TYPE_PKT: ++ assert(fs->rsqp == NULL ++ || fs->rsqp->rsqp_pkt_overhead_sz >= SCHUNK_OVERHEAD(sc->sc_pkt, sc)); ++ if (fs->rsqp != NULL) ++ DEBUG_PRINT(stderr, ++ "%s sc: %p sc overhead: %d pkt_buf_overhead_sz: %zu -> %zu\n", ++ OPENSSL_FUNC, (void *)sc, SCHUNK_OVERHEAD(sc->sc_pkt, sc), ++ fs->rsqp->rsqp_pkt_overhead_sz, ++ fs->rsqp->rsqp_pkt_overhead_sz - SCHUNK_OVERHEAD(sc->sc_pkt, sc)); ++ rsqp_sub_overhead(fs->rsqp, ++ UINT64_TO_SIZE_T(SCHUNK_OVERHEAD(sc->sc_pkt, sc))); + ossl_qrx_pkt_release(sc->sc_pkt); + break; + case ST_TYPE_HEAP: +@@ -338,10 +372,11 @@ static struct stream_range_t *create_range(SFRAME_SET *fs, + return sr; + } + +-void ossl_sframe_set_init(SFRAME_SET *fs) ++void ossl_sframe_set_init(SFRAME_SET *fs, QUIC_RSTREAM_QPARM *rsqp) + { + memset(fs, 0, sizeof(*fs)); + OSSL_RBT_INIT(srange, &fs->ranges); ++ fs->rsqp = rsqp; + } + + static uint64_t get_sc_dstorage_sz(struct stream_chunk_t *sc) +@@ -715,6 +750,7 @@ static int chop_range(SFRAME_SET *fs, struct stream_range_t *sr, + uint64_t new_end) + { + struct stream_chunk_t *sc; ++ size_t unused_sz; + + assert(sr->sr_range.end >= new_end); + +@@ -744,6 +780,15 @@ static int chop_range(SFRAME_SET *fs, struct stream_range_t *sr, + sc->sc_range.end = new_end; + sr->sr_range.end = new_end; + ++ if (sc->sc_st == ST_TYPE_PKT) { ++ rsqp_add_overhead(fs->rsqp, unused_sz); ++ if (fs->rsqp != NULL) ++ DEBUG_PRINT(stderr, "%s sc: %p unused_sz: %zu %zu -> %zu\n", ++ OPENSSL_FUNC, (void *)sc, unused_sz, ++ fs->rsqp->rsqp_pkt_overhead_sz - unused_sz, ++ fs->rsqp->rsqp_pkt_overhead_sz); ++ } ++ + return 1; + } + +@@ -764,7 +809,8 @@ static struct stream_range_t *merge_ranges(SFRAME_SET *fs, + * sub_sr and super_sr are equal ranges (sets) super_sr + * sub_sr is subset of super_sr (super_sr includes sub_sr). + */ +- assert(super_sr->sr_range.start <= sub_sr->sr_range.start && super_sr->sr_range.end >= sub_sr->sr_range.end); ++ assert(super_sr->sr_range.start <= sub_sr->sr_range.start ++ && super_sr->sr_range.end >= sub_sr->sr_range.end); + + DEBUG_PRINT(stderr, "%s super: %p [ %llu, %llu ], sub: %p [ %llu, %llu]\n", + OPENSSL_FUNC, (void *)super_sr, super_sr->sr_range.start, +@@ -953,7 +999,7 @@ int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *r, OSSL_QRX_PKT *pkt, + (void *)sr, sr->sr_range.start, sr->sr_range.end); + + /* +- * sandwich, append, prepend can still be improved to handle ++ * Following calls can still be improved to handle + * chunks with direct storage better, but I don't think it's + * worth the effort. out of order short data chunks (less + * than DIRECT_STORAGE_SZ) should be considered exceptional. +@@ -1006,23 +1052,27 @@ int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *r, OSSL_QRX_PKT *pkt, + adjacent_sr->sr_range.end); + fs->stream_ranges--; + +- if (sr->sr_range.start <= adjacent_sr->sr_range.start && sr->sr_range.end >= adjacent_sr->sr_range.end) { ++ if (sr->sr_range.start <= adjacent_sr->sr_range.start ++ && sr->sr_range.end >= adjacent_sr->sr_range.end) { + /* + * adjacent_sr subset of sr + */ + joined_sr = merge_ranges(fs, sr, adjacent_sr); +- } else if (sr->sr_range.start >= adjacent_sr->sr_range.start && sr->sr_range.end <= adjacent_sr->sr_range.end) { ++ } else if (sr->sr_range.start >= adjacent_sr->sr_range.start ++ && sr->sr_range.end <= adjacent_sr->sr_range.end) { + /* + * sr subset of adjacent_sr + */ + joined_sr = merge_ranges(fs, adjacent_sr, sr); +- } else if (sr->sr_range.start < adjacent_sr->sr_range.start && sr->sr_range.end >= adjacent_sr->sr_range.start) { ++ } else if (sr->sr_range.start < adjacent_sr->sr_range.start ++ && sr->sr_range.end >= adjacent_sr->sr_range.start) { + /* + * adjacent_sr follows sr + */ + assert(sr->sr_range.end < adjacent_sr->sr_range.end); + joined_sr = append_range(fs, sr, adjacent_sr); +- } else if (sr->sr_range.start <= adjacent_sr->sr_range.end && sr->sr_range.end > adjacent_sr->sr_range.end) { ++ } else if (sr->sr_range.start <= adjacent_sr->sr_range.end ++ && sr->sr_range.end > adjacent_sr->sr_range.end) { + /* + * sr follows adjacent_sr + */ +@@ -1185,6 +1235,7 @@ int ossl_sframe_set_move_offset(SFRAME_SET *fs, uint64_t new_offset) + { + struct stream_range_t *sr = OSSL_RBT_MIN(srange, &fs->ranges); + struct stream_chunk_t *sc, *save_sc; ++ size_t unused_sz; + + if (new_offset == fs->offset) + return 1; +@@ -1231,6 +1282,15 @@ int ossl_sframe_set_move_offset(SFRAME_SET *fs, uint64_t new_offset) + sr->sr_range.start = new_offset; + DEBUG_PRINT(stderr, "[ %lli, %llu ]\n", + sr->sr_range.start, sr->sr_range.end); ++ ++ if (sc->sc_st == ST_TYPE_PKT) { ++ rsqp_add_overhead(fs->rsqp, unused_sz); ++ if (fs->rsqp != NULL) ++ DEBUG_PRINT(stderr, "%s sc: %p unused_sz: %zu %zu -> %zu\n", ++ OPENSSL_FUNC, (void *)sc, unused_sz, ++ fs->rsqp->rsqp_pkt_overhead_sz - unused_sz, ++ fs->rsqp->rsqp_pkt_overhead_sz); ++ } + } + + return 1; +@@ -1249,3 +1309,24 @@ int ossl_sframe_set_avail(SFRAME_SET *fs, uint64_t *avail, int *fin) + *fin = (fs->fin && fs->offset + *avail == fs->fin_off) ? 1 : 0; + return 1; + } ++ ++QUIC_RSTREAM_QPARM *ossl_quic_rstream_qparm_new(void) ++{ ++ QUIC_RSTREAM_QPARM *rsqp; ++ ++ rsqp = OPENSSL_malloc(sizeof(QUIC_RSTREAM_QPARM)); ++ if (rsqp != NULL) { ++ rsqp->rsqp_pkt_overhead_treshold = PKT_BUFFER_OVERHEAD_TRESHOLD; ++ rsqp->rsqp_pkt_overhead_sz = 0; ++ } ++ ++ return rsqp; ++} ++ ++void ossl_quic_rstream_qparm_destroy(QUIC_RSTREAM_QPARM *rsqp) ++{ ++ if (rsqp != NULL) { ++ assert(rsqp->rsqp_pkt_overhead_sz == 0); ++ OPENSSL_free(rsqp); ++ } ++} +diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c +index b5fd755a54d9..93a745eaff86 100644 +--- a/test/quic_stream_test.c ++++ b/test/quic_stream_test.c +@@ -6,10 +6,13 @@ + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ ++#include ++ + #include "internal/packet.h" + #include "internal/quic_record_rx.h" + #include "internal/quic_stream.h" + #include "../ssl/quic/quic_record_rx_local.h" ++#include "internal/nelem.h" + #include "testutil.h" + + /* +@@ -403,116 +406,13 @@ static int test_single_copy_read(QUIC_RSTREAM *qrs, + return 1; + } + +-static const unsigned char simple_data[] = "Hello world! And thank you for all the fish!"; +- +-static int test_rstream_simple(int idx) +-{ +- QUIC_RSTREAM *rstream = NULL; +- OSSL_QRX_PKT *pkt[8] = { NULL }; +- int ret = 0; +- unsigned char buf[sizeof(simple_data)]; +- size_t readbytes = 0, avail = 0, i; +- int fin = 0; +- int use_sc = idx % 2; +- int (*read_fn)(QUIC_RSTREAM *, unsigned char *, size_t, size_t *, +- int *) +- = use_sc ? test_single_copy_read +- : ossl_quic_rstream_read; +- +- /* every frame arrives in a packet, as it does in production */ +- for (i = 0; i < OSSL_NELEM(pkt); ++i) +- if (!TEST_ptr(pkt[i] = pkt_test_new(1200))) +- goto err; +- +- if (!TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL))) +- goto err; +- +- if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[0], 5, +- simple_data + 5, 10, 0)) +- || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[1], +- sizeof(simple_data) - 1, +- simple_data + sizeof(simple_data) - 1, +- 1, 1)) +- || !TEST_true(ossl_quic_rstream_peek(rstream, buf, sizeof(buf), +- &readbytes, &fin)) +- || !TEST_false(fin) +- || !TEST_size_t_eq(readbytes, 0) +- || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[2], +- sizeof(simple_data) - 10, +- simple_data + sizeof(simple_data) - 10, +- 10, 1)) +- || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[3], 0, +- simple_data, 1, 0)) +- || !TEST_true(ossl_quic_rstream_peek(rstream, buf, sizeof(buf), +- &readbytes, &fin)) +- || !TEST_false(fin) +- || !TEST_size_t_eq(readbytes, 1) +- || !TEST_mem_eq(buf, 1, simple_data, 1) +- || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[4], +- 0, simple_data, +- 10, 0)) +- || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[5], +- sizeof(simple_data), +- NULL, +- 0, 1)) +- || !TEST_true(ossl_quic_rstream_peek(rstream, buf, sizeof(buf), +- &readbytes, &fin)) +- || !TEST_false(fin) +- || !TEST_size_t_eq(readbytes, 15) +- || !TEST_mem_eq(buf, 15, simple_data, 15) +- || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[6], +- 15, +- simple_data + 15, +- sizeof(simple_data) - 15, 1)) +- || !TEST_true(ossl_quic_rstream_available(rstream, &avail, &fin)) +- || !TEST_true(fin) +- || !TEST_size_t_eq(avail, sizeof(simple_data)) +- || !TEST_true(read_fn(rstream, buf, 2, &readbytes, &fin)) +- || !TEST_false(fin) +- || !TEST_size_t_eq(readbytes, 2) +- || !TEST_mem_eq(buf, 2, simple_data, 2) +- || !TEST_true(read_fn(rstream, buf + 2, 12, &readbytes, &fin)) +- || !TEST_false(fin) +- || !TEST_size_t_eq(readbytes, 12) +- || !TEST_mem_eq(buf + 2, 12, simple_data + 2, 12) +- || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[7], +- sizeof(simple_data), +- NULL, +- 0, 1)) +- || !TEST_true(read_fn(rstream, buf + 14, 5, &readbytes, &fin)) +- || !TEST_false(fin) +- || !TEST_size_t_eq(readbytes, 5) +- || !TEST_mem_eq(buf, 14 + 5, simple_data, 14 + 5) +- || !TEST_true(read_fn(rstream, buf + 14 + 5, sizeof(buf) - 14 - 5, +- &readbytes, &fin)) +- || !TEST_true(fin) +- || !TEST_size_t_eq(readbytes, sizeof(buf) - 14 - 5) +- || !TEST_mem_eq(buf, sizeof(buf), simple_data, sizeof(simple_data)) +- || !TEST_true(read_fn(rstream, buf, sizeof(buf), &readbytes, &fin)) +- || !TEST_true(fin) +- || !TEST_size_t_eq(readbytes, 0)) +- goto err; +- +- ret = 1; +- +-err: +- ossl_quic_rstream_free(rstream); +- /* All the references held by the stream must have been released */ +- for (i = 0; i < OSSL_NELEM(pkt); ++i) { +- if (pkt[i] != NULL +- && !TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) +- ret = 0; +- pkt_test_free(pkt[i]); +- } +- return ret; +-} +- + static int test_rstream_random(int idx) + { + unsigned char *bulk_data = NULL; + unsigned char *read_buf = NULL; + QUIC_RSTREAM *rstream = NULL; + OSSL_QRX_PKT **pkts = NULL; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; + size_t i, read_off, queued_min, queued_max, num_pkts = 0; + const size_t data_size = 10000; + /* At most two frames are queued per each of the 100 * 10 iterations */ +@@ -524,7 +424,8 @@ static int test_rstream_random(int idx) + if (!TEST_ptr(bulk_data = OPENSSL_malloc(data_size)) + || !TEST_ptr(read_buf = OPENSSL_malloc(data_size)) + || !TEST_ptr(pkts = OPENSSL_zalloc(sizeof(*pkts) * max_pkts)) +- || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) + goto err; + + if (idx % 3 == 0) +@@ -644,6 +545,7 @@ static int test_rstream_random(int idx) + } + OPENSSL_free(pkts); + } ++ ossl_quic_rstream_qparm_destroy(rsqp); + OPENSSL_free(bulk_data); + OPENSSL_free(read_buf); + return ret; +@@ -656,6 +558,7 @@ static int test_rstream_random(int idx) + static int test_rstream_pkt(void) + { + QUIC_RSTREAM *rstream = NULL; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; + OSSL_QRX_PKT *pkt_a = NULL, *pkt_b = NULL, *pkt_c = NULL; + unsigned char pdata[64], cbuf[64], buf[64]; + size_t readbytes = 0, avail = 0, i; +@@ -668,7 +571,8 @@ static int test_rstream_pkt(void) + if (!TEST_ptr(pkt_a = pkt_test_new(1200)) + || !TEST_ptr(pkt_b = pkt_test_new(1200)) + || !TEST_ptr(pkt_c = pkt_test_new(1200)) +- || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, 0))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) + goto err; + + /* A buffered frame holds a reference to its packet */ +@@ -742,7 +646,7 @@ static int test_rstream_pkt(void) + * data, leaving the surrounding bytes intact. + */ + memset(cbuf, 0xAA, sizeof(cbuf)); +- if (!TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, 0))) ++ if (!TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) + goto err; + ossl_quic_rstream_set_cleanse(rstream, 1); + if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_a, 0, +@@ -761,6 +665,7 @@ static int test_rstream_pkt(void) + + err: + ossl_quic_rstream_free(rstream); ++ ossl_quic_rstream_qparm_destroy(rsqp); + pkt_test_free(pkt_a); + pkt_test_free(pkt_b); + pkt_test_free(pkt_c); +@@ -777,6 +682,7 @@ static int test_rstream_pkt_overhead(void) + { + QUIC_RSTREAM *rstream = NULL; + OSSL_QRX_PKT **pkt = NULL; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; + unsigned char *data = NULL, *buf = NULL; + const size_t framesz = 8; + const size_t nframes = 4096; /* far past a 64 KiB overhead limit */ +@@ -788,7 +694,8 @@ static int test_rstream_pkt_overhead(void) + if (!TEST_ptr(data = OPENSSL_malloc(total)) + || !TEST_ptr(buf = OPENSSL_malloc(total)) + || !TEST_ptr(pkt = OPENSSL_zalloc(nframes * sizeof(*pkt))) +- || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, 0))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) + goto err; + + for (i = 0; i < total; ++i) +@@ -836,6 +743,7 @@ static int test_rstream_pkt_overhead(void) + if (pkt != NULL) + for (i = 0; i < nframes; ++i) + pkt_test_free(pkt[i]); ++ ossl_quic_rstream_qparm_destroy(rsqp); + OPENSSL_free(pkt); + OPENSSL_free(data); + OPENSSL_free(buf); +@@ -857,6 +765,7 @@ static int test_rstream_reorder(int idx) + unsigned char *data = NULL, *buf = NULL, *arena = NULL, *ap; + QUIC_RSTREAM *rstream = NULL; + OSSL_QRX_PKT **pkts = NULL; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; + const size_t data_size = 4096; + const size_t framesz = 1 + (size_t)(idx % 17); + const int cleanse = (idx & 1); +@@ -870,7 +779,8 @@ static int test_rstream_reorder(int idx) + || !TEST_ptr(arena = OPENSSL_malloc(3 * data_size)) + || !TEST_ptr(order = OPENSSL_malloc(nframes * sizeof(*order))) + || !TEST_ptr(pkts = OPENSSL_zalloc(2 * nframes * sizeof(*pkts))) +- || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, 0))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) + goto err; + + if (cleanse) +@@ -951,6 +861,7 @@ static int test_rstream_reorder(int idx) + } + OPENSSL_free(pkts); + } ++ ossl_quic_rstream_qparm_destroy(rsqp); + OPENSSL_free(order); + OPENSSL_free(arena); + OPENSSL_free(data); +@@ -958,14 +869,1022 @@ static int test_rstream_reorder(int idx) + return ret; + } + ++#define FILL_PATTERN "abcdefghijklmnopqrstuvwxyz0123456789" \ ++ "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ++ ++typedef struct test_stream_chunk { ++ const unsigned char *tsc_data; ++ uint64_t tsc_off; /* start == offset */ ++ uint64_t tsc_len; /* end = offset + len */ ++ int tsc_fin; ++ size_t tsc_chunks_exp; ++ size_t tsc_ranges_exp; ++} TEST_STREAM_CHUNK_T; ++ ++static int test_rstream_chunk_partial_overlap(void) ++{ ++ unsigned char data[4096]; ++ unsigned char read_buf[4096]; ++ TEST_STREAM_CHUNK_T tsc_buf[7]; ++ OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 }; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ TEST_STREAM_CHUNK_T *tsc; ++ QUIC_RSTREAM *rstream; ++ size_t readbytes; ++ unsigned int i; ++ unsigned int send_order[7]; ++ int fin = 0; ++ int ok = 0; ++ ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())) ++ return 0; ++ ++ rstream = ossl_quic_rstream_new(NULL, NULL, rsqp); ++ if (!TEST_ptr(rstream)) ++ goto err; ++ ++ for (i = 0; i < sizeof(data); i++) ++ data[i] = FILL_PATTERN[i % (sizeof(FILL_PATTERN) - 1)]; ++ ++ memset(tsc_buf, 0, sizeof(tsc_buf)); ++ memset(read_buf, 0, sizeof(read_buf)); ++ ++ /* ++ * 1 range, (0, 120) with 5 stream chunks. there is a partial overlap ++ * between chunks. ++ */ ++ tsc = &tsc_buf[0]; ++ tsc->tsc_data = &data[0]; ++ tsc->tsc_off = 0; ++ tsc->tsc_len = 32; ++ tsc->tsc_chunks_exp = 5; ++ tsc->tsc_ranges_exp = 1; ++ send_order[6] = 0; ++ ++ tsc = &tsc_buf[1]; ++ tsc->tsc_data = &data[24]; ++ tsc->tsc_off = 24; ++ tsc->tsc_len = 48; ++ tsc->tsc_chunks_exp = 5; ++ tsc->tsc_ranges_exp = 1; ++ send_order[4] = 1; ++ ++ tsc = &tsc_buf[2]; ++ tsc->tsc_data = &data[44]; ++ tsc->tsc_off = 44; ++ tsc->tsc_len = 20; ++ tsc->tsc_chunks_exp = 4; ++ tsc->tsc_ranges_exp = 1; ++ send_order[3] = 2; ++ ++ tsc = &tsc_buf[3]; ++ tsc->tsc_data = &data[55]; ++ tsc->tsc_off = 55; ++ tsc->tsc_len = 50; ++ tsc->tsc_chunks_exp = 1; ++ tsc->tsc_ranges_exp = 1; ++ send_order[0] = 3; ++ ++ tsc = &tsc_buf[4]; ++ tsc->tsc_data = &data[100]; ++ tsc->tsc_off = 100; ++ tsc->tsc_len = 19; ++ tsc->tsc_chunks_exp = 3; ++ tsc->tsc_ranges_exp = 1; ++ send_order[2] = 4; ++ ++ tsc = &tsc_buf[5]; ++ tsc->tsc_data = &data[119]; ++ tsc->tsc_off = 119; ++ tsc->tsc_len = 1; ++ tsc->tsc_fin = 1; ++ tsc->tsc_chunks_exp = 2; ++ tsc->tsc_ranges_exp = 2; ++ send_order[1] = 5; ++ ++ /* ++ * add duplicate chunk, the chunk range 48, 64 exists already ++ * in the range, thus no additional stream chunk will be created. ++ */ ++ tsc = &tsc_buf[6]; ++ tsc->tsc_data = &data[49]; ++ tsc->tsc_off = 48; ++ tsc->tsc_len = 16; ++ tsc->tsc_chunks_exp = 5; ++ tsc->tsc_ranges_exp = 1; ++ send_order[5] = 6; ++ ++ /* ++ * send everything except offset 0. ++ */ ++ assert(OSSL_NELEM(tsc_buf) == OSSL_NELEM(pkt)); ++ for (i = 0; i < OSSL_NELEM(tsc_buf) - 1; i++) { ++ pkt[i] = pkt_test_new(1200); ++ if (!TEST_ptr(pkt[i])) ++ goto err; ++ tsc = &tsc_buf[send_order[i]]; ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[i], ++ tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, tsc->tsc_fin))) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ /* ++ * check our assumptions about about reassemble process internals. ++ */ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream), ++ tsc->tsc_ranges_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream), ++ tsc->tsc_chunks_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ /* ++ * the offset 0 chunk is not transmitted in loop here, ++ * make sure the stream does not become readable. ++ */ ++ if (!TEST_true(ossl_quic_rstream_peek(rstream, read_buf, ++ sizeof(read_buf), &readbytes, &fin))) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_false(fin)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_size_t_eq(readbytes, 0)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ } ++ ++ /* ++ * although 6 chunks were inserted, we expect to find only 5 chunks ++ * in range, the last chunk was duplicate. ++ */ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream), 5)) ++ goto err; ++ ++ /* ++ * send offset 0 chunk, and try to read from stream. ++ */ ++ pkt[i] = pkt_test_new(1200); ++ if (!TEST_ptr(pkt[i])) ++ goto err; ++ tsc = &tsc_buf[0]; ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[i], ++ tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, tsc->tsc_fin))) ++ goto err; ++ ++ /* ++ * writing chunk offset 0 makes stream readable ++ */ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, read_buf, ++ sizeof(read_buf), &readbytes, &fin))) ++ goto err; ++ ++ /* ++ * we expect to read 120 bytes ++ */ ++ if (!TEST_uint64_t_eq(readbytes, 120)) ++ goto err; ++ ++ /* ++ * the fin written by loop should be signaled too. ++ */ ++ if (!TEST_true(fin)) ++ goto err; ++ ++ if (!TEST_mem_eq(read_buf, readbytes, data, readbytes)) ++ goto err; ++ ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) ++ if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) ++ goto err; ++ ++ ok = 1; ++err: ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) ++ pkt_test_free(pkt[i]); ++ ++ ossl_quic_rstream_free(rstream); ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ ++ return ok; ++} ++ ++/* ++ * use 64kB as datagram size to force QUIC stack ++ * to rach overhead threshold for all packets used ++ * by test. So all data will be moved to stream buffers ++ */ ++#define MOVE_TRESHOLD 65535 * 2 ++ ++static int test_rstream_chunk_full_overlap(void) ++{ ++ unsigned char data[4096]; ++ unsigned char read_buf[4096]; ++ TEST_STREAM_CHUNK_T tsc_buf[5]; ++ OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 }; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ TEST_STREAM_CHUNK_T *tsc; ++ QUIC_RSTREAM *rstream; ++ size_t readbytes; ++ unsigned int i; ++ unsigned int send_order[5]; ++ int fin; ++ int ok = 0; ++ ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())) ++ return 0; ++ ++ rstream = ossl_quic_rstream_new(NULL, NULL, rsqp); ++ if (!TEST_ptr(rstream)) ++ goto err; ++ ++ for (i = 0; i < sizeof(data); i++) ++ data[i] = FILL_PATTERN[i % (sizeof(FILL_PATTERN) - 1)]; ++ ++ memset(tsc_buf, 0, sizeof(tsc_buf)); ++ memset(read_buf, 0, sizeof(read_buf)); ++ ++ /* ++ * 1 range, (0, 256) with 5 stream chunks. 5 chunks overlap ++ * partially, The last chunk we insert overlaps the whole range. ++ */ ++ tsc = &tsc_buf[0]; ++ tsc->tsc_data = &data[0]; ++ tsc->tsc_off = 0; ++ tsc->tsc_len = 256; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[4] = 0; ++ ++ tsc = &tsc_buf[1]; ++ tsc->tsc_data = &data[24]; ++ tsc->tsc_off = 24; ++ tsc->tsc_len = 48; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 4; ++ send_order[3] = 1; ++ ++ tsc = &tsc_buf[2]; ++ tsc->tsc_data = &data[44]; ++ tsc->tsc_off = 44; ++ tsc->tsc_len = 20; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 3; ++ send_order[2] = 2; ++ ++ tsc = &tsc_buf[3]; ++ tsc->tsc_data = &data[55]; ++ tsc->tsc_off = 55; ++ tsc->tsc_len = 50; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[0] = 3; ++ ++ tsc = &tsc_buf[4]; ++ tsc->tsc_data = &data[100]; ++ tsc->tsc_off = 100; ++ tsc->tsc_len = 20; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 2; ++ send_order[1] = 4; ++ ++ assert(OSSL_NELEM(tsc_buf) == OSSL_NELEM(pkt)); ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) { ++ pkt[i] = pkt_test_new(MOVE_TRESHOLD); ++ if (!TEST_ptr(pkt[i])) ++ goto err; ++ tsc = &tsc_buf[send_order[i]]; ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[i], ++ tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, 0))) ++ goto err; ++ ++ /* ++ * check our assumptions about about reassemble process internals. ++ */ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream), ++ tsc->tsc_ranges_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream), ++ tsc->tsc_chunks_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ } ++ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, read_buf, ++ sizeof(read_buf), &readbytes, &fin))) ++ goto err; ++ ++ if (!TEST_false(fin)) ++ goto err; ++ ++ /* ++ * we expect to read 256 bytes ++ */ ++ if (!TEST_uint64_t_eq(readbytes, 256)) ++ goto err; ++ ++ if (!TEST_mem_eq(read_buf, readbytes, data, readbytes)) ++ goto err; ++ ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) ++ if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) ++ goto err; ++ ++ ok = 1; ++err: ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) ++ pkt_test_free(pkt[i]); ++ ++ ossl_quic_rstream_free(rstream); ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ ++ return ok; ++} ++ ++static int test_rstream_range_overlap(void) ++{ ++ unsigned char data[4096]; ++ unsigned char read_buf[4096]; ++ TEST_STREAM_CHUNK_T tsc_buf[9]; ++ OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 }; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ TEST_STREAM_CHUNK_T *tsc; ++ QUIC_RSTREAM *rstream; ++ size_t readbytes; ++ unsigned int i; ++ int fin; ++ int ok = 0; ++ ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())) ++ return 0; ++ ++ rstream = ossl_quic_rstream_new(NULL, NULL, rsqp); ++ if (!TEST_ptr(rstream)) ++ goto err; ++ ++ for (i = 0; i < sizeof(data); i++) ++ data[i] = FILL_PATTERN[i % (sizeof(FILL_PATTERN) - 1)]; ++ ++ memset(tsc_buf, 0, sizeof(tsc_buf)); ++ memset(read_buf, 0, sizeof(read_buf)); ++ ++ /* ++ * start with 5 ranges, ++ */ ++ tsc = &tsc_buf[0]; ++ tsc->tsc_data = &data[0]; ++ tsc->tsc_off = 0; ++ tsc->tsc_len = 64; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ ++ tsc = &tsc_buf[1]; ++ tsc->tsc_data = &data[128]; ++ tsc->tsc_off = 128; ++ tsc->tsc_len = 64; ++ tsc->tsc_ranges_exp = 2; ++ tsc->tsc_chunks_exp = 2; ++ ++ tsc = &tsc_buf[2]; ++ tsc->tsc_data = &data[256]; ++ tsc->tsc_off = 256; ++ tsc->tsc_len = 64; ++ tsc->tsc_ranges_exp = 3; ++ tsc->tsc_chunks_exp = 3; ++ ++ tsc = &tsc_buf[3]; ++ tsc->tsc_data = &data[384]; ++ tsc->tsc_off = 384; ++ tsc->tsc_len = 64; ++ tsc->tsc_ranges_exp = 4; ++ tsc->tsc_chunks_exp = 4; ++ ++ tsc = &tsc_buf[4]; ++ tsc->tsc_data = &data[512]; ++ tsc->tsc_off = 512; ++ tsc->tsc_len = 64; ++ tsc->tsc_ranges_exp = 5; ++ tsc->tsc_chunks_exp = 5; ++ ++ /* ++ * chunk 6 appends data to last range ++ */ ++ tsc = &tsc_buf[5]; ++ tsc->tsc_data = &data[548]; ++ tsc->tsc_off = 548; ++ tsc->tsc_len = 220; ++ tsc->tsc_ranges_exp = 5; ++ tsc->tsc_chunks_exp = 6; ++ ++ /* ++ * chunk 7 prepends data to last range ++ */ ++ tsc = &tsc_buf[6]; ++ tsc->tsc_data = &data[480]; ++ tsc->tsc_off = 480; ++ tsc->tsc_len = 64; ++ tsc->tsc_chunks_exp = 7; ++ tsc->tsc_ranges_exp = 5; ++ ++ /* ++ * chunk 8 fully covers range 4 and partially ++ * overlaps with 5 ++ */ ++ tsc = &tsc_buf[7]; ++ tsc->tsc_data = &data[364]; ++ tsc->tsc_off = 364; ++ tsc->tsc_len = 500; ++ /* ++ * note the expected number of chunks actually decreases!!! ++ * here is what happened: ++ * chunk [ 364, 864 ] is going to be inserted into range number 4 ++ * which spans over [ 384, 448 ]. After chunk is inserted the ++ * 4th range looks as follows: ++ * [ 364, 864 ], it contains 3 chunks: ++ * [ 364, 384 ] ++ * [ 384, 448 ] ++ * [ 448, 864 ] ++ * ++ * however the 4th range now overlaps with 5th range [ 480, 768 ]. ++ * the fifth range also contains 3 chunks: ++ * [ 480, 768 ] ++ * [ 480, 512 ] ++ * [ 512, 576 ] ++ * [ 576, 768 ] ++ * as you can see there is a full overlap. The new range is going ++ * to look as: ++ * [ 364, 864 ] ++ * [ 364, 384 ] ++ * [ 384, 448 ] ++ * [ 448, 864 ] ++ * the 5th range is gone with all its ranges. the ranges 1, 2 and 3 ++ * where not touched so far, each of them contain one range, this ++ * makes total 6 ranges. ++ */ ++ tsc->tsc_chunks_exp = 4; ++ tsc->tsc_ranges_exp = 4; ++ ++ /* ++ * chunk 9 partially overlaps with the first and ++ * the last range, ++ */ ++ tsc = &tsc_buf[8]; ++ tsc->tsc_data = &data[32]; ++ tsc->tsc_off = 32; ++ tsc->tsc_len = 500; ++ tsc->tsc_chunks_exp = 3; ++ tsc->tsc_ranges_exp = 1; ++ ++ assert(OSSL_NELEM(tsc_buf) == OSSL_NELEM(pkt)); ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) { ++ pkt[i] = pkt_test_new(1200); ++ if (!TEST_ptr(pkt[i])) ++ goto err; ++ tsc = &tsc_buf[i]; ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[i], ++ tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, 0))) ++ goto err; ++ ++ /* ++ * check our assumptions about about reassemble process internals. ++ */ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream), ++ tsc->tsc_ranges_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream), ++ tsc->tsc_chunks_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ } ++ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, read_buf, ++ sizeof(read_buf), &readbytes, &fin))) ++ goto err; ++ ++ if (!TEST_false(fin)) ++ goto err; ++ ++ /* ++ * we expect to read 864 bytes ++ */ ++ if (!TEST_uint64_t_eq(readbytes, 864)) ++ goto err; ++ ++ if (!TEST_mem_eq(read_buf, readbytes, data, readbytes)) ++ goto err; ++ ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) ++ if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) ++ goto err; ++ ++ ok = 1; ++err: ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) ++ pkt_test_free(pkt[i]); ++ ++ ossl_quic_rstream_free(rstream); ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ ++ return ok; ++} ++ ++static int test_rstream_prepend_byte_chunks(void) ++{ ++ unsigned char data[4096]; ++ unsigned char read_buf[4096]; ++ TEST_STREAM_CHUNK_T tsc_buf[6]; ++ OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 }; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ TEST_STREAM_CHUNK_T *tsc; ++ QUIC_RSTREAM *rstream; ++ size_t readbytes; ++ unsigned int i; ++ unsigned int send_order[6]; ++ int fin; ++ int ok = 0; ++ ++ if (sizeof(void *) != 8) { ++ TEST_info("%s is implemented for 64-bit platforms only", OPENSSL_FUNC); ++ return 1; ++ } ++ ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())) ++ return 0; ++ ++ rstream = ossl_quic_rstream_new(NULL, NULL, rsqp); ++ if (!TEST_ptr(rstream)) ++ goto err; ++ ++ for (i = 0; i < sizeof(data); i++) ++ data[i] = FILL_PATTERN[i % (sizeof(FILL_PATTERN) - 1)]; ++ ++ memset(tsc_buf, 0, sizeof(tsc_buf)); ++ memset(read_buf, 0, sizeof(read_buf)); ++ ++ /* ++ * here we test one byte stream chunks. this test verifies ++ * more short stream chunks are stored in single chunk buffer. ++ */ ++ tsc = &tsc_buf[0]; ++ tsc->tsc_data = &data[0]; ++ tsc->tsc_off = 0; ++ tsc->tsc_len = 1; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[4] = 0; ++ ++ tsc = &tsc_buf[1]; ++ tsc->tsc_data = &data[1]; ++ tsc->tsc_off = 1; ++ tsc->tsc_len = 2; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[3] = 1; ++ ++ tsc = &tsc_buf[2]; ++ tsc->tsc_data = &data[3]; ++ tsc->tsc_off = 3; ++ tsc->tsc_len = 5; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[2] = 2; ++ ++ tsc = &tsc_buf[3]; ++ tsc->tsc_data = &data[8]; ++ tsc->tsc_off = 8; ++ tsc->tsc_len = 4; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[1] = 3; ++ ++ tsc = &tsc_buf[4]; ++ tsc->tsc_data = &data[12]; ++ tsc->tsc_off = 12; ++ tsc->tsc_len = 4; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[0] = 4; ++ ++ tsc = &tsc_buf[5]; ++ tsc->tsc_data = &data[12]; ++ tsc->tsc_off = 12; ++ tsc->tsc_len = 12; ++ tsc->tsc_ranges_exp = 1; ++ /* ++ * this chunk partially overlaps. It does not fit to stream chunk buffer ++ * created earlier, therefore a new stream chunk will be created. ++ */ ++ tsc->tsc_chunks_exp = 2; ++ send_order[5] = 5; ++ ++ assert(OSSL_NELEM(tsc_buf) == OSSL_NELEM(pkt)); ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) { ++ pkt[i] = pkt_test_new(MOVE_TRESHOLD); ++ if (!TEST_ptr(pkt[i])) ++ goto err; ++ tsc = &tsc_buf[send_order[i]]; ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[i], ++ tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, 0))) ++ goto err; ++ ++ /* ++ * check our assumptions about about reassemble process internals. ++ */ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream), ++ tsc->tsc_ranges_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream), ++ tsc->tsc_chunks_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ } ++ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, read_buf, ++ sizeof(read_buf), &readbytes, &fin))) ++ goto err; ++ ++ if (!TEST_false(fin)) ++ goto err; ++ ++ /* ++ * we expect to read 24 bytes ++ */ ++ if (!TEST_uint64_t_eq(readbytes, 24)) ++ goto err; ++ ++ if (!TEST_mem_eq(read_buf, readbytes, data, readbytes)) ++ goto err; ++ ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) ++ if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) ++ goto err; ++ ++ ok = 1; ++err: ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) ++ pkt_test_free(pkt[i]); ++ ++ ossl_quic_rstream_free(rstream); ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ ++ return ok; ++} ++ ++static int test_rstream_append_byte_chunks(void) ++{ ++ unsigned char data[4096]; ++ unsigned char read_buf[4096]; ++ TEST_STREAM_CHUNK_T tsc_buf[6]; ++ OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 }; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ TEST_STREAM_CHUNK_T *tsc; ++ QUIC_RSTREAM *rstream; ++ size_t readbytes; ++ unsigned int i; ++ unsigned int send_order[6]; ++ int fin; ++ int ok = 0; ++ ++ if (sizeof(void *) != 8) { ++ TEST_info("%s is implemented for 64-bit platforms only", OPENSSL_FUNC); ++ return 1; ++ } ++ ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())) ++ return 0; ++ ++ rstream = ossl_quic_rstream_new(NULL, NULL, rsqp); ++ if (!TEST_ptr(rstream)) ++ goto err; ++ ++ for (i = 0; i < sizeof(data); i++) ++ data[i] = FILL_PATTERN[i % (sizeof(FILL_PATTERN) - 1)]; ++ ++ memset(tsc_buf, 0, sizeof(tsc_buf)); ++ memset(read_buf, 0, sizeof(read_buf)); ++ ++ /* ++ * here we test one byte stream chunks. this test verifies ++ * more short stream chunks are stored in single chunk buffer. ++ */ ++ tsc = &tsc_buf[0]; ++ tsc->tsc_data = &data[0]; ++ tsc->tsc_off = 0; ++ tsc->tsc_len = 1; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[0] = 0; ++ ++ tsc = &tsc_buf[1]; ++ tsc->tsc_data = &data[1]; ++ tsc->tsc_off = 1; ++ tsc->tsc_len = 2; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[1] = 1; ++ ++ tsc = &tsc_buf[2]; ++ tsc->tsc_data = &data[3]; ++ tsc->tsc_off = 3; ++ tsc->tsc_len = 5; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[2] = 2; ++ ++ tsc = &tsc_buf[3]; ++ tsc->tsc_data = &data[8]; ++ tsc->tsc_off = 8; ++ tsc->tsc_len = 4; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[3] = 3; ++ ++ tsc = &tsc_buf[4]; ++ tsc->tsc_data = &data[12]; ++ tsc->tsc_off = 12; ++ tsc->tsc_len = 4; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[4] = 4; ++ ++ tsc = &tsc_buf[5]; ++ tsc->tsc_data = &data[12]; ++ tsc->tsc_off = 12; ++ tsc->tsc_len = 12; ++ /* ++ * this chunk partially overlaps. It does not fit to stream chunk buffer ++ * created by for() loop above, therefore a new stream chunk will be created. ++ */ ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 2; ++ send_order[5] = 5; ++ ++ assert(OSSL_NELEM(tsc_buf) == OSSL_NELEM(pkt)); ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) { ++ pkt[i] = pkt_test_new(MOVE_TRESHOLD); ++ if (!TEST_ptr(pkt[i])) ++ goto err; ++ tsc = &tsc_buf[send_order[i]]; ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[i], ++ tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, 0))) ++ goto err; ++ ++ /* ++ * check our assumptions about about reassemble process internals. ++ */ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream), ++ tsc->tsc_ranges_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream), ++ tsc->tsc_chunks_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ } ++ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, read_buf, ++ sizeof(read_buf), &readbytes, &fin))) ++ goto err; ++ ++ /* ++ * we expect to read 24 bytes ++ */ ++ if (!TEST_uint64_t_eq(readbytes, 24)) ++ goto err; ++ ++ if (!TEST_false(fin)) ++ goto err; ++ ++ if (!TEST_mem_eq(read_buf, readbytes, data, readbytes)) ++ goto err; ++ ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) ++ if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) ++ goto err; ++ ++ ok = 1; ++err: ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) ++ pkt_test_free(pkt[i]); ++ ++ ossl_quic_rstream_free(rstream); ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ ++ return ok; ++} ++ ++static int test_rstream_mix_chunks(void) ++{ ++ unsigned char data[4096]; ++ unsigned char read_buf[4096]; ++ TEST_STREAM_CHUNK_T tsc_buf[7]; ++ OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 }; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ TEST_STREAM_CHUNK_T *tsc; ++ QUIC_RSTREAM *rstream; ++ size_t readbytes; ++ unsigned int i; ++ unsigned int send_order[7]; ++ int fin; ++ int ok = 0; ++ ++ if (sizeof(void *) != 8) { ++ TEST_info("%s is implemented for 64-bit platforms only", OPENSSL_FUNC); ++ return 1; ++ } ++ ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())) ++ return 0; ++ ++ rstream = ossl_quic_rstream_new(NULL, NULL, rsqp); ++ if (!TEST_ptr(rstream)) ++ goto err; ++ ++ for (i = 0; i < sizeof(data); i++) ++ data[i] = FILL_PATTERN[i % (sizeof(FILL_PATTERN) - 1)]; ++ ++ memset(tsc_buf, 0, sizeof(tsc_buf)); ++ memset(read_buf, 0, sizeof(read_buf)); ++ ++ /* ++ * we start with 4-byte nibble which at offset 8. ++ */ ++ tsc = &tsc_buf[0]; ++ tsc->tsc_data = &data[8]; ++ tsc->tsc_off = 8; ++ tsc->tsc_len = 4; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[0] = 0; ++ ++ /* ++ * the next 3 byte nibble partially overlaps with ++ * earlier one. it adds 1 byte. it is prepended ++ */ ++ tsc = &tsc_buf[1]; ++ tsc->tsc_data = &data[7]; ++ tsc->tsc_off = 7; ++ tsc->tsc_len = 3; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[1] = 1; ++ ++ /* ++ * the next 3 byte nibble partially overlaps with ++ * range. it effectively adds 1 byte to end. ++ */ ++ tsc = &tsc_buf[2]; ++ tsc->tsc_data = &data[10]; ++ tsc->tsc_off = 10; ++ tsc->tsc_len = 3; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[2] = 2; ++ ++ /* ++ * append nibble that fully overlaps with range ++ * (the new nibble is superset of existing range) ++ * it effectively adds two bytes ++ */ ++ tsc = &tsc_buf[3]; ++ tsc->tsc_data = &data[6]; ++ tsc->tsc_off = 6; ++ tsc->tsc_len = 8; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 1; ++ send_order[3] = 3; ++ ++ /* ++ * prepend the nibble which starts yet another range. ++ */ ++ tsc = &tsc_buf[4]; ++ tsc->tsc_data = &data[0]; ++ tsc->tsc_off = 0; ++ tsc->tsc_len = 1; ++ tsc->tsc_ranges_exp = 2; ++ tsc->tsc_chunks_exp = 2; ++ send_order[4] = 4; ++ ++ /* ++ * nibble here appends bytes to right range. the range count and chunk ++ * count must not change as new data still fit to dstorage. ++ */ ++ tsc = &tsc_buf[5]; ++ tsc->tsc_data = &data[14]; ++ tsc->tsc_off = 14; ++ tsc->tsc_len = 3; ++ tsc->tsc_ranges_exp = 2; ++ tsc->tsc_chunks_exp = 2; ++ send_order[5] = 5; ++ ++ /* ++ * send chunk that overlaps everything ++ */ ++ tsc = &tsc_buf[6]; ++ tsc->tsc_data = &data[0]; ++ tsc->tsc_off = 0; ++ tsc->tsc_len = 24; ++ tsc->tsc_ranges_exp = 1; ++ tsc->tsc_chunks_exp = 2; ++ send_order[6] = 6; ++ ++ /* ++ * all nibbles we've sent so far must fit to single range. ++ */ ++ assert(OSSL_NELEM(tsc_buf) == OSSL_NELEM(pkt)); ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) { ++ pkt[i] = pkt_test_new(MOVE_TRESHOLD); ++ if (!TEST_ptr(pkt[i])) ++ goto err; ++ tsc = &tsc_buf[send_order[i]]; ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[i], ++ tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, 0))) ++ goto err; ++ ++ /* ++ * check our assumptions about about reassemble process internals. ++ */ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream), ++ tsc->tsc_ranges_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream), ++ tsc->tsc_chunks_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ } ++ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, read_buf, ++ sizeof(read_buf), &readbytes, &fin))) ++ goto err; ++ ++ if (!TEST_uint64_t_eq(readbytes, 24)) ++ goto err; ++ ++ if (!TEST_false(fin)) ++ goto err; ++ ++ if (!TEST_mem_eq(read_buf, readbytes, data, readbytes)) ++ goto err; ++ ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) ++ if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) ++ goto err; ++ ++ ok = 1; ++err: ++ for (i = 0; i < OSSL_NELEM(tsc_buf); i++) ++ pkt_test_free(pkt[i]); ++ ++ ossl_quic_rstream_free(rstream); ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ ++ return ok; ++} ++ + int setup_tests(void) + { + ADD_TEST(test_sstream_simple); + ADD_ALL_TESTS(test_sstream_bulk, 100); +- ADD_ALL_TESTS(test_rstream_simple, 4); + ADD_ALL_TESTS(test_rstream_random, 100); + ADD_TEST(test_rstream_pkt); + ADD_TEST(test_rstream_pkt_overhead); + ADD_ALL_TESTS(test_rstream_reorder, 40); ++ ADD_TEST(test_rstream_chunk_partial_overlap); ++ ADD_TEST(test_rstream_chunk_full_overlap); ++ ADD_TEST(test_rstream_range_overlap); ++ ADD_TEST(test_rstream_prepend_byte_chunks); ++ ADD_TEST(test_rstream_append_byte_chunks); ++ ADD_TEST(test_rstream_mix_chunks); ++ + return 1; + } +diff --git a/test/quic_txp_test.c b/test/quic_txp_test.c +index 010297235ce7..f79b588f28be 100644 +--- a/test/quic_txp_test.c ++++ b/test/quic_txp_test.c +@@ -11,6 +11,8 @@ + #include "internal/quic_statm.h" + #include "internal/quic_demux.h" + #include "internal/quic_record_rx.h" ++#include "internal/quic_channel.h" ++#include "../ssl/quic/quic_channel_local.h" + #include "testutil.h" + #include "quic_record_test_util.h" + +@@ -1303,10 +1305,18 @@ static int run_script(int script_idx, const struct script_op *script) + struct helper h; + const struct script_op *op; + size_t opn = 0; ++ QUIC_CHANNEL *ch = NULL; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; + + if (!helper_init(&h)) + goto err; + ++ if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))) ++ goto err; ++ ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))) ++ goto err; ++ + have_helper = 1; + for (op = script, opn = 0; op->opcode != OPK_END; ++op, ++opn) { + switch (op->opcode) { +@@ -1500,7 +1510,7 @@ static int run_script(int script_idx, const struct script_op *script) + 16 * 1024 * 1024, + fake_now, NULL)) + || !TEST_ptr(s->rstream = ossl_quic_rstream_new(&s->rxfc, +- NULL))) { ++ NULL, rsqp))) { + ossl_quic_sstream_free(s->sstream); + ossl_quic_stream_map_release(h.args.qsm, s); + goto err; +@@ -1598,6 +1608,8 @@ static int run_script(int script_idx, const struct script_op *script) + TEST_error("script %d failed at op %zu", script_idx + 1, opn + 1); + if (have_helper) + helper_cleanup(&h); ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ ossl_quic_channel_free(ch); + return testresult; + } + diff -Nru openssl-3.5.7/debian/patches/Make-the-ecp_sm2p256-scalar-multiplication-constant-time.patch openssl-3.5.7/debian/patches/Make-the-ecp_sm2p256-scalar-multiplication-constant-time.patch --- openssl-3.5.7/debian/patches/Make-the-ecp_sm2p256-scalar-multiplication-constant-time.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/Make-the-ecp_sm2p256-scalar-multiplication-constant-time.patch 2026-09-29 20:10:10.000000000 +0000 @@ -0,0 +1,339 @@ +From: Igor Ustinov +Date: Mon, 3 Aug 2026 16:56:53 +0200 +Subject: Make the ecp_sm2p256 scalar multiplication constant time + +Fixes CVE-2026-54875. + +This fix implemets the same idea as in PR#30649 by Joshua Rogers (@MegaManSec). + +Assisted-by: Claude:claude-opus-4-8 +--- + crypto/ec/ecp_sm2p256.c | 249 +++++++++++++++++++++++++++++++----------------- + 1 file changed, 161 insertions(+), 88 deletions(-) + +diff --git a/crypto/ec/ecp_sm2p256.c b/crypto/ec/ecp_sm2p256.c +index 5cb5f1be9948..e53e7ec5440b 100644 +--- a/crypto/ec/ecp_sm2p256.c ++++ b/crypto/ec/ecp_sm2p256.c +@@ -171,22 +171,35 @@ static ossl_inline void ecp_sm2p256_mod_inverse(BN_ULONG *out, + BN_MOD_INV(out, in, ecp_sm2p256_div_by_2, ecp_sm2p256_sub, def_p); + } + +-/* Point double: R <- P + P */ +-static void ecp_sm2p256_point_double(P256_POINT *R, const P256_POINT *P) ++/* ++ * Constant-time conditional copy: r = mask ? a : r, where mask is either 0 or ++ * all-ones. Used to select point-addition results without branching. ++ */ ++static ossl_inline void ecp_sm2p256_cond_copy(P256_POINT *r, ++ const P256_POINT *a, BN_ULONG mask) + { + unsigned int i; ++ ++ for (i = 0; i < P256_LIMBS; ++i) { ++ r->X[i] = constant_time_select_64(mask, a->X[i], r->X[i]); ++ r->Y[i] = constant_time_select_64(mask, a->Y[i], r->Y[i]); ++ r->Z[i] = constant_time_select_64(mask, a->Z[i], r->Z[i]); ++ } ++} ++ ++/* ++ * Point double: R <- P + P ++ * ++ * Branch-free: the doubling formula already produces Z = 0 (the point at ++ * infinity) when the input Z is 0 (R->Z = 2*Y*Z), and the resulting X, Y are ++ * irrelevant for a Z = 0 point, so no is_zeros(P->Z) special case is needed. ++ */ ++static void ecp_sm2p256_point_double(P256_POINT *R, const P256_POINT *P) ++{ + ALIGN32 BN_ULONG tmp0[P256_LIMBS]; + ALIGN32 BN_ULONG tmp1[P256_LIMBS]; + ALIGN32 BN_ULONG tmp2[P256_LIMBS]; + +- /* zero-check P->Z */ +- if (is_zeros(P->Z)) { +- for (i = 0; i < P256_LIMBS; ++i) +- R->Z[i] = 0; +- +- return; +- } +- + ecp_sm2p256_sqr(tmp0, P->Z); + ecp_sm2p256_sub(tmp1, P->X, tmp0); + ecp_sm2p256_add(tmp0, P->X, tmp0); +@@ -208,6 +221,13 @@ static void ecp_sm2p256_point_double(P256_POINT *R, const P256_POINT *P) + } + + /* Point add affine: R <- P + Q */ ++/* ++ * NB: this function is deliberately NOT constant time. It is used only to ++ * precompute the table of small multiples of the *public* input point (see ++ * ecp_sm2p256_point_P_mul_by_scalar); the secret scalar never flows through ++ * it, so its identity-dependent branches cannot leak it. Keeping the fast ++ * branchy formula here avoids the constant-time overhead on the table build. ++ */ + static void ecp_sm2p256_point_add_affine(P256_POINT *R, const P256_POINT *P, + const P256_POINT_AFFINE *Q) + { +@@ -274,107 +294,164 @@ static void ecp_sm2p256_point_add_affine(P256_POINT *R, const P256_POINT *P, + static void ecp_sm2p256_point_add(P256_POINT *R, const P256_POINT *P, + const P256_POINT *Q) + { +- unsigned int i; + ALIGN32 BN_ULONG tmp0[P256_LIMBS] = { 0 }; + ALIGN32 BN_ULONG tmp1[P256_LIMBS] = { 0 }; + ALIGN32 BN_ULONG tmp2[P256_LIMBS] = { 0 }; +- +- /* zero-check P | Q ->Z */ +- if (is_zeros(P->Z)) { +- for (i = 0; i < P256_LIMBS; ++i) { +- R->X[i] = Q->X[i]; +- R->Y[i] = Q->Y[i]; +- R->Z[i] = Q->Z[i]; +- } +- +- return; +- } else if (is_zeros(Q->Z)) { +- for (i = 0; i < P256_LIMBS; ++i) { +- R->X[i] = P->X[i]; +- R->Y[i] = P->Y[i]; +- R->Z[i] = P->Z[i]; +- } +- +- return; +- } else if (is_point_equal(P, Q)) { +- ecp_sm2p256_point_double(R, Q); +- +- return; +- } +- ++ P256_POINT sum, dbl, res; ++ BN_ULONG p_inf, q_inf, is_dbl; ++ ++ p_inf = is_zeros(P->Z); ++ q_inf = is_zeros(Q->Z); ++ ++ /* ++ * General P + Q addition into |sum|, valid unless P or Q is infinity or ++ * P == +-Q. tmp0 = H and tmp1 = R are the coordinate differences: P and Q ++ * are the same point iff both are zero; P == -Q iff only H is zero (the ++ * formula then yields Z = 0, i.e. infinity, on its own). ++ */ + ecp_sm2p256_sqr(tmp0, P->Z); + ecp_sm2p256_mul(tmp1, tmp0, P->Z); + ecp_sm2p256_mul(tmp0, tmp0, Q->X); + ecp_sm2p256_mul(tmp1, tmp1, Q->Y); +- ecp_sm2p256_mul(R->Y, P->Y, Q->Z); +- ecp_sm2p256_mul(R->Z, Q->Z, P->Z); ++ ecp_sm2p256_mul(sum.Y, P->Y, Q->Z); ++ ecp_sm2p256_mul(sum.Z, Q->Z, P->Z); + ecp_sm2p256_sqr(tmp2, Q->Z); +- ecp_sm2p256_mul(R->Y, tmp2, R->Y); +- ecp_sm2p256_mul(R->X, tmp2, P->X); +- ecp_sm2p256_sub(tmp0, tmp0, R->X); +- ecp_sm2p256_mul(R->Z, tmp0, R->Z); +- ecp_sm2p256_sub(tmp1, tmp1, R->Y); ++ ecp_sm2p256_mul(sum.Y, tmp2, sum.Y); ++ ecp_sm2p256_mul(sum.X, tmp2, P->X); ++ ecp_sm2p256_sub(tmp0, tmp0, sum.X); ++ ecp_sm2p256_mul(sum.Z, tmp0, sum.Z); ++ ecp_sm2p256_sub(tmp1, tmp1, sum.Y); ++ is_dbl = is_zeros(tmp0) & is_zeros(tmp1); + ecp_sm2p256_sqr(tmp2, tmp0); + ecp_sm2p256_mul(tmp0, tmp0, tmp2); +- ecp_sm2p256_mul(tmp2, tmp2, R->X); +- ecp_sm2p256_sqr(R->X, tmp1); +- ecp_sm2p256_sub(R->X, R->X, tmp2); +- ecp_sm2p256_sub(R->X, R->X, tmp2); +- ecp_sm2p256_sub(R->X, R->X, tmp0); +- ecp_sm2p256_sub(tmp2, tmp2, R->X); ++ ecp_sm2p256_mul(tmp2, tmp2, sum.X); ++ ecp_sm2p256_sqr(sum.X, tmp1); ++ ecp_sm2p256_sub(sum.X, sum.X, tmp2); ++ ecp_sm2p256_sub(sum.X, sum.X, tmp2); ++ ecp_sm2p256_sub(sum.X, sum.X, tmp0); ++ ecp_sm2p256_sub(tmp2, tmp2, sum.X); + ecp_sm2p256_mul(tmp2, tmp1, tmp2); +- ecp_sm2p256_mul(tmp0, tmp0, R->Y); +- ecp_sm2p256_sub(R->Y, tmp2, tmp0); ++ ecp_sm2p256_mul(tmp0, tmp0, sum.Y); ++ ecp_sm2p256_sub(sum.Y, tmp2, tmp0); ++ ++ /* 2*P, for the P == Q case. */ ++ ecp_sm2p256_point_double(&dbl, P); ++ ++ /* ++ * Select the result without branching, in increasing priority: ++ * default -> sum (distinct points; also P == -Q which gives infinity) ++ * is_dbl -> dbl (P == Q) ++ * q_inf -> P (Q is infinity) ++ * p_inf -> Q (P is infinity; highest priority) ++ * All reads of P and Q happen before R is written, so R may alias P or Q. ++ */ ++ memcpy(&res, &sum, sizeof(res)); ++ ecp_sm2p256_cond_copy(&res, &dbl, is_dbl); ++ ecp_sm2p256_cond_copy(&res, P, q_inf); ++ ecp_sm2p256_cond_copy(&res, Q, p_inf); ++ memcpy(R, &res, sizeof(res)); + } + + #if !defined(OPENSSL_NO_SM2_PRECOMP) + /* Base point mul by scalar: k - scalar, G - base point */ ++/* ++ * Constant-time gather of the affine entry |index| from a 256-entry sub-table. ++ * |sub| points to the sub-table for one 8-bit comb window; entry v is stored ++ * as X[P256_LIMBS] followed by Y[P256_LIMBS] at sub + v * (2 * P256_LIMBS). ++ * Entry 0 is not stored, so index 0 yields the all-zero (X, Y) placeholder, ++ * which the caller turns into the point at infinity. ++ */ ++static void ecp_sm2p256_select_G(P256_POINT_AFFINE *R, const BN_ULONG *sub, ++ unsigned int index) ++{ ++ unsigned int v, j; ++ ++ memset(R, 0, sizeof(*R)); ++ for (v = 1; v < 256; ++v) { ++ BN_ULONG mask = constant_time_is_zero_64((BN_ULONG)(index ^ v)); ++ const BN_ULONG *e = sub + (size_t)v * (2 * P256_LIMBS); ++ ++ for (j = 0; j < P256_LIMBS; ++j) { ++ R->X[j] |= constant_time_select_64(mask, e[j], 0); ++ R->Y[j] |= constant_time_select_64(mask, e[P256_LIMBS + j], 0); ++ } ++ } ++} ++ ++/* ++ * R = k*G using the precomputed comb. Constant-time: every 8-bit window is ++ * gathered from its full 256-entry sub-table with a mask and lifted to a ++ * Jacobian point whose Z is 1 for a non-zero window and 0 (infinity) for a ++ * zero window, so the unconditional, branch-free addition contributes nothing ++ * for a zero window and no secret-dependent branch or table index remains. ++ */ + static void ecp_sm2p256_point_G_mul_by_scalar(P256_POINT *R, const BN_ULONG *k) + { +- unsigned int i, index, mask = 0xff; +- P256_POINT_AFFINE Q; ++ unsigned int i, j, index; ++ P256_POINT_AFFINE Qaff; ++ P256_POINT QJ; + + memset(R, 0, sizeof(P256_POINT)); + +- if (is_zeros(k)) +- return; ++ for (i = 0; i < 32; ++i) { ++ index = (k[i / 8] >> (8 * (i % 8))) & 0xff; ++ ecp_sm2p256_select_G(&Qaff, ++ ecp_sm2p256_precomputed + (size_t)i * 256 * (2 * P256_LIMBS), ++ index); + +- index = k[0] & mask; +- if (index) { +- index = index * 8; +- memcpy(R->X, ecp_sm2p256_precomputed + index, 32); +- memcpy(R->Y, ecp_sm2p256_precomputed + index + P256_LIMBS, 32); +- R->Z[0] = 1; ++ { ++ /* Z = 1 iff the window is non-zero, else 0 (point at infinity). */ ++ BN_ULONG nz = ~constant_time_is_zero_64((BN_ULONG)index); ++ ++ for (j = 0; j < P256_LIMBS; ++j) { ++ QJ.X[j] = Qaff.X[j]; ++ QJ.Y[j] = Qaff.Y[j]; ++ QJ.Z[j] = 0; ++ } ++ QJ.Z[0] = nz & 1; ++ } ++ ecp_sm2p256_point_add(R, R, &QJ); + } ++} ++#endif ++ ++/* ++ * Constant-time gather of |index| from a 16-entry table of Jacobian points. ++ * Index 0 yields the all-zero point (Z = 0, i.e. the point at infinity). ++ */ ++static void ecp_sm2p256_select_P(P256_POINT *R, const P256_POINT tbl[16], ++ unsigned int index) ++{ ++ unsigned int i, j; + +- for (i = 1; i < 32; ++i) { +- index = (k[i / 8] >> (8 * (i % 8))) & mask; ++ memset(R, 0, sizeof(*R)); ++ for (i = 1; i < 16; ++i) { ++ BN_ULONG mask = constant_time_is_zero_64((BN_ULONG)(index ^ i)); + +- if (index) { +- index = index + i * 256; +- index = index * 8; +- memcpy(Q.X, ecp_sm2p256_precomputed + index, 32); +- memcpy(Q.Y, ecp_sm2p256_precomputed + index + P256_LIMBS, 32); +- ecp_sm2p256_point_add_affine(R, R, &Q); ++ for (j = 0; j < P256_LIMBS; ++j) { ++ R->X[j] |= constant_time_select_64(mask, tbl[i].X[j], 0); ++ R->Y[j] |= constant_time_select_64(mask, tbl[i].Y[j], 0); ++ R->Z[j] |= constant_time_select_64(mask, tbl[i].Z[j], 0); + } + } + } +-#endif + + /* + * Affine point mul by scalar: k - scalar, P - affine point ++ * ++ * Constant-time windowed multiplication: every 4-bit window is processed ++ * uniformly with four doublings followed by a masked table gather and an ++ * unconditional, branch-free addition. There is no init/skip logic and no ++ * secret-dependent table index, so the running time and memory-access pattern ++ * do not depend on the value of the secret scalar. + */ + static void ecp_sm2p256_point_P_mul_by_scalar(P256_POINT *R, const BN_ULONG *k, + P256_POINT_AFFINE P) + { +- int i, init = 0; ++ int i; + unsigned int index, mask = 0x0f; + ALIGN64 P256_POINT precomputed[16]; +- +- memset(R, 0, sizeof(P256_POINT)); +- +- if (is_zeros(k)) +- return; ++ P256_POINT T; + + /* The first value of the precomputed table is P. */ + memcpy(precomputed[1].X, P.X, 32); +@@ -391,22 +468,18 @@ static void ecp_sm2p256_point_P_mul_by_scalar(P256_POINT *R, const BN_ULONG *k, + for (i = 3; i < 16; ++i) + ecp_sm2p256_point_add_affine(&precomputed[i], &precomputed[i - 1], &P); + ++ memset(R, 0, sizeof(*R)); /* R = point at infinity */ ++ + for (i = 64 - 1; i >= 0; --i) { + index = (k[i / 16] >> (4 * (i % 16))) & mask; + +- if (init == 0) { +- if (index) { +- memcpy(R, &precomputed[index], sizeof(P256_POINT)); +- init = 1; +- } +- } else { +- ecp_sm2p256_point_double(R, R); +- ecp_sm2p256_point_double(R, R); +- ecp_sm2p256_point_double(R, R); +- ecp_sm2p256_point_double(R, R); +- if (index) +- ecp_sm2p256_point_add(R, R, &precomputed[index]); +- } ++ ecp_sm2p256_point_double(R, R); ++ ecp_sm2p256_point_double(R, R); ++ ecp_sm2p256_point_double(R, R); ++ ecp_sm2p256_point_double(R, R); ++ ++ ecp_sm2p256_select_P(&T, precomputed, index); ++ ecp_sm2p256_point_add(R, R, &T); + } + } + diff -Nru openssl-3.5.7/debian/patches/New-implementation-of-stream-reassembly-for-QUIC.patch openssl-3.5.7/debian/patches/New-implementation-of-stream-reassembly-for-QUIC.patch --- openssl-3.5.7/debian/patches/New-implementation-of-stream-reassembly-for-QUIC.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/New-implementation-of-stream-reassembly-for-QUIC.patch 2026-09-29 20:10:12.000000000 +0000 @@ -0,0 +1,2408 @@ +From: Alexandr Nedvedicky +Date: Mon, 10 Aug 2026 10:49:47 +0200 +Subject: New implementation of stream reassembly for QUIC. +MIME-Version: 1.0 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 8bit + +The new implementation makes clear distinction between stream chunk +and stream range. + +The strem chunk is defined by its `start` and `end` offset with +respect to offset 0 (the start of the stream). The `start` < `end`. +The lenght of the stream chunk is `end - start`. The stream chunks +are delivered as QUIC stream frames. + +The stream range is list of stream chunks which together create +one continuous range of stream. The range is also deined by +`start` and `end` offset. The ranges are kept in R/B tree. + +The chunks which are arriving in order are kept in list which +forms one node of R/B tree. If newly arriving stream chunk +can not be inserted to existing stream range for example because +chunk.start > range.end (there is a gap between existing range and +new chunk), then the new range is created and inserted to R/B +tree. + +If the newly arriving chunk closes the gap between two existing +ranges, the ranges are merged to single tree node. The join +proces uses list join operation with O(1) complexity to make +the new continuous range of stream chunks. + +In a nutshell: stream reassembly process is R/B tree look up/insert +with tail/head insertion to list. + +There is also a preparatory work that will allow us to deal with +memory hog issue. Currently the QUIC stack keeps all stream +frame data on packet buffers until data is moved to application. +On hone hand, this saves one copy opration between on the other +hand it may cause receiver to hold lot more memory than currently +needed. Consider situation where QUIC stack needs to hold reference +to whole packet buffer, just because of sinle 1byte stream chunk. +The 1byte stream chunk (at let's say offset 10) can not be moved +to application yet, because QUIC stack is waiting for data at +offset 0 - 9. Such buffer might be waiting in reassembly queue +for a long time holding a reference to whole packet. + +To mitigate this we need to allow the QUIC stack to move data +from packet buffers to stream buffers. The logic which decides +when data should be moved from packet buffers is missing in this +change. Here we just bring the code that supports improved +buffer handling. The data for short stream frames/chunks (16B) +are kept inside the stream chunk structure itself (this is referred +as direct storage, or dstorage). The code coalesces data from adjacent +stream frames into dstorage until it fill up. Once dstorage is full the +new stream chunk is allocated and added to range. + +Larger stream chunks (size > 16B) get buffer from the heap. +It is then linked to stream buffer. + +This commit just brings the quic_strm_reas.c in. It is not currently +hooked to build. + +Fixes: CVE-2026-42772 + +Co-authored-by: Tomáš Mráz + +Co-authored-by: Jakub Zelenka + +Co-authored-by: Mounir IDRASSI +Reviewed-by: Tomas Mraz +Reviewed-by: Milan Broz +Merge-date: Sat Sep 26 11:53:39 2026 +Merged-from: https://github.com/openssl/openssl/pull/32769 +--- + include/internal/quic_sf_list.h | 151 ----- + include/internal/quic_stream.h | 38 +- + include/internal/quic_strm_reas.h | 89 +++ + ssl/quic/build.info | 2 +- + ssl/quic/quic_channel.c | 4 +- + ssl/quic/quic_rstream.c | 133 ++-- + ssl/quic/quic_sf_list.c | 334 ---------- + ssl/quic/quic_strm_reas.c | 1251 +++++++++++++++++++++++++++++++++++++ + test/quic_stream_test.c | 22 +- + test/quic_txp_test.c | 2 +- + 10 files changed, 1397 insertions(+), 629 deletions(-) + delete mode 100644 include/internal/quic_sf_list.h + create mode 100644 include/internal/quic_strm_reas.h + delete mode 100644 ssl/quic/quic_sf_list.c + create mode 100644 ssl/quic/quic_strm_reas.c + +diff --git a/include/internal/quic_sf_list.h b/include/internal/quic_sf_list.h +deleted file mode 100644 +index 1a22cc3f387a..000000000000 +--- a/include/internal/quic_sf_list.h ++++ /dev/null +@@ -1,151 +0,0 @@ +-/* +- * Copyright 2022-2023 The OpenSSL Project Authors. All Rights Reserved. +- * +- * Licensed under the Apache License 2.0 (the "License"). You may not use +- * this file except in compliance with the License. You can obtain a copy +- * in the file LICENSE in the source distribution or at +- * https://www.openssl.org/source/license.html +- */ +- +-#ifndef OSSL_QUIC_SF_LIST_H +-#define OSSL_QUIC_SF_LIST_H +- +-#include "internal/common.h" +-#include "internal/uint_set.h" +-#include "internal/quic_record_rx.h" +- +-/* +- * Stream frame list +- * ================= +- * +- * This data structure supports similar operations as uint64 set but +- * it has slightly different invariants and also carries data associated with +- * the ranges in the list. +- * +- * Operations: +- * Insert frame (optimized insertion at the beginning and at the end). +- * Iterated peek into the frame(s) from the beginning. +- * Dropping frames from the beginning up to an offset (exclusive). +- * +- * Invariant: The frames in the list are sorted by the start and end bounds. +- * Invariant: There are no fully overlapping frames or frames that would +- * be fully encompassed by another frame in the list. +- * Invariant: No frame has start > end. +- * Invariant: The range start is inclusive the end is exclusive to be +- * able to mark an empty frame. +- * Invariant: The offset never points further than into the first frame. +- */ +-#ifndef OPENSSL_NO_QUIC +- +-typedef struct stream_frame_st STREAM_FRAME; +- +-typedef struct sframe_list_st { +- STREAM_FRAME *head, *tail; +- /* Is the tail frame final. */ +- unsigned int fin; +- /* Number of stream frames in the list. */ +- size_t num_frames; +- /* Offset of data not yet dropped */ +- uint64_t offset; +- /* Is head locked ? */ +- int head_locked; +- /* Cleanse data on release? */ +- int cleanse; +-} SFRAME_LIST; +- +-/* +- * Initializes the stream frame list fl. +- */ +-void ossl_sframe_list_init(SFRAME_LIST *fl); +- +-/* +- * Destroys the stream frame list fl releasing any data +- * still present inside it. +- */ +-void ossl_sframe_list_destroy(SFRAME_LIST *fl); +- +-/* +- * Insert a stream frame data into the list. +- * The data covers an offset range (range.start is inclusive, +- * range.end is exclusive). +- * fin should be set if this is the final frame of the stream. +- * Returns an error if a frame cannot be inserted - due to +- * STREAM_FRAME allocation error, or in case of erroneous +- * fin flag (this is an ossl_assert() check so a caller must +- * check it on its own too). +- */ +-int ossl_sframe_list_insert(SFRAME_LIST *fl, UINT_RANGE *range, +- OSSL_QRX_PKT *pkt, +- const unsigned char *data, int fin); +- +-/* +- * Iterator to peek at the contiguous frames at the beginning +- * of the frame list fl. +- * The *data covers an offset range (range.start is inclusive, +- * range.end is exclusive). +- * *fin is set if this is the final frame of the stream. +- * Opaque iterator *iter can be used to peek at the subsequent +- * frame if there is any without any gap before it. +- * Returns 1 on success. +- * Returns 0 if there is no further contiguous frame. In that +- * case *fin is set, if the end of the stream is reached. +- */ +-int ossl_sframe_list_peek(const SFRAME_LIST *fl, void **iter, +- UINT_RANGE *range, const unsigned char **data, +- int *fin); +- +-/* +- * Drop all frames up to the offset limit. +- * Also unlocks the head frame if locked. +- * Returns 1 on success. +- * Returns 0 when trying to drop frames at offsets that were not +- * received yet. (ossl_assert() is used to check, so this is an invalid call.) +- */ +-int ossl_sframe_list_drop_frames(SFRAME_LIST *fl, uint64_t limit); +- +-/* +- * Locks and returns the head frame of fl if it is readable - read offset is +- * at the beginning or middle of the frame. +- * range is set to encompass the not yet read part of the head frame, +- * data pointer is set to appropriate offset within the frame if the read +- * offset points in the middle of the frame, +- * fin is set to 1 if the head frame is also the tail frame. +- * Returns 1 on success, 0 if there is no readable data or the head +- * frame is already locked. +- */ +-int ossl_sframe_list_lock_head(SFRAME_LIST *fl, UINT_RANGE *range, +- const unsigned char **data, +- int *fin); +- +-/* +- * Just returns whether the head frame is locked by previous +- * ossl_sframe_list_lock_head() call. +- */ +-int ossl_sframe_list_is_head_locked(SFRAME_LIST *fl); +- +-/* +- * Callback function type to write stream frame data to some +- * side storage before the packet containing the frame data +- * is released. +- * It should return 1 on success or 0 if there is not enough +- * space available in the side storage. +- */ +-typedef int(sframe_list_write_at_cb)(uint64_t logical_offset, +- const unsigned char *buf, +- size_t buf_len, +- void *cb_arg); +- +-/* +- * Move the frame data in all the stream frames in the list fl +- * from the packets to the side storage using the write_at_cb +- * callback. +- * Returns 1 if all the calls to the callback return 1. +- * If the callback returns 0, the function stops processing further +- * frames and returns 0. +- */ +-int ossl_sframe_list_move_data(SFRAME_LIST *fl, +- sframe_list_write_at_cb *write_at_cb, +- void *cb_arg); +-#endif +- +-#endif +diff --git a/include/internal/quic_stream.h b/include/internal/quic_stream.h +index 824d4b896967..b9431831d054 100644 +--- a/include/internal/quic_stream.h ++++ b/include/internal/quic_stream.h +@@ -318,11 +318,9 @@ void ossl_quic_sstream_set_cleanse(QUIC_SSTREAM *qss, int cleanse); + * controller and statistics module. They can be NULL for unit testing. + * If they are non-NULL, the `rxfc` is called when receive stream data + * is read by application. `statm` is queried for current rtt. +- * `rbuf_size` is the initial size of the ring buffer to be used +- * when ossl_quic_rstream_move_to_rbuf() is called. + */ + QUIC_RSTREAM *ossl_quic_rstream_new(QUIC_RXFC *rxfc, +- OSSL_STATM *statm, size_t rbuf_size); ++ OSSL_STATM *statm); + + /* + * Frees a QUIC_RSTREAM and any associated storage. +@@ -330,10 +328,10 @@ QUIC_RSTREAM *ossl_quic_rstream_new(QUIC_RXFC *rxfc, + void ossl_quic_rstream_free(QUIC_RSTREAM *qrs); + + /* +- * Adds received stream frame data to `qrs`. The `pkt_wrap` refcount is +- * incremented if the `data` is queued directly without copying. +- * It can be NULL for unit-testing purposes, i.e. if `data` is static or +- * never released before calling ossl_quic_rstream_free(). ++ * Adds received stream frame data to `qrs`. `pkt` must be the packet ++ * carrying `data`; its refcount is incremented if the data is kept ++ * referenced on the packet rather than copied. `pkt` and `data` can ++ * be NULL only for an empty frame indicating `fin`. + * The `offset` is the absolute offset of the data in the stream. + * `data_len` can be 0 - can be useful for indicating `fin` for empty stream. + * Or to indicate `fin` without any further data added to the stream. +@@ -378,8 +376,6 @@ int ossl_quic_rstream_available(QUIC_RSTREAM *qrs, size_t *avail, int *fin); + * Returns 1 on success (including calls if no record is available, or + * after end of the stream - in that case *fin will be set to 1 and + * *rec_len to 0), 0 on error. +- * It is an error to call ossl_quic_rstream_get_record() multiple times +- * without calling ossl_quic_rstream_release_record() in between. + */ + int ossl_quic_rstream_get_record(QUIC_RSTREAM *qrs, + const unsigned char **record, size_t *rec_len, +@@ -394,35 +390,23 @@ int ossl_quic_rstream_get_record(QUIC_RSTREAM *qrs, + * call to ossl_quic_rstream_get_record() is needed to obtain further + * stream data. + * Returns 1 on success, 0 on error. +- * It is an error to call ossl_quic_rstream_release_record() multiple +- * times without calling ossl_quic_rstream_get_record() in between. + */ + int ossl_quic_rstream_release_record(QUIC_RSTREAM *qrs, size_t read_len); + + /* +- * Moves received frame data from decrypted packets to ring buffer. +- * This should be called when there are too many decrypted packets allocated. +- * Returns 1 on success, 0 when it was not possible to release all +- * referenced packets due to an insufficient size of the ring buffer. +- * Exception is the packet from the record returned previously by +- * ossl_quic_rstream_get_record() - that one will be always skipped. ++ * Sets flag to cleanse the buffered data when user reads it. + */ +-int ossl_quic_rstream_move_to_rbuf(QUIC_RSTREAM *qrs); ++void ossl_quic_rstream_set_cleanse(QUIC_RSTREAM *qrs, int cleanse); + + /* +- * Resizes the internal ring buffer to a new `rbuf_size` size. +- * Returns 1 on success, 0 on error. +- * Possible error conditions are an allocation failure, trying to resize +- * the ring buffer when ossl_quic_rstream_get_record() was called and +- * not yet released, or trying to resize the ring buffer to a smaller size +- * than currently occupied. ++ * returns the number of stream chunks kept in rstream + */ +-int ossl_quic_rstream_resize_rbuf(QUIC_RSTREAM *qrs, size_t rbuf_size); ++size_t ossl_quic_rstream_get_chunk_count(QUIC_RSTREAM *qrs); + + /* +- * Sets flag to cleanse the buffered data when user reads it. ++ * returns the number of stream ranges kept in rstream + */ +-void ossl_quic_rstream_set_cleanse(QUIC_RSTREAM *qrs, int cleanse); ++size_t ossl_quic_rstream_get_range_count(QUIC_RSTREAM *qrs); + #endif + + #endif +diff --git a/include/internal/quic_strm_reas.h b/include/internal/quic_strm_reas.h +new file mode 100644 +index 000000000000..1d1af12c74eb +--- /dev/null ++++ b/include/internal/quic_strm_reas.h +@@ -0,0 +1,89 @@ ++/* ++ * Copyright 2022-2026 The OpenSSL Project Authors. All Rights Reserved. ++ * ++ * Licensed under the Apache License 2.0 (the "License"). You may not use ++ * this file except in compliance with the License. You can obtain a copy ++ * in the file LICENSE in the source distribution or at ++ * https://www.openssl.org/source/license.html ++ */ ++ ++#ifndef OSSL_QUIC_STRM_REAS_H ++#define OSSL_QUIC_STRM_REAS_H ++ ++#include "internal/common.h" ++#include "internal/uint_set.h" ++#include "internal/quic_record_rx.h" ++ ++#ifndef OPENSSL_NO_QUIC ++#include "internal/ossl_rbtree.h" ++ ++typedef struct sframe_set_t { ++ OSSL_RBT_HEAD(srange, sframe_set_t) ++ ranges; ++ /* Is the tail frame final. */ ++ unsigned int fin; ++ uint64_t fin_off; ++ /* Number of stream frames in the list. */ ++ size_t stream_ranges; ++ size_t stream_chunks; ++ /* Offset of data not yet dropped */ ++ uint64_t offset; ++ /* Cleanse data on release? */ ++ int cleanse; ++ int move_buffers; ++} SFRAME_SET; ++ ++/* ++ * Initializes the stream frame list fs. ++ */ ++void ossl_sframe_set_init(SFRAME_SET *fs); ++ ++/* ++ * Destroys the stream frame list fs releasing any data ++ * still present inside it. ++ */ ++void ossl_sframe_set_destroy_ranges(SFRAME_SET *fs); ++ ++/* ++ * Insert a stream frame data into the list. ++ * The data covers an offset range (range.start is inclusive, ++ * range.end is exclusive). ++ * fin should be set if this is the final frame of the stream. ++ * Returns an error if a frame cannot be inserted - due to ++ * STREAM_FRAME allocation error, or in case of erroneous ++ * fin flag. ++ */ ++int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *range, ++ OSSL_QRX_PKT *pkt, ++ const unsigned char *data, int fin); ++ ++/* ++ * Iterator to peek at the contiguous frames at the beginning ++ * of the frame set (the first stream range). ++ * The *data covers an offset range (range.start is inclusive, ++ * range.end is exclusive). ++ * *fin is set if this is the final frame of the stream. ++ * Opaque iterator *iter can be used to peek at the subsequent ++ * frame if there is any without any gap before it. ++ * Returns 1 on success. ++ * Returns 0 if there is no further contiguous frame. In that ++ * case *fin is set, if the end of the stream is reached. ++ */ ++int ossl_sframe_set_peek(SFRAME_SET *fs, void **iter, ++ UINT_RANGE *range, const unsigned char **data, ++ int *fin); ++ ++/* ++ * moves reading offset to new position, discarding all consumed ++ * chunks (which end offset is less than offset). ++ */ ++int ossl_sframe_set_move_offset(SFRAME_SET *fs, uint64_t offset); ++ ++/* ++ * returns how many bytes is available to read from stream. ++ */ ++int ossl_sframe_set_avail(SFRAME_SET *fs, uint64_t *avail, int *fin); ++ ++#endif ++ ++#endif +diff --git a/ssl/quic/build.info b/ssl/quic/build.info +index 230341db7625..c13c6dffbf64 100644 +--- a/ssl/quic/build.info ++++ b/ssl/quic/build.info +@@ -10,7 +10,7 @@ IF[{- !$disabled{quic} -}] + SOURCE[$LIBSSL]=quic_fc.c uint_set.c + SOURCE[$LIBSSL]=quic_cfq.c quic_txpim.c quic_fifd.c quic_txp.c + SOURCE[$LIBSSL]=quic_stream_map.c +- SOURCE[$LIBSSL]=quic_sf_list.c quic_rstream.c quic_sstream.c ++ SOURCE[$LIBSSL]=quic_strm_reas.c quic_rstream.c quic_sstream.c + SOURCE[$LIBSSL]=quic_reactor.c + SOURCE[$LIBSSL]=quic_reactor_wait_ctx.c + SOURCE[$LIBSSL]=quic_channel.c quic_port.c quic_engine.c +diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c +index c63272b511fa..15b2ed22e900 100644 +--- a/ssl/quic/quic_channel.c ++++ b/ssl/quic/quic_channel.c +@@ -338,7 +338,7 @@ static int ch_init(QUIC_CHANNEL *ch) + } + + for (pn_space = QUIC_PN_SPACE_INITIAL; pn_space < QUIC_PN_SPACE_NUM; ++pn_space) { +- ch->crypto_recv[pn_space] = ossl_quic_rstream_new(NULL, NULL, 0); ++ ch->crypto_recv[pn_space] = ossl_quic_rstream_new(NULL, NULL); + if (ch->crypto_recv[pn_space] == NULL) + goto err; + } +@@ -3774,7 +3774,7 @@ static int ch_init_new_stream(QUIC_CHANNEL *ch, QUIC_STREAM *qs, + goto err; + + if (can_recv) +- if ((qs->rstream = ossl_quic_rstream_new(NULL, NULL, 0)) == NULL) ++ if ((qs->rstream = ossl_quic_rstream_new(NULL, NULL)) == NULL) + goto err; + + /* TXFC */ +diff --git a/ssl/quic/quic_rstream.c b/ssl/quic/quic_rstream.c +index 2fe1cb2cdbe4..a2dc038e4fe2 100644 +--- a/ssl/quic/quic_rstream.c ++++ b/ssl/quic/quic_rstream.c +@@ -10,32 +10,32 @@ + #include "internal/common.h" + #include "internal/time.h" + #include "internal/quic_stream.h" +-#include "internal/quic_sf_list.h" ++#include "internal/quic_strm_reas.h" + #include "internal/ring_buf.h" + + struct quic_rstream_st { +- SFRAME_LIST fl; ++ SFRAME_SET fs; + QUIC_RXFC *rxfc; + OSSL_STATM *statm; + UINT_RANGE head_range; +- struct ring_buf rbuf; + }; + ++#if !defined(NDEBUG) && defined(WITH_RSTREAM_DEBUG) ++#include ++#define DEBUG_PRINT(...) fprintf(__VA_ARGS__) ++#else ++#define DEBUG_PRINT(...) (void)(0) ++#endif ++ + QUIC_RSTREAM *ossl_quic_rstream_new(QUIC_RXFC *rxfc, +- OSSL_STATM *statm, size_t rbuf_size) ++ OSSL_STATM *statm) + { + QUIC_RSTREAM *ret = OPENSSL_zalloc(sizeof(*ret)); + + if (ret == NULL) + return NULL; + +- ring_buf_init(&ret->rbuf); +- if (!ring_buf_resize(&ret->rbuf, rbuf_size, 0)) { +- OPENSSL_free(ret); +- return NULL; +- } +- +- ossl_sframe_list_init(&ret->fl); ++ ossl_sframe_set_init(&ret->fs); + ret->rxfc = rxfc; + ret->statm = statm; + return ret; +@@ -43,14 +43,10 @@ QUIC_RSTREAM *ossl_quic_rstream_new(QUIC_RXFC *rxfc, + + void ossl_quic_rstream_free(QUIC_RSTREAM *qrs) + { +- int cleanse; +- + if (qrs == NULL) + return; + +- cleanse = qrs->fl.cleanse; +- ossl_sframe_list_destroy(&qrs->fl); +- ring_buf_destroy(&qrs->rbuf, cleanse); ++ ossl_sframe_set_destroy_ranges(&qrs->fs); + OPENSSL_free(qrs); + } + +@@ -70,7 +66,7 @@ int ossl_quic_rstream_queue_data(QUIC_RSTREAM *qrs, OSSL_QRX_PKT *pkt, + range.start = offset; + range.end = offset + data_len; + +- return ossl_sframe_list_insert(&qrs->fl, &range, pkt, data, fin); ++ return ossl_sframe_set_insert(&qrs->fs, &range, pkt, data, fin); + } + + static int read_internal(QUIC_RSTREAM *qrs, unsigned char *buf, size_t size, +@@ -83,9 +79,11 @@ static int read_internal(QUIC_RSTREAM *qrs, unsigned char *buf, size_t size, + size_t readbytes_ = 0; + int fin_ = 0, ret = 1; + +- while (ossl_sframe_list_peek(&qrs->fl, &iter, &range, &data, &fin_)) { ++ DEBUG_PRINT(stderr, "%s want: %zu\n", OPENSSL_FUNC, size); ++ while (ossl_sframe_set_peek(&qrs->fs, &iter, &range, &data, &fin_)) { + size_t l = (size_t)(range.end - range.start); + ++ DEBUG_PRINT(stderr, "\t[ %llu, %llu ]\n", range.start, range.end); + if (l > size) { + l = size; + fin_ = 0; +@@ -94,25 +92,6 @@ static int read_internal(QUIC_RSTREAM *qrs, unsigned char *buf, size_t size, + if (l == 0) + break; + +- if (data == NULL) { +- size_t max_len; +- +- data = ring_buf_get_ptr(&qrs->rbuf, range.start, &max_len); +- if (!ossl_assert(data != NULL)) +- return 0; +- if (max_len < l) { +- memcpy(buf, data, max_len); +- size -= max_len; +- buf += max_len; +- readbytes_ += max_len; +- l -= max_len; +- data = ring_buf_get_ptr(&qrs->rbuf, range.start + max_len, +- &max_len); +- if (!ossl_assert(data != NULL) || !ossl_assert(max_len > l)) +- return 0; +- } +- } +- + memcpy(buf, data, l); + size -= l; + buf += l; +@@ -121,14 +100,15 @@ static int read_internal(QUIC_RSTREAM *qrs, unsigned char *buf, size_t size, + break; + } + +- if (drop && offset != 0) { +- ret = ossl_sframe_list_drop_frames(&qrs->fl, offset); +- ring_buf_cpop_range(&qrs->rbuf, 0, offset - 1, qrs->fl.cleanse); +- } ++ if (drop && offset != 0) ++ ret = ossl_sframe_set_move_offset(&qrs->fs, offset); + + if (ret) { ++ DEBUG_PRINT(stderr, "%s got: %zu\n", OPENSSL_FUNC, readbytes_); + *readbytes = readbytes_; + *fin = fin_; ++ } else { ++ DEBUG_PRINT(stderr, "%s got: nothing\n", OPENSSL_FUNC); + } + + return ret; +@@ -172,13 +152,9 @@ int ossl_quic_rstream_peek(QUIC_RSTREAM *qrs, unsigned char *buf, size_t size, + + int ossl_quic_rstream_available(QUIC_RSTREAM *qrs, size_t *avail, int *fin) + { +- void *iter = NULL; +- UINT_RANGE range; +- const unsigned char *data; + uint64_t avail_ = 0; + +- while (ossl_sframe_list_peek(&qrs->fl, &iter, &range, &data, fin)) +- avail_ += range.end - range.start; ++ ossl_sframe_set_avail(&qrs->fs, &avail_, fin); + + #if SIZE_MAX < UINT64_MAX + *avail = avail_ > SIZE_MAX ? SIZE_MAX : (size_t)avail_; +@@ -193,38 +169,32 @@ int ossl_quic_rstream_get_record(QUIC_RSTREAM *qrs, + int *fin) + { + const unsigned char *record_ = NULL; +- size_t rec_len_, max_len; ++ void *iterator = NULL; ++ size_t rec_len_; ++ int ok; + +- if (!ossl_sframe_list_lock_head(&qrs->fl, &qrs->head_range, &record_, fin)) { +- /* No head frame to lock and return */ ++ ok = ossl_sframe_set_peek(&qrs->fs, &iterator, &qrs->head_range, &record_, ++ fin); ++ if (ok == 0) { + *record = NULL; + *rec_len = 0; + return 1; + } + ++ DEBUG_PRINT(stderr, "%s head: [ %llu, %llu ]\n", OPENSSL_FUNC, ++ qrs->head_range.start, qrs->head_range.end); + /* if final empty frame, we drop it immediately */ + if (qrs->head_range.end == qrs->head_range.start) { + if (!ossl_assert(*fin)) + return 0; +- if (!ossl_sframe_list_drop_frames(&qrs->fl, qrs->head_range.end)) ++ if (!ossl_sframe_set_move_offset(&qrs->fs, qrs->head_range.end)) + return 0; + } + + rec_len_ = (size_t)(qrs->head_range.end - qrs->head_range.start); +- +- if (record_ == NULL && rec_len_ != 0) { +- record_ = ring_buf_get_ptr(&qrs->rbuf, qrs->head_range.start, +- &max_len); +- if (!ossl_assert(record_ != NULL)) +- return 0; +- if (max_len < rec_len_) { +- rec_len_ = max_len; +- qrs->head_range.end = qrs->head_range.start + max_len; +- } +- } +- + *rec_len = rec_len_; + *record = record_; ++ + return 1; + } + +@@ -232,9 +202,6 @@ int ossl_quic_rstream_release_record(QUIC_RSTREAM *qrs, size_t read_len) + { + uint64_t offset; + +- if (!ossl_sframe_list_is_head_locked(&qrs->fl)) +- return 0; +- + if (read_len > qrs->head_range.end - qrs->head_range.start) { + if (read_len != SIZE_MAX) + return 0; +@@ -243,12 +210,9 @@ int ossl_quic_rstream_release_record(QUIC_RSTREAM *qrs, size_t read_len) + offset = qrs->head_range.start + read_len; + } + +- if (!ossl_sframe_list_drop_frames(&qrs->fl, offset)) ++ if (!ossl_sframe_set_move_offset(&qrs->fs, offset)) + return 0; + +- if (offset > 0) +- ring_buf_cpop_range(&qrs->rbuf, 0, offset - 1, qrs->fl.cleanse); +- + if (qrs->rxfc != NULL) { + OSSL_TIME rtt = get_rtt(qrs); + +@@ -259,36 +223,17 @@ int ossl_quic_rstream_release_record(QUIC_RSTREAM *qrs, size_t read_len) + return 1; + } + +-static int write_at_ring_buf_cb(uint64_t logical_offset, +- const unsigned char *buf, +- size_t buf_len, +- void *cb_arg) +-{ +- struct ring_buf *rbuf = cb_arg; +- +- return ring_buf_write_at(rbuf, logical_offset, buf, buf_len); +-} +- +-int ossl_quic_rstream_move_to_rbuf(QUIC_RSTREAM *qrs) ++void ossl_quic_rstream_set_cleanse(QUIC_RSTREAM *qrs, int cleanse) + { +- if (ring_buf_avail(&qrs->rbuf) == 0) +- return 0; +- return ossl_sframe_list_move_data(&qrs->fl, +- write_at_ring_buf_cb, &qrs->rbuf); ++ qrs->fs.cleanse = cleanse; + } + +-int ossl_quic_rstream_resize_rbuf(QUIC_RSTREAM *qrs, size_t rbuf_size) ++size_t ossl_quic_rstream_get_chunk_count(QUIC_RSTREAM *qrs) + { +- if (ossl_sframe_list_is_head_locked(&qrs->fl)) +- return 0; +- +- if (!ring_buf_resize(&qrs->rbuf, rbuf_size, qrs->fl.cleanse)) +- return 0; +- +- return 1; ++ return qrs->fs.stream_chunks; + } + +-void ossl_quic_rstream_set_cleanse(QUIC_RSTREAM *qrs, int cleanse) ++size_t ossl_quic_rstream_get_range_count(QUIC_RSTREAM *qrs) + { +- qrs->fl.cleanse = cleanse; ++ return qrs->fs.stream_ranges; + } +diff --git a/ssl/quic/quic_sf_list.c b/ssl/quic/quic_sf_list.c +deleted file mode 100644 +index 03bbbe6d3561..000000000000 +--- a/ssl/quic/quic_sf_list.c ++++ /dev/null +@@ -1,334 +0,0 @@ +-/* +- * Copyright 2022-2023 The OpenSSL Project Authors. All Rights Reserved. +- * +- * Licensed under the Apache License 2.0 (the "License"). You may not use +- * this file except in compliance with the License. You can obtain a copy +- * in the file LICENSE in the source distribution or at +- * https://www.openssl.org/source/license.html +- */ +- +-#include "internal/uint_set.h" +-#include "internal/common.h" +-#include "internal/quic_sf_list.h" +- +-struct stream_frame_st { +- struct stream_frame_st *prev, *next; +- UINT_RANGE range; +- OSSL_QRX_PKT *pkt; +- const unsigned char *data; +-}; +- +-static void stream_frame_free(SFRAME_LIST *fl, STREAM_FRAME *sf) +-{ +- if (fl->cleanse && sf->data != NULL) +- OPENSSL_cleanse((unsigned char *)sf->data, +- (size_t)(sf->range.end - sf->range.start)); +- ossl_qrx_pkt_release(sf->pkt); +- OPENSSL_free(sf); +-} +- +-static STREAM_FRAME *stream_frame_new(UINT_RANGE *range, OSSL_QRX_PKT *pkt, +- const unsigned char *data) +-{ +- STREAM_FRAME *sf = OPENSSL_zalloc(sizeof(*sf)); +- +- if (sf == NULL) +- return NULL; +- +- if (pkt != NULL) +- ossl_qrx_pkt_up_ref(pkt); +- +- sf->range = *range; +- sf->pkt = pkt; +- sf->data = data; +- +- return sf; +-} +- +-void ossl_sframe_list_init(SFRAME_LIST *fl) +-{ +- memset(fl, 0, sizeof(*fl)); +-} +- +-void ossl_sframe_list_destroy(SFRAME_LIST *fl) +-{ +- STREAM_FRAME *sf, *next_frame; +- +- for (sf = fl->head; sf != NULL; sf = next_frame) { +- next_frame = sf->next; +- stream_frame_free(fl, sf); +- } +-} +- +-static int append_frame(SFRAME_LIST *fl, UINT_RANGE *range, +- OSSL_QRX_PKT *pkt, +- const unsigned char *data) +-{ +- STREAM_FRAME *new_frame; +- +- if ((new_frame = stream_frame_new(range, pkt, data)) == NULL) +- return 0; +- new_frame->prev = fl->tail; +- if (fl->tail != NULL) +- fl->tail->next = new_frame; +- fl->tail = new_frame; +- ++fl->num_frames; +- return 1; +-} +- +-int ossl_sframe_list_insert(SFRAME_LIST *fl, UINT_RANGE *range, +- OSSL_QRX_PKT *pkt, +- const unsigned char *data, int fin) +-{ +- STREAM_FRAME *sf, *new_frame, *prev_frame, *next_frame; +-#ifndef NDEBUG +- uint64_t curr_end = fl->tail != NULL ? fl->tail->range.end +- : fl->offset; +- +- /* This check for FINAL_SIZE_ERROR is handled by QUIC FC already */ +- assert((!fin || curr_end <= range->end) +- && (!fl->fin || curr_end >= range->end)); +-#endif +- +- if (fl->offset >= range->end) +- goto end; +- +- /* nothing there yet */ +- if (fl->tail == NULL) { +- fl->tail = fl->head = stream_frame_new(range, pkt, data); +- if (fl->tail == NULL) +- return 0; +- +- ++fl->num_frames; +- goto end; +- } +- +- /* optimize insertion at the end */ +- if (fl->tail->range.start < range->start) { +- if (fl->tail->range.end >= range->end) +- goto end; +- +- if (!append_frame(fl, range, pkt, data)) +- return 0; +- goto end; +- } +- +- prev_frame = NULL; +- for (sf = fl->head; sf != NULL && sf->range.start < range->start; +- sf = sf->next) +- prev_frame = sf; +- +- if (!ossl_assert(sf != NULL)) +- /* frame list invariant broken */ +- return 0; +- +- if (prev_frame != NULL && prev_frame->range.end >= range->end) +- goto end; +- +- /* +- * Now we must create a new frame although in the end we might drop it, +- * because we will be potentially dropping existing overlapping frames. +- */ +- new_frame = stream_frame_new(range, pkt, data); +- if (new_frame == NULL) +- return 0; +- +- for (next_frame = sf; +- next_frame != NULL && next_frame->range.end <= range->end;) { +- STREAM_FRAME *drop_frame = next_frame; +- +- next_frame = next_frame->next; +- if (next_frame != NULL) +- next_frame->prev = drop_frame->prev; +- if (prev_frame != NULL) +- prev_frame->next = drop_frame->next; +- if (fl->head == drop_frame) +- fl->head = next_frame; +- if (fl->tail == drop_frame) +- fl->tail = prev_frame; +- --fl->num_frames; +- stream_frame_free(fl, drop_frame); +- } +- +- if (next_frame != NULL) { +- /* check whether the new_frame is redundant because there is no gap */ +- if (prev_frame != NULL +- && next_frame->range.start <= prev_frame->range.end) { +- stream_frame_free(fl, new_frame); +- goto end; +- } +- next_frame->prev = new_frame; +- } else { +- fl->tail = new_frame; +- } +- +- new_frame->next = next_frame; +- new_frame->prev = prev_frame; +- +- if (prev_frame != NULL) +- prev_frame->next = new_frame; +- else +- fl->head = new_frame; +- +- ++fl->num_frames; +- +-end: +- fl->fin = fin || fl->fin; +- +- return 1; +-} +- +-int ossl_sframe_list_peek(const SFRAME_LIST *fl, void **iter, +- UINT_RANGE *range, const unsigned char **data, +- int *fin) +-{ +- STREAM_FRAME *sf = *iter; +- uint64_t start; +- +- if (sf == NULL) { +- start = fl->offset; +- sf = fl->head; +- } else { +- start = sf->range.end; +- sf = sf->next; +- } +- +- range->start = start; +- +- if (sf == NULL || sf->range.start > start +- || !ossl_assert(start < sf->range.end)) { +- range->end = start; +- *data = NULL; +- *iter = NULL; +- /* set fin only if we are at the end */ +- *fin = sf == NULL ? fl->fin : 0; +- return 0; +- } +- +- range->end = sf->range.end; +- if (sf->data != NULL) +- *data = sf->data + (start - sf->range.start); +- else +- *data = NULL; +- *fin = sf->next == NULL ? fl->fin : 0; +- *iter = sf; +- return 1; +-} +- +-int ossl_sframe_list_drop_frames(SFRAME_LIST *fl, uint64_t limit) +-{ +- STREAM_FRAME *sf; +- +- /* offset cannot move back or past the data received */ +- if (!ossl_assert(limit >= fl->offset) +- || !ossl_assert(fl->tail == NULL +- || limit <= fl->tail->range.end) +- || !ossl_assert(fl->tail != NULL +- || limit == fl->offset)) +- return 0; +- +- fl->offset = limit; +- +- for (sf = fl->head; sf != NULL && sf->range.end <= limit;) { +- STREAM_FRAME *drop_frame = sf; +- +- sf = sf->next; +- --fl->num_frames; +- stream_frame_free(fl, drop_frame); +- } +- fl->head = sf; +- +- if (sf != NULL) +- sf->prev = NULL; +- else +- fl->tail = NULL; +- +- fl->head_locked = 0; +- +- return 1; +-} +- +-int ossl_sframe_list_lock_head(SFRAME_LIST *fl, UINT_RANGE *range, +- const unsigned char **data, +- int *fin) +-{ +- int ret; +- void *iter = NULL; +- +- if (fl->head_locked) +- return 0; +- +- ret = ossl_sframe_list_peek(fl, &iter, range, data, fin); +- if (ret) +- fl->head_locked = 1; +- return ret; +-} +- +-int ossl_sframe_list_is_head_locked(SFRAME_LIST *fl) +-{ +- return fl->head_locked; +-} +- +-int ossl_sframe_list_move_data(SFRAME_LIST *fl, +- sframe_list_write_at_cb *write_at_cb, +- void *cb_arg) +-{ +- STREAM_FRAME *sf = fl->head, *prev_frame = NULL; +- uint64_t limit = fl->offset; +- +- if (sf == NULL) +- return 1; +- +- if (fl->head_locked) +- sf = sf->next; +- +- for (; sf != NULL; sf = sf->next) { +- size_t len; +- const unsigned char *data = sf->data; +- +- if (limit < sf->range.start) +- limit = sf->range.start; +- +- if (data != NULL) { +- if (limit > sf->range.start) +- data += (size_t)(limit - sf->range.start); +- len = (size_t)(sf->range.end - limit); +- +- if (!write_at_cb(limit, data, len, cb_arg)) +- /* data did not fit */ +- return 0; +- +- if (fl->cleanse) +- OPENSSL_cleanse((unsigned char *)sf->data, +- (size_t)(sf->range.end - sf->range.start)); +- +- /* release the packet */ +- sf->data = NULL; +- ossl_qrx_pkt_release(sf->pkt); +- sf->pkt = NULL; +- } +- +- limit = sf->range.end; +- +- /* merge contiguous frames */ +- if (prev_frame != NULL +- && prev_frame->range.end >= sf->range.start) { +- prev_frame->range.end = sf->range.end; +- prev_frame->next = sf->next; +- +- if (sf->next != NULL) +- sf->next->prev = prev_frame; +- else +- fl->tail = prev_frame; +- +- --fl->num_frames; +- stream_frame_free(fl, sf); +- sf = prev_frame; +- continue; +- } +- +- prev_frame = sf; +- } +- +- return 1; +-} +diff --git a/ssl/quic/quic_strm_reas.c b/ssl/quic/quic_strm_reas.c +new file mode 100644 +index 000000000000..9e6bbe285a07 +--- /dev/null ++++ b/ssl/quic/quic_strm_reas.c +@@ -0,0 +1,1251 @@ ++/* ++ * Copyright 2022-2026 The OpenSSL Project Authors. All Rights Reserved. ++ * ++ * Licensed under the Apache License 2.0 (the "License"). You may not use ++ * this file except in compliance with the License. You can obtain a copy ++ * in the file LICENSE in the source distribution or at ++ * https://www.openssl.org/source/license.html ++ */ ++ ++#include "internal/uint_set.h" ++#include "internal/common.h" ++#include "internal/quic_strm_reas.h" ++#include "internal/list.h" ++ ++#if !defined(NDEBUG) && defined(WITH_STRM_REAS_DEBUG) ++#include ++#define DEBUG_PRINT(...) fprintf(__VA_ARGS__) ++#else ++#define DEBUG_PRINT(...) (void)(0) ++#endif ++ ++#define DIRECT_STORAGE_SZ (2 * sizeof(void *)) ++ ++/* ++ * storage type indicates where stream data bytes ++ * are stored. ++ */ ++enum { ++ ST_TYPE_DIRECT, /* in chunk structure itself (sc_dstorage) */ ++ ST_TYPE_PKT, /* bytes are stored in attached pkt (sc_pkt) */ ++ ST_TYPE_HEAP /* data are stored on memory heap buffer */ ++}; ++ ++/* ++ * Stream chunk keeps stream bytes as received from QUIC STREAM_FRAME. ++ * Each chunk of stream data by [start, end). ++ */ ++struct stream_chunk_t { ++ OSSL_LIST_MEMBER(sc, struct stream_chunk_t); ++ UINT_RANGE sc_range; ++ int sc_st; /* storage type */ ++ union { ++ const unsigned char *u_data; ++ unsigned char *u_data_w; ++ } sc_data_u; ++ union { ++ OSSL_QRX_PKT *u_sc_pkt; ++ unsigned char *u_sc_buf; ++ unsigned char u_sc_dstorage[DIRECT_STORAGE_SZ]; ++ } sc_storage_u; ++}; ++ ++#define sc_data sc_data_u.u_data ++#define sc_data_w sc_data_u.u_data_w ++ ++#define sc_pkt sc_storage_u.u_sc_pkt ++#define sc_buf sc_storage_u.u_sc_buf ++#define sc_dstorage sc_storage_u.u_sc_dstorage ++ ++DEFINE_LIST_OF(sc, struct stream_chunk_t); ++ ++#define SCHUNK_SIZE(_sc) ((_sc)->sc_range.end - (_sc)->sc_range.start) ++#define SRANGE_SIZE(_sr) ((_sr)->sr_range.end - (_sr)->sr_range.start) ++ ++/* ++ * Stream range keeps list of continuous stream chunks. The range ++ * is also defined by [start, end) interval. For every chunk ++ * in range this assertion must hold: ++ * sc->sc_range.end == sc->sc_next->sc_range.start ++ * ++ * If newly arriving stream chunk can not be inserted to existing ++ * stream range, then new range must be created. ++ */ ++struct stream_range_t { ++ OSSL_LIST(sc) ++ sr_chunks; ++ OSSL_RBT_ENTRY(stream_range_t) ++ sr_rbe; ++ UINT_RANGE sr_range; ++ struct stream_chunk_t *sr_it_sc; /* iterator */ ++}; ++ ++static int srange_cmp(const struct stream_range_t *, const struct stream_range_t *); ++ ++OSSL_RBT_PROTOTYPE(srange, stream_range_t, sr_rbe, srange_cmp) ++ ++OSSL_RBT_GENERATE(srange, stream_range_t, sr_rbe, srange_cmp); ++ ++#define UINT64_TO_SIZE_T(_x) ((size_t)(((_x) > SIZE_MAX) ? SIZE_MAX : (_x))) ++ ++/* ++ * Cleansing (SSL_OP_CLEANSE_PLAINTEXT) must write through the const ++ * data pointers received from ossl_sframe_set_insert(), which may ++ * point into a shared packet buffer. That is safe: each chunk ++ * references the disjoint payload slice of its own frame and a ++ * processed packet is kept alive only by the chunks stored on it, ++ * so nobody else reads the wiped bytes. ++ * ++ * The const should eventually be dropped from the prototypes ++ * instead; until then deconst() is used. ++ */ ++static unsigned char *deconst(const unsigned char *data) ++{ ++ union { ++ const unsigned char *u_data; ++ unsigned char *u_data_w; ++ } data_u; ++ ++ data_u.u_data = data; ++ ++ return data_u.u_data_w; ++} ++ ++static void sc_data_trim_left(struct stream_chunk_t *sc, size_t trim_sz, ++ int cleanse) ++{ ++ if (sc->sc_st == ST_TYPE_DIRECT) { ++ assert(SCHUNK_SIZE(sc) >= trim_sz); ++ memmove(sc->sc_data_w, &sc->sc_data[trim_sz], ++ UINT64_TO_SIZE_T((SCHUNK_SIZE(sc) - trim_sz))); ++ if (cleanse && trim_sz > 0) { ++ OPENSSL_cleanse( ++ sc->sc_data_w + UINT64_TO_SIZE_T(SCHUNK_SIZE(sc) - trim_sz), ++ UINT64_TO_SIZE_T(trim_sz)); ++ } ++ } else { ++ if (cleanse && trim_sz > 0) { ++ OPENSSL_cleanse(sc->sc_data_w, trim_sz); ++ } ++ sc->sc_data += trim_sz; ++ } ++} ++ ++static void sc_data_trim_right(struct stream_chunk_t *sc, size_t trim_sz, ++ int cleanse) ++{ ++ unsigned char *data_realloc; ++ size_t w_offset; ++ ++ assert(SCHUNK_SIZE(sc) >= trim_sz); ++ ++ if (cleanse) ++ OPENSSL_cleanse( ++ &sc->sc_data_w[sc->sc_range.end - trim_sz - sc->sc_range.start], ++ trim_sz); ++ ++ if (sc->sc_st == ST_TYPE_HEAP) { ++ /* ++ * this is a shrinking realloc() here, so it should not fail. ++ * even if it fails, we still don't care, the worst outcome ++ * of such failure is waste of memory. ++ */ ++ assert(sc->sc_data_w >= sc->sc_buf); ++ w_offset = sc->sc_data_w - sc->sc_buf; ++ if (trim_sz > 0) { ++ assert(SCHUNK_SIZE(sc) > trim_sz); ++ data_realloc = OPENSSL_realloc(sc->sc_buf, ++ w_offset + UINT64_TO_SIZE_T(SCHUNK_SIZE(sc)) - trim_sz); ++ if (data_realloc != NULL) { ++ sc->sc_buf = data_realloc; ++ sc->sc_data_w = sc->sc_buf + w_offset; ++ } ++ } ++ } ++} ++ ++static int srange_cmp(const struct stream_range_t *a_sr, ++ const struct stream_range_t *b_sr) ++{ ++ assert(a_sr->sr_range.start < a_sr->sr_range.end); ++ assert(b_sr->sr_range.start < b_sr->sr_range.end); ++ /* ++ * no overlap, A precedes B ++ */ ++ if (a_sr->sr_range.end < b_sr->sr_range.start) ++ return -1; ++ ++ /* ++ * no overlap, A follows B ++ */ ++ if (a_sr->sr_range.start > b_sr->sr_range.end) ++ return 1; ++ ++ /* ++ * partial or full overlap or ranges are adjacent. ++ * the program needs to do close examination on ++ * how to add new chunk to existing stream range. ++ */ ++ return 0; ++} ++ ++static int keep_schunk_data_on_packet(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, ++ UINT_RANGE *r) ++{ ++ /* ++ * the function decides whether stream data should be moved ++ * from packet buffer to stream buffer or if data can stay ++ * at packet buffer. ++ * ++ * Keeping the data at packet saves yet another buffer ++ * allocation at heap (+ data transfer). On the other hand ++ * it opens door to malicious peer to force stack to use more ++ * memory than necessary. ++ * ++ * The function here should asses a current stream quality: ++ * how many stream chunks are there ++ * the time elapsed since the arrival of earlier chunk ++ * the time elapsed since the application consumed the data ++ * the size of the chunk compared with the whole packet size ++ * the size of chunk with respect to DIRECT_STORAGE_SZ ++ * ... ++ * the code to collect those parameters is still missing, once ++ * this gap will be filled this function will be able to ++ * make the decision. ++ */ ++ ++ return 1; ++} ++ ++static struct stream_chunk_t *new_schunk(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, ++ UINT_RANGE *r, const unsigned char *data) ++{ ++ struct stream_chunk_t *sc; ++ uint64_t rsize; ++ ++ if (pkt == NULL) ++ return NULL; ++ ++ sc = OPENSSL_zalloc(sizeof(*sc)); ++ if (sc == NULL) ++ return NULL; ++ ++ if (keep_schunk_data_on_packet(fs, pkt, r) == 1) { ++ sc->sc_st = ST_TYPE_PKT; ++ sc->sc_pkt = pkt; ++ ossl_qrx_pkt_up_ref(pkt); ++ sc->sc_data = data; ++ sc->sc_range = *r; ++ } else { ++ rsize = r->end - r->start; ++ if (rsize <= DIRECT_STORAGE_SZ) { ++ DEBUG_PRINT(stderr, "%s ST_TYPE_DIRECT sc: %p %llu\n", OPENSSL_FUNC, ++ (void *)sc, rsize); ++ sc->sc_st = ST_TYPE_DIRECT; ++ sc->sc_data_w = sc->sc_dstorage; ++ } else { ++ DEBUG_PRINT(stderr, "%s ST_TYPE_HEAP sc: %p %llu\n", OPENSSL_FUNC, ++ (void *)sc, rsize); ++ sc->sc_st = ST_TYPE_HEAP; ++ sc->sc_buf = OPENSSL_malloc(UINT64_TO_SIZE_T(rsize)); ++ if (sc->sc_buf == NULL) { ++ OPENSSL_free(sc); ++ return NULL; ++ } ++ sc->sc_data_w = sc->sc_buf; ++ } ++ sc->sc_range = *r; ++ memcpy(sc->sc_data_w, data, UINT64_TO_SIZE_T(rsize)); ++ ++ if (fs->cleanse) ++ OPENSSL_cleanse(deconst(data), UINT64_TO_SIZE_T(rsize)); ++ } ++ ++ return sc; ++} ++ ++static void destroy_schunk(SFRAME_SET *fs, struct stream_chunk_t *sc) ++{ ++ if (sc == NULL) ++ return; ++ ++ if (fs->cleanse) ++ OPENSSL_cleanse(sc->sc_data_w, ++ UINT64_TO_SIZE_T(sc->sc_st == ST_TYPE_DIRECT ++ ? DIRECT_STORAGE_SZ ++ : SCHUNK_SIZE(sc))); ++ ++ switch (sc->sc_st) { ++ case ST_TYPE_PKT: ++ ossl_qrx_pkt_release(sc->sc_pkt); ++ break; ++ case ST_TYPE_HEAP: ++ OPENSSL_free(sc->sc_buf); ++ break; ++ default: ++ assert(sc->sc_st == ST_TYPE_DIRECT); ++ } ++ ++ OPENSSL_free(sc); ++} ++ ++static struct stream_range_t *new_srange(void) ++{ ++ struct stream_range_t *sr; ++ ++ sr = OPENSSL_zalloc(sizeof(*sr)); ++ if (sr != NULL) { ++ ossl_list_sc_init(&sr->sr_chunks); ++ } ++ ++ return sr; ++} ++ ++static void destroy_srange(SFRAME_SET *fs, struct stream_range_t *sr) ++{ ++ struct stream_chunk_t *sc; ++ ++ if (sr == NULL) ++ return; ++ ++ assert(sr->sr_rbe.rb_parent == NULL); ++ assert(sr->sr_rbe.rb_left == NULL); ++ assert(sr->sr_rbe.rb_right == NULL); ++ ++ while ((sc = ossl_list_sc_head(&sr->sr_chunks)) != NULL) { ++ ossl_list_sc_remove(&sr->sr_chunks, sc); ++ fs->stream_chunks--; ++ destroy_schunk(fs, sc); ++ } ++ ++ OPENSSL_free(sr); ++} ++ ++static struct stream_range_t *create_range(SFRAME_SET *fs, ++ struct stream_chunk_t *sc) ++{ ++ struct stream_range_t *sr; ++ ++ assert(sc != NULL); ++ ++ sr = new_srange(); ++ if (sr != NULL) { ++ ossl_list_sc_insert_head(&sr->sr_chunks, sc); ++ sr->sr_range = sc->sc_range; ++ fs->stream_chunks++; ++ } ++ ++ return sr; ++} ++ ++void ossl_sframe_set_init(SFRAME_SET *fs) ++{ ++ memset(fs, 0, sizeof(*fs)); ++ OSSL_RBT_INIT(srange, &fs->ranges); ++} ++ ++static uint64_t get_sc_dstorage_sz(struct stream_chunk_t *sc) ++{ ++ uint64_t sz = 0; ++ ++ if (sc->sc_st == ST_TYPE_DIRECT && SCHUNK_SIZE(sc) < DIRECT_STORAGE_SZ) ++ sz = DIRECT_STORAGE_SZ - SCHUNK_SIZE(sc); ++ ++ return sz; ++} ++ ++static int try_dstorage(SFRAME_SET *fs, OSSL_QRX_PKT *pkt, ++ struct stream_range_t *sr, UINT_RANGE *r, const unsigned char **data) ++{ ++ struct stream_chunk_t *head_sc, *tail_sc, *new_sc; ++ uint64_t rsize; ++ uint64_t offset; ++ uint64_t dsize; ++ ++ /* ++ * full overlap which spans over more range with more than 1 chunk, ++ * nothing to be done here, caller will handle that. ++ */ ++ rsize = r->end - r->start; ++ if (r->start < sr->sr_range.start && r->end > sr->sr_range.end ++ && rsize > DIRECT_STORAGE_SZ && ossl_list_sc_num(&sr->sr_chunks) > 1) ++ return 0; ++ ++ head_sc = ossl_list_sc_head(&sr->sr_chunks); ++ assert(head_sc != NULL); ++ tail_sc = ossl_list_sc_tail(&sr->sr_chunks); ++ assert(tail_sc != NULL); ++ ++ /* ++ * full overlap of direct storage can be treated when range contains ++ * exactly one chunk. ++ */ ++ if (head_sc == tail_sc ++ && head_sc->sc_range.start > r->start ++ && head_sc->sc_range.end < r->end) { ++ /* ++ * can deal with full overlap ++ */ ++ if (head_sc->sc_st != ST_TYPE_DIRECT) ++ return 0; ++ ++ DEBUG_PRINT(stderr, "%s @in %p [ %llu, %llu ]\n", ++ OPENSSL_FUNC, *data, r->start, r->end); ++ rsize = r->end - r->start; ++ if (rsize <= DIRECT_STORAGE_SZ) { ++ /* ++ * update existing chunk ++ */ ++ DEBUG_PRINT(stderr, ++ "%s overwrite dstorage %p [ %llu, %llu ] -> [ %llu, %llu ] " ++ "sr: %p [ %llu, %llu ]\n", ++ OPENSSL_FUNC, (void *)head_sc, ++ head_sc->sc_range.start, head_sc->sc_range.end, ++ r->start, r->end, ++ (void *)sr, sr->sr_range.start, sr->sr_range.end); ++ memcpy(head_sc->sc_data_w, *data, UINT64_TO_SIZE_T(rsize)); ++ ++ if (fs->cleanse) ++ OPENSSL_cleanse(deconst(*data), UINT64_TO_SIZE_T(rsize)); ++ ++ *data += rsize; ++ head_sc->sc_range.start = r->start; ++ head_sc->sc_range.end = r->end; ++ } else { ++ /* ++ * try to replace existing chunk ++ */ ++ new_sc = new_schunk(fs, pkt, r, *data); ++ if (new_sc == NULL) { ++ DEBUG_PRINT(stderr, "%s new_chunk() alloc failed\n", ++ OPENSSL_FUNC); ++ return -1; ++ } ++ ++ DEBUG_PRINT(stderr, ++ "%s replace chunk %p [ %llu, %llu ] -> %p [ %llu, %llu ]\n", ++ OPENSSL_FUNC, ++ (void *)head_sc, head_sc->sc_range.start, head_sc->sc_range.end, ++ (void *)new_sc, new_sc->sc_range.start, new_sc->sc_range.end); ++ ossl_list_sc_remove(&sr->sr_chunks, head_sc); ++ ossl_list_sc_insert_head(&sr->sr_chunks, new_sc); ++ destroy_schunk(fs, head_sc); ++ } ++ DEBUG_PRINT(stderr, "\trange: %p [ %llu, %llu ] -> [ %llu, %llu ]\n", ++ (void *)sr, sr->sr_range.start, sr->sr_range.end, r->start, r->end); ++ sr->sr_range.start = r->start; ++ sr->sr_range.end = r->end; ++ /* ++ * indicate that while range got consumed. ++ */ ++ r->start = 0; ++ r->end = 0; ++ } else if (tail_sc->sc_range.end < r->end) { ++ /* ++ * append only ++ */ ++ if (tail_sc->sc_st != ST_TYPE_DIRECT) ++ return 0; ++ ++ dsize = get_sc_dstorage_sz(tail_sc); ++ if (dsize == 0) ++ return 0; ++ ++ DEBUG_PRINT(stderr, "%s append: @in %p [ %llu, %llu ] dsize: %llu " ++ "tail_sc: %p [ %llu, %llu] sr: %p [ %llu, %llu ]\n", ++ OPENSSL_FUNC, *data, r->start, r->end, dsize, ++ (void *)tail_sc, tail_sc->sc_range.start, tail_sc->sc_range.end, ++ (void *)sr, sr->sr_range.start, sr->sr_range.end); ++ ++ if (r->start < tail_sc->sc_range.end) { ++ rsize = tail_sc->sc_range.end - r->start; ++ ++ if (fs->cleanse) ++ OPENSSL_cleanse(deconst(*data), UINT64_TO_SIZE_T(rsize)); ++ ++ *data += rsize; ++ ++ r->start = tail_sc->sc_range.end; ++ } ++ ++ rsize = r->end - r->start; ++ /* ++ * earlier check done in ossl_sframe_set_insert() ensures ++ * there is at least some data to append. ++ */ ++ assert(rsize > 0); ++ rsize = (rsize < dsize) ? rsize : dsize; ++ offset = SCHUNK_SIZE(tail_sc); ++ memcpy(&tail_sc->sc_data_w[offset], *data, UINT64_TO_SIZE_T(rsize)); ++ DEBUG_PRINT(stderr, "%s append tail_sc: %p [ %llu, %llu ] -> ", ++ OPENSSL_FUNC, (void *)tail_sc, ++ tail_sc->sc_range.start, tail_sc->sc_range.end); ++ tail_sc->sc_range.end += rsize; ++ DEBUG_PRINT(stderr, "[ %llu, %llu ]\n", ++ tail_sc->sc_range.start, tail_sc->sc_range.end); ++ assert(SCHUNK_SIZE(tail_sc) <= DIRECT_STORAGE_SZ); ++ DEBUG_PRINT(stderr, "\trange: %p [ %llu, %llu ] -> ", ++ (void *)sr, sr->sr_range.start, sr->sr_range.end); ++ sr->sr_range.end = tail_sc->sc_range.end; ++ DEBUG_PRINT(stderr, "[ %llu, %llu ]\n", ++ sr->sr_range.start, sr->sr_range.end); ++ ++ if (fs->cleanse) ++ OPENSSL_cleanse(deconst(*data), UINT64_TO_SIZE_T(rsize)); ++ ++ *data += rsize; ++ r->start = tail_sc->sc_range.end; ++ } else if (head_sc->sc_range.start > r->start) { ++ const unsigned char *data_buf; ++ /* ++ * prepend only ++ */ ++ if (head_sc->sc_st != ST_TYPE_DIRECT) ++ return 0; ++ ++ dsize = get_sc_dstorage_sz(head_sc); ++ if (dsize == 0) ++ return 0; ++ ++ DEBUG_PRINT(stderr, "%s prepend: @in %p [ %llu, %llu ] dsize: %llu " ++ "sr: %p [ %llu, %llu ]\n", ++ OPENSSL_FUNC, *data, r->start, r->end, dsize, ++ (void *)sr, sr->sr_range.start, sr->sr_range.end); ++ ++ if (r->end > head_sc->sc_range.start) { ++ if (fs->cleanse) ++ OPENSSL_cleanse( ++ deconst(*data + (head_sc->sc_range.start - r->start)), ++ UINT64_TO_SIZE_T(r->end - head_sc->sc_range.start)); ++ r->end = head_sc->sc_range.start; ++ } ++ ++ rsize = r->end - r->start; ++ /* ++ * earlier check done in ossl_sframe_set_insert() ensures ++ * there is at least some data to append. ++ */ ++ assert(rsize > 0); ++ rsize = (rsize < dsize) ? rsize : dsize; ++ memmove(&head_sc->sc_data_w[rsize], head_sc->sc_data, ++ UINT64_TO_SIZE_T(SCHUNK_SIZE(head_sc))); ++ offset = r->end - rsize - r->start; ++ assert(offset < r->end - r->start); ++ data_buf = *data; ++ memcpy(head_sc->sc_data_w, &data_buf[offset], UINT64_TO_SIZE_T(rsize)); ++ DEBUG_PRINT(stderr, "%s prepend head_sc: %p [ %llu, %llu ] -> ", ++ OPENSSL_FUNC, (void *)head_sc, ++ head_sc->sc_range.start, head_sc->sc_range.end); ++ head_sc->sc_range.start -= rsize; ++ DEBUG_PRINT(stderr, "[ %llu, %llu ]\n", ++ head_sc->sc_range.start, head_sc->sc_range.end); ++ assert(SCHUNK_SIZE(head_sc) <= DIRECT_STORAGE_SZ); ++ DEBUG_PRINT(stderr, "\trange: %p [ %llu, %llu ] -> ", ++ (void *)sr, sr->sr_range.start, sr->sr_range.end); ++ sr->sr_range.start = head_sc->sc_range.start; ++ DEBUG_PRINT(stderr, "[ %llu, %llu ]\n", ++ sr->sr_range.start, sr->sr_range.end); ++ assert(r->end - r->start >= rsize); ++ ++ if (fs->cleanse) ++ OPENSSL_cleanse(deconst(&data_buf[offset]), ++ UINT64_TO_SIZE_T(rsize)); ++ ++ r->end -= rsize; ++ } else { ++ assert(0); ++ return -1; ++ } ++ ++ /* ++ * returns 1 if all data were consumed ++ */ ++ assert(r->end >= r->start); ++ DEBUG_PRINT(stderr, "%s @out %p [ %llu, %llu ]\n", ++ OPENSSL_FUNC, *data, r->start, r->end); ++ ++ return ((r->end - r->start) == 0) ? 1 : 0; ++} ++ ++/* ++ * If there is partial overlap between newly received data `r` and ++ * existing stream range `sr`, then this function trims overlapping ++ * bytes from `r`. ++ * sr - pointer to stream range ++ * r - pointer to range of bytes received in stream frame ++ * data - pointer to data bytes delivered in stream frame ++ * function updates r so there is no partial overlap between and sr ++ * after function returns. Function returns pointer to the first ++ * data byte in stream after `r` is adjusted. Function returns `data` ++ * when no trimming happened. ++ */ ++static const unsigned char *trim_partial_overlap(SFRAME_SET *fs, ++ struct stream_range_t *sr, UINT_RANGE *r, const unsigned char *data) ++{ ++ uint64_t unused_sz; ++ ++ if (!(r->start < sr->sr_range.start && r->end > sr->sr_range.end)) { ++ if (r->end > sr->sr_range.end && r->start < sr->sr_range.end) { ++ unused_sz = sr->sr_range.end - r->start; ++ if (fs->cleanse) ++ OPENSSL_cleanse(deconst(data), UINT64_TO_SIZE_T(unused_sz)); ++ ++ DEBUG_PRINT(stderr, "%s right overlap %p [ %llu, %llu ]:\n\t" ++ "r: [ %llu, %llu ] -> [ %llu, %llu ]\n", ++ OPENSSL_FUNC, ++ (void *)sr, sr->sr_range.start, sr->sr_range.end, ++ r->start, r->end, ++ sr->sr_range.end, r->end); ++ ++ data += unused_sz; ++ r->start = sr->sr_range.end; ++ } else if (r->start < sr->sr_range.start ++ && r->end > sr->sr_range.start) { ++ unused_sz = r->end - sr->sr_range.start; ++ if (fs->cleanse) ++ OPENSSL_cleanse(deconst(data ++ + (sr->sr_range.start - r->start)), ++ UINT64_TO_SIZE_T(unused_sz)); ++ ++ DEBUG_PRINT(stderr, "%s left overlap %p [ %llu, %llu]:\n\t" ++ "r: [ %llu, %llu ] -> [ %llu, %llu ]\n", ++ OPENSSL_FUNC, ++ (void *)sr, sr->sr_range.start, sr->sr_range.end, ++ r->start, r->end, ++ r->start, sr->sr_range.start); ++ r->end = sr->sr_range.start; ++ } ++ } ++ ++ return data; ++} ++ ++/* ++ * Inserts a newly received chunk to the head of the chunk list. ++ */ ++static void prepend_chunk(SFRAME_SET *fs, struct stream_range_t *sr, ++ struct stream_chunk_t *sc) ++{ ++ DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] add to head %p [ %llu, %llu ] " ++ "-> [ %llu, %llu ]\n", ++ OPENSSL_FUNC, ++ (void *)sc, sc->sc_range.start, sc->sc_range.end, ++ (void *)sr, sr->sr_range.start, sr->sr_range.end, ++ sc->sc_range.start, sr->sr_range.end); ++ ++ /* ++ * the new chunk must not be empty ++ */ ++ assert(sc->sc_range.end > sc->sc_range.start); ++ /* ++ * and must not overlap range. ++ */ ++ assert(sc->sc_range.end == sr->sr_range.start); ++ ++ ossl_list_sc_insert_head(&sr->sr_chunks, sc); ++ sr->sr_range.start = sc->sc_range.start; ++ fs->stream_chunks++; ++} ++ ++/* ++ * Inserts a newly received chunk to the tail of the chunk list. ++ */ ++static void append_chunk(SFRAME_SET *fs, struct stream_range_t *sr, ++ struct stream_chunk_t *sc) ++{ ++ DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] add to tail %p [ %llu, %llu ] " ++ "-> [ %llu, %llu ]\n", ++ OPENSSL_FUNC, ++ (void *)sc, sc->sc_range.start, sc->sc_range.end, ++ (void *)sr, sr->sr_range.start, sr->sr_range.end, ++ sr->sr_range.start, sc->sc_range.end); ++ ++ /* ++ * the new chunk must not be empty ++ */ ++ assert(sc->sc_range.end > sc->sc_range.start); ++ /* ++ * and must not overlap range ++ */ ++ assert(sc->sc_range.start == sr->sr_range.end); ++ ++ ossl_list_sc_insert_tail(&sr->sr_chunks, sc); ++ sr->sr_range.end = sc->sc_range.end; ++ fs->stream_chunks++; ++} ++ ++static void replace_chunks_in_range(SFRAME_SET *fs, struct stream_range_t *sr, ++ struct stream_chunk_t *sc) ++{ ++ struct stream_chunk_t *destroy_sc; ++ ++ while ((destroy_sc = ossl_list_sc_head(&sr->sr_chunks)) != NULL) { ++ ossl_list_sc_remove(&sr->sr_chunks, destroy_sc); ++ fs->stream_chunks--; ++ destroy_schunk(fs, destroy_sc); ++ } ++ ++ ossl_list_sc_insert_head(&sr->sr_chunks, sc); ++ fs->stream_chunks++; ++ DEBUG_PRINT(stderr, "%s range: %p [ %llu, %llu ] -> [ %llu, %llu ]\n", ++ OPENSSL_FUNC, (void *)sr, sr->sr_range.start, sr->sr_range.end, ++ sc->sc_range.start, sc->sc_range.end); ++ sr->sr_range.start = sc->sc_range.start; ++ sr->sr_range.end = sc->sc_range.end; ++} ++ ++static struct stream_range_t *find_range(SFRAME_SET *fs, ++ struct stream_range_t *key) ++{ ++ struct stream_range_t *sr = NULL; ++ ++ if (!OSSL_RBT_EMPTY(srange, &fs->ranges)) ++ sr = OSSL_RBT_FIND(srange, &fs->ranges, key); ++ ++ return sr; ++} ++ ++/* ++ * This function help us to merge two ranges (list of chunks) ++ * into single range. Function moves the end of the range ++ * towards start. It effectively chops n last chunks until ++ * new_end is found. ++ */ ++static int chop_range(SFRAME_SET *fs, struct stream_range_t *sr, ++ uint64_t new_end) ++{ ++ struct stream_chunk_t *sc; ++ ++ assert(sr->sr_range.end >= new_end); ++ ++ while ((sc = ossl_list_sc_tail(&sr->sr_chunks)) != NULL) { ++ if (sc->sc_range.start >= new_end) { ++ ossl_list_sc_remove(&sr->sr_chunks, sc); ++ fs->stream_chunks--; ++ destroy_schunk(fs, sc); ++ } else { ++ break; ++ } ++ } ++ ++ if (sc == NULL) ++ return 0; ++ ++ assert(new_end <= sc->sc_range.end); ++ assert(sc->sc_range.start < new_end); ++ ++ unused_sz = UINT64_TO_SIZE_T(sc->sc_range.end - new_end); ++ if (unused_sz == 0) { ++ sr->sr_range.end = new_end; ++ return 1; ++ } ++ ++ sc_data_trim_right(sc, unused_sz, fs->cleanse); ++ sc->sc_range.end = new_end; ++ sr->sr_range.end = new_end; ++ ++ return 1; ++} ++ ++/* ++ * function merges two with full overlap. The super_sr range ++ * contains the whole sub_sr range. The function destroys ++ * sub_sr and returns super_sr. ++ */ ++static struct stream_range_t *merge_ranges(SFRAME_SET *fs, ++ struct stream_range_t *super_sr, struct stream_range_t *sub_sr) ++{ ++ /* ++ * both ranges must not be empty ++ */ ++ assert(super_sr->sr_range.start < super_sr->sr_range.end); ++ assert(sub_sr->sr_range.start < sub_sr->sr_range.end); ++ /* ++ * sub_sr and super_sr are equal ranges (sets) super_sr ++ * sub_sr is subset of super_sr (super_sr includes sub_sr). ++ */ ++ assert(super_sr->sr_range.start <= sub_sr->sr_range.start && super_sr->sr_range.end >= sub_sr->sr_range.end); ++ ++ DEBUG_PRINT(stderr, "%s super: %p [ %llu, %llu ], sub: %p [ %llu, %llu]\n", ++ OPENSSL_FUNC, (void *)super_sr, super_sr->sr_range.start, ++ super_sr->sr_range.end, (void *)sub_sr, sub_sr->sr_range.start, ++ sub_sr->sr_range.end); ++ destroy_srange(fs, sub_sr); ++ ++ return super_sr; ++} ++ ++/* ++ * The ranges are either adjacent ++ * (left_sr->sr_range.end == right_sr->sr_range.end) or there ++ * is partial overlap between left_sr and right_sr( ++ * (left_sr->sr_range.end >= right_sr->sr_range.start). ++ * If there is partial overlap, then the left range is chopped ++ * so its end is aligned with start of right_sr. ++ */ ++static struct stream_range_t *append_range(SFRAME_SET *fs, ++ struct stream_range_t *left_sr, struct stream_range_t *right_sr) ++{ ++ /* ++ * both ranges must not be empty ++ */ ++ assert(left_sr->sr_range.start < left_sr->sr_range.end); ++ assert(right_sr->sr_range.start < right_sr->sr_range.end); ++ /* ++ * right range follows left range (left < right) ++ */ ++ assert(left_sr->sr_range.end >= right_sr->sr_range.start); ++ ++ DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] + %p [ %llu, %llu ] = %p " ++ "[ %llu, %llu ]\n", ++ OPENSSL_FUNC, (void *)left_sr, left_sr->sr_range.start, ++ left_sr->sr_range.end, (void *)right_sr, right_sr->sr_range.start, ++ right_sr->sr_range.end, (void *)left_sr, ++ left_sr->sr_range.start, right_sr->sr_range.end); ++ ++ /* ++ * make sure there is no overlap between ranges ++ * (right_sr->sr_range.start == left_sr->sr_range.end) ++ */ ++ if (chop_range(fs, left_sr, right_sr->sr_range.start) == 0) ++ return NULL; ++ ++ ossl_list_sc_join(&left_sr->sr_chunks, &right_sr->sr_chunks); ++ left_sr->sr_range.end = right_sr->sr_range.end; ++ ++ destroy_srange(fs, right_sr); ++ ++ return left_sr; ++} ++ ++/* ++ * receives a chunk of data from stream frame. ++ */ ++int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *r, OSSL_QRX_PKT *pkt, ++ const unsigned char *data, int fin) ++{ ++ struct stream_range_t *sr = NULL; ++ struct stream_range_t *adjacent_sr = NULL; ++ struct stream_range_t *joined_sr = NULL; ++ struct stream_chunk_t *sc = NULL; ++ struct stream_range_t key_sr = { 0 }; ++ ++ /* ++ * receive the FIN frame if FIN frame. If FIN was not seen yet, ++ * then record FIN's offset (r->end). If FIN was received then ++ * verify FIN's offset match, error out on mismatch. ++ */ ++ if (fin != 0) { ++ if (fs->fin == 0) { ++ fs->fin = 1; ++ fs->fin_off = r->end; ++ } else if (fs->fin_off != r->end) { ++ return 0; ++ } ++ } ++ ++ /* ++ * discard any data past FIN offset (of FIN offset is set). ++ */ ++ if (fs->fin != 0) { ++ if (fs->fin_off < r->end) ++ r->end = fs->fin_off; /* truncate bytes beyond FIN */ ++ if (fs->fin_off < r->start) ++ return 0; ++ } ++ ++ if (r->end <= fs->offset) { ++ /* ++ * retransmitted range got consumed already. ++ */ ++ DEBUG_PRINT(stderr, "%s [ %llu, %llu ] <= %llu\n", OPENSSL_FUNC, ++ r->start, r->end, fs->offset); ++ if (fs->cleanse && data != NULL) ++ OPENSSL_cleanse(deconst(data), UINT64_TO_SIZE_T(r->end - r->start)); ++ return 1; ++ } ++ ++ if (r->start < fs->offset) { ++ /* ++ * Make sure retransmitted chunk does not reintroduce ++ * bytes which were consumed already. ++ * Make sure retransmitted chunk does not reintroduce ++ * bytes which were consumed already. ++ */ ++ DEBUG_PRINT(stderr, "%s [ %llu, %llu ] -> [ %llu, %llu ]\n", OPENSSL_FUNC, ++ r->start, r->end, fs->offset, r->end); ++ if (fs->cleanse) ++ OPENSSL_cleanse(deconst(data), ++ UINT64_TO_SIZE_T(fs->offset - r->start)); ++ data += fs->offset - r->start; ++ r->start = fs->offset; ++ } ++ ++ key_sr.sr_range = *r; ++ ++ /* ++ * Empty, 0 size chunk can carry the FIN bit only, ++ * and that has been just handled above. ++ */ ++ if (r->start == r->end) ++ return 1; ++ ++ assert(r->start < r->end); ++ ++ if ((sr = find_range(fs, &key_sr)) == NULL) { ++ sc = new_schunk(fs, pkt, r, data); ++ if (sc == NULL) ++ goto err; ++ ++ sr = create_range(fs, sc); ++ if (sr == NULL) ++ goto err; ++ DEBUG_PRINT(stderr, "%s chunk: %p [ %llu, %llu ] new range: %p\n", ++ OPENSSL_FUNC, (void *)sc, sc->sc_range.start, sc->sc_range.end, ++ (void *)sr); ++ sc = NULL; ++ OSSL_RBT_INSERT(srange, &fs->ranges, sr); ++ fs->stream_ranges++; ++ } else { ++ /* ++ * retransmission, the whole chunk is found in existing range already ++ */ ++ if (r->start >= sr->sr_range.start && r->end <= sr->sr_range.end) { ++ DEBUG_PRINT(stderr, ++ "%s [ %llu, %llu ] found in %p [ %llu, %llu ]\n", OPENSSL_FUNC, ++ r->start, r->end, (void *)sr, sr->sr_range.start, ++ sr->sr_range.end); ++ if (fs->cleanse) ++ OPENSSL_cleanse(deconst(data), ++ UINT64_TO_SIZE_T(r->end - r->start)); ++ goto done; /* Range is present already. */ ++ } ++ ++ switch (try_dstorage(fs, pkt, sr, r, &data)) { ++ case 0: ++ break; ++ case 1: ++ /* ++ * all data were consumed, range is updated. ++ */ ++ goto range_updated; ++ default: ++ /* ++ * malloc error. forget the range we found. ++ */ ++ sr = NULL; ++ goto err; ++ } ++ ++ /* ++ * full overlap between sr and r is handled by replace_chunks_in_range() ++ * we call after we allocate stream chunk sc for newly received range r. ++ */ ++ data = trim_partial_overlap(fs, sr, r, data); ++ ++ sc = new_schunk(fs, pkt, r, data); ++ if (sc == NULL) { ++ sr = NULL; ++ goto err; ++ } ++ DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] -> %p [ %llu, %llu ]\n", ++ OPENSSL_FUNC, (void *)sc, sc->sc_range.start, sc->sc_range.end, ++ (void *)sr, sr->sr_range.start, sr->sr_range.end); ++ ++ /* ++ * sandwich, append, prepend can still be improved to handle ++ * chunks with direct storage better, but I don't think it's ++ * worth the effort. out of order short data chunks (less ++ * than DIRECT_STORAGE_SZ) should be considered exceptional. ++ */ ++ if (sc->sc_range.start < sr->sr_range.start ++ && sc->sc_range.end > sr->sr_range.end) { ++ /* new chunk includes the whole range */ ++ replace_chunks_in_range(fs, sr, sc); ++ sc = NULL; /* chunk got consumed */ ++ } else if (sc->sc_range.end > sr->sr_range.end ++ && sc->sc_range.start <= sr->sr_range.end) { ++ append_chunk(fs, sr, sc); ++ sc = NULL; /* chunk got consumed */ ++ } else if (sc->sc_range.start < sr->sr_range.start ++ && sc->sc_range.end >= sr->sr_range.start) { ++ prepend_chunk(fs, sr, sc); ++ sc = NULL; /* chunk got consumed */ ++ } else { ++ assert(NULL); /* unreachable */ ++ sr = NULL; ++ goto err; ++ } ++ ++ range_updated: ++ /* ++ * Range got updated we may need to join updated range with ++ * another ranges which exist in tree. The current range ++ * is removed here and used as a search key. If nothing is found ++ * range is inserted back to tree. ++ * ++ * If another range is found the ranges are merged to single ++ * range. The process repeats (merging ranges may be cascade effect, ++ * where more ranges collapse to single range). The merge result is ++ * removed from tree and used as a search key to find another range. ++ * If nothing is found then update is done. otherwise the ranges ++ * are merged again. ++ */ ++ OSSL_RBT_REMOVE(srange, &fs->ranges, sr); ++ fs->stream_ranges--; ++ /* ++ * _INSERT() returns range where sr needs to be joined ++ */ ++ adjacent_sr = OSSL_RBT_INSERT(srange, &fs->ranges, sr); ++ ++ while (adjacent_sr != NULL) { ++ OSSL_RBT_REMOVE(srange, &fs->ranges, adjacent_sr); ++ DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] >< %p [ %llu, %llu ]\n", ++ OPENSSL_FUNC, (void *)sr, sr->sr_range.start, sr->sr_range.end, ++ (void *)adjacent_sr, adjacent_sr->sr_range.start, ++ adjacent_sr->sr_range.end); ++ fs->stream_ranges--; ++ ++ if (sr->sr_range.start <= adjacent_sr->sr_range.start && sr->sr_range.end >= adjacent_sr->sr_range.end) { ++ /* ++ * adjacent_sr subset of sr ++ */ ++ joined_sr = merge_ranges(fs, sr, adjacent_sr); ++ } else if (sr->sr_range.start >= adjacent_sr->sr_range.start && sr->sr_range.end <= adjacent_sr->sr_range.end) { ++ /* ++ * sr subset of adjacent_sr ++ */ ++ joined_sr = merge_ranges(fs, adjacent_sr, sr); ++ } else if (sr->sr_range.start < adjacent_sr->sr_range.start && sr->sr_range.end >= adjacent_sr->sr_range.start) { ++ /* ++ * adjacent_sr follows sr ++ */ ++ assert(sr->sr_range.end < adjacent_sr->sr_range.end); ++ joined_sr = append_range(fs, sr, adjacent_sr); ++ } else if (sr->sr_range.start <= adjacent_sr->sr_range.end && sr->sr_range.end > adjacent_sr->sr_range.end) { ++ /* ++ * sr follows adjacent_sr ++ */ ++ assert(sr->sr_range.end > adjacent_sr->sr_range.end); ++ joined_sr = append_range(fs, adjacent_sr, sr); ++ } else { ++ assert(NULL); /* never happens */ ++ joined_sr = NULL; ++ } ++ if (joined_sr == NULL) ++ goto err; ++ ++ sr = joined_sr; ++ adjacent_sr = OSSL_RBT_INSERT(srange, &fs->ranges, sr); ++ } ++ fs->stream_ranges++; ++ } ++ ++done: ++ return 1; ++ ++err: ++ destroy_schunk(fs, sc); ++ destroy_srange(fs, sr); ++ destroy_srange(fs, adjacent_sr); ++ /* ++ * not enough memory (or another serious error) has occurred, ++ * any error here is fatal as some stream chunks could be ACKed ++ * already (RFC 9000, 31.1 Packet processing). At least stream ++ * needs to be reset. Preferred action is to close connection. ++ */ ++ ++ return 0; ++} ++ ++/* ++ * peeks over the continuous range which is ready to ++ * read. ready to read means the fs->offset must be ++ * found in range. Also fs->offset can not reach past ++ * the first gap in stream data received so far, thus ++ * the only range we can use for peek operation is ++ * OSSL_RBT_MIN(&fs->ranges). ++ * ++ * NOTE: it is unsafe to carry more _peek() operations ++ * over single SFRMAE_SET. ++ */ ++int ossl_sframe_set_peek(SFRAME_SET *fs, void **iterator, ++ UINT_RANGE *range, const unsigned char **data, ++ int *fin) ++{ ++ uint64_t start; ++ struct stream_range_t *sr = (struct stream_range_t *)*iterator; ++ struct stream_chunk_t *sc = NULL; ++ ++ if (sr == NULL) { ++ sr = OSSL_RBT_MIN(srange, &fs->ranges); ++ start = fs->offset; ++ if (sr != NULL) { ++ sc = ossl_list_sc_head(&sr->sr_chunks); ++ sr->sr_it_sc = NULL; ++ assert(sc->sc_range.start == sr->sr_range.start); ++ } ++ /* ++ * no chunks are ready to be consumed, if there is a gap. ++ */ ++ if (sc != NULL && sc->sc_range.start > start) { ++ DEBUG_PRINT(stderr, "%s sc: %p sr: %p sc->start %llu, fs->offset: %llu\n", ++ OPENSSL_FUNC, (void *)sc, (void *)sr, sc->sc_range.start, start); ++ sc = NULL; ++ } ++ } else if (sr == OSSL_RBT_MIN(srange, &fs->ranges) && sr->sr_it_sc != NULL) { ++ /* ++ * sr == _RB_MIN(), revalidates iterator in case the range we ++ * work with disappears because it's got joined with other range ++ * after new chunk arrival. perhaps not issue now as those operations ++ * are mutually exclusive now. ++ * ++ * sr->sr_it_sc becomes NULL on _move() or _flatten() operation. ++ * ++ * We may need to revisit iterator implementation as current ++ * iterator supports one caller only. ++ */ ++ start = sr->sr_it_sc->sc_range.end; ++ sc = ossl_list_sc_next(sr->sr_it_sc); ++ assert(sc == NULL || sc->sc_range.start == start); ++ assert(sc == NULL || sc->sc_range.start < sc->sc_range.end); ++ } else { ++ /* iterator got invalidated by move/flatten operation on range */ ++ DEBUG_PRINT(stderr, "%s iterator got invalidated\n", OPENSSL_FUNC); ++ return 0; ++ } ++ ++ range->start = start; ++ ++ if (sc == NULL) { ++ range->end = start; ++ *data = NULL; ++ *iterator = NULL; ++ ++ /* ++ * set fin only if we are at the end of the stream and application ++ * can read from the stream. In other words: there must be no gap ++ * between FIN offset and offset where application reads from stream. ++ */ ++ if (fs->fin && start == fs->fin_off) ++ *fin = fs->fin; ++ else ++ *fin = 0; ++ ++ DEBUG_PRINT(stderr, "%s no more chunks\n", OPENSSL_FUNC); ++ ++ return 0; ++ } ++ ++ range->end = sc->sc_range.end; ++ /* chunk keeps data always attached, data dies with chunk */ ++ assert(sc->sc_data != NULL); ++ assert(sc->sc_range.start <= start); ++ *data = sc->sc_data + (start - sc->sc_range.start); ++ *fin = fs->fin && sc->sc_range.end == fs->fin_off; ++ ++ if (sr->sr_it_sc != NULL) ++ DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] %p [ %llu, %llu ]\n", ++ OPENSSL_FUNC, (void *)sr->sr_it_sc, ++ sr->sr_it_sc->sc_range.start, sr->sr_it_sc->sc_range.end, ++ (void *)sc, sc->sc_range.start, sc->sc_range.end); ++ ++ sr->sr_it_sc = sc; ++ *iterator = sr; ++ ++ /* ++ * peek operation indicates error if there are no data to read ++ * in range. ++ */ ++ DEBUG_PRINT(stderr, ++ "%s peek range: [ %llu, %llu ] range: %p [ %llu, %llu ]\n", OPENSSL_FUNC, ++ range->start, range->end, (void *)sr, sr->sr_range.start, ++ sr->sr_range.end); ++ ++ return (range->start == range->end) ? 0 : 1; ++} ++ ++void ossl_sframe_set_destroy_ranges(SFRAME_SET *fs) ++{ ++ struct stream_range_t *sr, *save_sr; ++ ++ OSSL_RBT_FOREACH_SAFE (sr, srange, &fs->ranges, save_sr) { ++ OSSL_RBT_REMOVE(srange, &fs->ranges, sr); ++ fs->stream_ranges--; ++ destroy_srange(fs, sr); ++ } ++} ++ ++/* ++ * moves the read offset, freeing all chunks which end offset ++ * is less than new_offset ++ * sc->sc_range.end < new_offset ++ */ ++int ossl_sframe_set_move_offset(SFRAME_SET *fs, uint64_t new_offset) ++{ ++ struct stream_range_t *sr = OSSL_RBT_MIN(srange, &fs->ranges); ++ struct stream_chunk_t *sc, *save_sc; ++ ++ if (new_offset == fs->offset) ++ return 1; ++ ++ /* ++ * Offset can move forward within the continuous head range only. ++ * It can not move backward, into a gap or past the head range end. ++ */ ++ if (sr == NULL || new_offset < fs->offset ++ || new_offset < sr->sr_range.start || new_offset > sr->sr_range.end) ++ return 0; ++ ++ fs->offset = new_offset; ++ ++ OSSL_LIST_FOREACH_DELSAFE (sc, save_sc, sc, &sr->sr_chunks) { ++ if (new_offset >= sc->sc_range.end) { ++ ossl_list_sc_remove(&sr->sr_chunks, sc); ++ fs->stream_chunks--; ++ if (sr->sr_it_sc == sc) ++ sr->sr_it_sc = NULL; /* invalidate iterator chunk */ ++ destroy_schunk(fs, sc); ++ } else { ++ break; ++ } ++ } ++ ++ DEBUG_PRINT(stderr, "%s offset: %llu -> %llu range: %p [ %llu, %llu ] -> ", ++ OPENSSL_FUNC, fs->offset - new_offset, new_offset, ++ (void *)sr, sr->sr_range.start, sr->sr_range.end); ++ ++ if (sc == NULL) { ++ /* ++ * the whole range was consumed. ++ * this step invalidates iterator we use in ossl_sframe_peek() ++ */ ++ OSSL_RBT_REMOVE(srange, &fs->ranges, sr); ++ destroy_srange(fs, sr); ++ fs->stream_ranges--; ++ DEBUG_PRINT(stderr, "[ NULL ]\n"); ++ } else { ++ unused_sz = UINT64_TO_SIZE_T(new_offset - sc->sc_range.start); ++ sc_data_trim_left(sc, unused_sz, fs->cleanse); ++ sc->sc_range.start = new_offset; ++ sr->sr_range.start = new_offset; ++ DEBUG_PRINT(stderr, "[ %lli, %llu ]\n", ++ sr->sr_range.start, sr->sr_range.end); ++ } ++ ++ return 1; ++} ++ ++/* Contiguous bytes available from fs->offset, in O(log n): the first range. */ ++int ossl_sframe_set_avail(SFRAME_SET *fs, uint64_t *avail, int *fin) ++{ ++ struct stream_range_t *sr = OSSL_RBT_MIN(srange, &fs->ranges); ++ ++ if (sr != NULL && sr->sr_range.start <= fs->offset ++ && sr->sr_range.end > fs->offset) ++ *avail = sr->sr_range.end - fs->offset; ++ else ++ *avail = 0; ++ *fin = (fs->fin && fs->offset + *avail == fs->fin_off) ? 1 : 0; ++ return 1; ++} +diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c +index c17fe934901a..b5fd755a54d9 100644 +--- a/test/quic_stream_test.c ++++ b/test/quic_stream_test.c +@@ -413,8 +413,7 @@ static int test_rstream_simple(int idx) + unsigned char buf[sizeof(simple_data)]; + size_t readbytes = 0, avail = 0, i; + int fin = 0; +- int use_sc = (idx & 1) != 0; +- int use_rbuf = (idx & 2) != 0; ++ int use_sc = idx % 2; + int (*read_fn)(QUIC_RSTREAM *, unsigned char *, size_t, size_t *, + int *) + = use_sc ? test_single_copy_read +@@ -425,7 +424,7 @@ static int test_rstream_simple(int idx) + if (!TEST_ptr(pkt[i] = pkt_test_new(1200))) + goto err; + +- if (!TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, 0))) ++ if (!TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL))) + goto err; + + if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[0], 5, +@@ -449,11 +448,6 @@ static int test_rstream_simple(int idx) + || !TEST_false(fin) + || !TEST_size_t_eq(readbytes, 1) + || !TEST_mem_eq(buf, 1, simple_data, 1) +- || (use_rbuf && !TEST_false(ossl_quic_rstream_move_to_rbuf(rstream))) +- || (use_rbuf +- && !TEST_true(ossl_quic_rstream_resize_rbuf(rstream, +- sizeof(simple_data)))) +- || (use_rbuf && !TEST_true(ossl_quic_rstream_move_to_rbuf(rstream))) + || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[4], + 0, simple_data, + 10, 0)) +@@ -485,10 +479,6 @@ static int test_rstream_simple(int idx) + sizeof(simple_data), + NULL, + 0, 1)) +- || (use_rbuf +- && !TEST_true(ossl_quic_rstream_resize_rbuf(rstream, +- 2 * sizeof(simple_data)))) +- || (use_rbuf && !TEST_true(ossl_quic_rstream_move_to_rbuf(rstream))) + || !TEST_true(read_fn(rstream, buf + 14, 5, &readbytes, &fin)) + || !TEST_false(fin) + || !TEST_size_t_eq(readbytes, 5) +@@ -498,7 +488,6 @@ static int test_rstream_simple(int idx) + || !TEST_true(fin) + || !TEST_size_t_eq(readbytes, sizeof(buf) - 14 - 5) + || !TEST_mem_eq(buf, sizeof(buf), simple_data, sizeof(simple_data)) +- || (use_rbuf && !TEST_true(ossl_quic_rstream_move_to_rbuf(rstream))) + || !TEST_true(read_fn(rstream, buf, sizeof(buf), &readbytes, &fin)) + || !TEST_true(fin) + || !TEST_size_t_eq(readbytes, 0)) +@@ -535,7 +524,7 @@ static int test_rstream_random(int idx) + if (!TEST_ptr(bulk_data = OPENSSL_malloc(data_size)) + || !TEST_ptr(read_buf = OPENSSL_malloc(data_size)) + || !TEST_ptr(pkts = OPENSSL_zalloc(sizeof(*pkts) * max_pkts)) +- || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, 0))) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL))) + goto err; + + if (idx % 3 == 0) +@@ -611,11 +600,6 @@ static int test_rstream_random(int idx) + goto err; + read_off += readbytes; + queued_min = read_off; +- if (test_random() % 50 == 0) +- if (!TEST_true(ossl_quic_rstream_resize_rbuf(rstream, +- queued_max - read_off + 1)) +- || !TEST_true(ossl_quic_rstream_move_to_rbuf(rstream))) +- goto err; + if (!fin_set && queued_max >= data_size - test_random() % 200) { + fin_set = 1; + /* Queue empty fin frame */ +diff --git a/test/quic_txp_test.c b/test/quic_txp_test.c +index 6b5a63e596b3..010297235ce7 100644 +--- a/test/quic_txp_test.c ++++ b/test/quic_txp_test.c +@@ -1500,7 +1500,7 @@ static int run_script(int script_idx, const struct script_op *script) + 16 * 1024 * 1024, + fake_now, NULL)) + || !TEST_ptr(s->rstream = ossl_quic_rstream_new(&s->rxfc, +- NULL, 1024))) { ++ NULL))) { + ossl_quic_sstream_free(s->sstream); + ossl_quic_stream_map_release(h.args.qsm, s); + goto err; diff -Nru openssl-3.5.7/debian/patches/TLS-Reject-undersized-TLS-1.2-AEAD-records-before-AEAD-pr.patch openssl-3.5.7/debian/patches/TLS-Reject-undersized-TLS-1.2-AEAD-records-before-AEAD-pr.patch --- openssl-3.5.7/debian/patches/TLS-Reject-undersized-TLS-1.2-AEAD-records-before-AEAD-pr.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/TLS-Reject-undersized-TLS-1.2-AEAD-records-before-AEAD-pr.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,297 @@ +From: Daniel Kubec +Date: Thu, 3 Sep 2026 14:28:23 +0200 +Subject: TLS: Reject undersized TLS 1.2 AEAD records before AEAD processing + +In tls1_cipher() the receive path invoked EVP_CTRL_AEAD_TLS1_AAD before +checking that the record was long enough to contain the mandatory +explicit IV and authentication tag. A record shorter than that overhead +made the provider reject the impossible length, and the error path +raised SSL_AD_INTERNAL_ERROR. + +Validate the record length against the explicit IV and tag length before +AEAD processing and return failure without raising an alert, leaving the +caller to react correctly: TLS reports bad_record_mac and DTLS silently +discards the record. + +Co-Authored-By: Mounir IDRASSI + +Fixes CVE-2026-75806 +--- + ssl/record/methods/tls1_meth.c | 8 ++ + test/recordlentest.c | 218 ++++++++++++++++++++++++++++++++++++++++- + 2 files changed, 225 insertions(+), 1 deletion(-) + +diff --git a/ssl/record/methods/tls1_meth.c b/ssl/record/methods/tls1_meth.c +index ed4a436dffc1..6bf7bb46e3aa 100644 +--- a/ssl/record/methods/tls1_meth.c ++++ b/ssl/record/methods/tls1_meth.c +@@ -261,6 +261,14 @@ static int tls1_cipher(OSSL_RECORD_LAYER *rl, TLS_RL_RECORD *recs, + != 0) { + unsigned char *seq; + ++ /* ++ * Publicly invalid: the record is shorter than the mandatory ++ * AEAD overhead. Leave alert handling to the caller so TLS ++ * reports bad_record_mac and DTLS silently discards the record. ++ */ ++ if (!sending && reclen[ctr] < rl->eivlen + rl->taglen) ++ return 0; ++ + seq = rl->sequence; + + if (rl->isdtls) { +diff --git a/test/recordlentest.c b/test/recordlentest.c +index c38ca3903f12..01003ffd7f90 100644 +--- a/test/recordlentest.c ++++ b/test/recordlentest.c +@@ -1,5 +1,5 @@ + /* +- * Copyright 2017-2023 The OpenSSL Project Authors. All Rights Reserved. ++ * Copyright 2017-2026 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the Apache License 2.0 (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy +@@ -9,6 +9,8 @@ + + #include + ++#include ++ + #include "helpers/ssltestlib.h" + #include "testutil.h" + +@@ -60,6 +62,31 @@ static int write_record(BIO *b, size_t len, uint8_t rectype, int recversion) + return 1; + } + ++#if !defined(OPENSSL_NO_DTLS1_2) ++static int write_dtls_record(BIO *b, size_t len, uint8_t rectype, ++ int recversion, uint16_t epoch, uint64_t seq) ++{ ++ unsigned char record[DTLS1_RT_HEADER_LENGTH + 256] = { 0 }; ++ size_t written, i; ++ ++ if (len > sizeof(record) - DTLS1_RT_HEADER_LENGTH) ++ return 0; ++ ++ record[0] = rectype; ++ record[1] = (recversion >> 8) & 0xff; ++ record[2] = recversion & 0xff; ++ record[3] = (epoch >> 8) & 0xff; ++ record[4] = epoch & 0xff; ++ for (i = 0; i < 6; i++) ++ record[10 - i] = (seq >> (8 * i)) & 0xff; ++ record[11] = (len >> 8) & 0xff; ++ record[12] = len & 0xff; ++ ++ return BIO_write_ex(b, record, DTLS1_RT_HEADER_LENGTH + len, &written) ++ && written == DTLS1_RT_HEADER_LENGTH + len; ++} ++#endif ++ + static int fail_due_to_record_overflow(int enc) + { + long err = ERR_peek_error(); +@@ -181,6 +208,188 @@ static int test_record_overflow(int idx) + return testresult; + } + ++static int write_and_read_app_data(SSL *serverssl, SSL *clientssl) ++{ ++ static const unsigned char msg = 0xa5; ++ unsigned char buf = 0; ++ size_t readbytes = 0, written = 0; ++ ++ return TEST_true(SSL_write_ex(clientssl, &msg, sizeof(msg), &written)) ++ && TEST_size_t_eq(written, sizeof(msg)) ++ && TEST_true(SSL_read_ex(serverssl, &buf, sizeof(buf), ++ &readbytes)) ++ && TEST_size_t_eq(readbytes, sizeof(msg)) ++ && TEST_uchar_eq(buf, msg); ++} ++ ++typedef enum { ++ SHORT_AEAD_RECORD_AES, ++ SHORT_AEAD_RECORD_ARIA, ++ SHORT_AEAD_RECORD_CHACHA ++} SHORT_AEAD_RECORD_CIPHER; ++ ++typedef struct { ++ const char *cipher; ++ size_t record_len; ++ SHORT_AEAD_RECORD_CIPHER cipher_type; ++} SHORT_AEAD_RECORD_TEST; ++ ++#if !defined(OPENSSL_NO_TLS1_2) ++static const SHORT_AEAD_RECORD_TEST short_aead_record_tests[] = { ++ { TLS1_TXT_RSA_WITH_AES_128_CCM, 0, SHORT_AEAD_RECORD_AES }, ++ { TLS1_TXT_RSA_WITH_AES_128_CCM, ++ EVP_CCM_TLS_EXPLICIT_IV_LEN + EVP_CCM_TLS_TAG_LEN - 1, ++ SHORT_AEAD_RECORD_AES }, ++ { TLS1_TXT_RSA_WITH_AES_128_CCM_8, ++ EVP_CCM_TLS_EXPLICIT_IV_LEN + EVP_CCM8_TLS_TAG_LEN - 1, ++ SHORT_AEAD_RECORD_AES }, ++ { TLS1_TXT_RSA_WITH_AES_128_GCM_SHA256, ++ EVP_GCM_TLS_EXPLICIT_IV_LEN + EVP_GCM_TLS_TAG_LEN - 1, ++ SHORT_AEAD_RECORD_AES }, ++ { TLS1_TXT_RSA_WITH_ARIA_128_GCM_SHA256, ++ EVP_GCM_TLS_EXPLICIT_IV_LEN + EVP_GCM_TLS_TAG_LEN - 1, ++ SHORT_AEAD_RECORD_ARIA }, ++ { TLS1_TXT_ECDHE_RSA_WITH_CHACHA20_POLY1305, ++ EVP_CHACHAPOLY_TLS_TAG_LEN - 1, SHORT_AEAD_RECORD_CHACHA }, ++}; ++#endif ++ ++static void alert_cb(int write_p, int version, int content_type, ++ const void *buf, size_t len, SSL *ssl, void *arg) ++{ ++ unsigned char *alert = arg; ++ const unsigned char *alert_data = buf; ++ ++ if (write_p && content_type == SSL3_RT_ALERT && len == 2) { ++ alert[0] = alert_data[0]; ++ alert[1] = alert_data[1]; ++ } ++} ++ ++#ifndef OPENSSL_NO_TLS1_2 ++static int test_tls12_short_aead_record(int idx) ++{ ++ const SHORT_AEAD_RECORD_TEST *test = &short_aead_record_tests[idx]; ++ SSL_CTX *cctx = NULL, *sctx = NULL; ++ SSL *clientssl = NULL, *serverssl = NULL; ++ int testresult = 0; ++ size_t readbytes; ++ unsigned char buf, alert[2] = { 0 }; ++ ++#ifdef OPENSSL_NO_AES ++ if (test->cipher_type == SHORT_AEAD_RECORD_AES) ++ return TEST_skip("AES is disabled"); ++#endif ++#ifdef OPENSSL_NO_ARIA ++ if (test->cipher_type == SHORT_AEAD_RECORD_ARIA) ++ return TEST_skip("ARIA is disabled"); ++#endif ++#if defined(OPENSSL_NO_CHACHA) || defined(OPENSSL_NO_POLY1305) \ ++ || defined(OPENSSL_NO_EC) ++ if (test->cipher_type == SHORT_AEAD_RECORD_CHACHA) ++ return TEST_skip("ChaCha20-Poly1305 or EC is disabled"); ++#endif ++ ++ if (!TEST_true(create_ssl_ctx_pair(NULL, TLS_server_method(), ++ TLS_client_method(), TLS1_2_VERSION, TLS1_2_VERSION, ++ &sctx, &cctx, cert, privkey))) ++ goto end; ++ ++ /* CCM-8 is not available at the default security level. */ ++ SSL_CTX_set_security_level(sctx, 0); ++ SSL_CTX_set_security_level(cctx, 0); ++ ++ if (!TEST_true(SSL_CTX_set_cipher_list(sctx, test->cipher)) ++ || !TEST_true(SSL_CTX_set_cipher_list(cctx, test->cipher)) ++ || !TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl, ++ NULL, NULL))) ++ goto end; ++ ++ SSL_set_msg_callback(serverssl, alert_cb); ++ SSL_set_msg_callback_arg(serverssl, &alert); ++ ++ if (!TEST_true(create_ssl_connection(serverssl, clientssl, ++ SSL_ERROR_NONE)) ++ || !TEST_true(write_and_read_app_data(serverssl, clientssl)) ++ || !TEST_true(write_record(SSL_get_rbio(serverssl), test->record_len, ++ SSL3_RT_APPLICATION_DATA, TLS1_2_VERSION)) ++ || !TEST_false(SSL_read_ex(serverssl, &buf, sizeof(buf), &readbytes)) ++ || !TEST_uchar_eq(alert[0], SSL3_AL_FATAL) ++ || !TEST_uchar_eq(alert[1], SSL_AD_BAD_RECORD_MAC)) ++ goto end; ++ ++ testresult = 1; ++end: ++ SSL_free(serverssl); ++ SSL_free(clientssl); ++ SSL_CTX_free(sctx); ++ SSL_CTX_free(cctx); ++ return testresult; ++} ++#endif ++ ++#ifndef OPENSSL_NO_DTLS1_2 ++static int test_dtls12_short_aead_record(void) ++{ ++ SSL_CTX *cctx = NULL, *sctx = NULL; ++ SSL *clientssl = NULL, *serverssl = NULL; ++ int testresult = 0, ret; ++ size_t readbytes = 0; ++ unsigned char buf = 0, alert[2] = { 0 }; ++ ++#ifdef OPENSSL_NO_AES ++ return TEST_skip("AES is disabled"); ++#endif ++ ++ if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(), ++ DTLS_client_method(), DTLS1_2_VERSION, DTLS1_2_VERSION, ++ &sctx, &cctx, cert, privkey))) ++ goto end; ++ ++ SSL_CTX_set_security_level(sctx, 0); ++ SSL_CTX_set_security_level(cctx, 0); ++ ++ if (!TEST_true(SSL_CTX_set_cipher_list(sctx, ++ TLS1_TXT_RSA_WITH_AES_128_GCM_SHA256)) ++ || !TEST_true(SSL_CTX_set_cipher_list(cctx, ++ TLS1_TXT_RSA_WITH_AES_128_GCM_SHA256)) ++ || !TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl, ++ NULL, NULL))) ++ goto end; ++ ++ SSL_set_msg_callback(serverssl, alert_cb); ++ SSL_set_msg_callback_arg(serverssl, &alert); ++ ++ /* ++ * The forged record uses the first application-data sequence number. A ++ * valid client write after this must still be accepted. ++ */ ++ if (!TEST_true(create_ssl_connection(serverssl, clientssl, ++ SSL_ERROR_NONE)) ++ || !TEST_true(write_dtls_record(SSL_get_rbio(serverssl), ++ EVP_GCM_TLS_EXPLICIT_IV_LEN + EVP_GCM_TLS_TAG_LEN - 1, ++ SSL3_RT_APPLICATION_DATA, DTLS1_2_VERSION, 1, 1))) ++ goto end; ++ ++ ERR_clear_error(); ++ ret = SSL_read_ex(serverssl, &buf, sizeof(buf), &readbytes); ++ if (!TEST_false(ret) ++ || !TEST_int_eq(SSL_get_error(serverssl, ret), SSL_ERROR_WANT_READ) ++ || !TEST_uchar_eq(alert[0], 0) ++ || !TEST_uchar_eq(alert[1], 0) ++ || !TEST_true(write_and_read_app_data(serverssl, clientssl))) ++ goto end; ++ ++ testresult = 1; ++end: ++ SSL_free(serverssl); ++ SSL_free(clientssl); ++ SSL_CTX_free(sctx); ++ SSL_CTX_free(cctx); ++ return testresult; ++} ++#endif ++ + OPT_TEST_DECLARE_USAGE("certfile privkeyfile\n") + + int setup_tests(void) +@@ -195,6 +404,13 @@ int setup_tests(void) + return 0; + + ADD_ALL_TESTS(test_record_overflow, TOTAL_RECORD_OVERFLOW_TESTS); ++#ifndef OPENSSL_NO_TLS1_2 ++ ADD_ALL_TESTS(test_tls12_short_aead_record, ++ OSSL_NELEM(short_aead_record_tests)); ++#endif ++#ifndef OPENSSL_NO_DTLS1_2 ++ ADD_TEST(test_dtls12_short_aead_record); ++#endif + return 1; + } + diff -Nru openssl-3.5.7/debian/patches/don-t-double-count-full-databgram-length-on-unvalidated-c.patch openssl-3.5.7/debian/patches/don-t-double-count-full-databgram-length-on-unvalidated-c.patch --- openssl-3.5.7/debian/patches/don-t-double-count-full-databgram-length-on-unvalidated-c.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/don-t-double-count-full-databgram-length-on-unvalidated-c.patch 2026-09-29 20:10:10.000000000 +0000 @@ -0,0 +1,53 @@ +From: Neil Horman +Date: Tue, 25 Aug 2026 14:23:50 -0400 +Subject: don't double count full databgram length on unvalidated connections + +If a connection is coalescing frames, we pass through +ossl_quic_handle_frames multiple times, each time accounting the full +datagram length to the connection, erroneously amplifying our +unvalidated credit. + +Fix it by moving where we account unvalidated credit. If we move the +adding of unvalidated credit to port_default_packet_handler, we can add +the datagram length to the channels unvalidated credit before it gets +broken up into multiple OSSL_QRX_PKT structures. + +Fixes CVE-2026-35191 +--- + ssl/quic/quic_port.c | 2 ++ + ssl/quic/quic_rx_depack.c | 2 -- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/ssl/quic/quic_port.c b/ssl/quic/quic_port.c +index aad9c3a5b3d5..6ddc1322897a 100644 +--- a/ssl/quic/quic_port.c ++++ b/ssl/quic/quic_port.c +@@ -1510,6 +1510,7 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg, + && ossl_quic_lcidm_lookup(port->lcidm, dcid, NULL, + (void **)&ch)) { + assert(ch != NULL); ++ ossl_quic_tx_packetiser_add_unvalidated_credit(ch->txp, e->data_len); + ossl_quic_channel_inject(ch, e); + return; + } +@@ -1721,6 +1722,7 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg, + * Time to reinject packets from qrx to channel before + * qrx will be destroyed here. + */ ++ ossl_quic_tx_packetiser_add_unvalidated_credit(new_ch->txp, e->data_len); + while (ossl_qrx_read_pkt(qrx_src, &qrx_pkt) == 1) + ossl_quic_channel_inject_pkt(new_ch, qrx_pkt); + ossl_qrx_update_pn_space(qrx_src, new_ch->qrx); +diff --git a/ssl/quic/quic_rx_depack.c b/ssl/quic/quic_rx_depack.c +index 7ab59f01a1cd..0ead9097016e 100644 +--- a/ssl/quic/quic_rx_depack.c ++++ b/ssl/quic/quic_rx_depack.c +@@ -1486,8 +1486,6 @@ int ossl_quic_handle_frames(QUIC_CHANNEL *ch, OSSL_QRX_PKT *qpacket) + */ + if (enc_level == QUIC_ENC_LEVEL_HANDSHAKE) + ossl_quic_tx_packetiser_set_validated(ch->txp); +- else +- ossl_quic_tx_packetiser_add_unvalidated_credit(ch->txp, dgram_len); + + /* Now that special cases are out of the way, parse frames */ + if (!PACKET_buf_init(&pkt, qpacket->hdr->data, qpacket->hdr->len) diff -Nru openssl-3.5.7/debian/patches/dtls-reset-init_off-before-retransmitting-a-message.patch openssl-3.5.7/debian/patches/dtls-reset-init_off-before-retransmitting-a-message.patch --- openssl-3.5.7/debian/patches/dtls-reset-init_off-before-retransmitting-a-message.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/dtls-reset-init_off-before-retransmitting-a-message.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,535 @@ +From: Ryan Hooper +Date: Wed, 2 Sep 2026 15:15:41 -0400 +Subject: dtls: reset init_off before retransmitting a message + +dtls1_retransmit_message() copies a queued message's saved bytes into +s->init_buf and sets s->init_num to its length, but left s->init_off +untouched. If a different message was still mid-write (suspended on +WANT_WRITE, e.g. because the underlying BIO couldn't take any more) +when a retransmit fired, s->init_off would still hold whatever offset +that suspended write had reached - and dtls1_do_write() would resume +the retransmitted message from that stale, unrelated offset instead of +from the start. + +The result is a corrupted retransmission: the message goes out +mislabelled with a bogus fragment offset/length, and its body is +whatever bytes happen to sit at that offset in the now-repurposed +init_buf - typically leftover content from the other, larger message +that was mid-write when the retransmit happened, rather than the +message actually meant to be sent. + +Fix this by always resetting s->init_off to 0 before resending, so +every retransmit starts from the beginning of the message being sent, +regardless of what any other in-progress write had left behind. + +dtls1_handle_timeout() also needs a guard: if write_state is anything +other than WRITE_STATE_TRANSITION, a write is still parked mid-flight +from a previous call into the state machine, so there is nothing valid +to retransmit yet. Retransmitting anyway would reconstruct an +already-sent message from the retransmit queue into +s->init_buf/s->init_off/s->init_num/s->d1->w_msg - the same fields the +parked write is still using - corrupting that write's state out from +under it. Skip retransmission in that case and let the next +SSL_read()/SSL_write()/SSL_accept()/SSL_connect() call resume the +parked write normally instead. + +Add test_dtls_client_retransmit() and test_dtls_server_retransmit() to +test/dtlstest.c, covering both directions on DTLS1.2: use a +fragment-limiting BIO to suspend a large in-flight write mid-fragment, +then fire the retransmit timer several times via +DTLSv1_get_timeout()/DTLSv1_handle_timeout() while it stays parked, +verifying no retransmission is attempted and the suspended write's +state is reproduced unchanged on every timer fire, and that resuming +the write afterward completes as an ordinary handshake continuation +instead of hitting dtls1_do_write()'s entry assertion. + +Assited-by: Claude:claude-sonnet-5 +Fixes: CVE-2026-84782 +--- + ssl/d1_lib.c | 17 ++ + ssl/statem/statem_dtls.c | 2 + + test/dtlstest.c | 416 ++++++++++++++++++++++++++++++++++++++++++++++- + 3 files changed, 433 insertions(+), 2 deletions(-) + +diff --git a/ssl/d1_lib.c b/ssl/d1_lib.c +index ad1bc7d8c830..5e8f57f01a3a 100644 +--- a/ssl/d1_lib.c ++++ b/ssl/d1_lib.c +@@ -406,6 +406,23 @@ int dtls1_handle_timeout(SSL_CONNECTION *s) + } + + dtls1_start_timer(s); ++ ++ /* ++ * If write_state is anything other than WRITE_STATE_TRANSITION, a write ++ * is still parked mid-flight (WANT_WRITE) from a previous call into the ++ * state machine - the current flight hasn't actually finished going out ++ * yet, so there's nothing valid to retransmit. Retransmitting anyway ++ * would reconstruct an already-sent message from the retransmit queue ++ * into s->init_buf/s->init_off/s->init_num/s->d1->w_msg - the same ++ * fields the parked write is still using - corrupting that write's ++ * state out from under it. Leave it alone and let the next ++ * SSL_read()/SSL_write()/SSL_accept()/SSL_connect() call resume the ++ * parked write normally instead. ++ */ ++ if (s->statem.state == MSG_FLOW_WRITING ++ && s->statem.write_state != WRITE_STATE_TRANSITION) ++ return 0; ++ + /* Calls SSLfatal() if required */ + return dtls1_retransmit_buffered_messages(s); + } +diff --git a/ssl/statem/statem_dtls.c b/ssl/statem/statem_dtls.c +index f62b757721fc..96ea03ab73cc 100644 +--- a/ssl/statem/statem_dtls.c ++++ b/ssl/statem/statem_dtls.c +@@ -1218,6 +1218,8 @@ int dtls1_retransmit_message(SSL_CONNECTION *s, unsigned short seq, int *found) + memcpy(s->init_buf->data, frag->fragment, + frag->msg_header.msg_len + header_length); + s->init_num = frag->msg_header.msg_len + header_length; ++ /* Always retransmit from the start, not wherever init_off was left */ ++ s->init_off = 0; + + dtls1_set_message_header_int(s, frag->msg_header.type, + frag->msg_header.msg_len, +diff --git a/test/dtlstest.c b/test/dtlstest.c +index 725d7dc4f2b8..4e5ea51e9599 100644 +--- a/test/dtlstest.c ++++ b/test/dtlstest.c +@@ -60,7 +60,6 @@ static int verify_cookie_cb(SSL *ssl, const unsigned char *cookie, + static unsigned int timer_cb(SSL *s, unsigned int timer_us) + { + ++timer_cb_count; +- + if (timer_us == 0) + return 50000; + else +@@ -681,6 +680,416 @@ static int test_duplicate_app_data(void) + return testresult; + } + ++/* ++ * frag_bio and the four retransmit tests below need DTLS1.2 ++ */ ++#ifndef OPENSSL_NO_DTLS1_2 ++ ++/* ++ * Number of times the retransmit timer is fired while a write is parked, in ++ * each of the four tests below ++ */ ++#define NUM_RETRANSMITS 3 ++ ++typedef struct { ++ BIO *bio; ++ int allowed; /* number of fragment writes to let through before suspending */ ++ int write_calls; /* number of times frag_write() has been invoked at all */ ++} frag_bio; ++ ++/* ++ * Each call to this function corresponds to exactly one DTLS fragment being ++ * handed to the BIO by dtls1_do_write(). Once |allowed| fragments have been ++ * let through, every further write suspends (WANT_WRITE) until the test ++ * bumps |allowed| again. ++ */ ++static int frag_write(BIO *bio, const char *buf, size_t len, size_t *written) ++{ ++ frag_bio *f = BIO_get_data(bio); ++ ++ BIO_clear_retry_flags(bio); ++ ++ f->write_calls++; ++ ++ if (f->allowed <= 0) { ++ BIO_set_retry_write(bio); ++ *written = 0; ++ return 0; ++ } ++ f->allowed--; ++ ++ if (!BIO_write_ex(f->bio, buf, len, written)) { ++ fprintf(stderr, "Failed to send data via BIO_write_ex\n"); ++ return 0; ++ } ++ ++ return 1; ++} ++ ++static int frag_read(BIO *bio, char *buf, size_t buf_len, size_t *readbytes) ++{ ++ frag_bio *f = BIO_get_data(bio); ++ return BIO_read_ex(f->bio, buf, buf_len, readbytes); ++} ++ ++static long frag_ctrl(BIO *bio, int cmd, long num, void *ptr) ++{ ++ frag_bio *f = BIO_get_data(bio); ++ return BIO_ctrl(f->bio, cmd, num, ptr); ++} ++ ++static int frag_puts(BIO *bio, const char *str) ++{ ++ size_t written; ++ return frag_write(bio, str, strlen(str), &written) ? (int)written : -1; ++} ++ ++static int frag_create(BIO *bio) ++{ ++ frag_bio *f = OPENSSL_zalloc(sizeof(*f)); ++ if (f == NULL) ++ return 0; ++ BIO_set_data(bio, f); ++ BIO_set_init(bio, 1); ++ return 1; ++} ++ ++static int frag_destroy(BIO *bio) ++{ ++ frag_bio *f = BIO_get_data(bio); ++ if (f == NULL) ++ return 1; ++ ++ BIO_free(f->bio); ++ OPENSSL_free(f); ++ BIO_set_data(bio, NULL); ++ BIO_set_init(bio, 0); ++ return 1; ++} ++ ++static BIO_METHOD *frag_method(void) ++{ ++ static BIO_METHOD *m = NULL; ++ if (m == NULL) { ++ m = BIO_meth_new(BIO_TYPE_SOURCE_SINK | BIO_TYPE_FILTER, "fragment-limited dgram"); ++ BIO_meth_set_write_ex(m, frag_write); ++ BIO_meth_set_read_ex(m, frag_read); ++ BIO_meth_set_ctrl(m, frag_ctrl); ++ BIO_meth_set_puts(m, frag_puts); ++ BIO_meth_set_create(m, frag_create); ++ BIO_meth_set_destroy(m, frag_destroy); ++ } ++ return m; ++} ++ ++static BIO *frag_new(BIO *bio, int allowed) ++{ ++ BIO *b = BIO_new(frag_method()); ++ frag_bio *f; ++ if (b == NULL) { ++ BIO_free(bio); ++ return NULL; ++ } ++ f = BIO_get_data(b); ++ f->bio = bio; ++ f->allowed = allowed; ++ return b; ++} ++ ++/* ++ * DTLS1.2 only. Exercises dtls1_handle_timeout()'s write_state guard (see ++ * d1_lib.c): while a write is parked mid-flight (WANT_WRITE, suspended here ++ * by a fragment-limiting BIO), a firing retransmit timer must not touch the ++ * retransmit queue at all - dtls1_retransmit_sent_messages() should never be ++ * entered, since retransmitting a message concurrently with the live write ++ * reusing the very same s->init_off/s->init_num/s->d1->w_msg fields would ++ * corrupt whichever write resumes second. ++ * ++ * Without the guard, letting that retransmit run ClientHello to completion ++ * while its own live write is still parked resets s->init_off/s->init_num ++ * to 0/0 out from under that write - so when the app resumes with ++ * SSL_connect(), dtls1_do_write() re-enters believing s->init_off == 0 means ++ * a brand new message is starting, when s->init_num no longer matches ++ * ClientHello's real length the way a fresh message's would - hitting an ++ * internal entry assertion and calling OPENSSL_die()/abort(). ++ * ++ * The client never calls SSL_connect() again until the very end, so the ++ * server is never driven far enough to respond - there's nothing to ++ * retransmit ClientHello against except the timer, driven purely by ++ * DTLSv1_get_timeout()/DTLSv1_handle_timeout(). ++ */ ++static int test_dtls_client_retransmit(void) ++{ ++ SSL_CTX *sctx = NULL, *cctx = NULL; ++ SSL *serverssl = NULL, *clientssl = NULL; ++ int testresult = 0; ++ int ret, err, i; ++ struct timeval tv; ++ static unsigned char alpn[750]; ++ size_t j, used = 0; ++ BIO *c_to_s_bio = NULL; ++ BIO *frag_wbio; ++ frag_bio *fb; ++ int write_calls_before; ++ ++ if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(), ++ DTLS_client_method(), ++ DTLS1_2_VERSION, DTLS1_2_VERSION, ++ &sctx, &cctx, cert, privkey))) ++ return 0; ++ ++ SSL_CTX_set_verify(cctx, SSL_VERIFY_NONE, NULL); ++ ++ /* Pad the ClientHello out via ALPN so it needs multiple fragments. */ ++ for (j = 0; j < 3; j++) { ++ char name[250]; ++ int n = snprintf(name, sizeof(name), ++ "proto-%04u-%s", (unsigned int)j, ++ "padpadpadpadpadpadpadpadpadpadpadpadpadpadpad" ++ "padpadpadpadpadpadpadpadpadpadpadpadpadpadpad" ++ "padpadpadpadpadpadpadpadpadpadpadpadpadpadpad" ++ "padpadpadpadpadpadpadpadpadpadpadpadpadpadpad" ++ "padpadpadpadpadpadpadpadpadpadpadpadpadpad"); ++ ++ if (!TEST_int_ge(n, 0) || !TEST_size_t_lt((size_t)n, sizeof(name))) ++ goto end; ++ if (!TEST_size_t_le(used + 1 + (size_t)n, sizeof(alpn))) ++ goto end; ++ ++ alpn[used++] = (unsigned char)n; ++ memcpy(alpn + used, name, (size_t)n); ++ used += (size_t)n; ++ } ++ if (!TEST_false(SSL_CTX_set_alpn_protos(cctx, alpn, (unsigned int)used))) ++ goto end; ++ ++ if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl, ++ NULL, NULL))) ++ goto end; ++ ++ /* ++ * Pin the MTU so fragmentation of the ClientHello (padded out via ALPN ++ * above) is deterministic. SSL_OP_NO_QUERY_MTU stops dtls1_query_mtu() ++ * from overriding this with a value queried from the BIO. ++ */ ++ SSL_set_options(clientssl, SSL_OP_NO_QUERY_MTU); ++ if (!TEST_true(SSL_set_mtu(clientssl, 256))) ++ goto end; ++ ++ /* ++ * Wrap the client's write BIO in our fragment-limiting filter, initially ++ * allowing only the ClientHello's first fragment through. SSL_get_wbio() ++ * is a borrowed reference, and SSL_set0_wbio() will free whatever the old ++ * wbio pointer was as soon as we install the replacement - so we need our ++ * own ref on the underlying bio before frag_new() stores it internally, ++ * otherwise the wrapper is left holding a dangling pointer. ++ */ ++ c_to_s_bio = SSL_get_wbio(clientssl); ++ ++ if (!TEST_ptr(c_to_s_bio) || !TEST_true(BIO_up_ref(c_to_s_bio))) ++ goto end; ++ ++ frag_wbio = frag_new(c_to_s_bio, 1); ++ if (!TEST_ptr(frag_wbio)) { ++ BIO_free(c_to_s_bio); ++ goto end; ++ } ++ fb = BIO_get_data(frag_wbio); ++ ++ SSL_set0_wbio(clientssl, frag_wbio); ++ ++ DTLS_set_timer_cb(clientssl, timer_cb); ++ ++ /* ++ * Flight 1: frag_wbio's budget of 1 write buys exactly ClientHello's ++ * first fragment - that's all that goes out. The next write (its ++ * second fragment) is what actually suspends, leaving write_state ++ * parked at WRITE_STATE_SEND, waiting to resume ClientHello. ++ */ ++ ret = SSL_connect(clientssl); ++ if (!TEST_int_le(ret, 0) ++ || !TEST_int_eq(SSL_get_error(clientssl, ret), SSL_ERROR_WANT_WRITE)) ++ goto end; ++ ++ /* ++ * Let the retransmit timer fire, NUM_RETRANSMITS times, while the write ++ * above is still parked. fb->allowed = 100 so our custom BIO isn't what ++ * would block a retransmit, if one were sent. frag_write()'s call ++ * counter shouldn't move at all, round after round. ++ */ ++ fb->allowed = 100; ++ ++ for (i = 0; i < NUM_RETRANSMITS; i++) { ++ write_calls_before = fb->write_calls; ++ ++ if (!TEST_int_gt((int)DTLSv1_get_timeout(clientssl, &tv), 0)) ++ goto end; ++ ++ /* Wait for the retransmit timer to actually expire */ ++ OSSL_sleep((uint64_t)(tv.tv_sec * 1000 + tv.tv_usec / 1000) + 10); ++ ++ if (!TEST_int_ge((int)DTLSv1_handle_timeout(clientssl), 0)) ++ goto end; ++ ++ if (!TEST_int_eq(fb->write_calls, write_calls_before)) ++ goto end; ++ } ++ ++ /* ++ * Resume the original suspended write. This is the call expected to ++ * hit the entry assertion described above if the guard weren't there. ++ */ ++ ret = SSL_connect(clientssl); ++ err = SSL_get_error(clientssl, ret); ++ ++ /* ++ * If we get here at all (i.e. we didn't just abort()), the resume ++ * should look like an ordinary handshake continuation - WANT_READ or ++ * WANT_WRITE, not an internal failure. ++ */ ++ if (!TEST_false(err == SSL_ERROR_SSL || err == SSL_ERROR_SYSCALL)) ++ goto end; ++ ++ testresult = 1; ++end: ++ SSL_free(serverssl); ++ SSL_free(clientssl); ++ SSL_CTX_free(sctx); ++ SSL_CTX_free(cctx); ++ ++ return testresult; ++} ++ ++/* ++ * DTLS1.2 only. Server-side counterpart to test_dtls_client_retransmit(): ++ * same write_state guard, but Certificate suspends mid-write during the ++ * server's flight instead of ClientHello. The cipher is pinned to a ++ * static-RSA suite so the flight is just ServerHello + Certificate + ++ * ServerHelloDone (no ServerKeyExchange); with a 256 byte MTU, ServerHello ++ * fits in a single fragment but Certificate does not. ++ * ++ * The client never calls SSL_accept() again until the very end, so the ++ * server is driven purely by DTLSv1_get_timeout()/DTLSv1_handle_timeout(). ++ */ ++static int test_dtls_server_retransmit(void) ++{ ++ SSL_CTX *sctx = NULL, *cctx = NULL; ++ SSL *serverssl = NULL, *clientssl = NULL; ++ int testresult = 0; ++ int ret, err, i; ++ struct timeval tv; ++ BIO *s_to_c_bio = NULL; ++ BIO *frag_wbio; ++ frag_bio *fb; ++ int write_calls_before; ++ ++ if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(), ++ DTLS_client_method(), ++ DTLS1_2_VERSION, DTLS1_2_VERSION, ++ &sctx, &cctx, cert, privkey))) ++ return 0; ++ ++ /* Static RSA cipher: no ServerKeyExchange, keeps the flight simple. */ ++ if (!TEST_true(SSL_CTX_set_cipher_list(cctx, "AES128-SHA"))) ++ goto end; ++ ++ if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl, ++ NULL, NULL))) ++ goto end; ++ ++ /* ++ * Pin the server's MTU so its flight fragments deterministically. The ++ * client is left alone - its ClientHello is small and unfragmented. ++ */ ++ SSL_set_options(serverssl, SSL_OP_NO_QUERY_MTU); ++ if (!TEST_true(SSL_set_mtu(serverssl, 256))) ++ goto end; ++ ++ /* Wrap only the server's write BIO in our fragment-limiting filter. */ ++ s_to_c_bio = SSL_get_wbio(serverssl); ++ ++ if (!TEST_ptr(s_to_c_bio) || !TEST_true(BIO_up_ref(s_to_c_bio))) ++ goto end; ++ ++ /* Only let ServerHello and Certificate's first fragment out initially. */ ++ frag_wbio = frag_new(s_to_c_bio, 2); ++ if (!TEST_ptr(frag_wbio)) { ++ BIO_free(s_to_c_bio); ++ goto end; ++ } ++ fb = BIO_get_data(frag_wbio); ++ ++ SSL_set0_wbio(serverssl, frag_wbio); ++ ++ DTLS_set_timer_cb(serverssl, timer_cb); ++ ++ /* Flight 1: the client sends its ClientHello without any restriction. */ ++ if (!TEST_int_le(SSL_connect(clientssl), 0)) ++ goto end; ++ ++ /* ++ * Flight 2: frag_wbio's budget of 2 writes buys exactly ServerHello in ++ * full plus the first fragment of Certificate - that's all that goes ++ * out. The next write (Certificate's second fragment) is what actually ++ * suspends, leaving write_state parked at WRITE_STATE_SEND, waiting to ++ * resume Certificate specifically. ++ */ ++ ret = SSL_accept(serverssl); ++ if (!TEST_int_le(ret, 0) ++ || !TEST_int_eq(SSL_get_error(serverssl, ret), SSL_ERROR_WANT_WRITE)) ++ goto end; ++ ++ /* ++ * Let the retransmit timer fire, NUM_RETRANSMITS times, while the write ++ * above is still parked. fb->allowed = 100 so our custom BIO isn't what ++ * would block a retransmit, if one were sent. frag_write()'s call ++ * counter shouldn't move at all, round after round. ++ */ ++ fb->allowed = 100; ++ ++ for (i = 0; i < NUM_RETRANSMITS; i++) { ++ write_calls_before = fb->write_calls; ++ ++ if (!TEST_int_gt((int)DTLSv1_get_timeout(serverssl, &tv), 0)) ++ goto end; ++ ++ OSSL_sleep((uint64_t)(tv.tv_sec * 1000 + tv.tv_usec / 1000) + 10); ++ ++ if (!TEST_int_ge((int)DTLSv1_handle_timeout(serverssl), 0)) ++ goto end; ++ ++ if (!TEST_int_eq(fb->write_calls, write_calls_before)) ++ goto end; ++ } ++ ++ /* ++ * Resume the original suspended write. This is the call expected to ++ * hit the entry assertion described in test_dtls_client_retransmit() if ++ * the guard weren't there. ++ */ ++ ret = SSL_accept(serverssl); ++ err = SSL_get_error(serverssl, ret); ++ ++ /* ++ * If we get here at all (i.e. we didn't just abort()), the resume ++ * should look like an ordinary handshake continuation - WANT_READ or ++ * WANT_WRITE, not an internal failure. ++ */ ++ if (!TEST_false(err == SSL_ERROR_SSL || err == SSL_ERROR_SYSCALL)) ++ goto end; ++ ++ testresult = 1; ++end: ++ SSL_free(serverssl); ++ SSL_free(clientssl); ++ SSL_CTX_free(sctx); ++ SSL_CTX_free(cctx); ++ ++ return testresult; ++} ++ ++#endif ++ + /* Confirm that we can create a connections using DTLSv1_listen() */ + static int test_listen(void) + { +@@ -753,7 +1162,10 @@ int setup_tests(void) + ADD_ALL_TESTS(test_swap_records, 4); + ADD_TEST(test_listen); + ADD_TEST(test_duplicate_app_data); +- ++#ifndef OPENSSL_NO_DTLS1_2 ++ ADD_TEST(test_dtls_client_retransmit); ++ ADD_TEST(test_dtls_server_retransmit); ++#endif + return 1; + } + diff -Nru openssl-3.5.7/debian/patches/ec-make-ossl_ec_scalar_mul_ladder-scalar-padding-constant.patch openssl-3.5.7/debian/patches/ec-make-ossl_ec_scalar_mul_ladder-scalar-padding-constant.patch --- openssl-3.5.7/debian/patches/ec-make-ossl_ec_scalar_mul_ladder-scalar-padding-constant.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/ec-make-ossl_ec_scalar_mul_ladder-scalar-padding-constant.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,141 @@ +From: Igor Ustinov +Date: Tue, 28 Jul 2026 22:09:29 +0200 +Subject: ec: make ossl_ec_scalar_mul_ladder() scalar padding constant time + +Fixes CVE-2026-54872 + +Assisted-by: Claude:claude-opus-4-8 +--- + crypto/bn/bn_intern.c | 38 ++++++++++++++++++++++++++++++++++++++ + crypto/ec/ec_mult.c | 38 ++++++++++++++++++++++++++++++++++++++ + include/crypto/bn.h | 1 + + 3 files changed, 77 insertions(+) + +diff --git a/crypto/bn/bn_intern.c b/crypto/bn/bn_intern.c +index bd299cd1442d..6cf72c015682 100644 +--- a/crypto/bn/bn_intern.c ++++ b/crypto/bn/bn_intern.c +@@ -9,6 +9,7 @@ + + #include "internal/cryptlib.h" + #include "bn_local.h" ++#include "internal/constant_time.h" + + /* + * Determine the modified width-(w+1) Non-Adjacent Form (wNAF) of 'scalar'. +@@ -152,6 +153,43 @@ void bn_set_all_zero(BIGNUM *a) + a->d[i] = 0; + } + ++/* ++ * Zero-extend |a| so that it occupies exactly |words| words, flag it ++ * BN_FLG_FIXED_TOP and leave its numeric value unchanged. ++ * ++ * This is a companion to bn_correct_top(): where the latter minimises the top ++ * of a BIGNUM, this one pins the top to a caller-chosen, value-independent ++ * width. Constant-time code uses it to make the cost of subsequent word-wise ++ * operations (e.g. BN_uadd()/BN_add()) independent of the magnitude of a ++ * secret value. |words| must be greater than or equal to the current top. ++ * ++ * The routine is itself constant time with respect to the current a->top: it ++ * always sweeps a fixed |words| iterations and selects value-or-zero per word ++ * with an arithmetic mask, rather than looping over the (possibly secret) ++ * a->top..words range. Masking the high words with zero also launders any ++ * uninitialised padding, so it is safe for the memory sanitiser. ++ */ ++int bn_set_top_fixed(BIGNUM *a, int words) ++{ ++ size_t i, n = (size_t)words; ++ BN_ULONG mask; ++ ++ if (words < a->top) ++ return 0; ++ if (bn_wexpand(a, words) == NULL) { ++ ERR_raise(ERR_LIB_BN, ERR_R_BN_LIB); ++ return 0; ++ } ++ for (i = 0; i < n; i++) { ++ /* mask = all ones iff i < a->top, else all zeros */ ++ mask = value_barrier_bn((BN_ULONG)0 - ((i - a->top) >> (8 * sizeof(i) - 1))); ++ a->d[i] &= mask; ++ } ++ a->top = words; ++ a->flags |= BN_FLG_FIXED_TOP; ++ return 1; ++} ++ + int bn_copy_words(BN_ULONG *out, const BIGNUM *in, int size) + { + if (in->top > size) +diff --git a/crypto/ec/ec_mult.c b/crypto/ec/ec_mult.c +index 18f3d47d936f..94208a7c6a43 100644 +--- a/crypto/ec/ec_mult.c ++++ b/crypto/ec/ec_mult.c +@@ -213,6 +213,18 @@ int ossl_ec_scalar_mul_ladder(const EC_GROUP *group, EC_POINT *r, + goto err; + } + ++ /* ++ * Constant-timeness of this copy depends on the caller: BN_copy() moves ++ * scalar->top words unless |scalar| is flagged BN_FLG_CONSTTIME (in which ++ * case scalar->dmax words are moved). Secret scalars are therefore ++ * expected to arrive either BN_FLG_CONSTTIME or fixed-top, so that their ++ * top is a public, value-independent width and the copy length does not ++ * leak their magnitude. ECDSA satisfies this via ++ * ossl_bn_priv_rand_range_fixed_top(); the generic SM2 signing path does ++ * not yet (see the sm2_sig_gen() hardening tracked separately). The ++ * fixed-top pinning below makes the subsequent arithmetic constant time ++ * regardless, but cannot retroactively fix the copy length here. ++ */ + if (!BN_copy(k, scalar)) { + ERR_raise(ERR_LIB_EC, ERR_R_BN_LIB); + goto err; +@@ -231,10 +243,36 @@ int ossl_ec_scalar_mul_ladder(const EC_GROUP *group, EC_POINT *r, + } + } + ++ /* ++ * |k| may still carry a top that depends on the value of the secret ++ * scalar: callers pass either a fixed-top BIGNUM (e.g. the ECDSA nonce) ++ * or a minimal-top one (e.g. SM2), and BN_copy() above preserves that ++ * top. Pin |k| to a fixed number of words (matching the group ++ * cardinality) so that the additions below run in constant time, ++ * independently of the bit length of the scalar. Otherwise the work ++ * done by BN_add()/BN_uadd() depends on the operand tops and leaks the ++ * magnitude of the secret scalar. ++ */ ++ if (!bn_set_top_fixed(k, group_top)) { ++ ERR_raise(ERR_LIB_EC, ERR_R_BN_LIB); ++ goto err; ++ } ++ + if (!BN_add(lambda, k, cardinality)) { + ERR_raise(ERR_LIB_EC, ERR_R_BN_LIB); + goto err; + } ++ /* ++ * |lambda| = scalar + cardinality may or may not have produced a carry ++ * into an extra word depending on the secret scalar. Pin its top to one ++ * word above the group top so that the second addition, which consumes ++ * |lambda|, is likewise constant time and so that the BN_is_bit_set() ++ * below always inspects a defined word. ++ */ ++ if (!bn_set_top_fixed(lambda, group_top + 1)) { ++ ERR_raise(ERR_LIB_EC, ERR_R_BN_LIB); ++ goto err; ++ } + BN_set_flags(lambda, BN_FLG_CONSTTIME); + if (!BN_add(k, lambda, cardinality)) { + ERR_raise(ERR_LIB_EC, ERR_R_BN_LIB); +diff --git a/include/crypto/bn.h b/include/crypto/bn.h +index 952840be5e66..dd6fd89ec8a4 100644 +--- a/include/crypto/bn.h ++++ b/include/crypto/bn.h +@@ -18,6 +18,7 @@ BIGNUM *bn_wexpand(BIGNUM *a, int words); + BIGNUM *bn_expand2(BIGNUM *a, int words); + + void bn_correct_top(BIGNUM *a); ++int bn_set_top_fixed(BIGNUM *a, int words); + + /* + * Determine the modified width-(w+1) Non-Adjacent Form (wNAF) of 'scalar'. diff -Nru openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-1.patch openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-1.patch --- openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-1.patch 2026-09-29 20:10:10.000000000 +0000 @@ -0,0 +1,40 @@ +From: Neil Horman +Date: Fri, 28 Aug 2026 09:40:29 -0400 +Subject: fixup! Add a test to check for quic unvalidated credit + +--- + test/quicapitest.c | 19 ++++++++++++++----- + 1 file changed, 14 insertions(+), 5 deletions(-) + +diff --git a/test/quicapitest.c b/test/quicapitest.c +index 68577e4a5150..d5a04cb04afd 100644 +--- a/test/quicapitest.c ++++ b/test/quicapitest.c +@@ -3039,13 +3039,22 @@ static int test_quic_amplification_limit(void) + * 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)) +- || !TEST_true(SSL_CTX_set1_groups_list(sctx, "X25519")) +- || !TEST_true(SSL_CTX_set_ciphersuites(sctx, "TLS_AES_128_GCM_SHA256")) +- || !TEST_true(SSL_CTX_set1_groups_list(cctx, "X25519")) +- || !TEST_true(SSL_CTX_set_ciphersuites(cctx, "TLS_AES_128_GCM_SHA256"))) ++ || !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 diff -Nru openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-2.patch openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-2.patch --- openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-2.patch 2026-09-29 20:10:10.000000000 +0000 @@ -0,0 +1,21 @@ +From: Neil Horman +Date: Fri, 28 Aug 2026 10:05:56 -0400 +Subject: fixup! Add a test to check for quic unvalidated credit + +--- + test/quicapitest.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/test/quicapitest.c b/test/quicapitest.c +index d5a04cb04afd..03cd72c6cc4c 100644 +--- a/test/quicapitest.c ++++ b/test/quicapitest.c +@@ -2943,7 +2943,7 @@ static int drain_server_output(BIO *b, uint64_t *total, + size_t *num_datagrams) + { + unsigned char buf[65536]; +- BIO_MSG msg; ++ BIO_MSG msg = { 0 }; + size_t num_processed; + int ret = 0; + diff -Nru openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-3.patch openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-3.patch --- openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-3.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-3.patch 2026-09-29 20:10:10.000000000 +0000 @@ -0,0 +1,25 @@ +From: Neil Horman +Date: Fri, 28 Aug 2026 10:27:21 -0400 +Subject: fixup! Add a test to check for quic unvalidated credit + +--- + test/quicapitest.c | 2 -- + 1 file changed, 2 deletions(-) + +diff --git a/test/quicapitest.c b/test/quicapitest.c +index 03cd72c6cc4c..85d6f6c4dd7f 100644 +--- a/test/quicapitest.c ++++ b/test/quicapitest.c +@@ -11,12 +11,10 @@ + #include + + #include +-#include + #include + #include + #include + #include +-#include + + #include + #include diff -Nru openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-4.patch openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-4.patch --- openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-4.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-4.patch 2026-09-29 20:10:10.000000000 +0000 @@ -0,0 +1,25 @@ +From: Neil Horman +Date: Thu, 10 Sep 2026 11:31:27 -0400 +Subject: fixup! Add a test to check for quic unvalidated credit + +--- + test/quicapitest.c | 4 ---- + 1 file changed, 4 deletions(-) + +diff --git a/test/quicapitest.c b/test/quicapitest.c +index 85d6f6c4dd7f..b73cdd51ac43 100644 +--- a/test/quicapitest.c ++++ b/test/quicapitest.c +@@ -9,12 +9,8 @@ + + #include + #include +- + #include + #include +-#include +-#include +-#include + + #include + #include diff -Nru openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-5.patch openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-5.patch --- openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-5.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-5.patch 2026-09-29 20:10:10.000000000 +0000 @@ -0,0 +1,63 @@ +From: Neil Horman +Date: Fri, 11 Sep 2026 08:42:20 -0400 +Subject: fixup! Add a test to check for quic unvalidated credit + +--- + test/quicapitest.c | 27 ++++++++++++++++++++------- + 1 file changed, 20 insertions(+), 7 deletions(-) + +diff --git a/test/quicapitest.c b/test/quicapitest.c +index b73cdd51ac43..51ad1b1ece0e 100644 +--- a/test/quicapitest.c ++++ b/test/quicapitest.c +@@ -3010,6 +3010,7 @@ static int test_quic_amplification_limit(void) + 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; +@@ -3080,16 +3081,26 @@ static int test_quic_amplification_limit(void) + * 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)))) +- || (!TEST_true(BIO_new_bio_dgram_pair(&c_bio, 65535, &s_bio, 65535))) +- || (!TEST_true(bio_addr_bind(c_bio, client_addr))) +- || (!TEST_true(bio_addr_bind(s_bio, server_addr))) +- || (!TEST_ptr((listener = SSL_new_listener(sctx, +- SSL_LISTENER_FLAG_NO_VALIDATE)))) ++ || (!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, server_addr))) ++ || (!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))))) +@@ -3235,6 +3246,8 @@ static int test_quic_amplification_limit(void) + SSL_free(listener); + SSL_CTX_free(cctx); + SSL_CTX_free(sctx); ++ BIO_ADDR_free(client_addr); ++ BIO_ADDR_free(server_addr); + return ret; + } + diff -Nru openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-6.patch openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-6.patch --- openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-6.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit-6.patch 2026-09-29 20:10:10.000000000 +0000 @@ -0,0 +1,31 @@ +From: Neil Horman +Date: Mon, 14 Sep 2026 09:17:08 -0400 +Subject: fixup! Add a test to check for quic unvalidated credit + +--- + test/quicapitest.c | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/test/quicapitest.c b/test/quicapitest.c +index 51ad1b1ece0e..4ed21b5ff20e 100644 +--- a/test/quicapitest.c ++++ b/test/quicapitest.c +@@ -2982,15 +2982,15 @@ static int send_coalesced_initial(OSSL_QTX *qtx, QUIC_PKT_HDR *hdr, + + if (ossl_qtx_get_queue_len_datagrams(qtx) != 1 + || ossl_qtx_get_queue_len_bytes(qtx) != QUIC_MDPL) { +- fprintf(stderr, "crafted queue has %zu datagrams / %zu bytes, " +- "expected 1 / %d\n", ++ 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) { +- fprintf(stderr, "failed to flush crafted datagram\n"); ++ TEST_info("failed to flush crafted datagram"); + return 0; + } + diff -Nru openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit.patch openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit.patch --- openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/fixup-Add-a-test-to-check-for-quic-unvalidated-credit.patch 2026-09-29 20:10:10.000000000 +0000 @@ -0,0 +1,30 @@ +From: Neil Horman +Date: Fri, 28 Aug 2026 09:08:11 -0400 +Subject: fixup! Add a test to check for quic unvalidated credit + +--- + test/quicapitest.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/test/quicapitest.c b/test/quicapitest.c +index 164154e13517..68577e4a5150 100644 +--- a/test/quicapitest.c ++++ b/test/quicapitest.c +@@ -3074,7 +3074,7 @@ static int test_quic_amplification_limit(void) + * 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 +- * acidentally drop frames. ++ * accidentally drop frames. + */ + ina.s_addr = htonl(INADDR_LOOPBACK); + if (!TEST_ptr((server_addr = create_addr(&ina, SERVER_PORT))) +@@ -3179,7 +3179,7 @@ static int test_quic_amplification_limit(void) + * + * 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, jsut as it did above, but ++ * 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. + */ diff -Nru openssl-3.5.7/debian/patches/fixup-Fix-out-of-bounds-valid_flags-access-after-SSL_set_.patch openssl-3.5.7/debian/patches/fixup-Fix-out-of-bounds-valid_flags-access-after-SSL_set_.patch --- openssl-3.5.7/debian/patches/fixup-Fix-out-of-bounds-valid_flags-access-after-SSL_set_.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/fixup-Fix-out-of-bounds-valid_flags-access-after-SSL_set_.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,22 @@ +From: Matt Caswell +Date: Thu, 10 Sep 2026 09:40:03 +0100 +Subject: fixup! Fix out-of-bounds valid_flags access after SSL_set_SSL_CTX() + +--- + ssl/ssl_lib.c | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c +index 73395b36ff25..df35b4650eaa 100644 +--- a/ssl/ssl_lib.c ++++ b/ssl/ssl_lib.c +@@ -5502,6 +5502,9 @@ SSL_CTX *SSL_set_SSL_CTX(SSL *ssl, SSL_CTX *ctx) + * its preferences take effect on an established connection. + */ + if (sc->s3.tmp.valid_flags != NULL) { ++ /* Should never happen: ssl_cert_new() enforces this */ ++ if (!ossl_assert(new_cert->ssl_pkey_num >= SSL_PKEY_NUM)) ++ goto err; + new_valid_flags = OPENSSL_zalloc(new_cert->ssl_pkey_num + * sizeof(*new_valid_flags)); + if (new_valid_flags == NULL) diff -Nru openssl-3.5.7/debian/patches/fixup-don-t-double-count-full-databgram-length-on-unvalid.patch openssl-3.5.7/debian/patches/fixup-don-t-double-count-full-databgram-length-on-unvalid.patch --- openssl-3.5.7/debian/patches/fixup-don-t-double-count-full-databgram-length-on-unvalid.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/fixup-don-t-double-count-full-databgram-length-on-unvalid.patch 2026-09-29 20:10:10.000000000 +0000 @@ -0,0 +1,21 @@ +From: Neil Horman +Date: Fri, 28 Aug 2026 09:05:57 -0400 +Subject: fixup! don't double count full databgram length on unvalidated + connections + +--- + ssl/quic/quic_rx_depack.c | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/ssl/quic/quic_rx_depack.c b/ssl/quic/quic_rx_depack.c +index 0ead9097016e..4fbc6eca74e0 100644 +--- a/ssl/quic/quic_rx_depack.c ++++ b/ssl/quic/quic_rx_depack.c +@@ -1451,7 +1451,6 @@ int ossl_quic_handle_frames(QUIC_CHANNEL *ch, OSSL_QRX_PKT *qpacket) + PACKET pkt; + OSSL_ACKM_RX_PKT ackm_data; + uint32_t enc_level; +- size_t dgram_len = qpacket->datagram_len; + + if (ch == NULL) + return 0; diff -Nru openssl-3.5.7/debian/patches/make-ch_cleanup-just-safe-enough-to-be-called-by-quic_str.patch openssl-3.5.7/debian/patches/make-ch_cleanup-just-safe-enough-to-be-called-by-quic_str.patch --- openssl-3.5.7/debian/patches/make-ch_cleanup-just-safe-enough-to-be-called-by-quic_str.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/make-ch_cleanup-just-safe-enough-to-be-called-by-quic_str.patch 2026-09-29 20:10:12.000000000 +0000 @@ -0,0 +1,42 @@ +From: Alexandr Nedvedicky +Date: Thu, 24 Sep 2026 17:18:12 +0200 +Subject: make ch_cleanup() just safe enough to be called by quic_stream_test + +quic_stream_test (quic_txp) use dummy channel to perform testing. +the channel object is QUIC_CHANNEL instance allocated by OPENSSL_zalloc() +and released by ossl_quic_channel_free() which calls ch_cleanup() + +the ossl_quic_channel_free() assumes the QUIC_CHANNEL is fully +initialized and valid object which is not a case of dummy channel +object created for testing. + +this change is specific to openssl 3.4 and 3.5. Newer +OpenSSL versions provide a safe variant of ch_cleanup() + +Reviewed-by: Tomas Mraz +Reviewed-by: Milan Broz +Merge-date: Sat Sep 26 11:53:47 2026 +Merged-from: https://github.com/openssl/openssl/pull/32769 +--- + ssl/quic/quic_channel.c | 8 ++++++-- + 1 file changed, 6 insertions(+), 2 deletions(-) + +diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c +index b5a19d389ccb..aa110b3c4254 100644 +--- a/ssl/quic/quic_channel.c ++++ b/ssl/quic/quic_channel.c +@@ -404,8 +404,12 @@ static void ch_cleanup(QUIC_CHANNEL *ch) + ++pn_space) + ossl_ackm_on_pkt_space_discarded(ch->ackm, pn_space); + +- ossl_quic_lcidm_cull(ch->lcidm, ch); +- ossl_quic_srtm_cull(ch->srtm, ch); ++ if (ch->lcidm != NULL) ++ ossl_quic_lcidm_cull(ch->lcidm, ch); ++ ++ if (ch->srtm != NULL) ++ ossl_quic_srtm_cull(ch->srtm, ch); ++ + ossl_quic_tx_packetiser_free(ch->txp); + ossl_quic_txpim_free(ch->txpim); + ossl_quic_cfq_free(ch->cfq); diff -Nru openssl-3.5.7/debian/patches/quic-move-the-RXE-definition-to-a-local-header.patch openssl-3.5.7/debian/patches/quic-move-the-RXE-definition-to-a-local-header.patch --- openssl-3.5.7/debian/patches/quic-move-the-RXE-definition-to-a-local-header.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/quic-move-the-RXE-definition-to-a-local-header.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,191 @@ +From: Jakub Zelenka +Date: Wed, 26 Aug 2026 14:55:57 +0200 +Subject: quic: move the RXE definition to a local header +MIME-Version: 1.0 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 8bit + +The RXE structure carries the reference count of a received packet and +embeds the OSSL_QRX_PKT handed out to the users of the QRX as its first +member. It is defined in quic_record_rx.c, so a test that wants to build +a packet without a QRX behind it cannot allocate one or look at its +reference count, and would have to guess the size of the structure and +the offset of the fields it needs. + +Move the definition to a local header, following the other local headers +in ssl/quic, so that a test can include it and construct a packet of its +own rather than the library having to provide a constructor for it. + +Assisted-by: Claude:claude-opus-5 + +Reviewed-by: Tomas Mraz +Reviewed-by: Saša Nedvědický +MergeDate: Fri Aug 28 14:03:45 2026 +Reviewed-by: Milan Broz +Merged-from: https://github.com/openssl/openssl/pull/32769 +--- + ssl/quic/quic_record_rx.c | 56 +---------------------------- + ssl/quic/quic_record_rx_local.h | 80 +++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 81 insertions(+), 55 deletions(-) + create mode 100644 ssl/quic/quic_record_rx_local.h + +diff --git a/ssl/quic/quic_record_rx.c b/ssl/quic/quic_record_rx.c +index 0065f1c1e573..75668584846b 100644 +--- a/ssl/quic/quic_record_rx.c ++++ b/ssl/quic/quic_record_rx.c +@@ -10,6 +10,7 @@ + #include + #include "internal/quic_record_rx.h" + #include "quic_record_shared.h" ++#include "quic_record_rx_local.h" + #include "internal/common.h" + #include "internal/list.h" + #include "../ssl_local.h" +@@ -32,61 +33,6 @@ static ossl_inline int pkt_is_marked(const uint64_t *bitf, size_t pkt_idx) + return (*bitf & (((uint64_t)1) << pkt_idx)) != 0; + } + +-/* +- * RXE +- * === +- * +- * RX Entries (RXEs) store processed (i.e., decrypted) data received from the +- * network. One RXE is used per received QUIC packet. +- */ +-typedef struct rxe_st RXE; +- +-struct rxe_st { +- OSSL_QRX_PKT pkt; +- OSSL_LIST_MEMBER(rxe, RXE); +- size_t data_len, alloc_len, refcount; +- +- /* Extra fields for per-packet information. */ +- QUIC_PKT_HDR hdr; /* data/len are decrypted payload */ +- +- /* Decoded packet number. */ +- QUIC_PN pn; +- +- /* Addresses copied from URXE. */ +- BIO_ADDR peer, local; +- +- /* Time we received the packet (not when we processed it). */ +- OSSL_TIME time; +- +- /* Total length of the datagram which contained this packet. */ +- size_t datagram_len; +- +- /* +- * The key epoch the packet was received with. Always 0 for non-1-RTT +- * packets. +- */ +- uint64_t key_epoch; +- +- /* +- * Monotonically increases with each datagram received. +- * For diagnostic use only. +- */ +- uint64_t datagram_id; +- +- /* +- * alloc_len allocated bytes (of which data_len bytes are valid) follow this +- * structure. +- */ +-}; +- +-DEFINE_LIST_OF(rxe, RXE); +-typedef OSSL_LIST(rxe) RXE_LIST; +- +-static ossl_inline unsigned char *rxe_data(const RXE *e) +-{ +- return (unsigned char *)(e + 1); +-} +- + /* + * QRL + * === +diff --git a/ssl/quic/quic_record_rx_local.h b/ssl/quic/quic_record_rx_local.h +new file mode 100644 +index 000000000000..4a2fd8c0e1b8 +--- /dev/null ++++ b/ssl/quic/quic_record_rx_local.h +@@ -0,0 +1,80 @@ ++/* ++ * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved. ++ * ++ * Licensed under the Apache License 2.0 (the "License"). You may not use ++ * this file except in compliance with the License. You can obtain a copy ++ * in the file LICENSE in the source distribution or at ++ * https://www.openssl.org/source/license.html ++ */ ++ ++#ifndef OSSL_QUIC_RECORD_RX_LOCAL_H ++#define OSSL_QUIC_RECORD_RX_LOCAL_H ++ ++#include "internal/quic_record_rx.h" ++#include "internal/list.h" ++ ++#ifndef OPENSSL_NO_QUIC ++ ++/* ++ * RXE ++ * === ++ * ++ * RX Entries (RXEs) store processed (i.e., decrypted) data received from the ++ * network. One RXE is used per received QUIC packet. ++ * ++ * The OSSL_QRX_PKT handed out to users of the QRX is the first member, so a ++ * packet pointer can be cast back to its RXE. It is intended that only the ++ * QRX implementation access this structure directly, tests which need to ++ * construct a packet without a QRX being the exception. ++ */ ++typedef struct rxe_st RXE; ++ ++struct rxe_st { ++ OSSL_QRX_PKT pkt; ++ OSSL_LIST_MEMBER(rxe, RXE); ++ size_t data_len, alloc_len, refcount; ++ ++ /* Extra fields for per-packet information. */ ++ QUIC_PKT_HDR hdr; /* data/len are decrypted payload */ ++ ++ /* Decoded packet number. */ ++ QUIC_PN pn; ++ ++ /* Addresses copied from URXE. */ ++ BIO_ADDR peer, local; ++ ++ /* Time we received the packet (not when we processed it). */ ++ OSSL_TIME time; ++ ++ /* Total length of the datagram which contained this packet. */ ++ size_t datagram_len; ++ ++ /* ++ * The key epoch the packet was received with. Always 0 for non-1-RTT ++ * packets. ++ */ ++ uint64_t key_epoch; ++ ++ /* ++ * Monotonically increases with each datagram received. ++ * For diagnostic use only. ++ */ ++ uint64_t datagram_id; ++ ++ /* ++ * alloc_len allocated bytes (of which data_len bytes are valid) follow this ++ * structure. ++ */ ++}; ++ ++DEFINE_LIST_OF(rxe, RXE); ++typedef OSSL_LIST(rxe) RXE_LIST; ++ ++static ossl_inline unsigned char *rxe_data(const RXE *e) ++{ ++ return (unsigned char *)(e + 1); ++} ++ ++#endif ++ ++#endif diff -Nru openssl-3.5.7/debian/patches/series openssl-3.5.7/debian/patches/series --- openssl-3.5.7/debian/patches/series 2026-08-23 15:26:22.000000000 +0000 +++ openssl-3.5.7/debian/patches/series 2026-09-29 20:10:12.000000000 +0000 @@ -21,3 +21,44 @@ Don-t-store-ACK-only-frames-in-TX-history-for-QUIC.patch Check-the-tag-on-EVP_Cipher-finalize-Poly1305-and-OCB-AEA.patch QUIC-server-limit-number-of-pending-QUIC-channels-connect.patch +Defer-computation-of-relative-CRLDP-names.patch +don-t-double-count-full-databgram-length-on-unvalidated-c.patch +Add-a-test-to-check-for-quic-unvalidated-credit.patch +fixup-don-t-double-count-full-databgram-length-on-unvalid.patch +fixup-Add-a-test-to-check-for-quic-unvalidated-credit.patch +fixup-Add-a-test-to-check-for-quic-unvalidated-credit-1.patch +fixup-Add-a-test-to-check-for-quic-unvalidated-credit-2.patch +fixup-Add-a-test-to-check-for-quic-unvalidated-credit-3.patch +fixup-Add-a-test-to-check-for-quic-unvalidated-credit-4.patch +fixup-Add-a-test-to-check-for-quic-unvalidated-credit-5.patch +fixup-Add-a-test-to-check-for-quic-unvalidated-credit-6.patch +Make-the-ecp_sm2p256-scalar-multiplication-constant-time.patch +Fix-out-of-bounds-valid_flags-access-after-SSL_set_SSL_CT.patch +Add-regression-tests-for-the-SSL_set_SSL_CTX-sigalg-state.patch +fixup-Fix-out-of-bounds-valid_flags-access-after-SSL_set_.patch +CVE-2026-75804-QUIC-connection-level-flow-control-not-enf.patch +test-verifies-the-connection-level-RX-flow-control.patch +Guard-comparision-when-values-are-NULL.patch +Add-test-for-CVE-2026-75805.patch +TLS-Reject-undersized-TLS-1.2-AEAD-records-before-AEAD-pr.patch +ec-make-ossl_ec_scalar_mul_ladder-scalar-padding-constant.patch +sm2-make-sm2_sig_gen-constant-time.patch +dtls-reset-init_off-before-retransmitting-a-message.patch +CVE-2026-84784-QUIC-unbounded-RETIRE_CONNECTION_ID-backlo.patch +CVE-2026-84784-QUIC-unbounded-RETIRE_CONNECTION_ID-backlo-1.patch +Add-a-red-black-tree-implementation.patch +quic-move-the-RXE-definition-to-a-local-header.patch +test-cover-the-packet-pinning-path-of-QUIC-stream-reassem.patch +test-cover-a-long-lagging-read-of-packet-backed-stream-da.patch +test-reassemble-small-out-of-order-frames-and-check-the-b.patch +New-implementation-of-stream-reassembly-for-QUIC.patch +Limit-packet-buffer-overhead-to-64kB-per-stream.patch +Enforce-final-size-for-streams.patch +Add-explicit-tests-to-check-some-typical-stream-reassembl.patch +test-cover-FIN-final-size-against-buffered-data.patch +test-cover-zero-length-read-of-a-QUIC-rstream.patch +test-cover-two-sided-overlap-of-direct-tail-chunk.patch +test-cover-cleansing-of-dropped-duplicate-bytes.patch +test-cover-sc_data_trim_right-function.patch +make-ch_cleanup-just-safe-enough-to-be-called-by-quic_str.patch +Add-ossl_list_TYPE_join-head-tail-function.patch diff -Nru openssl-3.5.7/debian/patches/sm2-make-sm2_sig_gen-constant-time.patch openssl-3.5.7/debian/patches/sm2-make-sm2_sig_gen-constant-time.patch --- openssl-3.5.7/debian/patches/sm2-make-sm2_sig_gen-constant-time.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/sm2-make-sm2_sig_gen-constant-time.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,156 @@ +From: Igor Ustinov +Date: Fri, 31 Jul 2026 17:18:41 +0200 +Subject: sm2: make sm2_sig_gen() constant time + +After fixing CVE-2025-9231, sm2_sig_gen() still used variable-time +BN_mod_mul() and BN_sub() on private key and nonce material. +This commit makes it fully constant time. + +Fixes CVE-2026-77696 + +Co-authored-by: Viktor Dukhovni + +Assisted-by: Claude:claude-opus-4-8 +--- + crypto/ec/ec_mult.c | 7 +++--- + crypto/sm2/sm2_sign.c | 61 +++++++++++++++++++++++++++++++++++++++++++-------- + 2 files changed, 55 insertions(+), 13 deletions(-) + +diff --git a/crypto/ec/ec_mult.c b/crypto/ec/ec_mult.c +index 94208a7c6a43..d38368c40392 100644 +--- a/crypto/ec/ec_mult.c ++++ b/crypto/ec/ec_mult.c +@@ -220,10 +220,9 @@ int ossl_ec_scalar_mul_ladder(const EC_GROUP *group, EC_POINT *r, + * expected to arrive either BN_FLG_CONSTTIME or fixed-top, so that their + * top is a public, value-independent width and the copy length does not + * leak their magnitude. ECDSA satisfies this via +- * ossl_bn_priv_rand_range_fixed_top(); the generic SM2 signing path does +- * not yet (see the sm2_sig_gen() hardening tracked separately). The +- * fixed-top pinning below makes the subsequent arithmetic constant time +- * regardless, but cannot retroactively fix the copy length here. ++ * ossl_bn_priv_rand_range_fixed_top(). The fixed-top pinning below makes ++ * the subsequent arithmetic constant time regardless, but cannot ++ * retroactively fix the copy length here. + */ + if (!BN_copy(k, scalar)) { + ERR_raise(ERR_LIB_EC, ERR_R_BN_LIB); +diff --git a/crypto/sm2/sm2_sign.c b/crypto/sm2/sm2_sign.c +index 9389c70817a5..2ba4914357b3 100644 +--- a/crypto/sm2/sm2_sign.c ++++ b/crypto/sm2/sm2_sign.c +@@ -14,6 +14,7 @@ + #include "crypto/sm2.h" + #include "crypto/sm2err.h" + #include "crypto/ec.h" /* ossl_ec_group_do_inverse_ord() */ ++#include "crypto/bn.h" /* fixed-top / Montgomery constant-time BN helpers */ + #include "internal/numbers.h" + #include + #include +@@ -215,17 +216,22 @@ static ECDSA_SIG *sm2_sig_gen(const EC_KEY *key, const BIGNUM *e) + EC_POINT *kG = NULL; + BN_CTX *ctx = NULL; + BIGNUM *k = NULL; +- BIGNUM *rk = NULL; + BIGNUM *r = NULL; + BIGNUM *s = NULL; + BIGNUM *x1 = NULL; + BIGNUM *tmp = NULL; ++ BN_MONT_CTX *mont = EC_GROUP_get_mont_data(group); + OSSL_LIB_CTX *libctx = ossl_ec_key_get_libctx(key); + + if (dA == NULL) { + ERR_raise(ERR_LIB_SM2, SM2_R_INVALID_PRIVATE_KEY); + goto done; + } ++ ++ if (mont == NULL) { ++ ERR_raise(ERR_LIB_SM2, ERR_R_EC_LIB); ++ goto done; ++ } + kG = EC_POINT_new(group); + if (kG == NULL) { + ERR_raise(ERR_LIB_SM2, ERR_R_EC_LIB); +@@ -239,7 +245,6 @@ static ECDSA_SIG *sm2_sig_gen(const EC_KEY *key, const BIGNUM *e) + + BN_CTX_start(ctx); + k = BN_CTX_get(ctx); +- rk = BN_CTX_get(ctx); + x1 = BN_CTX_get(ctx); + tmp = BN_CTX_get(ctx); + if (tmp == NULL) { +@@ -273,6 +278,18 @@ static ECDSA_SIG *sm2_sig_gen(const EC_KEY *key, const BIGNUM *e) + ERR_raise(ERR_LIB_SM2, ERR_R_INTERNAL_ERROR); + goto done; + } ++ /* ++ * Pin the nonce to a fixed, value-independent width and flag it ++ * BN_FLG_CONSTTIME, so its magnitude does not leak through operand ++ * lengths in the scalar copy inside the ladder or in the arithmetic ++ * below. BN_priv_rand_range_ex() is kept so the nonce value itself ++ * is unchanged; only its representation is pinned. ++ */ ++ BN_set_flags(k, BN_FLG_CONSTTIME); ++ if (!bn_set_top_fixed(k, bn_get_top(order))) { ++ ERR_raise(ERR_LIB_SM2, ERR_R_BN_LIB); ++ goto done; ++ } + + if (!EC_POINT_mul(group, kG, k, NULL, NULL, ctx) + || !EC_POINT_get_affine_coordinates(group, kG, x1, NULL, +@@ -282,23 +299,49 @@ static ECDSA_SIG *sm2_sig_gen(const EC_KEY *key, const BIGNUM *e) + goto done; + } + +- /* try again if r == 0 or r+k == n */ ++ /* try again if r == 0 or r + k == n */ + if (BN_is_zero(r)) + continue; + +- if (!BN_add(rk, r, k)) { +- ERR_raise(ERR_LIB_SM2, ERR_R_INTERNAL_ERROR); ++ /* ++ * Since 0 < r < n and 0 < k < n, r + k == n is the same as ++ * k == n - r. Both operands of the subtraction are public, so ++ * compute it in the open and then compare against the nonce with a ++ * fixed-width constant-time comparison. A BN_cmp() on r + k would ++ * branch on whether the sum carried into an extra word, which ++ * depends on the value of k. ++ */ ++ if (!BN_sub(tmp, order, r) ++ || !bn_set_top_fixed(tmp, bn_get_top(order))) { ++ ERR_raise(ERR_LIB_SM2, ERR_R_BN_LIB); + goto done; + } + +- if (BN_cmp(rk, order) == 0) ++ if (CRYPTO_memcmp(bn_get_words(k), bn_get_words(tmp), ++ bn_get_top(order) * sizeof(BN_ULONG)) ++ == 0) + continue; + ++ /* ++ * s = ((1 + dA)^-1 * (k - r * dA)) mod order ++ * ++ * Computed with fixed-top / Montgomery constant-time primitives, so ++ * that the running time does not depend on the secret k or dA (the ++ * generic BN_mod_mul()/BN_sub() used previously reduce via BN_div(), ++ * whose timing is value dependent). This mirrors the ECDSA path. ++ * ++ * s holds (1 + dA)^-1 throughout; the (k - r * dA) term is built in ++ * tmp. bn_mul_mont_fixed_top() with one operand in the Montgomery ++ * domain yields the plain product, and the final ++ * BN_mod_mul_montgomery() returns the user-visible, normalised value. ++ */ + if (!BN_add(s, dA, BN_value_one()) + || !ossl_ec_group_do_inverse_ord(group, s, s, ctx) +- || !BN_mod_mul(tmp, dA, r, order, ctx) +- || !BN_sub(tmp, k, tmp) +- || !BN_mod_mul(s, s, tmp, order, ctx)) { ++ || !bn_to_mont_fixed_top(tmp, r, mont, ctx) ++ || !bn_mul_mont_fixed_top(tmp, tmp, dA, mont, ctx) ++ || !bn_mod_sub_fixed_top(tmp, k, tmp, order) ++ || !bn_to_mont_fixed_top(tmp, tmp, mont, ctx) ++ || !BN_mod_mul_montgomery(s, tmp, s, mont, ctx)) { + ERR_raise(ERR_LIB_SM2, ERR_R_BN_LIB); + goto done; + } diff -Nru openssl-3.5.7/debian/patches/test-cover-FIN-final-size-against-buffered-data.patch openssl-3.5.7/debian/patches/test-cover-FIN-final-size-against-buffered-data.patch --- openssl-3.5.7/debian/patches/test-cover-FIN-final-size-against-buffered-data.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/test-cover-FIN-final-size-against-buffered-data.patch 2026-09-29 20:10:12.000000000 +0000 @@ -0,0 +1,193 @@ +From: Jakub Zelenka +Date: Mon, 7 Sep 2026 17:09:41 +0200 +Subject: test: cover FIN final size against buffered data + +A FIN whose final size lies below data already buffered in a higher +range passes the current validation because only the lowest range is +checked, so the frames beyond the final size stay pinned unreadable +until the stream is torn down. A FIN below the offset the application +has consumed already skips the validation entirely when no ranges are +buffered, so it is recorded but can never signal the end of the stream. +Both must be rejected as a final size error per RFC 9000 section 4.5. + +Assisted-by: Claude:claude-fable-5 +Reviewed-by: Tomas Mraz +Reviewed-by: Milan Broz +Merge-date: Sat Sep 26 11:53:43 2026 +Merged-from: https://github.com/openssl/openssl/pull/32769 +--- + test/quic_stream_test.c | 154 ++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 154 insertions(+) + +diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c +index 23a2ec85b460..7e01dffc7b65 100644 +--- a/test/quic_stream_test.c ++++ b/test/quic_stream_test.c +@@ -1105,6 +1105,158 @@ static int test_rstream_dstorage_two_sided_overlap(void) + return ret; + } + ++/* ++ * A zero length read is a successful no-op returning zero read bytes, ++ * and releasing a record without consuming any bytes succeeds likewise. ++ * Neither may fail once at least one byte has been consumed, otherwise ++ * quic_read_actual() turns the failed read into a fatal SSL error for ++ * SSL_read_ex() called with a zero length buffer. ++ */ ++static int test_rstream_zero_length_read(void) ++{ ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ OSSL_QRX_PKT *pkt = NULL; ++ unsigned char pdata[10], buf[10]; ++ const unsigned char *record = NULL; ++ size_t readbytes = 0, rec_len = 0, i; ++ int fin = 0; ++ int ret = 0; ++ ++ for (i = 0; i < sizeof(pdata); ++i) ++ pdata[i] = (unsigned char)(0x40 + i); ++ ++ if (!TEST_ptr(pkt = pkt_test_new(1200)) ++ || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) ++ goto err; ++ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt, 0, ++ pdata, sizeof(pdata), 0))) ++ goto err; ++ ++ /* a zero length read before anything is consumed */ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 0)) ++ goto err; ++ ++ /* consume some bytes so the stream offset is not zero */ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 5, &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 5) ++ || !TEST_mem_eq(buf, 5, pdata, 5)) ++ goto err; ++ ++ /* a zero length read with data pending at a nonzero offset */ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 0)) ++ goto err; ++ ++ /* releasing a record without consuming anything succeeds too */ ++ if (!TEST_true(ossl_quic_rstream_get_record(rstream, &record, &rec_len, ++ &fin)) ++ || !TEST_size_t_eq(rec_len, 5) ++ || !TEST_true(ossl_quic_rstream_release_record(rstream, 0))) ++ goto err; ++ ++ /* the remaining bytes are intact and still readable */ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, buf, sizeof(buf), ++ &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 5) ++ || !TEST_mem_eq(buf, 5, pdata + 5, 5)) ++ goto err; ++ ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ ++ ret = 1; ++ ++err: ++ ossl_quic_rstream_free(rstream); ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ pkt_test_free(pkt); ++ ossl_quic_channel_free(ch); ++ return ret; ++} ++ ++/* ++ * A FIN must be rejected as a final size error when data is already ++ * buffered past its offset, or when the application has consumed more ++ * bytes than the final size claims. Otherwise the frames beyond the FIN ++ * offset stay pinned unreadable until the stream is torn down, and a ++ * FIN below the consumed offset is recorded but can never signal the ++ * end of the stream to the application. ++ */ ++static int test_rstream_fin_final_size(void) ++{ ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ OSSL_QRX_PKT *pkt_a = NULL, *pkt_b = NULL; ++ unsigned char pdata[16], buf[16]; ++ size_t readbytes = 0, i; ++ int fin = 0; ++ int ret = 0; ++ ++ for (i = 0; i < sizeof(pdata); ++i) ++ pdata[i] = (unsigned char)(0x40 + i); ++ ++ /* a FIN below data which is already buffered in a higher range */ ++ if (!TEST_ptr(pkt_a = pkt_test_new(1200)) ++ || !TEST_ptr(pkt_b = pkt_test_new(1200)) ++ || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) ++ goto err; ++ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_a, 0, ++ pdata, 10, 0)) ++ || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_b, 100, ++ pdata, 10, 0))) ++ goto err; ++ ++ if (!TEST_false(ossl_quic_rstream_queue_data(rstream, NULL, 50, NULL, ++ 0, 1)) ++ || !TEST_int_eq(ch->protocol_error, 1)) ++ goto err; ++ ++ ossl_quic_rstream_free(rstream); ++ rstream = NULL; ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ rsqp = NULL; ++ ossl_quic_channel_free(ch); ++ ch = NULL; ++ ++ /* a FIN below the offset the application has consumed already */ ++ if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) ++ goto err; ++ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_a, 0, ++ pdata, 10, 0)) ++ || !TEST_true(ossl_quic_rstream_read(rstream, buf, sizeof(buf), ++ &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 10)) ++ goto err; ++ ++ if (!TEST_false(ossl_quic_rstream_queue_data(rstream, NULL, 3, NULL, ++ 0, 1)) ++ || !TEST_int_eq(ch->protocol_error, 1)) ++ goto err; ++ ++ ret = 1; ++ ++err: ++ ossl_quic_rstream_free(rstream); ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ pkt_test_free(pkt_a); ++ pkt_test_free(pkt_b); ++ ossl_quic_channel_free(ch); ++ return ret; ++} ++ + #define FILL_PATTERN "abcdefghijklmnopqrstuvwxyz0123456789" \ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +@@ -2412,6 +2564,8 @@ int setup_tests(void) + ADD_TEST(test_rstream_pkt_overhead); + ADD_ALL_TESTS(test_rstream_reorder, 40); + ADD_TEST(test_rstream_dstorage_two_sided_overlap); ++ ADD_TEST(test_rstream_zero_length_read); ++ ADD_TEST(test_rstream_fin_final_size); + ADD_TEST(test_rstream_chunk_partial_overlap); + ADD_TEST(test_rstream_chunk_full_overlap); + ADD_TEST(test_rstream_range_overlap); diff -Nru openssl-3.5.7/debian/patches/test-cover-a-long-lagging-read-of-packet-backed-stream-da.patch openssl-3.5.7/debian/patches/test-cover-a-long-lagging-read-of-packet-backed-stream-da.patch --- openssl-3.5.7/debian/patches/test-cover-a-long-lagging-read-of-packet-backed-stream-da.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/test-cover-a-long-lagging-read-of-packet-backed-stream-da.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,118 @@ +From: Jakub Zelenka +Date: Wed, 26 Aug 2026 16:36:28 +0200 +Subject: test: cover a long lagging read of packet backed stream data +MIME-Version: 1.0 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 8bit + +Queue many small contiguous frames that each pin their own packet while +the reader stays behind, so a large number of packets are held at once and +then released as the data is finally consumed. This exercises the packet +reference lifecycle at a scale the other tests do not reach, and gives a +reassembly that copies data out of packets under memory pressure something +to run against. + +Assisted-by: Claude:claude-opus-4-8 + +Reviewed-by: Tomas Mraz +Reviewed-by: Saša Nedvědický +MergeDate: Fri Aug 28 14:03:47 2026 +Reviewed-by: Milan Broz +Merged-from: https://github.com/openssl/openssl/pull/32769 +--- + test/quic_stream_test.c | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 76 insertions(+) + +diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c +index 2493d3ff136d..2e6cadda35ba 100644 +--- a/test/quic_stream_test.c ++++ b/test/quic_stream_test.c +@@ -783,6 +783,81 @@ static int test_rstream_pkt(void) + return ret; + } + ++/* ++ * Many small contiguous frames, each pinning its own packet while the reader ++ * lags behind, so a large number of packets are held at once and released only ++ * as the data is finally consumed. Every byte must still read back in order and ++ * every packet reference must end up released. ++ */ ++static int test_rstream_pkt_overhead(void) ++{ ++ QUIC_RSTREAM *rstream = NULL; ++ OSSL_QRX_PKT **pkt = NULL; ++ unsigned char *data = NULL, *buf = NULL; ++ const size_t framesz = 8; ++ const size_t nframes = 4096; /* far past a 64 KiB overhead limit */ ++ const size_t total = framesz * nframes; ++ const size_t read_lag = 200; /* read only after this many arrive */ ++ size_t i, got = 0, readbytes = 0; ++ int fin = 0, ret = 0; ++ ++ if (!TEST_ptr(data = OPENSSL_malloc(total)) ++ || !TEST_ptr(buf = OPENSSL_malloc(total)) ++ || !TEST_ptr(pkt = OPENSSL_zalloc(nframes * sizeof(*pkt))) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, 0))) ++ goto err; ++ ++ for (i = 0; i < total; ++i) ++ data[i] = (unsigned char)(i & 0xff); ++ ++ for (i = 0; i < nframes; ++i) { ++ if (!TEST_ptr(pkt[i] = pkt_test_new(1200))) ++ goto err; ++ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[i], ++ i * framesz, data + i * framesz, framesz, ++ i == nframes - 1))) ++ goto err; ++ ++ /* let the reader fall behind, then drain what has become available */ ++ if (i % read_lag == read_lag - 1) ++ while (got < total ++ && TEST_true(ossl_quic_rstream_read(rstream, buf + got, ++ total - got, &readbytes, &fin)) ++ && readbytes > 0) ++ got += readbytes; ++ } ++ ++ /* drain whatever is left and check every byte survived in order */ ++ while (got < total ++ && TEST_true(ossl_quic_rstream_read(rstream, buf + got, total - got, ++ &readbytes, &fin)) ++ && readbytes > 0) ++ got += readbytes; ++ ++ if (!TEST_size_t_eq(got, total) ++ || !TEST_true(fin) ++ || !TEST_mem_eq(buf, got, data, total)) ++ goto err; ++ ++ /* every consumed frame has released the reference it held on its packet */ ++ for (i = 0; i < nframes; ++i) ++ if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) ++ goto err; ++ ++ ret = 1; ++ ++err: ++ ossl_quic_rstream_free(rstream); ++ if (pkt != NULL) ++ for (i = 0; i < nframes; ++i) ++ pkt_test_free(pkt[i]); ++ OPENSSL_free(pkt); ++ OPENSSL_free(data); ++ OPENSSL_free(buf); ++ return ret; ++} ++ + int setup_tests(void) + { + ADD_TEST(test_sstream_simple); +@@ -790,5 +865,6 @@ int setup_tests(void) + ADD_ALL_TESTS(test_rstream_simple, 4); + ADD_ALL_TESTS(test_rstream_random, 100); + ADD_TEST(test_rstream_pkt); ++ ADD_TEST(test_rstream_pkt_overhead); + return 1; + } diff -Nru openssl-3.5.7/debian/patches/test-cover-cleansing-of-dropped-duplicate-bytes.patch openssl-3.5.7/debian/patches/test-cover-cleansing-of-dropped-duplicate-bytes.patch --- openssl-3.5.7/debian/patches/test-cover-cleansing-of-dropped-duplicate-bytes.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/test-cover-cleansing-of-dropped-duplicate-bytes.patch 2026-09-29 20:10:12.000000000 +0000 @@ -0,0 +1,158 @@ +From: Jakub Zelenka +Date: Tue, 8 Sep 2026 11:56:39 +0200 +Subject: test: cover cleansing of dropped duplicate bytes + +With cleansing enabled every dropped duplicate byte of an incoming +frame must be wiped in its packet buffer: a retransmit reaching below +the consumed offset, a retransmit consumed entirely, a duplicate +contained in an existing range and the tail trimmed away when a short +frame is prepended to a direct storage chunk. Otherwise the plaintext +stays in the recycled packet buffer which nothing else cleanses. + +Assisted-by: Claude:claude-fable-5 +Reviewed-by: Tomas Mraz +Reviewed-by: Milan Broz +Merge-date: Sat Sep 26 11:53:46 2026 +Merged-from: https://github.com/openssl/openssl/pull/32769 +--- + test/quic_stream_test.c | 120 ++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 120 insertions(+) + +diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c +index 6e2047040822..d157e61edd90 100644 +--- a/test/quic_stream_test.c ++++ b/test/quic_stream_test.c +@@ -1257,6 +1257,125 @@ static int test_rstream_fin_final_size(void) + return ret; + } + ++static int cleanse_buf_check(const unsigned char *buf, size_t size, ++ size_t data_start, size_t data_end) ++{ ++ size_t i; ++ ++ for (i = 0; i < size; ++i) ++ if (!TEST_uchar_eq(buf[i], ++ i >= data_start && i < data_end ? 0x00 : 0xAA)) { ++ TEST_info("byte %zu", i); ++ return 0; ++ } ++ ++ return 1; ++} ++ ++/* ++ * With cleansing enabled, every dropped duplicate byte of an incoming ++ * frame must be wiped in its packet buffer: a retransmit reaching below ++ * the consumed offset, a retransmit consumed entirely, a duplicate ++ * contained in an existing range and the tail trimmed away when a short ++ * frame is prepended to a direct storage chunk. Otherwise the plaintext ++ * stays in the recycled packet buffer which nothing else cleanses. ++ */ ++static int test_rstream_cleanse_dropped_bytes(void) ++{ ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ OSSL_QRX_PKT *pkts[60] = { NULL }; ++ unsigned char src_a[16], src_b[16], src_c[16], src_d[16], src_e[16]; ++ unsigned char buf[16], fill = 0xFF; ++ size_t num_pkts = 0, readbytes = 0, i; ++ int fin = 0; ++ int ret = 0; ++ ++ memset(src_a, 0xAA, sizeof(src_a)); ++ memset(src_b, 0xAA, sizeof(src_b)); ++ memset(src_c, 0xAA, sizeof(src_c)); ++ memset(src_d, 0xAA, sizeof(src_d)); ++ memset(src_e, 0xAA, sizeof(src_e)); ++ for (i = 0; i < 10; ++i) ++ src_a[3 + i] = (unsigned char)(0x40 + i); ++ ++ for (i = 0; i < OSSL_NELEM(pkts); ++i) ++ if (!TEST_ptr(pkts[num_pkts++] = pkt_test_new(1200))) ++ goto err; ++ ++ if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) ++ goto err; ++ ossl_quic_rstream_set_cleanse(rstream, 1); ++ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[0], 0, ++ src_a + 3, 10, 0)) ++ || !TEST_true(ossl_quic_rstream_read(rstream, buf, 5, &readbytes, ++ &fin)) ++ || !TEST_size_t_eq(readbytes, 5)) ++ goto err; ++ ++ /* a retransmit reaching below the consumed offset is wiped whole */ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[1], 0, ++ src_b + 3, 10, 0)) ++ || !TEST_size_t_eq(pkt_test_refcount(pkts[1]), 1) ++ || !cleanse_buf_check(src_b, sizeof(src_b), 3, 13)) ++ goto err; ++ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 5, &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 5)) ++ goto err; ++ ++ /* a fully consumed retransmit is wiped whole */ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[2], 0, ++ src_c + 3, 10, 0)) ++ || !TEST_size_t_eq(pkt_test_refcount(pkts[2]), 1) ++ || !cleanse_buf_check(src_c, sizeof(src_c), 3, 13)) ++ goto err; ++ ++ /* ++ * disjoint 1-byte frames on 1200 byte datagrams take the stream ++ * past the overhead limit so short chunks use direct storage ++ */ ++ for (i = 0; i < 55; ++i) ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[3 + i], ++ 100 + 2 * i, &fill, 1, 0))) ++ goto err; ++ ++ /* the copied direct storage chunk has its source wiped already */ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[58], 20, ++ src_d + 3, 2, 0)) ++ || !TEST_size_t_eq(pkt_test_refcount(pkts[58]), 1) ++ || !cleanse_buf_check(src_d, sizeof(src_d), 3, 5)) ++ goto err; ++ ++ /* ++ * [18, 21) prepends [18, 20) to the direct storage chunk [20, 22) ++ * and drops the duplicate byte [20, 21), all three source bytes ++ * must be wiped ++ */ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[59], 18, ++ src_e + 3, 3, 0)) ++ || !TEST_size_t_eq(pkt_test_refcount(pkts[59]), 1) ++ || !cleanse_buf_check(src_e, sizeof(src_e), 3, 6)) ++ goto err; ++ ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ ++ ret = 1; ++ ++err: ++ ossl_quic_rstream_free(rstream); ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ for (i = 0; i < num_pkts; ++i) ++ pkt_test_free(pkts[i]); ++ ossl_quic_channel_free(ch); ++ return ret; ++} ++ + #define FILL_PATTERN "abcdefghijklmnopqrstuvwxyz0123456789" \ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +@@ -2566,6 +2685,7 @@ int setup_tests(void) + ADD_TEST(test_rstream_dstorage_two_sided_overlap); + ADD_TEST(test_rstream_zero_length_read); + ADD_TEST(test_rstream_fin_final_size); ++ ADD_TEST(test_rstream_cleanse_dropped_bytes); + ADD_TEST(test_rstream_chunk_partial_overlap); + ADD_TEST(test_rstream_chunk_full_overlap); + ADD_TEST(test_rstream_range_overlap); diff -Nru openssl-3.5.7/debian/patches/test-cover-sc_data_trim_right-function.patch openssl-3.5.7/debian/patches/test-cover-sc_data_trim_right-function.patch --- openssl-3.5.7/debian/patches/test-cover-sc_data_trim_right-function.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/test-cover-sc_data_trim_right-function.patch 2026-09-29 20:10:12.000000000 +0000 @@ -0,0 +1,244 @@ +From: Alexandr Nedvedicky +Date: Mon, 21 Sep 2026 17:15:33 +0200 +Subject: test: cover sc_data_trim_right() function + +sc_data_trim_right() function shrinks heap buffer with +stream data by trimming the buffer from its end. +In order to reclaim unused memory the function uses realloc(). + +To reach code in the sc_data_trim_right() the test queues the fill +frames first so packet buffer overhead tresshold is reached, +The fill frames are intentionally placed beyond the gap so +they don't interfer with testing done later. + +The test creates 3 dis-joint ranges. Each range is 64B long. +The offsets are: 0, 128, 256. Then partial read of 16 bytes happens. +After that the test inserts yet another stream frame that carries +160 bytes of data and lands to offset 63. The frame joins the +first two ranges. The join operation calls to sc_data_trim_right(). + +Finally to verify things do work as expected The test reads +all available data from stream and verifies those data match +the expected content. + +The test is based on code submitted by Mounir IDRASSI + +Co-authored-by: Mounir IDRASSI +Reviewed-by: Tomas Mraz +Reviewed-by: Milan Broz +Merge-date: Sat Sep 26 11:53:46 2026 +Merged-from: https://github.com/openssl/openssl/pull/32769 +--- + test/quic_stream_test.c | 192 ++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 192 insertions(+) + +diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c +index d157e61edd90..8e029650a734 100644 +--- a/test/quic_stream_test.c ++++ b/test/quic_stream_test.c +@@ -2673,6 +2673,197 @@ static int test_final_size_violation_data_first(void) + return ok; + } + ++#define FILLERS 56 ++ ++static int test_trim_right(void) ++{ ++ unsigned char data[4096]; ++ unsigned char read_buf[4096]; ++ size_t readbytes; ++ TEST_STREAM_CHUNK_T tsc_buf[4]; ++ OSSL_QRX_PKT *pkt[FILLERS + OSSL_NELEM(tsc_buf)] = { 0 }; ++ unsigned char fill = 0xFF; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ TEST_STREAM_CHUNK_T *tsc; ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; ++ int fin; ++ unsigned int i, pktnum; ++ int ok = 0; ++ ++ if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))) ++ return 0; ++ ++ if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))) ++ goto err; ++ ++ rstream = ossl_quic_rstream_new(NULL, NULL, rsqp); ++ if (!TEST_ptr(rstream)) ++ goto err; ++ ++ for (i = 0; i < sizeof(data); i++) ++ data[i] = FILL_PATTERN[i % (sizeof(FILL_PATTERN) - 1)]; ++ ++ memset(tsc_buf, 0, sizeof(tsc_buf)); ++ memset(read_buf, 0, sizeof(read_buf)); ++ ++ pktnum = 0; ++ for (i = 0; i < FILLERS; i++) { ++ pkt[pktnum] = pkt_test_new(1200); ++ if (!TEST_ptr(pkt[pktnum])) ++ goto err; ++ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[pktnum], ++ 1024 + 2 * i, &fill, 1, 0))) ++ goto err; ++ ++ pktnum++; ++ } ++ ++ /* ++ * send three chunks. each 64 bytes long, ++ * chunks create three ranges. ++ */ ++ tsc = &tsc_buf[0]; ++ tsc->tsc_data = &data[0]; ++ tsc->tsc_off = 0; ++ tsc->tsc_len = 64; ++ tsc->tsc_fin = 0; ++ tsc->tsc_ranges_exp = FILLERS + 1; ++ tsc->tsc_chunks_exp = FILLERS + 1; ++ ++ tsc = &tsc_buf[1]; ++ tsc->tsc_data = &data[128]; ++ tsc->tsc_off = 128; ++ tsc->tsc_len = 64; ++ tsc->tsc_fin = 0; ++ tsc->tsc_ranges_exp = FILLERS + 2; ++ tsc->tsc_chunks_exp = FILLERS + 2; ++ ++ tsc = &tsc_buf[2]; ++ tsc->tsc_data = &data[256]; ++ tsc->tsc_off = 256; ++ tsc->tsc_len = 64; ++ tsc->tsc_fin = 0; ++ tsc->tsc_ranges_exp = FILLERS + 3; ++ tsc->tsc_chunks_exp = FILLERS + 3; ++ ++ for (i = 0; i < OSSL_NELEM(tsc_buf) - 1; i++) { ++ pkt[pktnum] = pkt_test_new(1200); ++ if (!TEST_ptr(pkt[pktnum])) ++ goto err; ++ tsc = &tsc_buf[i]; ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[pktnum], ++ tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, tsc->tsc_fin))) ++ goto err; ++ ++ /* ++ * check our assumptions about about reassemble process internals. ++ */ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream), ++ tsc->tsc_ranges_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream), ++ tsc->tsc_chunks_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ /* ++ * the packet reference should stay at 1, because _rstream_queue_data() ++ * copies data to stream buffer instead of grabbing a reference to ++ * packet (advancing the packet's ref counter) ++ */ ++ if (!TEST_size_t_eq(pkt_test_refcount(pkt[pktnum]), 1)) ++ goto err; ++ pktnum++; ++ } ++ ++ /* ++ * do a partial read of 16 bytes. ++ */ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, read_buf, ++ 16, &readbytes, &fin))) ++ goto err; ++ ++ if (!TEST_uint64_t_eq(readbytes, 16)) ++ goto err; ++ ++ if (!TEST_false(fin)) ++ goto err; ++ ++ if (!TEST_mem_eq(read_buf, readbytes, data, readbytes)) ++ goto err; ++ ++ /* ++ * send chunk that glues three ranges together. ++ */ ++ tsc = &tsc_buf[3]; ++ tsc->tsc_data = &data[63]; ++ tsc->tsc_off = 63; ++ tsc->tsc_len = 160; ++ tsc->tsc_fin = 0; ++ tsc->tsc_ranges_exp = FILLERS + 2; ++ tsc->tsc_chunks_exp = FILLERS + 3; ++ ++ pkt[pktnum] = pkt_test_new(1200); ++ if (!TEST_ptr(pkt[pktnum])) ++ goto err; ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[pktnum], ++ tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, tsc->tsc_fin))) ++ goto err; ++ ++ /* ++ * check our assumptions about about reassemble process internals. ++ */ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream), ++ tsc->tsc_ranges_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream), ++ tsc->tsc_chunks_exp)) { ++ TEST_info("%s failing iteration %u", OPENSSL_FUNC, i); ++ goto err; ++ } ++ ++ if (!TEST_size_t_eq(pkt_test_refcount(pkt[pktnum]), 1)) ++ goto err; ++ pktnum++; ++ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, read_buf, ++ 4096, &readbytes, &fin))) ++ goto err; ++ ++ /* ++ * the ranges are still dis-joint, only 207 bytes can be read ++ */ ++ if (!TEST_uint64_t_eq(readbytes, 207)) ++ goto err; ++ ++ if (!TEST_false(fin)) ++ goto err; ++ ++ if (!TEST_mem_eq(read_buf, readbytes, &data[16], readbytes)) ++ goto err; ++ ++ ok = 1; ++err: ++ ossl_quic_rstream_free(rstream); ++ ++ for (i = 0; i < OSSL_NELEM(pkt); i++) ++ pkt_test_free(pkt[i]); ++ ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ ossl_quic_channel_free(ch); ++ ++ return ok; ++} ++ + int setup_tests(void) + { + ADD_TEST(test_sstream_simple); +@@ -2694,6 +2885,7 @@ int setup_tests(void) + ADD_TEST(test_rstream_mix_chunks); + ADD_TEST(test_final_size_violation_fin_first); + ADD_TEST(test_final_size_violation_data_first); ++ ADD_TEST(test_trim_right); + + return 1; + } diff -Nru openssl-3.5.7/debian/patches/test-cover-the-packet-pinning-path-of-QUIC-stream-reassem.patch openssl-3.5.7/debian/patches/test-cover-the-packet-pinning-path-of-QUIC-stream-reassem.patch --- openssl-3.5.7/debian/patches/test-cover-the-packet-pinning-path-of-QUIC-stream-reassem.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/test-cover-the-packet-pinning-path-of-QUIC-stream-reassem.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,374 @@ +From: Jakub Zelenka +Date: Wed, 26 Aug 2026 14:56:03 +0200 +Subject: test: cover the packet pinning path of QUIC stream reassembly +MIME-Version: 1.0 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 8bit + +Until now every rstream test queued data with a NULL packet, so the +production path where received chunks pin their OSSL_QRX_PKT via +reference counting was never exercised by any unit test. A NULL packet +is never passed in production, so the tests now always queue data in a +mock packet rather than toggling it. + +Have test_rstream_simple and test_rstream_random queue every frame in a +mock packet, and add a new test_rstream_pkt which asserts the reference +counting behaviour directly: references held while chunks are buffered, +shared packets referenced once per frame, references released when frames +are consumed, dropped by overlapping frames, moved to the ring buffer or +freed with the stream, and cleansing of a packet backed chunk wiping +exactly the chunk data. + +Assisted-by: Claude:claude-fable-5 + +Reviewed-by: Tomas Mraz +Reviewed-by: Saša Nedvědický +MergeDate: Fri Aug 28 14:03:46 2026 +Reviewed-by: Milan Broz +Merged-from: https://github.com/openssl/openssl/pull/32769 +--- + test/quic_stream_test.c | 217 ++++++++++++++++++++++++++++++++++++++++++++---- + 1 file changed, 203 insertions(+), 14 deletions(-) + +diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c +index 6a646d93636e..2493d3ff136d 100644 +--- a/test/quic_stream_test.c ++++ b/test/quic_stream_test.c +@@ -7,9 +7,42 @@ + * https://www.openssl.org/source/license.html + */ + #include "internal/packet.h" ++#include "internal/quic_record_rx.h" + #include "internal/quic_stream.h" ++#include "../ssl/quic/quic_record_rx_local.h" + #include "testutil.h" + ++/* ++ * A received packet as the stream code sees it, without a QRX behind it. ++ * The reference the caller keeps is never released by the stream code, so ++ * the reference count never reaches zero and the packet is never recycled ++ * through the QRX it does not have. It is freed with pkt_test_free() once ++ * the test is done with it. ++ */ ++static OSSL_QRX_PKT *pkt_test_new(size_t datagram_len) ++{ ++ RXE *rxe = OPENSSL_zalloc(sizeof(*rxe)); ++ ++ if (rxe == NULL) ++ return NULL; ++ ++ rxe->refcount = 1; ++ rxe->datagram_len = datagram_len; ++ rxe->pkt.datagram_len = datagram_len; ++ return &rxe->pkt; ++} ++ ++/* The number of references held on a packet, including the caller's own. */ ++static size_t pkt_test_refcount(const OSSL_QRX_PKT *pkt) ++{ ++ return ((const RXE *)pkt)->refcount; ++} ++ ++static void pkt_test_free(OSSL_QRX_PKT *pkt) ++{ ++ OPENSSL_free((RXE *)pkt); ++} ++ + static int compare_iov(const unsigned char *ref, size_t ref_len, + const OSSL_QTX_IOVEC *iov, size_t iov_len) + { +@@ -375,23 +408,29 @@ static const unsigned char simple_data[] = "Hello world! And thank you for all t + static int test_rstream_simple(int idx) + { + QUIC_RSTREAM *rstream = NULL; ++ OSSL_QRX_PKT *pkt[8] = { NULL }; + int ret = 0; + unsigned char buf[sizeof(simple_data)]; +- size_t readbytes = 0, avail = 0; ++ size_t readbytes = 0, avail = 0, i; + int fin = 0; +- int use_rbuf = idx > 1; +- int use_sc = idx % 2; ++ int use_sc = (idx & 1) != 0; ++ int use_rbuf = (idx & 2) != 0; + int (*read_fn)(QUIC_RSTREAM *, unsigned char *, size_t, size_t *, + int *) + = use_sc ? test_single_copy_read + : ossl_quic_rstream_read; + ++ /* every frame arrives in a packet, as it does in production */ ++ for (i = 0; i < OSSL_NELEM(pkt); ++i) ++ if (!TEST_ptr(pkt[i] = pkt_test_new(1200))) ++ goto err; ++ + if (!TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, 0))) + goto err; + +- if (!TEST_true(ossl_quic_rstream_queue_data(rstream, NULL, 5, ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[0], 5, + simple_data + 5, 10, 0)) +- || !TEST_true(ossl_quic_rstream_queue_data(rstream, NULL, ++ || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[1], + sizeof(simple_data) - 1, + simple_data + sizeof(simple_data) - 1, + 1, 1)) +@@ -399,11 +438,11 @@ static int test_rstream_simple(int idx) + &readbytes, &fin)) + || !TEST_false(fin) + || !TEST_size_t_eq(readbytes, 0) +- || !TEST_true(ossl_quic_rstream_queue_data(rstream, NULL, ++ || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[2], + sizeof(simple_data) - 10, + simple_data + sizeof(simple_data) - 10, + 10, 1)) +- || !TEST_true(ossl_quic_rstream_queue_data(rstream, NULL, 0, ++ || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[3], 0, + simple_data, 1, 0)) + || !TEST_true(ossl_quic_rstream_peek(rstream, buf, sizeof(buf), + &readbytes, &fin)) +@@ -415,10 +454,10 @@ static int test_rstream_simple(int idx) + && !TEST_true(ossl_quic_rstream_resize_rbuf(rstream, + sizeof(simple_data)))) + || (use_rbuf && !TEST_true(ossl_quic_rstream_move_to_rbuf(rstream))) +- || !TEST_true(ossl_quic_rstream_queue_data(rstream, NULL, ++ || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[4], + 0, simple_data, + 10, 0)) +- || !TEST_true(ossl_quic_rstream_queue_data(rstream, NULL, ++ || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[5], + sizeof(simple_data), + NULL, + 0, 1)) +@@ -427,7 +466,7 @@ static int test_rstream_simple(int idx) + || !TEST_false(fin) + || !TEST_size_t_eq(readbytes, 15) + || !TEST_mem_eq(buf, 15, simple_data, 15) +- || !TEST_true(ossl_quic_rstream_queue_data(rstream, NULL, ++ || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[6], + 15, + simple_data + 15, + sizeof(simple_data) - 15, 1)) +@@ -442,7 +481,7 @@ static int test_rstream_simple(int idx) + || !TEST_false(fin) + || !TEST_size_t_eq(readbytes, 12) + || !TEST_mem_eq(buf + 2, 12, simple_data + 2, 12) +- || !TEST_true(ossl_quic_rstream_queue_data(rstream, NULL, ++ || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[7], + sizeof(simple_data), + NULL, + 0, 1)) +@@ -469,6 +508,13 @@ static int test_rstream_simple(int idx) + + err: + ossl_quic_rstream_free(rstream); ++ /* All the references held by the stream must have been released */ ++ for (i = 0; i < OSSL_NELEM(pkt); ++i) { ++ if (pkt[i] != NULL ++ && !TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1)) ++ ret = 0; ++ pkt_test_free(pkt[i]); ++ } + return ret; + } + +@@ -477,14 +523,18 @@ static int test_rstream_random(int idx) + unsigned char *bulk_data = NULL; + unsigned char *read_buf = NULL; + QUIC_RSTREAM *rstream = NULL; +- size_t i, read_off, queued_min, queued_max; ++ OSSL_QRX_PKT **pkts = NULL; ++ size_t i, read_off, queued_min, queued_max, num_pkts = 0; + const size_t data_size = 10000; ++ /* At most two frames are queued per each of the 100 * 10 iterations */ ++ const size_t max_pkts = 100 * 10 * 2; + int r, s, fin = 0, fin_set = 0; + int ret = 0; + size_t readbytes = 0; + + if (!TEST_ptr(bulk_data = OPENSSL_malloc(data_size)) + || !TEST_ptr(read_buf = OPENSSL_malloc(data_size)) ++ || !TEST_ptr(pkts = OPENSSL_zalloc(sizeof(*pkts) * max_pkts)) + || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, 0))) + goto err; + +@@ -498,6 +548,7 @@ static int test_rstream_random(int idx) + for (r = 0; r < 100; ++r) { + for (s = 0; s < 10; ++s) { + size_t off = (r * 10 + s) * 10, size = 10; ++ OSSL_QRX_PKT *pkt = NULL; + + if (test_random() % 10 == 0) + /* drop packet */ +@@ -506,7 +557,12 @@ static int test_rstream_random(int idx) + if (off <= queued_min && off + size > queued_min) + queued_min = off + size; + +- if (!TEST_true(ossl_quic_rstream_queue_data(rstream, NULL, off, ++ /* each frame arrives in its own packet */ ++ if (!TEST_ptr(pkt = pkt_test_new(1200))) ++ goto err; ++ pkts[num_pkts++] = pkt; ++ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt, off, + bulk_data + off, + size, 0))) + goto err; +@@ -526,7 +582,12 @@ static int test_rstream_random(int idx) + if (off <= queued_min && off + size > queued_min) + queued_min = off + size; + +- if (!TEST_true(ossl_quic_rstream_queue_data(rstream, NULL, off, ++ /* a retransmit arrives in its own packet */ ++ if (!TEST_ptr(pkt = pkt_test_new(1200))) ++ goto err; ++ pkts[num_pkts++] = pkt; ++ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt, off, + bulk_data + off, + size, 0))) + goto err; +@@ -590,16 +651,144 @@ static int test_rstream_random(int idx) + + err: + ossl_quic_rstream_free(rstream); ++ if (pkts != NULL) { ++ /* All the references held by the stream must have been released */ ++ for (i = 0; i < num_pkts; ++i) { ++ if (!TEST_size_t_eq(pkt_test_refcount(pkts[i]), 1)) ++ ret = 0; ++ pkt_test_free(pkts[i]); ++ } ++ OPENSSL_free(pkts); ++ } + OPENSSL_free(bulk_data); + OPENSSL_free(read_buf); + return ret; + } + ++/* ++ * Verify the reference counting of packets pinned by buffered stream ++ * chunks and the cleansing of packet backed chunks. ++ */ ++static int test_rstream_pkt(void) ++{ ++ QUIC_RSTREAM *rstream = NULL; ++ OSSL_QRX_PKT *pkt_a = NULL, *pkt_b = NULL, *pkt_c = NULL; ++ unsigned char pdata[64], cbuf[64], buf[64]; ++ size_t readbytes = 0, avail = 0, i; ++ int fin = 0; ++ int ret = 0; ++ ++ for (i = 0; i < sizeof(pdata); ++i) ++ pdata[i] = (unsigned char)(0x40 + i); ++ ++ if (!TEST_ptr(pkt_a = pkt_test_new(1200)) ++ || !TEST_ptr(pkt_b = pkt_test_new(1200)) ++ || !TEST_ptr(pkt_c = pkt_test_new(1200)) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, 0))) ++ goto err; ++ ++ /* A buffered frame holds a reference to its packet */ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_a, 0, ++ pdata, 10, 0)) ++ || !TEST_size_t_eq(pkt_test_refcount(pkt_a), 2)) ++ goto err; ++ ++ /* Two frames from the same packet hold two references */ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_a, 20, ++ pdata + 20, 10, 0)) ++ || !TEST_size_t_eq(pkt_test_refcount(pkt_a), 3)) ++ goto err; ++ ++ /* A frame contained in already buffered data takes no reference */ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_b, 2, ++ pdata + 2, 6, 0)) ++ || !TEST_size_t_eq(pkt_test_refcount(pkt_b), 1)) ++ goto err; ++ ++ /* ++ * An overlapping frame drops the frames it covers and releases ++ * their references ++ */ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_c, 0, ++ pdata, 15, 0)) ++ || !TEST_size_t_eq(pkt_test_refcount(pkt_a), 2) ++ || !TEST_size_t_eq(pkt_test_refcount(pkt_c), 2)) ++ goto err; ++ ++ /* Reading past a frame releases its reference */ ++ if (!TEST_true(ossl_quic_rstream_available(rstream, &avail, &fin)) ++ || !TEST_size_t_eq(avail, 15) ++ || !TEST_true(ossl_quic_rstream_read(rstream, buf, sizeof(buf), ++ &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 15) ++ || !TEST_mem_eq(buf, 15, pdata, 15) ++ || !TEST_size_t_eq(pkt_test_refcount(pkt_c), 1) ++ || !TEST_size_t_eq(pkt_test_refcount(pkt_a), 2)) ++ goto err; ++ ++ /* Moving frames to the ring buffer releases their references */ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_b, 15, ++ pdata + 15, 5, 0)) ++ || !TEST_size_t_eq(pkt_test_refcount(pkt_b), 2) ++ || !TEST_true(ossl_quic_rstream_resize_rbuf(rstream, sizeof(pdata))) ++ || !TEST_true(ossl_quic_rstream_move_to_rbuf(rstream)) ++ || !TEST_size_t_eq(pkt_test_refcount(pkt_a), 1) ++ || !TEST_size_t_eq(pkt_test_refcount(pkt_b), 1)) ++ goto err; ++ ++ /* The moved data is still readable from the ring buffer */ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, buf, sizeof(buf), ++ &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 15) ++ || !TEST_mem_eq(buf, 15, pdata + 15, 15)) ++ goto err; ++ ++ /* Freeing the stream releases the references of buffered frames */ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_c, 30, ++ pdata + 30, 10, 0)) ++ || !TEST_size_t_eq(pkt_test_refcount(pkt_c), 2)) ++ goto err; ++ ossl_quic_rstream_free(rstream); ++ rstream = NULL; ++ if (!TEST_size_t_eq(pkt_test_refcount(pkt_c), 1)) ++ goto err; ++ ++ /* ++ * Cleansing a consumed packet backed chunk wipes exactly the chunk ++ * data, leaving the surrounding bytes intact. ++ */ ++ memset(cbuf, 0xAA, sizeof(cbuf)); ++ if (!TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, 0))) ++ goto err; ++ ossl_quic_rstream_set_cleanse(rstream, 1); ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_a, 0, ++ cbuf + 8, 48, 0)) ++ || !TEST_size_t_eq(pkt_test_refcount(pkt_a), 2) ++ || !TEST_true(ossl_quic_rstream_read(rstream, buf, 48, ++ &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 48) ++ || !TEST_size_t_eq(pkt_test_refcount(pkt_a), 1)) ++ goto err; ++ for (i = 0; i < sizeof(cbuf); ++i) ++ if (!TEST_uchar_eq(cbuf[i], i >= 8 && i < 56 ? 0 : 0xAA)) ++ goto err; ++ ++ ret = 1; ++ ++err: ++ ossl_quic_rstream_free(rstream); ++ pkt_test_free(pkt_a); ++ pkt_test_free(pkt_b); ++ pkt_test_free(pkt_c); ++ return ret; ++} ++ + int setup_tests(void) + { + ADD_TEST(test_sstream_simple); + ADD_ALL_TESTS(test_sstream_bulk, 100); + ADD_ALL_TESTS(test_rstream_simple, 4); + ADD_ALL_TESTS(test_rstream_random, 100); ++ ADD_TEST(test_rstream_pkt); + return 1; + } diff -Nru openssl-3.5.7/debian/patches/test-cover-two-sided-overlap-of-direct-tail-chunk.patch openssl-3.5.7/debian/patches/test-cover-two-sided-overlap-of-direct-tail-chunk.patch --- openssl-3.5.7/debian/patches/test-cover-two-sided-overlap-of-direct-tail-chunk.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/test-cover-two-sided-overlap-of-direct-tail-chunk.patch 2026-09-29 20:10:12.000000000 +0000 @@ -0,0 +1,271 @@ +From: Jakub Zelenka +Date: Mon, 7 Sep 2026 15:28:13 +0200 +Subject: test: cover two sided overlap of direct tail chunk + +A short retransmitted frame which starts below an existing range and +ends past its direct storage tail chunk slips past the full overlap +guard in try_dstorage() because it is not larger than the direct +storage size. The append path then treats every byte below the tail +chunk end as a duplicate and drops the genuinely new bytes below the +range start while reporting success, so the frame is acked and the +gap in the stream becomes permanent. + +Assisted-by: Claude:claude-fable-5 +Reviewed-by: Tomas Mraz +Reviewed-by: Milan Broz +Merge-date: Sat Sep 26 11:53:45 2026 +Merged-from: https://github.com/openssl/openssl/pull/32769 +--- + test/quic_stream_test.c | 225 ++++++++++++++++-------------------------------- + 1 file changed, 75 insertions(+), 150 deletions(-) + +diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c +index da322249ef1d..6e2047040822 100644 +--- a/test/quic_stream_test.c ++++ b/test/quic_stream_test.c +@@ -1013,6 +1013,81 @@ static int test_rstream_reorder(int idx) + return ret; + } + ++/* ++ * A zero length read is a successful no-op returning zero read bytes, ++ * and releasing a record without consuming any bytes succeeds likewise. ++ * Neither may fail once at least one byte has been consumed, otherwise ++ * quic_read_actual() turns the failed read into a fatal SSL error for ++ * SSL_read_ex() called with a zero length buffer. ++ */ ++static int test_rstream_zero_length_read(void) ++{ ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ OSSL_QRX_PKT *pkt = NULL; ++ unsigned char pdata[10], buf[10]; ++ const unsigned char *record = NULL; ++ size_t readbytes = 0, rec_len = 0, i; ++ int fin = 0; ++ int ret = 0; ++ ++ for (i = 0; i < sizeof(pdata); ++i) ++ pdata[i] = (unsigned char)(0x40 + i); ++ ++ if (!TEST_ptr(pkt = pkt_test_new(1200)) ++ || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) ++ goto err; ++ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt, 0, ++ pdata, sizeof(pdata), 0))) ++ goto err; ++ ++ /* a zero length read before anything is consumed */ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 0)) ++ goto err; ++ ++ /* consume some bytes so the stream offset is not zero */ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 5, &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 5) ++ || !TEST_mem_eq(buf, 5, pdata, 5)) ++ goto err; ++ ++ /* a zero length read with data pending at a nonzero offset */ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 0)) ++ goto err; ++ ++ /* releasing a record without consuming anything succeeds too */ ++ if (!TEST_true(ossl_quic_rstream_get_record(rstream, &record, &rec_len, ++ &fin)) ++ || !TEST_size_t_eq(rec_len, 5) ++ || !TEST_true(ossl_quic_rstream_release_record(rstream, 0))) ++ goto err; ++ ++ /* the remaining bytes are intact and still readable */ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, buf, sizeof(buf), ++ &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 5) ++ || !TEST_mem_eq(buf, 5, pdata + 5, 5)) ++ goto err; ++ ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ ++ ret = 1; ++ ++err: ++ ossl_quic_rstream_free(rstream); ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ pkt_test_free(pkt); ++ ossl_quic_channel_free(ch); ++ return ret; ++} ++ + /* + * A short retransmit which starts below an existing range and ends past its + * direct storage tail chunk is small enough to slip past the full overlap +@@ -1105,81 +1180,6 @@ static int test_rstream_dstorage_two_sided_overlap(void) + return ret; + } + +-/* +- * A zero length read is a successful no-op returning zero read bytes, +- * and releasing a record without consuming any bytes succeeds likewise. +- * Neither may fail once at least one byte has been consumed, otherwise +- * quic_read_actual() turns the failed read into a fatal SSL error for +- * SSL_read_ex() called with a zero length buffer. +- */ +-static int test_rstream_zero_length_read(void) +-{ +- QUIC_RSTREAM *rstream = NULL; +- QUIC_CHANNEL *ch = NULL; +- QUIC_RSTREAM_QPARM *rsqp = NULL; +- OSSL_QRX_PKT *pkt = NULL; +- unsigned char pdata[10], buf[10]; +- const unsigned char *record = NULL; +- size_t readbytes = 0, rec_len = 0, i; +- int fin = 0; +- int ret = 0; +- +- for (i = 0; i < sizeof(pdata); ++i) +- pdata[i] = (unsigned char)(0x40 + i); +- +- if (!TEST_ptr(pkt = pkt_test_new(1200)) +- || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))) +- || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)) +- || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) +- goto err; +- +- if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt, 0, +- pdata, sizeof(pdata), 0))) +- goto err; +- +- /* a zero length read before anything is consumed */ +- if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin)) +- || !TEST_size_t_eq(readbytes, 0)) +- goto err; +- +- /* consume some bytes so the stream offset is not zero */ +- if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 5, &readbytes, &fin)) +- || !TEST_size_t_eq(readbytes, 5) +- || !TEST_mem_eq(buf, 5, pdata, 5)) +- goto err; +- +- /* a zero length read with data pending at a nonzero offset */ +- if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin)) +- || !TEST_size_t_eq(readbytes, 0)) +- goto err; +- +- /* releasing a record without consuming anything succeeds too */ +- if (!TEST_true(ossl_quic_rstream_get_record(rstream, &record, &rec_len, +- &fin)) +- || !TEST_size_t_eq(rec_len, 5) +- || !TEST_true(ossl_quic_rstream_release_record(rstream, 0))) +- goto err; +- +- /* the remaining bytes are intact and still readable */ +- if (!TEST_true(ossl_quic_rstream_read(rstream, buf, sizeof(buf), +- &readbytes, &fin)) +- || !TEST_size_t_eq(readbytes, 5) +- || !TEST_mem_eq(buf, 5, pdata + 5, 5)) +- goto err; +- +- if (!TEST_int_eq(ch->protocol_error, 0)) +- goto err; +- +- ret = 1; +- +-err: +- ossl_quic_rstream_free(rstream); +- ossl_quic_rstream_qparm_destroy(rsqp); +- pkt_test_free(pkt); +- ossl_quic_channel_free(ch); +- return ret; +-} +- + /* + * A FIN must be rejected as a final size error when data is already + * buffered past its offset, or when the application has consumed more +@@ -1257,81 +1257,6 @@ static int test_rstream_fin_final_size(void) + return ret; + } + +-/* +- * A zero length read is a successful no-op returning zero read bytes, +- * and releasing a record without consuming any bytes succeeds likewise. +- * Neither may fail once at least one byte has been consumed, otherwise +- * quic_read_actual() turns the failed read into a fatal SSL error for +- * SSL_read_ex() called with a zero length buffer. +- */ +-static int test_rstream_zero_length_read(void) +-{ +- QUIC_RSTREAM *rstream = NULL; +- QUIC_CHANNEL *ch = NULL; +- QUIC_RSTREAM_QPARM *rsqp = NULL; +- OSSL_QRX_PKT *pkt = NULL; +- unsigned char pdata[10], buf[10]; +- const unsigned char *record = NULL; +- size_t readbytes = 0, rec_len = 0, i; +- int fin = 0; +- int ret = 0; +- +- for (i = 0; i < sizeof(pdata); ++i) +- pdata[i] = (unsigned char)(0x40 + i); +- +- if (!TEST_ptr(pkt = pkt_test_new(1200)) +- || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))) +- || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)) +- || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) +- goto err; +- +- if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt, 0, +- pdata, sizeof(pdata), 0))) +- goto err; +- +- /* a zero length read before anything is consumed */ +- if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin)) +- || !TEST_size_t_eq(readbytes, 0)) +- goto err; +- +- /* consume some bytes so the stream offset is not zero */ +- if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 5, &readbytes, &fin)) +- || !TEST_size_t_eq(readbytes, 5) +- || !TEST_mem_eq(buf, 5, pdata, 5)) +- goto err; +- +- /* a zero length read with data pending at a nonzero offset */ +- if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin)) +- || !TEST_size_t_eq(readbytes, 0)) +- goto err; +- +- /* releasing a record without consuming anything succeeds too */ +- if (!TEST_true(ossl_quic_rstream_get_record(rstream, &record, &rec_len, +- &fin)) +- || !TEST_size_t_eq(rec_len, 5) +- || !TEST_true(ossl_quic_rstream_release_record(rstream, 0))) +- goto err; +- +- /* the remaining bytes are intact and still readable */ +- if (!TEST_true(ossl_quic_rstream_read(rstream, buf, sizeof(buf), +- &readbytes, &fin)) +- || !TEST_size_t_eq(readbytes, 5) +- || !TEST_mem_eq(buf, 5, pdata + 5, 5)) +- goto err; +- +- if (!TEST_int_eq(ch->protocol_error, 0)) +- goto err; +- +- ret = 1; +- +-err: +- ossl_quic_rstream_free(rstream); +- ossl_quic_rstream_qparm_destroy(rsqp); +- pkt_test_free(pkt); +- ossl_quic_channel_free(ch); +- return ret; +-} +- + #define FILL_PATTERN "abcdefghijklmnopqrstuvwxyz0123456789" \ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + diff -Nru openssl-3.5.7/debian/patches/test-cover-zero-length-read-of-a-QUIC-rstream.patch openssl-3.5.7/debian/patches/test-cover-zero-length-read-of-a-QUIC-rstream.patch --- openssl-3.5.7/debian/patches/test-cover-zero-length-read-of-a-QUIC-rstream.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/test-cover-zero-length-read-of-a-QUIC-rstream.patch 2026-09-29 20:10:12.000000000 +0000 @@ -0,0 +1,106 @@ +From: Jakub Zelenka +Date: Mon, 7 Sep 2026 15:29:04 +0200 +Subject: test: cover zero length read of a QUIC rstream + +A zero length read has been a successful no-op returning zero read +bytes, and releasing a record without consuming any bytes succeeded +likewise. The new ossl_sframe_set_move_offset() refuses to keep the +offset in place, so both operations now fail once at least one byte +has been consumed, and quic_read_actual() turns the failed read into +a fatal SSL error for SSL_read_ex() with a zero length buffer. + +Assisted-by: Claude:claude-fable-5 +Reviewed-by: Tomas Mraz +Reviewed-by: Milan Broz +Merge-date: Sat Sep 26 11:53:44 2026 +Merged-from: https://github.com/openssl/openssl/pull/32769 +--- + test/quic_stream_test.c | 75 +++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 75 insertions(+) + +diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c +index 7e01dffc7b65..da322249ef1d 100644 +--- a/test/quic_stream_test.c ++++ b/test/quic_stream_test.c +@@ -1257,6 +1257,81 @@ static int test_rstream_fin_final_size(void) + return ret; + } + ++/* ++ * A zero length read is a successful no-op returning zero read bytes, ++ * and releasing a record without consuming any bytes succeeds likewise. ++ * Neither may fail once at least one byte has been consumed, otherwise ++ * quic_read_actual() turns the failed read into a fatal SSL error for ++ * SSL_read_ex() called with a zero length buffer. ++ */ ++static int test_rstream_zero_length_read(void) ++{ ++ QUIC_RSTREAM *rstream = NULL; ++ QUIC_CHANNEL *ch = NULL; ++ QUIC_RSTREAM_QPARM *rsqp = NULL; ++ OSSL_QRX_PKT *pkt = NULL; ++ unsigned char pdata[10], buf[10]; ++ const unsigned char *record = NULL; ++ size_t readbytes = 0, rec_len = 0, i; ++ int fin = 0; ++ int ret = 0; ++ ++ for (i = 0; i < sizeof(pdata); ++i) ++ pdata[i] = (unsigned char)(0x40 + i); ++ ++ if (!TEST_ptr(pkt = pkt_test_new(1200)) ++ || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))) ++ || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp))) ++ goto err; ++ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt, 0, ++ pdata, sizeof(pdata), 0))) ++ goto err; ++ ++ /* a zero length read before anything is consumed */ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 0)) ++ goto err; ++ ++ /* consume some bytes so the stream offset is not zero */ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 5, &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 5) ++ || !TEST_mem_eq(buf, 5, pdata, 5)) ++ goto err; ++ ++ /* a zero length read with data pending at a nonzero offset */ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 0)) ++ goto err; ++ ++ /* releasing a record without consuming anything succeeds too */ ++ if (!TEST_true(ossl_quic_rstream_get_record(rstream, &record, &rec_len, ++ &fin)) ++ || !TEST_size_t_eq(rec_len, 5) ++ || !TEST_true(ossl_quic_rstream_release_record(rstream, 0))) ++ goto err; ++ ++ /* the remaining bytes are intact and still readable */ ++ if (!TEST_true(ossl_quic_rstream_read(rstream, buf, sizeof(buf), ++ &readbytes, &fin)) ++ || !TEST_size_t_eq(readbytes, 5) ++ || !TEST_mem_eq(buf, 5, pdata + 5, 5)) ++ goto err; ++ ++ if (!TEST_int_eq(ch->protocol_error, 0)) ++ goto err; ++ ++ ret = 1; ++ ++err: ++ ossl_quic_rstream_free(rstream); ++ ossl_quic_rstream_qparm_destroy(rsqp); ++ pkt_test_free(pkt); ++ ossl_quic_channel_free(ch); ++ return ret; ++} ++ + #define FILL_PATTERN "abcdefghijklmnopqrstuvwxyz0123456789" \ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + diff -Nru openssl-3.5.7/debian/patches/test-reassemble-small-out-of-order-frames-and-check-the-b.patch openssl-3.5.7/debian/patches/test-reassemble-small-out-of-order-frames-and-check-the-b.patch --- openssl-3.5.7/debian/patches/test-reassemble-small-out-of-order-frames-and-check-the-b.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/test-reassemble-small-out-of-order-frames-and-check-the-b.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,161 @@ +From: Jakub Zelenka +Date: Wed, 26 Aug 2026 17:42:43 +0200 +Subject: test: reassemble small out of order frames and check the bytes +MIME-Version: 1.0 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 8bit + +Deliver a buffer as small packet backed frames in a random order with +overlapping retransmits, each carrying its own copy of its bytes, then +read it back and compare. The random order drives insertion at the head, +the tail and the middle of the reassembly, and the frame size is swept +across the boundary where a design may change how it stores a chunk, so +short frames and their overlaps are exercised in every combination with +and without cleanse. It passes on the current implementation and would +catch a reassembly that mishandles any of those. + +Assisted-by: Claude:claude-opus-4-8 + +Reviewed-by: Tomas Mraz +Reviewed-by: Saša Nedvědický +MergeDate: Fri Aug 28 14:03:49 2026 +Reviewed-by: Milan Broz +Merged-from: https://github.com/openssl/openssl/pull/32769 +--- + test/quic_stream_test.c | 117 ++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 117 insertions(+) + +diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c +index 2e6cadda35ba..c17fe934901a 100644 +--- a/test/quic_stream_test.c ++++ b/test/quic_stream_test.c +@@ -858,6 +858,122 @@ static int test_rstream_pkt_overhead(void) + return ret; + } + ++/* ++ * Reassemble a buffer delivered as small frames in a random order with ++ * overlapping retransmits, each frame carrying its own copy of its bytes on ++ * its own packet as a real one would. The random order drives insertion at the ++ * head, the tail and the middle of the reassembly, and the frame size is swept ++ * across the boundary where a design may switch how it stores a chunk, so short ++ * frames and their overlaps are merged in every combination. The read back must ++ * match what was sent whether or not the data is cleansed, since each frame's ++ * own copy is what gets wiped, never the reference. ++ */ ++static int test_rstream_reorder(int idx) ++{ ++ unsigned char *data = NULL, *buf = NULL, *arena = NULL, *ap; ++ QUIC_RSTREAM *rstream = NULL; ++ OSSL_QRX_PKT **pkts = NULL; ++ const size_t data_size = 4096; ++ const size_t framesz = 1 + (size_t)(idx % 17); ++ const int cleanse = (idx & 1); ++ const size_t nframes = (data_size + framesz - 1) / framesz; ++ size_t *order = NULL; ++ size_t i, num_pkts = 0, got = 0, readbytes = 0; ++ int fin = 0, ret = 0; ++ ++ if (!TEST_ptr(data = OPENSSL_malloc(data_size)) ++ || !TEST_ptr(buf = OPENSSL_malloc(data_size)) ++ || !TEST_ptr(arena = OPENSSL_malloc(3 * data_size)) ++ || !TEST_ptr(order = OPENSSL_malloc(nframes * sizeof(*order))) ++ || !TEST_ptr(pkts = OPENSSL_zalloc(2 * nframes * sizeof(*pkts))) ++ || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, 0))) ++ goto err; ++ ++ if (cleanse) ++ ossl_quic_rstream_set_cleanse(rstream, 1); ++ ++ for (i = 0; i < data_size; ++i) ++ data[i] = (unsigned char)(test_random() & 0xFF); ++ ++ for (i = 0; i < nframes; ++i) ++ order[i] = i; ++ for (i = nframes; i > 1; --i) { ++ size_t j = (size_t)(test_random() % i); ++ size_t tmp = order[i - 1]; ++ ++ order[i - 1] = order[j]; ++ order[j] = tmp; ++ } ++ ++ ap = arena; ++ for (i = 0; i < nframes; ++i) { ++ size_t off = order[i] * framesz; ++ size_t size = off + framesz > data_size ? data_size - off : framesz; ++ OSSL_QRX_PKT *pkt; ++ ++ if (!TEST_ptr(pkt = pkt_test_new(1200))) ++ goto err; ++ pkts[num_pkts++] = pkt; ++ memcpy(ap, data + off, size); ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt, off, ap, ++ size, 0))) ++ goto err; ++ ap += size; ++ ++ /* an overlapping retransmit straddling this frame and the next */ ++ if (off + framesz + framesz / 2 <= data_size ++ && test_random() % 3 == 0) { ++ size_t roff = off + framesz / 2; ++ OSSL_QRX_PKT *rpkt; ++ ++ if (!TEST_ptr(rpkt = pkt_test_new(1200))) ++ goto err; ++ pkts[num_pkts++] = rpkt; ++ memcpy(ap, data + roff, framesz); ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, rpkt, roff, ap, ++ framesz, 0))) ++ goto err; ++ ap += framesz; ++ } ++ } ++ ++ /* final empty fin frame past the last byte */ ++ if (!TEST_true(ossl_quic_rstream_queue_data(rstream, NULL, data_size, NULL, ++ 0, 1))) ++ goto err; ++ ++ while (got < data_size) { ++ if (!TEST_true(ossl_quic_rstream_read(rstream, buf + got, ++ data_size - got, &readbytes, &fin))) ++ goto err; ++ if (readbytes == 0) ++ break; ++ got += readbytes; ++ } ++ ++ if (!TEST_size_t_eq(got, data_size) ++ || !TEST_mem_eq(buf, got, data, data_size)) ++ goto err; ++ ++ ret = 1; ++ ++err: ++ ossl_quic_rstream_free(rstream); ++ if (pkts != NULL) { ++ for (i = 0; i < num_pkts; ++i) { ++ if (!TEST_size_t_eq(pkt_test_refcount(pkts[i]), 1)) ++ ret = 0; ++ pkt_test_free(pkts[i]); ++ } ++ OPENSSL_free(pkts); ++ } ++ OPENSSL_free(order); ++ OPENSSL_free(arena); ++ OPENSSL_free(data); ++ OPENSSL_free(buf); ++ return ret; ++} ++ + int setup_tests(void) + { + ADD_TEST(test_sstream_simple); +@@ -866,5 +982,6 @@ int setup_tests(void) + ADD_ALL_TESTS(test_rstream_random, 100); + ADD_TEST(test_rstream_pkt); + ADD_TEST(test_rstream_pkt_overhead); ++ ADD_ALL_TESTS(test_rstream_reorder, 40); + return 1; + } diff -Nru openssl-3.5.7/debian/patches/test-verifies-the-connection-level-RX-flow-control.patch openssl-3.5.7/debian/patches/test-verifies-the-connection-level-RX-flow-control.patch --- openssl-3.5.7/debian/patches/test-verifies-the-connection-level-RX-flow-control.patch 1970-01-01 00:00:00.000000000 +0000 +++ openssl-3.5.7/debian/patches/test-verifies-the-connection-level-RX-flow-control.patch 2026-09-29 20:10:11.000000000 +0000 @@ -0,0 +1,45 @@ +From: Alexandr Nedvedicky +Date: Fri, 4 Sep 2026 01:24:34 +0200 +Subject: test verifies the connection level RX flow control window is + enforced. + +--- + test/quic_fc_test.c | 23 ++++++++++++++++++++++- + 1 file changed, 22 insertions(+), 1 deletion(-) + +diff --git a/test/quic_fc_test.c b/test/quic_fc_test.c +index 52351e5ffe11..876609486323 100644 +--- a/test/quic_fc_test.c ++++ b/test/quic_fc_test.c +@@ -467,9 +467,30 @@ static const struct rx_test_op rx_script_2[] = { + RX_OP_END + }; + ++/* ++ * The test verifies connection window is still enforced. ++ * The stream frame fits to stream control window, but exceeds ++ * connection flow control window. ++ */ ++static const struct rx_test_op rx_script_3[] = { ++ RX_OP_STEP_TIME(1000 * OSSL_TIME_MS) ++ RX_OP_INIT_CONN(INIT_WINDOW_SIZE, 10 * INIT_WINDOW_SIZE) ++ RX_OP_INIT_STREAM(0, INIT_S_WINDOW_SIZE, 30 * INIT_S_WINDOW_SIZE) ++ RX_OP_INIT_STREAM(1, INIT_S_WINDOW_SIZE, 30 * INIT_S_WINDOW_SIZE) ++ RX_OP_INIT_STREAM(2, INIT_S_WINDOW_SIZE, 30 * INIT_S_WINDOW_SIZE) ++ RX_OP_RX(0, INIT_S_WINDOW_SIZE, 0) ++ RX_OP_CHECK_ERROR_STREAM(0, OSSL_QUIC_ERR_NO_ERROR, 0) ++ RX_OP_RX(1, INIT_S_WINDOW_SIZE, 0) ++ RX_OP_CHECK_ERROR_STREAM(1, OSSL_QUIC_ERR_NO_ERROR, 0) ++ RX_OP_RX(2, INIT_S_WINDOW_SIZE, 0) ++ RX_OP_CHECK_ERROR_STREAM(2, OSSL_QUIC_ERR_FLOW_CONTROL_ERROR, 0) ++ RX_OP_END ++}; ++ + static const struct rx_test_op *rx_scripts[] = { + rx_script_1, +- rx_script_2 ++ rx_script_2, ++ rx_script_3 + }; + + static int run_rxfc_script(const struct rx_test_op *script)