From 34770087147185e8083514a9d4fd74f20ca74da5 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Sun, 12 Jul 2026 10:44:02 +0000 Subject: [PATCH 1/8] Show curl option name in error message Use curl_easy_option_by_id to retrieve the name of the option, and also show the name of the option in the case where strings contain a null byte. curl_easy_option_by_id simplifies the code, but was introduced in curl 7.73.0, so this also bumps the minimum version of curl. 7.73.0 was released in 2020, so I think that's acceptable. Showing the option name is especially useful when using curl_setopt_array. A user may specify many options, and this change makes it clear which option is wrong exactly. --- ext/curl/interface.c | 43 +++++------------------------------- ext/curl/sync-constants.php | 2 +- ext/curl/tests/bug68089.phpt | 2 +- 3 files changed, 8 insertions(+), 39 deletions(-) diff --git a/ext/curl/interface.c b/ext/curl/interface.c index 07e53dfe0f9f..a17ea7233ee3 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -65,7 +65,8 @@ ZEND_DECLARE_MODULE_GLOBALS(curl) static zend_result php_curl_option_str(php_curl *ch, zend_long option, const char *str, const size_t len) { if (zend_char_has_nul_byte(str, len)) { - zend_value_error("%s(): cURL option must not contain any null bytes", get_active_function_name()); + const struct curl_easyoption *option_info = curl_easy_option_by_id(option); + zend_value_error("%s(): cURL option CURLOPT_%s must not contain any null bytes", get_active_function_name(), option_info->name); return FAILURE; } @@ -2085,43 +2086,10 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue HashTable *ph; zend_string *val, *tmp_val; struct curl_slist *slist = NULL; - const char *name = NULL; - - switch (option) { - case CURLOPT_HTTPHEADER: - name = "CURLOPT_HTTPHEADER"; - break; - case CURLOPT_QUOTE: - name = "CURLOPT_QUOTE"; - break; - case CURLOPT_HTTP200ALIASES: - name = "CURLOPT_HTTP200ALIASES"; - break; - case CURLOPT_POSTQUOTE: - name = "CURLOPT_POSTQUOTE"; - break; - case CURLOPT_PREQUOTE: - name = "CURLOPT_PREQUOTE"; - break; - case CURLOPT_TELNETOPTIONS: - name = "CURLOPT_TELNETOPTIONS"; - break; - case CURLOPT_MAIL_RCPT: - name = "CURLOPT_MAIL_RCPT"; - break; - case CURLOPT_RESOLVE: - name = "CURLOPT_RESOLVE"; - break; - case CURLOPT_PROXYHEADER: - name = "CURLOPT_PROXYHEADER"; - break; - case CURLOPT_CONNECT_TO: - name = "CURLOPT_CONNECT_TO"; - break; - } if (Z_TYPE_P(zvalue) != IS_ARRAY) { - zend_type_error("%s(): The %s option must have an array value", get_active_function_name(), name); + const struct curl_easyoption *option_info = curl_easy_option_by_id(option); + zend_type_error("%s(): The CURLOPT_%s option must have an array value", get_active_function_name(), option_info->name); return FAILURE; } @@ -2133,7 +2101,8 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue if (zend_str_has_nul_byte(val)) { curl_slist_free_all(slist); zend_tmp_string_release(tmp_val); - zend_value_error("%s(): cURL option %s must not contain any null bytes", get_active_function_name(), name); + const struct curl_easyoption *option_info = curl_easy_option_by_id(option); + zend_value_error("%s(): cURL option CURLOPT_%s must not contain any null bytes", get_active_function_name(), option_info->name); return FAILURE; } diff --git a/ext/curl/sync-constants.php b/ext/curl/sync-constants.php index e24c773a5209..8165953c6d36 100755 --- a/ext/curl/sync-constants.php +++ b/ext/curl/sync-constants.php @@ -12,7 +12,7 @@ const SOURCE_FILE = __DIR__ . '/curl_arginfo.h'; -const MIN_SUPPORTED_CURL_VERSION = '7.61.0'; +const MIN_SUPPORTED_CURL_VERSION = '7.73.0'; const IGNORED_CURL_CONSTANTS = [ 'CURLOPT_PROGRESSDATA', diff --git a/ext/curl/tests/bug68089.phpt b/ext/curl/tests/bug68089.phpt index b8733c5066b8..c175df5248fe 100644 --- a/ext/curl/tests/bug68089.phpt +++ b/ext/curl/tests/bug68089.phpt @@ -16,5 +16,5 @@ try { ?> Done --EXPECT-- -curl_setopt(): cURL option must not contain any null bytes +curl_setopt(): cURL option CURLOPT_URL must not contain any null bytes Done From 1f3eb0f91a164e9889c2b8ffdca9544e5dee2a70 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Tue, 28 Jul 2026 12:01:48 +0000 Subject: [PATCH 2/8] Add function to determine name --- ext/curl/interface.c | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/ext/curl/interface.c b/ext/curl/interface.c index a17ea7233ee3..4abeb22ea622 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -62,11 +62,38 @@ ZEND_DECLARE_MODULE_GLOBALS(curl) # define php_curl_ret(__ret) RETVAL_FALSE; return; #endif +// php_curl_option_get_name(CURLOPT_HTTPHEADER) -> "HTTPHEADER" +static const char * php_curl_option_get_name(zend_long option) { +#if LIBCURL_VERSION_NUM >= 0x074900 + const struct curl_easyoption * opt = curl_easy_option_by_id(option); + if (EXPECTED(opt != NULL)) { + return opt->name; + } +#else + const char * prefix = "CURLOPT_"; + const size_t prefix_len = sizeof(prefix) - 1; + zend_string *key; + zend_constant *constant; + + ZEND_HASH_FOREACH_STR_KEY_PTR(EG(zend_constants), key, constant) { + if (!key + || Z_TYPE(constant->value) != IS_LONG + || strncmp(ZSTR_VAL(key), prefix, prefix_len) != 0) { + continue; + } + + if (Z_LVAL(constant->value) == option) { + return ZSTR_VAL(key) + prefix_len; + } + } ZEND_HASH_FOREACH_END(); +#endif + return "UNKNOWN_OPTION"; +} + static zend_result php_curl_option_str(php_curl *ch, zend_long option, const char *str, const size_t len) { if (zend_char_has_nul_byte(str, len)) { - const struct curl_easyoption *option_info = curl_easy_option_by_id(option); - zend_value_error("%s(): cURL option CURLOPT_%s must not contain any null bytes", get_active_function_name(), option_info->name); + zend_value_error("%s(): cURL option CURLOPT_%s must not contain any null bytes", get_active_function_name(), php_curl_option_get_name(option)); return FAILURE; } @@ -2088,8 +2115,7 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue struct curl_slist *slist = NULL; if (Z_TYPE_P(zvalue) != IS_ARRAY) { - const struct curl_easyoption *option_info = curl_easy_option_by_id(option); - zend_type_error("%s(): The CURLOPT_%s option must have an array value", get_active_function_name(), option_info->name); + zend_type_error("%s(): The CURLOPT_%s option must have an array value", get_active_function_name(), php_curl_option_get_name(option)); return FAILURE; } @@ -2101,8 +2127,7 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue if (zend_str_has_nul_byte(val)) { curl_slist_free_all(slist); zend_tmp_string_release(tmp_val); - const struct curl_easyoption *option_info = curl_easy_option_by_id(option); - zend_value_error("%s(): cURL option CURLOPT_%s must not contain any null bytes", get_active_function_name(), option_info->name); + zend_value_error("%s(): cURL option CURLOPT_%s must not contain any null bytes", get_active_function_name(), php_curl_option_get_name(option)); return FAILURE; } From 792a6cc70afa35bb7d9116bd563028185ce71192 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Tue, 28 Jul 2026 12:10:50 +0000 Subject: [PATCH 3/8] Use option name in more error messages --- ext/curl/interface.c | 6 +++--- ext/curl/tests/bug48207.phpt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/curl/interface.c b/ext/curl/interface.c index 4abeb22ea622..98969592bece 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -2039,7 +2039,7 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue ch->handlers.write->method = PHP_CURL_FILE; ZVAL_COPY(&ch->handlers.write->stream, zvalue); } else { - zend_value_error("%s(): The provided file handle must be writable", get_active_function_name()); + zend_value_error("%s(): The file handle provided for CURLOPT_FILE must be writable", get_active_function_name()); return FAILURE; } break; @@ -2057,7 +2057,7 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue ch->handlers.write_header->method = PHP_CURL_FILE; ZVAL_COPY(&ch->handlers.write_header->stream, zvalue); } else { - zend_value_error("%s(): The provided file handle must be writable", get_active_function_name()); + zend_value_error("%s(): The file handle provided for CURLOPT_WRITEHEADER must be writable", get_active_function_name()); return FAILURE; } break; @@ -2086,7 +2086,7 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue zval_ptr_dtor(&ch->handlers.std_err); ZVAL_COPY(&ch->handlers.std_err, zvalue); } else { - zend_value_error("%s(): The provided file handle must be writable", get_active_function_name()); + zend_value_error("%s(): The file handle provided for CURLOPT_STDERR must be writable", get_active_function_name()); return FAILURE; } ZEND_FALLTHROUGH; diff --git a/ext/curl/tests/bug48207.phpt b/ext/curl/tests/bug48207.phpt index 086f949ff63d..794c5deec645 100644 --- a/ext/curl/tests/bug48207.phpt +++ b/ext/curl/tests/bug48207.phpt @@ -47,6 +47,6 @@ is_file($tempfile) and @unlink($tempfile); isset($tempname) and is_file($tempname) and @unlink($tempname); ?> --EXPECT-- -curl_setopt(): The provided file handle must be writable +curl_setopt(): The file handle provided for CURLOPT_FILE must be writable Hello World! Hello World! From d5d5cec9f35060f7a4c48b4bf600c3395b8f2ee3 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Tue, 28 Jul 2026 12:12:14 +0000 Subject: [PATCH 4/8] Revert MIN_SUPPORTED_CURL_VERSION --- ext/curl/sync-constants.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/curl/sync-constants.php b/ext/curl/sync-constants.php index 8165953c6d36..e24c773a5209 100755 --- a/ext/curl/sync-constants.php +++ b/ext/curl/sync-constants.php @@ -12,7 +12,7 @@ const SOURCE_FILE = __DIR__ . '/curl_arginfo.h'; -const MIN_SUPPORTED_CURL_VERSION = '7.73.0'; +const MIN_SUPPORTED_CURL_VERSION = '7.61.0'; const IGNORED_CURL_CONSTANTS = [ 'CURLOPT_PROGRESSDATA', From e108fe14a35b32b1b5ec2399a20619e22892d906 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Wed, 29 Jul 2026 08:58:26 +0000 Subject: [PATCH 5/8] Fix type of prefix string --- ext/curl/interface.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/curl/interface.c b/ext/curl/interface.c index 98969592bece..9f7acf7d3ec0 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -70,7 +70,7 @@ static const char * php_curl_option_get_name(zend_long option) { return opt->name; } #else - const char * prefix = "CURLOPT_"; + const char prefix[] = "CURLOPT_"; const size_t prefix_len = sizeof(prefix) - 1; zend_string *key; zend_constant *constant; From a95781c1ac15a33f9739176325664032f8306ea6 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Wed, 29 Jul 2026 17:43:53 +0000 Subject: [PATCH 6/8] Fix review comments - Add line to NEWS - Test more values for writable file handles - Fallback to zend_constants if curl_easy_option_by_id fails --- NEWS | 2 ++ ext/curl/interface.c | 5 +++-- ext/curl/tests/bug48207.phpt | 17 +++++++++++++---- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/NEWS b/NEWS index cb70bda237ff..91ac5f7b9c11 100644 --- a/NEWS +++ b/NEWS @@ -120,6 +120,8 @@ PHP NEWS and CURL_SEEKFUNC_CANTSEEK constants, letting libcurl rewind a streamed request body to resend it on a redirect, multi-pass authentication or a retried reused connection. (GrahamCampbell) + . Improved cURL option validation errors to include the option name. + (Sjoerd Langkemper) - Date: . Update timelib to 2022.17. (Derick) diff --git a/ext/curl/interface.c b/ext/curl/interface.c index 9f7acf7d3ec0..25bdffa44ce5 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -64,12 +64,14 @@ ZEND_DECLARE_MODULE_GLOBALS(curl) // php_curl_option_get_name(CURLOPT_HTTPHEADER) -> "HTTPHEADER" static const char * php_curl_option_get_name(zend_long option) { + #if LIBCURL_VERSION_NUM >= 0x074900 const struct curl_easyoption * opt = curl_easy_option_by_id(option); if (EXPECTED(opt != NULL)) { return opt->name; } -#else +#endif + const char prefix[] = "CURLOPT_"; const size_t prefix_len = sizeof(prefix) - 1; zend_string *key; @@ -86,7 +88,6 @@ static const char * php_curl_option_get_name(zend_long option) { return ZSTR_VAL(key) + prefix_len; } } ZEND_HASH_FOREACH_END(); -#endif return "UNKNOWN_OPTION"; } diff --git a/ext/curl/tests/bug48207.phpt b/ext/curl/tests/bug48207.phpt index 794c5deec645..6487076e9ee3 100644 --- a/ext/curl/tests/bug48207.phpt +++ b/ext/curl/tests/bug48207.phpt @@ -36,10 +36,17 @@ $tempfile = tempnam(sys_get_temp_dir(), 'CURL_FILE_HANDLE'); $fp = fopen($tempfile, "r"); // Opening 'fubar' with the incorrect readonly flag $ch = curl_init($url); -try { - curl_setopt($ch, CURLOPT_FILE, $fp); -} catch (ValueError $exception) { - echo $exception->getMessage() . "\n"; + +foreach ([ + CURLOPT_FILE, + CURLOPT_WRITEHEADER, + CURLOPT_STDERR, +] as $option) { + try { + curl_setopt($ch, $option, $fp); + } catch (ValueError $exception) { + echo $exception->getMessage(), "\n"; + } } curl_exec($ch); @@ -48,5 +55,7 @@ isset($tempname) and is_file($tempname) and @unlink($tempname); ?> --EXPECT-- curl_setopt(): The file handle provided for CURLOPT_FILE must be writable +curl_setopt(): The file handle provided for CURLOPT_WRITEHEADER must be writable +curl_setopt(): The file handle provided for CURLOPT_STDERR must be writable Hello World! Hello World! From adf91f33f9a81b84d615f41e048b353943de82c8 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Thu, 30 Jul 2026 06:04:05 +0000 Subject: [PATCH 7/8] Move NEWS item to alpha3 --- NEWS | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index 91ac5f7b9c11..c557daeccdba 100644 --- a/NEWS +++ b/NEWS @@ -25,6 +25,10 @@ PHP NEWS limit is reached while the tracing JIT enters a call frame). (Arnaud, iliaal) +- Curl: + . Improved cURL option validation errors to include the option name. + (Sjoerd Langkemper) + - DOM: . Fixed bug GH-22825 (DOMElement::setAttribute() fails silently when the DTD declares a default value for the attribute). (iliaal) @@ -120,8 +124,6 @@ PHP NEWS and CURL_SEEKFUNC_CANTSEEK constants, letting libcurl rewind a streamed request body to resend it on a redirect, multi-pass authentication or a retried reused connection. (GrahamCampbell) - . Improved cURL option validation errors to include the option name. - (Sjoerd Langkemper) - Date: . Update timelib to 2022.17. (Derick) From 37c402b09bff30b693b5d4099a289efcdc969755 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Thu, 30 Jul 2026 12:08:20 +0000 Subject: [PATCH 8/8] Move NEWS message even more --- NEWS | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index c557daeccdba..dd4bac185e72 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.6.0beta1 +- Curl: + . Improved cURL option validation errors to include the option name. + (Sjoerd Langkemper) + - GMP: . Added optional $definitely_prime output parameter to gmp_prevprime(). (Weilin Du) @@ -25,10 +29,6 @@ PHP NEWS limit is reached while the tracing JIT enters a call frame). (Arnaud, iliaal) -- Curl: - . Improved cURL option validation errors to include the option name. - (Sjoerd Langkemper) - - DOM: . Fixed bug GH-22825 (DOMElement::setAttribute() fails silently when the DTD declares a default value for the attribute). (iliaal)