X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/184e88237dea64ce48076cdd0184612d057cbafd..d7978c0f8af20ff4c3f770589b1bb81568aecff3:/src/src/transports/appendfile.c diff --git a/src/src/transports/appendfile.c b/src/src/transports/appendfile.c index 8266bf9bb..1e92add35 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.20 2007/01/08 10:50:20 ph10 Exp $ */ - /************************************************* * Exim - an Internet mail transport agent * *************************************************/ -/* Copyright (c) University of Cambridge 1995 - 2007 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ /* See the file NOTICE for conditions of use and distribution. */ @@ -16,28 +14,16 @@ #endif -/* Encodings for mailbox formats, and their names. MBX format is actually -supported only if SUPPORT_MBX is set. */ - -enum { mbf_unix, mbf_mbx, mbf_smail, mbf_maildir, mbf_mailstore }; - -static char *mailbox_formats[] = { - "unix", "mbx", "smail", "maildir", "mailstore" }; - - -/* Check warn threshold only if quota size set or not a percentage threshold - percentage check should only be done if quota > 0 */ - -#define THRESHOLD_CHECK (ob->quota_warn_threshold_value > 0 && \ - (!ob->quota_warn_threshold_is_percent || ob->quota_value > 0)) - - /* Options specific to the appendfile transport. They must be in alphabetic order (note that "_" comes before the lower case letters). Some of them are 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,7 +91,7 @@ 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 ) }, @@ -168,6 +154,16 @@ address can appear in the tables drtables.c. */ int appendfile_transport_options_count = sizeof(appendfile_transport_options)/sizeof(optionlist); + +#ifdef MACRO_PREDEF + +/* Dummy values */ +appendfile_transport_options_block appendfile_transport_option_defaults = {0}; +void appendfile_transport_init(transport_instance *tblock) {} +BOOL appendfile_transport_entry(transport_instance *tblock, address_item *addr) {return FALSE;} + +#else /*!MACRO_PREDEF*/ + /* Default private options block for the appendfile transport. */ appendfile_transport_options_block appendfile_transport_option_defaults = { @@ -184,6 +180,7 @@ 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 */ @@ -231,10 +228,28 @@ appendfile_transport_options_block appendfile_transport_option_defaults = { FALSE, /* mailstore_format */ FALSE, /* mbx_format */ FALSE, /* quota_warn_threshold_is_percent */ - TRUE /* quota_is_inclusive */ + TRUE, /* quota_is_inclusive */ + FALSE, /* quota_no_check */ + FALSE /* quota_filecount_no_check */ }; +/* Encodings for mailbox formats, and their names. MBX format is actually +supported only if SUPPORT_MBX is set. */ + +enum { mbf_unix, mbf_mbx, mbf_smail, mbf_maildir, mbf_mailstore }; + +static const char *mailbox_formats[] = { + "unix", "mbx", "smail", "maildir", "mailstore" }; + + +/* Check warn threshold only if quota size set or not a percentage threshold + percentage check should only be done if quota > 0 */ + +#define THRESHOLD_CHECK (ob->quota_warn_threshold_value > 0 && \ + (!ob->quota_warn_threshold_is_percent || ob->quota_value > 0)) + + /************************************************* * Setup entry point * @@ -265,31 +280,36 @@ appendfile_transport_options_block *ob = (appendfile_transport_options_block *)(tblock->options_block); uschar *q = ob->quota; double default_value = 0.0; -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 */ -for (i = 0; i < 5; i++) +for (int i = 0; i < 5; i++) { double d; + int no_check = 0; uschar *which = NULL; - if (q == NULL) d = default_value; else + if (q == NULL) d = default_value; + else { uschar *rest; uschar *s = expand_string(q); - if (s == NULL) + if (!s) { *errmsg = string_sprintf("Expansion of \"%s\" in %s transport failed: " "%s", q, tblock->name, expand_string_message); - return search_find_defer? DEFER : FAIL; + return f.search_find_defer ? DEFER : FAIL; } d = Ustrtod(s, &rest); @@ -303,7 +323,8 @@ for (i = 0; i < 5; i++) else if (tolower(*rest) == 'g') { d *= 1024.0*1024.0*1024.0; rest++; } else if (*rest == '%' && i == 2) { - if (ob->quota_value <= 0 && !ob->maildir_use_size_file) d = 0; + if (ob->quota_value <= 0 && !ob->maildir_use_size_file) + d = 0; else if ((int)d < 0 || (int)d > 100) { *errmsg = string_sprintf("Invalid quota_warn_threshold percentage (%d)" @@ -314,6 +335,15 @@ for (i = 0; i < 5; i++) rest++; } + + /* For quota and quota_filecount there may be options + appended. Currently only "no_check", so we can be lazy parsing it */ + if (i < 2 && Ustrstr(rest, "/no_check") == rest) + { + no_check = 1; + rest += sizeof("/no_check") - 1; + } + while (isspace(*rest)) rest++; if (*rest != 0) @@ -329,39 +359,44 @@ for (i = 0; i < 5; i++) 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; + if (d >= 2.0*1024.0*1024.0*1024.0 && sizeof(off_t) <= 4) + which = US"quota"; + ob->quota_value = (off_t)d; + ob->quota_no_check = no_check; + 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; + if (d >= 2.0*1024.0*1024.0*1024.0) + which = US"quota_filecount"; + ob->quota_filecount_value = (int)d; + ob->quota_filecount_no_check = no_check; + 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; + 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; + 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 (d >= 2.0*1024.0*1024.0*1024.0) + which = US"mailbox_filecount"; + ob->mailbox_filecount_value = (int)d; + break; } - if (which != NULL) + if (which) { *errmsg = string_sprintf("%s value %.10g is too large (overflow) in " "%s transport", which, d, tblock->name); @@ -543,12 +578,12 @@ else if (ob->dirname == NULL && !ob->maildir_format && !ob->mailstore_format) driver options. Only one of body_only and headers_only can be set. */ ob->options |= - (tblock->body_only? topt_no_headers : 0) | - (tblock->headers_only? topt_no_body : 0) | - (tblock->return_path_add? topt_add_return_path : 0) | - (tblock->delivery_date_add? topt_add_delivery_date : 0) | - (tblock->envelope_to_add? topt_add_envelope_to : 0) | - ((ob->use_crlf || ob->mbx_format)? topt_use_crlf : 0); + (tblock->body_only ? topt_no_headers : 0) | + (tblock->headers_only ? topt_no_body : 0) | + (tblock->return_path_add ? topt_add_return_path : 0) | + (tblock->delivery_date_add ? topt_add_delivery_date : 0) | + (tblock->envelope_to_add ? topt_add_envelope_to : 0) | + ((ob->use_crlf || ob->mbx_format) ? topt_use_crlf : 0); } @@ -578,7 +613,6 @@ notify_comsat(uschar *user, off_t offset) { struct servent *sp; host_item host; -host_item *h; uschar buffer[256]; DEBUG(D_transport) debug_printf("notify_comsat called\n"); @@ -612,19 +646,18 @@ if (host_find_byname(&host, NULL, 0, NULL, FALSE) == HOST_FIND_FAILED) host.address = US"127.0.0.1"; -for (h = &host; h != NULL; h = h->next) +for (host_item * h = &host; h; h = h->next) { int sock, rc; - int host_af = (Ustrchr(h->address, ':') != NULL)? AF_INET6 : AF_INET; + int host_af = Ustrchr(h->address, ':') != NULL ? AF_INET6 : AF_INET; DEBUG(D_transport) debug_printf("calling comsat on %s\n", h->address); - sock = ip_socket(SOCK_DGRAM, host_af); - if (sock < 0) continue; + if ((sock = ip_socket(SOCK_DGRAM, host_af)) < 0) continue; /* Connect never fails for a UDP socket, so don't set a timeout. */ - (void)ip_connect(sock, host_af, h->address, ntohs(sp->s_port), 0); + (void)ip_connect(sock, host_af, h->address, ntohs(sp->s_port), 0, NULL); rc = send(sock, buffer, Ustrlen(buffer) + 1, 0); (void)close(sock); @@ -657,7 +690,7 @@ Returns: pointer to the required transport, or NULL transport_instance * check_file_format(int cfd, transport_instance *tblock, address_item *addr) { -uschar *format = +const uschar *format = ((appendfile_transport_options_block *)(tblock->options_block))->file_format; uschar data[256]; int len = read(cfd, data, sizeof(data)); @@ -672,15 +705,15 @@ if (len == 0) return tblock; /* Search the formats for a match */ -while ((s = string_nextinlist(&format,&sep,big_buffer,big_buffer_size))!= NULL) +while ((s = string_nextinlist(&format,&sep,big_buffer,big_buffer_size))) { int slen = Ustrlen(s); BOOL match = len >= slen && Ustrncmp(data, s, slen) == 0; uschar *tp = string_nextinlist(&format, &sep, big_buffer, big_buffer_size); - if (match) + + if (match && tp) { - transport_instance *tt; - for (tt = transports; tt != NULL; tt = tt->next) + for (transport_instance * tt = transports; tt; tt = tt->next) if (Ustrcmp(tp, tt->name) == 0) { DEBUG(D_transport) @@ -848,10 +881,10 @@ if (dofcntl) { if (fcntltime > 0) { - alarm(fcntltime); + ALARM(fcntltime); yield = fcntl(fd, F_SETLKW, &lock_data); save_errno = errno; - alarm(0); + ALARM_CLR(0); errno = save_errno; } else yield = fcntl(fd, F_SETLK, &lock_data); @@ -860,13 +893,13 @@ if (dofcntl) #ifndef NO_FLOCK if (doflock && (yield >= 0)) { - int flocktype = (fcntltype == F_WRLCK)? LOCK_EX : LOCK_SH; + int flocktype = (fcntltype == F_WRLCK) ? LOCK_EX : LOCK_SH; if (flocktime > 0) { - alarm(flocktime); + ALARM(flocktime); yield = flock(fd, flocktype); save_errno = errno; - alarm(0); + ALARM_CLR(0); errno = save_errno; } else yield = flock(fd, flocktype | LOCK_NB); @@ -909,19 +942,19 @@ copy_mbx_message(int to_fd, int from_fd, off_t saved_size) int used; off_t size; struct stat statbuf; +transport_ctx tctx = { .u={.fd = to_fd}, .options = topt_not_socket }; /* If the current mailbox size is zero, write a header block */ if (saved_size == 0) { - int i; uschar *s; memset (deliver_out_buffer, '\0', MBX_HDRSIZE); sprintf(CS(s = deliver_out_buffer), "*mbx*\015\012%08lx00000000\015\012", (long int)time(NULL)); - for (i = 0; i < MBX_NUSERFLAGS; i++) + for (int i = 0; i < MBX_NUSERFLAGS; i++) sprintf (CS(s += Ustrlen(s)), "\015\012"); - if (!transport_write_block (to_fd, deliver_out_buffer, MBX_HDRSIZE)) + if (!transport_write_block (&tctx, deliver_out_buffer, MBX_HDRSIZE, FALSE)) return DEFER; } @@ -939,7 +972,7 @@ used = Ustrlen(deliver_out_buffer); /* Rewind the temporary file, and copy it over in chunks. */ -lseek(from_fd, 0 , SEEK_SET); +if (lseek(from_fd, 0 , SEEK_SET) < 0) return DEFER; while (size > 0) { @@ -950,7 +983,7 @@ while (size > 0) if (len == 0) errno = ERRNO_MBXLENGTH; return DEFER; } - if (!transport_write_block(to_fd, deliver_out_buffer, used + len)) + if (!transport_write_block(&tctx, deliver_out_buffer, used + len, FALSE)) return DEFER; size -= len; used = 0; @@ -1010,10 +1043,10 @@ if (deliver_home != NULL && create_file != create_anywhere) #ifndef NO_REALPATH if (yield && create_file == create_belowhome) { - uschar *slash, *next; + uschar *next; uschar *rp = NULL; - for (slash = Ustrrchr(file, '/'); /* There is known to be one */ - rp == NULL && slash > file; /* Stop if reached beginning */ + for (uschar * slash = Ustrrchr(file, '/'); /* There is known to be one */ + rp == NULL && slash > file; /* Stop if reached beginning */ slash = next) { *slash = 0; @@ -1139,7 +1172,7 @@ directory name) is given, that is, when appending to a single file: Open with O_WRONLY + O_EXCL + O_CREAT with configured mode, unless we know this is via a symbolic link (only possible if allow_symlinks is set), in - which case don't use O_EXCL, as it dosn't work. + which case don't use O_EXCL, as it doesn't work. If open fails because the file already exists, go to (6f). To avoid looping for ever in a situation where the file is continuously being @@ -1236,7 +1269,7 @@ BOOL wait_for_tick = FALSE; uid_t uid = geteuid(); /* See note above */ gid_t gid = getegid(); int mbformat; -int mode = (addr->mode > 0)? addr->mode : ob->mode; +int mode = (addr->mode > 0) ? addr->mode : ob->mode; off_t saved_size = -1; off_t mailbox_size = ob->mailbox_size_value; int mailbox_filecount = ob->mailbox_filecount_value; @@ -1314,7 +1347,7 @@ if ((ob->maildir_format || ob->mailstore_format) && !isdirectory) addr->transport_return = PANIC; addr->message = string_sprintf("mail%s_format requires \"directory\" " "to be specified for the %s transport", - ob->maildir_format? "dir" : "store", tblock->name); + ob->maildir_format ? "dir" : "store", tblock->name); return FALSE; } @@ -1341,11 +1374,8 @@ if (path[0] != '/') to the true local part. */ if (testflag(addr, af_file)) - { - address_item *addr2; - for (addr2 = addr; addr2 != NULL; addr2 = addr2->next) + for (address_item * 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. */ @@ -1354,10 +1384,10 @@ if (isdirectory) { mbformat = #ifdef SUPPORT_MAILDIR - (ob->maildir_format)? mbf_maildir : + (ob->maildir_format) ? mbf_maildir : #endif #ifdef SUPPORT_MAILSTORE - (ob->mailstore_format)? mbf_mailstore : + (ob->mailstore_format) ? mbf_mailstore : #endif mbf_smail; } @@ -1365,7 +1395,7 @@ else { mbformat = #ifdef SUPPORT_MBX - (ob->mbx_format)? mbf_mbx : + (ob->mbx_format) ? mbf_mbx : #endif mbf_unix; } @@ -1373,29 +1403,32 @@ else DEBUG(D_transport) { debug_printf("appendfile: mode=%o notify_comsat=%d quota=" OFF_T_FMT + "%s%s" " 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, ob->quota_value, + ob->quota_no_check ? " (no_check)" : "", + ob->quota_filecount_no_check ? " (no_check_filecount)" : "", ob->quota_warn_threshold_value, - ob->quota_warn_threshold_is_percent? "%" : "", - isdirectory? "directory" : "file", + ob->quota_warn_threshold_is_percent ? "%" : "", + isdirectory ? "directory" : "file", path, mailbox_formats[mbformat], - (ob->message_prefix == NULL)? US"null" : string_printing(ob->message_prefix), - (ob->message_suffix == NULL)? US"null" : string_printing(ob->message_suffix), - (ob->maildir_use_size_file)? "yes" : "no"); + (ob->message_prefix == NULL) ? US"null" : string_printing(ob->message_prefix), + (ob->message_suffix == NULL) ? US"null" : string_printing(ob->message_suffix), + (ob->maildir_use_size_file) ? "yes" : "no"); if (!isdirectory) debug_printf(" locking by %s%s%s%s%s\n", - ob->use_lockfile? "lockfile " : "", - ob->use_mbx_lock? "mbx locking (" : "", - ob->use_fcntl? "fcntl " : "", - ob->use_flock? "flock" : "", - ob->use_mbx_lock? ")" : ""); + ob->use_lockfile ? "lockfile " : "", + ob->use_mbx_lock ? "mbx locking (" : "", + ob->use_fcntl ? "fcntl " : "", + ob->use_flock ? "flock" : "", + ob->use_mbx_lock ? ")" : ""); } /* If the -N option is set, can't do any more. */ -if (dont_deliver) +if (f.dont_deliver) { DEBUG(D_transport) debug_printf("*** delivery by %s transport bypassed by -N option\n", @@ -1613,6 +1646,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()); @@ -1701,7 +1735,7 @@ if (!isdirectory) int sleep_before_retry = TRUE; file_opened = FALSE; - if((use_lstat? Ulstat(filename, &statbuf) : Ustat(filename, &statbuf)) != 0) + if((use_lstat ? Ulstat(filename, &statbuf) : Ustat(filename, &statbuf)) != 0) { /* Let's hope that failure to stat (other than non-existence) is a rare event. */ @@ -1748,7 +1782,7 @@ if (!isdirectory) get a shared lock. */ fd = Uopen(filename, O_RDWR | O_APPEND | O_CREAT | - (use_lstat? O_EXCL : 0), mode); + (use_lstat ? O_EXCL : 0), mode); if (fd < 0) { if (errno == EEXIST) continue; @@ -1764,8 +1798,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; + } } @@ -1791,7 +1831,7 @@ if (!isdirectory) addr->basic_errno = ERRNO_BADUGID; addr->message = string_sprintf("mailbox %s%s has wrong uid " "(%ld != %ld)", filename, - islink? " (symlink)" : "", + islink ? " (symlink)" : "", (long int)(statbuf.st_uid), (long int)uid); goto RETURN; } @@ -1802,10 +1842,22 @@ if (!isdirectory) { addr->basic_errno = ERRNO_BADUGID; addr->message = string_sprintf("mailbox %s%s has wrong gid (%d != %d)", - filename, islink? " (symlink)" : "", statbuf.st_gid, gid); + filename, islink ? " (symlink)" : "", statbuf.st_gid, gid); 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 @@ -1827,7 +1879,7 @@ if (!isdirectory) { addr->basic_errno = ERRNO_NOTREGULAR; addr->message = string_sprintf("mailbox %s is not a regular file%s", - filename, ob->allow_fifo? " or named pipe" : ""); + filename, ob->allow_fifo ? " or named pipe" : ""); goto RETURN; } @@ -1876,7 +1928,7 @@ if (!isdirectory) a FIFO is opened WRONLY + NDELAY so that it fails if there is no process reading the pipe. */ - fd = Uopen(filename, isfifo? (O_WRONLY|O_NDELAY) : (O_RDWR|O_APPEND), + fd = Uopen(filename, isfifo ? (O_WRONLY|O_NDELAY) : (O_RDWR|O_APPEND), mode); if (fd < 0) { @@ -1923,7 +1975,7 @@ if (!isdirectory) { addr->basic_errno = ERRNO_INODECHANGED; addr->message = string_sprintf("opened mailbox %s inode number changed " - "from %d to %ld", filename, inode, statbuf.st_ino); + "from " INO_T_FMT " to " INO_T_FMT, filename, inode, statbuf.st_ino); addr->special_action = SPECIAL_FREEZE; goto RETURN; } @@ -1937,7 +1989,7 @@ if (!isdirectory) addr->basic_errno = ERRNO_NOTREGULAR; addr->message = string_sprintf("opened mailbox %s is no longer a %s", filename, - isfifo? "named pipe" : "regular file"); + isfifo ? "named pipe" : "regular file"); addr->special_action = SPECIAL_FREEZE; goto RETURN; } @@ -1998,6 +2050,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) @@ -2005,6 +2059,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) @@ -2023,7 +2092,19 @@ if (!isdirectory) } } - mbx_lockfd = Uopen(mbx_lockname, O_RDWR | O_CREAT, ob->lockfile_mode); + /* 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; @@ -2032,6 +2113,60 @@ if (!isdirectory) goto RETURN; } + 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, @@ -2292,7 +2427,7 @@ else { uschar *s = path + check_path_len; while (*s == '/') s++; - s = (*s == 0)? US "new" : string_sprintf("%s/new", 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; @@ -2324,6 +2459,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; @@ -2406,7 +2544,7 @@ else $message_size is accurately known. */ if (nametag != NULL && expand_string(nametag) == NULL && - !expand_string_forcedfail) + !f.expand_string_forcedfail) { addr->transport_return = PANIC; addr->message = string_sprintf("Expansion of \"%s\" (maildir_tag " @@ -2423,13 +2561,13 @@ else checked at the end, to make sure we don't release this process until the clock has ticked. */ - for (i = 1;; i++) + for (int i = 1;; i++) { uschar *basename; (void)gettimeofday(&msg_tv, NULL); - basename = string_sprintf("%lu.H%luP%lu.%s", msg_tv.tv_sec, - msg_tv.tv_usec, getpid(), primary_hostname); + basename = string_sprintf(TIME_T_FMT ".H%luP" PID_T_FMT ".%s", + msg_tv.tv_sec, msg_tv.tv_usec, getpid(), primary_hostname); filename = dataname = string_sprintf("tmp/%s", basename); newname = string_sprintf("new/%s", basename); @@ -2449,8 +2587,10 @@ else if (i >= ob->maildir_retries) { addr->message = string_sprintf ("failed to open %s (%d tr%s)", - filename, i, (i == 1)? "y" : "ies"); + 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; } @@ -2466,8 +2606,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 */ @@ -2482,7 +2627,6 @@ else else { FILE *env_file; - address_item *taddr; mailstore_basename = string_sprintf("%s/%s-%s", path, message_id, string_base62((long int)getpid())); @@ -2508,8 +2652,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. */ @@ -2531,7 +2680,7 @@ else uschar *s = expand_string(ob->mailstore_prefix); if (s == NULL) { - if (!expand_string_forcedfail) + if (!f.expand_string_forcedfail) { addr->transport_return = PANIC; addr->message = string_sprintf("Expansion of \"%s\" (mailstore " @@ -2552,7 +2701,7 @@ else fprintf(env_file, "%s\n", sender_address); - for (taddr = addr; taddr!= NULL; taddr = taddr->next) + for (address_item * taddr = addr; taddr; taddr = taddr->next) fprintf(env_file, "%s@%s\n", taddr->local_part, taddr->domain); if (ob->mailstore_suffix != NULL) @@ -2560,7 +2709,7 @@ else uschar *s = expand_string(ob->mailstore_suffix); if (s == NULL) { - if (!expand_string_forcedfail) + if (!f.expand_string_forcedfail) { addr->transport_return = PANIC; addr->message = string_sprintf("Expansion of \"%s\" (mailstore " @@ -2600,8 +2749,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 */ @@ -2610,8 +2764,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; + } } @@ -2636,25 +2795,37 @@ if (!disable_quota && ob->quota_value > 0) 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 "); + ob->quota_is_inclusive ? "" : "not "); debug_printf(" file count quota = %d count = %d\n", ob->quota_filecount_value, mailbox_filecount); } - if (mailbox_size + (ob->quota_is_inclusive? message_size:0) > ob->quota_value) - { - DEBUG(D_transport) debug_printf("mailbox quota exceeded\n"); - yield = DEFER; - errno = ERRNO_EXIMQUOTA; - } - else if (ob->quota_filecount_value > 0 && - mailbox_filecount + (ob->quota_is_inclusive ? 1:0) > - ob->quota_filecount_value) + + if (mailbox_size + (ob->quota_is_inclusive ? message_size:0) > ob->quota_value) { - DEBUG(D_transport) debug_printf("mailbox file count quota exceeded\n"); - yield = DEFER; - errno = ERRNO_EXIMQUOTA; - filecount_msg = US" filecount"; + + if (!ob->quota_no_check) + { + DEBUG(D_transport) debug_printf("mailbox quota exceeded\n"); + yield = DEFER; + errno = ERRNO_EXIMQUOTA; + } + else DEBUG(D_transport) debug_printf("mailbox quota exceeded but ignored\n"); + } + + if (ob->quota_filecount_value > 0 + && mailbox_filecount + (ob->quota_is_inclusive ? 1:0) > + ob->quota_filecount_value) + if(!ob->quota_filecount_no_check) + { + DEBUG(D_transport) debug_printf("mailbox file count quota exceeded\n"); + yield = DEFER; + errno = ERRNO_EXIMQUOTA; + filecount_msg = US" filecount"; + } + else DEBUG(D_transport) if (ob->quota_filecount_no_check) + debug_printf("mailbox file count quota exceeded but ignored\n"); + } /* If we are writing in MBX format, what we actually do is to write the message @@ -2685,6 +2856,7 @@ if (yield == OK && ob->mbx_format) functions. */ transport_count = 0; +transport_newlines = 0; /* Write any configured prefix text first */ @@ -2710,21 +2882,25 @@ 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; - for (a = addr; a != NULL; a = a->next) + transport_newlines++; + for (address_item * a = addr; a != NULL; a = a->next) { - address_item *b = testflag(a, af_pfr)? a->parent: a; + 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++; } } @@ -2733,9 +2909,15 @@ at initialization time. */ if (yield == OK) { - if (!transport_write_message(addr, fd, ob->options, 0, tblock->add_headers, - tblock->remove_headers, ob->check_string, ob->escape_string, - tblock->rewrite_rules, tblock->rewrite_existflags)) + transport_ctx tctx = { + .u = {.fd=fd}, + .tblock = tblock, + .addr = addr, + .check_string = ob->check_string, + .escape_string = ob->escape_string, + .options = ob->options | topt_not_socket + }; + if (!transport_write_message(&tctx, 0)) yield = DEFER; } @@ -2757,8 +2939,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), @@ -2776,6 +2960,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"; @@ -2791,12 +2977,15 @@ 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, except when the quota has been disabled because we @@ -2808,7 +2997,8 @@ 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); + if (maildirsize_fd >= 0) + (void)close(maildirsize_fd); errno = maildir_save_errno; } #endif /* SUPPORT_MAILDIR */ @@ -2881,7 +3071,7 @@ if (yield != OK) } else /* Want a repeatable time when in test harness */ { - addr->more_errno = running_in_test_harness? 10 : + addr->more_errno = f.running_in_test_harness ? 10 : (int)time(NULL) - statbuf.st_mtime; } DEBUG(D_transport) @@ -2903,10 +3093,11 @@ 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 = ", - isdirectory? US"" : readconf_printtime(addr->more_errno)); + isdirectory ? US"" : US": time since file read = ", + isdirectory ? US"" : readconf_printtime(addr->more_errno)); } /* Handle Exim's own quota-imposition */ @@ -2919,8 +3110,8 @@ if (yield != OK) addr->user_message = US"mailbox is full"; DEBUG(D_transport) debug_printf("Exim%s quota exceeded for %s%s%s\n", filecount_msg, dataname, - isdirectory? US"" : US": time since file read = ", - isdirectory? US"" : readconf_printtime(addr->more_errno)); + isdirectory ? US"" : US": time since file read = ", + isdirectory ? US"" : readconf_printtime(addr->more_errno)); } /* Handle a process failure while writing via a filter; the return @@ -2931,7 +3122,7 @@ if (yield != OK) yield = PANIC; addr->message = string_sprintf("transport filter process failed (%d) " "while writing to %s%s", addr->more_errno, dataname, - (addr->more_errno == EX_EXECFAILED)? ": unable to execute command" : ""); + (addr->more_errno == EX_EXECFAILED) ? ": unable to execute command" : ""); } /* Handle failure to expand header changes */ @@ -2973,7 +3164,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 resetting file size\n"); } /* Handle successful writing - we want the modification time to be now for @@ -3004,7 +3196,7 @@ else { addr->basic_errno = errno; addr->message = string_sprintf("close() error for %s", - (ob->mailstore_format)? dataname : filename); + (ob->mailstore_format) ? dataname : filename); yield = DEFER; } @@ -3038,11 +3230,10 @@ else if (newname == NULL) { - int i; uschar *renameleaf; uschar *old_renameleaf = US""; - for (i = 0; ; sleep(1), i++) + for (int i = 0; ; sleep(1), i++) { deliver_inode = statbuf.st_ino; renameleaf = expand_string(ob->dirfilename); @@ -3218,4 +3409,5 @@ put in the first address of a batch. */ return FALSE; } +#endif /*!MACRO_PREDEF*/ /* End of transport/appendfile.c */