Protect against symlink attacks on MBX lockfile in /tmp as best we can:
[exim.git] / src / src / transports / appendfile.c
index 6874fa7464284da62208213cb0c3a3d78772c8fb..780e4b245ff3e4c97ef4bc4689f3785a8116d26f 100644 (file)
@@ -1,10 +1,10 @@
-/* $Cambridge: exim/src/src/transports/appendfile.c,v 1.7 2005/06/07 15:20:56 ph10 Exp $ */
+/* $Cambridge: exim/src/src/transports/appendfile.c,v 1.26 2010/05/29 12:11:48 pdp Exp $ */
 
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2005 */
+/* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 
@@ -107,6 +107,8 @@ optionlist appendfile_transport_options[] = {
       (void *)offsetof(appendfile_transport_options_block, maildir_tag) },
   { "maildir_use_size_file", opt_bool,
       (void *)offsetof(appendfile_transport_options_block, maildir_use_size_file ) } ,
+  { "maildirfolder_create_regex", opt_stringptr,
+      (void *)offsetof(appendfile_transport_options_block, maildirfolder_create_regex ) },
 #endif  /* SUPPORT_MAILDIR */
 #ifdef SUPPORT_MAILSTORE
   { "mailstore_format",  opt_bool,
@@ -184,6 +186,7 @@ appendfile_transport_options_block appendfile_transport_option_defaults = {
   NULL,           /* mailbox_filecount_string */
   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)*/
@@ -247,6 +250,8 @@ Arguments:
   tblock     points to the transport instance
   addrlist   addresses about to be delivered (not used)
   dummy      not used (doesn't pass back data)
+  uid        the uid that will be set (not used)
+  gid        the gid that will be set (not used)
   errmsg     where to put an error message
 
 Returns:     OK, FAIL, or DEFER
@@ -254,7 +259,7 @@ Returns:     OK, FAIL, or DEFER
 
 static int
 appendfile_transport_setup(transport_instance *tblock, address_item *addrlist,
-  transport_feedback *dummy, uschar **errmsg)
+  transport_feedback *dummy, uid_t uid, gid_t gid, uschar **errmsg)
 {
 appendfile_transport_options_block *ob =
   (appendfile_transport_options_block *)(tblock->options_block);
@@ -264,6 +269,8 @@ int i;
 
 addrlist = addrlist;    /* Keep picky compilers happy */
 dummy = dummy;
+uid = uid;
+gid = gid;
 
 /* Loop for quota, quota_filecount, quota_warn_threshold, mailbox_size,
 mailbox_filecount */
@@ -271,6 +278,7 @@ mailbox_filecount */
 for (i = 0; i < 5; i++)
   {
   double d;
+  uschar *which = NULL;
 
   if (q == NULL) d = default_value; else
     {
@@ -316,33 +324,49 @@ for (i = 0; i < 5; i++)
       }
     }
 
+  /* Set each value, checking for possible overflow. */
+
   switch (i)
     {
     case 0:
+    if (d >= 2.0*1024.0*1024.0*1024.0 && sizeof(off_t) <= 4) which = US"quota";
     ob->quota_value = (off_t)d;
     q = ob->quota_filecount;
     break;
 
     case 1:
+    if (d >= 2.0*1024.0*1024.0*1024.0) which = US"quota_filecount";
     ob->quota_filecount_value = (int)d;
     q = ob->quota_warn_threshold;
     break;
 
     case 2:
+    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;
     break;
 
     case 3:
+    if (d >= 2.0*1024.0*1024.0*1024.0 && sizeof(off_t) <= 4)
+      which = US"mailbox_size";;
     ob->mailbox_size_value = (off_t)d;
     q = ob->mailbox_filecount_string;
     break;
 
     case 4:
+    if (d >= 2.0*1024.0*1024.0*1024.0) which = US"mailbox_filecount";
     ob->mailbox_filecount_value = (int)d;
     break;
     }
+
+  if (which != NULL)
+    {
+    *errmsg = string_sprintf("%s value %.10g is too large (overflow) in "
+      "%s transport", which, d, tblock->name);
+    return FAIL;
+    }
   }
 
 return OK;
@@ -559,7 +583,7 @@ uschar buffer[256];
 
 DEBUG(D_transport) debug_printf("notify_comsat called\n");
 
-sprintf(CS buffer, "%.200s@%.30g\n", user, (double)offset);
+sprintf(CS buffer, "%.200s@" OFF_T_FMT "\n", user, offset);
 
 if ((sp = getservbyname("biff", "udp")) == NULL)
   {
@@ -575,10 +599,10 @@ host.next = NULL;
 until one succeeds. However, it appears that at least on some systems, comsat
 doesn't listen on the ::1 address. So for the moment, just force the address to
 be 127.0.0.1. At some future stage, when IPv6 really is superseding IPv4, this
-can be changed. */
+can be changed. (But actually, comsat is probably dying out anyway.) */
 
 /******
-if (host_find_byname(&host, NULL, NULL, FALSE) == HOST_FIND_FAILED)
+if (host_find_byname(&host, NULL, 0, NULL, FALSE) == HOST_FIND_FAILED)
   {
   DEBUG(D_transport) debug_printf("\"localhost\" unknown\n");
   return;
@@ -602,7 +626,7 @@ for (h = &host; h != NULL; h = h->next)
 
   (void)ip_connect(sock, host_af, h->address, ntohs(sp->s_port), 0);
   rc = send(sock, buffer, Ustrlen(buffer) + 1, 0);
-  close(sock);
+  (void)close(sock);
 
   if (rc >= 0) break;
   DEBUG(D_transport)
@@ -741,8 +765,8 @@ while ((ent = readdir(dir)) != NULL)
         {
         sum += size;
         DEBUG(D_transport)
-          debug_printf("check_dir_size: size from %s is %.30g\n", name,
-            (double)size);
+          debug_printf("check_dir_size: size from %s is " OFF_T_FMT "\n", name,
+            size);
         continue;
         }
       }
@@ -776,8 +800,8 @@ while ((ent = readdir(dir)) != NULL)
 
 closedir(dir);
 DEBUG(D_transport)
-  debug_printf("check_dir_size: dir=%s sum=%.30g count=%d\n", dirname,
-    (double)sum, count);
+  debug_printf("check_dir_size: dir=%s sum=" OFF_T_FMT " count=%d\n", dirname,
+    sum, count);
 
 *countptr = count;
 return sum;
@@ -909,8 +933,8 @@ size, including CRLFs, which is the size of the input (temporary) file. */
 if (fstat(from_fd, &statbuf) < 0) return DEFER;
 size = statbuf.st_size;
 
-sprintf (CS deliver_out_buffer, "%s,%.30g;%08lx%04x-%08x\015\012",
-  tod_stamp(tod_mbx), (double)size, 0L, 0, 0);
+sprintf (CS deliver_out_buffer, "%s," OFF_T_FMT ";%08lx%04x-%08x\015\012",
+  tod_stamp(tod_mbx), size, 0L, 0, 0);
 used = Ustrlen(deliver_out_buffer);
 
 /* Rewind the temporary file, and copy it over in chunks. */
@@ -1205,6 +1229,7 @@ uschar *filecount_msg = US"";
 uschar *path;
 struct utimbuf times;
 struct timeval msg_tv;
+BOOL disable_quota = FALSE;
 BOOL isdirectory = FALSE;
 BOOL isfifo = FALSE;
 BOOL wait_for_tick = FALSE;
@@ -1312,10 +1337,15 @@ if (path[0] != '/')
   return FALSE;
   }
 
-/* For a file delivery, make sure the local part in the address is updated to
-the true local part. */
+/* For a file delivery, make sure the local part in the address(es) is updated
+to the true local part. */
 
-if (testflag(addr, af_file)) addr->local_part = string_copy(path);
+if (testflag(addr, af_file))
+  {
+  address_item *addr2;
+  for (addr2 = addr; addr2 != NULL; addr2 = addr2->next)
+    addr2->local_part = string_copy(path);
+  }
 
 /* The available mailbox formats depend on whether it is a directory or a file
 delivery. */
@@ -1342,12 +1372,12 @@ else
 
 DEBUG(D_transport)
   {
-  debug_printf("appendfile: mode=%o notify_comsat=%d quota=%.30g "
-    "warning=%.30g%s\n"
+  debug_printf("appendfile: mode=%o notify_comsat=%d quota=" OFF_T_FMT
+    " warning=" OFF_T_FMT "%s\n"
     "  %s=%s format=%s\n  message_prefix=%s\n  message_suffix=%s\n  "
     "maildir_use_size_file=%s\n",
-    mode, ob->notify_comsat, (double)ob->quota_value,
-    (double)ob->quota_warn_threshold_value,
+    mode, ob->notify_comsat, ob->quota_value,
+    ob->quota_warn_threshold_value,
     ob->quota_warn_threshold_is_percent? "%" : "",
     isdirectory? "directory" : "file",
     path, mailbox_formats[mbformat],
@@ -1430,7 +1460,7 @@ if (!isdirectory)
     if (cfd >= 0)
       {
       transport_instance *tt = check_file_format(cfd, tblock, addr);
-      close(cfd);
+      (void)close(cfd);
 
       /* If another transport is indicated, call it and return; if no transport
       was found, just return - the error data will have been set up.*/
@@ -1622,7 +1652,7 @@ if (!isdirectory)
       sufficiently worried. */
 
       if ((rc = Ulink(hitchname, lockname)) != 0) fstat(hd, &statbuf);
-      close(hd);
+      (void)close(hd);
       Uunlink(hitchname);
       if (rc != 0 && statbuf.st_nlink != 2)
         {
@@ -1734,8 +1764,8 @@ if (!isdirectory)
       /* We have successfully created and opened the file. Ensure that the group
       and the mode are correct. */
 
-      Uchown(filename, uid, gid);
-      Uchmod(filename, mode);
+      (void)Uchown(filename, uid, gid);
+      (void)Uchmod(filename, mode);
       }
 
 
@@ -1776,6 +1806,18 @@ if (!isdirectory)
         goto RETURN;
         }
 
+      /* Just in case this is a sticky-bit mail directory, we don't want
+      users to be able to create hard links to other users' files. */
+
+      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);
+        goto RETURN;
+
+        }
+
       /* If symlinks are permitted (not recommended), the lstat() above will
       have found the symlink. Its ownership has just been checked; go round
       the loop again, using stat() instead of lstat(). That will never yield a
@@ -1968,6 +2010,8 @@ if (!isdirectory)
     #ifdef SUPPORT_MBX
     else if (ob->use_mbx_lock)
       {
+      int mbx_tmp_oflags;
+      struct stat lstatbuf, statbuf2;
       if (apply_lock(fd, F_RDLCK, ob->use_fcntl, ob->lock_fcntl_timeout,
            ob->use_flock, ob->lock_flock_timeout) >= 0 &&
            fstat(fd, &statbuf) >= 0)
@@ -1975,6 +2019,21 @@ if (!isdirectory)
         sprintf(CS mbx_lockname, "/tmp/.%lx.%lx", (long)statbuf.st_dev,
           (long)statbuf.st_ino);
 
+        /*
+         * 2010-05-29: SECURITY
+         * Dan Rosenberg reported the presence of a race-condition in the
+         * original code here.  Beware that many systems still allow symlinks
+         * to be followed in /tmp so an attacker can create a symlink pointing
+         * elsewhere between a stat and an open, which we should avoid
+         * following.
+         *
+         * It's unfortunate that we can't just use all the heavily debugged
+         * locking from above.
+         *
+         * Also: remember to mirror changes into exim_lock.c */
+
+        /* first leave the old pre-check in place, it provides better
+         * diagnostics for common cases */
         if (Ulstat(mbx_lockname, &statbuf) >= 0)
           {
           if ((statbuf.st_mode & S_IFMT) == S_IFLNK)
@@ -1993,7 +2052,19 @@ if (!isdirectory)
             }
           }
 
-        mbx_lockfd = Uopen(mbx_lockname, O_RDWR | O_CREAT, 0600);
+        /* If we could just declare "we must be the ones who create this
+         * file" then a hitching post in a subdir would work, since a
+         * subdir directly in /tmp/ which we create wouldn't follow links
+         * but this isn't our locking logic, so we can't safely change the
+         * file existence rules. */
+
+        /* On systems which support O_NOFOLLOW, it's the easiest and most
+         * obviously correct security fix */
+        mbx_tmp_oflags = O_RDWR | O_CREAT;
+#ifdef O_NOFOLLOW
+        mbx_tmp_oflags |= O_NOFOLLOW;
+#endif
+        mbx_lockfd = Uopen(mbx_lockname, mbx_tmp_oflags, ob->lockfile_mode);
         if (mbx_lockfd < 0)
           {
           addr->basic_errno = ERRNO_LOCKFAILED;
@@ -2002,7 +2073,61 @@ if (!isdirectory)
           goto RETURN;
           }
 
-        Uchmod(mbx_lockname, 0600);
+        if (lstat(mbx_lockname, &lstatbuf) < 0)
+          {
+          addr->basic_errno = ERRNO_LOCKFAILED;
+          addr->message = string_sprintf("attempting to lstat open MBX "
+             "lock file %s: %s", mbx_lockname, strerror(errno));
+          goto RETURN;
+          }
+        if (fstat(mbx_lockfd, &statbuf2) < 0)
+          {
+          addr->basic_errno = ERRNO_LOCKFAILED;
+          addr->message = string_sprintf("attempting to stat fd of open MBX "
+              "lock file %s: %s", mbx_lockname, strerror(errno));
+          goto RETURN;
+          }
+
+        /*
+         * At this point:
+         *  statbuf: if exists, is file which existed prior to opening the
+         *           lockfile, might have been replaced since then
+         *  statbuf2: result of stat'ing the open fd, is what was actually
+         *            opened
+         *  lstatbuf: result of lstat'ing the filename immediately after
+         *            the open but there's a race condition again between
+         *            those two steps: before open, symlink to foo, after
+         *            open but before lstat have one of:
+         *             * was no symlink, so is the opened file
+         *               (we created it, no messing possible after that point)
+         *             * hardlink to foo
+         *             * symlink elsewhere
+         *             * hardlink elsewhere
+         *             * new file/other
+         * Don't want to compare to device of /tmp because some modern systems
+         * have regressed to having /tmp be the safe actual filesystem as
+         * valuable data, so is mostly worthless, unless we assume that *only*
+         * Linux systems do this and that all Linux has O_NOFOLLOW.  Something
+         * for further consideration.
+         * No point in doing a readlink on the lockfile as that will always be
+         * at a different point in time from when we open it, so tells us
+         * nothing; attempts to clean up and delete after ourselves would risk
+         * deleting a *third* filename.
+         */
+        if ((statbuf2.st_nlink > 1) ||
+            (lstatbuf.st_nlink > 1) ||
+            (!S_ISREG(lstatbuf.st_mode)) ||
+            (lstatbuf.st_dev != statbuf2.st_dev) ||
+            (lstatbuf.st_ino != statbuf2.st_ino))
+          {
+          addr->basic_errno = ERRNO_LOCKFAILED;
+          addr->message = string_sprintf("RACE CONDITION detected: "
+              "mismatch post-initial-checks between \"%s\" and opened "
+              "fd lead us to abort!", mbx_lockname);
+          goto RETURN;
+          }
+
+        (void)Uchmod(mbx_lockname, ob->lockfile_mode);
 
         if (apply_lock(mbx_lockfd, F_WRLCK, ob->use_fcntl,
             ob->lock_fcntl_timeout, ob->use_flock, ob->lock_flock_timeout) >= 0)
@@ -2023,7 +2148,7 @@ if (!isdirectory)
 
         DEBUG(D_transport) debug_printf("failed to lock %s: %s\n", mbx_lockname,
           strerror(errno));
-        close(mbx_lockfd);
+        (void)close(mbx_lockfd);
         mbx_lockfd = -1;
         }
       else
@@ -2039,7 +2164,7 @@ if (!isdirectory)
     DEBUG(D_transport)
       debug_printf("fcntl(), flock(), or MBX locking failed - retrying\n");
 
-    close(fd);
+    (void)close(fd);
     fd = -1;
     use_lstat = TRUE;             /* Reset to use lstat first */
 
@@ -2125,10 +2250,11 @@ else
     }
 
   #ifdef SUPPORT_MAILDIR
-  /* For a maildir delivery, ensure that all the relevant directories exist */
+  /* For a maildir delivery, ensure that all the relevant directories exist,
+  and a maildirfolder file if necessary. */
 
   if (mbformat == mbf_maildir && !maildir_ensure_directories(path, addr,
-    ob->create_directory, ob->dirmode))
+    ob->create_directory, ob->dirmode, ob->maildirfolder_create_regex))
       return FALSE;
   #endif  /* SUPPORT_MAILDIR */
 
@@ -2144,7 +2270,7 @@ else
     const uschar *error;
     int offset;
 
-    /* Compile the regex if there is one */
+    /* Compile the regex if there is one. */
 
     if (ob->quota_size_regex != NULL)
       {
@@ -2157,11 +2283,8 @@ else
           ob->quota_size_regex);
         return FALSE;
         }
-      else
-        {
-        DEBUG(D_transport) debug_printf("using regex for file sizes: %s\n",
-          ob->quota_size_regex);
-        }
+      DEBUG(D_transport) debug_printf("using regex for file sizes: %s\n",
+        ob->quota_size_regex);
       }
 
     /* Use an explicitly configured directory if set */
@@ -2212,6 +2335,8 @@ else
             {
             *slash = 0;
             check_path = new_check_path;
+            DEBUG(D_transport) debug_printf("maildirfolder file exists: "
+              "quota check directory changed to %s\n", check_path);
             }
           }
         }
@@ -2237,6 +2362,8 @@ else
 
     if (ob->maildir_dir_regex != NULL)
       {
+      int check_path_len = Ustrlen(check_path);
+
       dir_regex = pcre_compile(CS ob->maildir_dir_regex, PCRE_COPT,
         (const char **)&error, &offset, NULL);
       if (dir_regex == NULL)
@@ -2246,11 +2373,27 @@ else
           ob->maildir_dir_regex);
         return FALSE;
         }
-      else
+
+      DEBUG(D_transport)
+        debug_printf("using regex for maildir directory selection: %s\n",
+          ob->maildir_dir_regex);
+
+      /* Check to see if we are delivering into an ignored directory, that is,
+      if the delivery path starts with the quota check path, and the rest
+      of the deliver path matches the regex; if so, set a flag to disable quota
+      checking and maildirsize updating. */
+
+      if (Ustrncmp(path, check_path, check_path_len) == 0)
         {
-        DEBUG(D_transport)
-          debug_printf("using regex for maildir directory selection: %s\n",
-            ob->maildir_dir_regex);
+        uschar *s = path + check_path_len;
+        while (*s == '/') s++;
+        s = (*s == 0)? US "new" : string_sprintf("%s/new", s);
+        if (pcre_exec(dir_regex, NULL, CS s, Ustrlen(s), 0, 0, NULL, 0) < 0)
+          {
+          disable_quota = TRUE;
+          DEBUG(D_transport) debug_printf("delivery directory does not match "
+            "maildir_quota_directory_regex: disabling quota\n");
+          }
         }
       }
 
@@ -2261,6 +2404,7 @@ else
 
 /*  if (???? || ob->quota_value > 0) */
 
+    if (!disable_quota)
       {
       off_t size;
       int filecount;
@@ -2296,13 +2440,15 @@ else
     }
   #endif  /* SUPPORT_MAILDIR */
 
-  /* Otherwise (mailbox_size is not yet set), if we are going to do a quota
-  check later on, find the current size of the mailbox. (We don't need to check
-  ob->quota_filecount_value, because it can only be set if ob->quota_value is
-  set.) */
+  /* Otherwise if we are going to do a quota check later on, and the mailbox
+  size is not set, find the current size of the mailbox. Ditto for the file
+  count. Note that ob->quota_filecount_value cannot be set without
+  ob->quota_value being set. */
 
-  if ((mailbox_size < 0 || mailbox_filecount < 0) &&
-      (ob->quota_value > 0 || THRESHOLD_CHECK))
+  if (!disable_quota &&
+      (ob->quota_value > 0 || THRESHOLD_CHECK) &&
+      (mailbox_size < 0 ||
+        (mailbox_filecount < 0 && ob->quota_filecount_value > 0)))
     {
     off_t size;
     int filecount = 0;
@@ -2400,6 +2546,8 @@ else
         addr->message = string_sprintf ("failed to open %s (%d tr%s)",
           filename, i, (i == 1)? "y" : "ies");
         addr->basic_errno = errno;
+        if (errno == errno_quota || errno == ENOSPC)
+          addr->user_message = US"mailbox is full";
         return FALSE;
         }
 
@@ -2415,8 +2563,8 @@ else
     /* Why are these here? Put in because they are present in the non-maildir
     directory case above. */
 
-    Uchown(filename, uid, gid);
-    Uchmod(filename, mode);
+    (void)Uchown(filename, uid, gid);
+    (void)Uchmod(filename, mode);
     }
 
   #endif  /* SUPPORT_MAILDIR */
@@ -2457,8 +2605,8 @@ else
     /* Why are these here? Put in because they are present in the non-maildir
     directory case above. */
 
-    Uchown(filename, uid, gid);
-    Uchmod(filename, mode);
+    (void)Uchown(filename, uid, gid);
+    (void)Uchmod(filename, mode);
 
     /* Built a C stream from the open file descriptor. */
 
@@ -2468,7 +2616,7 @@ else
       addr->transport_return = PANIC;
       addr->message = string_sprintf("fdopen of %s ("
         "for %s transport) failed", filename, tblock->name);
-      close(fd);
+      (void)close(fd);
       Uunlink(filename);
       return FALSE;
       }
@@ -2486,7 +2634,7 @@ else
           addr->message = string_sprintf("Expansion of \"%s\" (mailstore "
             "prefix for %s transport) failed: %s", ob->mailstore_prefix,
             tblock->name, expand_string_message);
-          fclose(env_file);
+          (void)fclose(env_file);
           Uunlink(filename);
           return FALSE;
           }
@@ -2515,7 +2663,7 @@ else
           addr->message = string_sprintf("Expansion of \"%s\" (mailstore "
             "suffix for %s transport) failed: %s", ob->mailstore_suffix,
             tblock->name, expand_string_message);
-          fclose(env_file);
+          (void)fclose(env_file);
           Uunlink(filename);
           return FALSE;
           }
@@ -2549,8 +2697,8 @@ else
       Uunlink(filename);
       return FALSE;
       }
-    Uchown(dataname, uid, gid);
-    Uchmod(dataname, mode);
+    (void)Uchown(dataname, uid, gid);
+    (void)Uchmod(dataname, mode);
     }
 
   #endif  /* SUPPORT_MAILSTORE */
@@ -2559,8 +2707,8 @@ else
   /* In all cases of writing to a new file, ensure that the file which is
   going to be renamed has the correct ownership and mode. */
 
-  Uchown(filename, uid, gid);
-  Uchmod(filename, mode);
+  (void)Uchown(filename, uid, gid);
+  (void)Uchmod(filename, mode);
   }
 
 
@@ -2578,13 +2726,14 @@ with this message if quota_is_inclusive is set; if it is not set, the check
 is for the mailbox already being over quota (i.e. the current message is not
 included in the check). */
 
-if (ob->quota_value > 0)
+if (!disable_quota && ob->quota_value > 0)
   {
   DEBUG(D_transport)
     {
-    debug_printf("Exim quota = %.30g old size = %.30g this message = %d "
-      "(%sincluded)\n", (double)ob->quota_value, (double)mailbox_size,
-      message_size, ob->quota_is_inclusive? "" : "not ");
+    debug_printf("Exim quota = " OFF_T_FMT " old size = " OFF_T_FMT
+      " this message = %d (%sincluded)\n",
+      ob->quota_value, mailbox_size, message_size,
+      ob->quota_is_inclusive? "" : "not ");
     debug_printf("  file count quota = %d count = %d\n",
       ob->quota_filecount_value, mailbox_filecount);
     }
@@ -2731,7 +2880,7 @@ if (temp_file != NULL && ob->mbx_format)
   /* Preserve errno while closing the temporary file. */
 
   mbx_save_errno = errno;
-  fclose(temp_file);
+  (void)fclose(temp_file);
   errno = mbx_save_errno;
   }
 #endif  /* SUPPORT_MBX */
@@ -2739,7 +2888,7 @@ if (temp_file != NULL && ob->mbx_format)
 /* Force out the remaining data to check for any errors; some OS don't allow
 fsync() to be called for a FIFO. */
 
-if (yield == OK && !isfifo && fsync(fd) < 0) yield = DEFER;
+if (yield == OK && !isfifo && EXIMfsync(fd) < 0) yield = DEFER;
 
 /* Update message_size to the accurate count of bytes written, including
 added headers. */
@@ -2747,30 +2896,35 @@ added headers. */
 message_size = transport_count;
 
 /* If using a maildir++ quota file, add this message's size to it, and
-close the file descriptor. */
+close the file descriptor, except when the quota has been disabled because we
+are delivering into an uncounted folder. */
 
 #ifdef SUPPORT_MAILDIR
-if (yield == OK && maildirsize_fd >= 0)
-  maildir_record_length(maildirsize_fd, message_size);
-
-maildir_save_errno = errno;       /* Preserve errno while closing the file */
-close(maildirsize_fd);
-errno = maildir_save_errno;
+if (!disable_quota)
+  {
+  if (yield == OK && maildirsize_fd >= 0)
+    maildir_record_length(maildirsize_fd, message_size);
+  maildir_save_errno = errno;    /* Preserve errno while closing the file */
+  (void)close(maildirsize_fd);
+  errno = maildir_save_errno;
+  }
 #endif  /* SUPPORT_MAILDIR */
 
 /* If there is a quota warning threshold and we are have crossed it with this
 message, set the SPECIAL_WARN flag in the address, to cause a warning message
 to be sent. */
 
-if (THRESHOLD_CHECK)
+if (!disable_quota && THRESHOLD_CHECK)
   {
   off_t threshold = ob->quota_warn_threshold_value;
   if (ob->quota_warn_threshold_is_percent)
     threshold = (off_t)(((double)ob->quota_value * threshold) / 100);
   DEBUG(D_transport)
-    debug_printf("quota = %.30g threshold = %.30g old size = %.30g "
-      "message size = %d\n",
-      (double)ob->quota_value, (double)threshold, (double)mailbox_size,
+    debug_printf("quota = " OFF_T_FMT
+      " threshold = " OFF_T_FMT
+      " old size = " OFF_T_FMT
+      " message size = %d\n",
+      ob->quota_value, threshold, mailbox_size,
       message_size);
   if (mailbox_size <= threshold && mailbox_size + message_size > threshold)
     addr->special_action = SPECIAL_WARN;
@@ -2846,6 +3000,7 @@ if (yield != OK)
     #else
     addr->message = string_sprintf("mailbox is full");
     #endif  /* EDQUOT */
+    addr->user_message = US"mailbox is full";
     DEBUG(D_transport) debug_printf("System quota exceeded for %s%s%s\n",
       dataname,
       isdirectory? US"" : US": time since file read = ",
@@ -2916,7 +3071,7 @@ if (yield != OK)
   investigated so far have ftruncate(), whereas not all have the F_FREESP
   fcntl() call (BSDI & FreeBSD do not). */
 
-  if (!isdirectory) ftruncate(fd, saved_size);
+  if (!isdirectory) (void)ftruncate(fd, saved_size);
   }
 
 /* Handle successful writing - we want the modification time to be now for
@@ -3127,7 +3282,7 @@ if (mbx_lockfd >= 0)
       debug_printf("unlinking MBX lock file %s\n", mbx_lockname);
     Uunlink(mbx_lockname);
     }
-  close(mbx_lockfd);
+  (void)close(mbx_lockfd);
   }
 #endif  /* SUPPORT_MBX */