Skip to content

[Observer] Fatal error support #6114

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 5 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
15 changes: 15 additions & 0 deletions Zend/zend.c
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,21 @@ static ZEND_COLD void zend_error_impl(

zend_observer_error_notify(type, error_filename, error_lineno, message);

/* Call all the observer end handlers for fatal errors */
if (ZEND_OBSERVER_ENABLED && (type & E_FATAL_ERRORS) && EG(current_execute_data)) {
zend_execute_data *ex = EG(current_execute_data);
do {
if (ex->func->type != ZEND_INTERNAL_FUNCTION) {
zend_observer_fcall_end(ex, NULL);
/* If an extension catches a fatal error and raises another one, the
* observer end handlers will fire more than once. This requires us
* to "unobserve" the functions after the end handlers have fired the
* first time. */
zend_observer_fcall_unobserve(ex->func);
}
} while ((ex = ex->prev_execute_data) != NULL);
}

/* if we don't have a user defined error handler */
if (Z_TYPE(EG(user_error_handler)) == IS_UNDEF
|| !(EG(user_error_handler_error_reporting) & type)
Expand Down
9 changes: 9 additions & 0 deletions Zend/zend_observer.c
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ ZEND_API void ZEND_FASTCALL zend_observer_fcall_end(
}
}

ZEND_API void zend_observer_fcall_unobserve(zend_function *func)
{
if (ZEND_OBSERVER_ENABLED
&& ZEND_OBSERVABLE_FN(func->common.fn_flags)
&& ZEND_OBSERVER_DATA(&func->op_array)) {
ZEND_OBSERVER_DATA(&func->op_array) = ZEND_OBSERVER_NOT_OBSERVED;
}
}

ZEND_API void zend_observer_error_register(zend_observer_error_cb cb)
{
zend_llist_add_element(&zend_observer_error_callbacks, &cb);
Expand Down
2 changes: 2 additions & 0 deletions Zend/zend_observer.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ ZEND_API void ZEND_FASTCALL zend_observer_fcall_end(
zend_execute_data *execute_data,
zval *return_value);

ZEND_API void zend_observer_fcall_unobserve(zend_function *func);

typedef void (*zend_observer_error_cb)(int type, const char *error_filename, uint32_t error_lineno, zend_string *message);

ZEND_API void zend_observer_error_register(zend_observer_error_cb callback);
Expand Down
40 changes: 40 additions & 0 deletions ext/zend_test/tests/observer_call_user_func_01.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--TEST--
Observer: call_user_func() from root namespace
--SKIPIF--
<?php if (!extension_loaded('zend-test')) die('skip: zend-test extension required'); ?>
--INI--
zend_test.observer.enabled=1
zend_test.observer.observe_all=1
--FILE--
<?php
namespace Test {
final class MyClass
{
public static function myMethod()
{
echo 'MyClass::myMethod called' . PHP_EOL;
}
}

function my_function()
{
echo 'my_function called' . PHP_EOL;
}
}
namespace {
call_user_func('Test\\MyClass::myMethod');
call_user_func('Test\\my_function');
}
?>
--EXPECTF--
<!-- init '%s/observer_call_user_func_%d.php' -->
<file '%s/observer_call_user_func_%d.php'>
<!-- init Test\MyClass::myMethod() -->
<Test\MyClass::myMethod>
MyClass::myMethod called
</Test\MyClass::myMethod>
<!-- init Test\my_function() -->
<Test\my_function>
my_function called
</Test\my_function>
</file '%s/observer_call_user_func_%d.php'>
40 changes: 40 additions & 0 deletions ext/zend_test/tests/observer_call_user_func_02.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--TEST--
Observer: call_user_func_array() from root namespace
--SKIPIF--
<?php if (!extension_loaded('zend-test')) die('skip: zend-test extension required'); ?>
--INI--
zend_test.observer.enabled=1
zend_test.observer.observe_all=1
--FILE--
<?php
namespace Test {
final class MyClass
{
public static function myMethod(string $msg)
{
echo 'MyClass::myMethod ' . $msg . PHP_EOL;
}
}

function my_function(string $msg)
{
echo 'my_function ' . $msg . PHP_EOL;
}
}
namespace {
call_user_func_array('Test\\MyClass::myMethod', ['called']);
call_user_func_array('Test\\my_function', ['called']);
}
?>
--EXPECTF--
<!-- init '%s/observer_call_user_func_%d.php' -->
<file '%s/observer_call_user_func_%d.php'>
<!-- init Test\MyClass::myMethod() -->
<Test\MyClass::myMethod>
MyClass::myMethod called
</Test\MyClass::myMethod>
<!-- init Test\my_function() -->
<Test\my_function>
my_function called
</Test\my_function>
</file '%s/observer_call_user_func_%d.php'>
39 changes: 39 additions & 0 deletions ext/zend_test/tests/observer_call_user_func_03.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
--TEST--
Observer: call_user_func() from namespace
--SKIPIF--
<?php if (!extension_loaded('zend-test')) die('skip: zend-test extension required'); ?>
--INI--
zend_test.observer.enabled=1
zend_test.observer.observe_all=1
--FILE--
<?php
namespace Test {
final class MyClass
{
public static function myMethod()
{
echo 'MyClass::myMethod called' . PHP_EOL;
}
}

function my_function()
{
echo 'my_function called' . PHP_EOL;
}

call_user_func('Test\\MyClass::myMethod');
call_user_func('Test\\my_function');
}
?>
--EXPECTF--
<!-- init '%s/observer_call_user_func_%d.php' -->
<file '%s/observer_call_user_func_%d.php'>
<!-- init Test\MyClass::myMethod() -->
<Test\MyClass::myMethod>
MyClass::myMethod called
</Test\MyClass::myMethod>
<!-- init Test\my_function() -->
<Test\my_function>
my_function called
</Test\my_function>
</file '%s/observer_call_user_func_%d.php'>
39 changes: 39 additions & 0 deletions ext/zend_test/tests/observer_call_user_func_04.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
--TEST--
Observer: call_user_func_array() from namespace
--SKIPIF--
<?php if (!extension_loaded('zend-test')) die('skip: zend-test extension required'); ?>
--INI--
zend_test.observer.enabled=1
zend_test.observer.observe_all=1
--FILE--
<?php
namespace Test {
final class MyClass
{
public static function myMethod(string $msg)
{
echo 'MyClass::myMethod ' . $msg . PHP_EOL;
}
}

function my_function(string $msg)
{
echo 'my_function ' . $msg . PHP_EOL;
}

call_user_func_array('Test\\MyClass::myMethod', ['called']);
call_user_func_array('Test\\my_function', ['called']);
}
?>
--EXPECTF--
<!-- init '%s/observer_call_user_func_%d.php' -->
<file '%s/observer_call_user_func_%d.php'>
<!-- init Test\MyClass::myMethod() -->
<Test\MyClass::myMethod>
MyClass::myMethod called
</Test\MyClass::myMethod>
<!-- init Test\my_function() -->
<Test\my_function>
my_function called
</Test\my_function>
</file '%s/observer_call_user_func_%d.php'>
29 changes: 29 additions & 0 deletions ext/zend_test/tests/observer_error_01.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
--TEST--
Observer: End handlers fire after a fatal error
--SKIPIF--
<?php if (!extension_loaded('zend-test')) die('skip: zend-test extension required'); ?>
--INI--
zend_test.observer.enabled=1
zend_test.observer.observe_all=1
zend_test.observer.show_return_value=1
memory_limit=1M
--FILE--
<?php
function foo()
{
str_repeat('.', 1024 * 1024 * 2); // 2MB
}

foo();

echo 'You should not see this.';
?>
--EXPECTF--
<!-- init '%s/observer_error_%d.php' -->
<file '%s/observer_error_%d.php'>
<!-- init foo() -->
<foo>
</foo:NULL>
</file '%s/observer_error_%d.php'>

Fatal error: Allowed memory size of 2097152 bytes exhausted%s(tried to allocate %d bytes) in %s on line %d
28 changes: 28 additions & 0 deletions ext/zend_test/tests/observer_error_02.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
Observer: End handlers fire after a userland fatal error
--SKIPIF--
<?php if (!extension_loaded('zend-test')) die('skip: zend-test extension required'); ?>
--INI--
zend_test.observer.enabled=1
zend_test.observer.observe_all=1
zend_test.observer.show_return_value=1
--FILE--
<?php
function foo()
{
trigger_error('Foo error', E_USER_ERROR);
}

foo();

echo 'You should not see this.';
?>
--EXPECTF--
<!-- init '%s/observer_error_%d.php' -->
<file '%s/observer_error_%d.php'>
<!-- init foo() -->
<foo>
</foo:NULL>
</file '%s/observer_error_%d.php'>

Fatal error: Foo error in %s on line %d
39 changes: 39 additions & 0 deletions ext/zend_test/tests/observer_error_03.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
--TEST--
Observer: non-fatal errors do not fire end handlers prematurely
--SKIPIF--
<?php if (!extension_loaded('zend-test')) die('skip: zend-test extension required'); ?>
--INI--
zend_test.observer.enabled=1
zend_test.observer.observe_all=1
zend_test.observer.show_return_value=1
--FILE--
<?php
function foo()
{
return $this_does_not_exit; // E_WARNING
}

function main()
{
foo();
echo 'After error.' . PHP_EOL;
}

main();

echo 'Done.' . PHP_EOL;
?>
--EXPECTF--
<!-- init '%s/observer_error_%d.php' -->
<file '%s/observer_error_%d.php'>
<!-- init main() -->
<main>
<!-- init foo() -->
<foo>

Warning: Undefined variable $this_does_not_exit in %s on line %d
</foo:NULL>
After error.
</main:NULL>
Done.
</file '%s/observer_error_%d.php'>
44 changes: 44 additions & 0 deletions ext/zend_test/tests/observer_error_04.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
--TEST--
Observer: fatal errors caught with zend_try will fire end handlers once
--SKIPIF--
<?php if (!extension_loaded('zend-test')) die('skip: zend-test extension required'); ?>
<?php if (!extension_loaded('soap')) die('skip: soap extension required'); ?>
--INI--
zend_test.observer.enabled=1
zend_test.observer.observe_all=1
zend_test.observer.show_return_value=1
--FILE--
<?php
function foo()
{
// ext/soap catches a fatal error and then throws an exception
$client = new SoapClient('foo');
}

function main()
{
foo();
}

// try/catch is on main() to ensure ZEND_HANDLE_EXCEPTION does not fire the end handlers again
try {
main();
} catch (SoapFault $e) {
echo $e->getMessage() . PHP_EOL;
}

echo 'Done.' . PHP_EOL;
?>
--EXPECTF--
<!-- init '%s/observer_error_%d.php' -->
<file '%s/observer_error_%d.php'>
<!-- init main() -->
<main>
<!-- init foo() -->
<foo>
</foo:NULL>
</main:NULL>
</file '%s/observer_error_%d.php'>
SOAP-ERROR: Parsing WSDL: Couldn't load from 'foo' : failed to load external entity "foo"

Done.
Loading