Commit 28febe2a91c for php.net
commit 28febe2a91c5ef7dd5764780729981f912726749
Author: Sjoerd Langkemper <sjoerd-github@linuxonly.nl>
Date: Fri Aug 7 20:16:08 2026 +0200
ext/curl: abort curl transfer if callback throws exception (#22745)
Closes GH-16513
Closes GH-16790
diff --git a/ext/curl/interface.c b/ext/curl/interface.c
index 8c6d6a0c1f2..e198b0bb7d7 100644
--- a/ext/curl/interface.c
+++ b/ext/curl/interface.c
@@ -583,6 +583,8 @@ static size_t curl_write(char *data, size_t size, size_t nmemb, void *ctx)
_php_curl_verify_handlers(ch, /* reporterror */ true);
/* TODO Check callback returns an int or something castable to int */
length = php_curl_get_long(&retval);
+ } else {
+ length = -1;
}
zval_ptr_dtor(&argv[0]);
@@ -632,14 +634,14 @@ static int curl_fnmatch(void *ctx, const char *pattern, const char *string)
static int curl_progress(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow)
{
php_curl *ch = (php_curl *)clientp;
- int rval = 0;
+ int rval = 1; // error
#if PHP_CURL_DEBUG
fprintf(stderr, "curl_progress() called\n");
fprintf(stderr, "clientp = %p, dltotal = %f, dlnow = %f, ultotal = %f, ulnow = %f\n", clientp, dltotal, dlnow, ultotal, ulnow);
#endif
if (!ZEND_FCC_INITIALIZED(ch->handlers.progress)) {
- return rval;
+ return 0; // ok
}
zval args[5];
@@ -659,8 +661,8 @@ static int curl_progress(void *clientp, double dltotal, double dlnow, double ult
if (!Z_ISUNDEF(retval)) {
_php_curl_verify_handlers(ch, /* reporterror */ true);
/* TODO Check callback returns an int or something castable to int */
- if (0 != php_curl_get_long(&retval)) {
- rval = 1;
+ if (0 == php_curl_get_long(&retval)) {
+ rval = 0; // ok
}
}
@@ -673,14 +675,14 @@ static int curl_progress(void *clientp, double dltotal, double dlnow, double ult
static int curl_xferinfo(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
{
php_curl *ch = (php_curl *)clientp;
- int rval = 0;
+ int rval = 1; // error
#if PHP_CURL_DEBUG
fprintf(stderr, "curl_xferinfo() called\n");
fprintf(stderr, "clientp = %p, dltotal = %ld, dlnow = %ld, ultotal = %ld, ulnow = %ld\n", clientp, dltotal, dlnow, ultotal, ulnow);
#endif
- if (!ZEND_FCC_INITIALIZED(ch->handlers.xferinfo)) {
- return rval;
+ if (UNEXPECTED(!ZEND_FCC_INITIALIZED(ch->handlers.xferinfo))) {
+ return 0; // ok
}
zval argv[5];
@@ -700,8 +702,8 @@ static int curl_xferinfo(void *clientp, curl_off_t dltotal, curl_off_t dlnow, cu
if (!Z_ISUNDEF(retval)) {
_php_curl_verify_handlers(ch, /* reporterror */ true);
/* TODO Check callback returns an int or something castable to int */
- if (0 != php_curl_get_long(&retval)) {
- rval = 1;
+ if (0 == php_curl_get_long(&retval)) {
+ rval = 0; // ok
}
}
@@ -714,13 +716,13 @@ static int curl_xferinfo(void *clientp, curl_off_t dltotal, curl_off_t dlnow, cu
static int curl_prereqfunction(void *clientp, char *conn_primary_ip, char *conn_local_ip, int conn_primary_port, int conn_local_port)
{
php_curl *ch = (php_curl *)clientp;
- int rval = CURL_PREREQFUNC_OK;
+ int rval = CURL_PREREQFUNC_ABORT;
// when CURLOPT_PREREQFUNCTION is set to null, curl_prereqfunction still
// gets called. Return CURL_PREREQFUNC_OK immediately in this case to avoid
// zend_call_known_fcc() with an uninitialized FCC.
- if (!ZEND_FCC_INITIALIZED(ch->handlers.prereq)) {
- return rval;
+ if (UNEXPECTED(!ZEND_FCC_INITIALIZED(ch->handlers.prereq))) {
+ return CURL_PREREQFUNC_OK;
}
#if PHP_CURL_DEBUG
@@ -858,6 +860,8 @@ static size_t curl_read(char *data, size_t size, size_t nmemb, void *ctx)
}
// TODO Do type error if invalid type?
zval_ptr_dtor(&retval);
+ } else {
+ length = CURL_READFUNC_ABORT;
}
zval_ptr_dtor(&argv[0]);
@@ -952,6 +956,8 @@ static size_t curl_write_header(char *data, size_t size, size_t nmemb, void *ctx
// TODO: Check for valid int type for return value
_php_curl_verify_handlers(ch, /* reporterror */ true);
length = php_curl_get_long(&retval);
+ } else {
+ length = -1;
}
zval_ptr_dtor(&argv[0]);
zval_ptr_dtor(&argv[1]);
diff --git a/ext/curl/tests/curl_headerfunction_throws_abort.phpt b/ext/curl/tests/curl_headerfunction_throws_abort.phpt
new file mode 100644
index 00000000000..9a69c966f14
--- /dev/null
+++ b/ext/curl/tests/curl_headerfunction_throws_abort.phpt
@@ -0,0 +1,45 @@
+--TEST--
+CURLOPT_HEADERFUNCTION aborts transfer when callback throws
+--EXTENSIONS--
+curl
+--SKIPIF--
+<?php
+if (!defined('CURLOPT_HEADERFUNCTION')) {
+ die('skip CURLOPT_HEADERFUNCTION not available');
+}
+?>
+--FILE--
+<?php
+
+include 'server.inc';
+$host = curl_cli_server_start();
+$ch = curl_init("{$host}/get.inc");
+
+echo "Test: header function throws exception\n";
+curl_setopt($ch, CURLOPT_HEADERFUNCTION,
+ function (): int {
+ throw new Exception('header exception');
+ }
+);
+
+try {
+ curl_exec($ch);
+} catch (Exception $e) {
+ echo $e->getMessage(), "\n";
+}
+
+var_dump(curl_errno($ch) === CURLE_WRITE_ERROR);
+
+echo "Test: header function is null\n";
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_setopt($ch, CURLOPT_HEADERFUNCTION, null);
+curl_exec($ch);
+var_dump(curl_errno($ch) === CURLE_OK);
+
+?>
+--EXPECTF--
+Test: header function throws exception
+header exception
+bool(true)
+Test: header function is null
+bool(true)
diff --git a/ext/curl/tests/curl_prereqfunction_throws_abort.phpt b/ext/curl/tests/curl_prereqfunction_throws_abort.phpt
new file mode 100644
index 00000000000..7e8ccbf94f5
--- /dev/null
+++ b/ext/curl/tests/curl_prereqfunction_throws_abort.phpt
@@ -0,0 +1,35 @@
+--TEST--
+CURLOPT_PREREQFUNCTION aborts transfer when callback throws
+--EXTENSIONS--
+curl
+--SKIPIF--
+<?php
+if (!defined('CURLOPT_PREREQFUNCTION')) {
+ die('skip CURLOPT_PREREQFUNCTION not available');
+}
+?>
+--FILE--
+<?php
+
+include 'server.inc';
+$host = curl_cli_server_start();
+$ch = curl_init("{$host}/get.inc");
+
+curl_setopt($ch, CURLOPT_PREREQFUNCTION,
+ function (): int {
+ throw new Exception('prereq exception');
+ }
+);
+
+try {
+ curl_exec($ch);
+} catch (Exception $e) {
+ echo $e->getMessage(), "\n";
+}
+
+var_dump(curl_errno($ch) === CURLE_ABORTED_BY_CALLBACK);
+
+?>
+--EXPECTF--
+prereq exception
+bool(true)
diff --git a/ext/curl/tests/curl_progressfunction_throws_abort.phpt b/ext/curl/tests/curl_progressfunction_throws_abort.phpt
new file mode 100644
index 00000000000..55e0f76cb61
--- /dev/null
+++ b/ext/curl/tests/curl_progressfunction_throws_abort.phpt
@@ -0,0 +1,46 @@
+--TEST--
+CURLOPT_PROGRESSFUNCTION aborts transfer when callback throws
+--EXTENSIONS--
+curl
+--SKIPIF--
+<?php
+if (!defined('CURLOPT_PROGRESSFUNCTION')) {
+ die('skip CURLOPT_PROGRESSFUNCTION not available');
+}
+?>
+--FILE--
+<?php
+
+include 'server.inc';
+$host = curl_cli_server_start();
+$ch = curl_init("{$host}/get.inc");
+
+echo "Test: progress function throws exception\n";
+curl_setopt($ch, CURLOPT_NOPROGRESS, 0);
+curl_setopt($ch, CURLOPT_PROGRESSFUNCTION,
+ function (): int {
+ throw new Exception('info exception');
+ }
+);
+
+try {
+ curl_exec($ch);
+} catch (Exception $e) {
+ echo $e->getMessage(), "\n";
+}
+
+var_dump(curl_errno($ch) === CURLE_ABORTED_BY_CALLBACK);
+
+echo "Test: progress function is null\n";
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, null);
+curl_exec($ch);
+var_dump(curl_errno($ch) === CURLE_OK);
+
+?>
+--EXPECTF--
+Test: progress function throws exception
+info exception
+bool(true)
+Test: progress function is null
+bool(true)
diff --git a/ext/curl/tests/curl_readfunction_throws_abort.phpt b/ext/curl/tests/curl_readfunction_throws_abort.phpt
new file mode 100644
index 00000000000..a030f8c4f41
--- /dev/null
+++ b/ext/curl/tests/curl_readfunction_throws_abort.phpt
@@ -0,0 +1,48 @@
+--TEST--
+CURLOPT_READFUNCTION aborts transfer when callback throws
+--EXTENSIONS--
+curl
+--SKIPIF--
+<?php
+if (!defined('CURLOPT_READFUNCTION')) {
+ die('skip CURLOPT_READFUNCTION not available');
+}
+?>
+--FILE--
+<?php
+
+include 'server.inc';
+$host = curl_cli_server_start();
+$ch = curl_init("{$host}/get.inc");
+
+$file = new CURLFile(__DIR__ . '/curl_testdata1.txt');
+curl_setopt($ch, CURLOPT_POST, 1);
+
+echo "Test: read function throws exception\n";
+curl_setopt($ch, CURLOPT_READFUNCTION,
+ function (): int {
+ throw new Exception('read exception');
+ }
+);
+
+try {
+ curl_exec($ch);
+} catch (Exception $e) {
+ echo $e->getMessage(), "\n";
+}
+
+var_dump(curl_errno($ch) === CURLE_ABORTED_BY_CALLBACK);
+
+echo "Test: read function is null\n";
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_setopt($ch, CURLOPT_READFUNCTION, null);
+curl_exec($ch);
+var_dump(curl_errno($ch) === CURLE_OK);
+
+?>
+--EXPECTF--
+Test: read function throws exception
+read exception
+bool(true)
+Test: read function is null
+bool(true)
diff --git a/ext/curl/tests/curl_writefunction_throws_abort.phpt b/ext/curl/tests/curl_writefunction_throws_abort.phpt
new file mode 100644
index 00000000000..3da2fe8107b
--- /dev/null
+++ b/ext/curl/tests/curl_writefunction_throws_abort.phpt
@@ -0,0 +1,45 @@
+--TEST--
+CURLOPT_WRITEFUNCTION aborts transfer when callback throws
+--EXTENSIONS--
+curl
+--SKIPIF--
+<?php
+if (!defined('CURLOPT_WRITEFUNCTION')) {
+ die('skip CURLOPT_WRITEFUNCTION not available');
+}
+?>
+--FILE--
+<?php
+
+include 'server.inc';
+$host = curl_cli_server_start();
+$ch = curl_init("{$host}/get.inc");
+
+echo "Test: write function throws exception\n";
+curl_setopt($ch, CURLOPT_WRITEFUNCTION,
+ function (): int {
+ throw new Exception('write exception');
+ }
+);
+
+try {
+ curl_exec($ch);
+} catch (Exception $e) {
+ echo $e->getMessage(), "\n";
+}
+
+var_dump(curl_errno($ch) === CURLE_WRITE_ERROR);
+
+echo "Test: write function is null\n";
+curl_setopt($ch, CURLOPT_WRITEFUNCTION, null);
+curl_exec($ch);
+var_dump(curl_errno($ch) === CURLE_OK);
+
+?>
+--EXPECTF--
+Test: write function throws exception
+write exception
+bool(true)
+Test: write function is null
+Hello World!
+Hello World!bool(true)
diff --git a/ext/curl/tests/curl_xferinfofunction_throws_abort.phpt b/ext/curl/tests/curl_xferinfofunction_throws_abort.phpt
new file mode 100644
index 00000000000..fbc28f07ee9
--- /dev/null
+++ b/ext/curl/tests/curl_xferinfofunction_throws_abort.phpt
@@ -0,0 +1,46 @@
+--TEST--
+CURLOPT_XFERINFOFUNCTION aborts transfer when callback throws
+--EXTENSIONS--
+curl
+--SKIPIF--
+<?php
+if (!defined('CURLOPT_XFERINFOFUNCTION')) {
+ die('skip CURLOPT_XFERINFOFUNCTION not available');
+}
+?>
+--FILE--
+<?php
+
+include 'server.inc';
+$host = curl_cli_server_start();
+$ch = curl_init("{$host}/get.inc");
+
+echo "Test: xfer info function throws exception\n";
+curl_setopt($ch, CURLOPT_NOPROGRESS, 0);
+curl_setopt($ch, CURLOPT_XFERINFOFUNCTION,
+ function (): int {
+ throw new Exception('info exception');
+ }
+);
+
+try {
+ curl_exec($ch);
+} catch (Exception $e) {
+ echo $e->getMessage(), "\n";
+}
+
+var_dump(curl_errno($ch) === CURLE_ABORTED_BY_CALLBACK);
+
+echo "Test: xfer info function is null\n";
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_setopt($ch, CURLOPT_XFERINFOFUNCTION, null);
+curl_exec($ch);
+var_dump(curl_errno($ch) === CURLE_OK);
+
+?>
+--EXPECTF--
+Test: xfer info function throws exception
+info exception
+bool(true)
+Test: xfer info function is null
+bool(true)