Skip to content

Add support for limiting maximum execution time based on wall-time #6504

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

Closed
wants to merge 2 commits into from
Closed
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
2 changes: 2 additions & 0 deletions Zend/zend_execute.h
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,9 @@ ZEND_API bool zend_is_executing(void);
ZEND_API ZEND_COLD void ZEND_FASTCALL zend_cannot_pass_by_reference(uint32_t arg_num);

ZEND_API void zend_set_timeout(zend_long seconds, bool reset_signals);
ZEND_API void zend_set_wall_timeout(zend_long seconds, bool reset_signals);
ZEND_API void zend_unset_timeout(void);
ZEND_API void zend_unset_wall_timeout(void);
ZEND_API ZEND_NORETURN void ZEND_FASTCALL zend_timeout(void);
ZEND_API zend_class_entry *zend_fetch_class(zend_string *class_name, int fetch_type);
ZEND_API zend_class_entry *zend_fetch_class_by_name(zend_string *class_name, zend_string *lcname, int fetch_type);
Expand Down
169 changes: 166 additions & 3 deletions Zend/zend_execute_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -1260,9 +1260,14 @@ ZEND_API zend_result zend_eval_string_ex(const char *str, zval *retval_ptr, cons
/* }}} */

static void zend_set_timeout_ex(zend_long seconds, bool reset_signals);
static void zend_set_wall_timeout_ex(zend_long seconds, bool reset_signals);

ZEND_API ZEND_NORETURN void ZEND_FASTCALL zend_timeout(void) /* {{{ */
{
zend_long original_timed_out = EG(timed_out);
zend_long original_timeout_seconds = EG(timeout_seconds);
zend_long original_wall_timeout_seconds = EG(wall_timeout_seconds);

#if defined(PHP_WIN32)
# ifndef ZTS
/* No action is needed if we're timed out because zero seconds are
Expand All @@ -1278,10 +1283,18 @@ ZEND_API ZEND_NORETURN void ZEND_FASTCALL zend_timeout(void) /* {{{ */
# endif
#else
EG(timed_out) = 0;
zend_set_timeout_ex(0, 1);
if (original_timed_out == 1) {
zend_set_timeout_ex(0, 1);
} else {
zend_set_wall_timeout_ex(0, 1);
}
#endif

zend_error_noreturn(E_ERROR, "Maximum execution time of " ZEND_LONG_FMT " second%s exceeded", EG(timeout_seconds), EG(timeout_seconds) == 1 ? "" : "s");
if (original_timed_out == 1) {
zend_error_noreturn(E_ERROR, "Maximum execution time of " ZEND_LONG_FMT " second%s exceeded", original_timeout_seconds, original_timeout_seconds == 1 ? "" : "s");
} else {
zend_error_noreturn(E_ERROR, "Maximum execution wall-time of " ZEND_LONG_FMT " second%s exceeded", original_wall_timeout_seconds, original_wall_timeout_seconds == 1 ? "" : "s");
}
}
/* }}} */

Expand Down Expand Up @@ -1337,6 +1350,57 @@ static void zend_timeout_handler(int dummy) /* {{{ */
/* }}} */
#endif

#ifndef ZEND_WIN32
static void zend_wall_timeout_handler(int dummy) /* {{{ */
{
#ifndef ZTS
if (EG(timed_out)) {
/* Die on hard timeout */
const char *error_filename = NULL;
uint32_t error_lineno = 0;
char log_buffer[2048];
int output_len = 0;

if (zend_is_compiling()) {
error_filename = ZSTR_VAL(zend_get_compiled_filename());
error_lineno = zend_get_compiled_lineno();
} else if (zend_is_executing()) {
error_filename = zend_get_executed_filename();
if (error_filename[0] == '[') { /* [no active file] */
error_filename = NULL;
error_lineno = 0;
} else {
error_lineno = zend_get_executed_lineno();
}
}
if (!error_filename) {
error_filename = "Unknown";
}

output_len = snprintf(log_buffer, sizeof(log_buffer), "\nFatal error: Maximum execution wall-time of " ZEND_LONG_FMT "+" ZEND_LONG_FMT " seconds exceeded (terminated) in %s on line %d\n", EG(wall_timeout_seconds), EG(hard_timeout), error_filename, error_lineno);
if (output_len > 0) {
zend_quiet_write(2, log_buffer, MIN(output_len, sizeof(log_buffer)));
}
_exit(124);
}
#endif

if (zend_on_timeout) {
zend_on_timeout(EG(wall_timeout_seconds));
}

EG(timed_out) = 2;
EG(vm_interrupt) = 1;

#ifndef ZTS
if (EG(hard_timeout)) {
zend_set_wall_timeout_ex(EG(hard_timeout), 1);
Copy link
Member Author

@kocsismate kocsismate Feb 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding https://externals.io/message/112492#112783, my understanding is that the following happens:

  • when max_execution_wall_time is reached, the necessary globals are set (timed_out and vm_interrupt)
  • the timer is restarted with the hard_timeout when it has a value other than 0 (BTW: shouldn't we disallow negative values?)
  • if the timeout is reached again, the process gets aborted

What is not clear for me, why timeouts inside internal functions can cause a process abort immediately, given their individual timeout is >= hard_timeout? What am I missing?

Another thing I've just become unsure about is whether it is really a good idea to restart the real-time timer instead of the original CPU-time one:

Suggested change
zend_set_wall_timeout_ex(EG(hard_timeout), 1);
zend_set_timeout_ex(EG(hard_timeout), 1);

}
#endif
}
/* }}} */
#endif

#ifdef ZEND_WIN32
VOID CALLBACK tq_timer_cb(PVOID arg, BOOLEAN timed_out)
{
Expand Down Expand Up @@ -1431,15 +1495,89 @@ static void zend_set_timeout_ex(zend_long seconds, bool reset_signals) /* {{{ */
}
/* }}} */

void zend_set_timeout(zend_long seconds, bool reset_signals) /* {{{ */
static void zend_set_wall_timeout_ex(zend_long seconds, bool reset_signals) /* {{{ */
{
#ifdef ZEND_WIN32
zend_executor_globals *eg;

if (!seconds) {
return;
}

/* Don't use ChangeTimerQueueTimer() as it will not restart an expired
* timer, so we could end up with just an ignored timeout. Instead
* delete and recreate. */
if (NULL != tq_timer) {
if (!DeleteTimerQueueTimer(NULL, tq_timer, INVALID_HANDLE_VALUE)) {
tq_timer = NULL;
zend_error_noreturn(E_ERROR, "Could not delete queued timer");
return;
}
tq_timer = NULL;
}

/* XXX passing NULL means the default timer queue provided by the system is used */
eg = ZEND_MODULE_GLOBALS_BULK(executor);
if (!CreateTimerQueueTimer(&tq_timer, NULL, (WAITORTIMERCALLBACK)tq_timer_cb, (VOID*)eg, seconds*1000, 0, WT_EXECUTEONLYONCE)) {
tq_timer = NULL;
zend_error_noreturn(E_ERROR, "Could not queue new timer");
return;
}
#elif defined(HAVE_SETITIMER)
{
struct itimerval t_r; /* timeout requested */
int signo;

if(seconds) {
t_r.it_value.tv_sec = seconds;
t_r.it_value.tv_usec = t_r.it_interval.tv_sec = t_r.it_interval.tv_usec = 0;

setitimer(ITIMER_REAL, &t_r, NULL);
}
signo = SIGALRM;

if (reset_signals) {

# ifdef ZEND_SIGNALS
zend_signal(signo, zend_wall_timeout_handler);
# else
sigset_t sigset;
# ifdef HAVE_SIGACTION
struct sigaction act;

act.sa_handler = zend_wall_timeout_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESETHAND | SA_NODEFER;
sigaction(signo, &act, NULL);
# else
signal(signo, zend_wall_timeout_handler);
# endif /* HAVE_SIGACTION */
sigemptyset(&sigset);
sigaddset(&sigset, signo);
sigprocmask(SIG_UNBLOCK, &sigset, NULL);
# endif /* ZEND_SIGNALS */
}
}
#endif /* HAVE_SETITIMER */
}
/* }}} */

void zend_set_timeout(zend_long seconds, bool reset_signals) /* {{{ */
{
EG(timeout_seconds) = seconds;
zend_set_timeout_ex(seconds, reset_signals);
EG(timed_out) = 0;
}
/* }}} */

void zend_set_wall_timeout(zend_long seconds, bool reset_signals) /* {{{ */
{
EG(wall_timeout_seconds) = seconds;
zend_set_wall_timeout_ex(seconds, reset_signals);
EG(timed_out) = 0;
}
/* }}} */

void zend_unset_timeout(void) /* {{{ */
{
#ifdef ZEND_WIN32
Expand Down Expand Up @@ -1469,6 +1607,31 @@ void zend_unset_timeout(void) /* {{{ */
}
/* }}} */

void zend_unset_wall_timeout(void) /* {{{ */
{
#ifdef ZEND_WIN32
if (NULL != tq_timer) {
if (!DeleteTimerQueueTimer(NULL, tq_timer, INVALID_HANDLE_VALUE)) {
EG(timed_out) = 0;
tq_timer = NULL;
zend_error_noreturn(E_ERROR, "Could not delete queued timer");
return;
}
tq_timer = NULL;
}
#elif defined(HAVE_SETITIMER)
if (EG(wall_timeout_seconds)) {
struct itimerval no_timeout;

no_timeout.it_value.tv_sec = no_timeout.it_value.tv_usec = no_timeout.it_interval.tv_sec = no_timeout.it_interval.tv_usec = 0;

setitimer(ITIMER_REAL, &no_timeout, NULL);
}
#endif
EG(timed_out) = 0;
}
/* }}} */

zend_class_entry *zend_fetch_class(zend_string *class_name, int fetch_type) /* {{{ */
{
zend_class_entry *ce, *scope;
Expand Down
3 changes: 2 additions & 1 deletion Zend/zend_globals.h
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ struct _zend_executor_globals {
bool no_extensions;

bool vm_interrupt;
bool timed_out;
zend_long timed_out;
zend_long hard_timeout;

#ifdef ZEND_WIN32
Expand All @@ -206,6 +206,7 @@ struct _zend_executor_globals {

/* timeout support */
zend_long timeout_seconds;
zend_long wall_timeout_seconds;

int capture_warnings_during_sccp;

Expand Down
11 changes: 5 additions & 6 deletions Zend/zend_ini.c
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,7 @@ ZEND_API void zend_ini_refresh_caches(int stage) /* {{{ */

ZEND_API zend_result zend_alter_ini_entry(zend_string *name, zend_string *new_value, int modify_type, int stage) /* {{{ */
{

return zend_alter_ini_entry_ex(name, new_value, modify_type, stage, 0);
return zend_alter_ini_entry_ex(name, new_value, modify_type, stage, 0, 0);
}
/* }}} */

Expand All @@ -288,7 +287,7 @@ ZEND_API zend_result zend_alter_ini_entry_chars(zend_string *name, const char *v
zend_string *new_value;

new_value = zend_string_init(value, value_length, !(stage & ZEND_INI_STAGE_IN_REQUEST));
ret = zend_alter_ini_entry_ex(name, new_value, modify_type, stage, 0);
ret = zend_alter_ini_entry_ex(name, new_value, modify_type, stage, 0, 0);
zend_string_release(new_value);
return ret;
}
Expand All @@ -300,13 +299,13 @@ ZEND_API zend_result zend_alter_ini_entry_chars_ex(zend_string *name, const char
zend_string *new_value;

new_value = zend_string_init(value, value_length, !(stage & ZEND_INI_STAGE_IN_REQUEST));
ret = zend_alter_ini_entry_ex(name, new_value, modify_type, stage, force_change);
ret = zend_alter_ini_entry_ex(name, new_value, modify_type, stage, force_change, 0);
zend_string_release(new_value);
return ret;
}
/* }}} */

ZEND_API zend_result zend_alter_ini_entry_ex(zend_string *name, zend_string *new_value, int modify_type, int stage, bool force_change) /* {{{ */
ZEND_API zend_result zend_alter_ini_entry_ex(zend_string *name, zend_string *new_value, int modify_type, int stage, bool force_change, bool skip_on_update) /* {{{ */
{
zend_ini_entry *ini_entry;
zend_string *duplicate;
Expand Down Expand Up @@ -344,7 +343,7 @@ ZEND_API zend_result zend_alter_ini_entry_ex(zend_string *name, zend_string *new
duplicate = zend_string_copy(new_value);

if (!ini_entry->on_modify
|| ini_entry->on_modify(ini_entry, duplicate, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage) == SUCCESS) {
|| skip_on_update || ini_entry->on_modify(ini_entry, duplicate, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage) == SUCCESS) {
if (modified && ini_entry->orig_value != ini_entry->value) { /* we already changed the value, free the changed value */
zend_string_release(ini_entry->value);
}
Expand Down
2 changes: 1 addition & 1 deletion Zend/zend_ini.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ ZEND_API zend_result zend_register_ini_entries(const zend_ini_entry_def *ini_ent
ZEND_API void zend_unregister_ini_entries(int module_number);
ZEND_API void zend_ini_refresh_caches(int stage);
ZEND_API zend_result zend_alter_ini_entry(zend_string *name, zend_string *new_value, int modify_type, int stage);
ZEND_API zend_result zend_alter_ini_entry_ex(zend_string *name, zend_string *new_value, int modify_type, int stage, bool force_change);
ZEND_API zend_result zend_alter_ini_entry_ex(zend_string *name, zend_string *new_value, int modify_type, int stage, bool force_change, bool skip_on_update);
ZEND_API zend_result zend_alter_ini_entry_chars(zend_string *name, const char *value, size_t value_length, int modify_type, int stage);
ZEND_API zend_result zend_alter_ini_entry_chars_ex(zend_string *name, const char *value, size_t value_length, int modify_type, int stage, int force_change);
ZEND_API zend_result zend_restore_ini_entry(zend_string *name, int stage);
Expand Down
9 changes: 1 addition & 8 deletions Zend/zend_signal.c
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,7 @@ ZEND_API zend_signal_globals_t zend_signal_globals;
static void zend_signal_handler(int signo, siginfo_t *siginfo, void *context);
static int zend_signal_register(int signo, void (*handler)(int, siginfo_t*, void*));

#if defined(__CYGWIN__) || defined(__PASE__)
/* Matches zend_excute_API.c; these platforms don't support ITIMER_PROF. */
#define TIMEOUT_SIG SIGALRM
#else
#define TIMEOUT_SIG SIGPROF
#endif

static int zend_sigs[] = { TIMEOUT_SIG, SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2 };
static int zend_sigs[] = { SIGPROF, SIGALRM, SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2 };

#define SA_FLAGS_MASK ~(SA_NODEFER | SA_RESETHAND)

Expand Down
2 changes: 1 addition & 1 deletion ext/session/session.c
Original file line number Diff line number Diff line change
Expand Up @@ -2452,7 +2452,7 @@ static int php_session_start_set_ini(zend_string *varname, zend_string *new_valu
smart_str_appendc(&buf, '.');
smart_str_append(&buf, varname);
smart_str_0(&buf);
ret = zend_alter_ini_entry_ex(buf.s, new_value, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0);
ret = zend_alter_ini_entry_ex(buf.s, new_value, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0, 0);
smart_str_free(&buf);
return ret;
}
Expand Down
8 changes: 4 additions & 4 deletions ext/standard/assert.c
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ PHP_FUNCTION(assert_options)
}

key = zend_string_init("assert.active", sizeof("assert.active")-1, 0);
zend_alter_ini_entry_ex(key, value_str, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0);
zend_alter_ini_entry_ex(key, value_str, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0, 0);
zend_string_release_ex(key, 0);
zend_string_release_ex(value_str, 0);
}
Expand All @@ -244,7 +244,7 @@ PHP_FUNCTION(assert_options)
}

key = zend_string_init("assert.bail", sizeof("assert.bail")-1, 0);
zend_alter_ini_entry_ex(key, value_str, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0);
zend_alter_ini_entry_ex(key, value_str, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0, 0);
zend_string_release_ex(key, 0);
zend_string_release_ex(value_str, 0);
}
Expand All @@ -260,7 +260,7 @@ PHP_FUNCTION(assert_options)
}

key = zend_string_init("assert.warning", sizeof("assert.warning")-1, 0);
zend_alter_ini_entry_ex(key, value_str, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0);
zend_alter_ini_entry_ex(key, value_str, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0, 0);
zend_string_release_ex(key, 0);
zend_string_release_ex(value_str, 0);
}
Expand Down Expand Up @@ -295,7 +295,7 @@ PHP_FUNCTION(assert_options)
}

key = zend_string_init("assert.exception", sizeof("assert.exception")-1, 0);
zend_alter_ini_entry_ex(key, val, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0);
zend_alter_ini_entry_ex(key, val, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0, 0);
zend_string_release_ex(val, 0);
zend_string_release_ex(key, 0);
}
Expand Down
4 changes: 2 additions & 2 deletions ext/standard/basic_functions.c
Original file line number Diff line number Diff line change
Expand Up @@ -2128,7 +2128,7 @@ PHP_FUNCTION(ini_set)
}
#undef _CHECK_PATH

if (zend_alter_ini_entry_ex(varname, new_value_str, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0) == FAILURE) {
if (zend_alter_ini_entry_ex(varname, new_value_str, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0, 0) == FAILURE) {
zval_ptr_dtor_str(return_value);
RETVAL_FALSE;
}
Expand Down Expand Up @@ -2171,7 +2171,7 @@ PHP_FUNCTION(set_include_path)
}

key = zend_string_init("include_path", sizeof("include_path") - 1, 0);
if (zend_alter_ini_entry_ex(key, new_value, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0) == FAILURE) {
if (zend_alter_ini_entry_ex(key, new_value, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0, 0) == FAILURE) {
zend_string_release_ex(key, 0);
zval_ptr_dtor_str(return_value);
RETURN_FALSE;
Expand Down
Loading