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)