Skip to content

Commit e519909

Browse files
committed
Add support for limiting maximum execution time based on wall-time
1 parent 78c0338 commit e519909

7 files changed

+226
-12
lines changed

Zend/zend_execute.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,9 @@ ZEND_API zend_bool zend_is_executing(void);
321321
ZEND_API ZEND_COLD void ZEND_FASTCALL zend_cannot_pass_by_reference(uint32_t arg_num);
322322

323323
ZEND_API void zend_set_timeout(zend_long seconds, bool reset_signals);
324+
ZEND_API void zend_set_wall_timeout(zend_long seconds, bool reset_signals);
324325
ZEND_API void zend_unset_timeout(void);
326+
ZEND_API void zend_unset_wall_timeout(void);
325327
ZEND_API ZEND_NORETURN void ZEND_FASTCALL zend_timeout(void);
326328
ZEND_API zend_class_entry *zend_fetch_class(zend_string *class_name, int fetch_type);
327329
ZEND_API zend_class_entry *zend_fetch_class_by_name(zend_string *class_name, zend_string *lcname, int fetch_type);

Zend/zend_execute_API.c

Lines changed: 166 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,9 +1246,14 @@ ZEND_API zend_result zend_eval_string_ex(const char *str, zval *retval_ptr, cons
12461246
/* }}} */
12471247

12481248
static void zend_set_timeout_ex(zend_long seconds, bool reset_signals);
1249+
static void zend_set_wall_timeout_ex(zend_long seconds, bool reset_signals);
12491250

12501251
ZEND_API ZEND_NORETURN void ZEND_FASTCALL zend_timeout(void) /* {{{ */
12511252
{
1253+
zend_long original_timed_out = EG(timed_out);
1254+
zend_long original_timeout_seconds = EG(timeout_seconds);
1255+
zend_long original_wall_timeout_seconds = EG(wall_timeout_seconds);
1256+
12521257
#if defined(PHP_WIN32)
12531258
# ifndef ZTS
12541259
/* No action is needed if we're timed out because zero seconds are
@@ -1264,10 +1269,18 @@ ZEND_API ZEND_NORETURN void ZEND_FASTCALL zend_timeout(void) /* {{{ */
12641269
# endif
12651270
#else
12661271
EG(timed_out) = 0;
1267-
zend_set_timeout_ex(0, 1);
1272+
if (original_timed_out == 1) {
1273+
zend_set_timeout_ex(0, 1);
1274+
} else {
1275+
zend_set_wall_timeout_ex(0, 1);
1276+
}
12681277
#endif
12691278

1270-
zend_error_noreturn(E_ERROR, "Maximum execution time of " ZEND_LONG_FMT " second%s exceeded", EG(timeout_seconds), EG(timeout_seconds) == 1 ? "" : "s");
1279+
if (original_timed_out == 1) {
1280+
zend_error_noreturn(E_ERROR, "Maximum execution time of " ZEND_LONG_FMT " second%s exceeded", original_timeout_seconds, original_timeout_seconds == 1 ? "" : "s");
1281+
} else {
1282+
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");
1283+
}
12711284
}
12721285
/* }}} */
12731286

@@ -1323,6 +1336,57 @@ static void zend_timeout_handler(int dummy) /* {{{ */
13231336
/* }}} */
13241337
#endif
13251338

1339+
#ifndef ZEND_WIN32
1340+
static void zend_wall_timeout_handler(int dummy) /* {{{ */
1341+
{
1342+
#ifndef ZTS
1343+
if (EG(timed_out)) {
1344+
/* Die on hard timeout */
1345+
const char *error_filename = NULL;
1346+
uint32_t error_lineno = 0;
1347+
char log_buffer[2048];
1348+
int output_len = 0;
1349+
1350+
if (zend_is_compiling()) {
1351+
error_filename = ZSTR_VAL(zend_get_compiled_filename());
1352+
error_lineno = zend_get_compiled_lineno();
1353+
} else if (zend_is_executing()) {
1354+
error_filename = zend_get_executed_filename();
1355+
if (error_filename[0] == '[') { /* [no active file] */
1356+
error_filename = NULL;
1357+
error_lineno = 0;
1358+
} else {
1359+
error_lineno = zend_get_executed_lineno();
1360+
}
1361+
}
1362+
if (!error_filename) {
1363+
error_filename = "Unknown";
1364+
}
1365+
1366+
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);
1367+
if (output_len > 0) {
1368+
zend_quiet_write(2, log_buffer, MIN(output_len, sizeof(log_buffer)));
1369+
}
1370+
_exit(124);
1371+
}
1372+
#endif
1373+
1374+
if (zend_on_timeout) {
1375+
//zend_on_timeout(EG(wall_timeout_seconds));
1376+
}
1377+
1378+
EG(timed_out) = 2;
1379+
EG(vm_interrupt) = 1;
1380+
1381+
#ifndef ZTS
1382+
if (EG(hard_timeout)) {
1383+
zend_set_wall_timeout_ex(EG(hard_timeout), 1);
1384+
}
1385+
#endif
1386+
}
1387+
/* }}} */
1388+
#endif
1389+
13261390
#ifdef ZEND_WIN32
13271391
VOID CALLBACK tq_timer_cb(PVOID arg, BOOLEAN timed_out)
13281392
{
@@ -1417,15 +1481,89 @@ static void zend_set_timeout_ex(zend_long seconds, bool reset_signals) /* {{{ */
14171481
}
14181482
/* }}} */
14191483

1420-
void zend_set_timeout(zend_long seconds, bool reset_signals) /* {{{ */
1484+
static void zend_set_wall_timeout_ex(zend_long seconds, bool reset_signals) /* {{{ */
14211485
{
1486+
#ifdef ZEND_WIN32
1487+
zend_executor_globals *eg;
1488+
1489+
if (!seconds) {
1490+
return;
1491+
}
1492+
1493+
/* Don't use ChangeTimerQueueTimer() as it will not restart an expired
1494+
* timer, so we could end up with just an ignored timeout. Instead
1495+
* delete and recreate. */
1496+
if (NULL != tq_timer) {
1497+
if (!DeleteTimerQueueTimer(NULL, tq_timer, INVALID_HANDLE_VALUE)) {
1498+
tq_timer = NULL;
1499+
zend_error_noreturn(E_ERROR, "Could not delete queued timer");
1500+
return;
1501+
}
1502+
tq_timer = NULL;
1503+
}
1504+
1505+
/* XXX passing NULL means the default timer queue provided by the system is used */
1506+
eg = ZEND_MODULE_GLOBALS_BULK(executor);
1507+
if (!CreateTimerQueueTimer(&tq_timer, NULL, (WAITORTIMERCALLBACK)tq_timer_cb, (VOID*)eg, seconds*1000, 0, WT_EXECUTEONLYONCE)) {
1508+
tq_timer = NULL;
1509+
zend_error_noreturn(E_ERROR, "Could not queue new timer");
1510+
return;
1511+
}
1512+
#elif defined(HAVE_SETITIMER)
1513+
{
1514+
struct itimerval t_r; /* timeout requested */
1515+
int signo;
1516+
1517+
if(seconds) {
1518+
t_r.it_value.tv_sec = seconds;
1519+
t_r.it_value.tv_usec = t_r.it_interval.tv_sec = t_r.it_interval.tv_usec = 0;
1520+
1521+
setitimer(ITIMER_REAL, &t_r, NULL);
1522+
}
1523+
signo = SIGALRM;
14221524

1525+
if (reset_signals) {
1526+
1527+
# ifdef ZEND_SIGNALS
1528+
zend_signal(signo, zend_wall_timeout_handler);
1529+
# else
1530+
sigset_t sigset;
1531+
# ifdef HAVE_SIGACTION
1532+
struct sigaction act;
1533+
1534+
act.sa_handler = zend_wall_timeout_handler;
1535+
sigemptyset(&act.sa_mask);
1536+
act.sa_flags = SA_RESETHAND | SA_NODEFER;
1537+
sigaction(signo, &act, NULL);
1538+
# else
1539+
signal(signo, zend_wall_timeout_handler);
1540+
# endif /* HAVE_SIGACTION */
1541+
sigemptyset(&sigset);
1542+
sigaddset(&sigset, signo);
1543+
sigprocmask(SIG_UNBLOCK, &sigset, NULL);
1544+
# endif /* ZEND_SIGNALS */
1545+
}
1546+
}
1547+
#endif /* HAVE_SETITIMER */
1548+
}
1549+
/* }}} */
1550+
1551+
void zend_set_timeout(zend_long seconds, bool reset_signals) /* {{{ */
1552+
{
14231553
EG(timeout_seconds) = seconds;
14241554
zend_set_timeout_ex(seconds, reset_signals);
14251555
EG(timed_out) = 0;
14261556
}
14271557
/* }}} */
14281558

1559+
void zend_set_wall_timeout(zend_long seconds, bool reset_signals) /* {{{ */
1560+
{
1561+
EG(wall_timeout_seconds) = seconds;
1562+
zend_set_wall_timeout_ex(seconds, reset_signals);
1563+
EG(timed_out) = 0;
1564+
}
1565+
/* }}} */
1566+
14291567
void zend_unset_timeout(void) /* {{{ */
14301568
{
14311569
#ifdef ZEND_WIN32
@@ -1455,6 +1593,31 @@ void zend_unset_timeout(void) /* {{{ */
14551593
}
14561594
/* }}} */
14571595

1596+
void zend_unset_wall_timeout(void) /* {{{ */
1597+
{
1598+
#ifdef ZEND_WIN32
1599+
if (NULL != tq_timer) {
1600+
if (!DeleteTimerQueueTimer(NULL, tq_timer, INVALID_HANDLE_VALUE)) {
1601+
EG(timed_out) = 0;
1602+
tq_timer = NULL;
1603+
zend_error_noreturn(E_ERROR, "Could not delete queued timer");
1604+
return;
1605+
}
1606+
tq_timer = NULL;
1607+
}
1608+
#elif defined(HAVE_SETITIMER)
1609+
if (EG(wall_timeout_seconds)) {
1610+
struct itimerval no_timeout;
1611+
1612+
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;
1613+
1614+
setitimer(ITIMER_REAL, &no_timeout, NULL);
1615+
}
1616+
#endif
1617+
EG(timed_out) = 0;
1618+
}
1619+
/* }}} */
1620+
14581621
zend_class_entry *zend_fetch_class(zend_string *class_name, int fetch_type) /* {{{ */
14591622
{
14601623
zend_class_entry *ce, *scope;

Zend/zend_globals.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ struct _zend_executor_globals {
182182
zend_bool no_extensions;
183183

184184
zend_bool vm_interrupt;
185-
zend_bool timed_out;
185+
zend_long timed_out;
186186
zend_long hard_timeout;
187187

188188
#ifdef ZEND_WIN32
@@ -204,6 +204,7 @@ struct _zend_executor_globals {
204204

205205
/* timeout support */
206206
zend_long timeout_seconds;
207+
zend_long wall_timeout_seconds;
207208

208209
int lambda_count;
209210

Zend/zend_signal.c

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,7 @@ ZEND_API zend_signal_globals_t zend_signal_globals;
6262
static void zend_signal_handler(int signo, siginfo_t *siginfo, void *context);
6363
static int zend_signal_register(int signo, void (*handler)(int, siginfo_t*, void*));
6464

65-
#ifdef __CYGWIN__
66-
#define TIMEOUT_SIG SIGALRM
67-
#else
68-
#define TIMEOUT_SIG SIGPROF
69-
#endif
70-
71-
static int zend_sigs[] = { TIMEOUT_SIG, SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2 };
65+
static int zend_sigs[] = { SIGPROF, SIGALRM, SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2 };
7266

7367
#define SA_FLAGS_MASK ~(SA_NODEFER | SA_RESETHAND)
7468

main/main.c

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,21 @@ static PHP_INI_MH(OnUpdateTimeout)
400400
}
401401
/* }}} */
402402

403+
/* {{{ PHP_INI_MH */
404+
static PHP_INI_MH(OnUpdateWallTimeout)
405+
{
406+
if (stage==PHP_INI_STAGE_STARTUP) {
407+
/* Don't set a timeout on startup, only per-request */
408+
ZEND_ATOL(EG(wall_timeout_seconds), ZSTR_VAL(new_value));
409+
return SUCCESS;
410+
}
411+
zend_unset_wall_timeout();
412+
ZEND_ATOL(EG(wall_timeout_seconds), ZSTR_VAL(new_value));
413+
zend_set_wall_timeout(EG(wall_timeout_seconds), 0);
414+
return SUCCESS;
415+
}
416+
/* }}} */
417+
403418
/* {{{ php_get_display_errors_mode() helper function */
404419
static zend_uchar php_get_display_errors_mode(char *value, size_t value_length)
405420
{
@@ -688,6 +703,7 @@ PHP_INI_BEGIN()
688703
STD_PHP_INI_ENTRY("sys_temp_dir", NULL, PHP_INI_SYSTEM, OnUpdateStringUnempty, sys_temp_dir, php_core_globals, core_globals)
689704
STD_PHP_INI_ENTRY("include_path", PHP_INCLUDE_PATH, PHP_INI_ALL, OnUpdateStringUnempty, include_path, php_core_globals, core_globals)
690705
PHP_INI_ENTRY("max_execution_time", "30", PHP_INI_ALL, OnUpdateTimeout)
706+
PHP_INI_ENTRY("max_execution_wall_time", "0", PHP_INI_ALL, OnUpdateWallTimeout)
691707
STD_PHP_INI_ENTRY("open_basedir", NULL, PHP_INI_ALL, OnUpdateBaseDir, open_basedir, php_core_globals, core_globals)
692708

693709
STD_PHP_INI_BOOLEAN("file_uploads", "1", PHP_INI_SYSTEM, OnUpdateBool, file_uploads, php_core_globals, core_globals)
@@ -1686,6 +1702,7 @@ int php_request_startup(void)
16861702
} else {
16871703
zend_set_timeout(PG(max_input_time), 1);
16881704
}
1705+
zend_set_wall_timeout(EG(wall_timeout_seconds), 1);
16891706

16901707
/* Disable realpath cache if an open_basedir is set */
16911708
if (PG(open_basedir) && *PG(open_basedir)) {
@@ -1772,9 +1789,10 @@ void php_request_shutdown(void *dummy)
17721789
}
17731790
} zend_end_try();
17741791

1775-
/* 4. Reset max_execution_time (no longer executing php code after response sent) */
1792+
/* 4. Reset max_execution_time and max_execution_wall_time (no longer executing php code after response sent) */
17761793
zend_try {
17771794
zend_unset_timeout();
1795+
zend_unset_wall_timeout();
17781796
} zend_end_try();
17791797

17801798
/* 5. Call all extensions RSHUTDOWN functions */
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
Retrieve max_execution_wall_time ini value
3+
--FILE--
4+
<?php
5+
6+
var_dump(ini_get("max_execution_wall_time"));
7+
8+
ini_set("max_execution_wall_time", 1);
9+
10+
var_dump(ini_get("max_execution_wall_time"));
11+
12+
?>
13+
--EXPECTF--
14+
string(1) "0"
15+
string(1) "1"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
Timeout in loop
3+
--SKIPIF--
4+
<?php
5+
if (getenv("SKIP_SLOW_TESTS")) die("skip slow test");
6+
?>
7+
--FILE--
8+
<?php
9+
10+
ini_set("max_execution_time", 1);
11+
ini_set("max_execution_wall_time", 1);
12+
13+
for ($i = 0; $i < 3; $i++) {
14+
sleep(1);
15+
}
16+
17+
echo "Never reached";
18+
19+
?>
20+
--EXPECTF--
21+
Fatal error: Maximum execution wall-time of 1 second exceeded in %s on line %d

0 commit comments

Comments
 (0)