[php-src] master: ext/curl: abort curl transfer if callback throws exception (#22745)

Sjoerd Langkemper via GitHub <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Sjoerd Langkemper (Sjord)
Committer: GitHub (web-flow)
Pusher: Girgias
Date: 2026-08-07T19:16:08+01:00

Commit: https://github.com/php/php-src/commit/28febe2a91c5ef7dd5764780729981f912726749
Raw diff: https://github.com/php/php-src/commit/28febe2a91c5ef7dd5764780729981f912726749.diff

ext/curl: abort curl transfer if callback throws exception (#22745)

Closes GH-16513

Closes GH-16790

Changed paths:
  A  ext/curl/tests/curl_headerfunction_throws_abort.phpt
  A  ext/curl/tests/curl_prereqfunction_throws_abort.phpt
  A  ext/curl/tests/curl_progressfunction_throws_abort.phpt
  A  ext/curl/tests/curl_readfunction_throws_abort.phpt
  A  ext/curl/tests/curl_writefunction_throws_abort.phpt
  A  ext/curl/tests/curl_xferinfofunction_throws_abort.phpt
  M  ext/curl/interface.c


Diff:

diff --git a/ext/curl/interface.c b/ext/curl/interface.c
index 8c6d6a0c1f20..e198b0bb7d77 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 000000000000..9a69c966f144
--- /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 000000000000..7e8ccbf94f53
--- /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 000000000000..55e0f76cb61f
--- /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 000000000000..a030f8c4f41e
--- /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 000000000000..3da2fe8107b4
--- /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 000000000000..fbc28f07ee96
--- /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)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.