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)