Commit ac37a9760b8 for php.net
commit ac37a9760b8fffa9a3e86d71737903efdf480e59
Author: Sjoerd Langkemper <sjoerd-github@linuxonly.nl>
Date: Wed Aug 5 21:59:28 2026 +0100
ext/curl: error when curl read func returns unexpected long
Raise a value error when the callback registered with
CURLOPT_READFUNCTION returns an unexpected long.
The function registered with CURLOPT_READFUNCTION should return a
string. PHP then writes that string to a buffer and returns the
length, so that curl can read that many bytes from the buffer.
The function can also return CURL_READFUNC_ABORT and
CURL_READFUNC_PAUSE, so it also supports returning longs. However, when
it returns a long other than these two constants, it is interpreted as a
length. PHP does not update the buffer, but does instruct curl it can
read that many bytes from the buffer. It reads whatever uninitialized
data that is in the buffer and sends it over the line to the server.
This seems bad, so validate the return value of the read function and
raise an error.
Returning 0 is a bit of an edge case. It is not documented but does
results in correct behavior (i.e. end-of-file). So we accept that, but
don't advertise it as valid in the error message.
Related to https://github.com/php/php-src/issues/10270
Close GH-22757
diff --git a/NEWS b/NEWS
index 0f3ba67434d..96895dc4a4b 100644
--- a/NEWS
+++ b/NEWS
@@ -6,6 +6,10 @@ PHP NEWS
. Changed run-tests.php to run test subprocesses without a shell where
possible. (NickSdot)
+- Curl:
+ . Raise a value error when the callback registered with CURLOPT_READFUNCTION
+ returns an unexpected long. (Sjoerd Langkemper)
+
- Date:
. Update timelib to 2026.01. (Derick, timwolla)
diff --git a/UPGRADING b/UPGRADING
index ced36548070..453bae75701 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -24,6 +24,11 @@ PHP 8.6 UPGRADE NOTES
has materialised the property by writing into the property table.
The freshly-written value is returned directly. isset() is unaffected.
+- Curl:
+ . The callback registered with CURLOPT_READFUNCTION now throws a ValueError
+ when returning an integer other than 0, CURL_READFUNC_ABORT or
+ CURL_READFUNC_PAUSE.
+
- COM
. It is no longer possible to clone variant objects, this is because
the cloning behaviour was ill defined.
@@ -602,6 +607,7 @@ PHP 8.6 UPGRADE NOTES
. CURL_SEEKFUNC_OK.
. CURL_SEEKFUNC_FAIL.
. CURL_SEEKFUNC_CANTSEEK.
+ . CURL_READFUNC_ABORT.
- OpenSSL:
. OPENSSL_RSA_PSS_SALTLEN_DIGEST.
diff --git a/ext/curl/curl.stub.php b/ext/curl/curl.stub.php
index 70e87cc9b14..6953f0e97cb 100644
--- a/ext/curl/curl.stub.php
+++ b/ext/curl/curl.stub.php
@@ -1788,6 +1788,11 @@
* @cvalue CURLPAUSE_SEND_CONT
*/
const CURLPAUSE_SEND_CONT = UNKNOWN;
+/**
+ * @var int
+ * @cvalue CURL_READFUNC_ABORT
+ */
+const CURL_READFUNC_ABORT = UNKNOWN;
/**
* @var int
* @cvalue CURL_READFUNC_PAUSE
diff --git a/ext/curl/curl_arginfo.h b/ext/curl/curl_arginfo.h
index f2929f60c4e..ea354d16df5 100644
Binary files a/ext/curl/curl_arginfo.h and b/ext/curl/curl_arginfo.h differ
diff --git a/ext/curl/interface.c b/ext/curl/interface.c
index 07e53dfe0f9..6df7cf66fbe 100644
--- a/ext/curl/interface.c
+++ b/ext/curl/interface.c
@@ -819,7 +819,13 @@ static size_t curl_read(char *data, size_t size, size_t nmemb, void *ctx)
length = MIN(nmemb, Z_STRLEN(retval));
memcpy(data, Z_STRVAL(retval), length);
} else if (Z_TYPE(retval) == IS_LONG) {
- length = Z_LVAL_P(&retval);
+ zend_long long_rv = Z_LVAL_P(&retval);
+ if (long_rv == 0 || long_rv == CURL_READFUNC_ABORT || long_rv == CURL_READFUNC_PAUSE) {
+ length = (size_t) long_rv;
+ } else {
+ zend_value_error("The CURLOPT_READFUNCTION callback must return a string or CURL_READFUNC_ABORT or CURL_READFUNC_PAUSE");
+ length = CURL_READFUNC_ABORT;
+ }
}
// TODO Do type error if invalid type?
zval_ptr_dtor(&retval);
diff --git a/ext/curl/tests/curl_read_function_error_on_int.phpt b/ext/curl/tests/curl_read_function_error_on_int.phpt
new file mode 100644
index 00000000000..30ba9773772
--- /dev/null
+++ b/ext/curl/tests/curl_read_function_error_on_int.phpt
@@ -0,0 +1,30 @@
+--TEST--
+error when CURLOPT_READFUNCTION returns an integer
+--EXTENSIONS--
+curl
+--FILE--
+<?php
+function custom_readfunction($oCurl, $hReadHandle, $iMaxOut)
+{
+ static $size = 2;
+ return $size--;
+}
+
+include 'server.inc';
+$host = curl_cli_server_start();
+$ch = curl_init();
+curl_setopt($ch, CURLOPT_URL, "{$host}/get.php?test=post");
+curl_setopt($ch, CURLOPT_POST, ['f' => 'f']);
+curl_setopt($ch, CURLOPT_TIMEOUT, 2);
+curl_setopt($ch, CURLOPT_READFUNCTION, "custom_readfunction" );
+
+try {
+ curl_exec($ch);
+} catch (ValueError $e) {
+ echo $e->getMessage() . "\n";
+}
+var_dump(curl_error($ch));
+?>
+--EXPECT--
+The CURLOPT_READFUNCTION callback must return a string or CURL_READFUNC_ABORT or CURL_READFUNC_PAUSE
+string(29) "operation aborted by callback"
diff --git a/ext/curl/tests/curl_readfunc_abort.phpt b/ext/curl/tests/curl_readfunc_abort.phpt
new file mode 100644
index 00000000000..39103904fdc
--- /dev/null
+++ b/ext/curl/tests/curl_readfunc_abort.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Returning CURL_READFUNC_ABORT aborts the transfer
+--EXTENSIONS--
+curl
+--FILE--
+<?php
+include 'server.inc';
+$host = curl_cli_server_start();
+
+$ch = curl_init();
+curl_setopt($ch, CURLOPT_URL, "{$host}/get.inc");
+curl_setopt($ch, CURLOPT_POST, 1);
+curl_setopt($ch, CURLOPT_READFUNCTION, function () {
+ return CURL_READFUNC_ABORT;
+});
+curl_exec($ch);
+
+echo "No output expected, because transfer was aborted by read function.\n";
+?>
+--EXPECT--
+No output expected, because transfer was aborted by read function.