Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow limiting Sieve :regex execution time #4767

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions cassandane/tiny-tests/Sieve/regex-timeout
Original file line number Diff line number Diff line change
@@ -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(<<EOF
require ["regex", "imap4flags"];
if header :regex "Subject"
"a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?aaaaaaaaaaaaaaaaaaaaaaaaa" {
addflag "\\\\Flagged";
keep;
}
EOF
);

xlog $self, "Install a script for manifold";
$self->{instance}->install_sieve_script(<<EOF
require ["regex", "imap4flags"];
if header :regex "Subject" "aaaa" {
keep :flags "\\\\Flagged";
}
EOF
, username => '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);
}

14 changes: 14 additions & 0 deletions changes/next/sieve_regex_timeout
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

Description:

Allow limiting Sieve :regex execution time.


Config changes:

Adds 'sieve_regex_timeout' option.


Upgrade instructions:

None.
6 changes: 6 additions & 0 deletions lib/imapoptions
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
90 changes: 88 additions & 2 deletions sieve/comparator.c
Original file line number Diff line number Diff line change
Expand Up @@ -334,20 +334,97 @@ static int octet_matches(const char *text, size_t tlen, const char *pat,


#ifdef ENABLE_REGEX
#include <errno.h>
#include <setjmp.h>
#include <signal.h>
#include <syslog.h>

#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);
nmatch = MAX_MATCH_VARS+1;
memset(&pm, 0, sizeof(pm));
}

if (!action.sa_handler) {
ksmurchison marked this conversation as resolved.
Show resolved Hide resolved
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;
Expand All @@ -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;
Expand All @@ -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) --- */
Expand Down
3 changes: 3 additions & 0 deletions sieve/sieve_err.et
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading