Commit message: Recast more internal string routines to use growable-strings
CVE: CVE-2019-16928
Vulnerability description: Exim 4.92 through 4.92.2 allows remote code execution, a different vulnerability than CVE-2019-15846. There is a heap-based buffer overflow in string_vformat in string.c involving a long EHLO command.

Project: exim
File: deliver.c

deliver_split_address(address_item * addr)
{
uschar * address = addr->address;
uschar * domain;
uschar * t;
int len;

if (!(domain = Ustrrchr(address, '@')))
  return DEFER;		/* should always have a domain, but just in case... */

len = domain - address;
addr->domain = string_copylc(domain+1);    /* Domains are always caseless */

/* The implication in the RFCs (though I can't say I've seen it spelled out
explicitly) is that quoting should be removed from local parts at the point
where they are locally interpreted. [The new draft "821" is more explicit on
this, Jan 1999.] We know the syntax is valid, so this can be done by simply
removing quoting backslashes and any unquoted doublequotes. */

t = addr->cc_local_part = store_get(len+1);
while(len-- > 0)
  {
  int c = *address++;
  if (c == '\"') continue;
  if (c == '\\')
    {
    *t++ = *address++;
    len--;
    }
  else *t++ = c;
  }
*t = 0;

/* We do the percent hack only for those domains that are listed in
percent_hack_domains. A loop is required, to copy with multiple %-hacks. */

if (percent_hack_domains)
  {
  int rc;
  uschar *new_address = NULL;
  uschar *local_part = addr->cc_local_part;

  deliver_domain = addr->domain;  /* set $domain */

  while (  (rc = match_isinlist(deliver_domain, (const uschar **)&percent_hack_domains, 0,
	       &domainlist_anchor, addr->domain_cache, MCL_DOMAIN, TRUE, NULL))
             == OK
	&& (t = Ustrrchr(local_part, '%')) != NULL
	)
    {
    new_address = string_copy(local_part);
    new_address[t - local_part] = '@';
    deliver_domain = string_copylc(t+1);
    local_part = string_copyn(local_part, t - local_part);
    }

  if (rc == DEFER) return DEFER;   /* lookup deferred */

  /* If hackery happened, set up new parent and alter the current address. */

  if (new_address)
    {
    address_item *new_parent = store_get(sizeof(address_item));
    *new_parent = *addr;
    addr->parent = new_parent;
    new_parent->child_count = 1;
    addr->address = new_address;
    addr->unique = string_copy(new_address);
    addr->domain = deliver_domain;
    addr->cc_local_part = local_part;
    DEBUG(D_deliver) debug_printf("%%-hack changed address to: %s\n",
      addr->address);
    }
  }

/* Create the lowercased version of the final local part, and make that the
default one to be used. */

addr->local_part = addr->lc_local_part = string_copylc(addr->cc_local_part);
return OK;
}

Project: exim

acl_warn(int where, uschar *user_message, uschar *log_message)
{
if (log_message != NULL && log_message != user_message)
  {
  uschar *text;
  string_item *logged;

  text = string_sprintf("%s Warning: %s",  host_and_ident(TRUE),
    string_printing(log_message));

  /* If a sender verification has failed, and the log message is "sender verify
  failed", add the failure message. */

  if (sender_verified_failed != NULL &&
      sender_verified_failed->message != NULL &&
      strcmpic(log_message, US"sender verify failed") == 0)
    text = string_sprintf("%s: %s", text, sender_verified_failed->message);

  /* Search previously logged warnings. They are kept in malloc
  store so they can be freed at the start of a new message. */

  for (logged = acl_warn_logged; logged != NULL; logged = logged->next)
    if (Ustrcmp(logged->text, text) == 0) break;

  if (logged == NULL)
    {
    int length = Ustrlen(text) + 1;
    log_write(0, LOG_MAIN, "%s", text);
    logged = store_malloc(sizeof(string_item) + length);
    logged->text = US logged + sizeof(string_item);
    memcpy(logged->text, text, length);
    logged->next = acl_warn_logged;
    acl_warn_logged = logged;
    }
  }

/* If there's no user message, we are done. */

if (user_message == NULL) return;

/* If this isn't a message ACL, we can't do anything with a user message.
Log an error. */

if (where > ACL_WHERE_NOTSMTP)
  {
  log_write(0, LOG_MAIN|LOG_PANIC, "ACL \"warn\" with \"message\" setting "
    "found in a non-message (%s) ACL: cannot specify header lines here: "
    "message ignored", acl_wherenames[where]);
  return;
  }

/* The code for setting up header lines is now abstracted into a separate
function so that it can be used for the add_header modifier as well. */

setup_header(user_message);
}

Project: exim

transport_pass_socket(const uschar *transport_name, const uschar *hostname,
  const uschar *hostaddress, uschar *id, int socket_fd)
{
pid_t pid;
int status;

DEBUG(D_transport) debug_printf("transport_pass_socket entered\n");

if ((pid = fork()) == 0)
  {
  /* Disconnect entirely from the parent process. If we are running in the
  test harness, wait for a bit to allow the previous process time to finish,
  write the log, etc., so that the output is always in the same order for
  automatic comparison. */

  if ((pid = fork()) != 0)
    {
    DEBUG(D_transport) debug_printf("transport_pass_socket succeeded (final-pid %d)\n", pid);
    _exit(EXIT_SUCCESS);
    }
  if (f.running_in_test_harness) sleep(1);

  transport_do_pass_socket(transport_name, hostname, hostaddress,
    id, socket_fd);
  }

/* If the process creation succeeded, wait for the first-level child, which
immediately exits, leaving the second level process entirely disconnected from
this one. */

if (pid > 0)
  {
  int rc;
  while ((rc = wait(&status)) != pid && (rc >= 0 || errno != ECHILD));
  DEBUG(D_transport) debug_printf("transport_pass_socket succeeded (inter-pid %d)\n", pid);
  return TRUE;
  }
else
  {
  DEBUG(D_transport) debug_printf("transport_pass_socket failed to fork: %s\n",
    strerror(errno));
  return FALSE;
  }
}

Project: exim

lmtp_write_command(int fd, const char *format, ...)
{
gstring gs = { .size = big_buffer_size, .ptr = 0, .s = big_buffer };
int rc;
va_list ap;

va_start(ap, format);
if (!string_vformat(&gs, FALSE, CS format, ap))
  {
  va_end(ap);
  errno = ERRNO_SMTPFORMAT;
  return FALSE;
  }
va_end(ap);
DEBUG(D_transport|D_v) debug_printf("  LMTP>> %s", string_from_gstring(&gs));
rc = write(fd, gs.s, gs.ptr);
gs.ptr -= 2; string_from_gstring(&gs); /* remove \r\n for debug and error message */
if (rc > 0) return TRUE;
DEBUG(D_transport) debug_printf("write failed: %s\n", strerror(errno));
return FALSE;
}

Project: exim

host_find_byname(host_item *host, const uschar *ignore_target_hosts, int flags,
  const uschar **fully_qualified_name, BOOL local_host_check)
{
int i, yield, times;
uschar **addrlist;
host_item *last = NULL;
BOOL temp_error = FALSE;
#if HAVE_IPV6
int af;
#endif

/* Make sure DNS options are set as required. This appears to be necessary in
some circumstances when the get..byname() function actually calls the DNS. */

dns_init((flags & HOST_FIND_QUALIFY_SINGLE) != 0,
         (flags & HOST_FIND_SEARCH_PARENTS) != 0,
	 FALSE);		/* Cannot retrieve dnssec status so do not request */

/* In an IPv6 world, unless IPv6 has been disabled, we need to scan for both
kinds of address, so go round the loop twice. Note that we have ensured that
AF_INET6 is defined even in an IPv4 world, which makes for slightly tidier
code. However, if dns_ipv4_lookup matches the domain, we also just do IPv4
lookups here (except when testing standalone). */

#if HAVE_IPV6
  #ifdef STAND_ALONE
  if (disable_ipv6)
  #else
  if (disable_ipv6 ||
    (dns_ipv4_lookup != NULL &&
        match_isinlist(host->name, CUSS &dns_ipv4_lookup, 0, NULL, NULL,
	  MCL_DOMAIN, TRUE, NULL) == OK))
  #endif

    { af = AF_INET; times = 1; }
  else
    { af = AF_INET6; times = 2; }

/* No IPv6 support */

#else   /* HAVE_IPV6 */
  times = 1;
#endif  /* HAVE_IPV6 */

/* Initialize the flag that gets set for DNS syntax check errors, so that the
interface to this function can be similar to host_find_bydns. */

f.host_find_failed_syntax = FALSE;

/* Loop to look up both kinds of address in an IPv6 world */

for (i = 1; i <= times;
     #if HAVE_IPV6
       af = AF_INET,     /* If 2 passes, IPv4 on the second */
     #endif
     i++)
  {
  BOOL ipv4_addr;
  int error_num = 0;
  struct hostent *hostdata;
  unsigned long time_msec = 0;	/* compiler quietening */

  #ifdef STAND_ALONE
  printf("Looking up: %s\n", host->name);
  #endif

  if (slow_lookup_log) time_msec = get_time_in_ms();

  #if HAVE_IPV6
  if (f.running_in_test_harness)
    hostdata = host_fake_gethostbyname(host->name, af, &error_num);
  else
    {
    #if HAVE_GETIPNODEBYNAME
    hostdata = getipnodebyname(CS host->name, af, 0, &error_num);
    #else
    hostdata = gethostbyname2(CS host->name, af);
    error_num = h_errno;
    #endif
    }

  #else    /* not HAVE_IPV6 */
  if (f.running_in_test_harness)
    hostdata = host_fake_gethostbyname(host->name, AF_INET, &error_num);
  else
    {
    hostdata = gethostbyname(CS host->name);
    error_num = h_errno;
    }
  #endif   /* HAVE_IPV6 */

  if (   slow_lookup_log
      && (time_msec = get_time_in_ms() - time_msec) > slow_lookup_log)
    log_long_lookup(US"name", host->name, time_msec);

  if (hostdata == NULL)
    {
    uschar *error;
    switch (error_num)
      {
      case HOST_NOT_FOUND: error = US"HOST_NOT_FOUND"; break;
      case TRY_AGAIN:      error = US"TRY_AGAIN"; break;
      case NO_RECOVERY:    error = US"NO_RECOVERY"; break;
      case NO_DATA:        error = US"NO_DATA"; break;
      #if NO_DATA != NO_ADDRESS
      case NO_ADDRESS:     error = US"NO_ADDRESS"; break;
      #endif
      default: error = US"?"; break;
      }

    DEBUG(D_host_lookup) debug_printf("%s returned %d (%s)\n",
      #if HAVE_IPV6
        #if HAVE_GETIPNODEBYNAME
        (af == AF_INET6)? "getipnodebyname(af=inet6)" : "getipnodebyname(af=inet)",
        #else
        (af == AF_INET6)? "gethostbyname2(af=inet6)" : "gethostbyname2(af=inet)",
        #endif
      #else
      "gethostbyname",
      #endif
      error_num, error);

    if (error_num == TRY_AGAIN || error_num == NO_RECOVERY) temp_error = TRUE;
    continue;
    }
  if ((hostdata->h_addr_list)[0] == NULL) continue;

  /* Replace the name with the fully qualified one if necessary, and fill in
  the fully_qualified_name pointer. */

  if (hostdata->h_name[0] != 0 &&
      Ustrcmp(host->name, hostdata->h_name) != 0)
    host->name = string_copy_dnsdomain(US hostdata->h_name);
  if (fully_qualified_name != NULL) *fully_qualified_name = host->name;

  /* Get the list of addresses. IPv4 and IPv6 addresses can be distinguished
  by their different lengths. Scan the list, ignoring any that are to be
  ignored, and build a chain from the rest. */

  ipv4_addr = hostdata->h_length == sizeof(struct in_addr);

  for (addrlist = USS hostdata->h_addr_list; *addrlist != NULL; addrlist++)
    {
    uschar *text_address =
      host_ntoa(ipv4_addr? AF_INET:AF_INET6, *addrlist, NULL, NULL);

    #ifndef STAND_ALONE
    if (ignore_target_hosts != NULL &&
        verify_check_this_host(&ignore_target_hosts, NULL, host->name,
          text_address, NULL) == OK)
      {
      DEBUG(D_host_lookup)
        debug_printf("ignored host %s [%s]\n", host->name, text_address);
      continue;
      }
    #endif

    /* If this is the first address, last == NULL and we put the data in the
    original block. */

    if (last == NULL)
      {
      host->address = text_address;
      host->port = PORT_NONE;
      host->status = hstatus_unknown;
      host->why = hwhy_unknown;
      host->dnssec = DS_UNK;
      last = host;
      }

    /* Else add further host item blocks for any other addresses, keeping
    the order. */

    else
      {
      host_item *next = store_get(sizeof(host_item));
      next->name = host->name;
      next->mx = host->mx;
      next->address = text_address;
      next->port = PORT_NONE;
      next->status = hstatus_unknown;
      next->why = hwhy_unknown;
      next->dnssec = DS_UNK;
      next->last_try = 0;
      next->next = last->next;
      last->next = next;
      last = next;
      }
    }
  }

/* If no hosts were found, the address field in the original host block will be
NULL. If temp_error is set, at least one of the lookups gave a temporary error,
so we pass that back. */

if (host->address == NULL)
  {
  uschar *msg =
    #ifndef STAND_ALONE
    (message_id[0] == 0 && smtp_in != NULL)?
      string_sprintf("no IP address found for host %s (during %s)", host->name,
          smtp_get_connection_info()) :
    #endif
    string_sprintf("no IP address found for host %s", host->name);

  HDEBUG(D_host_lookup) debug_printf("%s\n", msg);
  if (temp_error) goto RETURN_AGAIN;
  if (host_checking || !f.log_testing_mode)
    log_write(L_host_lookup_failed, LOG_MAIN, "%s", msg);
  return HOST_FIND_FAILED;
  }

/* Remove any duplicate IP addresses, then check to see if this is the local
host if required. */

host_remove_duplicates(host, &last);
yield = local_host_check?
  host_scan_for_local_hosts(host, &last, NULL) : HOST_FOUND;

HDEBUG(D_host_lookup)
  {
  const host_item *h;
  if (fully_qualified_name != NULL)
    debug_printf("fully qualified name = %s\n", *fully_qualified_name);
  debug_printf("%s looked up these IP addresses:\n",
    #if HAVE_IPV6
      #if HAVE_GETIPNODEBYNAME
      "getipnodebyname"
      #else
      "gethostbyname2"
      #endif
    #else
    "gethostbyname"
    #endif
    );
  for (h = host; h != last->next; h = h->next)
    debug_printf("  name=%s address=%s\n", h->name,
      (h->address == NULL)? US"<null>" : h->address);
  }

/* Return the found status. */

return yield;

/* Handle the case when there is a temporary error. If the name matches
dns_again_means_nonexist, return permanent rather than temporary failure. */

RETURN_AGAIN:
  {
  #ifndef STAND_ALONE
  int rc;
  const uschar *save = deliver_domain;
  deliver_domain = host->name;  /* set $domain */
  rc = match_isinlist(host->name, CUSS &dns_again_means_nonexist, 0, NULL, NULL,
    MCL_DOMAIN, TRUE, NULL);
  deliver_domain = save;
  if (rc == OK)
    {
    DEBUG(D_host_lookup) debug_printf("%s is in dns_again_means_nonexist: "
      "returning HOST_FIND_FAILED\n", host->name);
    return HOST_FIND_FAILED;
    }
  #endif
  return HOST_FIND_AGAIN;
  }
}

Project: exim
File: deliver.c

address_done(address_item *addr, uschar *now)
{
address_item *dup;

update_spool = TRUE;        /* Ensure spool gets updated */

/* Top-level address */

if (!addr->parent)
  {
  tree_add_nonrecipient(addr->unique);
  tree_add_nonrecipient(addr->address);
  }

/* Homonymous child address */

else if (testflag(addr, af_homonym))
  {
  if (addr->transport)
    tree_add_nonrecipient(
      string_sprintf("%s/%s", addr->unique + 3, addr->transport->name));
  }

/* Non-homonymous child address */

else tree_add_nonrecipient(addr->unique);

/* Check the list of duplicate addresses and ensure they are now marked
done as well. */

for (dup = addr_duplicate; dup; dup = dup->next)
  if (Ustrcmp(addr->unique, dup->unique) == 0)
    {
    tree_add_nonrecipient(dup->unique);
    child_done(dup, now);
    }
}

Project: exim

log_close_all(void)
{
if (mainlogfd >= 0)
  { (void)close(mainlogfd); mainlogfd = -1; }
if (rejectlogfd >= 0)
  { (void)close(rejectlogfd); rejectlogfd = -1; }
closelog();
syslog_open = FALSE;
}

Project: exim

function_dummy_free(void *block) { block = block; }

Project: exim
File: deliver.c

sort_remote_deliveries(void)
{
int sep = 0;
address_item **aptr = &addr_remote;
const uschar *listptr = remote_sort_domains;
uschar *pattern;
uschar patbuf[256];

while (  *aptr
      && (pattern = string_nextinlist(&listptr, &sep, patbuf, sizeof(patbuf)))
      )
  {
  address_item *moved = NULL;
  address_item **bptr = &moved;

  while (*aptr)
    {
    address_item **next;
    deliver_domain = (*aptr)->domain;   /* set $domain */
    if (match_isinlist(deliver_domain, (const uschar **)&pattern, UCHAR_MAX+1,
          &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL) == OK)
      {
      aptr = &(*aptr)->next;
      continue;
      }

    next = &(*aptr)->next;
    while (  *next
	  && (deliver_domain = (*next)->domain,  /* Set $domain */
            match_isinlist(deliver_domain, (const uschar **)&pattern, UCHAR_MAX+1,
              &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL)) != OK
	  )
      next = &(*next)->next;

    /* If the batch of non-matchers is at the end, add on any that were
    extracted further up the chain, and end this iteration. Otherwise,
    extract them from the chain and hang on the moved chain. */

    if (!*next)
      {
      *next = moved;
      break;
      }

    *bptr = *aptr;
    *aptr = *next;
    *next = NULL;
    bptr = next;
    aptr = &(*aptr)->next;
    }

  /* If the loop ended because the final address matched, *aptr will
  be NULL. Add on to the end any extracted non-matching addresses. If
  *aptr is not NULL, the loop ended via "break" when *next is null, that
  is, there was a string of non-matching addresses at the end. In this
  case the extracted addresses have already been added on the end. */

  if (!*aptr) *aptr = moved;
  }

DEBUG(D_deliver)
  {
  address_item *addr;
  debug_printf("remote addresses after sorting:\n");
  for (addr = addr_remote; addr; addr = addr->next)
    debug_printf("  %s\n", addr->address);
  }
}

Project: exim

set_readline(char * (**fn_readline_ptr)(const char *),
             void   (**fn_addhist_ptr)(const char *))
{
void *dlhandle;
void *dlhandle_curses = dlopen("libcurses." DYNLIB_FN_EXT, RTLD_GLOBAL|RTLD_LAZY);

dlhandle = dlopen("libreadline." DYNLIB_FN_EXT, RTLD_GLOBAL|RTLD_NOW);
if (dlhandle_curses != NULL) dlclose(dlhandle_curses);

if (dlhandle != NULL)
  {
  /* Checked manual pages; at least in GNU Readline 6.1, the prototypes are:
   *   char * readline (const char *prompt);
   *   void add_history (const char *string);
   */
  *fn_readline_ptr = (char *(*)(const char*))dlsym(dlhandle, "readline");
  *fn_addhist_ptr = (void(*)(const char*))dlsym(dlhandle, "add_history");
  }
else
  {
  DEBUG(D_any) debug_printf("failed to load readline: %s\n", dlerror());
  }

return dlhandle;
}

Project: exim

debug_print_ids(uschar *s)
{
debug_printf("%s uid=%ld gid=%ld euid=%ld egid=%ld\n", s,
  (long int)getuid(), (long int)getgid(), (long int)geteuid(),
  (long int)getegid());
}

Project: exim

setup_header(const uschar *hstring)
{
const uschar *p, *q;
int hlen = Ustrlen(hstring);

/* Ignore any leading newlines */
while (*hstring == '\n') hstring++, hlen--;

/* An empty string does nothing; ensure exactly one final newline. */
if (hlen <= 0) return;
if (hstring[--hlen] != '\n')		/* no newline */
  q = string_sprintf("%s\n", hstring);
else if (hstring[hlen-1] == '\n')	/* double newline */
  {
  uschar * s = string_copy(hstring);
  while(s[--hlen] == '\n')
    s[hlen+1] = '\0';
  q = s;
  }
else
  q = hstring;

/* Loop for multiple header lines, taking care about continuations */

for (p = q; *p; p = q)
  {
  const uschar *s;
  uschar * hdr;
  int newtype = htype_add_bot;
  header_line **hptr = &acl_added_headers;

  /* Find next header line within the string */

  for (;;)
    {
    q = Ustrchr(q, '\n');		/* we know there was a newline */
    if (*++q != ' ' && *q != '\t') break;
    }

  /* If the line starts with a colon, interpret the instruction for where to
  add it. This temporarily sets up a new type. */

  if (*p == ':')
    {
    if (strncmpic(p, US":after_received:", 16) == 0)
      {
      newtype = htype_add_rec;
      p += 16;
      }
    else if (strncmpic(p, US":at_start_rfc:", 14) == 0)
      {
      newtype = htype_add_rfc;
      p += 14;
      }
    else if (strncmpic(p, US":at_start:", 10) == 0)
      {
      newtype = htype_add_top;
      p += 10;
      }
    else if (strncmpic(p, US":at_end:", 8) == 0)
      {
      newtype = htype_add_bot;
      p += 8;
      }
    while (*p == ' ' || *p == '\t') p++;
    }

  /* See if this line starts with a header name, and if not, add X-ACL-Warn:
  to the front of it. */

  for (s = p; s < q - 1; s++)
    if (*s == ':' || !isgraph(*s)) break;

  hdr = string_sprintf("%s%.*s", *s == ':' ? "" : "X-ACL-Warn: ", (int) (q - p), p);
  hlen = Ustrlen(hdr);

  /* See if this line has already been added */

  while (*hptr)
    {
    if (Ustrncmp((*hptr)->text, hdr, hlen) == 0) break;
    hptr = &(*hptr)->next;
    }

  /* Add if not previously present */

  if (!*hptr)
    {
    header_line *h = store_get(sizeof(header_line));
    h->text = hdr;
    h->next = NULL;
    h->type = newtype;
    h->slen = hlen;
    *hptr = h;
    hptr = &h->next;
    }
  }
}