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)