Commit message: Fix memory leak parsing group addresses without a display name.

When there was a group address terminator with no previous
addresses (including the group display-name), an address would be
allocated but not attached to the address list.

Change this to only allocate when last exists.

It would be more correct to not allocate at all unless we are inside a
group list, but I will address that in a separate commit to master.
CVE: CVE-2021-3181
CWE: CWE-401
Vulnerability description: rfc822.c in Mutt through 2.0.4 allows remote attackers to cause a denial of service (mailbox unavailability) by sending email messages with sequences of semicolon characters in RFC822 address fields (aka terminators of empty groups). A small email message from the attacker can cause large memory consumption, and the victim may then be unable to see email messages from other persons.

Project: mutt
File: rfc822.c

void rfc822_dequote_comment (char *s)
{
  char *w = s;

  for (; *s; s++)
  {
    if (*s == '\\')
    {
      if (!*++s)
	break; /* error? */
      *w++ = *s;
    }
    else if (*s != '\"')
    {
      if (w != s)
	*w = *s;
      w++;
    }
  }
  *w = 0;
}

Project: mutt
File: rfc822.c

ADDRESS *rfc822_cpy_adr_real (ADDRESS *addr)
{
  ADDRESS *p = rfc822_new_address ();

#ifdef EXACT_ADDRESS
  p->val = safe_strdup (addr->val);
#endif
  p->personal = safe_strdup (addr->personal);
  p->mailbox = safe_strdup (addr->mailbox);
  p->group = addr->group;
  p->is_intl = addr->is_intl;
  p->intl_checked = addr->intl_checked;
  return p;
}

Project: mutt
File: rfc822.c

ADDRESS *rfc822_parse_adrlist (ADDRESS *top, const char *s)
{
  int ws_pending, nl;
#ifdef EXACT_ADDRESS
  const char *begin;
#endif
  const char *ps;
  char comment[LONG_STRING], phrase[LONG_STRING];
  size_t phraselen = 0, commentlen = 0;
  ADDRESS *cur, *last = NULL;

  RFC822Error = 0;

  last = top;
  while (last && last->next)
    last = last->next;

  ws_pending = is_email_wsp (*s);
  if ((nl = mutt_strlen (s)))
    nl = s[nl - 1] == '\n';

  s = skip_email_wsp(s);
#ifdef EXACT_ADDRESS
  begin = s;
#endif
  while (*s)
  {
    if (*s == ',')
    {
      if (phraselen)
      {
	terminate_buffer (phrase, phraselen);
	add_addrspec (&top, &last, phrase, comment, &commentlen, sizeof (comment) - 1);
      }
      else if (commentlen && last && !last->personal)
      {
	terminate_buffer (comment, commentlen);
	last->personal = safe_strdup (comment);
      }

#ifdef EXACT_ADDRESS
      if (last && !last->val)
	last->val = mutt_substrdup (begin, s);
#endif
      commentlen = 0;
      phraselen = 0;
      s++;
#ifdef EXACT_ADDRESS
      begin = skip_email_wsp(s);
#endif
    }
    else if (*s == '(')
    {
      if (commentlen && commentlen < sizeof (comment) - 1)
	comment[commentlen++] = ' ';
      if ((ps = next_token (s, comment, &commentlen, sizeof (comment) - 1)) == NULL)
      {
	rfc822_free_address (&top);
	return NULL;
      }
      s = ps;
    }
    else if (*s == '"')
    {
      if (phraselen && phraselen < sizeof (phrase) - 1)
        phrase[phraselen++] = ' ';
      if ((ps = parse_quote (s + 1, phrase, &phraselen, sizeof (phrase) - 1)) == NULL)
      {
        rfc822_free_address (&top);
        return NULL;
      }
      s = ps;
    }
    else if (*s == '[')
    {
      if (phraselen && phraselen < sizeof (phrase) - 1 && ws_pending)
	phrase[phraselen++] = ' ';
      if (phraselen < sizeof (phrase) - 1)
        phrase[phraselen++] = '[';
      if ((ps = parse_literal (s + 1, phrase, &phraselen, sizeof (phrase) - 1)) == NULL)
      {
        rfc822_free_address (&top);
        return NULL;
      }
      s = ps;
    }
    else if (*s == ':')
    {
      cur = rfc822_new_address ();
      terminate_buffer (phrase, phraselen);
      cur->mailbox = safe_strdup (phrase);
      cur->group = 1;

      if (last)
	last->next = cur;
      else
	top = cur;
      last = cur;

#ifdef EXACT_ADDRESS
      last->val = mutt_substrdup (begin, s);
#endif

      phraselen = 0;
      commentlen = 0;
      s++;
#ifdef EXACT_ADDRESS
      begin = skip_email_wsp(s);
#endif
    }
    else if (*s == ';')
    {
      if (phraselen)
      {
	terminate_buffer (phrase, phraselen);
	add_addrspec (&top, &last, phrase, comment, &commentlen, sizeof (comment) - 1);
      }
      else if (commentlen && last && !last->personal)
      {
	terminate_buffer (comment, commentlen);
	last->personal = safe_strdup (comment);
      }
#ifdef EXACT_ADDRESS
      if (last && !last->val)
	last->val = mutt_substrdup (begin, s);
#endif

      /* add group terminator */
      if (last)
      {
	last->next = rfc822_new_address ();
	last = last->next;
      }

      phraselen = 0;
      commentlen = 0;
#ifdef EXACT_ADDRESS
      begin = s;
#endif
      s++;
    }
    else if (*s == '<')
    {
      terminate_buffer (phrase, phraselen);
      cur = rfc822_new_address ();
      if (phraselen)
	cur->personal = safe_strdup (phrase);
      if ((ps = parse_route_addr (s + 1, comment, &commentlen, sizeof (comment) - 1, cur)) == NULL)
      {
	rfc822_free_address (&top);
	rfc822_free_address (&cur);
	return NULL;
      }

      if (last)
	last->next = cur;
      else
	top = cur;
      last = cur;

      phraselen = 0;
      commentlen = 0;
      s = ps;
    }
    else
    {
      if (phraselen && phraselen < sizeof (phrase) - 1 && ws_pending)
	phrase[phraselen++] = ' ';
      if ((ps = next_token (s, phrase, &phraselen, sizeof (phrase) - 1)) == NULL)
      {
	rfc822_free_address (&top);
	return NULL;
      }
      s = ps;
    }
    ws_pending = is_email_wsp(*s);
    s = skip_email_wsp(s);
  }

  if (phraselen)
  {
    terminate_buffer (phrase, phraselen);
    terminate_buffer (comment, commentlen);
    add_addrspec (&top, &last, phrase, comment, &commentlen, sizeof (comment) - 1);
  }
  else if (commentlen && last && !last->personal)
  {
    terminate_buffer (comment, commentlen);
    last->personal = safe_strdup (comment);
  }
#ifdef EXACT_ADDRESS
  if (last && !last->val)
    last->val = mutt_substrdup (begin, s - nl < begin ? begin : s - nl);
#endif

  return top;
}

Project: mutt
File: rfc822.c

rfc822_cat (char *buf, size_t buflen, const char *value, const char *specials)
{
  if (strpbrk (value, specials))
  {
    char tmp[256], *pc = tmp;
    size_t tmplen = sizeof (tmp) - 3;

    *pc++ = '"';
    for (; *value && tmplen > 1; value++)
    {
      if (*value == '\\' || *value == '"')
      {
	*pc++ = '\\';
	tmplen--;
      }
      *pc++ = *value;
      tmplen--;
    }
    *pc++ = '"';
    *pc = 0;
    strfcpy (buf, tmp, buflen);
  }
  else
    strfcpy (buf, value, buflen);
}

Project: mutt
File: rfc822.c

next_token (const char *s, char *token, size_t *tokenlen, size_t tokenmax)
{
  if (*s == '(')
    return (rfc822_parse_comment (s + 1, token, tokenlen, tokenmax));
  if (*s == '"')
    return (parse_quote (s + 1, token, tokenlen, tokenmax));
  if (*s && is_special (*s))
  {
    if (*tokenlen < tokenmax)
      token[(*tokenlen)++] = *s;
    return (s + 1);
  }
  while (*s)
  {
    if (is_email_wsp(*s) || is_special (*s))
      break;
    if (*tokenlen < tokenmax)
      token[(*tokenlen)++] = *s;
    s++;
  }
  return s;
}

Project: mutt
File: rfc822.c

parse_mailboxdomain (const char *s, const char *nonspecial,
		     char *mailbox, size_t *mailboxlen, size_t mailboxmax,
		     char *comment, size_t *commentlen, size_t commentmax)
{
  const char *ps;

  while (*s)
  {
    s = skip_email_wsp(s);
    if (! *s)
      return s;

    if (strchr (nonspecial, *s) == NULL && is_special (*s))
      return s;

    if (*s == '(')
    {
      if (*commentlen && *commentlen < commentmax)
	comment[(*commentlen)++] = ' ';
      ps = next_token (s, comment, commentlen, commentmax);
    }
    else
      ps = next_token (s, mailbox, mailboxlen, mailboxmax);
    if (!ps)
      return NULL;
    s = ps;
  }

  return s;
}

Project: mutt
File: rfc822.c

parse_addr_spec (const char *s,
		 char *comment, size_t *commentlen, size_t commentmax,
		 ADDRESS *addr)
{
  char token[LONG_STRING];
  size_t tokenlen = 0;

  s = parse_address (s, token, &tokenlen, sizeof (token) - 1, comment, commentlen, commentmax, addr);
  if (s && *s && *s != ',' && *s != ';')
  {
    RFC822Error = ERR_BAD_ADDR_SPEC;
    return NULL;
  }
  return s;
}

Project: mutt
File: rfc822.c

parse_quote (const char *s, char *token, size_t *tokenlen, size_t tokenmax)
{
  while (*s)
  {
    if (*tokenlen < tokenmax)
      token[*tokenlen] = *s;
    if (*s == '\\')
    {
      if (!*++s)
	break;

      if (*tokenlen < tokenmax)
	token[*tokenlen] = *s;
    }
    else if (*s == '"')
      return (s + 1);
    (*tokenlen)++;
    s++;
  }
  RFC822Error = ERR_MISMATCH_QUOTE;
  return NULL;
}

Project: mutt
File: rfc822.c

parse_route_addr (const char *s,
		  char *comment, size_t *commentlen, size_t commentmax,
		  ADDRESS *addr)
{
  char token[LONG_STRING];
  size_t tokenlen = 0;

  s = skip_email_wsp(s);

  /* find the end of the route */
  if (*s == '@')
  {
    while (s && *s == '@')
    {
      if (tokenlen < sizeof (token) - 1)
	token[tokenlen++] = '@';
      s = parse_mailboxdomain (s + 1, ",.\\[](", token,
			       &tokenlen, sizeof (token) - 1,
			       comment, commentlen, commentmax);
    }
    if (!s || *s != ':')
    {
      RFC822Error = ERR_BAD_ROUTE;
      return NULL; /* invalid route */
    }

    if (tokenlen < sizeof (token) - 1)
      token[tokenlen++] = ':';
    s++;
  }

  if ((s = parse_address (s, token, &tokenlen, sizeof (token) - 1, comment, commentlen, commentmax, addr)) == NULL)
    return NULL;

  if (*s != '>')
  {
    RFC822Error = ERR_BAD_ROUTE_ADDR;
    return NULL;
  }

  if (!addr->mailbox)
    addr->mailbox = safe_strdup ("@");

  s++;
  return s;
}

Project: mutt
File: rfc822.c

int main (int argc, char **argv)
{
  ADDRESS *list;
  char buf[256];
# if 0
  char *str = "michael, Michael Elkins <me@mutt.org>, testing a really complex address: this example <@contains.a.source.route,@with.multiple.hosts:address@example.com>;, lothar@of.the.hillpeople (lothar)";
# else
  char *str = "a b c ";
# endif

  list = rfc822_parse_adrlist (NULL, str);
  buf[0] = 0;
  rfc822_write_address (buf, sizeof (buf), list);
  rfc822_free_address (&list);
  puts (buf);
  exit (0);
}

Project: mutt
File: rfc822.c

int rfc822_write_address (char *buf, size_t buflen, ADDRESS *addr, int display)
{
  char *pbuf = buf;
  size_t len = mutt_strlen (buf);

  buflen--; /* save room for the terminal nul */

  if (len > 0)
  {
    if (len > buflen)
      return pbuf - buf; /* safety check for bogus arguments */

    pbuf += len;
    buflen -= len;
    if (!buflen)
      goto done;
    *pbuf++ = ',';
    buflen--;
    if (!buflen)
      goto done;
    *pbuf++ = ' ';
    buflen--;
  }

  for (; addr && buflen > 0; addr = addr->next)
  {
    /* use buflen+1 here because we already saved space for the trailing
       nul char, and the subroutine can make use of it */
    rfc822_write_address_single (pbuf, buflen + 1, addr, display);

    /* this should be safe since we always have at least 1 char passed into
       the above call, which means `pbuf' should always be nul terminated */
    len = mutt_strlen (pbuf);
    pbuf += len;
    buflen -= len;

    /* if there is another address, and its not a group mailbox name or
       group terminator, add a comma to separate the addresses */
    if (addr->next && addr->next->mailbox && !addr->group)
    {
      if (!buflen)
	goto done;
      *pbuf++ = ',';
      buflen--;
      if (!buflen)
	goto done;
      *pbuf++ = ' ';
      buflen--;
    }
  }
done:
  *pbuf = 0;
  return pbuf - buf;
}

Project: mutt
File: rfc822.c

parse_address (const char *s,
               char *token, size_t *tokenlen, size_t tokenmax,
	       char *comment, size_t *commentlen, size_t commentmax,
	       ADDRESS *addr)
{
  s = parse_mailboxdomain (s, ".\"(\\",
			   token, tokenlen, tokenmax,
			   comment, commentlen, commentmax);
  if (!s)
    return NULL;

  if (*s == '@')
  {
    if (*tokenlen < tokenmax)
      token[(*tokenlen)++] = '@';
    s = parse_domain (s + 1,
                      token, tokenlen, tokenmax,
                      comment, commentlen, commentmax);
    if (!s)
      return NULL;
  }

  terminate_string (token, *tokenlen, tokenmax);
  addr->mailbox = safe_strdup (token);

  if (*commentlen && !addr->personal)
  {
    terminate_string (comment, *commentlen, commentmax);
    addr->personal = safe_strdup (comment);
  }

  return s;
}

Project: mutt
File: rfc822.c

int rfc822_remove_from_adrlist (ADDRESS **a, const char *mailbox)
{
  ADDRESS *p, *last = NULL, *t;
  int rv = -1;

  p = *a;
  last = NULL;
  while (p)
  {
    if (ascii_strcasecmp (mailbox, p->mailbox) == 0)
    {
      if (last)
	last->next = p->next;
      else
	(*a) = p->next;
      t = p;
      p = p->next;
      free_address (t);
      rv = 0;
    }
    else
    {
      last = p;
      p = p->next;
    }
  }

  return (rv);
}

Project: mutt
File: rfc822.c

void rfc822_free_address (ADDRESS **p)
{
  ADDRESS *t;

  while (*p)
  {
    t = *p;
    *p = (*p)->next;
#ifdef EXACT_ADDRESS
    FREE (&t->val);
#endif
    FREE (&t->personal);
    FREE (&t->mailbox);
    FREE (&t);
  }
}

Project: mutt
File: rfc822.c

void rfc822_write_address_single (char *buf, size_t buflen, ADDRESS *addr,
				  int display)
{
  size_t len;
  char *pbuf = buf;
  char *pc;

  if (!addr)
    return;

  buflen--; /* save room for the terminal nul */

#ifdef EXACT_ADDRESS
  if (addr->val)
  {
    if (!buflen)
      goto done;
    strfcpy (pbuf, addr->val, buflen);
    len = mutt_strlen (pbuf);
    pbuf += len;
    buflen -= len;
    if (addr->group)
    {
      if (!buflen)
	goto done;
      *pbuf++ = ':';
      buflen--;
      *pbuf = 0;
    }
    return;
  }
#endif

  if (addr->personal)
  {
    if (strpbrk (addr->personal, RFC822Specials))
    {
      if (!buflen)
	goto done;
      *pbuf++ = '"';
      buflen--;
      for (pc = addr->personal; *pc && buflen > 0; pc++)
      {
	if (*pc == '"' || *pc == '\\')
	{
	  *pbuf++ = '\\';
	  buflen--;
	}
	if (!buflen)
	  goto done;
	*pbuf++ = *pc;
	buflen--;
      }
      if (!buflen)
	goto done;
      *pbuf++ = '"';
      buflen--;
    }
    else
    {
      if (!buflen)
	goto done;
      strfcpy (pbuf, addr->personal, buflen);
      len = mutt_strlen (pbuf);
      pbuf += len;
      buflen -= len;
    }

    if (!buflen)
      goto done;
    *pbuf++ = ' ';
    buflen--;
  }

  if (addr->personal || (addr->mailbox && *addr->mailbox == '@'))
  {
    if (!buflen)
      goto done;
    *pbuf++ = '<';
    buflen--;
  }

  if (addr->mailbox)
  {
    if (!buflen)
      goto done;
    if (ascii_strcmp (addr->mailbox, "@") && !display)
    {
      strfcpy (pbuf, addr->mailbox, buflen);
      len = mutt_strlen (pbuf);
    }
    else if (ascii_strcmp (addr->mailbox, "@") && display)
    {
      strfcpy (pbuf, mutt_addr_for_display (addr), buflen);
      len = mutt_strlen (pbuf);
    }
    else
    {
      *pbuf = '\0';
      len = 0;
    }
    pbuf += len;
    buflen -= len;

    if (addr->personal || (addr->mailbox && *addr->mailbox == '@'))
    {
      if (!buflen)
	goto done;
      *pbuf++ = '>';
      buflen--;
    }

    if (addr->group)
    {
      if (!buflen)
	goto done;
      *pbuf++ = ':';
      buflen--;
      if (!buflen)
	goto done;
      *pbuf++ = ' ';
      buflen--;
    }
  }
  else
  {
    if (!buflen)
      goto done;
    *pbuf++ = ';';
    buflen--;
  }
done:
  /* no need to check for length here since we already save space at the
     beginning of this routine */
  *pbuf = 0;
}

Project: mutt
File: rfc822.c

void rfc822_qualify (ADDRESS *addr, const char *host)
{
  char *p;

  for (; addr; addr = addr->next)
    if (!addr->group && addr->mailbox && strchr (addr->mailbox, '@') == NULL)
    {
      p = safe_malloc (mutt_strlen (addr->mailbox) + mutt_strlen (host) + 2);
      sprintf (p, "%s@%s", addr->mailbox, host);	/* __SPRINTF_CHECKED__ */
      FREE (&addr->mailbox);
      addr->mailbox = p;
    }
}

Project: mutt
File: rfc822.c

parse_literal (const char *s, char *token, size_t *tokenlen, size_t tokenmax)
{
  while (*s)
  {
    if (*s == '\\' || *s == '[')
    {
      RFC822Error = ERR_BAD_LITERAL;
      return NULL;
    }

    if (*tokenlen < tokenmax)
      token[(*tokenlen)++] = *s;

    if (*s == ']')
      return s + 1;

    s++;
  }
  RFC822Error = ERR_BAD_LITERAL;
  return NULL;
}

Project: mutt
File: rfc822.c

add_addrspec (ADDRESS **top, ADDRESS **last, const char *phrase,
	      char *comment, size_t *commentlen, size_t commentmax)
{
  ADDRESS *cur = rfc822_new_address ();

  if (parse_addr_spec (phrase, comment, commentlen, commentmax, cur) == NULL)
  {
    rfc822_free_address (&cur);
    return;
  }

  if (*last)
    (*last)->next = cur;
  else
    *top = cur;
  *last = cur;
}

Project: mutt
File: rfc822.c

rfc822_parse_comment (const char *s,
                      char *comment, size_t *commentlen, size_t commentmax)
{
  int level = 1;

  while (*s && level)
  {
    if (*s == '(')
      level++;
    else if (*s == ')')
    {
      if (--level == 0)
      {
	s++;
	break;
      }
    }
    else if (*s == '\\')
    {
      if (!*++s)
	break;
    }
    if (*commentlen < commentmax)
      comment[(*commentlen)++] = *s;
    s++;
  }
  if (level)
  {
    RFC822Error = ERR_MISMATCH_PAREN;
    return NULL;
  }
  return s;
}

Project: mutt
File: rfc822.c

static void free_address (ADDRESS *a)
{
  FREE(&a->personal);
  FREE(&a->mailbox);
#ifdef EXACT_ADDRESS
  FREE(&a->val);
#endif
  FREE(&a);
}

Project: mutt
File: rfc822.c

ADDRESS *rfc822_append (ADDRESS **a, ADDRESS *b, int prune)
{
  ADDRESS *tmp = *a;

  while (tmp && tmp->next)
    tmp = tmp->next;
  if (!b)
    return tmp;
  if (tmp)
    tmp->next = rfc822_cpy_adr (b, prune);
  else
    tmp = *a = rfc822_cpy_adr (b, prune);
  while (tmp && tmp->next)
    tmp = tmp->next;
  return tmp;
}

Project: mutt
File: rfc822.c

parse_domain (const char *s,
              char *mailbox, size_t *mailboxlen, size_t mailboxmax,
              char *comment, size_t *commentlen, size_t commentmax)
{
  const char *ps;
  const char *nonspecial;
  int domain_literal = 0;

  while (*s)
  {
    s = skip_email_wsp(s);
    if (! *s)
      return s;

    if (*s == '(')
    {
      if (*commentlen && *commentlen < commentmax)
	comment[(*commentlen)++] = ' ';
      ps = next_token (s, comment, commentlen, commentmax);
    }
    else
    {
      if (*s == '[')
      {
        domain_literal = 1;
        if (*mailboxlen < mailboxmax)
          mailbox[(*mailboxlen)++] = '[';
        s++;
        nonspecial = "@.,:;<>\"()";
      }
      else
        nonspecial = ".([]\\";

      s = parse_mailboxdomain (s, nonspecial,
                               mailbox, mailboxlen, mailboxmax,
                               comment, commentlen, commentmax);
      if (domain_literal)
      {
        if (!s || *s != ']')
        {
          RFC822Error = ERR_BAD_LITERAL;
          return NULL;
        }

        if (*mailboxlen < mailboxmax)
          mailbox[(*mailboxlen)++] = ']';
        s++;
      }
      return s;
    }
    if (!ps)
      return NULL;
    s = ps;
  }

  return s;
}

Project: mutt
File: rfc822.c

int safe_free (void **p)	/* __SAFE_FREE_CHECKED__ */
{
  free(*p);		/* __MEM_CHECKED__ */
  *p = 0;
}

Project: mutt
File: rfc822.c

int rfc822_valid_msgid (const char *msgid)
{
  /* msg-id         = "<" addr-spec ">"
   * addr-spec      = local-part "@" domain
   * local-part     = word *("." word)
   * word           = atom / quoted-string
   * atom           = 1*<any CHAR except specials, SPACE and CTLs>
   * CHAR           = ( 0.-127. )
   * specials       = "(" / ")" / "<" / ">" / "@"
                    / "," / ";" / ":" / "\" / <">
		    / "." / "[" / "]"
   * SPACE          = ( 32. )
   * CTLS           = ( 0.-31., 127.)
   * quoted-string  = <"> *(qtext/quoted-pair) <">
   * qtext          = <any CHAR except <">, "\" and CR>
   * CR             = ( 13. )
   * quoted-pair    = "\" CHAR
   * domain         = sub-domain *("." sub-domain)
   * sub-domain     = domain-ref / domain-literal
   * domain-ref     = atom
   * domain-literal = "[" *(dtext / quoted-pair) "]"
   */

  unsigned int l, i;

  if (!msgid || !*msgid)
    return -1;

  l = mutt_strlen (msgid);
  if (l < 5) /* <atom@atom> */
    return -1;
  if (msgid[0] != '<' || msgid[l-1] != '>')
    return -1;
  if (!(strrchr (msgid, '@')))
    return -1;

  /* TODO: complete parser */
  for (i = 0; i < l; i++)
    if ((unsigned char)msgid[i] > 127)
      return -1;

  return 0;
}

Project: mutt
File: rfc822.c

ADDRESS *rfc822_cpy_adr (ADDRESS *addr, int prune)
{
  ADDRESS *top = NULL, *last = NULL;

  for (; addr; addr = addr->next)
  {
    if (prune && addr->group && (!addr->next || !addr->next->mailbox))
    {
      /* ignore this element of the list */
    }
    else if (last)
    {
      last->next = rfc822_cpy_adr_real (addr);
      last = last->next;
    }
    else
      top = last = rfc822_cpy_adr_real (addr);
  }
  return top;
}