diff --git a/NEWS b/NEWS index 219e54c1f9d5..ac709587f699 100644 --- a/NEWS +++ b/NEWS @@ -20,6 +20,9 @@ PHP NEWS an error). (Derick) . Fixed Unix timestamps in February of the year 0 are misparsed with @-notation. (LukasGelbmann) + . Fixed bug GH-11368 (idate() doesn't work for the year -1). (Derick) + . Fixed bug GH-11310 (__debugInfo does nothing on userland classes extending + Date classes). (Derick) - DBA: . Fixed OOB read on malformed length field in dba flatfile handler. (alhudz) @@ -83,11 +86,17 @@ PHP NEWS and a proxy set). (CVE-2026-12184) (ndossche) - Zip: + . Fixed bug GH-22649 (ZipArchive::setCommentName() and setCommentIndex() + could crash after overwriting an entry and resetting its inherited + unchanged comment). (Weilin Du) . Fixed bug GH-21705 (ZipArchive::getFromIndex() ignores ZipArchive::FL_UNCHANGED for deleted entries). (Weilin Du) . Fixed bug GH-22176 (memory leak with ZipArchive::registerCancelBack() is used with reference returning function during shutdown). (David Carlier) + . ZipArchive::addGlob() and ZipArchive::addPattern() now raise a TypeError + for invalid "remove_all_path", "comp_method", "comp_flags", and + "enc_method" options instead of emitting a warning. (David Carlier) 02 Jul 2026, PHP 8.6.0alpha1 diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 1c602548b1ed..bdf874b382d0 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -169,6 +169,10 @@ PHP 8.6 INTERNALS UPGRADE NOTES 3. Module changes ======================== +- ext/date: + . php_idate() now returns the result state, and moves the return value into an + out parameter. + - ext/mbstring: . Added GB18030-2022 to default encoding list for zh-CN. diff --git a/ext/curl/interface.c b/ext/curl/interface.c index 1dff14b7c8c2..09c7009068b6 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -2030,42 +2030,42 @@ 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; - if (Z_TYPE_P(zvalue) != IS_ARRAY) { - 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; - } + 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); return FAILURE; } @@ -2074,6 +2074,14 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue ZEND_HASH_FOREACH_VAL(ph, current) { ZVAL_DEREF(current); val = zval_get_tmp_string(current, &tmp_val); + + 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); + return FAILURE; + } + struct curl_slist *new_slist = curl_slist_append(slist, ZSTR_VAL(val)); zend_tmp_string_release(tmp_val); if (!new_slist) { diff --git a/ext/curl/tests/curl_setopt_error_nul_byte.phpt b/ext/curl/tests/curl_setopt_error_nul_byte.phpt new file mode 100644 index 000000000000..23be973b82c2 --- /dev/null +++ b/ext/curl/tests/curl_setopt_error_nul_byte.phpt @@ -0,0 +1,52 @@ +--TEST-- +curl_setopt() throws ValueError for NUL bytes in lists +--EXTENSIONS-- +curl +--FILE-- +getMessage() . "\n\n"; + } +} + +$ch = null; +?> +--EXPECT-- +CURLOPT_HTTP200ALIASES: curl_setopt(): cURL option CURLOPT_HTTP200ALIASES must not contain any null bytes + +CURLOPT_HTTPHEADER: curl_setopt(): cURL option CURLOPT_HTTPHEADER must not contain any null bytes + +CURLOPT_POSTQUOTE: curl_setopt(): cURL option CURLOPT_POSTQUOTE must not contain any null bytes + +CURLOPT_PREQUOTE: curl_setopt(): cURL option CURLOPT_PREQUOTE must not contain any null bytes + +CURLOPT_QUOTE: curl_setopt(): cURL option CURLOPT_QUOTE must not contain any null bytes + +CURLOPT_TELNETOPTIONS: curl_setopt(): cURL option CURLOPT_TELNETOPTIONS must not contain any null bytes + +CURLOPT_MAIL_RCPT: curl_setopt(): cURL option CURLOPT_MAIL_RCPT must not contain any null bytes + +CURLOPT_RESOLVE: curl_setopt(): cURL option CURLOPT_RESOLVE must not contain any null bytes + +CURLOPT_PROXYHEADER: curl_setopt(): cURL option CURLOPT_PROXYHEADER must not contain any null bytes + +CURLOPT_CONNECT_TO: curl_setopt(): cURL option CURLOPT_CONNECT_TO must not contain any null bytes diff --git a/ext/date/php_date.c b/ext/date/php_date.c index 1dbcf51e72df..c37230c6ca08 100644 --- a/ext/date/php_date.c +++ b/ext/date/php_date.c @@ -901,13 +901,13 @@ PHPAPI zend_string *php_format_date(const char *format, size_t format_len, time_ /* }}} */ /* {{{ php_idate */ -PHPAPI int php_idate(char format, time_t ts, bool localtime) +PHPAPI bool php_idate(char format, time_t ts, bool localtime, int *result) { timelib_time *t; timelib_tzinfo *tzi; - int retval = -1; timelib_time_offset *offset = NULL; timelib_sll isoweek, isoyear; + bool success = true; t = timelib_time_ctor(); @@ -947,46 +947,49 @@ PHPAPI int php_idate(char format, time_t ts, bool localtime) switch (format) { /* day */ - case 'd': case 'j': retval = (int) t->d; break; + case 'd': case 'j': *result = (int) t->d; break; - case 'N': retval = (int) timelib_iso_day_of_week(t->y, t->m, t->d); break; - case 'w': retval = (int) timelib_day_of_week(t->y, t->m, t->d); break; - case 'z': retval = (int) timelib_day_of_year(t->y, t->m, t->d); break; + case 'N': *result = (int) timelib_iso_day_of_week(t->y, t->m, t->d); break; + case 'w': *result = (int) timelib_day_of_week(t->y, t->m, t->d); break; + case 'z': *result = (int) timelib_day_of_year(t->y, t->m, t->d); break; /* week */ - case 'W': retval = (int) isoweek; break; /* iso weeknr */ + case 'W': *result = (int) isoweek; break; /* iso weeknr */ /* month */ - case 'm': case 'n': retval = (int) t->m; break; - case 't': retval = (int) timelib_days_in_month(t->y, t->m); break; + case 'm': case 'n': *result = (int) t->m; break; + case 't': *result = (int) timelib_days_in_month(t->y, t->m); break; /* year */ - case 'L': retval = (int) timelib_is_leap((int) t->y); break; - case 'y': retval = (int) (t->y % 100); break; - case 'Y': retval = (int) t->y; break; - case 'o': retval = (int) isoyear; break; /* iso year */ + case 'L': *result = (int) timelib_is_leap((int) t->y); break; + case 'y': *result = (int) (t->y % 100); break; + case 'Y': *result = (int) t->y; break; + case 'o': *result = (int) isoyear; break; /* iso year */ /* Swatch Beat a.k.a. Internet Time */ case 'B': - retval = ((((long)t->sse)-(((long)t->sse) - ((((long)t->sse) % 86400) + 3600))) * 10); - if (retval < 0) { - retval += 864000; + *result = ((((long)t->sse)-(((long)t->sse) - ((((long)t->sse) % 86400) + 3600))) * 10); + if (*result < 0) { + *result += 864000; } /* Make sure to do this on a positive int to avoid rounding errors */ - retval = (retval / 864) % 1000; + *result = (*result / 864) % 1000; break; /* time */ - case 'g': case 'h': retval = (int) ((t->h % 12) ? (int) t->h % 12 : 12); break; - case 'H': case 'G': retval = (int) t->h; break; - case 'i': retval = (int) t->i; break; - case 's': retval = (int) t->s; break; + case 'g': case 'h': *result = (int) ((t->h % 12) ? (int) t->h % 12 : 12); break; + case 'H': case 'G': *result = (int) t->h; break; + case 'i': *result = (int) t->i; break; + case 's': *result = (int) t->s; break; /* timezone */ - case 'I': retval = (int) (!localtime ? offset->is_dst : 0); break; - case 'Z': retval = (int) (!localtime ? offset->offset : 0); break; + case 'I': *result = (int) (!localtime ? offset->is_dst : 0); break; + case 'Z': *result = (int) (!localtime ? offset->offset : 0); break; - case 'U': retval = (int) t->sse; break; + case 'U': *result = (int) t->sse; break; + + default: + success = false; } if (!localtime) { @@ -994,7 +997,7 @@ PHPAPI int php_idate(char format, time_t ts, bool localtime) } timelib_time_dtor(t); - return retval; + return success; } /* }}} */ @@ -1018,7 +1021,7 @@ PHP_FUNCTION(idate) zend_string *format; zend_long ts; bool ts_is_null = 1; - int ret; + int ret = 0; ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_STR(format) @@ -1035,8 +1038,8 @@ PHP_FUNCTION(idate) ts = php_time(); } - ret = php_idate(ZSTR_VAL(format)[0], ts, 0); - if (ret == -1) { + bool ok = php_idate(ZSTR_VAL(format)[0], ts, 0, &ret); + if (!ok) { php_error_docref(NULL, E_WARNING, "Unrecognized date format token"); RETURN_FALSE; } @@ -1955,12 +1958,22 @@ static HashTable *date_object_get_properties_for(zend_object *object, zend_prop_ php_date_obj *dateobj; switch (purpose) { - case ZEND_PROP_PURPOSE_DEBUG: case ZEND_PROP_PURPOSE_SERIALIZE: case ZEND_PROP_PURPOSE_VAR_EXPORT: case ZEND_PROP_PURPOSE_JSON: case ZEND_PROP_PURPOSE_ARRAY_CAST: break; + case ZEND_PROP_PURPOSE_DEBUG: { + if (object->ce->__debugInfo) { + int is_temp = 0; + HashTable *ht = zend_std_get_debug_info(object, &is_temp); + if (ht && !is_temp) { + GC_TRY_ADDREF(ht); + } + return ht; + } + break; + } default: return zend_std_get_properties_for(object, purpose); } @@ -2077,12 +2090,22 @@ static HashTable *date_object_get_properties_for_timezone(zend_object *object, z php_timezone_obj *tzobj; switch (purpose) { - case ZEND_PROP_PURPOSE_DEBUG: case ZEND_PROP_PURPOSE_SERIALIZE: case ZEND_PROP_PURPOSE_VAR_EXPORT: case ZEND_PROP_PURPOSE_JSON: case ZEND_PROP_PURPOSE_ARRAY_CAST: break; + case ZEND_PROP_PURPOSE_DEBUG: { + if (object->ce->__debugInfo) { + int is_temp = 0; + HashTable *ht = zend_std_get_debug_info(object, &is_temp); + if (ht && !is_temp) { + GC_TRY_ADDREF(ht); + } + return ht; + } + break; + } default: return zend_std_get_properties_for(object, purpose); } diff --git a/ext/date/php_date.h b/ext/date/php_date.h index f26617c15ea9..651cc28225fd 100644 --- a/ext/date/php_date.h +++ b/ext/date/php_date.h @@ -124,7 +124,7 @@ PHPAPI time_t php_time(void); /* Backwards compatibility wrapper */ PHPAPI zend_long php_parse_date(const char *string, zend_long *now); PHPAPI void php_mktime(INTERNAL_FUNCTION_PARAMETERS, bool gmt); -PHPAPI int php_idate(char format, time_t ts, bool localtime); +PHPAPI bool php_idate(char format, time_t ts, bool localtime, int *result); #define _php_strftime php_strftime diff --git a/ext/date/tests/bug-gh11310-1.phpt b/ext/date/tests/bug-gh11310-1.phpt new file mode 100644 index 000000000000..c12b0201294d --- /dev/null +++ b/ext/date/tests/bug-gh11310-1.phpt @@ -0,0 +1,37 @@ +--TEST-- +Bug GH-11310: __debugInfo does nothing on userland classes extending Date classes +--FILE-- + 'zzz']; } } +class UDateTimeImmutable extends DateTimeImmutable { public function __debugInfo(): array { return ['value' => 'zzz']; } } +class UDateTimeZone extends DateTimeZone { public function __debugInfo(): array { return ['value' => 'zzz']; } } +class UDateInterval extends DateInterval { public function __debugInfo(): array { return ['value' => 'zzz']; } } +class UDatePeriod extends DatePeriod { public function __debugInfo(): array { return ['value' => 'zzz']; } } + +$d = new UDateTime(); var_dump($d); +$d = new UDateTimeImmutable(); var_dump($d); +$d = new UDateTimeZone("Europe/Kyiv"); var_dump($d); +$d = new UDateInterval("P3D"); var_dump($d); +$d = UDatePeriod::createFromISO8601String("2026-07-09T17:23:06Z/P3D/R5"); var_dump($d); +?> +--EXPECTF-- +object(UDateTime)#%d (1) { + ["value"]=> + string(3) "zzz" +} +object(UDateTimeImmutable)#%d (1) { + ["value"]=> + string(3) "zzz" +} +object(UDateTimeZone)#%d (1) { + ["value"]=> + string(3) "zzz" +} +object(UDateInterval)#%d (1) { + ["value"]=> + string(3) "zzz" +} +object(UDatePeriod)#%d (1) { + ["value"]=> + string(3) "zzz" +} diff --git a/ext/date/tests/bug-gh11310-2.phpt b/ext/date/tests/bug-gh11310-2.phpt new file mode 100644 index 000000000000..8dce510a19d6 --- /dev/null +++ b/ext/date/tests/bug-gh11310-2.phpt @@ -0,0 +1,37 @@ +--TEST-- +Bug GH-11310: __debugInfo does nothing on userland classes extending Date classes +--FILE-- + 'zzz']; } } +class UDateTimeImmutable extends DateTimeImmutable { public function __construct() {} public function __debugInfo(): array { return ['value' => 'zzz']; } } +class UDateTimeZone extends DateTimeZone { public function __construct() {} public function __debugInfo(): array { return ['value' => 'zzz']; } } +class UDateInterval extends DateInterval { public function __construct() {} public function __debugInfo(): array { return ['value' => 'zzz']; } } +class UDatePeriod extends DatePeriod { public function __construct() {} public function __debugInfo(): array { return ['value' => 'zzz']; } } + +$d = new UDateTime(); var_dump($d); +$d = new UDateTimeImmutable(); var_dump($d); +$d = new UDateTimeZone("Europe/Kyiv"); var_dump($d); +$d = new UDateInterval("P3D"); var_dump($d); +$d = UDatePeriod::createFromISO8601String("2026-07-09T17:23:06Z/P3D/R5"); var_dump($d); +?> +--EXPECTF-- +object(UDateTime)#%d (1) { + ["value"]=> + string(3) "zzz" +} +object(UDateTimeImmutable)#%d (1) { + ["value"]=> + string(3) "zzz" +} +object(UDateTimeZone)#%d (1) { + ["value"]=> + string(3) "zzz" +} +object(UDateInterval)#%d (1) { + ["value"]=> + string(3) "zzz" +} +object(UDatePeriod)#%d (1) { + ["value"]=> + string(3) "zzz" +} diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c index 1301c6fc6b17..48075ac0ec6b 100644 --- a/ext/zip/php_zip.c +++ b/ext/zip/php_zip.c @@ -57,17 +57,26 @@ static int le_zip_entry; } /* }}} */ -/* {{{ PHP_ZIP_SET_FILE_COMMENT(za, index, comment, comment_len) */ -#define PHP_ZIP_SET_FILE_COMMENT(za, index, comment, comment_len) \ - if (comment_len == 0) { \ - /* Passing NULL remove the existing comment */ \ - if (zip_file_set_comment(za, index, NULL, 0, 0) < 0) { \ - RETURN_FALSE; \ - } \ - } else if (zip_file_set_comment(za, index, comment, comment_len, 0) < 0) { \ - RETURN_FALSE; \ - } \ - RETURN_TRUE; +/* {{{ php_zip_set_file_comment */ +static bool php_zip_set_file_comment(struct zip *za, zip_uint64_t index, const char *comment, size_t comment_len) +{ + zip_uint32_t current_comment_len; + const char *current_comment = zip_file_get_comment(za, index, ¤t_comment_len, ZIP_FL_ENC_RAW); + + /* Avoid a libzip use-after-free when resetting an unchanged inherited comment. */ + if (current_comment && current_comment_len == comment_len + && memcmp(current_comment, comment, comment_len) == 0) { + return true; + } + + if (comment_len == 0) { + /* Passing NULL removes the existing comment. */ + return zip_file_set_comment(za, index, NULL, 0, 0) == 0; + } + + ZEND_ASSERT(comment_len <= 0xffff); + return zip_file_set_comment(za, index, comment, (zip_uint16_t) comment_len, 0) == 0; +} /* }}} */ # define add_ascii_assoc_string add_assoc_string @@ -2183,7 +2192,7 @@ PHP_METHOD(ZipArchive, setCommentName) zval *self = ZEND_THIS; size_t comment_len, name_len; char * comment, *name; - int idx; + zip_int64_t idx; if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss", &name, &name_len, &comment, &comment_len) == FAILURE) { @@ -2206,7 +2215,7 @@ PHP_METHOD(ZipArchive, setCommentName) if (idx < 0) { RETURN_FALSE; } - PHP_ZIP_SET_FILE_COMMENT(intern, idx, comment, comment_len); + RETURN_BOOL(php_zip_set_file_comment(intern, (zip_uint64_t) idx, comment, comment_len)); } /* }}} */ @@ -2219,6 +2228,7 @@ PHP_METHOD(ZipArchive, setCommentIndex) size_t comment_len; char * comment; struct zip_stat sb; + zip_uint64_t idx; if (zend_parse_parameters(ZEND_NUM_ARGS(), "ls", &index, &comment, &comment_len) == FAILURE) { @@ -2233,7 +2243,9 @@ PHP_METHOD(ZipArchive, setCommentIndex) } PHP_ZIP_STAT_INDEX(intern, index, 0, sb); - PHP_ZIP_SET_FILE_COMMENT(intern, index, comment, comment_len); + idx = (zip_uint64_t) index; + + RETURN_BOOL(php_zip_set_file_comment(intern, idx, comment, comment_len)); } /* }}} */ diff --git a/ext/zip/tests/gh22649.phpt b/ext/zip/tests/gh22649.phpt new file mode 100644 index 000000000000..d6d0a0928184 --- /dev/null +++ b/ext/zip/tests/gh22649.phpt @@ -0,0 +1,55 @@ +--TEST-- +GH-22649 (setCommentName/Index after addFromString should not segfault) +--EXTENSIONS-- +zip +--FILE-- +open($file, ZipArchive::CREATE)) { + exit('failed'); +} + +$zip->addFromString('dir/entry2d.txt', 'entry #2'); +var_dump($zip->setCommentName('dir/entry2d.txt', 'dir/entry2d.txt')); + +$zip->addFromString('dir/entry3d.txt', 'entry #3'); +var_dump($zip->setCommentIndex($zip->lastId, 'dir/entry3d.txt')); + +$zip->close(); + +if (!$zip->open($file, ZipArchive::CREATE)) { + exit('failed'); +} + +$zip->addFromString('dir/entry2d.txt', 'updated entry #2'); +var_dump($zip->setCommentName('dir/entry2d.txt', 'dir/entry2d.txt')); + +$zip->addFromString('dir/entry3d.txt', 'updated entry #3'); +var_dump($zip->setCommentIndex($zip->lastId, 'dir/entry3d.txt')); + +$zip->close(); + +if (!$zip->open($file)) { + exit('failed'); +} + +var_dump($zip->getCommentName('dir/entry2d.txt')); +var_dump($zip->getCommentName('dir/entry3d.txt')); + +$zip->close(); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +string(15) "dir/entry2d.txt" +string(15) "dir/entry3d.txt" +--CLEAN-- +