Skip to content

Commit c1e5a45

Browse files
committed
Limit stack usage
1 parent 2673c1d commit c1e5a45

21 files changed

+567
-2
lines changed

Zend/Zend.m4

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,56 @@ _LT_AC_TRY_DLOPEN_SELF([
146146
])
147147
148148
dnl Checks for library functions.
149-
AC_CHECK_FUNCS(getpid kill sigsetjmp)
149+
AC_CHECK_FUNCS(getpid kill sigsetjmp pthread_getattr_np)
150+
151+
dnl Test whether the target system has __libc_stack_end
152+
AC_MSG_CHECKING(whether libc has __libc_stack_end)
153+
154+
AC_RUN_IFELSE([AC_LANG_SOURCE([[
155+
extern void* __libc_stack_end;
156+
int main()
157+
{
158+
void *test = __libc_stack_end;
159+
return 0;
160+
}
161+
]])], [
162+
AC_DEFINE([HAVE_LIBC_STACK_END], 1, [Define if the target system has __libc_stack_end])
163+
AC_MSG_RESULT(yes)
164+
], [
165+
AC_MSG_RESULT(no)
166+
], [
167+
AC_MSG_RESULT(no)
168+
])
169+
170+
dnl Test whether the stack grows downwards
171+
dnl Assumes contiguous stack
172+
AC_MSG_CHECKING(whether the stack grows downwards)
173+
174+
AC_RUN_IFELSE([AC_LANG_SOURCE([[
175+
#include <stdint.h>
176+
177+
int (*volatile f)(uintptr_t);
178+
179+
int stack_grows_downwards(uintptr_t arg) {
180+
int local;
181+
return (uintptr_t)&local < arg;
182+
}
183+
184+
int main() {
185+
int local;
186+
187+
f = stack_grows_downwards;
188+
return f((uintptr_t)&local) ? 0 : 1;
189+
}
190+
]])], [
191+
AC_DEFINE([ZEND_STACK_GROWS_DOWNWARDS], 1, [Define if the stack grows downwards])
192+
AC_DEFINE([ZEND_CHECK_STACK_LIMIT], 1, [Define if checking the stack limit is supported])
193+
AC_MSG_RESULT(yes)
194+
], [
195+
AC_MSG_RESULT(no)
196+
], [
197+
AC_MSG_RESULT(no)
198+
])
150199
151200
ZEND_CHECK_FLOAT_PRECISION
152201

Zend/tests/stack_limit_001.phpt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
--TEST--
2+
Stack limit 001
3+
--FILE--
4+
<?php
5+
6+
class Test1 {
7+
public function __destruct() {
8+
new Test1;
9+
}
10+
}
11+
12+
class Test2 {
13+
public function __clone() {
14+
clone $this;
15+
}
16+
}
17+
18+
function replace() {
19+
return preg_replace_callback('#.#', function () {
20+
return replace();
21+
}, 'x');
22+
}
23+
24+
try {
25+
new Test1;
26+
} catch (Error $e) {
27+
echo $e->getMessage(), "\n";
28+
}
29+
30+
try {
31+
clone new Test2;
32+
} catch (Error $e) {
33+
echo $e->getMessage(), "\n";
34+
}
35+
36+
try {
37+
replace();
38+
} catch (Error $e) {
39+
echo $e->getMessage(), "\n";
40+
}
41+
42+
?>
43+
--EXPECTF--
44+
Maximum call stack size reached. Infinite recursion?
45+
Maximum call stack size reached. Infinite recursion?
46+
Maximum call stack size reached. Infinite recursion?

Zend/tests/stack_limit_002.phpt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
--TEST--
2+
Stack limit 002
3+
--FILE--
4+
<?php
5+
6+
class Test1 {
7+
public function __destruct() {
8+
new Test1;
9+
}
10+
}
11+
12+
class Test2 {
13+
public function __clone() {
14+
clone $this;
15+
}
16+
}
17+
18+
function replace() {
19+
return preg_replace_callback('#.#', function () {
20+
return replace();
21+
}, 'x');
22+
}
23+
24+
$fiber = new Fiber(function (): void {
25+
try {
26+
new Test1;
27+
} catch (Error $e) {
28+
echo $e->getMessage(), "\n";
29+
}
30+
31+
try {
32+
clone new Test2;
33+
} catch (Error $e) {
34+
echo $e->getMessage(), "\n";
35+
}
36+
37+
try {
38+
replace();
39+
} catch (Error $e) {
40+
echo $e->getMessage(), "\n";
41+
}
42+
});
43+
44+
$fiber->start();
45+
46+
?>
47+
--EXPECTF--
48+
Maximum call stack size reached. Infinite recursion?
49+
Maximum call stack size reached. Infinite recursion?
50+
Maximum call stack size reached. Infinite recursion?

Zend/tests/stack_limit_003.phpt

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
--TEST--
2+
Stack limit 003
3+
--INI--
4+
zend.max_allowed_stack_size=40K
5+
--FILE--
6+
<?php
7+
8+
class Test1 {
9+
public function __destruct() {
10+
new Test1;
11+
}
12+
}
13+
14+
class Test2 {
15+
public function __clone() {
16+
clone $this;
17+
}
18+
}
19+
20+
function replace() {
21+
return preg_replace_callback('#.#', function () {
22+
return replace();
23+
}, 'x');
24+
}
25+
26+
try {
27+
new Test1;
28+
} catch (Error $e) {
29+
echo $e->getMessage(), "\n";
30+
}
31+
32+
try {
33+
clone new Test2;
34+
} catch (Error $e) {
35+
echo $e->getMessage(), "\n";
36+
}
37+
38+
try {
39+
replace();
40+
} catch (Error $e) {
41+
echo $e->getMessage(), "\n";
42+
}
43+
44+
?>
45+
--EXPECTF--
46+
Maximum call stack size reached. Infinite recursion?
47+
Maximum call stack size reached. Infinite recursion?
48+
Maximum call stack size reached. Infinite recursion?

Zend/tests/stack_limit_004.phpt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
--TEST--
2+
Stack limit 004
3+
--FILE--
4+
<?php
5+
6+
class Test1 {
7+
public function __destruct() {
8+
new Test1;
9+
}
10+
}
11+
12+
$callback = function (): int {
13+
try {
14+
new Test1;
15+
} catch (Error $e) {
16+
return count($e->getTrace());
17+
}
18+
19+
throw new \Exception();
20+
};
21+
22+
ini_set('fiber.stack_size', '100K');
23+
$fiber = new Fiber($callback);
24+
$fiber->start();
25+
$depth1 = $fiber->getReturn();
26+
27+
ini_set('fiber.stack_size', '50K');
28+
$fiber = new Fiber($callback);
29+
$fiber->start();
30+
$depth2 = $fiber->getReturn();
31+
32+
var_dump($depth1 > $depth2);
33+
34+
?>
35+
--EXPECT--
36+
bool(true)

Zend/tests/stack_limit_005.phpt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
Stack limit 005
3+
--INI--
4+
zend.max_allowed_stack_size=50K
5+
--FILE--
6+
<?php
7+
8+
$test
9+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
10+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
11+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
12+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
13+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
14+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
15+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
16+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
17+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
18+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
19+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
20+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
21+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
22+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
23+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
24+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
25+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
26+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
27+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
28+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
29+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
30+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
31+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
32+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
33+
->f()->f()->f()->f()->f()->f()->f()->f()->f()->f()
34+
;
35+
36+
--EXPECTF--
37+
Fatal error: Maximum call stack size reached. Try splitting expression in %s on line 3

Zend/zend.c

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include "zend_attributes.h"
3636
#include "zend_observer.h"
3737
#include "zend_fibers.h"
38+
#include "zend_call_stack.h"
3839
#include "Optimizer/zend_optimizer.h"
3940

4041
static size_t global_map_ptr_last = 0;
@@ -173,6 +174,38 @@ static ZEND_INI_MH(OnSetExceptionStringParamMaxLen) /* {{{ */
173174
}
174175
/* }}} */
175176

177+
#ifdef ZEND_CHECK_STACK_LIMIT
178+
static ZEND_INI_MH(OnUpdateMaxAllowedStackSize) /* {{{ */
179+
{
180+
EG(max_allowed_stack_size) = (size_t) zend_ini_parse_uquantity_warn(new_value, entry->name);
181+
if (EG(max_allowed_stack_size)) {
182+
return SUCCESS;
183+
}
184+
185+
/* Don't detect the stack size if we can't detect the stack base */
186+
if (zend_call_stack_base() != NULL) {
187+
size_t limit = zend_call_stack_limit();
188+
if (limit != 0) {
189+
EG(max_allowed_stack_size) = limit;
190+
return SUCCESS;
191+
}
192+
}
193+
194+
EG(max_allowed_stack_size) = 2 * 1024 * 1024;
195+
196+
return SUCCESS;
197+
}
198+
/* }}} */
199+
200+
static ZEND_INI_MH(OnUpdateReservedStackSize) /* {{{ */
201+
{
202+
EG(reserved_stack_size) = (size_t) zend_ini_parse_uquantity_warn(new_value, entry->name);
203+
204+
return SUCCESS;
205+
}
206+
/* }}} */
207+
#endif /* ZEND_CHECK_STACK_LIMIT */
208+
176209
static ZEND_INI_MH(OnUpdateFiberStackSize) /* {{{ */
177210
{
178211
if (new_value) {
@@ -203,6 +236,12 @@ ZEND_INI_BEGIN()
203236
STD_ZEND_INI_BOOLEAN("zend.exception_ignore_args", "0", ZEND_INI_ALL, OnUpdateBool, exception_ignore_args, zend_executor_globals, executor_globals)
204237
STD_ZEND_INI_ENTRY("zend.exception_string_param_max_len", "15", ZEND_INI_ALL, OnSetExceptionStringParamMaxLen, exception_string_param_max_len, zend_executor_globals, executor_globals)
205238
STD_ZEND_INI_ENTRY("fiber.stack_size", NULL, ZEND_INI_ALL, OnUpdateFiberStackSize, fiber_stack_size, zend_executor_globals, executor_globals)
239+
#ifdef ZEND_CHECK_STACK_LIMIT
240+
/* The maximum allowed call stack size. 0: auto detect. For fibers, this is fiber.stack_size. */
241+
STD_ZEND_INI_ENTRY("zend.max_allowed_stack_size", "0", ZEND_INI_PERDIR, OnUpdateMaxAllowedStackSize, max_allowed_stack_size, zend_executor_globals, executor_globals)
242+
/* Substracted from the max allowed stack size, as a buffer, when checking for overflow. */
243+
STD_ZEND_INI_ENTRY("zend.reserved_stack_size", "12288", ZEND_INI_PERDIR, OnUpdateReservedStackSize, reserved_stack_size, zend_executor_globals, executor_globals)
244+
#endif
206245

207246
ZEND_INI_END()
208247

0 commit comments

Comments
 (0)