* Exim - an Internet mail transport agent *
*************************************************/
+/* Copyright (c) The Exim Maintainers 2020 - 2024 */
/* Copyright (c) University of Cambridge 1995 - 2018 */
-/* Copyright (c) The Exim Maintainers 2020 - 2021 */
/* 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. */
*/
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;
*/
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");
}
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. */
? !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;
+const uschar * prevname = US"";
+open_db dbblock, * dbp;
+
+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 */
-if (!(dbm_file = dbfn_open(string_sprintf("wait-%.200s", tpname),
+if (!(dbp = dbfn_open(string_sprintf("wait-%.200s", tpname),
O_RDWR, &dbblock, TRUE, TRUE)))
return;
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];
/* Look up the host record; if there isn't one, make an empty one. */
- if (!(host_record = dbfn_read(dbm_file, host->name)))
+ if (!(host_record = dbfn_read(dbp, host->name)))
{
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(dbp, host->name);
+ for (int i = host_record->sequence - 1; i >= 0; i--)
+ (void) dbfn_delete(dbp,
+ (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. */
{
dbdata_wait *cont;
sprintf(CS buffer, "%.200s:%d", host->name, i);
- if ((cont = dbfn_read(dbm_file, buffer)))
+ if ((cont = dbfn_read(dbp, buffer)))
{
int clen = cont->count * MESSAGE_ID_LENGTH;
for (uschar * s = cont->text; s < cont->text + clen; s += MESSAGE_ID_LENGTH)
if (host_record->count >= WAIT_NAME_MAX)
{
sprintf(CS buffer, "%.200s:%d", host->name, host_record->sequence);
- dbfn_write(dbm_file, buffer, host_record, sizeof(dbdata_wait) + host_length);
+ dbfn_write(dbp, buffer, host_record, sizeof(dbdata_wait) + host_length);
#ifndef DISABLE_QUEUE_RAMP
if (f.queue_2stage && queue_fast_ramp && !queue_run_in_order)
queue_notify_daemon(message_id);
/* Update the database */
- dbfn_write(dbm_file, host->name, host_record, sizeof(dbdata_wait) + host_length);
+ dbfn_write(dbp, host->name, host_record, sizeof(dbdata_wait) + host_length);
DEBUG(D_transport) debug_printf("added %.*s to queue for %s\n",
MESSAGE_ID_LENGTH, message_id, host->name);
}
/* All now done */
-dbfn_close(dbm_file);
+dbfn_close(dbp);
}
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
} msgq_t;
BOOL
-transport_check_waiting(const uschar *transport_name, const uschar *hostname,
- int local_message_max, uschar *new_message_id, 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 dbblock, * dbp;
int i;
struct stat statbuf;
/* Open the waiting information database. */
-if (!(dbm_file = dbfn_open(string_sprintf("wait-%.200s", transport_name),
+if (!(dbp = dbfn_open(string_sprintf("wait-%.200s", transport_name),
O_RDWR, &dbblock, TRUE, TRUE)))
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)))
+if (!(host_record = dbfn_read(dbp, hostname)))
{
- dbfn_close(dbm_file);
DEBUG(D_transport) debug_printf_indent("no messages waiting for %s\n", hostname);
- goto retfalse;
+ goto dbclose_false;
}
/* If the data in the record looks corrupt, just log something and
if (host_record->count > WAIT_NAME_MAX)
{
- 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);
- goto retfalse;
+ goto dbclose_false;
}
/* Scan the message ids in the record from the end towards the beginning,
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(dbp, hostname);
+ for (int i = host_record->sequence - 1; i >= 0; i--)
+ (void) dbfn_delete(dbp,
+ (sprintf(CS buffer, "%.200s:%d", hostname, i), buffer));
+ goto dbclose_false;
+ }
msgq[i].bKeep = TRUE;
Ustrncpy_nt(msgq[i].message_id, host_record->text + (i * MESSAGE_ID_LENGTH),
for (int i = host_record->sequence - 1; i >= 0 && !newr; i--)
{
sprintf(CS buffer, "%.200s:%d", hostname, i);
- newr = dbfn_read(dbm_file, buffer);
+ newr = dbfn_read(dbp, buffer);
}
/* If no continuation, delete the current and break the loop */
if (!newr)
{
- dbfn_delete(dbm_file, hostname);
+ dbfn_delete(dbp, hostname);
break;
}
/* Else replace the current with the continuation */
- dbfn_delete(dbm_file, buffer);
+ dbfn_delete(dbp, buffer);
host_record = newr;
host_length = host_record->count * MESSAGE_ID_LENGTH;
if (host_length <= 0)
{
- dbfn_close(dbm_file);
DEBUG(D_transport) debug_printf_indent("waiting messages already delivered\n");
- goto retfalse;
+ goto dbclose_false;
}
/* 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);
- goto retfalse;
+ goto dbclose_false;
}
} /* 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);
+ dbfn_write(dbp, hostname, host_record, (int)sizeof(dbdata_wait) + host_length);
}
-dbfn_close(dbm_file);
-DEBUG(D_transport) {acl_level--; debug_printf("transport_check_waiting: TRUE\n"); }
+dbfn_close(dbp);
+
+DEBUG(D_transport)
+ {
+ acl_level--;
+ debug_printf("transport_check_waiting: TRUE (found %s)\n", new_message_id);
+ }
return TRUE;
+dbclose_false:
+ dbfn_close(dbp);
+
retfalse:
-DEBUG(D_transport) {acl_level--; debug_printf("transport_check_waiting: FALSE\n"); }
-return FALSE;
+ 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 = 13;
const uschar **argv;
#ifndef DISABLE_TLS
if (smtp_peer_options & OPTION_TLS) i += 6;
#endif
-#ifdef EXPERIMENTAL_ESMTP_LIMITS
+#ifndef DISABLE_ESMTP_LIMITS
if (continue_limit_mail || continue_limit_rcpt || continue_limit_rcptdom)
i += 4;
#endif
argv[i++] = US"-MCT";
#endif
-#ifdef EXPERIMENTAL_ESMTP_LIMITS
+#ifndef DISABLE_ESMTP_LIMITS
if (continue_limit_rcpt || continue_limit_rcptdom)
{
argv[i++] = US"-MCL";
BOOL
transport_pass_socket(const uschar *transport_name, const uschar *hostname,
const uschar *hostaddress, uschar *id, int socket_fd
-#ifdef EXPERIMENTAL_ESMTP_LIMITS
+#ifndef DISABLE_ESMTP_LIMITS
, unsigned peer_limit_mail, unsigned peer_limit_rcpt, unsigned peer_limit_rcptdom
#endif
)
DEBUG(D_transport) debug_printf("transport_pass_socket entered\n");
-#ifdef EXPERIMENTAL_ESMTP_LIMITS
+#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-interproc")) == 0)
+if ((pid = exim_fork(US"continued-transport")) == 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. */
-
- if ((pid = exim_fork(US"continued-transport")) != 0)
- _exit(EXIT_SUCCESS);
- testharness_pause_ms(1000);
+ /* 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));
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
arguments are verbatim. Copy each argument into a new string. */
s = cmd;
-while (isspace(*s)) s++;
+Uskip_whitespace(&s);
for (; *s && argcount < max_args; argcount++)
{
if (*s == '\'')
{
- ss = s + 1;
- while (*ss && *ss != '\'') ss++;
- argv[argcount] = ss = store_get(ss - s++, cmd);
- while (*s && *s != '\'') *ss++ = *s++;
- if (*s) 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] = NULL;
{
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 && addr->parent
- && 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]; i++)
{
+ DEBUG(D_expand) debug_printf_indent("arg %d\n", i);
/* Handle special fudge for passing an address list */
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++;
}
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);
+ s = expand_cstring(addr->local_part + 1);
if (!s || !*s)
{
return FALSE;
}
- while (isspace(*s)) s++; /* strip leading space */
+ Uskip_whitespace(&s); /* strip leading space */
while (*s && address_pipe_argcount < address_pipe_max_args)
{
if (*s == '\'')
- {
- int n;
- for (ss = s + 1; *ss && *ss != '\''; ) ss++;
- n = ss - s++;
- address_pipe_argv[address_pipe_argcount++] = ss = store_get(n, s);
- while (*s && *s != '\'') *ss++ = *s++;
- if (*s) 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] = NULL;
}
/* 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;
[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_argv[address_pipe_i];
address_pipe_i++, argcount++)
- argv[i++] = address_pipe_argv[address_pipe_i];
+ {
+ 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_printf("direct command after expansion:\n");
for (int i = 0; argv[i]; i++)
- debug_printf(" argv[%d] = %s\n", i, string_printing(argv[i]));
+ {
+ debug_printf(" argv[%d] = '%s'\n", i, string_printing(argv[i]));
+ debug_print_taint(argv[i]);
+ }
}
}