X-Git-Url: https://vcs.fsf.org/?a=blobdiff_plain;f=src%2Fsrc%2Ftransports%2Fappendfile.c;h=f56862beca3fcbb40e5d2c8c7f088e49c1a94e59;hb=19050083522f210e8bb4f1a5ba03928ed0077a49;hp=bb5d90bcb6b182be46d28882ef98f859dd3a3588;hpb=21c28500c0afea85a4acc9cd2e6c816522394431;p=exim.git diff --git a/src/src/transports/appendfile.c b/src/src/transports/appendfile.c index bb5d90bcb..f56862bec 100644 --- a/src/src/transports/appendfile.c +++ b/src/src/transports/appendfile.c @@ -1,10 +1,8 @@ -/* $Cambridge: exim/src/src/transports/appendfile.c,v 1.12 2006/02/10 16:29:20 ph10 Exp $ */ - /************************************************* * Exim - an Internet mail transport agent * *************************************************/ -/* Copyright (c) University of Cambridge 1995 - 2006 */ +/* Copyright (c) University of Cambridge 1995 - 2009 */ /* See the file NOTICE for conditions of use and distribution. */ @@ -21,7 +19,7 @@ supported only if SUPPORT_MBX is set. */ enum { mbf_unix, mbf_mbx, mbf_smail, mbf_maildir, mbf_mailstore }; -static char *mailbox_formats[] = { +static const char *mailbox_formats[] = { "unix", "mbx", "smail", "maildir", "mailstore" }; @@ -38,6 +36,10 @@ stored in the publicly visible instance block - these are flagged with the opt_public flag. */ optionlist appendfile_transport_options[] = { +#ifdef SUPPORT_MAILDIR + { "*expand_maildir_use_size_file", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, expand_maildir_use_size_file) }, +#endif { "*set_use_fcntl_lock",opt_bool | opt_hidden, (void *)offsetof(appendfile_transport_options_block, set_use_fcntl) }, { "*set_use_flock_lock",opt_bool | opt_hidden, @@ -105,8 +107,10 @@ optionlist appendfile_transport_options[] = { (void *)offsetof(appendfile_transport_options_block, maildir_retries) }, { "maildir_tag", opt_stringptr, (void *)offsetof(appendfile_transport_options_block, maildir_tag) }, - { "maildir_use_size_file", opt_bool, + { "maildir_use_size_file", opt_expand_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, @@ -182,8 +186,10 @@ appendfile_transport_options_block appendfile_transport_option_defaults = { 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)*/ @@ -247,6 +253,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 +262,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 +272,12 @@ int i; 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); /* Loop for quota, quota_filecount, quota_warn_threshold, mailbox_size, mailbox_filecount */ @@ -592,10 +606,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; @@ -1222,6 +1236,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; @@ -1329,10 +1344,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. */ @@ -1600,6 +1620,7 @@ if (!isdirectory) if (ob->use_lockfile) { + /* cf. exim_lock.c */ lockname = string_sprintf("%s.lock", filename); hitchname = string_sprintf( "%s.%s.%08x.%08x", lockname, primary_hostname, (unsigned int)(time(NULL)), (unsigned int)getpid()); @@ -1751,8 +1772,14 @@ if (!isdirectory) /* We have successfully created and opened the file. Ensure that the group and the mode are correct. */ - (void)Uchown(filename, uid, gid); - (void)Uchmod(filename, mode); + if(Uchown(filename, uid, gid) || Uchmod(filename, mode)) + { + addr->basic_errno = errno; + addr->message = string_sprintf("while setting perms on mailbox %s", + filename); + addr->transport_return = FAIL; + goto RETURN; + } } @@ -1793,6 +1820,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 @@ -1985,6 +2024,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) @@ -1992,6 +2033,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) @@ -2010,7 +2066,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; @@ -2019,7 +2087,61 @@ if (!isdirectory) goto RETURN; } - (void)Uchmod(mbx_lockname, 0600); + if (Ulstat(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) @@ -2142,10 +2264,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 */ @@ -2161,7 +2284,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) { @@ -2174,11 +2297,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 */ @@ -2229,6 +2349,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); } } } @@ -2254,6 +2376,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) @@ -2263,11 +2387,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"); + } } } @@ -2278,6 +2418,7 @@ else /* if (???? || ob->quota_value > 0) */ + if (!disable_quota) { off_t size; int filecount; @@ -2292,6 +2433,9 @@ else "%s/maildirsize", check_path); return FALSE; } + /* can also return -2, which means that the file was removed because of + raciness; but in this case, the size & filecount will still have been + updated. */ if (mailbox_size < 0) mailbox_size = size; if (mailbox_filecount < 0) mailbox_filecount = filecount; @@ -2313,13 +2457,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; @@ -2417,6 +2563,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; } @@ -2432,8 +2580,13 @@ else /* Why are these here? Put in because they are present in the non-maildir directory case above. */ - (void)Uchown(filename, uid, gid); - (void)Uchmod(filename, mode); + if(Uchown(filename, uid, gid) || Uchmod(filename, mode)) + { + addr->basic_errno = errno; + addr->message = string_sprintf("while setting perms on maildir %s", + filename); + return FALSE; + } } #endif /* SUPPORT_MAILDIR */ @@ -2474,8 +2627,13 @@ else /* Why are these here? Put in because they are present in the non-maildir directory case above. */ - (void)Uchown(filename, uid, gid); - (void)Uchmod(filename, mode); + if(Uchown(filename, uid, gid) || Uchmod(filename, mode)) + { + addr->basic_errno = errno; + addr->message = string_sprintf("while setting perms on file %s", + filename); + return FALSE; + } /* Built a C stream from the open file descriptor. */ @@ -2566,8 +2724,13 @@ else Uunlink(filename); return FALSE; } - (void)Uchown(dataname, uid, gid); - (void)Uchmod(dataname, mode); + if(Uchown(dataname, uid, gid) || Uchmod(dataname, mode)) + { + addr->basic_errno = errno; + addr->message = string_sprintf("while setting perms on file %s", + dataname); + return FALSE; + } } #endif /* SUPPORT_MAILSTORE */ @@ -2576,8 +2739,13 @@ 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. */ - (void)Uchown(filename, uid, gid); - (void)Uchmod(filename, mode); + if(Uchown(filename, uid, gid) || Uchmod(filename, mode)) + { + addr->basic_errno = errno; + addr->message = string_sprintf("while setting perms on file %s", + filename); + return FALSE; + } } @@ -2595,7 +2763,7 @@ 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) { @@ -2651,6 +2819,7 @@ if (yield == OK && ob->mbx_format) functions. */ transport_count = 0; +transport_newlines = 0; /* Write any configured prefix text first */ @@ -2676,21 +2845,26 @@ file, use its parent in the RCPT TO. */ if (yield == OK && ob->use_bsmtp) { transport_count = 0; + transport_newlines = 0; if (ob->use_crlf) cr = US"\r"; if (!transport_write_string(fd, "MAIL FROM:<%s>%s\n", return_path, cr)) yield = DEFER; else { address_item *a; + transport_newlines++; for (a = addr; a != NULL; a = a->next) { address_item *b = testflag(a, af_pfr)? a->parent: a; if (!transport_write_string(fd, "RCPT TO:<%s>%s\n", transport_rcpt_address(b, tblock->rcpt_include_affixes), cr)) { yield = DEFER; break; } + transport_newlines++; } if (yield == OK && !transport_write_string(fd, "DATA%s\n", cr)) yield = DEFER; + else + transport_newlines++; } } @@ -2723,8 +2897,10 @@ if (yield == OK && ob->message_suffix != NULL && ob->message_suffix[0] != 0) /* If batch smtp, write the terminating dot. */ -if (yield == OK && ob->use_bsmtp && - !transport_write_string(fd, ".%s\n", cr)) yield = DEFER; +if (yield == OK && ob->use_bsmtp ) { + if(!transport_write_string(fd, ".%s\n", cr)) yield = DEFER; + else transport_newlines++; +} /* If MBX format is being used, all that writing was to the temporary file. However, if there was an earlier failure (Exim quota exceeded, for example), @@ -2742,6 +2918,8 @@ if (temp_file != NULL && ob->mbx_format) if (yield == OK) { transport_count = 0; /* Reset transport count for actual write */ + /* No need to reset transport_newlines as we're just using a block copy + * routine so the number won't be affected */ yield = copy_mbx_message(fd, fileno(temp_file), saved_size); } else if (errno >= 0) dataname = US"temporary file"; @@ -2757,30 +2935,37 @@ 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. */ +/* Update message_size and message_linecount to the accurate count of bytes +written, including added headers. Note; we subtract 1 from message_linecount as +this variable doesn't count the new line between the header and the body of the +message. */ message_size = transport_count; +message_linecount = transport_newlines - 1; /* 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 */ -(void)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 */ + if (maildirsize_fd >= 0) + (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) @@ -2866,6 +3051,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 = ", @@ -2936,7 +3122,8 @@ if (yield != OK) investigated so far have ftruncate(), whereas not all have the F_FREESP fcntl() call (BSDI & FreeBSD do not). */ - if (!isdirectory) (void)ftruncate(fd, saved_size); + if (!isdirectory && ftruncate(fd, saved_size)) + DEBUG(D_transport) debug_printf("Error restting file size\n"); } /* Handle successful writing - we want the modification time to be now for