Commit e5850340f49 for php.net

commit e5850340f49eab3853f9ee693cfd45d8513eb949
Author: Marcos 'Marcão' Aurelio <macosaures@gmail.com>
Date:   Mon Apr 13 07:15:05 2026 -0300

    Fix GH-21699: callable resolution must fail if error handler threw during self/parent/static deprecations (#21712)

    When resolving string callables using self::, parent::, or static::,
    zend_is_callable_check_class() emits E_DEPRECATED. If the user error
    handler throws, EG(exception) is set but the function could still
    return true, leading to trampoline allocation and a failed assertion
    in shutdown_executor().

    Return false from zend_is_callable_check_class() when EG(exception)
    is set after handling.

diff --git a/NEWS b/NEWS
index 24846881de9..a09ef9491f0 100644
--- a/NEWS
+++ b/NEWS
@@ -8,6 +8,8 @@ PHP                                                                        NEWS
   . Fixed bug GH-21478 (Forward property operations to real instance for
     initialized lazy proxies). (iliaal)
   . Fixed bug GH-21605 (Missing addref for Countable::count()). (ilutov)
+  . Fixed bug GH-21699 (Assertion failure in shutdown_executor when resolving
+    self::/parent::/static:: callables if the error handler throws).

 - Curl:
   . Add support for brotli and zstd on Windows. (Shivam Mathur)
diff --git a/Zend/tests/gh16799.phpt b/Zend/tests/gh16799.phpt
index ce1dbd5b165..d31d1a5705c 100644
--- a/Zend/tests/gh16799.phpt
+++ b/Zend/tests/gh16799.phpt
@@ -15,7 +15,12 @@ static function ok() {
 --EXPECTF--
 Fatal error: Uncaught Exception: Use of "static" in callables is deprecated in %s:%d
 Stack trace:
-#0 %s(%d): {closure:%s:%d}(8192, 'Use of "static"...', %s, %d)
+#0 %s(%d): {closure:%s}(8192, 'Use of "static"%s', '%s', %d)
 #1 %s(%d): Test::test()
 #2 {main}
+
+Next TypeError: call_user_func(): Argument #1 ($callback) must be a valid callback, (null) in %s:%d
+Stack trace:
+#0 %s(%d): Test::test()
+#1 {main}
   thrown in %s on line %d
diff --git a/Zend/tests/gh_21699.phpt b/Zend/tests/gh_21699.phpt
new file mode 100644
index 00000000000..49b58365dab
--- /dev/null
+++ b/Zend/tests/gh_21699.phpt
@@ -0,0 +1,31 @@
+--TEST--
+GH-21699: Assertion failure in shutdown_executor when error handler throws during self:: callable resolution
+--FILE--
+<?php
+set_error_handler(function () {
+    throw new Exception;
+});
+class bar {
+    public static function __callstatic($fusion, $b)
+    {
+    }
+    public function test()
+    {
+        call_user_func('self::y');
+    }
+}
+$x = new bar;
+$x->test();
+?>
+--EXPECTF--
+Fatal error: Uncaught Exception in %s:%d
+Stack trace:
+#0 %s(%d): {closure:%s}(%d, 'Use of "self" i%s', '%s', %d)
+#1 %s(%d): bar->test()
+#2 {main}
+
+Next TypeError: call_user_func(): Argument #1 ($callback) must be a valid callback, (null) in %s:%d
+Stack trace:
+#0 %s(%d): bar->test()
+#1 {main}
+  thrown in %s on line %d
diff --git a/Zend/tests/gh_21699_parent.phpt b/Zend/tests/gh_21699_parent.phpt
new file mode 100644
index 00000000000..73cae41f3f5
--- /dev/null
+++ b/Zend/tests/gh_21699_parent.phpt
@@ -0,0 +1,32 @@
+--TEST--
+GH-21699 (parent::): no shutdown_executor trampoline assertion when error handler throws during parent:: callable resolution
+--FILE--
+<?php
+set_error_handler(function () {
+    throw new Exception;
+});
+class Base {
+    public static function __callStatic($name, $args)
+    {
+    }
+}
+class Child extends Base {
+    public function test()
+    {
+        call_user_func('parent::missing');
+    }
+}
+(new Child)->test();
+?>
+--EXPECTF--
+Fatal error: Uncaught Exception in %s:%d
+Stack trace:
+#0 %s(%d): {closure:%s}(%d, 'Use of "parent"%s', '%s', %d)
+#1 %s(%d): Child->test()
+#2 {main}
+
+Next TypeError: call_user_func(): Argument #1 ($callback) must be a valid callback, (null) in %s:%d
+Stack trace:
+#0 %s(%d): Child->test()
+#1 {main}
+  thrown in %s on line %d
diff --git a/Zend/tests/gh_21699_static.phpt b/Zend/tests/gh_21699_static.phpt
new file mode 100644
index 00000000000..4d9604ebe77
--- /dev/null
+++ b/Zend/tests/gh_21699_static.phpt
@@ -0,0 +1,31 @@
+--TEST--
+GH-21699 (static::): no shutdown_executor trampoline assertion when error handler throws during static:: callable resolution
+--FILE--
+<?php
+set_error_handler(function () {
+    throw new Exception;
+});
+class bar {
+    public static function __callstatic($fusion, $b)
+    {
+    }
+    public function test()
+    {
+        call_user_func('static::y');
+    }
+}
+$x = new bar;
+$x->test();
+?>
+--EXPECTF--
+Fatal error: Uncaught Exception in %s:%d
+Stack trace:
+#0 %s(%d): {closure:%s}(%d, 'Use of "static"%s', '%s', %d)
+#1 %s(%d): bar->test()
+#2 {main}
+
+Next TypeError: call_user_func(): Argument #1 ($callback) must be a valid callback, (null) in %s:%d
+Stack trace:
+#0 %s(%d): bar->test()
+#1 {main}
+  thrown in %s on line %d
diff --git a/Zend/zend_API.c b/Zend/zend_API.c
index e529c48b5ac..3c9891a00e9 100644
--- a/Zend/zend_API.c
+++ b/Zend/zend_API.c
@@ -3849,6 +3849,10 @@ static bool zend_is_callable_check_class(zend_string *name, zend_class_entry *sc
 		if (error) zend_spprintf(error, 0, "class \"%.*s\" not found", (int)name_len, ZSTR_VAL(name));
 	}
 	ZSTR_ALLOCA_FREE(lcname, use_heap);
+	/* User error handlers may throw from deprecations above; do not report callable as valid. */
+	if (UNEXPECTED(EG(exception))) {
+		return false;
+	}
 	return ret;
 }
 /* }}} */