Skip to content
Open
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: 1 addition & 1 deletion Zend/zend.c
Original file line number Diff line number Diff line change
Expand Up @@ -1651,7 +1651,7 @@ static ZEND_COLD void zend_error_va_list(

static ZEND_COLD void get_filename_lineno(int type, zend_string **filename, uint32_t *lineno) {
/* Obtain relevant filename and lineno */
switch (type) {
switch (type & E_ALL) {
case E_CORE_ERROR:
case E_CORE_WARNING:
*filename = NULL;
Expand Down
2 changes: 1 addition & 1 deletion Zend/zend_execute.h
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_cannot_pass_by_reference_ex(const zen

ZEND_API void zend_set_timeout(zend_long seconds, bool reset_signals);
ZEND_API void zend_unset_timeout(void);
ZEND_NORETURN ZEND_API void ZEND_FASTCALL zend_timeout(void);
ZEND_API ZEND_COLD void ZEND_FASTCALL zend_timeout(void);
ZEND_API zend_class_entry *zend_fetch_class(zend_string *class_name, uint32_t fetch_type);
ZEND_API zend_class_entry *zend_fetch_class_with_scope(zend_string *class_name, uint32_t fetch_type, zend_class_entry *scope);
ZEND_API zend_class_entry *zend_fetch_class_by_name(zend_string *class_name, zend_string *lcname, uint32_t fetch_type);
Expand Down
23 changes: 21 additions & 2 deletions Zend/zend_execute_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -1494,7 +1494,8 @@ 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);

ZEND_NORETURN ZEND_API void ZEND_FASTCALL zend_timeout(void) /* {{{ */
/* zend_timeout() is called by the VM interrupt handler or by zend_call_function() when EG(timed_out) is set */
ZEND_API ZEND_COLD void ZEND_FASTCALL zend_timeout(void) /* {{{ */
{
#if defined(PHP_WIN32)
# ifndef ZTS
Expand All @@ -1514,7 +1515,25 @@ ZEND_NORETURN ZEND_API void ZEND_FASTCALL zend_timeout(void) /* {{{ */
zend_set_timeout_ex(0, true);
#endif

zend_error_noreturn(E_ERROR, "Maximum execution time of " ZEND_LONG_FMT " second%s exceeded", EG(timeout_seconds), EG(timeout_seconds) == 1 ? "" : "s");
/* Trigger error but do not bail */
zend_error(E_ERROR | E_DONT_BAIL, "Maximum execution time of " ZEND_LONG_FMT " second%s exceeded", EG(timeout_seconds), EG(timeout_seconds) == 1 ? "" : "s");
Comment thread
arnaud-lb marked this conversation as resolved.
Resolved

/* Do not call destructors */
zend_objects_store_mark_destructed(&EG(objects_store));

/* Exception has been handled by zend_error() */
ZEND_ASSERT(!EG(exception));

EG(exception) = zend_create_unwind_exit();

zend_execute_data *execute_data = EG(current_execute_data);
if (execute_data
&& execute_data->func
&& ZEND_USER_CODE(execute_data->func->common.type)
&& execute_data->opline->opcode != ZEND_HANDLE_EXCEPTION) {
EG(opline_before_exception) = EG(current_execute_data)->opline;
Comment thread
arnaud-lb marked this conversation as resolved.
Resolved
EG(current_execute_data)->opline = EG(exception_op);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This duplicates zend_throw_unwind_exit() plus a guard; could the guard move into that helper instead?

}
/* }}} */

Expand Down
1 change: 1 addition & 0 deletions Zend/zend_vm_def.h
Original file line number Diff line number Diff line change
Expand Up @@ -10743,6 +10743,7 @@ ZEND_VM_HELPER(zend_interrupt_helper, ANY, ANY)
#endif
if (atomic_load(&EG(timed_out))) {
zend_timeout();
HANDLE_EXCEPTION();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

EG(opline_before_exception) here is the opline the interrupt fired before, not one that ran, so ZEND_HANDLE_EXCEPTION dtors its stale result. With while (true) { $s = $a . 'y'; } it frees the string $s still owns, and a shutdown function reading $s hits a UAF under valgrind. zend_handle_delayed_oom() NULLs opline_before_exception for this reason; doing the same after zend_timeout() here clears it. The JIT stub is unaffected thanks to zend_jit_exception_in_interrupt_handler_helper().

} else if (ZEND_HAS_OOM()) {
zend_handle_delayed_oom();
HANDLE_EXCEPTION();
Expand Down
2 changes: 2 additions & 0 deletions Zend/zend_vm_execute.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 14 additions & 6 deletions ext/opcache/jit/zend_jit_ir.c
Original file line number Diff line number Diff line change
Expand Up @@ -2052,23 +2052,30 @@ static int zend_jit_exception_handler_free_op2_stub(zend_jit_ctx *jit)
static int zend_jit_interrupt_handler_stub(zend_jit_ctx *jit)
{
ir_ref if_timeout, if_exception;
ir_ref end_inputs = IR_UNUSED;

// EX(opline) = opline
ir_STORE(jit_EX(opline), jit_IP(jit));

ir_STORE(jit_EG(vm_interrupt), ir_CONST_U8(0));
if_timeout = ir_IF(ir_EQ(ir_LOAD_U8(jit_EG(timed_out)), ir_CONST_U8(0)));

// if (EG(timed_out)) {
if_timeout = ir_IF(ir_LOAD_U8(jit_EG(timed_out)));
ir_IF_TRUE(if_timeout);
ir_CALL(IR_VOID, ir_CONST_FUNC(zend_timeout));
ir_CALL(IR_VOID, ir_CONST_FUNC(zend_jit_exception_in_interrupt_handler_helper));
// LOAD_OPLINE()
jit_STORE_IP(jit, ir_LOAD_A(jit_EX(opline)));
ir_END_list(end_inputs);
ir_IF_FALSE(if_timeout);
ir_CALL(IR_VOID, ir_CONST_FUNC(zend_timeout)); // noreturn
ir_MERGE_WITH_EMPTY_TRUE(if_timeout);

// if (ZEND_HAS_OOM()) {
// } else if (ZEND_HAS_OOM()) {
ir_ref if_delayed_oom = ir_IF(ir_LOAD_U8(ir_ADD_OFFSET(jit_EG(delayed_oom), offsetof(zend_oom_info, state))));
ir_IF_TRUE(if_delayed_oom);
ir_CALL(IR_VOID, ir_CONST_FC_FUNC(zend_handle_delayed_oom));
// LOAD_OPLINE()
jit_STORE_IP(jit, ir_LOAD_A(jit_EX(opline)));
ir_ref if_delayed_oom_end = ir_END();
ir_END_list(end_inputs);
ir_IF_FALSE(if_delayed_oom);

if (zend_interrupt_function) {
Expand All @@ -2082,7 +2089,8 @@ static int zend_jit_interrupt_handler_stub(zend_jit_ctx *jit)
jit_STORE_IP(jit, ir_LOAD_A(jit_EX(opline)));
}

ir_MERGE_WITH(if_delayed_oom_end);
ir_END_list(end_inputs);
ir_MERGE_list(end_inputs);

if (GCC_GLOBAL_REGS || ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL) {
zend_jit_tailcall_handler(jit, ir_LOAD_A(jit_IP(jit)));
Expand Down
31 changes: 31 additions & 0 deletions ext/zend_test/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -2793,3 +2793,34 @@ static PHP_FUNCTION(zend_test_gh19792)
zend_error(E_WARNING, "a warning");
zend_throw_error(NULL, "an exception");
}

/* Wait until max_execution_time is triggered. */
static PHP_FUNCTION(zend_test_timeout)
{
ZEND_PARSE_PARAMETERS_NONE();

/* Buzy wait, as some max_execution_time implementations use CPU time. */
while (true) {
if (atomic_load(&EG(vm_interrupt)) && EG(timed_out)) {
break;
}
}

RETURN_NULL();
}

/* Wait until max_execution_time is triggered, then throw. */
static PHP_FUNCTION(zend_test_timeout_throw)
{
ZEND_PARSE_PARAMETERS_NONE();

/* Buzy wait, as some max_execution_time implementations use CPU time. */
while (true) {
if (atomic_load(&EG(vm_interrupt)) && EG(timed_out)) {
break;
}
}

zend_throw_exception(NULL, "Timeout", 0);
RETURN_THROWS();
}
3 changes: 3 additions & 0 deletions ext/zend_test/test.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,9 @@ function zend_test_handle_post_data(mixed $stream, string $content_type, int $co
function zend_new_singleton_array(): array {}

function zend_test_set_local_var(string $name, mixed $value): bool {}

function zend_test_timeout(): void {}
function zend_test_timeout_throw(): void {}
}

namespace ZendTestNS {
Expand Down
10 changes: 9 additions & 1 deletion ext/zend_test/test_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions ext/zend_test/test_decl.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion ext/zend_test/test_legacy_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions tests/basic/timeout_variation_11.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--TEST--
Timeout within shutdown function, user function variation
--SKIPIF--
<?php
if (getenv("SKIP_SLOW_TESTS")) die("skip slow test");
?>
--INI--
max_execution_time=1
--FILE--
<?php

register_shutdown_function(function () {
while (true) {}
});
register_shutdown_function(function () {
echo "Not executed\n";
while (true) {}
});

?>
--EXPECTF--
Fatal error: Maximum execution time of 1 second exceeded in %stimeout_variation_11.php on line %d
27 changes: 27 additions & 0 deletions tests/basic/timeout_variation_12.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--TEST--
Timeout does not call destructors
--SKIPIF--
<?php
if (getenv("SKIP_SLOW_TESTS")) die("skip slow test");
?>
--INI--
max_execution_time=1
--FILE--
<?php

$obj = new class {
function __destruct() {
echo "destructor called\n";
}
};

register_shutdown_function(function() {
echo "shutdown function called\n";
});

while (true) {}

?>
--EXPECTF--
Fatal error: Maximum execution time of 1 second exceeded in %stimeout_variation_12.php on line %d
shutdown function called
38 changes: 38 additions & 0 deletions tests/basic/timeout_variation_13.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
--TEST--
Timeout does not execute finally blocks
--SKIPIF--
<?php
if (getenv("SKIP_SLOW_TESTS")) die("skip slow test");
?>
--INI--
max_execution_time=1
--FILE--
<?php

$gen1 = (function () {
try {
yield;
} finally {
echo "finally executed (1)\n";
}
})();
$gen1->current();

$gen2 = (function () {
try {
while (true) {}
} finally {
echo "finally executed (2)\n";
}
yield;
})();

try {
foreach ($gen2 as $value) {}
} finally {
echo "finally executed (3)\n";
}

?>
--EXPECTF--
Fatal error: Maximum execution time of 1 second exceeded in %stimeout_variation_13.php on line %d
16 changes: 16 additions & 0 deletions tests/basic/timeout_variation_14.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
--TEST--

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Needs --EXTENSIONS-- zend_test.

Timeout within internal function
--SKIPIF--
<?php
if (getenv("SKIP_SLOW_TESTS")) die("skip slow test");
?>
--INI--
max_execution_time=1
--FILE--
<?php

zend_test_timeout();

?>
--EXPECTF--
Fatal error: Maximum execution time of 1 second exceeded in %stimeout_variation_14.php on line 3
22 changes: 22 additions & 0 deletions tests/basic/timeout_variation_15.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--TEST--

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Needs --EXTENSIONS-- zend_test.

Timeout while EG(exception) is already set
--SKIPIF--
<?php
if (getenv("SKIP_SLOW_TESTS")) die("skip slow test");
?>
--INI--
max_execution_time=1
--FILE--
<?php

zend_test_timeout_throw();

?>
--EXPECTF--
Warning: Uncaught Exception: Timeout in %s:%d
Stack trace:
#0 %s(%d): zend_test_timeout_throw()
#1 {main}
thrown in %s on line %d

Fatal error: Maximum execution time of 1 second exceeded in %stimeout_variation_15.php on line 3
Loading