Commit 840625f266f for php.net
commit 840625f266f43810f49cb5223900b5d34383b4c3
Author: Julien Voisin <jvoisin@users.noreply.github.com>
Date: Mon Sep 21 17:36:35 2026 +0200
ext/curl: fix use-after-free when setting a callback option from its own callback (#23814)
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().
diff --git a/ext/curl/interface.c b/ext/curl/interface.c
index 55bcebbe711..73088b0c7c9 100644
--- a/ext/curl/interface.c
+++ b/ext/curl/interface.c
@@ -1611,8 +1611,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);
}
@@ -1636,7 +1642,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; \
@@ -1651,7 +1657,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)