Commit 551967dd910 for php
commit 551967dd910e3d541c0d40bfbf49e3e3e099f5dc
Author: Lazizbek Ergashev <lazerg2@gmail.com>
Date: Sat Oct 24 00:57:32 2026 +0500
Fix GH-24491: Static method stream error handler replaces pending exception (#24520)
Since 2f7b01b86c, `zend_is_callable()` fills in an error string even when it fails because an exception is already pending, so `php_stream_call_error_handler()` threw a TypeError over the StreamException (or whatever the user error handler threw) for any `Class::method` handler. It now only throws when nothing is pending, the same way curl already does.
The same commit made `zend_parse_arg()` leak that error string when it bails out on a pending exception, so it's freed there now too.
Fixes GH-24491
diff --git a/Zend/tests/zpp/gh24491.phpt b/Zend/tests/zpp/gh24491.phpt
new file mode 100644
index 00000000000..ec6e7719835
--- /dev/null
+++ b/Zend/tests/zpp/gh24491.phpt
@@ -0,0 +1,24 @@
+--TEST--
+GH-24491: Callable error string leaks when an exception is thrown during zpp
+--FILE--
+<?php
+set_error_handler(function (int $errno, string $errstr) {
+ throw new Exception($errstr);
+});
+
+class A {
+ public static function foo() {}
+
+ public static function test() {
+ register_shutdown_function(['self', 'foo']);
+ }
+}
+
+try {
+ A::test();
+} catch (Exception $e) {
+ echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Use of "self" in callables is deprecated
diff --git a/Zend/zend_API.c b/Zend/zend_API.c
index d83ea43add1..28439b6e301 100644
--- a/Zend/zend_API.c
+++ b/Zend/zend_API.c
@@ -1096,6 +1096,9 @@ static zend_result zend_parse_arg(uint32_t arg_num, zval *arg, va_list *va, cons
zend_expected_type expected_type = zend_parse_arg_impl(arg, va, spec, &error, arg_num);
if (expected_type != Z_EXPECTED_LAST) {
if (EG(exception)) {
+ if (error && (expected_type == Z_EXPECTED_FUNC || expected_type == Z_EXPECTED_FUNC_OR_NULL)) {
+ efree(error);
+ }
return FAILURE;
}
diff --git a/ext/standard/tests/streams/gh24491.phpt b/ext/standard/tests/streams/gh24491.phpt
new file mode 100644
index 00000000000..86a07d56662
--- /dev/null
+++ b/ext/standard/tests/streams/gh24491.phpt
@@ -0,0 +1,31 @@
+--TEST--
+GH-24491 (Stream error_handler given as 'Class::method' turns the real exception into a TypeError)
+--FILE--
+<?php
+class H {
+ public static function handle(array $errors): void {
+ echo "handler called\n";
+ }
+}
+
+function run(array $options): void {
+ try {
+ fopen('/nonexistent/dir/file', 'r', false, stream_context_create(['stream' => $options]));
+ } catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+ var_dump($e->getPrevious());
+ }
+}
+
+run(['error_mode' => StreamErrorMode::Exception, 'error_handler' => 'H::handle']);
+
+set_error_handler(function (int $errno, string $errstr) {
+ throw new ErrorException($errstr);
+});
+run(['error_handler' => ['H', 'handle']]);
+?>
+--EXPECT--
+StreamException: Failed to open stream: No such file or directory
+NULL
+ErrorException: fopen(): Failed to open stream: No such file or directory
+NULL
diff --git a/main/streams/stream_errors.c b/main/streams/stream_errors.c
index e2dfa310e61..211cf4df025 100644
--- a/main/streams/stream_errors.c
+++ b/main/streams/stream_errors.c
@@ -370,7 +370,9 @@ static void php_stream_call_error_handler(const zval *handler, zval *errors_arra
char *is_callable_error = NULL;
if (!zend_is_callable(handler, &fcc, &is_callable_error)) {
- zend_type_error("stream error handler must be a valid callback, %s", is_callable_error);
+ if (!EG(exception)) {
+ zend_type_error("stream error handler must be a valid callback, %s", is_callable_error);
+ }
efree(is_callable_error);
return;
}