----- Forwarded message from Acts1631 <[email protected]> -----

Date: Wed, 17 Jun 2026 20:16:18 +0000
From: Acts1631 <[email protected]>
To: "[email protected]" <[email protected]>
Subject: report 3/3: latent unterminated buffer in imap_wordcasecmp()
X-Spam-score: 0.0
X-Delivered-to: [email protected]
Message-ID: 
<ZJud85dInL0tNywwnZ6vrX_6gMApaSGaf5NjsqED3dGpZ90dK9EcV-n0dDrJp6GSOaRAE-OxwG_cXHhjKs6MJfTHncpVUwoyTMGzd_ZPbDY=@proton.me>

imap_wordcasecmp() copies one word from an IMAP response into a stack buffer 
and compares it case-insensitively:

  char tmp[SHORT_STRING];
  ...
  tmp[SHORT_STRING-1] = 0;
  for (i=0;i < SHORT_STRING-2;i++,s++)
  {
    if (!*s || IS_ASCII_WS(*s))
    {
      tmp[i] = 0;
      break;
    }
    tmp[i] = *s;
  }
  tmp[i+1] = 0;

With SHORT_STRING == 128, an input word of at least 126 non-whitespace 
characters fills tmp[0] through tmp[125]. The loop exits with i == 126 and then 
writes tmp[127] = 0. This leaves tmp[126] uninitialized, meaning tmp is not 
properly terminated.

The only current caller in the tree is cmd_parse_capability():

  if (imap_wordcasecmp(Capabilities[x], s) == 0)

Since Capabilities[x] elements are short fixed strings (e.g., "IMAP4rev1", 
"CONDSTORE"), ascii_strcasecmp() will return before reading the uninitialized tmp[126] 
byte. However, the helper is buggy in isolation.

Proposed fix: Zero-initialize tmp and simplify the copy loop:

  memset(tmp, 0, sizeof(tmp));
  for (i = 0; i < SHORT_STRING - 2 && *s && !IS_ASCII_WS(*s); i++, s++)
    tmp[i] = *s;

This ensures tmp is always fully initialized and correctly NUL-terminated.




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

--
Kevin J. McCarthy
GPG Fingerprint: 8975 A9B3 3AA3 7910 385C  5308 ADEF 7684 8031 6BDA
diff --git a/imap/util.c b/imap/util.c
index 9b6ef7b4..44cdfca9 100644
--- a/imap/util.c
+++ b/imap/util.c
@@ -887,17 +887,9 @@ int imap_wordcasecmp(const char *a, const char *b)
   const char *s = b;
   int i;
 
-  tmp[SHORT_STRING-1] = 0;
-  for (i=0;i < SHORT_STRING-2;i++,s++)
-  {
-    if (!*s || IS_ASCII_WS(*s))
-    {
-      tmp[i] = 0;
-      break;
-    }
+  memset(tmp, 0, sizeof(tmp));
+  for (i = 0; i < SHORT_STRING - 2 && *s && !IS_ASCII_WS(*s); i++, s++)
     tmp[i] = *s;
-  }
-  tmp[i+1] = 0;
 
   return ascii_strcasecmp(a, tmp);
 }
#!/usr/bin/env python3
"""
Standalone demonstrator for Bug 3's latent imap_wordcasecmp() termination
bug.

The earlier network reproducer was not valid: in the real mutt capability
parser, imap_wordcasecmp() is only called with short fixed capability names
as its first argument, so a malicious unknown 126-byte capability word does
not force ascii_strcasecmp() to read tmp[126].

This script writes a small C harness to /tmp that recreates the vulnerable
helper and compares it against a 126-byte string, which is the condition
needed to force a read of the uninitialized tmp[126] byte.  Detecting the
read requires a tool that tracks uninitialized memory, such as Clang
MemorySanitizer or Valgrind Memcheck.

Usage:

    python3 reproducer_imap_wordcase.py
    clang -fsanitize=memory -g -O1 /tmp/imap_wordcasecmp_repro.c -o /tmp/imap_wordcasecmp_repro
    /tmp/imap_wordcasecmp_repro
"""

from pathlib import Path


SOURCE = r'''
#include <ctype.h>
#include <stdio.h>
#include <string.h>

#define SHORT_STRING 128
#define IS_ASCII_WS(c) ((c) == ' ' || (c) == '\t' || (c) == '\r' || (c) == '\n')

static int ascii_tolower_local(int c)
{
  if (c >= 'A' && c <= 'Z')
    return c | 32;
  return c;
}

static int ascii_strcasecmp_local(const char *a, const char *b)
{
  int i;
  for (;; a++, b++)
  {
    if ((i = ascii_tolower_local((unsigned char)*a) -
             ascii_tolower_local((unsigned char)*b)))
      return i;
    if (!*a)
      break;
  }
  return 0;
}

static int vulnerable_imap_wordcasecmp(const char *a, const char *b)
{
  char tmp[SHORT_STRING];
  const char *s = b;
  int i;

  tmp[SHORT_STRING-1] = 0;
  for (i=0;i < SHORT_STRING-2;i++,s++)
  {
    if (!*s || IS_ASCII_WS(*s))
    {
      tmp[i] = 0;
      break;
    }
    tmp[i] = *s;
  }
  tmp[i+1] = 0;

  return ascii_strcasecmp_local(a, tmp);
}

int main(void)
{
  char a[SHORT_STRING];
  char b[SHORT_STRING];

  memset(a, 'A', SHORT_STRING - 2);
  a[SHORT_STRING - 2] = '\0';
  a[SHORT_STRING - 1] = '\0';

  memset(b, 'A', SHORT_STRING - 2);
  b[SHORT_STRING - 2] = '\0';
  b[SHORT_STRING - 1] = '\0';

  printf("result=%d\n", vulnerable_imap_wordcasecmp(a, b));
  return 0;
}
'''


def main():
    path = Path("/tmp/imap_wordcasecmp_repro.c")
    path.write_text(SOURCE)
    print(f"Wrote {path}")
    print("Compile with: clang -fsanitize=memory -g -O1 /tmp/imap_wordcasecmp_repro.c -o /tmp/imap_wordcasecmp_repro")


if __name__ == "__main__":
    main()

Attachment: signature.asc
Description: PGP signature

Reply via email to