Commit bb0ab98b9d2 for php.net

commit bb0ab98b9d2f415caa35398099cecff8f3247ac3
Author: David Carlier <devnexen@gmail.com>
Date:   Sat Aug 8 05:21:01 2026 +0100

    Zend: is_callable() wrongly accepts objects with no get_closure handler.

    Fix #23121

    zend_is_callable_at_frame() tested `get_closure && get_closure(...) == FAILURE`,
    so a NULL handler short circuited past the error branch into the success path,
    leaving fcc->function_handler NULL. zend_fcc_addref() then asserted and
    call_user_func() dereferenced the null zend_function.

    Close GH-23123

diff --git a/NEWS b/NEWS
index 8c84bee9f71..447d052e21d 100644
--- a/NEWS
+++ b/NEWS
@@ -7,6 +7,8 @@ PHP                                                                        NEWS
     possible. (NickSdot)
   . Fixed GH-23083 (SEGV build_trace_args in zend_exceptions.c with
     -d error_include_args=On). (David Carlier)
+  . Fixed GH-23121 (is_callable() wrongly accepts objects with no get_closure
+    handler). (David Carlier)

 - Curl:
   . Improved cURL option validation errors to include the option name.
diff --git a/Zend/tests/gh23121.phpt b/Zend/tests/gh23121.phpt
new file mode 100644
index 00000000000..158794ba04e
--- /dev/null
+++ b/Zend/tests/gh23121.phpt
@@ -0,0 +1,36 @@
+--TEST--
+GH-23121 (Assertion failure in zend_fcc_addref() for an object whose class has no get_closure handler)
+--EXTENSIONS--
+simplexml
+--FILE--
+<?php
+$sxe = new SimpleXMLElement('<root/>');
+
+var_dump(is_callable($sxe));
+
+try {
+    $sxe();
+} catch (Error $e) {
+    echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+    call_user_func($sxe);
+} catch (TypeError $e) {
+    echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+    libxml_set_external_entity_loader($sxe);
+} catch (TypeError $e) {
+    echo $e->getMessage(), PHP_EOL;
+}
+
+var_dump(libxml_get_external_entity_loader());
+?>
+--EXPECT--
+bool(false)
+Object of type SimpleXMLElement is not callable
+call_user_func(): Argument #1 ($callback) must be a valid callback, no array or string given
+libxml_set_external_entity_loader(): Argument #1 ($resolver_function) must be a valid callback or null, no array or string given
+NULL
diff --git a/Zend/zend_API.c b/Zend/zend_API.c
index de3570c5e84..7d407f7e905 100644
--- a/Zend/zend_API.c
+++ b/Zend/zend_API.c
@@ -4240,7 +4240,7 @@ ZEND_API bool zend_is_callable_at_frame(
 			}

 		case IS_OBJECT:
-			if (Z_OBJ_HANDLER_P(callable, get_closure) && Z_OBJ_HANDLER_P(callable, get_closure)(Z_OBJ_P(callable), &fcc->calling_scope, &fcc->function_handler, &fcc->object, 1) == FAILURE) {
+			if (!Z_OBJ_HANDLER_P(callable, get_closure) || Z_OBJ_HANDLER_P(callable, get_closure)(Z_OBJ_P(callable), &fcc->calling_scope, &fcc->function_handler, &fcc->object, 1) == FAILURE) {
 				if (error) *error = estrdup("no array or string given");
 				return 0;
 			}