[PATCH 1/7] cipher: fix spurious AEAD byte-counter carry for 4 GiB adds

Jussi Kivilinna jussi.kivilinna at iki.fi
Fri Jul 24 20:48:00 CEST 2026


* cipher/cipher-internal.h (cipher_bytecounter_add): New.
* cipher/cipher-gcm.c (gcm_bytecounter_add): Use shared helper.
* cipher/cipher-gcm-siv.c (gcm_siv_bytecounter_add): Likewise.
* cipher/cipher-poly1305.c (poly1305_bytecounter_add): Likewise.
* tests/Makefile.am (tests_bin): Add 't-cipher-internal'.
(t_cipher_internal_CPPFLAGS): New.
* tests/t-cipher-internal.c: New test.
--

Byte-counter helpers added high half of size_t addend, then detected
low-word carry by comparing updated low word against full 64-bit
addend. For single add of 4 GiB or more this comparison always
caused carry and incremented high word second time. Adding 4 GiB to
zero counter gave high word 2 instead of 1.

Detect carry against truncated low addend instead and use single shared
helper for all three modes. New test checks arithmetic across 32/64-bit
boundaries.

Reported-by: JEAN Jeremy <Jeremy.Jean at ssi.gouv.fr>
Signed-off-by: Jussi Kivilinna <jussi.kivilinna at iki.fi>
---
 cipher/cipher-gcm-siv.c   |  11 +--
 cipher/cipher-gcm.c       |  11 +--
 cipher/cipher-internal.h  |  24 ++++++
 cipher/cipher-poly1305.c  |  17 +----
 tests/Makefile.am         |   4 +-
 tests/t-cipher-internal.c | 157 ++++++++++++++++++++++++++++++++++++++
 6 files changed, 187 insertions(+), 37 deletions(-)
 create mode 100644 tests/t-cipher-internal.c

diff --git a/cipher/cipher-gcm-siv.c b/cipher/cipher-gcm-siv.c
index b92ae45d..6e0ce347 100644
--- a/cipher/cipher-gcm-siv.c
+++ b/cipher/cipher-gcm-siv.c
@@ -50,16 +50,7 @@ mulx_ghash (byte *a)
 static inline void
 gcm_siv_bytecounter_add (u32 ctr[2], size_t add)
 {
-  if (sizeof(add) > sizeof(u32))
-    {
-      u32 high_add = ((add >> 31) >> 1) & 0xffffffff;
-      ctr[1] += high_add;
-    }
-
-  ctr[0] += add;
-  if (ctr[0] >= add)
-    return;
-  ++ctr[1];
+  cipher_bytecounter_add (ctr, add);
 }
 
 
diff --git a/cipher/cipher-gcm.c b/cipher/cipher-gcm.c
index 1627cd1c..c04683f8 100644
--- a/cipher/cipher-gcm.c
+++ b/cipher/cipher-gcm.c
@@ -692,16 +692,7 @@ setupM (gcry_cipher_hd_t c)
 static inline void
 gcm_bytecounter_add (u32 ctr[2], size_t add)
 {
-  if (sizeof(add) > sizeof(u32))
-    {
-      u32 high_add = ((add >> 31) >> 1) & 0xffffffff;
-      ctr[1] += high_add;
-    }
-
-  ctr[0] += add;
-  if (ctr[0] >= add)
-    return;
-  ++ctr[1];
+  cipher_bytecounter_add (ctr, add);
 }
 
 
diff --git a/cipher/cipher-internal.h b/cipher/cipher-internal.h
index eabb7ea9..8e6cbffc 100644
--- a/cipher/cipher-internal.h
+++ b/cipher/cipher-internal.h
@@ -808,6 +808,30 @@ static inline unsigned int _gcry_blocksize_shift(gcry_cipher_hd_t c)
 }
 
 
+/* Add 32-bit or 64-bit size_t to 2x32-bit byte counter. */
+static inline int
+cipher_bytecounter_add (u32 ctr[2], size_t add)
+{
+  int overflow = 0;
+  u32 low_add = add;
+
+  if (sizeof(add) > sizeof(u32))
+    {
+      u32 high_add = ((add >> 31) >> 1) & 0xffffffff;
+      ctr[1] += high_add;
+      if (ctr[1] < high_add)
+        overflow = 1;
+    }
+
+  ctr[0] += low_add;
+  if (ctr[0] >= low_add)
+    return overflow;
+
+  ctr[1] += 1;
+  return (ctr[1] < 1) || overflow;
+}
+
+
 /* Optimized function for adding value to cipher block. */
 static inline void
 cipher_block_add(void *_dstsrc, unsigned int add, size_t blocksize)
diff --git a/cipher/cipher-poly1305.c b/cipher/cipher-poly1305.c
index c76dd9a4..c7441971 100644
--- a/cipher/cipher-poly1305.c
+++ b/cipher/cipher-poly1305.c
@@ -33,22 +33,7 @@
 static inline int
 poly1305_bytecounter_add (u32 ctr[2], size_t add)
 {
-  int overflow = 0;
-
-  if (sizeof(add) > sizeof(u32))
-    {
-      u32 high_add = ((add >> 31) >> 1) & 0xffffffff;
-      ctr[1] += high_add;
-      if (ctr[1] < high_add)
-        overflow = 1;
-    }
-
-  ctr[0] += add;
-  if (ctr[0] >= add)
-    return overflow;
-
-  ctr[1] += 1;
-  return (ctr[1] < 1) || overflow;
+  return cipher_bytecounter_add (ctr, add);
 }
 
 
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 9392dd36..8fcde4fc 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -25,7 +25,8 @@ tests_bin = \
         version t-secmem mpitests t-sexp t-convert \
 	t-mpi-bit t-mpi-point t-lock \
 	prime basic keygen pubkey hmac hashtest t-kdf keygrip \
-	aeswrap random t-kem t-thread-local t-fips-service-ind
+	aeswrap random t-kem t-thread-local t-fips-service-ind \
+	t-cipher-internal
 
 if USE_RSA
 tests_bin += pkcs1v2 t-rsa-pss t-rsa-15 t-rsa-testparm
@@ -104,6 +105,7 @@ t_lock_CFLAGS = $(GPG_ERROR_MT_CFLAGS)
 t_thread_local_LDADD = $(standard_ldadd) $(GPG_ERROR_MT_LIBS) @LDADD_FOR_TESTS_KLUDGE@
 t_thread_local_CFLAGS = $(GPG_ERROR_MT_CFLAGS)
 testdrv_LDADD = $(LDADD_FOR_TESTS_KLUDGE)
+t_cipher_internal_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/cipher
 
 # Build a version of the test driver for the build platform.
 testdrv-build: testdrv.c
diff --git a/tests/t-cipher-internal.c b/tests/t-cipher-internal.c
new file mode 100644
index 00000000..6a9cdc7d
--- /dev/null
+++ b/tests/t-cipher-internal.c
@@ -0,0 +1,157 @@
+/* t-cipher-internal.c - Regression tests for cipher-internal.h helpers
+ * Copyright (C) 2026 Jussi Kivilinna <jussi.kivilinna at iki.fi>
+ *
+ * This file is part of Libgcrypt.
+ *
+ * Libgcrypt is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * Libgcrypt is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "../src/g10lib.h"
+#include "../cipher/cipher-internal.h"
+
+#define PGM "t-cipher-internal"
+
+static int verbose;
+static int debug;
+static int error_count;
+
+static void
+print_hilo (const char *label, u64 v)
+{
+  fprintf (stderr, "%s=%08lx%08lx", label,
+           (unsigned long)(u32)(v >> 32), (unsigned long)(u32)v);
+}
+
+static void
+check_bytecounter_add (u64 start, u64 add)
+{
+  u32 ctr[2];
+  u64 want = start + add;
+  int want_ovf = add > (~(u64)0 - start);
+  int got_ovf;
+
+  ctr[0] = (u32)start;
+  ctr[1] = (u32)(start >> 32);
+
+  got_ovf = cipher_bytecounter_add (ctr, (size_t)add);
+
+  if ((((u64)ctr[1] << 32) | ctr[0]) != want || !!got_ovf != !!want_ovf)
+    {
+      error_count++;
+      print_hilo ("FAIL: start", start);
+      print_hilo (" add", add);
+      print_hilo (" -> got", ((u64)ctr[1] << 32) | ctr[0]);
+      fprintf (stderr, " ovf=%d,", !!got_ovf);
+      print_hilo (" want", want);
+      fprintf (stderr, " ovf=%d\n", !!want_ovf);
+    }
+  else if (debug)
+    {
+      print_hilo ("ok: start", start);
+      print_hilo (" add", add);
+      print_hilo (" -> ", want);
+      fprintf (stderr, " ovf=%d\n", !!want_ovf);
+    }
+}
+
+static void
+test_bytecounter_add (void)
+{
+  static const u64 starts[] = {
+    U64_C (0x0000000000000000), U64_C (0x0000000000000001),
+    U64_C (0x0000000090000000), U64_C (0x00000000ffffffff),
+    U64_C (0x00000001ffffffff), U64_C (0xfffffffe00000000),
+    U64_C (0x123456789abcdef0)
+  };
+  static const u64 adds[] = {
+    U64_C (0), U64_C (1), U64_C (15), U64_C (16),
+    U64_C (0x000000007fffffff), U64_C (0x0000000080000000),
+    U64_C (0x00000000ffffffff), U64_C (0x0000000100000000),
+    U64_C (0x0000000100000010), U64_C (0x0000000180000000),
+    U64_C (0x0000001000000000), U64_C (0x000000fffffffff0),
+    U64_C (0xffffffffffffffff)
+  };
+  unsigned int s, a;
+
+  if (verbose)
+    fprintf (stderr, "  checking cipher_bytecounter_add\n");
+
+  for (s = 0; s < DIM (starts); s++)
+    for (a = 0; a < DIM (adds); a++)
+      {
+        if (sizeof (size_t) < 8 && adds[a] > U64_C (0xffffffff))
+          continue;
+        check_bytecounter_add (starts[s], adds[a]);
+      }
+
+  if (sizeof (size_t) > 4)
+    {
+      u32 ctr[2] = { 0, 0 };
+      cipher_bytecounter_add (ctr, (size_t)U64_C (0x100000000));
+      if (ctr[0] != 0 || ctr[1] != 1)
+        {
+          error_count++;
+          fprintf (stderr, "FAIL: 4 GiB add gave %08lx:%08lx,"
+                   " expected 00000001:00000000\n",
+                   (unsigned long)ctr[1], (unsigned long)ctr[0]);
+        }
+    }
+}
+
+int
+main (int argc, char **argv)
+{
+  int last_argc = -1;
+
+  if (argc)
+    { argc--; argv++; }
+
+  while (argc && last_argc != argc)
+    {
+      last_argc = argc;
+      if (!strcmp (*argv, "--"))
+        {
+          argc--; argv++;
+          break;
+        }
+      else if (!strcmp (*argv, "--verbose"))
+        {
+          verbose++;
+          argc--; argv++;
+        }
+      else if (!strcmp (*argv, "--debug"))
+        {
+          verbose = debug = 1;
+          argc--; argv++;
+        }
+    }
+
+  if (verbose)
+    fprintf (stderr, "Starting cipher-internal checks.\n");
+
+  test_bytecounter_add ();
+
+  if (error_count)
+    fprintf (stderr, PGM ": %d test(s) failed\n", error_count);
+
+  return !!error_count;
+}
-- 
2.53.0




More information about the Gcrypt-devel mailing list