Commit 5b445cd96a for openssl.org

commit 5b445cd96a3de836c28105d1583f62472e8cc7e6
Author: Viktor Dukhovni <viktor@openssl.org>
Date:   Sun Jul 12 14:21:35 2026 +1000

    Fix removal of sole key exchange group in tuple

    - Don't attempt to float its keyshare if any
    - Include active tuple in memmove() that excises a newly empty closed tuple.

    Add comments to clarify the logic.

    Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
    Reviewed-by: Milan Broz <mbroz@openssl.org>
    Reviewed-by: Norbert Pocs <norbertp@openssl.org>
    MergeDate: Mon Jul 20 07:12:30 2026
    (Merged from https://github.com/openssl/openssl/pull/31926)

diff --git a/ssl/t1_lib.c b/ssl/t1_lib.c
index 5d0276a5a1..f8056c82da 100644
--- a/ssl/t1_lib.c
+++ b/ssl/t1_lib.c
@@ -1255,6 +1255,17 @@ static const char prefixes[] = { TUPLE_DELIMITER_CHARACTER,
  * Those callback functions are (indirectly) called by CONF_parse_list with
  * different separators (nominally ':' or '/'), a variable based on gid_cb_st
  * is used to keep track of the parsing results between the various calls
+ *
+ * Bookkeeping invariants maintained throughout parsing (see gid_cb_st below):
+ *  - gid_arr[0..gidcnt) is the flat list of groups, partitioned into tuples in
+ *    order: tuple t occupies a contiguous run of tuplcnt_arr[t] entries.
+ *  - The per-tuple counts therefore sum to the group count:
+ *        sum(tuplcnt_arr[0..tplcnt]) == gidcnt
+ *    (indices 0..tplcnt-1 are closed tuples, index tplcnt is the active one).
+ *  - ksid_arr[0..ksidcnt) holds keyshare group IDs; each is one of the groups
+ *    in gid_arr and they appear in the same relative order as their groups.
+ * Every add/remove path must preserve these; an OOB read in the remove path
+ * (GitHub #31315) was a symptom of the first invariant being violated.
  */

 typedef struct {
@@ -1542,9 +1553,16 @@ static int gid_cb(const char *elem, int len, void *arg)
              * Otherwise, iterate through the tuple check whether any keyshares
              * remain *after* the index of the group we're removing.  The first
              * of these, if any, is at index `k+1` in the keyshare list, which
-             * is the only slow we need to check.
+             * is the only slot we need to check.
+             *
+             * If the removal emptied the tuple (tuplcnt_arr[j] == 0 after the
+             * decrement above) there is no remaining group to float onto:
+             * gid_arr[tpl_start_idx] would now name a group belonging to the
+             * next tuple (or be past gid_arr entirely).  Drop the keyshare in
+             * that case too.
              */
-            drop_ks = ks_check_idx > tpl_start_idx || j >= garg->tplcnt;
+            drop_ks = ks_check_idx > tpl_start_idx || j >= garg->tplcnt
+                || garg->tuplcnt_arr[j] == 0;

             if (!drop_ks) {
                 size_t end; /* End index of affected tuple */
@@ -1573,11 +1591,19 @@ static int gid_cb(const char *elem, int len, void *arg)
          * Adjust closed or current tuple's group count, if a closed tuple
          * count reaches zero excise the resulting empty tuple.  The current
          * (not yet closed) tuple at the end of the list stays even if empty.
+         *
+         * The active tuple lives at index tplcnt, so the slots in use are
+         * tuplcnt_arr[0..tplcnt] (tplcnt + 1 entries).  Excising closed tuple
+         * j must therefore shift the closed tuples j+1..tplcnt-1 *and* the
+         * active tuple at index tplcnt down by one, i.e. (tplcnt - j) entries
+         * counted with the pre-decrement tplcnt.  Decrement tplcnt only after
+         * the move so the active-tuple slot is not left behind (which would
+         * inflate the per-tuple counts and desynchronise them from gid_arr).
          */
         if (garg->tuplcnt_arr[j] == 0 && j < garg->tplcnt) {
-            garg->tplcnt--;
             memmove(garg->tuplcnt_arr + j, garg->tuplcnt_arr + j + 1,
                 (garg->tplcnt - j) * sizeof(size_t));
+            garg->tplcnt--;
         }
     } else { /* Processing addition of a single new group */