SPDX: Mass-update to GPL-2.0-or-later
[exim.git] / src / src / transports / appendfile.c
index c8c0a58b6d3236517da3d336f29cac19483b2dfd..c39c07c9fd3813280eb4764d1964db0a7ea5d733 100644 (file)
@@ -2,9 +2,10 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2018 */
-/* Copyright (c) The Exim maintainers 2020 */
+/* Copyright (c) The Exim maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
@@ -112,70 +113,27 @@ BOOL appendfile_transport_entry(transport_instance *tblock, address_item *addr)
 /* Default private options block for the appendfile transport. */
 
 appendfile_transport_options_block appendfile_transport_option_defaults = {
-  NULL,           /* filename */
-  NULL,           /* dirname */
-  US"q${base62:$tod_epoch}-$inode", /* dirfilename */
-  NULL,           /* message_prefix (default reset in init if not bsmtp) */
-  NULL,           /* message_suffix (ditto) */
-  US"anywhere",   /* create_file_string (string value for create_file) */
-  NULL,           /* quota */
-  NULL,           /* quota_directory */
-  NULL,           /* quota_filecount */
-  NULL,           /* quota_size_regex */
-  NULL,           /* quota_warn_threshold */
-  NULL,           /* mailbox_size_string */
-  NULL,           /* mailbox_filecount_string */
-  NULL,           /* expand_maildir_use_size_file */
-  US"^(?:cur|new|\\..*)$",  /* maildir_dir_regex */
-  NULL,           /* maildir_tag */
-  NULL,           /* maildirfolder_create_regex */
-  NULL,           /* mailstore_prefix */
-  NULL,           /* mailstore_suffix */
-  NULL,           /* check_string (default changed for non-bsmtp file)*/
-  NULL,           /* escape_string (ditto) */
-  NULL,           /* file_format */
-  0,              /* quota_value */
-  0,              /* quota_warn_threshold_value */
-  -1,             /* mailbox_size_value */
-  -1,             /* mailbox_filecount_value */
-  0,              /* quota_filecount_value */
-  APPENDFILE_MODE,           /* mode */
-  APPENDFILE_DIRECTORY_MODE, /* dirmode */
-  APPENDFILE_LOCKFILE_MODE,  /* lockfile_mode */
-  30*60,          /* lockfile_timeout */
-  0,              /* lock_fcntl_timeout */
-  0,              /* lock_flock_timeout */
-  10,             /* lock_retries */
-   3,             /* lock_interval */
-  10,             /* maildir_retries */
-  create_anywhere,/* create_file */
-  0,              /* options */
-  FALSE,          /* allow_fifo */
-  FALSE,          /* allow_symlink */
-  FALSE,          /* check_group */
-  TRUE,           /* check_owner */
-  TRUE,           /* create_directory */
-  FALSE,          /* notify_comsat */
-  TRUE,           /* use_lockfile */
-  FALSE,          /* set_use_lockfile */
-  TRUE,           /* use_fcntl */
-  FALSE,          /* set_use_fcntl */
-  FALSE,          /* use_flock */
-  FALSE,          /* set_use_flock */
-  FALSE,          /* use_mbx_lock */
-  FALSE,          /* set_use_mbx_lock */
-  FALSE,          /* use_bsmtp */
-  FALSE,          /* use_crlf */
-  FALSE,          /* file_must_exist */
-  TRUE,           /* mode_fail_narrower */
-  FALSE,          /* maildir_format */
-  FALSE,          /* maildir_use_size_file */
-  FALSE,          /* mailstore_format */
-  FALSE,          /* mbx_format */
-  FALSE,          /* quota_warn_threshold_is_percent */
-  TRUE,           /* quota_is_inclusive */
-  FALSE,          /* quota_no_check */
-  FALSE           /* quota_filecount_no_check */
+  /* all non-mentioned members zero/null/false */
+  .dirfilename = US"q${base62:$tod_epoch}-$inode",
+  .create_file_string = US"anywhere",
+  .maildir_dir_regex = US"^(?:cur|new|\\..*)$",
+  .mailbox_size_value = -1,
+  .mailbox_filecount_value = -1,
+  .mode = APPENDFILE_MODE,
+  .dirmode = APPENDFILE_DIRECTORY_MODE,
+  .lockfile_mode = APPENDFILE_LOCKFILE_MODE,
+  .lockfile_timeout = 30*60,
+  .lock_retries = 10,
+   .lock_interval = 3,
+  .maildir_retries = 10,
+  .create_file = create_anywhere,
+  .check_owner = TRUE,
+  .create_directory = TRUE,
+  .notify_comsat = FALSE,
+  .use_lockfile = TRUE,
+  .use_fcntl = TRUE,
+  .mode_fail_narrower = TRUE,
+  .quota_is_inclusive = TRUE,
 };
 
 
@@ -226,11 +184,6 @@ appendfile_transport_options_block *ob =
 uschar *q = ob->quota;
 double default_value = 0.0;
 
-addrlist = addrlist;    /* Keep picky compilers happy */
-dummy = dummy;
-uid = uid;
-gid = gid;
-
 if (ob->expand_maildir_use_size_file)
        ob->maildir_use_size_file = expand_check_condition(ob->expand_maildir_use_size_file,
                US"`maildir_use_size_file` in transport", tblock->name);
@@ -240,17 +193,15 @@ mailbox_filecount */
 
 for (int i = 0; i < 5; i++)
   {
-  double d;
+  double d = default_value;
   int no_check = 0;
   uschar *which = NULL;
 
-  if (q == NULL) d = default_value;
-  else
+  if (q)
     {
-    uschar *rest;
-    uschar *s = expand_string(q);
+    uschar * rest, * s;
 
-    if (!s)
+    if (!(s =  expand_string(q)))
       {
       *errmsg = string_sprintf("Expansion of \"%s\" in %s transport failed: "
         "%s", q, tblock->name, expand_string_message);
@@ -320,8 +271,8 @@ for (int i = 0; i < 5; i++)
       break;
 
     case 2:
-    if (d >= 2.0*1024.0*1024.0*1024.0 && sizeof(off_t) <= 4)
-       which = US"quota_warn_threshold";
+      if (d >= 2.0*1024.0*1024.0*1024.0 && sizeof(off_t) <= 4)
+         which = US"quota_warn_threshold";
       ob->quota_warn_threshold_value = (off_t)d;
       q = ob->mailbox_size_string;
       default_value = -1.0;
@@ -367,6 +318,7 @@ appendfile_transport_init(transport_instance *tblock)
 {
 appendfile_transport_options_block *ob =
   (appendfile_transport_options_block *)(tblock->options_block);
+uschar * s;
 
 /* Set up the setup entry point, to be called in the privileged state */
 
@@ -465,20 +417,17 @@ if (tblock->uid_set && !tblock->gid_set && !tblock->expand_gid)
 /* If "create_file" is set, check that a valid option is given, and set the
 integer variable. */
 
-if (ob->create_file_string)
+if ((s = ob->create_file_string ) && *s)
   {
-  int value = 0;
-  if (Ustrcmp(ob->create_file_string, "anywhere") == 0)
-    value = create_anywhere;
-  else if (Ustrcmp(ob->create_file_string, "belowhome") == 0)
-    value = create_belowhome;
-  else if (Ustrcmp(ob->create_file_string, "inhome") == 0)
-    value = create_inhome;
+  int val = 0;
+  if (Ustrcmp(s, "anywhere") == 0)                     val = create_anywhere;
+  else if (*s == '/' || Ustrcmp(s, "belowhome") == 0)  val = create_belowhome;
+  else if (Ustrcmp(s, "inhome") == 0)                  val = create_inhome;
   else
     log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
-      "invalid value given for \"file_create\" for the %s transport: %s",
-        tblock->name, ob->create_file_string);
-  ob->create_file = value;
+      "invalid value given for \"create_file\" for the %s transport: '%s'",
+      tblock->name, s);
+  ob->create_file = val;
   }
 
 /* If quota_warn_threshold is set, set up default for warn_message. It may
@@ -649,7 +598,8 @@ if (len == 0) return tblock;
 
 /* Search the formats for a match */
 
-while ((s = string_nextinlist(&format,&sep,big_buffer,big_buffer_size)))
+/* not expanded so cannot be tainted */
+while ((s = string_nextinlist(&format, &sep, big_buffer, big_buffer_size)))
   {
   int slen = Ustrlen(s);
   BOOL match = len >= slen && Ustrncmp(data, s, slen) == 0;
@@ -702,14 +652,14 @@ the log, because we are running as an unprivileged user here.
 Arguments:
   dirname       the name of the directory
   countptr      where to add the file count (because this function recurses)
-  regex         a compiled regex to get the size from a name
+  re            a compiled regex to get the size from a name
 
 Returns:        the sum of the sizes of the stattable files
                 zero if the directory cannot be opened
 */
 
 off_t
-check_dir_size(const uschar * dirname, int *countptr, const pcre *regex)
+check_dir_size(const uschar * dirname, int * countptr, const pcre2_code * re)
 {
 DIR *dir;
 off_t sum = 0;
@@ -728,22 +678,28 @@ for (struct dirent *ent; ent = readdir(dir); )
 
   /* If there's a regex, try to find the size using it */
 
-  if (regex)
+  if (re)
     {
-    int ovector[6];
-    if (pcre_exec(regex, NULL, CS name, Ustrlen(name), 0, 0, ovector,6) >= 2)
+    pcre2_match_data * md = pcre2_match_data_create(2, pcre_gen_ctx);
+    int rc = pcre2_match(re, (PCRE2_SPTR)name, PCRE2_ZERO_TERMINATED,
+                         0, 0, md, pcre_gen_mtc_ctx);
+    PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
+    if (  rc >= 0
+       && (rc = pcre2_get_ovector_count(md)) >= 2)
       {
       uschar *endptr;
-      off_t size = (off_t)Ustrtod(name + ovector[2], &endptr);
-      if (endptr == name + ovector[3])
+      off_t size = (off_t)Ustrtod(name + ovec[2], &endptr);
+      if (endptr == name + ovec[3])
         {
         sum += size;
         DEBUG(D_transport)
           debug_printf("check_dir_size: size from %s is " OFF_T_FMT "\n", name,
             size);
+       /* pcre2_match_data_free(md);   gen ctx needs no free */
         continue;
         }
       }
+    /* pcre2_match_data_free(md);      gen ctx needs no free */
     DEBUG(D_transport)
       debug_printf("check_dir_size: regex did not match %s\n", name);
     }
@@ -762,7 +718,7 @@ for (struct dirent *ent; ent = readdir(dir); )
     if ((statbuf.st_mode & S_IFMT) == S_IFREG)
       sum += statbuf.st_size / statbuf.st_nlink;
     else if ((statbuf.st_mode & S_IFMT) == S_IFDIR)
-      sum += check_dir_size(path, &count, regex);
+      sum += check_dir_size(path, &count, re);
   }
 
 closedir(dir);
@@ -940,28 +896,28 @@ we can't do any tests.
 Arguments:
   filename     the file name
   create_file  the ob->create_file option
+  deliver_dir  the delivery directory
 
 Returns:       TRUE if creation is permitted
 */
 
 static BOOL
-check_creation(uschar *filename, int create_file)
+check_creation(uschar *filename, int create_file, const uschar * deliver_dir)
 {
 BOOL yield = TRUE;
 
-if (deliver_home  &&  create_file != create_anywhere)
+if (deliver_dir  &&  create_file != create_anywhere)
   {
-  int len = Ustrlen(deliver_home);
+  int len = Ustrlen(deliver_dir);
   uschar *file = filename;
 
   while (file[0] == '/' && file[1] == '/') file++;
-  if (Ustrncmp(file, deliver_home, len) != 0 || file[len] != '/' ||
-       ( Ustrchr(file+len+2, '/') != NULL &&
-         (
-         create_file != create_belowhome ||
-         Ustrstr(file+len, "/../") != NULL
-         )
-       )
+  if (  Ustrncmp(file, deliver_dir, len) != 0
+     || file[len] != '/'
+     ||    Ustrchr(file+len+2, '/') != NULL
+       && (  create_file != create_belowhome
+          || Ustrstr(file+len, "/../") != NULL
+          )
      ) yield = FALSE;
 
   /* If yield is TRUE, the file name starts with the home directory, and does
@@ -999,10 +955,10 @@ if (deliver_home  &&  create_file != create_anywhere)
     if (rp)
       {
       uschar hdbuffer[PATH_MAX+1];
-      uschar *rph = deliver_home;
+      const uschar * rph = deliver_dir;
       int rlen = Ustrlen(big_buffer);
 
-      if ((rp = US realpath(CS deliver_home, CS hdbuffer)))
+      if ((rp = US realpath(CS deliver_dir, CS hdbuffer)))
         {
         rph = hdbuffer;
         len = Ustrlen(rph);
@@ -1013,7 +969,7 @@ if (deliver_home  &&  create_file != create_anywhere)
         {
         yield = FALSE;
         DEBUG(D_transport) debug_printf("Real path \"%s\" does not match \"%s\"\n",
-          big_buffer, deliver_home);
+          big_buffer, deliver_dir);
         }
       }
     }
@@ -1183,6 +1139,7 @@ appendfile_transport_entry(
 appendfile_transport_options_block *ob =
   (appendfile_transport_options_block *)(tblock->options_block);
 struct stat statbuf;
+const uschar * deliver_dir;
 uschar *fdname = NULL;
 uschar *filename = NULL;
 uschar *hitchname = NULL;
@@ -1286,12 +1243,6 @@ if (!(path = expand_string(fdname)))
     expand_string_message);
   goto ret_panic;
   }
-if (is_tainted(path))
-  {
-  addr->message = string_sprintf("Tainted '%s' (file or directory "
-    "name for %s transport) not permitted", path, tblock->name);
-  goto ret_panic;
-  }
 
 if (path[0] != '/')
   {
@@ -1368,6 +1319,12 @@ if (f.dont_deliver)
   return FALSE;
   }
 
+/* If an absolute path was given for create_file the it overrides deliver_home
+(here) and de-taints the filename (below, after check_creation() */
+
+deliver_dir = *ob->create_file_string == '/'
+  ? ob->create_file_string : deliver_home;
+
 /* Handle the case of a file name. If the file name is /dev/null, we can save
 ourselves some effort and just give a success return right away. */
 
@@ -1384,10 +1341,20 @@ if (!isdirectory)
     }
 
   /* Set the name of the file to be opened, and the file to which the data
-  is written, and find out if we are permitted to create a non-existent file. */
+  is written, and find out if we are permitted to create a non-existent file.
+  If the create_file option is an absolute path and the file was within it,
+  de-taint.  Chaeck for a tainted path. */
 
+  if (  (allow_creation_here = check_creation(path, ob->create_file, deliver_dir))
+     && ob->create_file == create_belowhome)
+    if (is_tainted(path))
+      {
+      DEBUG(D_transport) debug_printf("de-tainting path '%s'\n", path);
+      path = string_copy_taint(path, GET_UNTAINTED);
+      }
+
+  if (is_tainted(path)) goto tainted_ret_panic;
   dataname = filename = path;
-  allow_creation_here = check_creation(filename, ob->create_file);
 
   /* If ob->create_directory is set, attempt to create the directories in
   which this mailbox lives, but only if we are permitted to create the file
@@ -1397,17 +1364,16 @@ if (!isdirectory)
   if (ob->create_directory && allow_creation_here)
     {
     uschar *p = Ustrrchr(path, '/');
-    *p = '\0';
-    if (!directory_make(NULL, path, ob->dirmode, FALSE))
+    p = string_copyn(path, p - path);
+    if (!directory_make(NULL, p, ob->dirmode, FALSE))
       {
       addr->basic_errno = errno;
       addr->message =
         string_sprintf("failed to create directories for %s: %s", path,
-          strerror(errno));
+          exim_errstr(errno));
       DEBUG(D_transport) debug_printf("%s transport: %s\n", tblock->name, path);
       return FALSE;
       }
-    *p = '/';
     }
 
   /* If file_format is set we must check that any existing file matches one of
@@ -1783,8 +1749,8 @@ if (!isdirectory)
       if (statbuf.st_nlink != 1)
         {
         addr->basic_errno = ERRNO_NOTREGULAR;
-        addr->message = string_sprintf("mailbox %s%s has too many links (%d)",
-          filename, islink ? " (symlink)" : "", statbuf.st_nlink);
+        addr->message = string_sprintf("mailbox %s%s has too many links (%lu)",
+          filename, islink ? " (symlink)" : "", (unsigned long)statbuf.st_nlink);
         goto RETURN;
 
         }
@@ -2204,10 +2170,11 @@ scanning is expensive; for maildirs some fudges have been invented:
 
 else
   {
-  uschar *check_path = path;    /* Default quota check path */
-  const pcre *regex = NULL;     /* Regex for file size from file name */
+  uschar *check_path;          /* Default quota check path */
+  const pcre2_code * re = NULL;     /* Regex for file size from file name */
 
-  if (!check_creation(string_sprintf("%s/any", path), ob->create_file))
+  if (!check_creation(string_sprintf("%s/any", path),
+                     ob->create_file, deliver_dir))
     {
     addr->basic_errno = ERRNO_BADCREATE;
     addr->message = string_sprintf("tried to create file in %s, but "
@@ -2215,6 +2182,20 @@ else
     goto RETURN;
     }
 
+  /* If the create_file option is an absolute path and the file was within
+  it, de-taint. Otherwise check for taint. */
+
+  if (is_tainted(path))
+    if (ob->create_file == create_belowhome)
+      {
+      DEBUG(D_transport) debug_printf("de-tainting path '%s'\n", path);
+      path = string_copy_taint(path, GET_UNTAINTED);
+      }
+    else
+      goto tainted_ret_panic;
+
+  check_path = path;
+
   #ifdef SUPPORT_MAILDIR
   /* For a maildir delivery, ensure that all the relevant directories exist,
   and a maildirfolder file if necessary. */
@@ -2233,21 +2214,14 @@ else
 
   if (ob->quota_value > 0 || THRESHOLD_CHECK || ob->maildir_use_size_file)
     {
-    const uschar *error;
-    int offset;
-
     /* Compile the regex if there is one. */
 
     if (ob->quota_size_regex)
       {
-      if (!(regex = pcre_compile(CS ob->quota_size_regex, PCRE_COPT,
-         CCSS &error, &offset, NULL)))
-        {
-        addr->message = string_sprintf("appendfile: regular expression "
-          "error: %s at offset %d while compiling %s", error, offset,
-          ob->quota_size_regex);
+      if (!(re = regex_compile(ob->quota_size_regex,
+                 MCS_NOFLAGS, &addr->message, pcre_gen_cmp_ctx)))
         return FALSE;
-        }
+
       DEBUG(D_transport) debug_printf("using regex for file sizes: %s\n",
         ob->quota_size_regex);
       }
@@ -2319,22 +2293,15 @@ else
   #ifdef SUPPORT_MAILDIR
   if (ob->maildir_use_size_file)
     {
-    const pcre *dir_regex = NULL;
-    const uschar *error;
-    int offset;
+    const pcre2_code * dir_regex = NULL;
 
     if (ob->maildir_dir_regex)
       {
       int check_path_len = Ustrlen(check_path);
 
-      if (!(dir_regex = pcre_compile(CS ob->maildir_dir_regex, PCRE_COPT,
-         CCSS &error, &offset, NULL)))
-        {
-        addr->message = string_sprintf("appendfile: regular expression "
-          "error: %s at offset %d while compiling %s", error, offset,
-          ob->maildir_dir_regex);
+      if (!(dir_regex = regex_compile(ob->maildir_dir_regex,
+           MCS_NOFLAGS, &addr->message, pcre_gen_cmp_ctx)))
         return FALSE;
-        }
 
       DEBUG(D_transport)
         debug_printf("using regex for maildir directory selection: %s\n",
@@ -2350,7 +2317,7 @@ else
         uschar *s = path + check_path_len;
         while (*s == '/') s++;
         s = *s ? string_sprintf("%s/new", s) : US"new";
-        if (pcre_exec(dir_regex, NULL, CS s, Ustrlen(s), 0, 0, NULL, 0) < 0)
+       if (!regex_match(dir_regex, s, -1, NULL))
           {
           disable_quota = TRUE;
           DEBUG(D_transport) debug_printf("delivery directory does not match "
@@ -2371,7 +2338,7 @@ else
       off_t size;
       int filecount;
 
-      if ((maildirsize_fd = maildir_ensure_sizefile(check_path, ob, regex, dir_regex,
+      if ((maildirsize_fd = maildir_ensure_sizefile(check_path, ob,  re, dir_regex,
          &size, &filecount)) == -1)
         {
         addr->basic_errno = errno;
@@ -2396,7 +2363,7 @@ else
  *    (void)unlink(CS string_sprintf("%s/maildirsize", check_path));
  *    if (THRESHOLD_CHECK)
  *      mailbox_size = maildir_compute_size(check_path, &mailbox_filecount, &old_latest,
- *        regex, dir_regex, FALSE);
+ *         re, dir_regex, FALSE);
  *    }
 */
 
@@ -2418,7 +2385,7 @@ else
     int filecount = 0;
     DEBUG(D_transport)
       debug_printf("quota checks on directory %s\n", check_path);
-    size = check_dir_size(check_path, &filecount, regex);
+    size = check_dir_size(check_path, &filecount,  re);
     if (mailbox_size < 0) mailbox_size = size;
     if (mailbox_filecount < 0) mailbox_filecount = filecount;
     }
@@ -3209,7 +3176,7 @@ else
           uschar *iptr = expand_string(nametag);
           if (iptr)
             {
-            uschar *etag = store_get(Ustrlen(iptr) + 2, is_tainted(iptr));
+            uschar *etag = store_get(Ustrlen(iptr) + 2, iptr);
             uschar *optr = etag;
             for ( ; *iptr; iptr++)
               if (mac_isgraph(*iptr) && *iptr != '/')
@@ -3323,6 +3290,9 @@ put in the first address of a batch. */
 
 return FALSE;
 
+tainted_ret_panic:
+  addr->message = string_sprintf("Tainted '%s' (file or directory "
+      "name for %s transport) not permitted", path, tblock->name);
 ret_panic:
   addr->transport_return = PANIC;
   return FALSE;