Commit message: Fix seqset iterator when it ends in a comma.

If the seqset ended with a comma, the substr_end marker would be just
before the trailing nul.  In the next call, the loop to skip the
marker would iterate right past the end of string too.

The fix is simple: place the substr_end marker and skip past it
immediately.
CVE: CVE-2021-32055
CWE: CWE-125
Vulnerability description: Mutt 1.11.0 through 2.0.x before 2.0.7 (and NeoMutt 2019-10-25 through 2021-05-04) has a $imap_qresync issue in which imap/util.c has an out-of-bounds read in situations where an IMAP sequence set ends with a comma. NOTE: the $imap_qresync setting for QRESYNC is not enabled by default.

Project: mutt
File: util.c

static void imap_msn_index_to_uid_seqset (BUFFER *b, IMAP_DATA *idata)
{
  int first = 1, state = 0, match = 0;
  HEADER *cur_header;
  unsigned int msn, cur_uid = 0, last_uid = 0;
  unsigned int range_begin = 0, range_end = 0;

  for (msn = 1; msn <= idata->max_msn + 1; msn++)
  {
    match = 0;
    if (msn <= idata->max_msn)
    {
      cur_header = idata->msn_index[msn - 1];
      cur_uid = cur_header ? HEADER_DATA(cur_header)->uid : 0;
      if (!state || (cur_uid && (cur_uid - 1 == last_uid)))
        match = 1;
      last_uid = cur_uid;
    }

    if (match)
    {
      switch (state)
      {
        case 1:            /* single: convert to a range */
          state = 2;
          /* fall through */
        case 2:            /* extend range ending */
          range_end = cur_uid;
          break;
        default:
          state = 1;
          range_begin = cur_uid;
          break;
      }
    }
    else if (state)
    {
      if (first)
        first = 0;
      else
        mutt_buffer_addch (b, ',');

      if (state == 1)
        mutt_buffer_add_printf (b, "%u", range_begin);
      else if (state == 2)
        mutt_buffer_add_printf (b, "%u:%u", range_begin, range_end);

      state = 1;
      range_begin = cur_uid;
    }
  }
}

Project: mutt
File: util.c

static void imap_hcache_namer (const char *path, BUFFER *dest)
{
  mutt_buffer_printf (dest, "%s.hcache", path);
}

Project: mutt
File: util.c

SEQSET_ITERATOR *mutt_seqset_iterator_new (const char *seqset)
{
  SEQSET_ITERATOR *iter;

  if (!seqset || !*seqset)
    return NULL;

  iter = safe_calloc (1, sizeof(SEQSET_ITERATOR));
  iter->full_seqset = safe_strdup (seqset);
  iter->eostr = strchr (iter->full_seqset, '\0');
  iter->substr_cur = iter->substr_end = iter->full_seqset;

  return iter;
}

Project: mutt
File: util.c

IMAP_DATA* imap_new_idata (void)
{
  IMAP_DATA* idata = safe_calloc (1, sizeof (IMAP_DATA));

  idata->cmdbuf = mutt_buffer_new ();
  idata->cmdslots = ImapPipelineDepth + 2;
  idata->cmds = safe_calloc (idata->cmdslots, sizeof(*idata->cmds));

  return idata;
}

Project: mutt
File: util.c

void mutt_seqset_iterator_free (SEQSET_ITERATOR **p_iter)
{
  SEQSET_ITERATOR *iter;

  if (!p_iter || !*p_iter)
    return;

  iter = *p_iter;
  FREE (&iter->full_seqset);
  FREE (p_iter);               /* __FREE_CHECKED__ */
}

Project: mutt
File: util.c

HEADER* imap_hcache_get (IMAP_DATA* idata, unsigned int uid)
{
  char key[16];
  void *data;
  unsigned int uv;
  HEADER* h = NULL;

  if (!idata->hcache)
    return NULL;

  sprintf (key, "/%u", uid);
  data = mutt_hcache_fetch (idata->hcache, key,
                            imap_hcache_keylen);
  if (data)
  {
    memcpy (&uv, data, sizeof(unsigned int));
    if (uv == idata->uid_validity)
      h = mutt_hcache_restore ((unsigned char *)data, NULL);
    else
      dprint (3, (debugfile, "hcache uidvalidity mismatch: %u", uv));
    mutt_hcache_free ((void **)&data);
  }

  return h;
}

Project: mutt
File: util.c

void imap_buffer_qualify_path (BUFFER *dest, IMAP_MBOX *mx, char* path)
{
  ciss_url_t url;

  mutt_account_tourl (&mx->account, &url);
  url.path = path;

  url_ciss_tobuffer (&url, dest, 0);
}

Project: mutt
File: util.c

void imap_pretty_mailbox (char* path, size_t pathlen)
{
  IMAP_MBOX home, target;
  ciss_url_t url;
  char* delim;
  int tlen;
  int hlen = 0;
  char home_match = 0;

  if (imap_parse_path (path, &target) < 0)
    return;

  tlen = mutt_strlen (target.mbox);
  /* check whether we can do '=' substitution */
  if (mx_is_imap(Maildir) && !imap_parse_path (Maildir, &home))
  {
    hlen = mutt_strlen (home.mbox);
    if (tlen && mutt_account_match (&home.account, &target.account) &&
	!mutt_strncmp (home.mbox, target.mbox, hlen))
    {
      if (! hlen)
	home_match = 1;
      else if (ImapDelimChars)
	for (delim = ImapDelimChars; *delim != '\0'; delim++)
	  if (target.mbox[hlen] == *delim)
	    home_match = 1;
    }
    FREE (&home.mbox);
  }

  /* do the '=' substitution */
  if (home_match)
  {
    *path++ = '=';
    /* copy remaining path, skipping delimiter */
    if (! hlen)
      hlen = -1;
    memcpy (path, target.mbox + hlen + 1, tlen - hlen - 1);
    path[tlen - hlen - 1] = '\0';
  }
  else
  {
    mutt_account_tourl (&target.account, &url);
    url.path = target.mbox;
    url_ciss_tostring (&url, path, pathlen, 0);
  }

  FREE (&target.mbox);
}

Project: mutt
File: util.c

int imap_expand_path (BUFFER* path)
{
  IMAP_MBOX mx;
  IMAP_DATA* idata;
  ciss_url_t url;
  char fixedpath[LONG_STRING];
  int rc;

  if (imap_parse_path (mutt_b2s (path), &mx) < 0)
    return -1;

  idata = imap_conn_find (&mx.account, MUTT_IMAP_CONN_NONEW);
  mutt_account_tourl (&mx.account, &url);
  imap_fix_path (idata, mx.mbox, fixedpath, sizeof (fixedpath));
  url.path = fixedpath;

  rc = url_ciss_tobuffer (&url, path, U_DECODE_PASSWD);
  FREE (&mx.mbox);

  return rc;
}

Project: mutt
File: util.c

int imap_hcache_clear_uid_seqset (IMAP_DATA *idata)
{
  if (!idata->hcache)
    return -1;

  return mutt_hcache_delete (idata->hcache, "/UIDSEQSET", imap_hcache_keylen);
}

Project: mutt
File: util.c

void imap_cachepath (IMAP_DATA *idata, const char *mailbox, BUFFER *dest)
{
  const char *p = mailbox;

  mutt_buffer_clear (dest);
  if (!p)
    return;

  while (*p)
  {
    if (*p == idata->delim)
    {
      mutt_buffer_addch (dest, '/');
      /* simple way to avoid collisions with UIDs */
      if (*(p + 1) >= '0' && *(p + 1) <= '9')
        mutt_buffer_addch (dest, '_');
    }
    else
      mutt_buffer_addch (dest, *p);
    p++;
  }
}

Project: mutt
File: util.c

int mutt_seqset_iterator_next (SEQSET_ITERATOR *iter, unsigned int *next)
{
  char *range_sep;

  if (!iter || !next)
    return -1;

  if (iter->in_range)
  {
    if ((iter->down && iter->range_cur == (iter->range_end - 1)) ||
        (!iter->down && iter->range_cur == (iter->range_end + 1)))
      iter->in_range = 0;
  }

  if (!iter->in_range)
  {
    iter->substr_cur = iter->substr_end;
    if (iter->substr_cur == iter->eostr)
      return 1;

    iter->substr_end = strchr (iter->substr_cur, ',');
    if (!iter->substr_end)
      iter->substr_end = iter->eostr;
    else
      *(iter->substr_end++) = '\0';

    range_sep = strchr (iter->substr_cur, ':');
    if (range_sep)
      *range_sep++ = '\0';

    if (mutt_atoui (iter->substr_cur, &iter->range_cur))
      return -1;
    if (range_sep)
    {
      if (mutt_atoui (range_sep, &iter->range_end))
        return -1;
    }
    else
      iter->range_end = iter->range_cur;

    iter->down = (iter->range_end < iter->range_cur);
    iter->in_range = 1;
  }

  *next = iter->range_cur;
  if (iter->down)
    iter->range_cur--;
  else
    iter->range_cur++;

  return 0;
}

Project: mutt
File: util.c

char *imap_hcache_get_uid_seqset (IMAP_DATA *idata)
{
  char *hc_seqset, *seqset;

  if (!idata->hcache)
    return NULL;

  hc_seqset = mutt_hcache_fetch_raw (idata->hcache, "/UIDSEQSET",
                                     imap_hcache_keylen);
  seqset = safe_strdup (hc_seqset);
  mutt_hcache_free ((void **)&hc_seqset);
  dprint (5, (debugfile, "Retrieved /UIDSEQSET %s\n", NONULL (seqset)));

  return seqset;
}

Project: mutt
File: util.c

header_cache_t* imap_hcache_open (IMAP_DATA* idata, const char* path)
{
  IMAP_MBOX mx;
  ciss_url_t url;
  BUFFER *cachepath = NULL;
  BUFFER *mbox = NULL;
  size_t len;
  header_cache_t *rv = NULL;

  mbox = mutt_buffer_pool_get ();
  cachepath = mutt_buffer_pool_get ();

  if (path)
    imap_cachepath (idata, path, mbox);
  else
  {
    if (!idata->ctx || imap_parse_path (idata->ctx->path, &mx) < 0)
      goto cleanup;

    imap_cachepath (idata, mx.mbox, mbox);
    FREE (&mx.mbox);
  }

  if (strstr (mutt_b2s (mbox), "/../") ||
      (strcmp (mutt_b2s (mbox), "..") == 0) ||
      (strncmp(mutt_b2s (mbox), "../", 3) == 0))
    goto cleanup;
  len = mutt_buffer_len (mbox);
  if ((len > 3) && (strcmp(mutt_b2s (mbox) + len - 3, "/..") == 0))
    goto cleanup;

  mutt_account_tourl (&idata->conn->account, &url);
  url.path = mbox->data;
  url_ciss_tobuffer (&url, cachepath, U_PATH);

  rv = mutt_hcache_open (HeaderCache, mutt_b2s (cachepath), imap_hcache_namer);

cleanup:
  mutt_buffer_pool_release (&mbox);
  mutt_buffer_pool_release (&cachepath);
  return rv;
}

Project: mutt
File: util.c

int imap_hcache_store_uid_seqset (IMAP_DATA *idata)
{
  BUFFER *b;
  int rc;

  if (!idata->hcache)
    return -1;

  b = mutt_buffer_new ();
  /* The seqset is likely large.  Preallocate to reduce reallocs */
  mutt_buffer_increase_size (b, HUGE_STRING);
  imap_msn_index_to_uid_seqset (b, idata);

  rc = mutt_hcache_store_raw (idata->hcache, "/UIDSEQSET",
                              b->data, mutt_buffer_len (b) + 1,
                              imap_hcache_keylen);
  dprint (5, (debugfile, "Stored /UIDSEQSET %s\n", b->data));
  mutt_buffer_free (&b);
  return rc;
}