Skip to content

Commit de519e5

Browse files
committed
Add support for limiting maximum execution time based on wall-time
1 parent 31088ee commit de519e5

9 files changed

+287
-13
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 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
@@ -1260,9 +1260,14 @@ ZEND_API zend_result zend_eval_string_ex(const char *str, zval *retval_ptr, cons
12601260
/* }}} */
12611261

12621262
static void zend_set_timeout_ex(zend_long seconds, bool reset_signals);
1263+
static void zend_set_wall_timeout_ex(zend_long seconds, bool reset_signals);
12631264

12641265
ZEND_API ZEND_NORETURN void ZEND_FASTCALL zend_timeout(void) /* {{{ */
12651266
{
1267+
zend_long original_timed_out = EG(timed_out);
1268+
zend_long original_timeout_seconds = EG(timeout_seconds);
1269+
zend_long original_wall_timeout_seconds = EG(wall_timeout_seconds);
1270+
12661271
#if defined(PHP_WIN32)
12671272
# ifndef ZTS
12681273
/* No action is needed if we're timed out because zero seconds are
@@ -1278,10 +1283,18 @@ ZEND_API ZEND_NORETURN void ZEND_FASTCALL zend_timeout(void) /* {{{ */
12781283
# endif
12791284
#else
12801285
EG(timed_out) = 0;
1281-
zend_set_timeout_ex(0, 1);
1286+
if (original_timed_out == 1) {
1287+
zend_set_timeout_ex(0, 1);
1288+
} else {
1289+
zend_set_wall_timeout_ex(0, 1);
1290+
}
12821291
#endif
12831292

1284-
zend_error_noreturn(E_ERROR, "Maximum execution time of " ZEND_LONG_FMT " second%s exceeded", EG(timeout_seconds), EG(timeout_seconds) == 1 ? "" : "s");
1293+
if (original_timed_out == 1) {
1294+
zend_error_noreturn(E_ERROR, "Maximum execution time of " ZEND_LONG_FMT " second%s exceeded", original_timeout_seconds, original_timeout_seconds == 1 ? "" : "s");
1295+
} else {
1296+
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");
1297+
}
12851298
}
12861299
/* }}} */
12871300

@@ -1337,6 +1350,57 @@ static void zend_timeout_handler(int dummy) /* {{{ */
13371350
/* }}} */
13381351
#endif
13391352

1353+
#ifndef ZEND_WIN32
1354+
static void zend_wall_timeout_handler(int dummy) /* {{{ */
1355+
{
1356+
#ifndef ZTS
1357+
if (EG(timed_out)) {
1358+
/* Die on hard timeout */
1359+
const char *error_filename = NULL;
1360+
uint32_t error_lineno = 0;
1361+
char log_buffer[2048];
1362+
int output_len = 0;
1363+
1364+
if (zend_is_compiling()) {
1365+
error_filename = ZSTR_VAL(zend_get_compiled_filename());
1366+
error_lineno = zend_get_compiled_lineno();
1367+
} else if (zend_is_executing()) {
1368+
error_filename = zend_get_executed_filename();
1369+
if (error_filename[0] == '[') { /* [no active file] */
1370+
error_filename = NULL;
1371+
error_lineno = 0;
1372+
} else {
1373+
error_lineno = zend_get_executed_lineno();
1374+
}
1375+
}
1376+
if (!error_filename) {
1377+
error_filename = "Unknown";
1378+
}
1379+
1380+
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);
1381+
if (output_len > 0) {
1382+
zend_quiet_write(2, log_buffer, MIN(output_len, sizeof(log_buffer)));
1383+
}
1384+
_exit(124);
1385+
}
1386+
#endif
1387+
1388+
if (zend_on_timeout) {
1389+
zend_on_timeout(EG(wall_timeout_seconds));
1390+
}
1391+
1392+
EG(timed_out) = 2;
1393+
EG(vm_interrupt) = 1;
1394+
1395+
#ifndef ZTS
1396+
if (EG(hard_timeout)) {
1397+
zend_set_wall_timeout_ex(EG(hard_timeout), 1);
1398+
}
1399+
#endif
1400+
}
1401+
/* }}} */
1402+
#endif
1403+
13401404
#ifdef ZEND_WIN32
13411405
VOID CALLBACK tq_timer_cb(PVOID arg, BOOLEAN timed_out)
13421406
{
@@ -1431,15 +1495,89 @@ static void zend_set_timeout_ex(zend_long seconds, bool reset_signals) /* {{{ */
14311495
}
14321496
/* }}} */
14331497

1434-
void zend_set_timeout(zend_long seconds, bool reset_signals) /* {{{ */
1498+
static void zend_set_wall_timeout_ex(zend_long seconds, bool reset_signals) /* {{{ */
14351499
{
1500+
#ifdef ZEND_WIN32
1501+
zend_executor_globals *eg;
1502+
1503+
if (!seconds) {
1504+
return;
1505+
}
1506+
1507+
/* Don't use ChangeTimerQueueTimer() as it will not restart an expired
1508+
* timer, so we could end up with just an ignored timeout. Instead
1509+
* delete and recreate. */
1510+
if (NULL != tq_timer) {
1511+
if (!DeleteTimerQueueTimer(NULL, tq_timer, INVALID_HANDLE_VALUE)) {
1512+
tq_timer = NULL;
1513+
zend_error_noreturn(E_ERROR, "Could not delete queued timer");
1514+
return;
1515+
}
1516+
tq_timer = NULL;
1517+
}
1518+
1519+
/* XXX passing NULL means the default timer queue provided by the system is used */
1520+
eg = ZEND_MODULE_GLOBALS_BULK(executor);
1521+
if (!CreateTimerQueueTimer(&tq_timer, NULL, (WAITORTIMERCALLBACK)tq_timer_cb, (VOID*)eg, seconds*1000, 0, WT_EXECUTEONLYONCE)) {
1522+
tq_timer = NULL;
1523+
zend_error_noreturn(E_ERROR, "Could not queue new timer");
1524+
return;
1525+
}
1526+
#elif defined(HAVE_SETITIMER)
1527+
{
1528+
struct itimerval t_r; /* timeout requested */
1529+
int signo;
1530+
1531+
if(seconds) {
1532+
t_r.it_value.tv_sec = seconds;
1533+
t_r.it_value.tv_usec = t_r.it_interval.tv_sec = t_r.it_interval.tv_usec = 0;
1534+
1535+
setitimer(ITIMER_REAL, &t_r, NULL);
1536+
}
1537+
signo = SIGALRM;
14361538

1539+
if (reset_signals) {
1540+
1541+
# ifdef ZEND_SIGNALS
1542+
zend_signal(signo, zend_wall_timeout_handler);
1543+
# else
1544+
sigset_t sigset;
1545+
# ifdef HAVE_SIGACTION
1546+
struct sigaction act;
1547+
1548+
act.sa_handler = zend_wall_timeout_handler;
1549+
sigemptyset(&act.sa_mask);
1550+
act.sa_flags = SA_RESETHAND | SA_NODEFER;
1551+
sigaction(signo, &act, NULL);
1552+
# else
1553+
signal(signo, zend_wall_timeout_handler);
1554+
# endif /* HAVE_SIGACTION */
1555+
sigemptyset(&sigset);
1556+
sigaddset(&sigset, signo);
1557+
sigprocmask(SIG_UNBLOCK, &sigset, NULL);
1558+
# endif /* ZEND_SIGNALS */
1559+
}
1560+
}
1561+
#endif /* HAVE_SETITIMER */
1562+
}
1563+
/* }}} */
1564+
1565+
void zend_set_timeout(zend_long seconds, bool reset_signals) /* {{{ */
1566+
{
14371567
EG(timeout_seconds) = seconds;
14381568
zend_set_timeout_ex(seconds, reset_signals);
14391569
EG(timed_out) = 0;
14401570
}
14411571
/* }}} */
14421572

1573+
void zend_set_wall_timeout(zend_long seconds, bool reset_signals) /* {{{ */
1574+
{
1575+
EG(wall_timeout_seconds) = seconds;
1576+
zend_set_wall_timeout_ex(seconds, reset_signals);
1577+
EG(timed_out) = 0;
1578+
}
1579+
/* }}} */
1580+
14431581
void zend_unset_timeout(void) /* {{{ */
14441582
{
14451583
#ifdef ZEND_WIN32
@@ -1469,6 +1607,31 @@ void zend_unset_timeout(void) /* {{{ */
14691607
}
14701608
/* }}} */
14711609

1610+
void zend_unset_wall_timeout(void) /* {{{ */
1611+
{
1612+
#ifdef ZEND_WIN32
1613+
if (NULL != tq_timer) {
1614+
if (!DeleteTimerQueueTimer(NULL, tq_timer, INVALID_HANDLE_VALUE)) {
1615+
EG(timed_out) = 0;
1616+
tq_timer = NULL;
1617+
zend_error_noreturn(E_ERROR, "Could not delete queued timer");
1618+
return;
1619+
}
1620+
tq_timer = NULL;
1621+
}
1622+
#elif defined(HAVE_SETITIMER)
1623+
if (EG(wall_timeout_seconds)) {
1624+
struct itimerval no_timeout;
1625+
1626+
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;
1627+
1628+
setitimer(ITIMER_REAL, &no_timeout, NULL);
1629+
}
1630+
#endif
1631+
EG(timed_out) = 0;
1632+
}
1633+
/* }}} */
1634+
14721635
zend_class_entry *zend_fetch_class(zend_string *class_name, int fetch_type) /* {{{ */
14731636
{
14741637
zend_class_entry *ce, *scope;

Zend/zend_globals.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ struct _zend_executor_globals {
184184
bool no_extensions;
185185

186186
bool vm_interrupt;
187-
bool timed_out;
187+
zend_long timed_out;
188188
zend_long hard_timeout;
189189

190190
#ifdef ZEND_WIN32
@@ -206,6 +206,7 @@ struct _zend_executor_globals {
206206

207207
/* timeout support */
208208
zend_long timeout_seconds;
209+
zend_long wall_timeout_seconds;
209210

210211
int capture_warnings_during_sccp;
211212

Zend/zend_signal.c

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +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-
#if defined(__CYGWIN__) || defined(__PASE__)
66-
/* Matches zend_excute_API.c; these platforms don't support ITIMER_PROF. */
67-
#define TIMEOUT_SIG SIGALRM
68-
#else
69-
#define TIMEOUT_SIG SIGPROF
70-
#endif
71-
72-
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 };
7366

7467
#define SA_FLAGS_MASK ~(SA_NODEFER | SA_RESETHAND)
7568

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
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
--TEST--
2+
Safe shutdown when hard timeout is not reached
3+
--SKIPIF--
4+
<?php
5+
if (getenv("SKIP_SLOW_TESTS")) die("skip slow test");
6+
include __DIR__ . "/../../sapi/cli/tests/skipif.inc";
7+
?>
8+
--INI--
9+
allow_url_fopen=1
10+
hard_timeout=5
11+
--FILE--
12+
<?php
13+
14+
require __DIR__ . "/../../sapi/cli/tests/php_cli_server.inc";
15+
16+
php_cli_server_start("sleep(3);");
17+
18+
ini_set("max_execution_time", 1);
19+
ini_set("max_execution_wall_time", 1);
20+
ini_set("default_socket_timeout", 1);
21+
22+
register_shutdown_function(function () {
23+
echo "OK";
24+
});
25+
26+
@file_get_contents("http://" . PHP_CLI_SERVER_ADDRESS);
27+
28+
?>
29+
--EXPECTF--
30+
Fatal error: Maximum execution wall-time of 1 second exceeded in %s on line %d
31+
OK

0 commit comments

Comments
 (0)