Bug#993489: bullseye-pu: package cyrus-imapd/3.2.6-2+deb11u1
Package: release.debian.org
Severity: normal
Tags: bullseye
User: release.debian.org@packages.debian.org
Usertags: pu
[ Reason ]
cyrus-imapd before 3.2.8 allows remote attackers to cause a denial of
service (multiple-minute daemon hang) via input that is mishandled
during hash-table interaction. Because there are many insertions into
a single bucket, strcmp becomes slow. This is fixed in 3.4.2, 3.2.8,
and 3.0.16.
[ Impact ]
Medium vulnerability
[ Tests ]
The new cunit/strhash.testc passed.
[ Risks ]
Low risk, patch is easy to read
[ Checklist ]
[X] *all* changes are documented in the d/changelog
[X] I reviewed all changes and I approve them
[X] attach debdiff against the package in (old)stable
[X] the issue is verified as fixed in unstable
[ Changes ]
New string hashing algorithm and test.
Cheers,
Yadd
diff --git a/debian/changelog b/debian/changelog
index c8259297..bd11af8d 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+cyrus-imapd (3.2.6-2+deb11u1) bullseye; urgency=medium
+
+ * Replace string hashing algorithm (Closes: #993433, CVE-2021-33582)
+
+ -- Yadd <yadd@debian.org> Wed, 01 Sep 2021 07:58:38 +0200
+
cyrus-imapd (3.2.6-2) unstable; urgency=medium
* Update gbp.conf for Bullseye branch
diff --git a/debian/control b/debian/control
index 3a4556b0..9b31670e 100644
--- a/debian/control
+++ b/debian/control
@@ -3,7 +3,7 @@ Maintainer: Debian Cyrus Team <team+cyrus@tracker.debian.org>
Uploaders: Henrique de Moraes Holschuh <hmh@debian.org>,
Ondřej Surý <ondrej@debian.org>,
Anthony Prades <toony.debian@chezouam.net>,
- Xavier Guimard <yadd@debian.org>
+ Yadd <yadd@debian.org>
Section: mail
Priority: optional
Build-Depends: bison,
diff --git a/debian/patches/CVE-2021-33582.patch b/debian/patches/CVE-2021-33582.patch
new file mode 100644
index 00000000..af48b338
--- /dev/null
+++ b/debian/patches/CVE-2021-33582.patch
@@ -0,0 +1,632 @@
+Description: Fixed CVE-2021-33582
+ Certain user inputs are used as hash table keys during processing. A
+ poorly chosen string hashing algorithm meant that the user could control
+ which bucket their data was stored in, allowing a malicious user to direct
+ many inputs to a single bucket. Each subsequent insertion to the same bucket
+ requires a strcmp of every other entry in it. At tens of thousands of
+ entries, each new insertion could keep the CPU busy in a strcmp loop for
+ minutes.
+ .
+ The string hashing algorithm has been replaced with a better one, and now
+ also uses a random seed per hash table, so malicious inputs cannot be
+ precomputed.
+ .
+ Discovered by Matthew Horsfall, Fastmail
+Author: ellie timoney <ellie@fastmail.com>
+Origin: upstream, https://github.com/cyrusimap/cyrus-imapd/compare/cyrus-imapd-3.2.7...cyrus-imapd-3.2.8
+Bug: https://security-tracker.debian.org/tracker/CVE-2021-33582
+Forwarded: not-needed
+Reviewed-By: Yadd <yadd@debian.org>
+Last-Update: 2021-09-01
+
+--- a/Makefile.am
++++ b/Makefile.am
+@@ -677,6 +677,7 @@
+ cunit/squat.testc \
+ cunit/strarray.testc \
+ cunit/strconcat.testc \
++ cunit/strhash.testc \
+ cunit/times.testc \
+ cunit/tok.testc \
+ cunit/vparse.testc
+--- a/configure.ac
++++ b/configure.ac
+@@ -191,7 +191,7 @@
+
+ AC_CHECK_HEADERS(unistd.h sys/select.h sys/param.h stdarg.h)
+ AC_REPLACE_FUNCS(memmove strcasecmp ftruncate strerror posix_fadvise strsep memmem memrchr)
+-AC_CHECK_FUNCS(strlcat strlcpy strnchr getgrouplist fmemopen pselect futimens futimes)
++AC_CHECK_FUNCS(strlcat strlcpy strnchr getgrouplist fmemopen pselect futimens futimes getline)
+ AC_HEADER_DIRENT
+
+ dnl check whether to use getpassphrase or getpass
+--- a/cunit/hash.testc
++++ b/cunit/hash.testc
+@@ -117,6 +117,9 @@
+ hash_enumerate(&ht, count_cb, &count);
+ CU_ASSERT_EQUAL(0, count);
+
++ /* check hash_numrecords */
++ CU_ASSERT_EQUAL(0, hash_numrecords(&ht));
++
+ /* free the hash table */
+ free_hash_table(&ht, NULL);
+ }
+@@ -146,6 +149,9 @@
+ hash_enumerate(&ht, count_cb, &count);
+ CU_ASSERT_EQUAL(1, count);
+
++ /* check hash_numrecords */
++ CU_ASSERT_EQUAL(1, hash_numrecords(&ht));
++
+ /* re-insert into the hash table */
+ d = hash_insert(KEY0, VALUE1, &ht);
+ /* get the old value back */
+@@ -160,6 +166,9 @@
+ hash_enumerate(&ht, count_cb, &count);
+ CU_ASSERT_EQUAL(1, count);
+
++ /* check hash_numrecords */
++ CU_ASSERT_EQUAL(1, hash_numrecords(&ht));
++
+ /* delete from the hash table */
+ d = hash_del(KEY0, &ht);
+ CU_ASSERT_PTR_EQUAL(VALUE1, d);
+@@ -173,6 +182,9 @@
+ hash_enumerate(&ht, count_cb, &count);
+ CU_ASSERT_EQUAL(0, count);
+
++ /* check hash_numrecords */
++ CU_ASSERT_EQUAL(0, hash_numrecords(&ht));
++
+ /* free the hash table */
+ free_hash_table(&ht, NULL);
+ }
+@@ -239,6 +251,9 @@
+ hash_enumerate(&ht, count_cb, &count);
+ CU_ASSERT_EQUAL(N, count);
+
++ /* check hash_numrecords */
++ CU_ASSERT_EQUAL(N, hash_numrecords(&ht));
++
+ /* delete from the hash table */
+ for (i = 0 ; i < N ; i++) {
+ d = hash_del(key(i), &ht);
+@@ -256,6 +271,9 @@
+ hash_enumerate(&ht, count_cb, &count);
+ CU_ASSERT_EQUAL(0, count);
+
++ /* check hash_numrecords */
++ CU_ASSERT_EQUAL(0, hash_numrecords(&ht));
++
+ /* free the hash table */
+ freed_count = 0;
+ free_hash_table(&ht, lincoln);
+@@ -286,6 +304,9 @@
+ hash_enumerate(&ht, count_cb, &count);
+ CU_ASSERT_EQUAL(N, count);
+
++ /* check hash_numrecords */
++ CU_ASSERT_EQUAL(N, hash_numrecords(&ht));
++
+ /* free the hash table */
+ freed_count = 0;
+ free_hash_table(&ht, lincoln);
+--- /dev/null
++++ b/cunit/strhash.testc
+@@ -0,0 +1,230 @@
++#include <config.h>
++
++#include <sys/types.h>
++#include <sys/stat.h>
++
++#include <errno.h>
++#include <stdio.h>
++#include <stdlib.h>
++#include <string.h>
++#include <unistd.h>
++
++#include "cunit/cyrunit.h"
++#include "lib/util.h"
++#include "lib/strhash.h"
++
++extern int verbose;
++
++static const char *repeat(int c, unsigned n)
++{
++ static char buf[1024];
++ char *p = buf;
++
++ /* always leave room for a \0 */
++ if (n >= sizeof(buf))
++ n = sizeof(buf) - 1;
++
++ memset(buf, 0, sizeof(buf));
++ while (n--) {
++ *p++ = c;
++ }
++
++ return buf;
++}
++
++static void test_repeated(void)
++{
++ /* repeated chars on the end should not obliterate earlier input */
++ unsigned suffix_lengths[] = { 15, 31, 63, 127, 255, 511, 1023 };
++ unsigned i;
++
++ for (i = 0; i < sizeof(suffix_lengths) / sizeof(suffix_lengths[0]); i++) {
++ char *cat = strconcat("cat", repeat('a', suffix_lengths[i]), NULL);
++ char *dog = strconcat("dog", repeat('a', suffix_lengths[i]), NULL);
++ char *mouse = strconcat("mouse", repeat('a', suffix_lengths[i]), NULL);
++
++ unsigned xcat = strhash(cat);
++ unsigned xdog = strhash(dog);
++ unsigned xmouse = strhash(mouse);
++
++ CU_ASSERT_NOT_EQUAL(xcat, xdog);
++ CU_ASSERT_NOT_EQUAL(xdog, xmouse);
++ CU_ASSERT_NOT_EQUAL(xmouse, xcat);
++
++ free(cat);
++ free(dog);
++ free(mouse);
++ }
++}
++
++static void test_seeded(void)
++{
++ const char *const words[] = { "lorem", "ipsum", "dolor", "sit", "amet" };
++ const size_t n_words = sizeof(words) / sizeof(words[0]);
++ unsigned hashes[n_words];
++ unsigned i, j;
++
++ memset(hashes, 0, sizeof(hashes));
++
++ /* with no seed, same input should produce same hash */
++ for (i = 0; i < n_words; i++) {
++ unsigned h1 = strhash(words[i]);
++ unsigned h2 = strhash(words[i]);
++ CU_ASSERT_EQUAL(h1, h2);
++ }
++
++ /* with explicit zero seed, same input should produce same hash */
++ for (i = 0; i < n_words; i++) {
++ unsigned h1 = strhash(words[i]);
++ unsigned h2 = strhash_seeded(0, words[i]);
++ unsigned h3 = strhash_seeded(0, words[i]);
++ CU_ASSERT_EQUAL(h1, h2);
++ CU_ASSERT_EQUAL(h2, h3);
++ CU_ASSERT_EQUAL(h3, h1);
++ }
++
++ /* with some seed, same input should produce same hash */
++ for (j = 0; j < 5; j++) {
++ uint32_t seed;
++ do {
++ seed = rand();
++ } while (seed == 0);
++
++ for (i = 0; i < n_words; i++) {
++ unsigned h1 = strhash_seeded(seed, words[i]);
++ unsigned h2 = strhash_seeded(seed, words[i]);
++ CU_ASSERT_EQUAL(h1, h2);
++ }
++ }
++
++ /* with different seed, same input should produce different hash */
++ for (i = 0; i < n_words; i++) {
++ uint32_t seed1, seed2;
++ do {
++ seed1 = rand();
++ seed2 = rand();
++ } while (seed1 == 0 || seed2 == 0 || seed1 == seed2);
++
++ unsigned h1 = strhash_seeded(seed1, words[i]);
++ unsigned h2 = strhash_seeded(seed2, words[i]);
++
++ CU_ASSERT_NOT_EQUAL(h1, h2);
++ }
++}
++
++/* We can't define-out an entire test function when a feature is missing
++ * (in this case getline), because it confuses cunit.pl. So instead we
++ * make sure it will at least compile, but then return early without doing
++ * anything if the feature we wanted was missing.
++ */
++#ifndef HAVE_GETLINE
++#define getline(a,b,c) (((void)(b)), -1)
++#endif
++
++#define NBUCKETS (0x10000)
++static void test_quality(void)
++{
++ const char *wordsfile = "/usr/share/dict/words";
++ unsigned buckets[NBUCKETS] = {0};
++ FILE *stream;
++ char *line = NULL;
++ size_t len = 0;
++ ssize_t nread;
++ unsigned i;
++ unsigned inputs = 0;
++ unsigned contains_none = 0;
++ unsigned contains_one = 0;
++ unsigned contains_many = 0;
++ unsigned contains_many_sum = 0;
++ unsigned highest_count = 0;
++ unsigned highest_count_freq = 0;
++ unsigned max_acceptable_count;
++ double load;
++
++#ifndef HAVE_GETLINE
++ /* can't do anything anyway */
++ return;
++#endif
++
++ stream = fopen(wordsfile, "r");
++ if (!stream) {
++ if (verbose)
++ fprintf(stderr, "%s: %s (skipping) ", wordsfile, strerror(errno));
++ return;
++ }
++
++ while ((nread = getline(&line, &len, stream)) != -1) {
++ /* chomp */
++ if (line[nread - 1] == '\n')
++ line[nread - 1] = '\0';
++
++ unsigned hash = strhash_seeded_djb2(0, line) % NBUCKETS;
++
++ buckets[hash]++;
++ inputs++;
++ }
++ free(line);
++
++ /* arbitrary declaration of quality: no buckets should have more
++ * than ten times the expected load
++ */
++ load = inputs * 1.0 / NBUCKETS;
++ max_acceptable_count = load * 10;
++
++ unsigned bucket_counts[max_acceptable_count + 2];
++ memset(bucket_counts, 0, sizeof(bucket_counts));
++
++ for (i = 0; i < NBUCKETS; i++) {
++ switch (buckets[i]) {
++ case 0:
++ contains_none++;
++ break;
++ case 1:
++ contains_one++;
++ break;
++ default:
++ contains_many++;
++ contains_many_sum += buckets[i];
++ break;
++ }
++
++ if (buckets[i] > max_acceptable_count) {
++ bucket_counts[max_acceptable_count+1]++;
++ }
++ else {
++ bucket_counts[buckets[i]]++;
++ }
++
++ if (buckets[i] > highest_count) {
++ highest_count = buckets[i];
++ highest_count_freq = 1;
++ }
++ else if (buckets[i] == highest_count) {
++ highest_count_freq++;
++ }
++ }
++
++ if (verbose) {
++ putc('\n', stderr);
++ fprintf(stderr, "buckets: %u inputs: %u load: %g\n",
++ NBUCKETS, inputs, load);
++ fprintf(stderr, "empty: %u unique: %u busy: %u\n",
++ contains_none, contains_one, contains_many);
++ fprintf(stderr, "avg count in busy buckets: %g\n",
++ contains_many_sum * 1.0 / contains_many);
++ fprintf(stderr, "busiest %u buckets contain %u each\n",
++ highest_count_freq, highest_count);
++ fprintf(stderr, "max acceptable count: %u\n", max_acceptable_count);
++ fprintf(stderr, "\nbucket count histogram:\ncount frequency\n");
++ for (i = 0; i <= max_acceptable_count; i++) {
++ fprintf(stderr, "%4u %u\n", i, bucket_counts[i]);
++ }
++ fprintf(stderr, "%4u+ %u\n", max_acceptable_count + 1,
++ bucket_counts[max_acceptable_count + 1]);
++ }
++
++ CU_ASSERT_EQUAL(bucket_counts[max_acceptable_count + 1], 0);
++}
++#undef NBUCKETS
++
++/* vim: set ft=c: */
+--- a/imap/http_dav.c
++++ b/imap/http_dav.c
+@@ -2678,13 +2678,10 @@
+
+ if (!propstat) {
+ /* Prescreen "property" request */
+- if (fctx->req_tgt->collection ||
+- (fctx->req_tgt->userid && fctx->depth >= 1) || fctx->depth >= 2) {
+- /* Add namespaces for possible privileges */
+- ensure_ns(fctx->ns, NS_CYRUS, fctx->root, XML_NS_CYRUS, "CY");
+- if (fctx->req_tgt->namespace->id == URL_NS_CALENDAR) {
+- ensure_ns(fctx->ns, NS_CALDAV, fctx->root, XML_NS_CALDAV, "C");
+- }
++ /* Add namespaces for possible privileges */
++ ensure_ns(fctx->ns, NS_CYRUS, fctx->root, XML_NS_CYRUS, "CY");
++ if (fctx->req_tgt->namespace->id == URL_NS_CALENDAR) {
++ ensure_ns(fctx->ns, NS_CALDAV, fctx->root, XML_NS_CALDAV, "C");
+ }
+
+ return 0;
+@@ -6047,7 +6044,7 @@
+ xmlDocPtr indoc = NULL, outdoc = NULL;
+ xmlNodePtr root, cur = NULL, props = NULL;
+ xmlNsPtr ns[NUM_NAMESPACE];
+- struct hash_table ns_table = { 0, NULL, NULL };
++ struct hash_table ns_table = HASH_TABLE_INITIALIZER;
+ struct propfind_ctx fctx;
+
+ memset(&fctx, 0, sizeof(struct propfind_ctx));
+@@ -8011,7 +8008,7 @@
+ xmlNodePtr inroot = NULL, outroot = NULL, cur, prop = NULL, props = NULL;
+ const struct report_type_t *report = NULL;
+ xmlNsPtr ns[NUM_NAMESPACE];
+- struct hash_table ns_table = { 0, NULL, NULL };
++ struct hash_table ns_table = HASH_TABLE_INITIALIZER;
+ struct propfind_ctx fctx;
+
+ memset(&fctx, 0, sizeof(struct propfind_ctx));
+--- a/imap/jmap_mail.c
++++ b/imap/jmap_mail.c
+@@ -4044,7 +4044,7 @@
+ memset(&touched_ids, 0, sizeof(hash_table));
+ construct_hash_table(&touched_ids, mdcount + 1, 0);
+
+- hashu64_table touched_cids = HASH_TABLE_INITIALIZER;
++ hashu64_table touched_cids = HASHU64_TABLE_INITIALIZER;
+ memset(&touched_cids, 0, sizeof(hashu64_table));
+ construct_hashu64_table(&touched_cids, mdcount + 1, 0);
+
+--- a/lib/hash.c
++++ b/lib/hash.c
+@@ -43,10 +43,12 @@
+ assert(table);
+ assert(size);
+
+- table->size = size;
++ table->size = size;
++ table->count = 0;
++ table->seed = rand(); /* might be zero, that's okay */
+
+ /* Allocate the table -- different for using memory pools and not */
+- if(use_mpool) {
++ if (use_mpool) {
+ /* Allocate an initial memory pool for 32 byte keys + the hash table
+ * + the buckets themselves */
+ table->pool =
+@@ -72,7 +74,7 @@
+
+ EXPORTED void *hash_insert(const char *key, void *data, hash_table *table)
+ {
+- unsigned val = strhash(key) % table->size;
++ unsigned val = strhash_seeded(table->seed, key) % table->size;
+ bucket *ptr, *newptr;
+ bucket **prev;
+
+@@ -93,12 +95,13 @@
+ }
+ (table->table)[val] -> next = NULL;
+ (table->table)[val] -> data = data;
++ table->count++;
+ return (table->table)[val] -> data;
+ }
+
+ /*
+ ** This spot in the table is already in use. See if the current string
+- ** has already been inserted, and if so, increment its count.
++ ** has already been inserted, and if so, replace its data
+ */
+ for (prev = &((table->table)[val]), ptr=(table->table)[val];
+ ptr;
+@@ -124,6 +127,7 @@
+ newptr->data = data;
+ newptr->next = ptr;
+ *prev = newptr;
++ table->count++;
+ return data;
+ }
+ }
+@@ -142,10 +146,10 @@
+ newptr->data = data;
+ newptr->next = NULL;
+ *prev = newptr;
++ table->count++;
+ return data;
+ }
+
+-
+ /*
+ ** Look up a key and return the associated data. Returns NULL if
+ ** the key is not in the table.
+@@ -159,7 +163,7 @@
+ if (!table->size)
+ return NULL;
+
+- val = strhash(key) % table->size;
++ val = strhash_seeded(table->seed, key) % table->size;
+
+ if (!(table->table)[val])
+ return NULL;
+@@ -183,8 +187,7 @@
+ * since it will leak memory until you get rid of the entire hash table */
+ EXPORTED void *hash_del(const char *key, hash_table *table)
+ {
+- unsigned val = strhash(key) % table->size;
+- void *data;
++ unsigned val = strhash_seeded(table->seed, key) % table->size;
+ bucket *ptr, *last = NULL;
+
+ if (!(table->table)[val])
+@@ -205,15 +208,10 @@
+ int cmpresult = strcmp(key, ptr->key);
+ if (!cmpresult)
+ {
++ void *data = ptr->data;
+ if (last != NULL )
+ {
+- data = ptr -> data;
+ last -> next = ptr -> next;
+- if(!table->pool) {
+- free(ptr->key);
+- free(ptr);
+- }
+- return data;
+ }
+
+ /*
+@@ -226,15 +224,16 @@
+
+ else
+ {
+- data = ptr->data;
+ (table->table)[val] = ptr->next;
+- if(!table->pool) {
+- free(ptr->key);
+- free(ptr);
+- }
+- return data;
+ }
+- } else if (cmpresult < 0) {
++ if(!table->pool) {
++ free(ptr->key);
++ free(ptr);
++ }
++ table->count--;
++ return data;
++ }
++ if (cmpresult < 0) {
+ /* its not here! */
+ return NULL;
+ }
+@@ -292,6 +291,7 @@
+ }
+ table->table = NULL;
+ table->size = 0;
++ table->count = 0;
+ }
+
+ /*
+@@ -340,19 +340,8 @@
+
+ EXPORTED int hash_numrecords(hash_table *table)
+ {
+- unsigned i;
+- bucket *temp;
+- int count = 0;
+-
+- for (i = 0; i < table->size; i++) {
+- temp = (table->table)[i];
+- while (temp) {
+- count++;
+- temp = temp->next;
+- }
+- }
+-
+- return count;
++ /* XXX macro or inline this if we keep the count field long term */
++ return table->count;
+ }
+
+ EXPORTED void hash_enumerate_sorted(hash_table *table, void (*func)(const char *, void *, void *),
+--- a/lib/hash.h
++++ b/lib/hash.h
+@@ -3,10 +3,11 @@
+ #define HASH__H
+
+ #include <stddef.h> /* For size_t */
++#include <stdint.h>
+ #include "mpool.h"
+ #include "strarray.h"
+
+-#define HASH_TABLE_INITIALIZER {0, NULL, NULL}
++#define HASH_TABLE_INITIALIZER {0, 0, 0, NULL, NULL}
+
+ /*
+ ** A hash table consists of an array of these buckets. Each bucket
+@@ -32,6 +33,8 @@
+
+ typedef struct hash_table {
+ size_t size;
++ size_t count;
++ uint32_t seed;
+ bucket **table;
+ struct mpool *pool;
+ } hash_table;
+--- a/lib/strhash.c
++++ b/lib/strhash.c
+@@ -42,17 +42,32 @@
+
+ #include "config.h"
+
+-EXPORTED unsigned strhash(const char *string)
++#include "lib/strhash.h"
++
++/* The well-known djb2 algorithm (e.g. http://www.cse.yorku.ca/~oz/hash.html),
++ * with the addition of an optional seed to limit predictability.
++ *
++ * XXX return type 'unsigned' for back-compat to previous version, but
++ * XXX ought to be 'uint32_t'
++ */
++EXPORTED unsigned strhash_seeded_djb2(uint32_t seed, const char *string)
+ {
+- unsigned ret_val = 0;
+- int i;
++ const unsigned char *ustr = (const unsigned char *) string;
++ unsigned hash = 5381;
++ int c;
++
++ if (seed) {
++ /* treat the bytes of the seed as a prefix to the string */
++ unsigned i;
++ for (i = 0; i < sizeof seed; i++) {
++ c = seed & 0xff;
++ hash = ((hash << 5) + hash) ^ c;
++ seed >>= 8;
++ }
++ }
++
++ while ((c = *ustr++))
++ hash = ((hash << 5) + hash) ^ c;
+
+- while (*string)
+- {
+- i = (int) *string;
+- ret_val ^= i;
+- ret_val <<= 1;
+- string ++;
+- }
+- return ret_val;
++ return hash;
+ }
+--- a/lib/strhash.h
++++ b/lib/strhash.h
+@@ -41,7 +41,11 @@
+ */
+
+ #ifndef _STRHASH_H_
++#include <stdint.h>
+
+-unsigned strhash(const char *string);
++unsigned strhash_seeded_djb2(uint32_t seed, const char *string);
++
++#define strhash(in) strhash_seeded_djb2((0), (in))
++#define strhash_seeded(sd, in) strhash_seeded_djb2((sd), (in))
+
+ #endif /* _STRHASH_H_ */
diff --git a/debian/patches/series b/debian/patches/series
index 27fc0ec9..1577d428 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -8,3 +8,4 @@
0012-Use-UnicodeData.txt-from-system.patch
0018-increase-test-timeout.patch
CVE-2021-32056.patch
+CVE-2021-33582.patch
Reply to: