Commit 039fa4958e for openssl.org

commit 039fa4958efc7acee16e9896103cbfdd249a10df
Author: Daniel Kubec <kubec@openssl.foundation>
Date:   Sat Oct 31 15:45:37 2026 +0100

    QUIC: mix the hashes of the SRTM, LCIDM and stream map tables

    OPENSSL_LHASH takes a bucket as the hash modulo a power of two
    (getrn() in crypto/lhash/lhash.c), without mixing, so a hash callback
    must spread its low bits itself. Three QUIC tables did not:

    - quic_srtm.c items_fwd_hash() and quic_lcidm.c lcidm_conn_hash()
      returned the connection's opaque pointer as is. A pointer's low bits
      are its allocation alignment, and QUIC_CHANNEL objects of one size come
      from the allocator at a fixed stride (1728 bytes for the 1720-byte
      channel of this tree, 64 low zero bits of six), so the connections of a
      port shared one bucket in 64 and every add, remove, retire and cull
      walked a chain that grew with the connection count: 4x the cost of a
      spread hash at 4096 connections, and worse at larger strides (a page
      stride puts every connection in one bucket).

    - quic_stream_map.c hash_stream() returned the stream ID as is. The low
      two bits of an ID are its initiator and direction, so a map holding
      mostly one kind of stream (an HTTP/3 connection's request streams) used
      a quarter of its buckets and its chains were four times as long.

    Mix the pointers with the splitmix64 finaliser, added to
    internal/hashfunc.h as ossl_hash_mix64() with LHASH-typed wrappers
    ossl_lh_hash_u64() and ossl_lh_hash_ptr(), and fold a stream ID's
    sequence bits into its type bits with id ^ (id >> 2): a bijection on each
    block of 16 IDs, so a dense sequence stays dense whether it comes from
    one stream type or from all four, which a general mix would spread at
    random and cost on the interleaved pattern.

      measure (ns/op)             before     after

      SRTM add, 1728 B stride     1066       272
      SRTM add, 4096 B stride     15551      271
      LCIDM enrol, 1728 B         1440       666
      LCIDM enrol, 4096 B         18461      664

    After the change the SRTM and LCIDM costs no longer depend on the
    pointer stride, and the stream map no longer depends on which stream
    types a connection uses.

    Co-authored-by: Mounir IDRASSI <mounir.idrassi@amcrypto.jp>
    Assisted-by: Claude:claude-fable-5-1
    Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
    Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
    Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
    Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org>
    Merge-date: Tue Nov  3 13:12:11 2026
    Merged-from: https://github.com/openssl/openssl/pull/33466

diff --git a/include/internal/hashfunc.h b/include/internal/hashfunc.h
index 78d7c859d5..a8f66cf8e7 100644
--- a/include/internal/hashfunc.h
+++ b/include/internal/hashfunc.h
@@ -12,9 +12,47 @@

 #include <openssl/e_os2.h>
 #include <stddef.h>
+#include <limits.h>
+
 /**
  * Generalized fnv1a 64 bit hash function
  */
 ossl_unused uint64_t ossl_fnv1a_hash(uint8_t *key, size_t len);

+/*
+ * Mix a 64-bit value so that every bit of the result depends on every bit
+ * of the input (the splitmix64 finaliser). An LHASH bucket is the hash
+ * modulo a power of two, so a hash that is a raw integer or pointer must be
+ * mixed first: a pointer's low bits are its allocation alignment, and
+ * objects of one size from the allocator sit at a fixed stride, so without
+ * mixing they share a handful of buckets and the chains grow with the
+ * number of objects. Likewise an integer whose low bits are a type tag.
+ */
+static ossl_unused ossl_inline uint64_t ossl_hash_mix64(uint64_t x)
+{
+    x ^= x >> 30;
+    x *= 0xbf58476d1ce4e5b9ULL;
+    x ^= x >> 27;
+    x *= 0x94d049bb133111ebULL;
+    x ^= x >> 31;
+    return x;
+}
+
+/* A 64-bit value, mixed, as an LHASH hash value. */
+static ossl_unused ossl_inline unsigned long ossl_lh_hash_u64(uint64_t x)
+{
+    x = ossl_hash_mix64(x);
+#if ULONG_MAX > 0xFFFFFFFFUL
+    return (unsigned long)x;
+#else
+    return (unsigned long)(x ^ (x >> 32));
+#endif
+}
+
+/* A pointer's identity, mixed, as an LHASH hash value. */
+static ossl_unused ossl_inline unsigned long ossl_lh_hash_ptr(const void *p)
+{
+    return ossl_lh_hash_u64((uint64_t)(uintptr_t)p);
+}
+
 #endif
diff --git a/ssl/quic/quic_lcidm.c b/ssl/quic/quic_lcidm.c
index 89ee95055d..e56a93dffc 100644
--- a/ssl/quic/quic_lcidm.c
+++ b/ssl/quic/quic_lcidm.c
@@ -11,6 +11,7 @@
 #include "internal/quic_types.h"
 #include "internal/quic_vlint.h"
 #include "internal/common.h"
+#include "internal/hashfunc.h"
 #include "crypto/siphash.h"
 #include <openssl/lhash.h>
 #include <openssl/rand.h>
@@ -100,7 +101,7 @@ static int lcid_comp(const QUIC_LCID *a, const QUIC_LCID *b)

 static unsigned long lcidm_conn_hash(const QUIC_LCIDM_CONN *conn)
 {
-    return (unsigned long)(uintptr_t)conn->opaque;
+    return ossl_lh_hash_ptr(conn->opaque);
 }

 static int lcidm_conn_comp(const QUIC_LCIDM_CONN *a, const QUIC_LCIDM_CONN *b)
diff --git a/ssl/quic/quic_srtm.c b/ssl/quic/quic_srtm.c
index e17433a6aa..02a67e3de4 100644
--- a/ssl/quic/quic_srtm.c
+++ b/ssl/quic/quic_srtm.c
@@ -9,6 +9,7 @@

 #include "internal/quic_srtm.h"
 #include "internal/common.h"
+#include "internal/hashfunc.h"
 #include <openssl/lhash.h>
 #include <openssl/core_names.h>
 #include <openssl/rand.h>
@@ -68,7 +69,7 @@ struct quic_srtm_st {

 static unsigned long items_fwd_hash(const SRTM_ITEM *item)
 {
-    return (unsigned long)(uintptr_t)item->opaque;
+    return ossl_lh_hash_ptr(item->opaque);
 }

 static int items_fwd_cmp(const SRTM_ITEM *a, const SRTM_ITEM *b)
diff --git a/ssl/quic/quic_stream_map.c b/ssl/quic/quic_stream_map.c
index 2d58d08a80..832b3ae996 100644
--- a/ssl/quic/quic_stream_map.c
+++ b/ssl/quic/quic_stream_map.c
@@ -74,7 +74,14 @@ static QUIC_STREAM *list_next(QUIC_STREAM_LIST_NODE *l, QUIC_STREAM_LIST_NODE *n

 static unsigned long hash_stream(const QUIC_STREAM *s)
 {
-    return (unsigned long)s->id;
+    /*
+     * The low two bits of a stream ID identify its initiator and direction.
+     * Folding in bits from the stream ordinal improves bucket use for
+     * consecutive IDs of one type. With mixed stream types, this can produce
+     * longer chains than the original hash at some intermediate table sizes
+     * while LHASH expands.
+     */
+    return (unsigned long)(s->id ^ (s->id >> 2));
 }

 static int cmp_stream(const QUIC_STREAM *a, const QUIC_STREAM *b)