SPDX: license tags (mostly by guesswork)
[exim.git] / src / src / rda.c
index 443cbb1bdcd64103217a7d36b96ed9c59e0ea486..1bc322e341f0d6550b2a201ccc491d9a14726479 100644 (file)
@@ -1,11 +1,11 @@
-/* $Cambridge: exim/src/src/rda.c,v 1.2 2004/11/04 10:42:11 ph10 Exp $ */
-
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2004 */
+/* Copyright (c) The Exim maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This module contains code for extracting addresses from a forwarding list
 (from an alias or forward file) or by running the filter interpreter. It may do
@@ -43,15 +43,15 @@ Returns:   FILTER_EXIM    if it starts with "# Exim filter"
 static BOOL
 match_tag(const uschar *s, const uschar *tag)
 {
-for (; *tag != 0; s++, tag++)
-  {
+for (; *tag; s++, tag++)
   if (*tag == ' ')
     {
     while (*s == ' ' || *s == '\t') s++;
     s--;
     }
-  else if (tolower(*s) != tolower(*tag)) break;
-  }
+  else
+   if (tolower(*s) != tolower(*tag)) break;
+
 return (*tag == 0);
 }
 
@@ -61,10 +61,10 @@ tags for other types of filter. */
 int
 rda_is_filter(const uschar *s)
 {
-while (isspace(*s)) s++;     /* Skips initial blank lines */
-if (match_tag(s, CUS"# exim filter")) return FILTER_EXIM;
-  else if (match_tag(s, CUS"# sieve filter")) return FILTER_SIEVE;
-    else return FILTER_FORWARD;
+Uskip_whitespace(&s);                  /* Skips initial blank lines */
+if (match_tag(s, CUS"# exim filter"))          return FILTER_EXIM;
+else if (match_tag(s, CUS"# sieve filter"))    return FILTER_SIEVE;
+else                                           return FILTER_FORWARD;
 }
 
 
@@ -98,37 +98,37 @@ static int
 rda_exists(uschar *filename, uschar **error)
 {
 int rc, saved_errno;
-uschar *slash;
 struct stat statbuf;
+uschar * s;
 
 if ((rc = Ustat(filename, &statbuf)) >= 0) return FILE_EXIST;
 saved_errno = errno;
 
-Ustrncpy(big_buffer, filename, big_buffer_size - 3);
+s = string_copy(filename);
 sigalrm_seen = FALSE;
 
 if (saved_errno == ENOENT)
   {
-  slash = Ustrrchr(big_buffer, '/');
-  Ustrcpy(slash+1, ".");
+  uschar * slash = Ustrrchr(s, '/');
+  Ustrcpy(slash+1, US".");
 
-  alarm(30);
-  rc = Ustat(big_buffer, &statbuf);
+  ALARM(30);
+  rc = Ustat(s, &statbuf);
   if (rc != 0 && errno == EACCES && !sigalrm_seen)
     {
     *slash = 0;
-    rc = Ustat(big_buffer, &statbuf);
+    rc = Ustat(s, &statbuf);
     }
   saved_errno = errno;
-  alarm(0);
+  ALARM_CLR(0);
 
-  DEBUG(D_route) debug_printf("stat(%s)=%d\n", big_buffer, rc);
+  DEBUG(D_route) debug_printf("stat(%s)=%d\n", s, rc);
   }
 
 if (sigalrm_seen || rc != 0)
   {
-  *error = string_sprintf("failed to stat %s (%s)", big_buffer,
-    sigalrm_seen? "timeout" : strerror(saved_errno));
+  *error = string_sprintf("failed to stat %s (%s)", s,
+    sigalrm_seen?  "timeout" : strerror(saved_errno));
   return FILE_EXIST_UNCLEAR;
   }
 
@@ -167,7 +167,7 @@ Returns:      pointer to string in store; NULL on error
 */
 
 static uschar *
-rda_get_file_contents(redirect_block *rdata, int options, uschar **error,
+rda_get_file_contents(const redirect_block *rdata, int options, uschar **error,
   int *yield)
 {
 FILE *fwd;
@@ -177,6 +177,17 @@ BOOL uid_ok = !rdata->check_owner;
 BOOL gid_ok = !rdata->check_group;
 struct stat statbuf;
 
+/* Reading a file is a form of expansion; we wish to deny attackers the
+capability to specify the file name. */
+
+if (is_tainted(filename))
+  {
+  *error = string_sprintf("Tainted name '%s' for file read not permitted\n",
+                       filename);
+  *yield = FF_ERROR;
+  return NULL;
+  }
+
 /* Attempt to open the file. If it appears not to exist, check up on the
 containing directory by statting it. If the directory does not exist, we treat
 this situation as an error (which will cause delivery to defer); otherwise we
@@ -186,40 +197,35 @@ However, if the ignore_enotdir option is set (to ignore "something on the
 path is not a directory" errors), the right behaviour seems to be not to do the
 directory test. */
 
-fwd = Ufopen(filename, "rb");
-if (fwd == NULL)
+if (!(fwd = Ufopen(filename, "rb"))) switch(errno)
   {
-  switch(errno)
-    {
-    case ENOENT:          /* File does not exist */
+  case ENOENT:          /* File does not exist */
     DEBUG(D_route) debug_printf("%s does not exist\n%schecking parent directory\n",
-      filename,
-      ((options & RDO_ENOTDIR) != 0)? "ignore_enotdir set => skip " : "");
-    *yield = (((options & RDO_ENOTDIR) != 0) ||
-              rda_exists(filename, error) == FILE_NOT_EXIST)?
-      FF_NONEXIST : FF_ERROR;
+      filename, options & RDO_ENOTDIR ? "ignore_enotdir set => skip " : "");
+    *yield =
+       options & RDO_ENOTDIR || rda_exists(filename, error) == FILE_NOT_EXIST
+       ? FF_NONEXIST : FF_ERROR;
     return NULL;
 
-    case ENOTDIR:         /* Something on the path isn't a directory */
-    if ((options & RDO_ENOTDIR) == 0) goto DEFAULT_ERROR;
+  case ENOTDIR:         /* Something on the path isn't a directory */
+    if (!(options & RDO_ENOTDIR)) goto DEFAULT_ERROR;
     DEBUG(D_route) debug_printf("non-directory on path %s: file assumed not to "
       "exist\n", filename);
     *yield = FF_NONEXIST;
     return NULL;
 
-    case EACCES:           /* Permission denied */
-    if ((options & RDO_EACCES) == 0) goto DEFAULT_ERROR;
+  case EACCES:           /* Permission denied */
+    if (!(options & RDO_EACCES)) goto DEFAULT_ERROR;
     DEBUG(D_route) debug_printf("permission denied for %s: file assumed not to "
       "exist\n", filename);
     *yield = FF_NONEXIST;
     return NULL;
 
-    DEFAULT_ERROR:
-    default:
-    *error = string_open_failed(errno, "%s", filename);
+DEFAULT_ERROR:
+  default:
+    *error = string_open_failed("%s", filename);
     *yield = FF_ERROR;
     return NULL;
-    }
   }
 
 /* Check that we have a regular file. */
@@ -248,28 +254,18 @@ if ((statbuf.st_mode & rdata->modemask) != 0)
 /* Check the file owner and file group if required to do so. */
 
 if (!uid_ok)
-  {
-  if (rdata->pw != NULL && statbuf.st_uid == rdata->pw->pw_uid)
+  if (rdata->pw && statbuf.st_uid == rdata->pw->pw_uid)
     uid_ok = TRUE;
-  else if (rdata->owners != NULL)
-    {
-    int i;
-    for (i = 1; i <= (int)(rdata->owners[0]); i++)
+  else if (rdata->owners)
+    for (int i = 1; i <= (int)(rdata->owners[0]); i++)
       if (rdata->owners[i] == statbuf.st_uid) { uid_ok = TRUE; break; }
-    }
-  }
 
 if (!gid_ok)
-  {
-  if (rdata->pw != NULL && statbuf.st_gid == rdata->pw->pw_gid)
+  if (rdata->pw && statbuf.st_gid == rdata->pw->pw_gid)
     gid_ok = TRUE;
-  else if (rdata->owngroups != NULL)
-    {
-    int i;
-    for (i = 1; i <= (int)(rdata->owngroups[0]); i++)
+  else if (rdata->owngroups)
+    for (int i = 1; i <= (int)(rdata->owngroups[0]); i++)
       if (rdata->owngroups[i] == statbuf.st_gid) { gid_ok = TRUE; break; }
-    }
-  }
 
 if (!uid_ok || !gid_ok)
   {
@@ -289,7 +285,7 @@ if (statbuf.st_size > MAX_FILTER_SIZE)
 
 /* Read the file in one go in order to minimize the time we have it open. */
 
-filebuf = store_get(statbuf.st_size + 1);
+filebuf = store_get(statbuf.st_size + 1, filename);
 
 if (fread(filebuf, 1, statbuf.st_size, fwd) != statbuf.st_size)
   {
@@ -299,23 +295,17 @@ if (fread(filebuf, 1, statbuf.st_size, fwd) != statbuf.st_size)
   }
 filebuf[statbuf.st_size] = 0;
 
-/* Don't pass statbuf.st_size directly to debug_printf. On some systems it
-is a long, which may not be the same as an int. */
-
-DEBUG(D_route)
-  {
-  int size = (int)statbuf.st_size;
-  debug_printf("%d bytes read from %s\n", size, filename);
-  }
+DEBUG(D_route) debug_printf(OFF_T_FMT " %sbytes read from %s\n",
+  statbuf.st_size, is_tainted(filename) ? "(tainted) " : "", filename);
 
-fclose(fwd);
+(void)fclose(fwd);
 return filebuf;
 
 /* Return an error: the string is already set up. */
 
 ERROR_RETURN:
 *yield = FF_ERROR;
-fclose(fwd);
+(void)fclose(fwd);
 return NULL;
 }
 
@@ -333,6 +323,9 @@ Arguments:
   options                   the options bits
   include_directory         restrain to this directory
   sieve_vacation_directory  passed to sieve_interpret
+  sieve_enotify_mailto_owner passed to sieve_interpret
+  sieve_useraddress         passed to sieve_interpret
+  sieve_subaddress          passed to sieve_interpret
   generated                 where to hang generated addresses
   error                     for error messages
   eblockp                   for details of skipped syntax errors
@@ -347,21 +340,23 @@ Returns:                    a suitable return for rda_interpret()
 */
 
 static int
-rda_extract(redirect_block *rdata, int options, uschar *include_directory,
-  uschar *sieve_vacation_directory, address_item **generated, uschar **error,
-  error_block **eblockp, int *filtertype)
+rda_extract(const redirect_block * rdata, int options,
+  const uschar * include_directory, const uschar * sieve_vacation_directory,
+  const uschar * sieve_enotify_mailto_owner, const uschar * sieve_useraddress,
+  const uschar * sieve_subaddress, address_item ** generated, uschar ** error,
+  error_block ** eblockp, int * filtertype)
 {
-uschar *data;
+const uschar * data;
 
 if (rdata->isfile)
   {
-  int yield;
-  data = rda_get_file_contents(rdata, options, error, &yield);
-  if (data == NULL) return yield;
+  int yield = 0;
+  if (!(data = rda_get_file_contents(rdata, options, error, &yield)))
+    return yield;
   }
 else data = rdata->string;
 
-*filtertype = system_filtering? FILTER_EXIM : rda_is_filter(data);
+*filtertype = f.system_filtering ? FILTER_EXIM : rda_is_filter(data);
 
 /* Filter interpretation is done by a general function that is also called from
 the filter testing option (-bf). There are two versions: one for Exim filtering
@@ -375,41 +370,41 @@ if (*filtertype != FILTER_FORWARD)
   int old_expand_forbid = expand_forbid;
 
   DEBUG(D_route) debug_printf("data is %s filter program\n",
-    (*filtertype == FILTER_EXIM)? "an Exim" : "a Sieve");
+    *filtertype == FILTER_EXIM ? "an Exim" : "a Sieve");
 
   /* RDO_FILTER is an "allow" bit */
-   
-  if ((options & RDO_FILTER) == 0)
+
+  if (!(options & RDO_FILTER))
     {
     *error = US"filtering not enabled";
     return FF_ERROR;
     }
 
   expand_forbid =
-    (expand_forbid & ~RDO_FILTER_EXPANSIONS) |
-    (options & RDO_FILTER_EXPANSIONS);
-  
+    expand_forbid & ~RDO_FILTER_EXPANSIONS  |  options & RDO_FILTER_EXPANSIONS;
+
   /* RDO_{EXIM,SIEVE}_FILTER are forbid bits */
-   
+
   if (*filtertype == FILTER_EXIM)
     {
     if ((options & RDO_EXIM_FILTER) != 0)
       {
       *error = US"Exim filtering not enabled";
       return FF_ERROR;
-      }    
+      }
     frc = filter_interpret(data, options, generated, error);
-    }  
+    }
   else
     {
-    if ((options & RDO_SIEVE_FILTER) != 0)
+    if (options & RDO_SIEVE_FILTER)
       {
       *error = US"Sieve filtering not enabled";
       return FF_ERROR;
       }
-    frc = sieve_interpret(data, options, sieve_vacation_directory, generated, 
-      error);
-    }   
+    frc = sieve_interpret(data, options, sieve_vacation_directory,
+      sieve_enotify_mailto_owner, sieve_useraddress, sieve_subaddress,
+      generated, error);
+    }
 
   expand_forbid = old_expand_forbid;
   return frc;
@@ -435,22 +430,24 @@ return parse_forward_list(data,
 *         Write string down pipe                 *
 *************************************************/
 
-/* This function is used for tranferring a string down a pipe between
+/* This function is used for transferring a string down a pipe between
 processes. If the pointer is NULL, a length of zero is written.
 
 Arguments:
   fd         the pipe
   s          the string
 
-Returns:     nothing
+Returns:     -1 on error, else 0
 */
 
-static void
-rda_write_string(int fd, uschar *s)
+static int
+rda_write_string(int fd, const uschar *s)
 {
-int len = (s == NULL)? 0 : Ustrlen(s) + 1;
-write(fd, &len, sizeof(int));
-if (s != NULL) write(fd, s, len);
+int len = s ? Ustrlen(s) + 1 : 0;
+return (  write(fd, &len, sizeof(int)) != sizeof(int)
+       || (s   &&  write(fd, s, len) != len)
+       )
+       ? -1 : 0;
 }
 
 
@@ -474,11 +471,13 @@ rda_read_string(int fd, uschar **sp)
 int len;
 
 if (read(fd, &len, sizeof(int)) != sizeof(int)) return FALSE;
-if (len == 0) *sp = NULL; else
-  {
-  *sp = store_get(len);
-  if (read(fd, *sp, len) != len) return FALSE;
-  }
+if (len == 0)
+  *sp = NULL;
+else
+  /* We know we have enough memory so disable the error on "len" */
+  /* coverity[tainted_data] */
+  /* We trust the data source, so untainted */
+  if (read(fd, *sp = store_get(len, GET_UNTAINTED), len) != len) return FALSE;
 return TRUE;
 }
 
@@ -491,7 +490,7 @@ return TRUE;
 /* This function is passed a forward list string (unexpanded) or the name of a
 file (unexpanded) whose contents are the forwarding list. The list may in fact
 be a filter program if it starts with "#Exim filter" or "#Sieve filter". Other
-types of filter, with different inital tag strings, may be introduced in due
+types of filter, with different initial tag strings, may be introduced in due
 course.
 
 The job of the function is to process the forwarding list or filter. It is
@@ -514,7 +513,10 @@ Arguments:
   options                   options to pass to the extraction functions,
                               plus ENOTDIR and EACCES handling bits
   include_directory         restrain :include: to this directory
-  sieve_vacation_directory  directory passed to sieve_interpret()
+  sieve_vacation_directory  directory passed to sieve_interpret
+  sieve_enotify_mailto_owner passed to sieve_interpret
+  sieve_useraddress         passed to sieve_interpret
+  sieve_subaddress          passed to sieve_interpret
   ugid                      uid/gid to run under - if NULL, no change
   generated                 where to hang generated addresses, initially NULL
   error                     pointer for error message
@@ -540,9 +542,11 @@ Returns:        values from extraction function, or FF_NONEXIST:
 */
 
 int
-rda_interpret(redirect_block *rdata, int options, uschar *include_directory,
-  uschar *sieve_vacation_directory, ugid_block *ugid, address_item **generated,
-  uschar **error, error_block **eblockp, int *filtertype, uschar *rname)
+rda_interpret(redirect_block * rdata, int options,
+  const uschar * include_directory, const uschar * sieve_vacation_directory,
+  const uschar * sieve_enotify_mailto_owner, const uschar * sieve_useraddress,
+  const uschar * sieve_subaddress, const ugid_block * ugid, address_item ** generated,
+  uschar ** error, error_block ** eblockp, int * filtertype, const uschar * rname)
 {
 int fd, rc, pfd[2];
 int yield, status;
@@ -552,22 +556,22 @@ uschar *data;
 uschar *readerror = US"";
 void (*oldsignal)(int);
 
-DEBUG(D_route) debug_printf("rda_interpret (%s): %s\n",
-  (rdata->isfile)? "file" : "string", rdata->string);
+DEBUG(D_route) debug_printf("rda_interpret (%s): '%s'\n",
+  rdata->isfile ? "file" : "string", string_printing(rdata->string));
 
 /* Do the expansions of the file name or data first, while still privileged. */
 
-data = expand_string(rdata->string);
-if (data == NULL)
+if (!(data = expand_string(rdata->string)))
   {
-  if (expand_string_forcedfail) return FF_NOTDELIVERED;
+  if (f.expand_string_forcedfail) return FF_NOTDELIVERED;
   *error = string_sprintf("failed to expand \"%s\": %s", rdata->string,
     expand_string_message);
   return FF_ERROR;
   }
 rdata->string = data;
 
-DEBUG(D_route) debug_printf("expanded: %s\n", data);
+DEBUG(D_route)
+  debug_printf("expanded: '%s'%s\n", data, is_tainted(data) ? " (tainted)":"");
 
 if (rdata->isfile && data[0] != '/')
   {
@@ -584,10 +588,9 @@ if (!ugid->uid_set ||                         /* Either there's no uid, or */
     (!rdata->isfile &&                        /* We've got the data, and */
      rda_is_filter(data) == FILTER_FORWARD && /* It's not a filter script, */
      Ustrstr(data, ":include:") == NULL))     /* and there's no :include: */
-  {
   return rda_extract(rdata, options, include_directory,
-    sieve_vacation_directory, generated, error, eblockp, filtertype);
-  }
+    sieve_vacation_directory, sieve_enotify_mailto_owner, sieve_useraddress,
+    sieve_subaddress, generated, error, eblockp, filtertype);
 
 /* We need to run the processing code in a sub-process. However, if we can
 determine the non-existence of a file first, we can decline without having to
@@ -606,15 +609,24 @@ if (pipe(pfd) != 0)
 
 /* Ensure that SIGCHLD is set to SIG_DFL before forking, so that the child
 process can be waited for. We sometimes get here with it set otherwise. Save
-the old state for resetting on the wait. */
+the old state for resetting on the wait. Ensure that all cached resources are
+freed so that the subprocess starts with a clean slate and doesn't interfere
+with the parent process. */
 
 oldsignal = signal(SIGCHLD, SIG_DFL);
-if ((pid = fork()) == 0)
+search_tidyup();
+
+if ((pid = exim_fork(US"router-interpret")) == 0)
   {
   header_line *waslast = header_last;   /* Save last header */
+  int fd_flags = -1;
 
   fd = pfd[pipe_write];
-  close(pfd[pipe_read]);
+  (void)close(pfd[pipe_read]);
+
+  if ((fd_flags = fcntl(fd, F_GETFD)) == -1) goto bad;
+  if (fcntl(fd, F_SETFD, fd_flags | FD_CLOEXEC) == -1) goto bad;
+
   exim_setugid(ugid->uid, ugid->gid, FALSE, rname);
 
   /* Addresses can get rewritten in filters; if we are not root or the exim
@@ -625,64 +637,72 @@ if ((pid = fork()) == 0)
     {
     DEBUG(D_rewrite) debug_printf("turned off address rewrite logging (not "
       "root or exim in this process)\n");
-    log_write_selector &= ~L_address_rewrite;
+    BIT_CLEAR(log_selector, log_selector_size, Li_address_rewrite);
     }
 
   /* Now do the business */
 
   yield = rda_extract(rdata, options, include_directory,
-    sieve_vacation_directory, generated, error, eblockp, filtertype);
+    sieve_vacation_directory, sieve_enotify_mailto_owner, sieve_useraddress,
+    sieve_subaddress, generated, error, eblockp, filtertype);
 
   /* Pass back whether it was a filter, and the return code and any overall
   error text via the pipe. */
 
-  write(fd, filtertype, sizeof(int));
-  write(fd, &yield, sizeof(int));
-  rda_write_string(fd, *error);
+  if (  write(fd, filtertype, sizeof(int)) != sizeof(int)
+     || write(fd, &yield, sizeof(int)) != sizeof(int)
+     || rda_write_string(fd, *error) != 0
+     )
+    goto bad;
 
   /* Pass back the contents of any syntax error blocks if we have a pointer */
 
-  if (eblockp != NULL)
+  if (eblockp)
     {
-    error_block *ep;
-    for (ep = *eblockp; ep != NULL; ep = ep->next)
-      {
-      rda_write_string(fd, ep->text1);
-      rda_write_string(fd, ep->text2);
-      }
-    rda_write_string(fd, NULL);    /* Indicates end of eblocks */
+    for (error_block * ep = *eblockp; ep; ep = ep->next)
+      if (  rda_write_string(fd, ep->text1) != 0
+         || rda_write_string(fd, ep->text2) != 0
+        )
+       goto bad;
+    if (rda_write_string(fd, NULL) != 0)    /* Indicates end of eblocks */
+      goto bad;
     }
 
   /* If this is a system filter, we have to pass back the numbers of any
   original header lines that were removed, and then any header lines that were
   added but not subsequently removed. */
 
-  if (system_filtering)
+  if (f.system_filtering)
     {
     int i = 0;
-    header_line *h;
-    for (h = header_list; h != waslast->next; i++, h = h->next)
-      {
-      if (h->type == htype_old) write(fd, &i, sizeof(i));
-      }
+    for (header_line * h = header_list; h != waslast->next; i++, h = h->next)
+      if (  h->type == htype_old
+         && write(fd, &i, sizeof(i)) != sizeof(i)
+        )
+       goto bad;
+
     i = -1;
-    write(fd, &i, sizeof(i));
+    if (write(fd, &i, sizeof(i)) != sizeof(i))
+       goto bad;
 
     while (waslast != header_last)
       {
       waslast = waslast->next;
       if (waslast->type != htype_old)
-        {
-        rda_write_string(fd, waslast->text);
-        write(fd, &(waslast->type), sizeof(waslast->type));
-        }
+       if (  rda_write_string(fd, waslast->text) != 0
+           || write(fd, &(waslast->type), sizeof(waslast->type))
+             != sizeof(waslast->type)
+          )
+         goto bad;
       }
-    rda_write_string(fd, NULL);    /* Indicates end of added headers */
+    if (rda_write_string(fd, NULL) != 0)    /* Indicates end of added headers */
+      goto bad;
     }
 
   /* Write the contents of the $n variables */
 
-  write(fd, filter_n, sizeof(filter_n));
+  if (write(fd, filter_n, sizeof(filter_n)) != sizeof(filter_n))
+    goto bad;
 
   /* If the result was DELIVERED or NOTDELIVERED, we pass back the generated
   addresses, and their associated information, through the pipe. This is
@@ -693,55 +713,72 @@ if ((pid = fork()) == 0)
   if (yield == FF_DELIVERED || yield == FF_NOTDELIVERED ||
       yield == FF_FAIL || yield == FF_FREEZE)
     {
-    address_item *addr;
-    for (addr = *generated; addr != NULL; addr = addr->next)
+    for (address_item * addr = *generated; addr; addr = addr->next)
       {
       int reply_options = 0;
-
-      rda_write_string(fd, addr->address);
-      write(fd, &(addr->mode), sizeof(addr->mode));
-      write(fd, &(addr->flags), sizeof(addr->flags));
-      rda_write_string(fd, addr->p.errors_address);
-
-      if (addr->pipe_expandn != NULL)
-        {
-        uschar **pp;
-        for (pp = addr->pipe_expandn; *pp != NULL; pp++)
-          rda_write_string(fd, *pp);
-        }
-      rda_write_string(fd, NULL);
-
-      if (addr->reply == NULL)
-        write(fd, &reply_options, sizeof(int));    /* 0 means no reply */
+      int ig_err = addr->prop.ignore_error ? 1 : 0;
+
+      if (  rda_write_string(fd, addr->address) != 0
+         || write(fd, &addr->mode, sizeof(addr->mode)) != sizeof(addr->mode)
+         || write(fd, &addr->flags, sizeof(addr->flags)) != sizeof(addr->flags)
+         || rda_write_string(fd, addr->prop.errors_address) != 0
+         || write(fd, &ig_err, sizeof(ig_err)) != sizeof(ig_err)
+        )
+       goto bad;
+
+      if (addr->pipe_expandn)
+        for (uschar ** pp = addr->pipe_expandn; *pp; pp++)
+          if (rda_write_string(fd, *pp) != 0)
+           goto bad;
+      if (rda_write_string(fd, NULL) != 0)
+        goto bad;
+
+      if (!addr->reply)
+       {
+        if (write(fd, &reply_options, sizeof(int)) != sizeof(int))    /* 0 means no reply */
+         goto bad;
+       }
       else
         {
         reply_options |= REPLY_EXISTS;
         if (addr->reply->file_expand) reply_options |= REPLY_EXPAND;
         if (addr->reply->return_message) reply_options |= REPLY_RETURN;
-        write(fd, &reply_options, sizeof(int));
-        write(fd, &(addr->reply->expand_forbid), sizeof(int));
-        write(fd, &(addr->reply->once_repeat), sizeof(time_t));
-        rda_write_string(fd, addr->reply->to);
-        rda_write_string(fd, addr->reply->cc);
-        rda_write_string(fd, addr->reply->bcc);
-        rda_write_string(fd, addr->reply->from);
-        rda_write_string(fd, addr->reply->reply_to);
-        rda_write_string(fd, addr->reply->subject);
-        rda_write_string(fd, addr->reply->headers);
-        rda_write_string(fd, addr->reply->text);
-        rda_write_string(fd, addr->reply->file);
-        rda_write_string(fd, addr->reply->logfile);
-        rda_write_string(fd, addr->reply->oncelog);
+        if (  write(fd, &reply_options, sizeof(int)) != sizeof(int)
+           || write(fd, &(addr->reply->expand_forbid), sizeof(int))
+             != sizeof(int)
+           || write(fd, &(addr->reply->once_repeat), sizeof(time_t))
+             != sizeof(time_t)
+           || rda_write_string(fd, addr->reply->to) != 0
+           || rda_write_string(fd, addr->reply->cc) != 0
+           || rda_write_string(fd, addr->reply->bcc) != 0
+           || rda_write_string(fd, addr->reply->from) != 0
+           || rda_write_string(fd, addr->reply->reply_to) != 0
+           || rda_write_string(fd, addr->reply->subject) != 0
+           || rda_write_string(fd, addr->reply->headers) != 0
+           || rda_write_string(fd, addr->reply->text) != 0
+           || rda_write_string(fd, addr->reply->file) != 0
+           || rda_write_string(fd, addr->reply->logfile) != 0
+           || rda_write_string(fd, addr->reply->oncelog) != 0
+          )
+         goto bad;
         }
       }
 
-    rda_write_string(fd, NULL);   /* Marks end of addresses */
+    if (rda_write_string(fd, NULL) != 0)   /* Marks end of addresses */
+      goto bad;
     }
 
-  /* OK, this process is now done. Must use _exit() and not exit() !! */
+  /* OK, this process is now done. Free any cached resources. Must use _exit()
+  and not exit() !! */
+
+out:
+  (void)close(fd);
+  search_tidyup();
+  exim_underbar_exit(EXIT_SUCCESS);
 
-  close(fd);
-  _exit(0);
+bad:
+  DEBUG(D_rewrite) debug_printf("rda_interpret: failed write to pipe\n");
+  goto out;
   }
 
 /* Back in the main process: panic if the fork did not succeed. */
@@ -753,7 +790,7 @@ if (pid < 0)
 writing end must be closed first, as otherwise read() won't return zero on an
 empty pipe. Afterwards, close the reading end. */
 
-close(pfd[pipe_write]);
+(void)close(pfd[pipe_write]);
 
 /* Read initial data, including yield and contents of *error */
 
@@ -762,34 +799,29 @@ if (read(fd, filtertype, sizeof(int)) != sizeof(int) ||
     read(fd, &yield, sizeof(int)) != sizeof(int) ||
     !rda_read_string(fd, error)) goto DISASTER;
 
-DEBUG(D_route)
-  debug_printf("rda_interpret: subprocess yield=%d error=%s\n", yield, *error);
-
 /* Read the contents of any syntax error blocks if we have a pointer */
 
-if (eblockp != NULL)
+if (eblockp)
   {
-  uschar *s;
   error_block *e;
-  error_block **p = eblockp;
-  for (;;)
+  for (error_block ** p = eblockp; ; p = &e->next)
     {
+    uschar *s;
     if (!rda_read_string(fd, &s)) goto DISASTER;
-    if (s == NULL) break;
-    e = store_get(sizeof(error_block));
+    if (!s) break;
+    e = store_get(sizeof(error_block), GET_UNTAINTED);
     e->next = NULL;
     e->text1 = s;
     if (!rda_read_string(fd, &s)) goto DISASTER;
     e->text2 = s;
     *p = e;
-    p = &(e->next);
     }
   }
 
 /* If this is a system filter, read the identify of any original header lines
 that were removed, and then read data for any new ones that were added. */
 
-if (system_filtering)
+if (f.system_filtering)
   {
   int hn = 0;
   header_line *h = header_list;
@@ -802,8 +834,7 @@ if (system_filtering)
     while (hn < n)
       {
       hn++;
-      h = h->next;
-      if (h == NULL) goto DISASTER_NO_HEADER;
+      if (!(h = h->next)) goto DISASTER_NO_HEADER;
       }
     h->type = htype_old;
     }
@@ -813,7 +844,7 @@ if (system_filtering)
     uschar *s;
     int type;
     if (!rda_read_string(fd, &s)) goto DISASTER;
-    if (s == NULL) break;
+    if (!s) break;
     if (read(fd, &type, sizeof(type)) != sizeof(type)) goto DISASTER;
     header_add(type, "%s", s);
     }
@@ -842,7 +873,7 @@ if (yield == FF_DELIVERED || yield == FF_NOTDELIVERED ||
     /* First string is the address; NULL => end of addresses */
 
     if (!rda_read_string(fd, &recipient)) goto DISASTER;
-    if (recipient == NULL) break;
+    if (!recipient) break;
 
     /* Hang on the end of the chain */
 
@@ -852,9 +883,13 @@ if (yield == FF_DELIVERED || yield == FF_NOTDELIVERED ||
 
     /* Next comes the mode and the flags fields */
 
-    if (read(fd, &(addr->mode), sizeof(addr->mode)) != sizeof(addr->mode) ||
-        read(fd, &(addr->flags), sizeof(addr->flags)) != sizeof(addr->flags) ||
-        !rda_read_string(fd, &(addr->p.errors_address))) goto DISASTER;
+    if (  read(fd, &addr->mode, sizeof(addr->mode)) != sizeof(addr->mode)
+       || read(fd, &addr->flags, sizeof(addr->flags)) != sizeof(addr->flags)
+       || !rda_read_string(fd, &addr->prop.errors_address)
+       || read(fd, &i, sizeof(i)) != sizeof(i)
+       )
+      goto DISASTER;
+    addr->prop.ignore_error = (i != 0);
 
     /* Next comes a possible setting for $thisaddress and any numerical
     variables for pipe expansion, terminated by a NULL string. The maximum
@@ -873,7 +908,7 @@ if (yield == FF_DELIVERED || yield == FF_NOTDELIVERED ||
 
     if (i > 0)
       {
-      addr->pipe_expandn = store_get((i+1) * sizeof(uschar **));
+      addr->pipe_expandn = store_get((i+1) * sizeof(uschar *), GET_UNTAINTED);
       addr->pipe_expandn[i] = NULL;
       while (--i >= 0) addr->pipe_expandn[i] = expandn[i];
       }
@@ -883,7 +918,7 @@ if (yield == FF_DELIVERED || yield == FF_NOTDELIVERED ||
     if (read(fd, &reply_options, sizeof(int)) != sizeof(int)) goto DISASTER;
     if ((reply_options & REPLY_EXISTS) != 0)
       {
-      addr->reply = store_get(sizeof(reply_item));
+      addr->reply = store_get(sizeof(reply_item), GET_UNTAINTED);
 
       addr->reply->file_expand = (reply_options & REPLY_EXPAND) != 0;
       addr->reply->return_message = (reply_options & REPLY_RETURN) != 0;
@@ -892,17 +927,17 @@ if (yield == FF_DELIVERED || yield == FF_NOTDELIVERED ||
             sizeof(int) ||
           read(fd,&(addr->reply->once_repeat),sizeof(time_t)) !=
             sizeof(time_t) ||
-          !rda_read_string(fd, &(addr->reply->to)) ||
-          !rda_read_string(fd, &(addr->reply->cc)) ||
-          !rda_read_string(fd, &(addr->reply->bcc)) ||
-          !rda_read_string(fd, &(addr->reply->from)) ||
-          !rda_read_string(fd, &(addr->reply->reply_to)) ||
-          !rda_read_string(fd, &(addr->reply->subject)) ||
-          !rda_read_string(fd, &(addr->reply->headers)) ||
-          !rda_read_string(fd, &(addr->reply->text)) ||
-          !rda_read_string(fd, &(addr->reply->file)) ||
-          !rda_read_string(fd, &(addr->reply->logfile)) ||
-          !rda_read_string(fd, &(addr->reply->oncelog)))
+          !rda_read_string(fd, &addr->reply->to) ||
+          !rda_read_string(fd, &addr->reply->cc) ||
+          !rda_read_string(fd, &addr->reply->bcc) ||
+          !rda_read_string(fd, &addr->reply->from) ||
+          !rda_read_string(fd, &addr->reply->reply_to) ||
+          !rda_read_string(fd, &addr->reply->subject) ||
+          !rda_read_string(fd, &addr->reply->headers) ||
+          !rda_read_string(fd, &addr->reply->text) ||
+          !rda_read_string(fd, &addr->reply->file) ||
+          !rda_read_string(fd, &addr->reply->logfile) ||
+          !rda_read_string(fd, &addr->reply->oncelog))
         goto DISASTER;
       }
     }
@@ -913,32 +948,31 @@ reading end of the pipe, and we are done. */
 
 WAIT_EXIT:
 while ((rc = wait(&status)) != pid)
-  {
   if (rc < 0 && errno == ECHILD)      /* Process has vanished */
     {
     log_write(0, LOG_MAIN, "redirection process %d vanished unexpectedly", pid);
     goto FINAL_EXIT;
     }
-  }
+
+DEBUG(D_route)
+  debug_printf("rda_interpret: subprocess yield=%d error=%s\n", yield, *error);
 
 if (had_disaster)
   {
   *error = string_sprintf("internal problem in %s: failure to transfer "
     "data from subprocess: status=%04x%s%s%s", rname,
     status, readerror,
-    (*error == NULL)? US"" : US": error=",
-    (*error == NULL)? US"" : *error);
+    *error ? US": error=" : US"",
+    *error ? *error : US"");
   log_write(0, LOG_MAIN|LOG_PANIC, "%s", *error);
   }
 else if (status != 0)
-  {
   log_write(0, LOG_MAIN|LOG_PANIC, "internal problem in %s: unexpected status "
     "%04x from redirect subprocess (but data correctly received)", rname,
     status);
-  }
 
 FINAL_EXIT:
-close(fd);
+(void)close(fd);
 signal(SIGCHLD, oldsignal);   /* restore */
 return yield;