-/* $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. */
(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,
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)*/
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
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);
addrlist = addrlist; /* Keep picky compilers happy */
dummy = dummy;
+uid = uid;
+gid = gid;
/* Loop for quota, quota_filecount, quota_warn_threshold, mailbox_size,
mailbox_filecount */
for (i = 0; i < 5; i++)
{
double d;
+ uschar *which = NULL;
if (q == NULL) d = default_value; else
{
}
}
+ /* 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;
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)
{
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;
(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)
{
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;
}
}
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;
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. */
uschar *path;
struct utimbuf times;
struct timeval msg_tv;
+BOOL disable_quota = FALSE;
BOOL isdirectory = FALSE;
BOOL isfifo = FALSE;
BOOL wait_for_tick = FALSE;
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. */
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],
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.*/
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)
{
/* 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);
}
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
#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)
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)
}
}
- 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;
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)
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
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 */
}
#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 */
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)
{
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 */
{
*slash = 0;
check_path = new_check_path;
+ DEBUG(D_transport) debug_printf("maildirfolder file exists: "
+ "quota check directory changed to %s\n", check_path);
}
}
}
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)
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");
+ }
}
}
/* if (???? || ob->quota_value > 0) */
+ if (!disable_quota)
{
off_t size;
int filecount;
}
#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;
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;
}
/* 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 */
/* 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. */
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;
}
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;
}
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;
}
Uunlink(filename);
return FALSE;
}
- Uchown(dataname, uid, gid);
- Uchmod(dataname, mode);
+ (void)Uchown(dataname, uid, gid);
+ (void)Uchmod(dataname, mode);
}
#endif /* SUPPORT_MAILSTORE */
/* 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);
}
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);
}
/* 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 */
/* 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. */
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;
#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 = ",
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
debug_printf("unlinking MBX lock file %s\n", mbx_lockname);
Uunlink(mbx_lockname);
}
- close(mbx_lockfd);
+ (void)close(mbx_lockfd);
}
#endif /* SUPPORT_MBX */