Skip to content

Commit

Permalink
Allow limiting Sieve :regex execution time
Browse files Browse the repository at this point in the history
  • Loading branch information
ksmurchison committed Dec 11, 2023
1 parent 72b106f commit efab067
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 2 deletions.
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) {
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

0 comments on commit efab067

Please sign in to comment.