diff --git a/cassandane/tiny-tests/Sieve/regex-timeout b/cassandane/tiny-tests/Sieve/regex-timeout new file mode 100644 index 0000000000..9762520ac0 --- /dev/null +++ b/cassandane/tiny-tests/Sieve/regex-timeout @@ -0,0 +1,57 @@ +#!perl +use Cassandane::Tiny; +use Cassandane::Util::Slurp; +use Cwd qw(abs_path); + +sub test_regex_timeout + :min_version_3_9 :needs_component_sieve :NoAltNameSpace :NoStartInstances +{ + my ($self) = @_; + + # The regex below takes ~0.75s to execute on the given input + $self->{instance}->{config}->set('sieve_regex_timeout' => '0.5'); + $self->_start_instances(); + + xlog, $self, "Create manifold user and make it readable by cassandane"; + my $admintalk = $self->{adminstore}->get_client(); + $admintalk->create("user.manifold"); + $admintalk->setacl("user.manifold", cassandane => 'lrs'); + + xlog $self, "Install a script for cassandane"; + $self->{instance}->install_sieve_script(<{instance}->install_sieve_script(< 'manifold'); + + my $msg = $self->{gen}->generate(subject => 'aaaaaaaaaaaaaaaaaaaaaaaaa'); + $self->{instance}->deliver($msg, users => [ 'cassandane', 'manifold' ]); + + xlog $self, "Check that the message made it to INBOX due to Sieve failure"; + $self->{store}->set_folder('INBOX'); + $self->{store}->set_fetch_attributes(qw(uid flags)); + $msg->set_attribute(uid => 1); + $msg->set_attribute(flags => [ '\\Recent', '$SieveFailed' ]); + $self->check_messages({ 1 => $msg }, check_guid => 0); + + xlog $self, "Check that the message made it to manifold INBOX with \Flagged"; + $self->{store}->set_folder('user.manifold'); + $self->{store}->set_fetch_attributes(qw(uid flags)); + $msg->set_attribute(uid => 1); + $msg->set_attribute(flags => [ '\\Recent', '\\Flagged' ]); + $self->check_messages({ 1 => $msg }, check_guid => 0); +} + diff --git a/changes/next/sieve_regex_timeout b/changes/next/sieve_regex_timeout new file mode 100644 index 0000000000..605377b6f7 --- /dev/null +++ b/changes/next/sieve_regex_timeout @@ -0,0 +1,14 @@ + +Description: + +Allow limiting Sieve :regex execution time. + + +Config changes: + +Adds 'sieve_regex_timeout' option. + + +Upgrade instructions: + +None. diff --git a/lib/imapoptions b/lib/imapoptions index 7d6a568ab7..93065fdd98 100644 --- a/lib/imapoptions +++ b/lib/imapoptions @@ -2622,6 +2622,12 @@ product version in the capabilities /* Maximum number of sieve scripts any user may have, enforced at submission by timsieved(8). */ +{ "sieve_regex_timeout", NULL, STRING, "UNRELEASED" } +/* Time in floating point seconds. Any Sieve :regex match that takes + longer than this time is aborted and the script will fail (message + will be delivered to INBOX). + A NULL value (default) or a value of "0" will disable the timeout */ + { "sieve_utf8fileinto", 0, SWITCH, "2.3.17" } /* If enabled, the sieve engine expects folder names for the \fIfileinto\fR action in scripts to use UTF8 encoding. Otherwise, diff --git a/sieve/comparator.c b/sieve/comparator.c index 02cfad07d1..c1c95f40de 100644 --- a/sieve/comparator.c +++ b/sieve/comparator.c @@ -334,13 +334,35 @@ static int octet_matches(const char *text, size_t tlen, const char *pat, #ifdef ENABLE_REGEX +#include +#include +#include +#include + +#include "libconfig.h" + +static sigjmp_buf jmpbuf; +static volatile sig_atomic_t canjump; + +static void sig_alrm(int sig __attribute__((unused))) +{ + if (!canjump) return; // unexpected signal, ignore + + canjump = 0; + siglongjmp(jmpbuf, 1); // jump back to octet_regex() +} + static int octet_regex(const char *text, size_t tlen, const char *pat, strarray_t *match_vars, void *rock __attribute__((unused))) { + static struct sigaction action; + static double timeout; + struct itimerval it = { 0 }; + struct timeval start, end; regmatch_t pm[MAX_MATCH_VARS+1]; size_t nmatch = 0; - int r; + static int r; if (match_vars) { strarray_fini(match_vars); @@ -348,6 +370,61 @@ static int octet_regex(const char *text, size_t tlen, const char *pat, memset(&pm, 0, sizeof(pm)); } + if (!action.sa_handler) { + const char *timeoutstr = config_getstring(IMAPOPT_SIEVE_REGEX_TIMEOUT); + + if (timeoutstr) timeout = atof(timeoutstr); + if (timeout < 0) timeout = 0; + + /* + * signal() on some platforms may set SA_RESTART by default. + * + * Therefore, we use sigaction(). + */ + action.sa_handler = &sig_alrm; + sigemptyset(&action.sa_mask); +#ifdef SA_INTERRUPT + action.sa_flags |= SA_INTERRUPT; +#endif + + if (timeout > 0 && sigaction(SIGALRM, &action, NULL) < 0) { + syslog(LOG_NOTICE, "sigaction(SIGALRM) failed for Sieve :regex: %m"); + errno = 0; + + /* Return an error so script execution fails */ + r = SIEVE_INTERNAL_ERROR; + } + } + + if (r < 0) return r; + + if (sigsetjmp(jmpbuf, 1)) { + /* + * regexec() timed out, return an error so script execution fails + * + * Since we have no way of freeing any resources used by + * regexec(), we will terminate lmtpd. + * + * The signal will not be caught until after deliver() + * completes and we return to the top of the lmtpmode() loop, + * so we will continue delivery to the remaining recipients. + */ + gettimeofday(&end, 0); + syslog(LOG_INFO, "Sieve :regex execution timed out after %fs", + timesub(&start, &end)); + raise(SIGTERM); + return SIEVE_REGEX_TIMEOUT; + } + + /* siglongjmp() is now OK */ + canjump = 1; + + /* enable timeout */ + it.it_value.tv_usec = (long) (timeout * 1000000); + setitimer(ITIMER_REAL, &it, NULL); + + gettimeofday(&start, 0); + #ifdef REG_STARTEND /* pcre, BSD, some linuxes support this handy trick */ pm[0].rm_so = 0; @@ -365,6 +442,15 @@ static int octet_regex(const char *text, size_t tlen, const char *pat, free(buf); #endif /* REG_STARTEND */ + /* disable timeout */ + canjump = it.it_value.tv_usec = 0; + setitimer(ITIMER_REAL, &it, NULL); + + /* log run time */ + gettimeofday(&end, 0); + syslog(LOG_INFO, "Sieve :regex run time: %fs", + timesub(&start, &end)); + if (r) { /* populate match variables */ size_t var_num; @@ -378,7 +464,7 @@ static int octet_regex(const char *text, size_t tlen, const char *pat, } return r; } -#endif +#endif /* ENABLE_REGEX */ /* --- i;ascii-casemap comparators (RFC 4790, Section 9.2) --- */ diff --git a/sieve/sieve_err.et b/sieve/sieve_err.et index 2e15bd7758..03fc9f86cf 100644 --- a/sieve/sieve_err.et +++ b/sieve/sieve_err.et @@ -65,6 +65,9 @@ ec SIEVE_DONE, ec SIEVE_SCRIPT_RELOADED, "Sieve script was loaded in the past" +ec SIEVE_REGEX_TIMEOUT, + "Sieve :regex execution too long" + # Parse errors