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;
 	}