* Exim - an Internet mail transport agent *
*************************************************/
+/* Copyright (c) The Exim Maintainers 2020 - 2024 */
/* Copyright (c) University of Cambridge 1995 - 2018 */
/* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
/* General functions concerned with transportation, and generic options for all
transports. */
for(;;)
{
- fd_set fds;
/* This code makes use of alarm() in order to implement the timeout. This
isn't a very tidy way of doing things. Using non-blocking I/O with select()
provides a neater approach. However, I don't know how to do this when TLS is
}
else /* Timeout wanted. */
{
+ sigalrm_seen = FALSE;
ALARM(local_timeout);
rc = tpt_write(fd, block, len, more, tctx->options);
save_errno = errno;
if (rc >= 0 || errno != ENOTCONN || connretry <= 0)
break;
- FD_ZERO(&fds); FD_SET(fd, &fds);
- select(fd+1, NULL, &fds, NULL, NULL); /* could set timout? */
+ poll_one_fd(fd, POLLOUT, -1); /* could set timeout? retval check? */
connretry--;
}
*/
BOOL
-write_chunk(transport_ctx * tctx, uschar *chunk, int len)
+write_chunk(transport_ctx * tctx, const uschar * chunk, int len)
{
-uschar *start = chunk;
-uschar *end = chunk + len;
+const uschar * start = chunk;
+const uschar * end = chunk + len;
int mlen = DELIVER_OUT_BUFFER_SIZE - nl_escape_length - 2;
/* The assumption is made that the check string will never stretch over move
for possible escaping. The code for the non-NL route should be as fast as
possible. */
-for (uschar * ptr = start; ptr < end; ptr++)
+for (const uschar * ptr = start; ptr < end; ptr++)
{
int ch, len;
Returns: a string
*/
-uschar *
+const uschar *
transport_rcpt_address(address_item *addr, BOOL include_affixes)
{
uschar *at;
for (ppp = *pdlist; ppp; ppp = ppp->next) if (p == ppp->ptr) return TRUE;
-ppp = store_get(sizeof(struct aci), FALSE);
+ppp = store_get(sizeof(struct aci), GET_UNTAINTED);
ppp->next = *pdlist;
*pdlist = ppp;
ppp->ptr = p;
/* Remember what we have output, and output it. */
-ppp = store_get(sizeof(struct aci), FALSE);
+ppp = store_get(sizeof(struct aci), GET_UNTAINTED);
ppp->next = *pplist;
*pplist = ppp;
ppp->ptr = pp;
*/
BOOL
transport_headers_send(transport_ctx * tctx,
- BOOL (*sendfn)(transport_ctx * tctx, uschar * s, int len))
+ BOOL (*sendfn)(transport_ctx * tctx, const uschar * s, int len))
{
-const uschar *list;
+const uschar * list;
transport_instance * tblock = tctx ? tctx->tblock : NULL;
address_item * addr = tctx ? tctx->addr : NULL;
if (include_header)
{
+ int len;
if (tblock && tblock->rewrite_rules)
{
rmark reset_point = store_mark();
- header_line *hh;
+ header_line * hh;
if ((hh = rewrite_header(h, NULL, NULL, tblock->rewrite_rules,
tblock->rewrite_existflags, FALSE)))
{
- if (!sendfn(tctx, hh->text, hh->slen)) return FALSE;
+ len = hh->slen;
+ if (tctx->options & topt_truncate_headers && len > 998) len = 998;
+ if (!sendfn(tctx, hh->text, len)) return FALSE;
store_reset(reset_point);
continue; /* With the next header line */
}
/* Either no rewriting rules, or it didn't get rewritten */
- if (!sendfn(tctx, h->text, h->slen)) return FALSE;
+ len = h->slen;
+ if (tctx->options & topt_truncate_headers && len > 998) len = 998;
+ if (!sendfn(tctx, h->text, len)) return FALSE;
}
/* Header removed */
else
- DEBUG(D_transport) debug_printf("removed header line:\n%s---\n", h->text);
+ DEBUG(D_transport) debug_printf("removed header line:\n %s---\n", h->text);
}
/* Add on any address-specific headers. If there are multiple addresses,
if (addr)
{
- header_line *hprev = addr->prop.extra_headers;
- header_line *hnext, * h;
+ header_line * hprev = addr->prop.extra_headers, * hnext, * h;
+
for (int i = 0; i < 2; i++)
for (h = hprev, hprev = NULL; h; h = hnext)
{
{
if (!sendfn(tctx, h->text, h->slen)) return FALSE;
DEBUG(D_transport)
- debug_printf("added header line(s):\n%s---\n", h->text);
+ debug_printf("added header line(s):\n %s---\n", h->text);
}
}
}
return FALSE;
DEBUG(D_transport)
{
- debug_printf("added header line:\n%s", s);
+ debug_printf("added header line:\n %s", s);
if (s[len-1] != '\n') debug_printf("\n");
debug_printf("---\n");
}
Arguments:
tctx
- (fd, msg) Either and fd, to write the message to,
+ (fd, msg) Either an fd, to write the message to,
or a string: if null write message to allocated space
otherwire take content as headers.
addr (chain of) addresses (for extra headers), or NULL;
add_delivery_date if TRUE, add a "delivery-date" header
use_crlf if TRUE, turn NL into CR LF
end_dot if TRUE, send a terminating "." line at the end
+ no_flush if TRUE, do not flush at end
no_headers if TRUE, omit the headers
no_body if TRUE, omit the body
check_string a string to check for at the start of lines, or NULL
if (tctx->options & topt_add_return_path)
{
- uschar buffer[ADDRESS_MAXLENGTH + 20];
- int n = sprintf(CS buffer, "Return-path: <%.*s>\n", ADDRESS_MAXLENGTH,
- return_path);
- if (!write_chunk(tctx, buffer, n)) goto bad;
+ int n;
+ uschar * s = string_sprintf("Return-path: <%.*s>\n%n",
+ EXIM_EMAILADDR_MAX, return_path, &n);
+ if (!write_chunk(tctx, s, n)) goto bad;
}
/* Add envelope-to: if requested */
if (!(tctx->options & topt_no_body))
{
if ((fsize = lseek(deliver_datafile, 0, SEEK_END)) < 0) return FALSE;
- fsize -= SPOOL_DATA_START_OFFSET;
+ fsize -= spool_data_start_offset(message_id);
if (size_limit > 0 && fsize > size_limit)
fsize = size_limit;
size = hsize + fsize;
)
{
ssize_t copied = 0;
- off_t offset = SPOOL_DATA_START_OFFSET;
+ off_t offset = spool_data_start_offset(message_id);
/* Write out any header data in the buffer */
nl_check_length = abs(nl_check_length);
nl_partial_match = 0;
- if (lseek(deliver_datafile, SPOOL_DATA_START_OFFSET, SEEK_SET) < 0)
+ if (lseek(deliver_datafile, spool_data_start_offset(message_id), SEEK_SET) < 0)
return FALSE;
while ( (len = MIN(DELIVER_IN_BUFFER_SIZE, size)) > 0
&& (len = read(deliver_datafile, deliver_in_buffer, len)) > 0)
/* If requested, add a terminating "." line (SMTP output). */
-if (tctx->options & topt_end_dot && !write_chunk(tctx, US".\n", 2))
- return FALSE;
+if (tctx->options & topt_end_dot)
+ {
+ smtp_debug_cmd(US".", 0);
+ if (!write_chunk(tctx, US".\n", 2))
+ return FALSE;
+ }
/* Write out any remaining data in the buffer before returning. */
-return (len = chunk_ptr - deliver_out_buffer) <= 0 ||
- transport_write_block(tctx, deliver_out_buffer, len, FALSE);
+return (len = chunk_ptr - deliver_out_buffer) <= 0
+ || transport_write_block(tctx, deliver_out_buffer, len,
+ !!(tctx->options & topt_no_flush));
}
{
int bits = fcntl(tctx->u.fd, F_GETFD);
- (void)fcntl(tctx->u.fd, F_SETFD, bits | FD_CLOEXEC);
+ (void) fcntl(tctx->u.fd, F_SETFD, bits | FD_CLOEXEC);
filter_pid = child_open(USS transport_filter_argv, NULL, 077,
- &fd_write, &fd_read, FALSE);
- (void)fcntl(tctx->u.fd, F_SETFD, bits & ~FD_CLOEXEC);
+ &fd_write, &fd_read, FALSE, US"transport-filter");
+ (void) fcntl(tctx->u.fd, F_SETFD, bits & ~FD_CLOEXEC);
}
if (filter_pid < 0) goto TIDY_UP; /* errno set */
smtp dots, or check string processing. */
if (pipe(pfd) != 0) goto TIDY_UP; /* errno set */
-if ((write_pid = exim_fork(US"transport filter writer")) == 0)
+if ((write_pid = exim_fork(US"tpt-filter-writer")) == 0)
{
BOOL rc;
(void)close(fd_read);
tctx->u.fd = fd_write;
tctx->check_string = tctx->escape_string = NULL;
- tctx->options &= ~(topt_use_crlf | topt_end_dot | topt_use_bdat);
+ tctx->options &= ~(topt_use_crlf | topt_end_dot | topt_use_bdat | topt_no_flush);
rc = internal_transport_write_message(tctx, size_limit);
!= sizeof(struct timeval)
)
rc = FALSE; /* compiler quietening */
- exim_underbar_exit(0, US"tpt-filter writer");
+ exim_underbar_exit(EXIT_SUCCESS);
}
save_errno = errno;
yield = FALSE;
}
else if (!ok)
- {
+ { /* Try to drain the pipe; read fails are don't care */
int dummy = read(pfd[pipe_read], (void *)&save_errno, sizeof(int));
dummy = read(pfd[pipe_read], (void *)&tctx->addr->more_errno, sizeof(int));
dummy = read(pfd[pipe_read], (void *)&tctx->addr->delivery_time, sizeof(struct timeval));
- dummy = dummy; /* compiler quietening */
yield = FALSE;
}
}
? !write_chunk(tctx, US".\n", 2)
: !write_chunk(tctx, US"\n.\n", 3)
) )
- yield = FALSE;
+ { smtp_debug_cmd(US".", 0); yield = FALSE; }
/* Write out any remaining data in the buffer. */
*/
void
-transport_update_waiting(host_item *hostlist, uschar *tpname)
+transport_update_waiting(host_item * hostlist, uschar * tpname)
{
const uschar *prevname = US"";
open_db dbblock;
open_db *dbm_file;
+if (!is_new_message_id(message_id))
+ {
+ DEBUG(D_transport) debug_printf("message_id %s is not new format; "
+ "skipping wait-%s database update\n", message_id, tpname);
+ return;
+ }
+
DEBUG(D_transport) debug_printf("updating wait-%s database\n", tpname);
/* Open the database for this transport */
for (host_item * host = hostlist; host; host = host->next)
{
BOOL already = FALSE;
- dbdata_wait *host_record;
+ dbdata_wait * host_record;
int host_length;
uschar buffer[256];
if (!(host_record = dbfn_read(dbm_file, host->name)))
{
- host_record = store_get(sizeof(dbdata_wait) + MESSAGE_ID_LENGTH, FALSE);
+ host_record = store_get(sizeof(dbdata_wait) + MESSAGE_ID_LENGTH, GET_UNTAINTED);
host_record->count = host_record->sequence = 0;
}
for (uschar * s = host_record->text; s < host_record->text + host_length;
s += MESSAGE_ID_LENGTH)
+ {
+ /* If any ID is seen which is not new-format, wipe the record and
+ any continuations */
+
+ if (!is_new_message_id(s))
+ {
+ DEBUG(D_hints_lookup)
+ debug_printf_indent("NOTE: old or corrupt message-id found in wait=%.200s"
+ " hints DB; deleting records for %s\n", tpname, host->name);
+
+ (void) dbfn_delete(dbm_file, host->name);
+ for (int i = host_record->sequence - 1; i >= 0; i--)
+ (void) dbfn_delete(dbm_file,
+ (sprintf(CS buffer, "%.200s:%d", host->name, i), buffer));
+
+ host_record->count = host_record->sequence = 0;
+ break;
+ }
if (Ustrncmp(s, message_id, MESSAGE_ID_LENGTH) == 0)
{ already = TRUE; break; }
+ }
/* If we haven't found this message in the main record, search any
continuation records that exist. */
{
sprintf(CS buffer, "%.200s:%d", host->name, host_record->sequence);
dbfn_write(dbm_file, buffer, host_record, sizeof(dbdata_wait) + host_length);
-#ifdef EXPERIMENTAL_QUEUE_RAMP
+#ifndef DISABLE_QUEUE_RAMP
if (f.queue_2stage && queue_fast_ramp && !queue_run_in_order)
queue_notify_daemon(message_id);
#endif
else
{
dbdata_wait *newr =
- store_get(sizeof(dbdata_wait) + host_length + MESSAGE_ID_LENGTH, FALSE);
+ store_get(sizeof(dbdata_wait) + host_length + MESSAGE_ID_LENGTH, GET_UNTAINTED);
memcpy(newr, host_record, sizeof(dbdata_wait) + host_length);
host_record = newr;
}
/* Update the database */
dbfn_write(dbm_file, host->name, host_record, sizeof(dbdata_wait) + host_length);
- DEBUG(D_transport) debug_printf("added to list for %s\n", host->name);
+ DEBUG(D_transport) debug_printf("added %.*s to queue for %s\n",
+ MESSAGE_ID_LENGTH, message_id, host->name);
}
/* All now done */
current continue sequence is greater than the maximum supplied as an argument,
or greater than the global connection_max_messages, which, if set, overrides.
+It is also called if conditions are otherwise right for pipelining a QUIT after
+the message data, since if there is another message waiting we do not want to
+send that QUIT.
+
Arguments:
transport_name name of the transport
hostname name of the host
local_message_max maximum number of messages down one connection
as set by the caller transport
new_message_id set to the message id of a waiting message
- more set TRUE if there are yet more messages waiting
oicf_func function to call to validate if it is ok to send
to this message_id from the current instance.
oicf_data opaque data for oicf_func
} msgq_t;
BOOL
-transport_check_waiting(const uschar *transport_name, const uschar *hostname,
- int local_message_max, uschar *new_message_id, BOOL *more, oicf oicf_func, void *oicf_data)
+transport_check_waiting(const uschar * transport_name, const uschar * hostname,
+ int local_message_max, uschar * new_message_id,
+ oicf oicf_func, void * oicf_data)
{
-dbdata_wait *host_record;
+dbdata_wait * host_record;
int host_length;
open_db dbblock;
-open_db *dbm_file;
+open_db * dbm_file;
int i;
struct stat statbuf;
-*more = FALSE;
-
DEBUG(D_transport)
{
debug_printf("transport_check_waiting entered\n");
debug_printf(" sequence=%d local_max=%d global_max=%d\n",
continue_sequence, local_message_max, connection_max_messages);
+ acl_level++;
}
/* Do nothing if we have hit the maximum number that can be send down one
if (local_message_max > 0 && continue_sequence >= local_message_max)
{
DEBUG(D_transport)
- debug_printf("max messages for one connection reached: returning\n");
- return FALSE;
+ debug_printf_indent("max messages for one connection reached: returning\n");
+ goto retfalse;
}
/* Open the waiting information database. */
if (!(dbm_file = dbfn_open(string_sprintf("wait-%.200s", transport_name),
O_RDWR, &dbblock, TRUE, TRUE)))
- return FALSE;
+ goto retfalse;
/* See if there is a record for this host; if not, there's nothing to do. */
if (!(host_record = dbfn_read(dbm_file, hostname)))
{
dbfn_close(dbm_file);
- DEBUG(D_transport) debug_printf("no messages waiting for %s\n", hostname);
- return FALSE;
+ DEBUG(D_transport) debug_printf_indent("no messages waiting for %s\n", hostname);
+ goto retfalse;
}
/* If the data in the record looks corrupt, just log something and
dbfn_close(dbm_file);
log_write(0, LOG_MAIN|LOG_PANIC, "smtp-wait database entry for %s has bad "
"count=%d (max=%d)", hostname, host_record->count, WAIT_NAME_MAX);
- return FALSE;
+ goto retfalse;
}
/* Scan the message ids in the record from the end towards the beginning,
/* create an array to read entire message queue into memory for processing */
- msgq = store_get(sizeof(msgq_t) * host_record->count, FALSE);
+ msgq = store_get(sizeof(msgq_t) * host_record->count, GET_UNTAINTED);
msgq_count = host_record->count;
msgq_actual = msgq_count;
for (i = 0; i < host_record->count; ++i)
{
+ /* If any ID is seen which is not new-format, wipe the record and
+ any continuations */
+
+ if (!is_new_message_id(host_record->text + (i * MESSAGE_ID_LENGTH)))
+ {
+ uschar buffer[256];
+ DEBUG(D_hints_lookup)
+ debug_printf_indent("NOTE: old or corrupt message-id found in wait=%.200s"
+ " hints DB; deleting records for %s\n", transport_name, hostname);
+ (void) dbfn_delete(dbm_file, hostname);
+ for (int i = host_record->sequence - 1; i >= 0; i--)
+ (void) dbfn_delete(dbm_file,
+ (sprintf(CS buffer, "%.200s:%d", hostname, i), buffer));
+ dbfn_close(dbm_file);
+ goto retfalse;
+ }
msgq[i].bKeep = TRUE;
- Ustrncpy_nt(msgq[i].message_id, host_record->text + (i * MESSAGE_ID_LENGTH),
+ Ustrncpy_nt(msgq[i].message_id, host_record->text + (i * MESSAGE_ID_LENGTH),
MESSAGE_ID_LENGTH);
msgq[i].message_id[MESSAGE_ID_LENGTH] = 0;
}
if (host_length <= 0)
{
dbfn_close(dbm_file);
- DEBUG(D_transport) debug_printf("waiting messages already delivered\n");
- return FALSE;
+ DEBUG(D_transport) debug_printf_indent("waiting messages already delivered\n");
+ goto retfalse;
}
/* we were not able to find an acceptable message, nor was there a
- * continuation record. So bug out, outer logic will clean this up.
- */
+ continuation record. So bug out, outer logic will clean this up.
+ */
if (!bContinuation)
{
Ustrcpy(new_message_id, message_id);
dbfn_close(dbm_file);
- return FALSE;
+ goto retfalse;
}
} /* we need to process a continuation record */
if (host_length > 0)
{
host_record->count = host_length/MESSAGE_ID_LENGTH;
-
dbfn_write(dbm_file, hostname, host_record, (int)sizeof(dbdata_wait) + host_length);
- *more = TRUE;
}
dbfn_close(dbm_file);
+DEBUG(D_transport)
+ {
+ acl_level--;
+ debug_printf("transport_check_waiting: TRUE (found %s)\n", new_message_id);
+ }
return TRUE;
+
+retfalse:
+DEBUG(D_transport) {acl_level--; debug_printf("transport_check_waiting: FALSE\n"); }
+return FALSE;
}
/*************************************************
/* Just the regain-root-privilege exec portion */
void
-transport_do_pass_socket(const uschar *transport_name, const uschar *hostname,
- const uschar *hostaddress, uschar *id, int socket_fd)
+transport_do_pass_socket(const uschar * transport_name, const uschar * hostname,
+ const uschar * hostaddress, uschar * id, int socket_fd)
{
-int i = 20;
+int i = 13;
const uschar **argv;
+#ifndef DISABLE_TLS
+if (smtp_peer_options & OPTION_TLS) i += 6;
+#endif
+#ifndef DISABLE_ESMTP_LIMITS
+if (continue_limit_mail || continue_limit_rcpt || continue_limit_rcptdom)
+ i += 4;
+#endif
+if (queue_run_pid != (pid_t)0) i += 3;
+#ifdef SUPPORT_SOCKS
+if (proxy_session) i += 5;
+#endif
+
/* Set up the calling arguments; use the standard function for the basics,
but we have a number of extras that may be added. */
argv[i++] = sending_ip_address;
argv[i++] = string_sprintf("%d", sending_port);
argv[i++] = tls_out.active.sock >= 0 ? tls_out.cipher : continue_proxy_cipher;
+
+ if (tls_out.sni)
+ {
+ argv[i++] =
+#ifdef SUPPORT_DANE
+ tls_out.dane_verified ? US"-MCr" :
+#endif
+ US"-MCs";
+ argv[i++] = tls_out.sni;
+ }
}
else
argv[i++] = US"-MCT";
#endif
+#ifndef DISABLE_ESMTP_LIMITS
+if (continue_limit_rcpt || continue_limit_rcptdom)
+ {
+ argv[i++] = US"-MCL";
+ argv[i++] = string_sprintf("%u", continue_limit_mail);
+ argv[i++] = string_sprintf("%u", continue_limit_rcpt);
+ argv[i++] = string_sprintf("%u", continue_limit_rcptdom);
+ }
+#endif
+
if (queue_run_pid != (pid_t)0)
{
argv[i++] = US"-MCQ";
argv[i++] = string_sprintf("%d", queue_run_pipe);
}
+#ifdef SUPPORT_SOCKS
+if (proxy_session)
+ {
+ argv[i++] = US"-MCp";
+ argv[i++] = proxy_local_address;
+ argv[i++] = string_sprintf("%d", proxy_local_port);
+ argv[i++] = proxy_external_address;
+ argv[i++] = string_sprintf("%d", proxy_external_port);
+ }
+#endif
+
argv[i++] = US"-MC";
argv[i++] = US transport_name;
argv[i++] = US hostname;
DEBUG(D_exec) debug_print_argv(argv);
exim_nullstd(); /* Ensure std{out,err} exist */
+/* argv[0] should be untainted, from child_exec_exim() */
execv(CS argv[0], (char *const *)argv);
DEBUG(D_any) debug_printf("execv failed: %s\n", strerror(errno));
BOOL
transport_pass_socket(const uschar *transport_name, const uschar *hostname,
- const uschar *hostaddress, uschar *id, int socket_fd)
+ const uschar *hostaddress, uschar *id, int socket_fd
+#ifndef DISABLE_ESMTP_LIMITS
+ , unsigned peer_limit_mail, unsigned peer_limit_rcpt, unsigned peer_limit_rcptdom
+#endif
+ )
{
pid_t pid;
int status;
DEBUG(D_transport) debug_printf("transport_pass_socket entered\n");
-if ((pid = exim_fork(US"continued-transport interproc")) == 0)
- {
- /* Disconnect entirely from the parent process. If we are running in the
- test harness, wait for a bit to allow the previous process time to finish,
- write the log, etc., so that the output is always in the same order for
- automatic comparison. */
+#ifndef DISABLE_ESMTP_LIMITS
+continue_limit_mail = peer_limit_mail;
+continue_limit_rcpt = peer_limit_rcpt;
+continue_limit_rcptdom = peer_limit_rcptdom;
+#endif
- if ((pid = exim_fork(US"continued-transport")) != 0)
- {
- DEBUG(D_transport) debug_printf("transport_pass_socket succeeded (final-pid %d)\n", pid);
- _exit(EXIT_SUCCESS);
- }
- testharness_pause_ms(1000);
+if ((pid = exim_fork(US"continued-transport")) == 0)
+ {
+ /* If we are running in the test harness, wait for a bit to allow the
+ previous process time to finish, write the log, etc., so that the output is
+ always in the same order for automatic comparison. */
+ testharness_pause_ms(500);
transport_do_pass_socket(transport_name, hostname, hostaddress,
id, socket_fd);
+ /*NOTREACHED*/
}
-/* If the process creation succeeded, wait for the first-level child, which
-immediately exits, leaving the second level process entirely disconnected from
-this one. */
-
if (pid > 0)
- {
- int rc;
- while ((rc = wait(&status)) != pid && (rc >= 0 || errno != ECHILD));
- DEBUG(D_transport) debug_printf("transport_pass_socket succeeded (inter-pid %d)\n", pid);
return TRUE;
- }
-else
- {
- DEBUG(D_transport) debug_printf("transport_pass_socket failed to fork: %s\n",
+
+DEBUG(D_transport) debug_printf("transport_pass_socket failed to fork: %s\n",
strerror(errno));
- return FALSE;
- }
+return FALSE;
}
+/* Enforce all args untainted, for consistency with a router-sourced pipe
+command, where (because the whole line is passed as one to the tpt) a
+tainted arg taints the executable name. It's unclear also that letting an
+attacker supply command arguments is wise. */
+
+static BOOL
+arg_is_tainted(const uschar * s, int argn, address_item * addr,
+ const uschar * etext, uschar ** errptr)
+{
+if (is_tainted(s))
+ {
+ uschar * msg = string_sprintf("Tainted arg %d for %s command: '%s'",
+ argn, etext, s);
+ if (addr)
+ {
+ addr->transport_return = FAIL;
+ addr->message = msg;
+ }
+ else *errptr = msg;
+ return TRUE;
+ }
+return FALSE;
+}
+
+
/*************************************************
* Set up direct (non-shell) command *
*************************************************/
/* This function is called when a command line is to be parsed and executed
directly, without the use of /bin/sh. It is called by the pipe transport,
-the queryprogram router, and also from the main delivery code when setting up a
+the queryprogram router, for any ${run } expansion,
+and also from the main delivery code when setting up a
transport filter process. The code for ETRN also makes use of this; in that
case, no addresses are passed.
Arguments:
argvptr pointer to anchor for argv vector
cmd points to the command string (modified IN PLACE)
- expand_arguments true if expansion is to occur
+ flags bits for expand-args, allow taint, allow $recipients
expand_failed error value to set if expansion fails; not relevant if
addr == NULL
addr chain of addresses, or NULL
*/
BOOL
-transport_set_up_command(const uschar ***argvptr, uschar *cmd,
- BOOL expand_arguments, int expand_failed, address_item *addr,
- uschar *etext, uschar **errptr)
+transport_set_up_command(const uschar *** argvptr, const uschar * cmd,
+ unsigned flags, int expand_failed, address_item * addr,
+ const uschar * etext, uschar ** errptr)
{
-const uschar **argv;
-uschar *s, *ss;
-int address_count = 0;
-int argcount = 0;
-int max_args;
+const uschar ** argv, * s;
+int address_count = 0, argcount = 0, max_args;
/* Get store in which to build an argument list. Count the number of addresses
supplied, and allow for that many arguments, plus an additional 60, which
for (address_item * ad = addr; ad; ad = ad->next) address_count++;
max_args = address_count + 60;
-*argvptr = argv = store_get((max_args+1)*sizeof(uschar *), FALSE);
+*argvptr = argv = store_get((max_args+1)*sizeof(uschar *), GET_UNTAINTED);
/* Split the command up into arguments terminated by white space. Lose
trailing space at the start and end. Double-quoted arguments can contain \\ and
arguments are verbatim. Copy each argument into a new string. */
s = cmd;
-while (isspace(*s)) s++;
+Uskip_whitespace(&s);
-for (; *s != 0 && argcount < max_args; argcount++)
+for (; *s && argcount < max_args; argcount++)
{
if (*s == '\'')
{
- ss = s + 1;
- while (*ss != 0 && *ss != '\'') ss++;
- argv[argcount] = ss = store_get(ss - s++, is_tainted(cmd));
- while (*s != 0 && *s != '\'') *ss++ = *s++;
- if (*s != 0) s++;
- *ss++ = 0;
+ int n = Ustrcspn(++s, "'");
+ argv[argcount] = string_copyn(s, n);
+ if (*(s += n) == '\'') s++;
}
else
argv[argcount] = string_dequote(CUSS &s);
- while (isspace(*s)) s++;
+ Uskip_whitespace(&s);
}
-argv[argcount] = US 0;
+argv[argcount] = NULL;
/* If *s != 0 we have run out of argument slots. */
-if (*s != 0)
+if (*s)
{
uschar *msg = string_sprintf("Too many arguments in command \"%s\" in "
"%s", cmd, etext);
- if (addr != NULL)
+ if (addr)
{
addr->transport_return = FAIL;
addr->message = msg;
debug_printf(" argv[%d] = '%s'\n", i, string_printing(argv[i]));
}
-if (expand_arguments)
+if (flags & TSUC_EXPAND_ARGS)
{
- BOOL allow_dollar_recipients = addr != NULL &&
- addr->parent != NULL &&
- Ustrcmp(addr->parent->address, "system-filter") == 0;
+ BOOL allow_dollar_recipients = (flags & TSUC_ALLOW_RECIPIENTS)
+ || (addr && addr->parent && Ustrcmp(addr->parent->address, "system-filter") == 0); /*XXX could we check this at caller? */
- for (int i = 0; argv[i] != US 0; i++)
+ for (int i = 0; argv[i]; i++)
{
+ DEBUG(D_expand) debug_printf_indent("arg %d\n", i);
/* Handle special fudge for passing an address list */
- if (addr != NULL &&
+ if (addr &&
(Ustrcmp(argv[i], "$pipe_addresses") == 0 ||
Ustrcmp(argv[i], "${pipe_addresses}") == 0))
{
for (address_item * ad = addr; ad; ad = ad->next)
{
+ /* $pipe_addresses is spefically not checked for taint, because there is
+ a testcase (321) depending on it. It's unclear if the exact thing being
+ done really needs to be legitimate, though I suspect it reflects an
+ actual use-case that showed up a bug.
+ This is a hole in the taint-pretection, mitigated only in that
+ shell-syntax metachars cannot be injected via this route. */
+
+ DEBUG(D_transport) if (is_tainted(ad->address))
+ debug_printf("tainted element '%s' from $pipe_addresses\n", ad->address);
+
argv[i++] = ad->address;
argcount++;
}
/* Handle special case of $address_pipe when af_force_command is set */
- else if (addr != NULL && testflag(addr,af_force_command) &&
+ else if (addr && testflag(addr,af_force_command) &&
(Ustrcmp(argv[i], "$address_pipe") == 0 ||
Ustrcmp(argv[i], "${address_pipe}") == 0))
{
int address_pipe_argcount = 0;
int address_pipe_max_args;
uschar **address_pipe_argv;
- BOOL tainted;
/* We can never have more then the argv we will be loading into */
address_pipe_max_args = max_args - argcount + 1;
debug_printf("address_pipe_max_args=%d\n", address_pipe_max_args);
/* We allocate an additional for (uschar *)0 */
- address_pipe_argv = store_get((address_pipe_max_args+1)*sizeof(uschar *), FALSE);
+ address_pipe_argv = store_get((address_pipe_max_args+1)*sizeof(uschar *), GET_UNTAINTED);
/* +1 because addr->local_part[0] == '|' since af_force_command is set */
- s = expand_string(addr->local_part + 1);
- tainted = is_tainted(s);
+ s = expand_cstring(addr->local_part + 1);
- if (s == NULL || *s == '\0')
+ if (!s || !*s)
{
addr->transport_return = FAIL;
addr->message = string_sprintf("Expansion of \"%s\" "
return FALSE;
}
- while (isspace(*s)) s++; /* strip leading space */
+ Uskip_whitespace(&s); /* strip leading space */
- while (*s != 0 && address_pipe_argcount < address_pipe_max_args)
+ while (*s && address_pipe_argcount < address_pipe_max_args)
{
if (*s == '\'')
- {
- ss = s + 1;
- while (*ss != 0 && *ss != '\'') ss++;
- address_pipe_argv[address_pipe_argcount++] = ss = store_get(ss - s++, tainted);
- while (*s != 0 && *s != '\'') *ss++ = *s++;
- if (*s != 0) s++;
- *ss++ = 0;
- }
- else address_pipe_argv[address_pipe_argcount++] =
- string_copy(string_dequote(CUSS &s));
- while (isspace(*s)) s++; /* strip space after arg */
+ {
+ int n = Ustrcspn(++s, "'");
+ argv[argcount] = string_copyn(s, n);
+ if (*(s += n) == '\'') s++;
+ }
+ else
+ address_pipe_argv[address_pipe_argcount++] = string_dequote(CUSS &s);
+ Uskip_whitespace(&s); /* strip space after arg */
}
- address_pipe_argv[address_pipe_argcount] = US 0;
+ address_pipe_argv[address_pipe_argcount] = NULL;
/* If *s != 0 we have run out of argument slots. */
- if (*s != 0)
+ if (*s)
{
uschar *msg = string_sprintf("Too many arguments in $address_pipe "
"\"%s\" in %s", addr->local_part + 1, etext);
- if (addr != NULL)
+ if (addr)
{
addr->transport_return = FAIL;
addr->message = msg;
}
/* address_pipe_argcount - 1
- * because we are replacing $address_pipe in the argument list
- * with the first thing it expands to */
+ because we are replacing $address_pipe in the argument list
+ with the first thing it expands to */
+
if (argcount + address_pipe_argcount - 1 > max_args)
{
addr->transport_return = FAIL;
}
/* If we are not just able to replace the slot that contained
- * $address_pipe (address_pipe_argcount == 1)
- * We have to move the existing argv by address_pipe_argcount - 1
- * Visually if address_pipe_argcount == 2:
- * [argv 0][argv 1][argv 2($address_pipe)][argv 3][0]
- * [argv 0][argv 1][ap_arg0][ap_arg1][old argv 3][0]
- */
+ $address_pipe (address_pipe_argcount == 1)
+ We have to move the existing argv by address_pipe_argcount - 1
+ Visually if address_pipe_argcount == 2:
+ [argv 0][argv 1][argv 2($address_pipe)][argv 3][0]
+ [argv 0][argv 1][ap_arg0][ap_arg1][old argv 3][0] */
+
if (address_pipe_argcount > 1)
memmove(
/* current position + additional args */
);
/* Now we fill in the slots we just moved argv out of
- * [argv 0][argv 1][argv 2=pipeargv[0]][argv 3=pipeargv[1]][old argv 3][0]
- */
+ [argv 0][argv 1][argv 2=pipeargv[0]][argv 3=pipeargv[1]][old argv 3][0] */
+
for (int address_pipe_i = 0;
- address_pipe_argv[address_pipe_i] != US 0;
- address_pipe_i++)
- {
- argv[i++] = address_pipe_argv[address_pipe_i];
- argcount++;
- }
+ address_pipe_argv[address_pipe_i];
+ address_pipe_i++, argcount++)
+ {
+ uschar * s = address_pipe_argv[address_pipe_i];
+ if (arg_is_tainted(s, i, addr, etext, errptr)) return FALSE;
+ argv[i++] = s;
+ }
/* Subtract one since we replace $address_pipe */
argcount--;
else
{
const uschar *expanded_arg;
+ BOOL enable_dollar_recipients_g = f.enable_dollar_recipients;
f.enable_dollar_recipients = allow_dollar_recipients;
expanded_arg = expand_cstring(argv[i]);
- f.enable_dollar_recipients = FALSE;
+ f.enable_dollar_recipients = enable_dollar_recipients_g;
if (!expanded_arg)
{
else *errptr = msg;
return FALSE;
}
+
+ if ( f.running_in_test_harness && is_tainted(expanded_arg)
+ && Ustrcmp(etext, "queryprogram router") == 0)
+ { /* hack, would be good to not need it */
+ DEBUG(D_transport)
+ debug_printf("SPECIFIC TESTSUITE EXEMPTION: tainted arg '%s'\n",
+ expanded_arg);
+ }
+ else if ( !(flags & TSUC_ALLOW_TAINTED_ARGS)
+ && arg_is_tainted(expanded_arg, i, addr, etext, errptr))
+ return FALSE;
argv[i] = expanded_arg;
}
}
DEBUG(D_transport)
{
debug_printf("direct command after expansion:\n");
- for (int i = 0; argv[i] != US 0; i++)
- debug_printf(" argv[%d] = %s\n", i, string_printing(argv[i]));
+ for (int i = 0; argv[i]; i++)
+ {
+ debug_printf(" argv[%d] = '%s'\n", i, string_printing(argv[i]));
+ debug_print_taint(argv[i]);
+ }
}
}
return TRUE;
}
+
+
+/* For error messages, a string describing the config location associated
+with current processing. NULL if we are not in a transport. */
+/* Name only, for now */
+
+uschar *
+transport_current_name(void)
+{
+if (!transport_name) return NULL;
+return string_sprintf(" (transport %s, %s %d)", transport_name, driver_srcfile, driver_srcline);
+}
+
#endif /*!MACRO_PREDEF*/
/* vi: aw ai sw=2
*/