Commit d82f1ff50f9 for php.net

commit d82f1ff50f90704caeda9a9aa8dddc3101a2296a
Author: jvoisin <julien.voisin@dustri.org>
Date:   Tue Sep 22 01:43:26 2026 +0800

    Backport: reject cURL callback changes from callbacks (#23814)

    Backport PR #23814 from master to PHP-8.4.
    Original commit: 840625f266f43810f49cb5223900b5d34383b4c3

    php_curl_set_callable_handler() frees the previously registered callback with
    zend_fcc_dtor() before installing the new one. The write/read/header/progress/
    etc. callbacks run with ch->in_callback set, so calling curl_setopt() with one
    of the callable options (CURLOPT_WRITEFUNCTION, CURLOPT_READFUNCTION, ...) from
    inside such a callback destroys the fcc that is still executing. For a
    non-closure callback such as [$obj, 'method'] whose object is only referenced
    by the fcc, this releases $this while its method is still on the stack,
    resulting in a use-after-free (closures are kept alive by the call frame,
    method receivers are not).

    Reject setting a callable option while inside a callback, matching the existing
    in_callback guards on curl_close() and curl_reset().

    Closes #23814

diff --git a/ext/curl/interface.c b/ext/curl/interface.c
index 9eb05a7cfbb..336a89a214c 100644
--- a/ext/curl/interface.c
+++ b/ext/curl/interface.c
@@ -1635,8 +1635,14 @@ PHP_FUNCTION(curl_copy_handle)
 }
 /* }}} */

-static bool php_curl_set_callable_handler(zend_fcall_info_cache *const handler_fcc, zval *callable, bool is_array_config, const char *option_name)
+static bool php_curl_set_callable_handler(php_curl *ch, zend_fcall_info_cache *const handler_fcc, zval *callable, bool is_array_config, const char *option_name)
 {
+	/* Replacing a callback would free the fcc that is still executing on the stack. */
+	if (ch->in_callback) {
+		zend_throw_error(NULL, "%s(): Attempt to set the %s option from a callback", get_active_function_name(), option_name);
+		return false;
+	}
+
 	if (ZEND_FCC_INITIALIZED(*handler_fcc)) {
 		zend_fcc_dtor(handler_fcc);
 	}
@@ -1660,7 +1666,7 @@ static bool php_curl_set_callable_handler(zend_fcall_info_cache *const handler_f

 #define HANDLE_CURL_OPTION_CALLABLE_PHP_CURL_USER(curl_ptr, constant_no_function, handler_type, default_method) \
 	case constant_no_function##FUNCTION: { \
-		bool result = php_curl_set_callable_handler(&curl_ptr->handlers.handler_type->fcc, zvalue, is_array_config, #constant_no_function "FUNCTION"); \
+		bool result = php_curl_set_callable_handler(curl_ptr, &curl_ptr->handlers.handler_type->fcc, zvalue, is_array_config, #constant_no_function "FUNCTION"); \
 		if (!result) { \
 			curl_ptr->handlers.handler_type->method = default_method; \
 			return FAILURE; \
@@ -1675,7 +1681,7 @@ static bool php_curl_set_callable_handler(zend_fcall_info_cache *const handler_f

 #define HANDLE_CURL_OPTION_CALLABLE(curl_ptr, constant_no_function, handler_fcc, c_callback) \
 	case constant_no_function##FUNCTION: { \
-		bool result = php_curl_set_callable_handler(&curl_ptr->handler_fcc, zvalue, is_array_config, #constant_no_function "FUNCTION"); \
+		bool result = php_curl_set_callable_handler(curl_ptr, &curl_ptr->handler_fcc, zvalue, is_array_config, #constant_no_function "FUNCTION"); \
 		if (!result) { \
 			return FAILURE; \
 		} \
diff --git a/ext/curl/tests/curl_setopt_callback_reentrancy.phpt b/ext/curl/tests/curl_setopt_callback_reentrancy.phpt
new file mode 100644
index 00000000000..662c42ac6ed
--- /dev/null
+++ b/ext/curl/tests/curl_setopt_callback_reentrancy.phpt
@@ -0,0 +1,38 @@
+--TEST--
+GH-23814 (Setting a callback option from within a curl callback is rejected)
+--EXTENSIONS--
+curl
+--SKIPIF--
+<?php
+if (!in_array('file', curl_version()['protocols'], true)) {
+    die('skip file protocol not supported');
+}
+?>
+--FILE--
+<?php
+
+$handle = curl_init('file://' . __FILE__);
+$callback = static function (CurlHandle $handle, string $data): int {
+    try {
+        curl_setopt($handle, CURLOPT_WRITEFUNCTION, static fn($handle, $data) => strlen($data));
+    } catch (Error $error) {
+        echo $error->getMessage(), "\n";
+    }
+
+    try {
+        curl_setopt_array($handle, [CURLOPT_WRITEFUNCTION => null]);
+    } catch (Error $error) {
+        echo $error->getMessage(), "\n";
+    }
+
+    return strlen($data);
+};
+curl_setopt($handle, CURLOPT_WRITEFUNCTION, $callback);
+var_dump(curl_exec($handle));
+var_dump(curl_setopt($handle, CURLOPT_WRITEFUNCTION, null));
+?>
+--EXPECT--
+curl_setopt(): Attempt to set the CURLOPT_WRITEFUNCTION option from a callback
+curl_setopt_array(): Attempt to set the CURLOPT_WRITEFUNCTION option from a callback
+bool(true)
+bool(true)