Commit 59f8e1d2653 for php
commit 59f8e1d2653c39f8f6e3ef9ada7dac8264c82512
Author: lacatoire <la.catoire@gmail.com>
Date: Mon Oct 19 14:03:27 2026 +0200
Fix use-after-free of CURLOPT_STDERR stream on cloned curl handle
_php_setup_easy_copy_handlers() copies the write, read and write_header
streams but not std_err. The clone never checks that stream is still
open, so libcurl keeps writing to a FILE* freed when the stream is
closed.
Copy std_err like the other handlers.
Closes GH-24427.
diff --git a/NEWS b/NEWS
index 91b1663e457..60b978e801d 100644
--- a/NEWS
+++ b/NEWS
@@ -36,6 +36,10 @@ PHP NEWS
- CLI:
. Fixed bug GH-24697 (PHP_HTTP_PARSER_STRICT build option doesn't work).
(ndossche, devnexen)
+
+- Curl:
+ . Fix use-after-free of CURLOPT_STDERR stream on cloned curl handle.
+ (lacatoire)
- Date:
. Fixed bug GH-20503 (Assertion failure with ext/date DateInterval property
diff --git a/ext/curl/interface.c b/ext/curl/interface.c
index 8aec63ca501..b369de7fc36 100644
--- a/ext/curl/interface.c
+++ b/ext/curl/interface.c
@@ -1305,6 +1305,10 @@ void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source)
Z_ADDREF(source->handlers.write_header->stream);
}
ch->handlers.write_header->stream = source->handlers.write_header->stream;
+ if (!Z_ISUNDEF(source->handlers.std_err)) {
+ Z_ADDREF(source->handlers.std_err);
+ }
+ ch->handlers.std_err = source->handlers.std_err;
ch->handlers.write->fp = source->handlers.write->fp;
ch->handlers.write_header->fp = source->handlers.write_header->fp;
diff --git a/ext/curl/tests/curl_copy_handle_stderr_uaf.phpt b/ext/curl/tests/curl_copy_handle_stderr_uaf.phpt
new file mode 100644
index 00000000000..9a46a27dfa0
--- /dev/null
+++ b/ext/curl/tests/curl_copy_handle_stderr_uaf.phpt
@@ -0,0 +1,38 @@
+--TEST--
+Cloning a curl handle keeps CURLOPT_STDERR tracked so a closed stream is detected on the clone too
+--EXTENSIONS--
+curl
+--FILE--
+<?php
+$srcfile = __DIR__ . '/curl_copy_handle_stderr_uaf_src.txt';
+file_put_contents($srcfile, "Test.");
+
+$stderrfile = __DIR__ . '/curl_copy_handle_stderr_uaf_stderr.txt';
+$stream = fopen($stderrfile, 'w+');
+
+$ch1 = curl_init();
+curl_setopt($ch1, CURLOPT_URL, "file://" . $srcfile);
+curl_setopt($ch1, CURLOPT_RETURNTRANSFER, 1);
+curl_setopt($ch1, CURLOPT_VERBOSE, true);
+curl_setopt($ch1, CURLOPT_STDERR, $stream);
+
+$ch2 = clone $ch1;
+
+fclose($stream);
+
+var_dump(curl_exec($ch1));
+var_dump(curl_exec($ch2));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/curl_copy_handle_stderr_uaf_src.txt');
+@unlink(__DIR__ . '/curl_copy_handle_stderr_uaf_stderr.txt');
+?>
+--EXPECTF--
+Warning: curl_exec(): CURLOPT_STDERR resource has gone away, resetting to stderr in %s on line %d
+%A
+string(5) "Test."
+
+Warning: curl_exec(): CURLOPT_STDERR resource has gone away, resetting to stderr in %s on line %d
+%A
+string(5) "Test."