From a6749046f6c5fd043919cb9090452165542ab612 Mon Sep 17 00:00:00 2001 From: Saki Takamachi Date: Tue, 17 Jun 2025 21:05:02 +0900 Subject: [PATCH 01/69] PHP-8.4 is now for PHP 8.4.10-dev --- NEWS | 5 ++++- Zend/zend.h | 2 +- configure.ac | 2 +- main/php_version.h | 6 +++--- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/NEWS b/NEWS index c8a8197d1417..5d8334fdf78f 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,9 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| -?? ??? ????, PHP 8.4.9 +?? ??? ????, PHP 8.4.10 + + +03 Jul 2025, PHP 8.4.9 - BcMath: . Fixed bug GH-18641 (Accessing a BcMath\Number property by ref crashes). diff --git a/Zend/zend.h b/Zend/zend.h index 34a6a0258a26..15a9b3d8189a 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -20,7 +20,7 @@ #ifndef ZEND_H #define ZEND_H -#define ZEND_VERSION "4.4.9-dev" +#define ZEND_VERSION "4.4.10-dev" #define ZEND_ENGINE_3 diff --git a/configure.ac b/configure.ac index 3662d3e985b0..c94038e6ca5d 100644 --- a/configure.ac +++ b/configure.ac @@ -17,7 +17,7 @@ dnl Basic autoconf initialization, generation of config.nice. dnl ---------------------------------------------------------------------------- AC_PREREQ([2.68]) -AC_INIT([PHP],[8.4.9-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) +AC_INIT([PHP],[8.4.10-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) AC_CONFIG_SRCDIR([main/php_version.h]) AC_CONFIG_AUX_DIR([build]) AC_PRESERVE_HELP_ORDER diff --git a/main/php_version.h b/main/php_version.h index 7bd5e8c37f89..da01e82826df 100644 --- a/main/php_version.h +++ b/main/php_version.h @@ -2,7 +2,7 @@ /* edit configure.ac to change version number */ #define PHP_MAJOR_VERSION 8 #define PHP_MINOR_VERSION 4 -#define PHP_RELEASE_VERSION 9 +#define PHP_RELEASE_VERSION 10 #define PHP_EXTRA_VERSION "-dev" -#define PHP_VERSION "8.4.9-dev" -#define PHP_VERSION_ID 80409 +#define PHP_VERSION "8.4.10-dev" +#define PHP_VERSION_ID 80410 From 50606f85696053f886ebd48e1b543bd09d3be3d8 Mon Sep 17 00:00:00 2001 From: Eric Mann Date: Tue, 17 Jun 2025 08:06:35 -0700 Subject: [PATCH 02/69] PHP 8.3 is now for PHP 8.3.24-dev --- NEWS | 5 ++++- Zend/zend.h | 2 +- configure.ac | 2 +- main/php_version.h | 6 +++--- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/NEWS b/NEWS index d32c60625ed2..c29118397857 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,9 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| -?? ??? ????, PHP 8.3.23 +?? ??? ????, PHP 8.3.24 + + +03 Jul 2025, PHP 8.3.23 - Core: . Fixed GH-18695 (zend_ast_export() - float number is not preserved). diff --git a/Zend/zend.h b/Zend/zend.h index 704df32e9c14..0d2c52e2505e 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -20,7 +20,7 @@ #ifndef ZEND_H #define ZEND_H -#define ZEND_VERSION "4.3.23-dev" +#define ZEND_VERSION "4.3.24-dev" #define ZEND_ENGINE_3 diff --git a/configure.ac b/configure.ac index 965c0bdd853e..e96c12486fa2 100644 --- a/configure.ac +++ b/configure.ac @@ -17,7 +17,7 @@ dnl Basic autoconf initialization, generation of config.nice. dnl ---------------------------------------------------------------------------- AC_PREREQ([2.68]) -AC_INIT([PHP],[8.3.23-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) +AC_INIT([PHP],[8.3.24-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) AC_CONFIG_SRCDIR([main/php_version.h]) AC_CONFIG_AUX_DIR([build]) AC_PRESERVE_HELP_ORDER diff --git a/main/php_version.h b/main/php_version.h index 28e4162421de..cf74940556dd 100644 --- a/main/php_version.h +++ b/main/php_version.h @@ -2,7 +2,7 @@ /* edit configure.ac to change version number */ #define PHP_MAJOR_VERSION 8 #define PHP_MINOR_VERSION 3 -#define PHP_RELEASE_VERSION 23 +#define PHP_RELEASE_VERSION 24 #define PHP_EXTRA_VERSION "-dev" -#define PHP_VERSION "8.3.23-dev" -#define PHP_VERSION_ID 80323 +#define PHP_VERSION "8.3.24-dev" +#define PHP_VERSION_ID 80324 From 5ff5ee0698f47e87f65ca7daa3b338e804cd108f Mon Sep 17 00:00:00 2001 From: Demon Date: Sun, 8 Jun 2025 18:25:25 +0800 Subject: [PATCH 03/69] Fix iconv tests skipped on windows Closes GH-18802. --- ext/iconv/tests/translit-failure.phpt | 2 +- ext/iconv/tests/translit-utf8.phpt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/iconv/tests/translit-failure.phpt b/ext/iconv/tests/translit-failure.phpt index b639c26b4eb3..33f992300a18 100644 --- a/ext/iconv/tests/translit-failure.phpt +++ b/ext/iconv/tests/translit-failure.phpt @@ -4,7 +4,7 @@ Translit failure iconv --SKIPIF-- --INI-- error_reporting=2039 diff --git a/ext/iconv/tests/translit-utf8.phpt b/ext/iconv/tests/translit-utf8.phpt index 14b5d7a05e1b..5a308820ca2b 100644 --- a/ext/iconv/tests/translit-utf8.phpt +++ b/ext/iconv/tests/translit-utf8.phpt @@ -4,7 +4,7 @@ Translit UTF-8 quotes iconv --SKIPIF-- --INI-- error_reporting=2047 From 5cf3c2663ba53049fc6326c0b401542429992215 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 11 Jun 2025 13:22:02 +0200 Subject: [PATCH 04/69] Fix use after free during shutdown destruction Closes GH-18834. --- NEWS | 3 +++ Zend/tests/gh18833.phpt | 24 ++++++++++++++++++++++++ Zend/zend_objects_API.c | 4 +++- 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 Zend/tests/gh18833.phpt diff --git a/NEWS b/NEWS index c29118397857..af154569e284 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,9 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.3.24 +- Core: + . Fixed bug GH-18833 (Use after free with weakmaps dependent on destruction + order). (Daniil Gentili) 03 Jul 2025, PHP 8.3.23 diff --git a/Zend/tests/gh18833.phpt b/Zend/tests/gh18833.phpt new file mode 100644 index 000000000000..d00f860ee43a --- /dev/null +++ b/Zend/tests/gh18833.phpt @@ -0,0 +1,24 @@ +--TEST-- +GH-18833 (Use after free with weakmaps dependent on destruction order) +--FILE-- +current(); + +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/zend_objects_API.c b/Zend/zend_objects_API.c index 80f5b747db71..ec4c88d6aa51 100644 --- a/Zend/zend_objects_API.c +++ b/Zend/zend_objects_API.c @@ -104,7 +104,9 @@ ZEND_API void ZEND_FASTCALL zend_objects_store_free_object_storage(zend_objects_ if (IS_OBJ_VALID(obj)) { if (!(OBJ_FLAGS(obj) & IS_OBJ_FREE_CALLED)) { GC_ADD_FLAGS(obj, IS_OBJ_FREE_CALLED); - if (obj->handlers->free_obj != zend_object_std_dtor) { + if (obj->handlers->free_obj != zend_object_std_dtor + || (OBJ_FLAGS(obj) & IS_OBJ_WEAKLY_REFERENCED) + ) { GC_ADDREF(obj); obj->handlers->free_obj(obj); } From 9cacc57350e1bb2f070b4a7bda6803da1d71e140 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 19 Jun 2025 16:28:45 +0200 Subject: [PATCH 05/69] Track heap->real_size for USE_TRACKED_ALLOC real_size is returned by memory_get_usage(true), which previously returned 0. Discovered in Symfony ConsumeMessagesCommandTest::testRunWithMemoryLimit() through nightly. Closes GH-18880 --- Zend/zend_alloc.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c index c41f6118607e..61792b37c9c5 100644 --- a/Zend/zend_alloc.c +++ b/Zend/zend_alloc.c @@ -2256,6 +2256,7 @@ void zend_mm_shutdown(zend_mm_heap *heap, bool full, bool silent) heap->custom_heap.std._free = free; } heap->size = 0; + heap->real_size = 0; } if (full) { @@ -2799,6 +2800,7 @@ static void *tracked_malloc(size_t size) void *ptr = __zend_malloc(size); tracked_add(heap, ptr, size); heap->size += size; + heap->real_size = heap->size; return ptr; } @@ -2810,6 +2812,7 @@ static void tracked_free(void *ptr) { zend_mm_heap *heap = AG(mm_heap); zval *size_zv = tracked_get_size_zv(heap, ptr); heap->size -= Z_LVAL_P(size_zv); + heap->real_size = heap->size; zend_hash_del_bucket(heap->tracked_allocs, (Bucket *) size_zv); free(ptr); } @@ -2835,6 +2838,7 @@ static void *tracked_realloc(void *ptr, size_t new_size) { ptr = __zend_realloc(ptr, new_size); tracked_add(heap, ptr, new_size); heap->size += new_size - old_size; + heap->real_size = heap->size; return ptr; } From 940441106dda47a644510731cd4193704f70651a Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Fri, 20 Jun 2025 15:11:31 +0200 Subject: [PATCH 06/69] ext/dom: Fix new MSVC compiler warning Closes GH-18889 --- ext/dom/html_document.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/dom/html_document.c b/ext/dom/html_document.c index 1ba39870d39b..248e85c74c39 100644 --- a/ext/dom/html_document.c +++ b/ext/dom/html_document.c @@ -752,7 +752,7 @@ static bool dom_parse_decode_encode_finish( static bool check_options_validity(uint32_t arg_num, zend_long options) { - const zend_long VALID_OPTIONS = XML_PARSE_NOERROR | XML_PARSE_COMPACT | HTML_PARSE_NOIMPLIED | DOM_HTML_NO_DEFAULT_NS; + const zend_long VALID_OPTIONS = HTML_PARSE_NOERROR | HTML_PARSE_COMPACT | HTML_PARSE_NOIMPLIED | DOM_HTML_NO_DEFAULT_NS; if ((options & ~VALID_OPTIONS) != 0) { zend_argument_value_error(arg_num, "contains invalid flags (allowed flags: " "LIBXML_NOERROR, " From 391bd2a48fb1e256ec7108166857997eea5da93a Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Fri, 20 Jun 2025 22:39:51 +0200 Subject: [PATCH 07/69] Remove bug61371 test These tests attempt to test that no memory is leaked for stream calls. However, it is incorrect to assume the memory will not increase for other reasons, e.g. when growing resource buffers, for the output buffer, etc. This was discovered through 9cacc57350e1bb2f070b4a7bda6803da1d71e140 with USE_TRACKED_ALLOC=1, but this can also fail with USE_ZEND_ALLOC=1 when increasing loop iterations. --- ext/standard/tests/streams/bug61371-unix.phpt | 45 ------------------- ext/standard/tests/streams/bug61371.phpt | 40 ----------------- 2 files changed, 85 deletions(-) delete mode 100644 ext/standard/tests/streams/bug61371-unix.phpt delete mode 100644 ext/standard/tests/streams/bug61371.phpt diff --git a/ext/standard/tests/streams/bug61371-unix.phpt b/ext/standard/tests/streams/bug61371-unix.phpt deleted file mode 100644 index e196c028cc94..000000000000 --- a/ext/standard/tests/streams/bug61371-unix.phpt +++ /dev/null @@ -1,45 +0,0 @@ ---TEST-- -Bug #61371: stream_context_create() causes memory leaks on use streams_socket_create ---SKIPIF-- - ---EXPECTF-- -memory: %dkb -bool(true) -memory: %dkb -bool(true) -memory: %dkb -memory: %dkb -bool(true) -memory: %dkb -bool(true) -memory: %dkb diff --git a/ext/standard/tests/streams/bug61371.phpt b/ext/standard/tests/streams/bug61371.phpt deleted file mode 100644 index 00e6372e85a4..000000000000 --- a/ext/standard/tests/streams/bug61371.phpt +++ /dev/null @@ -1,40 +0,0 @@ ---TEST-- -Bug #61371: stream_context_create() causes memory leaks on use streams_socket_create ---FILE-- - ---EXPECTF-- -memory: %dkb -bool(true) -memory: %dkb -bool(true) -memory: %dkb -memory: %dkb -bool(true) -memory: %dkb -bool(true) -memory: %dkb From c6b058b7d26d2501c3e8e75f5642f746693d4a7b Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Thu, 19 Jun 2025 22:48:57 +0200 Subject: [PATCH 08/69] Fix memory leaks when returning refcounted value from curl callback Closes GH-18883. --- NEWS | 4 +++ ext/curl/curl_private.h | 3 +++ ext/curl/interface.c | 22 +++++++++++---- ext/curl/multi.c | 2 +- .../refcounted_return_must_not_leak.phpt | 27 +++++++++++++++++++ 5 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 ext/curl/tests/refcounted_return_must_not_leak.phpt diff --git a/NEWS b/NEWS index af154569e284..1d91c11862ff 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,10 @@ PHP NEWS . Fixed bug GH-18833 (Use after free with weakmaps dependent on destruction order). (Daniil Gentili) +- Curl: + . Fix memory leaks when returning refcounted value from curl callback. + (nielsdos) + 03 Jul 2025, PHP 8.3.23 - Core: diff --git a/ext/curl/curl_private.h b/ext/curl/curl_private.h index 3800d6533e55..66cba9d562ad 100644 --- a/ext/curl/curl_private.h +++ b/ext/curl/curl_private.h @@ -147,6 +147,9 @@ void _php_curl_multi_cleanup_list(void *data); void _php_curl_verify_handlers(php_curl *ch, bool reporterror); void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source); +/* Consumes `zv` */ +zend_long php_curl_get_long(zval *zv); + static inline php_curl *curl_from_obj(zend_object *obj) { return (php_curl *)((char *)(obj) - XtOffsetOf(php_curl, std)); } diff --git a/ext/curl/interface.c b/ext/curl/interface.c index 61d830e8abfe..6c480907b762 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -623,7 +623,7 @@ static size_t curl_write(char *data, size_t size, size_t nmemb, void *ctx) length = -1; } else if (!Z_ISUNDEF(retval)) { _php_curl_verify_handlers(ch, /* reporterror */ true); - length = zval_get_long(&retval); + length = php_curl_get_long(&retval); } zval_ptr_dtor(&argv[0]); @@ -667,7 +667,7 @@ static int curl_fnmatch(void *ctx, const char *pattern, const char *string) php_error_docref(NULL, E_WARNING, "Cannot call the CURLOPT_FNMATCH_FUNCTION"); } else if (!Z_ISUNDEF(retval)) { _php_curl_verify_handlers(ch, /* reporterror */ true); - rval = zval_get_long(&retval); + rval = php_curl_get_long(&retval); } zval_ptr_dtor(&argv[0]); zval_ptr_dtor(&argv[1]); @@ -715,7 +715,7 @@ static size_t curl_progress(void *clientp, double dltotal, double dlnow, double php_error_docref(NULL, E_WARNING, "Cannot call the CURLOPT_PROGRESSFUNCTION"); } else if (!Z_ISUNDEF(retval)) { _php_curl_verify_handlers(ch, /* reporterror */ true); - if (0 != zval_get_long(&retval)) { + if (0 != php_curl_get_long(&retval)) { rval = 1; } } @@ -764,7 +764,7 @@ static size_t curl_xferinfo(void *clientp, curl_off_t dltotal, curl_off_t dlnow, php_error_docref(NULL, E_WARNING, "Cannot call the CURLOPT_XFERINFOFUNCTION"); } else if (!Z_ISUNDEF(retval)) { _php_curl_verify_handlers(ch, /* reporterror */ true); - if (0 != zval_get_long(&retval)) { + if (0 != php_curl_get_long(&retval)) { rval = 1; } } @@ -821,6 +821,7 @@ static int curl_ssh_hostkeyfunction(void *clientp, int keytype, const char *key, } } else { zend_throw_error(NULL, "The CURLOPT_SSH_HOSTKEYFUNCTION callback must return either CURLKHMATCH_OK or CURLKHMATCH_MISMATCH"); + zval_ptr_dtor(&retval); } } zval_ptr_dtor(&argv[0]); @@ -938,7 +939,7 @@ static size_t curl_write_header(char *data, size_t size, size_t nmemb, void *ctx length = -1; } else if (!Z_ISUNDEF(retval)) { _php_curl_verify_handlers(ch, /* reporterror */ true); - length = zval_get_long(&retval); + length = php_curl_get_long(&retval); } zval_ptr_dtor(&argv[0]); zval_ptr_dtor(&argv[1]); @@ -1290,6 +1291,17 @@ void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source) (*source->clone)++; } +zend_long php_curl_get_long(zval *zv) +{ + if (EXPECTED(Z_TYPE_P(zv) == IS_LONG)) { + return Z_LVAL_P(zv); + } else { + zend_long ret = zval_get_long(zv); + zval_ptr_dtor(zv); + return ret; + } +} + #if LIBCURL_VERSION_NUM >= 0x073800 /* 7.56.0 */ static size_t read_cb(char *buffer, size_t size, size_t nitems, void *arg) /* {{{ */ { diff --git a/ext/curl/multi.c b/ext/curl/multi.c index be8b26cec916..09802d7e37d5 100644 --- a/ext/curl/multi.c +++ b/ext/curl/multi.c @@ -430,7 +430,7 @@ static int _php_server_push_callback(CURL *parent_ch, CURL *easy, size_t num_hea if (error == FAILURE) { php_error_docref(NULL, E_WARNING, "Cannot call the CURLMOPT_PUSHFUNCTION"); } else if (!Z_ISUNDEF(retval)) { - if (CURL_PUSH_DENY != zval_get_long(&retval)) { + if (CURL_PUSH_DENY != php_curl_get_long(&retval)) { rval = CURL_PUSH_OK; zend_llist_add_element(&mh->easyh, &pz_ch); } else { diff --git a/ext/curl/tests/refcounted_return_must_not_leak.phpt b/ext/curl/tests/refcounted_return_must_not_leak.phpt new file mode 100644 index 000000000000..b4b07a1a4fc8 --- /dev/null +++ b/ext/curl/tests/refcounted_return_must_not_leak.phpt @@ -0,0 +1,27 @@ +--TEST-- +Returning refcounted value from callback must not leak +--EXTENSIONS-- +curl +--FILE-- + +--EXPECT-- +ok From a36b8fdc9423b015de005f724392e34a1cb21a62 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 5 Jun 2025 19:30:37 +0200 Subject: [PATCH 09/69] Fix GH-13264: fgets() and stream_get_line() do not return false on filter fatal error This happens because there are no checks in php_stream_fill_read_buffer calls. This should not fail always but only on fatal error so special flag is needed for that. Closes GH-18778 --- NEWS | 4 ++ ext/sqlite3/sqlite3.c | 4 ++ ext/standard/tests/filters/gh13264.phpt | 72 ++++++++++++++----------- main/php_streams.h | 3 ++ main/streams/memory.c | 5 ++ main/streams/streams.c | 16 +++++- 6 files changed, 70 insertions(+), 34 deletions(-) diff --git a/NEWS b/NEWS index 1d91c11862ff..9881c36d4b01 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,10 @@ PHP NEWS . Fix memory leaks when returning refcounted value from curl callback. (nielsdos) +- Streams: + . Fixed GH-13264 (fgets() and stream_get_line() do not return false on filter + fatal error). (Jakub Zelenka) + 03 Jul 2025, PHP 8.3.23 - Core: diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c index 09cb57410c87..9b3286b70220 100644 --- a/ext/sqlite3/sqlite3.c +++ b/ext/sqlite3/sqlite3.c @@ -1165,6 +1165,7 @@ static int php_sqlite3_stream_seek(php_stream *stream, zend_off_t offset, int wh sqlite3_stream->position = sqlite3_stream->position + offset; *newoffs = sqlite3_stream->position; stream->eof = 0; + stream->fatal_error = 0; return 0; } } else { @@ -1176,6 +1177,7 @@ static int php_sqlite3_stream_seek(php_stream *stream, zend_off_t offset, int wh sqlite3_stream->position = sqlite3_stream->position + offset; *newoffs = sqlite3_stream->position; stream->eof = 0; + stream->fatal_error = 0; return 0; } } @@ -1188,6 +1190,7 @@ static int php_sqlite3_stream_seek(php_stream *stream, zend_off_t offset, int wh sqlite3_stream->position = offset; *newoffs = sqlite3_stream->position; stream->eof = 0; + stream->fatal_error = 0; return 0; } case SEEK_END: @@ -1203,6 +1206,7 @@ static int php_sqlite3_stream_seek(php_stream *stream, zend_off_t offset, int wh sqlite3_stream->position = sqlite3_stream->size + offset; *newoffs = sqlite3_stream->position; stream->eof = 0; + stream->fatal_error = 0; return 0; } default: diff --git a/ext/standard/tests/filters/gh13264.phpt b/ext/standard/tests/filters/gh13264.phpt index e992d0868898..f778bbf915a7 100644 --- a/ext/standard/tests/filters/gh13264.phpt +++ b/ext/standard/tests/filters/gh13264.phpt @@ -1,49 +1,57 @@ --TEST-- -GH-81475: Memory leak during stream filter failure +GH-13264: fgets() and stream_get_line() do not return false on filter fatal error --SKIPIF-- --FILE-- 15]); -// Rewind and add the zlib filter -rewind($stream); -stream_filter_append($stream, 'zlib.inflate', STREAM_FILTER_READ, ['window' => 15]); + return $stream; +} -// Read the filtered stream line by line. +// Read the filtered stream line by line using fgets. +$stream = create_stream(); while (($line = fgets($stream)) !== false) { - $error = error_get_last(); - if ($error !== null) { - // An error is thrown but fgets didn't return false - var_dump(error_get_last()); - var_dump($line); - } + $error = error_get_last(); + if ($error !== null) { + // An error is thrown but fgets didn't return false + var_dump(error_get_last()); + var_dump($line); + } } +fclose($stream); +error_clear_last(); +// Read the filtered stream line by line using stream_get_line. +$stream = create_stream(); +while (($line = stream_get_line($stream, 0, "\n")) !== false) { + $error = error_get_last(); + if ($error !== null) { + // An error is thrown but stream_get_line didn't return false + var_dump(error_get_last()); + var_dump($line); + } +} fclose($stream); ?> --EXPECTF-- Notice: fgets(): zlib: data error in %s on line %d -array(4) { - ["type"]=> - int(8) - ["message"]=> - string(25) "fgets(): zlib: data error" - ["file"]=> - string(%d) "%s" - ["line"]=> - int(%d) -} -string(%d) "Hello%s" +Notice: stream_get_line(): zlib: data error in %s on line %d diff --git a/main/php_streams.h b/main/php_streams.h index 31b80de98605..8996ee8bcb07 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -223,6 +223,9 @@ struct _php_stream { /* whether stdio cast flushing is in progress */ uint16_t fclose_stdiocast_flush_in_progress:1; + /* whether fatal error happened and all operations should terminates as soon as possible */ + uint16_t fatal_error:1; + char mode[16]; /* "rwb" etc. ala stdio */ uint32_t flags; /* PHP_STREAM_FLAG_XXX */ diff --git a/main/streams/memory.c b/main/streams/memory.c index ce11aec382bf..785109db6582 100644 --- a/main/streams/memory.c +++ b/main/streams/memory.c @@ -136,10 +136,12 @@ static int php_stream_memory_seek(php_stream *stream, zend_off_t offset, int whe ms->fpos = ms->fpos + offset; *newoffs = ms->fpos; stream->eof = 0; + stream->fatal_error = 0; return 0; } } else { stream->eof = 0; + stream->fatal_error = 0; ms->fpos = ms->fpos + offset; *newoffs = ms->fpos; return 0; @@ -153,6 +155,7 @@ static int php_stream_memory_seek(php_stream *stream, zend_off_t offset, int whe ms->fpos = offset; *newoffs = ms->fpos; stream->eof = 0; + stream->fatal_error = 0; return 0; } case SEEK_END: @@ -160,6 +163,7 @@ static int php_stream_memory_seek(php_stream *stream, zend_off_t offset, int whe ms->fpos = ZSTR_LEN(ms->data) + offset; *newoffs = ms->fpos; stream->eof = 0; + stream->fatal_error = 0; return 0; } else if (ZSTR_LEN(ms->data) < (size_t)(-offset)) { ms->fpos = 0; @@ -169,6 +173,7 @@ static int php_stream_memory_seek(php_stream *stream, zend_off_t offset, int whe ms->fpos = ZSTR_LEN(ms->data) + offset; *newoffs = ms->fpos; stream->eof = 0; + stream->fatal_error = 0; return 0; } default: diff --git a/main/streams/streams.c b/main/streams/streams.c index 4f9c88e4774c..1471c98558e1 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -636,6 +636,7 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size) /* some fatal error. Theoretically, the stream is borked, so all * further reads should fail. */ stream->eof = 1; + stream->fatal_error = 1; /* free all data left in brigades */ while ((bucket = brig_inp->head)) { /* Remove unconsumed buckets from the input brigade */ @@ -1009,7 +1010,12 @@ PHPAPI char *_php_stream_get_line(php_stream *stream, char *buf, size_t maxlen, } } - php_stream_fill_read_buffer(stream, toread); + if (php_stream_fill_read_buffer(stream, toread) == FAILURE && stream->fatal_error) { + if (grow_mode) { + efree(bufstart); + } + return NULL; + } if (stream->writepos - stream->readpos == 0) { break; @@ -1084,7 +1090,9 @@ PHPAPI zend_string *php_stream_get_record(php_stream *stream, size_t maxlen, con to_read_now = MIN(maxlen - buffered_len, stream->chunk_size); - php_stream_fill_read_buffer(stream, buffered_len + to_read_now); + if (php_stream_fill_read_buffer(stream, buffered_len + to_read_now) == FAILURE && stream->fatal_error) { + return NULL; + } just_read = STREAM_BUFFERED_AMOUNT(stream) - buffered_len; @@ -1357,6 +1365,7 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) stream->readpos += offset; /* if offset = ..., then readpos = writepos */ stream->position += offset; stream->eof = 0; + stream->fatal_error = 0; return 0; } break; @@ -1366,6 +1375,7 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) stream->readpos += offset - stream->position; stream->position = offset; stream->eof = 0; + stream->fatal_error = 0; return 0; } break; @@ -1400,6 +1410,7 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) if (((stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0) || ret == 0) { if (ret == 0) { stream->eof = 0; + stream->fatal_error = 0; } /* invalidate the buffer contents */ @@ -1422,6 +1433,7 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) offset -= didread; } stream->eof = 0; + stream->fatal_error = 0; return 0; } From 2694eb9df04beaab1bc052a4da53d9adc6c29f0a Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sun, 22 Jun 2025 08:00:08 +0100 Subject: [PATCH 10/69] Fixed GH-18902: ldap_exop/ldap_exop_sync assert triggered on empty request OID close GH-18903 --- NEWS | 4 ++++ ext/ldap/ldap.c | 7 ++++++- ext/ldap/tests/gh18902.phpt | 30 ++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 ext/ldap/tests/gh18902.phpt diff --git a/NEWS b/NEWS index 9881c36d4b01..25706b1efc0a 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,10 @@ PHP NEWS . Fix memory leaks when returning refcounted value from curl callback. (nielsdos) +- LDAP: + . Fixed GH-18902 ldap_exop/ldap_exop_sync assert triggered on empty + request OID. (David Carlier) + - Streams: . Fixed GH-13264 (fgets() and stream_get_line() do not return false on filter fatal error). (Jakub Zelenka) diff --git a/ext/ldap/ldap.c b/ext/ldap/ldap.c index fecb8846400a..769e6caa277b 100644 --- a/ext/ldap/ldap.c +++ b/ext/ldap/ldap.c @@ -4036,7 +4036,12 @@ static void php_ldap_exop(INTERNAL_FUNCTION_PARAMETERS, bool force_sync) { LDAPControl **lserverctrls = NULL; int rc, msgid; - if (zend_parse_parameters(ZEND_NUM_ARGS(), "OS|S!a!zz", &link, ldap_link_ce, &reqoid, &reqdata, &serverctrls, &retdata, &retoid) != SUCCESS) { + if (zend_parse_parameters(ZEND_NUM_ARGS(), "OP|S!a!zz", &link, ldap_link_ce, &reqoid, &reqdata, &serverctrls, &retdata, &retoid) != SUCCESS) { + RETURN_THROWS(); + } + + if (ZSTR_LEN(reqoid) == 0) { + zend_argument_value_error(2, "must not be empty"); RETURN_THROWS(); } diff --git a/ext/ldap/tests/gh18902.phpt b/ext/ldap/tests/gh18902.phpt new file mode 100644 index 000000000000..329cbb59c1b1 --- /dev/null +++ b/ext/ldap/tests/gh18902.phpt @@ -0,0 +1,30 @@ +--TEST-- +GH-17704 (ldap_search fails when $attributes contains a non-packed array with numerical keys) +--EXTENSIONS-- +ldap +--FILE-- +getMessage(), PHP_EOL; +} + +try { + ldap_exop_sync($conn,""); +} catch (\ValueError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + ldap_exop_sync($conn,"test\0"); +} catch (\ValueError $e) { + echo $e->getMessage(), PHP_EOL; +} +?> +--EXPECTF-- +ldap_exop(): Argument #2 ($request_oid) must not contain any null bytes +ldap_exop_sync(): Argument #2 ($request_oid) must not be empty +ldap_exop_sync(): Argument #2 ($request_oid) must not contain any null bytes From a5f21ca7005f3825c47d52772ee2f66c6a81cf86 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sun, 22 Jun 2025 10:13:06 +0200 Subject: [PATCH 11/69] Fix GH-18901: integer overflow mb_split We prevent signed overflow by making the count unsigned. The actual interpretation of the count doesn't matter as it's just used to denote a limit. The test output for some limit values looks strange though, so that may need extra investigation. However, that's orthogonal to this fix. Closes GH-18906. --- NEWS | 3 ++ ext/mbstring/php_mbregex.c | 2 +- ext/mbstring/tests/gh18901.phpt | 54 +++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 ext/mbstring/tests/gh18901.phpt diff --git a/NEWS b/NEWS index 25706b1efc0a..ea77125b205e 100644 --- a/NEWS +++ b/NEWS @@ -14,6 +14,9 @@ PHP NEWS . Fixed GH-18902 ldap_exop/ldap_exop_sync assert triggered on empty request OID. (David Carlier) +- MbString: + . Fixed bug GH-18901 (integer overflow mb_split). (nielsdos) + - Streams: . Fixed GH-13264 (fgets() and stream_get_line() do not return false on filter fatal error). (Jakub Zelenka) diff --git a/ext/mbstring/php_mbregex.c b/ext/mbstring/php_mbregex.c index 99dc91e34dca..86bc5f61d854 100644 --- a/ext/mbstring/php_mbregex.c +++ b/ext/mbstring/php_mbregex.c @@ -1184,7 +1184,7 @@ PHP_FUNCTION(mb_split) size_t string_len; int err; - zend_long count = -1; + zend_ulong count = -1; /* unsigned, it's a limit and we want to prevent signed overflow */ if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss|l", &arg_pattern, &arg_pattern_len, &string, &string_len, &count) == FAILURE) { RETURN_THROWS(); diff --git a/ext/mbstring/tests/gh18901.phpt b/ext/mbstring/tests/gh18901.phpt new file mode 100644 index 000000000000..8d862a537c3b --- /dev/null +++ b/ext/mbstring/tests/gh18901.phpt @@ -0,0 +1,54 @@ +--TEST-- +GH-18901 (integer overflow mb_split) +--EXTENSIONS-- +mbstring +--SKIPIF-- + +--FILE-- + +--EXPECT-- +array(4) { + [0]=> + string(0) "" + [1]=> + string(0) "" + [2]=> + string(0) "" + [3]=> + string(0) "" +} +array(4) { + [0]=> + string(0) "" + [1]=> + string(0) "" + [2]=> + string(0) "" + [3]=> + string(0) "" +} +array(4) { + [0]=> + string(0) "" + [1]=> + string(0) "" + [2]=> + string(0) "" + [3]=> + string(0) "" +} +array(1) { + [0]=> + string(3) "123" +} +array(1) { + [0]=> + string(3) "123" +} From 2ccd2b016df2c4cf8ff36a65b5875f1a7e39ac21 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 14 Jun 2025 11:11:38 +0100 Subject: [PATCH 12/69] ext/calendar: jewishtojd overflow on year argument. Upper limit set to the 7th millenium (Messianic Age) in the jewish calendar, around 2239 year in the gregorian calendar. close GH-18849 --- NEWS | 3 +++ ext/calendar/calendar.c | 5 +++++ ext/calendar/jewish.c | 2 +- ext/calendar/tests/gh16234_2.phpt | 11 +++++++++++ ext/calendar/tests/gh16234_2_64.phpt | 21 +++++++++++++++++++++ 5 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 ext/calendar/tests/gh16234_2.phpt create mode 100644 ext/calendar/tests/gh16234_2_64.phpt diff --git a/NEWS b/NEWS index ea77125b205e..fd344ee94c72 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,9 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.3.24 +- Calendar: + . Fixed jewishtojd overflow on year argument. (David Carlier) + - Core: . Fixed bug GH-18833 (Use after free with weakmaps dependent on destruction order). (Daniil Gentili) diff --git a/ext/calendar/calendar.c b/ext/calendar/calendar.c index 756ce0e90dc9..6da7e69529e2 100644 --- a/ext/calendar/calendar.c +++ b/ext/calendar/calendar.c @@ -490,6 +490,11 @@ PHP_FUNCTION(jewishtojd) RETURN_THROWS(); } + if (ZEND_LONG_EXCEEDS_INT(year)) { + zend_argument_value_error(3, "must be between %d and %d", INT_MIN, INT_MAX); + RETURN_THROWS(); + } + RETURN_LONG(JewishToSdn(year, month, day)); } /* }}} */ diff --git a/ext/calendar/jewish.c b/ext/calendar/jewish.c index bdfc9b4f9101..2fbdcb059b09 100644 --- a/ext/calendar/jewish.c +++ b/ext/calendar/jewish.c @@ -714,7 +714,7 @@ zend_long JewishToSdn( int yearLength; int lengthOfAdarIAndII; - if (year <= 0 || day <= 0 || day > 30) { + if (year <= 0 || year >= 6000 || day <= 0 || day > 30) { return (0); } switch (month) { diff --git a/ext/calendar/tests/gh16234_2.phpt b/ext/calendar/tests/gh16234_2.phpt new file mode 100644 index 000000000000..76db2b9abf26 --- /dev/null +++ b/ext/calendar/tests/gh16234_2.phpt @@ -0,0 +1,11 @@ +--TEST-- +GH-16234 jewishtojd overflow on year argument +--EXTENSIONS-- +calendar +--FILE-- + +--EXPECTF-- +DONE diff --git a/ext/calendar/tests/gh16234_2_64.phpt b/ext/calendar/tests/gh16234_2_64.phpt new file mode 100644 index 000000000000..7da254609650 --- /dev/null +++ b/ext/calendar/tests/gh16234_2_64.phpt @@ -0,0 +1,21 @@ +--TEST-- +GH-16234 jewishtojd overflow on year argument +--EXTENSIONS-- +calendar +--SKIPIF-- + +--FILE-- +getMessage(), PHP_EOL; +} +?> +--EXPECTF-- +jewishtojd(): Argument #3 ($year) must be between %i and %d + From fe504d33571f7c21a3529594693460a863dbb5ed Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Mon, 23 Jun 2025 00:05:03 +0200 Subject: [PATCH 13/69] Fix leak when creating cycle in hook This is necessary because the VM frees operands with the nogc variants. We cannot just call gc_possible_root() because the object may no longer exist at that point. Fixes GH-18907 Closes GH-18917 --- NEWS | 1 + Zend/tests/gh18907.phpt | 26 ++++++++++++++++++++++++++ Zend/zend_object_handlers.c | 2 ++ 3 files changed, 29 insertions(+) create mode 100644 Zend/tests/gh18907.phpt diff --git a/NEWS b/NEWS index 94eb74cae517..80b805983e86 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,7 @@ PHP NEWS - Core: . Fixed bug GH-18833 (Use after free with weakmaps dependent on destruction order). (Daniil Gentili) + . Fixed bug GH-18907 (Leak when creating cycle in hook). (ilutov) - Curl: . Fix memory leaks when returning refcounted value from curl callback. diff --git a/Zend/tests/gh18907.phpt b/Zend/tests/gh18907.phpt new file mode 100644 index 000000000000..1be881fd4941 --- /dev/null +++ b/Zend/tests/gh18907.phpt @@ -0,0 +1,26 @@ +--TEST-- +GH-18907: Leak when creating cycle inside hook +--FILE-- +prop = $this; + return 1; + } + } +} + +function test() { + var_dump((new Foo)->prop); +} + +/* Call twice to test the ZEND_IS_PROPERTY_HOOK_SIMPLE_GET() path. */ +test(); +test(); + +?> +--EXPECT-- +int(1) +int(1) diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 0def95fc8522..2ddaeae96e99 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -719,7 +719,9 @@ static bool zend_call_get_hook( return false; } + GC_ADDREF(zobj); zend_call_known_instance_method_with_0_params(get, zobj, rv); + OBJ_RELEASE(zobj); return true; } From b50898894d885eb4a95e6ff88f90ab56f9c8c03c Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sun, 22 Jun 2025 16:03:45 +0200 Subject: [PATCH 14/69] Unbreak PRINTF_DEBUG macro usages Clearly nobody has used this in a while given the compile errors and warnings. This patch fixes them so there are no errors nor warnings anymore. Closes GH-18910. --- ext/standard/formatted_print.c | 46 ++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/ext/standard/formatted_print.c b/ext/standard/formatted_print.c index b988422df21c..00445d3cca7c 100644 --- a/ext/standard/formatted_print.c +++ b/ext/standard/formatted_print.c @@ -52,10 +52,10 @@ inline static void php_sprintf_appendchar(zend_string **buffer, size_t *pos, char add) { if ((*pos + 1) >= ZSTR_LEN(*buffer)) { - PRINTF_DEBUG(("%s(): ereallocing buffer to %d bytes\n", get_active_function_name(), ZSTR_LEN(*buffer))); + PRINTF_DEBUG(("%s(): ereallocing buffer to %zu bytes\n", get_active_function_name(), ZSTR_LEN(*buffer))); *buffer = zend_string_extend(*buffer, ZSTR_LEN(*buffer) << 1, 0); } - PRINTF_DEBUG(("sprintf: appending '%c', pos=\n", add, *pos)); + PRINTF_DEBUG(("sprintf: appending '%c', pos=%zu\n", add, *pos)); ZSTR_VAL(*buffer)[(*pos)++] = add; } /* }}} */ @@ -67,13 +67,13 @@ php_sprintf_appendchars(zend_string **buffer, size_t *pos, char *add, size_t len if ((*pos + len) >= ZSTR_LEN(*buffer)) { size_t nlen = ZSTR_LEN(*buffer); - PRINTF_DEBUG(("%s(): ereallocing buffer to %d bytes\n", get_active_function_name(), ZSTR_LEN(*buffer))); + PRINTF_DEBUG(("%s(): ereallocing buffer to %zu bytes\n", get_active_function_name(), ZSTR_LEN(*buffer))); do { nlen = nlen << 1; } while ((*pos + len) >= nlen); *buffer = zend_string_extend(*buffer, nlen, 0); } - PRINTF_DEBUG(("sprintf: appending \"%s\", pos=\n", add, *pos)); + PRINTF_DEBUG(("sprintf: appending \"%s\", pos=%zu\n", add, *pos)); memcpy(ZSTR_VAL(*buffer) + (*pos), add, len); *pos += len; } @@ -93,7 +93,7 @@ php_sprintf_appendstring(zend_string **buffer, size_t *pos, char *add, copy_len = (expprec ? MIN(max_width, len) : len); npad = (min_width < copy_len) ? 0 : min_width - copy_len; - PRINTF_DEBUG(("sprintf: appendstring(%x, %d, %d, \"%s\", %d, '%c', %d)\n", + PRINTF_DEBUG(("sprintf: appendstring(%p, %zu, %zu, \"%s\", %zu, '%c', %zu)\n", *buffer, *pos, ZSTR_LEN(*buffer), add, min_width, padding, alignment)); m_width = MAX(min_width, copy_len); @@ -111,7 +111,7 @@ php_sprintf_appendstring(zend_string **buffer, size_t *pos, char *add, } size <<= 1; } - PRINTF_DEBUG(("sprintf ereallocing buffer to %d bytes\n", size)); + PRINTF_DEBUG(("sprintf ereallocing buffer to %zu bytes\n", size)); *buffer = zend_string_extend(*buffer, size, 0); } if (alignment == ALIGN_RIGHT) { @@ -146,8 +146,8 @@ php_sprintf_appendint(zend_string **buffer, size_t *pos, zend_long number, zend_ulong magn, nmagn; unsigned int i = NUM_BUF_SIZE - 1, neg = 0; - PRINTF_DEBUG(("sprintf: appendint(%x, %x, %x, %d, %d, '%c', %d)\n", - *buffer, pos, &ZSTR_LEN(*buffer), number, width, padding, alignment)); + PRINTF_DEBUG(("sprintf: appendint(%p, %zu, %zu, " ZEND_LONG_FMT ", %zu, '%c', %zu)\n", + *buffer, *pos, ZSTR_LEN(*buffer), number, width, padding, alignment)); if (number < 0) { neg = 1; magn = ((zend_ulong) -(number + 1)) + 1; @@ -172,7 +172,7 @@ php_sprintf_appendint(zend_string **buffer, size_t *pos, zend_long number, } else if (always_sign) { numbuf[--i] = '+'; } - PRINTF_DEBUG(("sprintf: appending %d as \"%s\", i=%d\n", + PRINTF_DEBUG(("sprintf: appending " ZEND_LONG_FMT " as \"%s\", i=%u\n", number, &numbuf[i], i)); php_sprintf_appendstring(buffer, pos, &numbuf[i], width, 0, padding, alignment, (NUM_BUF_SIZE - 1) - i, @@ -190,8 +190,8 @@ php_sprintf_appenduint(zend_string **buffer, size_t *pos, zend_ulong magn, nmagn; unsigned int i = NUM_BUF_SIZE - 1; - PRINTF_DEBUG(("sprintf: appenduint(%x, %x, %x, %d, %d, '%c', %d)\n", - *buffer, pos, &ZSTR_LEN(*buffer), number, width, padding, alignment)); + PRINTF_DEBUG(("sprintf: appenduint(%p, %zu, %zu, " ZEND_LONG_FMT ", %zu, '%c', %zu)\n", + *buffer, *pos, ZSTR_LEN(*buffer), number, width, padding, alignment)); magn = (zend_ulong) number; /* Can't right-pad 0's on integers */ @@ -206,7 +206,7 @@ php_sprintf_appenduint(zend_string **buffer, size_t *pos, magn = nmagn; } while (magn > 0 && i > 0); - PRINTF_DEBUG(("sprintf: appending %d as \"%s\", i=%d\n", number, &numbuf[i], i)); + PRINTF_DEBUG(("sprintf: appending " ZEND_LONG_FMT " as \"%s\", i=%d\n", number, &numbuf[i], i)); php_sprintf_appendstring(buffer, pos, &numbuf[i], width, 0, padding, alignment, (NUM_BUF_SIZE - 1) - i, /* neg */ false, 0, 0); } @@ -232,8 +232,8 @@ php_sprintf_appenddouble(zend_string **buffer, size_t *pos, struct lconv *lconv; #endif - PRINTF_DEBUG(("sprintf: appenddouble(%x, %x, %x, %f, %d, '%c', %d, %c)\n", - *buffer, pos, &ZSTR_LEN(*buffer), number, width, padding, alignment, fmt)); + PRINTF_DEBUG(("sprintf: appenddouble(%p, %zu, %zu, %f, %zu, '%c', %zu, %c)\n", + *buffer, *pos, ZSTR_LEN(*buffer), number, width, padding, alignment, fmt)); if ((adjust & ADJ_PRECISION) == 0) { precision = FLOAT_PRECISION; } else if (precision > MAX_FLOAT_PRECISION) { @@ -330,8 +330,8 @@ php_sprintf_append2n(zend_string **buffer, size_t *pos, zend_long number, zend_ulong i = NUM_BUF_SIZE - 1; int andbits = (1 << n) - 1; - PRINTF_DEBUG(("sprintf: append2n(%x, %x, %x, %d, %d, '%c', %d, %d, %x)\n", - *buffer, pos, &ZSTR_LEN(*buffer), number, width, padding, alignment, n, + PRINTF_DEBUG(("sprintf: append2n(%p, %zu, %zu, " ZEND_LONG_FMT ", %zu, '%c', %zu, %d, %p)\n", + *buffer, *pos, ZSTR_LEN(*buffer), number, width, padding, alignment, n, chartable)); PRINTF_DEBUG(("sprintf: append2n 2^%d andbits=%x\n", n, andbits)); @@ -363,7 +363,7 @@ php_sprintf_getnumber(char **buffer, size_t *len) *len -= i; *buffer = endptr; } - PRINTF_DEBUG(("sprintf_getnumber: number was %d bytes long\n", i)); + PRINTF_DEBUG(("sprintf_getnumber: number was %zu bytes long\n", i)); if (num >= INT_MAX || num < 0) { return -1; @@ -431,6 +431,10 @@ php_formatted_print(char *format, size_t format_len, zval *args, int argc, int n int always_sign; int max_missing_argnum = -1; + /* For debugging */ + const char *format_orig = format; + ZEND_IGNORE_VALUE(format_orig); + result = zend_string_alloc(size, 0); currarg = 0; @@ -464,8 +468,8 @@ php_formatted_print(char *format, size_t format_len, zval *args, int argc, int n always_sign = 0; expprec = 0; - PRINTF_DEBUG(("sprintf: first looking at '%c', inpos=%d\n", - *format, format - Z_STRVAL_P(z_format))); + PRINTF_DEBUG(("sprintf: first looking at '%c', inpos=%zu\n", + *format, format - format_orig)); if (isalpha((int)*format)) { width = precision = 0; argnum = ARG_NUM_NEXT; @@ -478,8 +482,8 @@ php_formatted_print(char *format, size_t format_len, zval *args, int argc, int n /* after argnum comes modifiers */ PRINTF_DEBUG(("sprintf: looking for modifiers\n" - "sprintf: now looking at '%c', inpos=%d\n", - *format, format - Z_STRVAL_P(z_format))); + "sprintf: now looking at '%c', inpos=%zu\n", + *format, format - format_orig)); for (;; format++, format_len--) { if (*format == ' ' || *format == '0') { padding = *format; From 799ec7b8c50440f851d823d5c4b68f430234149b Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sun, 22 Jun 2025 16:29:19 +0200 Subject: [PATCH 15/69] Fix misleading errors in printf() The precision and width _can_ be zero. Closes GH-18911. --- NEWS | 3 +++ ext/standard/formatted_print.c | 6 +++--- ext/standard/tests/strings/sprintf_star.phpt | 16 +++++++++++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index fd344ee94c72..101020969624 100644 --- a/NEWS +++ b/NEWS @@ -20,6 +20,9 @@ PHP NEWS - MbString: . Fixed bug GH-18901 (integer overflow mb_split). (nielsdos) +- Standard: + . Fix misleading errors in printf(). (nielsdos) + - Streams: . Fixed GH-13264 (fgets() and stream_get_line() do not return false on filter fatal error). (Jakub Zelenka) diff --git a/ext/standard/formatted_print.c b/ext/standard/formatted_print.c index 00445d3cca7c..1ff0f36212bb 100644 --- a/ext/standard/formatted_print.c +++ b/ext/standard/formatted_print.c @@ -534,7 +534,7 @@ php_formatted_print(char *format, size_t format_len, zval *args, int argc, int n goto fail; } if (Z_LVAL_P(tmp) < 0 || Z_LVAL_P(tmp) > INT_MAX) { - zend_value_error("Width must be greater than zero and less than %d", INT_MAX); + zend_value_error("Width must be between 0 and %d", INT_MAX); goto fail; } width = Z_LVAL_P(tmp); @@ -542,7 +542,7 @@ php_formatted_print(char *format, size_t format_len, zval *args, int argc, int n } else if (isdigit((int)*format)) { PRINTF_DEBUG(("sprintf: getting width\n")); if ((width = php_sprintf_getnumber(&format, &format_len)) < 0) { - zend_value_error("Width must be greater than zero and less than %d", INT_MAX); + zend_value_error("Width must be between 0 and %d", INT_MAX); goto fail; } adjusting |= ADJ_WIDTH; @@ -586,7 +586,7 @@ php_formatted_print(char *format, size_t format_len, zval *args, int argc, int n expprec = 1; } else if (isdigit((int)*format)) { if ((precision = php_sprintf_getnumber(&format, &format_len)) < 0) { - zend_value_error("Precision must be greater than zero and less than %d", INT_MAX); + zend_value_error("Precision must be between 0 and %d", INT_MAX); goto fail; } adjusting |= ADJ_PRECISION; diff --git a/ext/standard/tests/strings/sprintf_star.phpt b/ext/standard/tests/strings/sprintf_star.phpt index 0e3e16c32642..0c8a211e5c43 100644 --- a/ext/standard/tests/strings/sprintf_star.phpt +++ b/ext/standard/tests/strings/sprintf_star.phpt @@ -62,6 +62,18 @@ try { echo $e->getMessage(), "\n"; } +try { + printf("%9999999999999999999999.f\n", $f); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} + +try { + printf("%.9999999999999999999999f\n", $f); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} + ?> --EXPECT-- float(1.2345678901234567) @@ -95,4 +107,6 @@ foo Precision must be an integer Precision must be between -1 and 2147483647 Precision -1 is only supported for %g, %G, %h and %H -Width must be greater than zero and less than 2147483647 +Width must be between 0 and 2147483647 +Width must be between 0 and 2147483647 +Precision must be between 0 and 2147483647 From 8e731ca622bfc2cf26375c950fdd58ccae2f999f Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sun, 22 Jun 2025 20:18:55 +0200 Subject: [PATCH 16/69] Fix GH-18639: Internal class aliases can break preloading + JIT ZEND_FUNC_INFO() can not be used on internal CE's. If preloading makes a CE that's an alias of an internal class, the invalid access happens when setting the FUNC_INFO. While we could check the class type to be of user code, we can just skip aliases altogether anyway which may be faster. Closes GH-18915. --- NEWS | 4 ++++ ext/opcache/jit/zend_jit.c | 10 +++++++++- ext/opcache/tests/gh18639.phpt | 20 ++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 ext/opcache/tests/gh18639.phpt diff --git a/NEWS b/NEWS index 101020969624..b453445854f9 100644 --- a/NEWS +++ b/NEWS @@ -20,6 +20,10 @@ PHP NEWS - MbString: . Fixed bug GH-18901 (integer overflow mb_split). (nielsdos) +- Opcache: + . Fixed bug GH-18639 (Internal class aliases can break preloading + JIT). + (nielsdos) + - Standard: . Fix misleading errors in printf(). (nielsdos) diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index 9adfe1719f62..7caa3387016e 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -4628,8 +4628,16 @@ ZEND_EXT_API int zend_jit_script(zend_script *script) || JIT_G(trigger) == ZEND_JIT_ON_HOT_TRACE) { zend_class_entry *ce; zend_op_array *op_array; + zval *zv; + + ZEND_HASH_MAP_FOREACH_VAL(&script->class_table, zv) { + if (Z_TYPE_P(zv) == IS_ALIAS_PTR) { + continue; + } + + ce = Z_PTR_P(zv); + ZEND_ASSERT(ce->type == ZEND_USER_CLASS); - ZEND_HASH_MAP_FOREACH_PTR(&script->class_table, ce) { ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, op_array) { if (!ZEND_FUNC_INFO(op_array)) { void *jit_extension = zend_shared_alloc_get_xlat_entry(op_array->opcodes); diff --git a/ext/opcache/tests/gh18639.phpt b/ext/opcache/tests/gh18639.phpt new file mode 100644 index 000000000000..28424032931a --- /dev/null +++ b/ext/opcache/tests/gh18639.phpt @@ -0,0 +1,20 @@ +--TEST-- +GH-18639 (Internal class aliases can break preloading + JIT) +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=1255 +opcache.jit_buffer_size=16M +opcache.preload={PWD}/preload_gh18567.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + +--EXPECT-- +ok From 56c4ddfaf62ff3935029847bb6fb44768f4b9452 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sun, 22 Jun 2025 09:43:08 +0200 Subject: [PATCH 17/69] Fix GH-18899: JIT function crash when emitting undefined variable warning and opline is not set yet The crash happens because EX(opline) is attempted to be accessed but it's not set yet. Closes GH-18904. --- NEWS | 2 ++ ext/opcache/jit/zend_jit_ir.c | 2 ++ ext/opcache/tests/jit/gh18899.phpt | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 ext/opcache/tests/jit/gh18899.phpt diff --git a/NEWS b/NEWS index 2e969e0830f4..f71edaedb61c 100644 --- a/NEWS +++ b/NEWS @@ -24,6 +24,8 @@ PHP NEWS - Opcache: . Fixed bug GH-18639 (Internal class aliases can break preloading + JIT). (nielsdos) + . Fixed bug GH-18899 (JIT function crash when emitting undefined variable + warning and opline is not set yet). (nielsdos) - Standard: . Fix misleading errors in printf(). (nielsdos) diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 74fad38ffee8..6afd768321cb 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -5981,6 +5981,7 @@ static int zend_jit_long_math_helper(zend_jit_ctx *jit, ir_IF_FALSE_cold(if_def); // zend_error_unchecked(E_WARNING, "Undefined variable $%S", CV_DEF_OF(EX_VAR_TO_NUM(opline->op1.var))); + jit_SET_EX_OPLINE(jit, opline); ir_CALL_1(IR_VOID, ir_CONST_FC_FUNC(zend_jit_undefined_op_helper), ir_CONST_U32(opline->op1.var)); ref2 = jit_EG(uninitialized_zval); @@ -5997,6 +5998,7 @@ static int zend_jit_long_math_helper(zend_jit_ctx *jit, ir_IF_FALSE_cold(if_def); // zend_error_unchecked(E_WARNING, "Undefined variable $%S", CV_DEF_OF(EX_VAR_TO_NUM(opline->op2.var))); + jit_SET_EX_OPLINE(jit, opline); ir_CALL_1(IR_VOID, ir_CONST_FC_FUNC(zend_jit_undefined_op_helper), ir_CONST_U32(opline->op2.var)); ref2 = jit_EG(uninitialized_zval); diff --git a/ext/opcache/tests/jit/gh18899.phpt b/ext/opcache/tests/jit/gh18899.phpt new file mode 100644 index 000000000000..47c9a3e1ae37 --- /dev/null +++ b/ext/opcache/tests/jit/gh18899.phpt @@ -0,0 +1,21 @@ +--TEST-- +GH-18899 (JIT function crash when emitting undefined variable warning and opline is not set yet) +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=1205 +opcache.jit_buffer_size=8M +--FILE-- +>= 8; + } +} +str_repeat("A",232).ptr2str(); +?> +--EXPECTF-- +Warning: Undefined variable $ptr in %s on line %d From 1e3d92f8a95c17d0fb19c11e17a0cd8c13c18309 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sun, 22 Jun 2025 20:28:02 +0200 Subject: [PATCH 18/69] Fix GH-14082: Segmentation fault on unknown address 0x600000000018 in ext/opcache/jit/zend_jit.c During persisting, the JIT may trigger and fill in the call graph. The call graph info is allocated on the arena which will be gone after preloading. To prevent invalid accesses during normal requests, the arena data should be cleared. This has to be done after all scripts have been persisted because shared op arrays between scripts can change the call graph. Closes GH-18916. --- NEWS | 2 + ext/opcache/ZendAccelerator.c | 47 +++++++++++++++++++++++ ext/opcache/tests/jit/gh14082.phpt | 23 +++++++++++ ext/opcache/tests/jit/preload_gh14082.inc | 9 +++++ 4 files changed, 81 insertions(+) create mode 100644 ext/opcache/tests/jit/gh14082.phpt create mode 100644 ext/opcache/tests/jit/preload_gh14082.inc diff --git a/NEWS b/NEWS index b453445854f9..34866abfb21d 100644 --- a/NEWS +++ b/NEWS @@ -23,6 +23,8 @@ PHP NEWS - Opcache: . Fixed bug GH-18639 (Internal class aliases can break preloading + JIT). (nielsdos) + . Fixed bug GH-14082 (Segmentation fault on unknown address 0x600000000018 + in ext/opcache/jit/zend_jit.c). (nielsdos) - Standard: . Fix misleading errors in printf(). (nielsdos) diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index bd6b323871bc..1b0101dbfd6c 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -52,6 +52,8 @@ #ifdef HAVE_JIT # include "jit/zend_jit.h" +# include "Optimizer/zend_func_info.h" +# include "Optimizer/zend_call_graph.h" #endif #ifndef ZEND_WIN32 @@ -4363,6 +4365,39 @@ static void preload_load(void) } } +#if HAVE_JIT +static void zend_accel_clear_call_graph_ptrs(zend_op_array *op_array) +{ + ZEND_ASSERT(ZEND_USER_CODE(op_array->type)); + zend_func_info *info = ZEND_FUNC_INFO(op_array); + if (info) { + info->caller_info = NULL; + info->callee_info = NULL; + } +} + +static void accel_reset_arena_info(zend_persistent_script *script) +{ + zend_op_array *op_array; + zend_class_entry *ce; + + zend_accel_clear_call_graph_ptrs(&script->script.main_op_array); + ZEND_HASH_MAP_FOREACH_PTR(&script->script.function_table, op_array) { + zend_accel_clear_call_graph_ptrs(op_array); + } ZEND_HASH_FOREACH_END(); + ZEND_HASH_MAP_FOREACH_PTR(&script->script.class_table, ce) { + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, op_array) { + if (op_array->scope == ce + && op_array->type == ZEND_USER_FUNCTION + && !(op_array->fn_flags & ZEND_ACC_ABSTRACT) + && !(op_array->fn_flags & ZEND_ACC_TRAIT_CLONE)) { + zend_accel_clear_call_graph_ptrs(op_array); + } + } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); +} +#endif + static zend_result accel_preload(const char *config, bool in_child) { zend_file_handle file_handle; @@ -4568,6 +4603,18 @@ static zend_result accel_preload(const char *config, bool in_child) } ZEND_HASH_FOREACH_END(); ZCSG(saved_scripts)[i] = NULL; +#if HAVE_JIT + /* During persisting, the JIT may trigger and fill in the call graph. + * The call graph info is allocated on the arena which will be gone after preloading. + * To prevent invalid accesses during normal requests, the arena data should be cleared. + * This has to be done after all scripts have been persisted because shared op arrays between + * scripts can change the call graph. */ + accel_reset_arena_info(ZCSG(preload_script)); + for (zend_persistent_script **scripts = ZCSG(saved_scripts); *scripts; scripts++) { + accel_reset_arena_info(*scripts); + } +#endif + zend_shared_alloc_save_state(); accel_interned_strings_save_state(); diff --git a/ext/opcache/tests/jit/gh14082.phpt b/ext/opcache/tests/jit/gh14082.phpt new file mode 100644 index 000000000000..eba67a096b82 --- /dev/null +++ b/ext/opcache/tests/jit/gh14082.phpt @@ -0,0 +1,23 @@ +--TEST-- +GH-14082 (Segmentation fault on unknown address 0x600000000018 in ext/opcache/jit/zend_jit.c) +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=1235 +opcache.jit_buffer_size=16M +opcache.preload={PWD}/preload_gh14082.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + +--EXPECT-- +int(1) +int(1) +ok diff --git a/ext/opcache/tests/jit/preload_gh14082.inc b/ext/opcache/tests/jit/preload_gh14082.inc new file mode 100644 index 000000000000..f5b11ac621eb --- /dev/null +++ b/ext/opcache/tests/jit/preload_gh14082.inc @@ -0,0 +1,9 @@ + Date: Thu, 10 Apr 2025 15:15:36 +0200 Subject: [PATCH 19/69] Fix GHSA-3cr5-j632-f35r: Null byte in hostnames This fixes stream_socket_client() and fsockopen(). Specifically it adds a check to parse_ip_address_ex and it also makes sure that the \0 is not ignored in fsockopen() hostname formatting. --- ext/standard/fsock.c | 27 +++++++++++++++++-- .../tests/network/ghsa-3cr5-j632-f35r.phpt | 21 +++++++++++++++ .../tests/streams/ghsa-3cr5-j632-f35r.phpt | 26 ++++++++++++++++++ main/streams/xp_socket.c | 9 ++++--- 4 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt create mode 100644 ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt diff --git a/ext/standard/fsock.c b/ext/standard/fsock.c index 9e1a53c0ec2a..67c68468f514 100644 --- a/ext/standard/fsock.c +++ b/ext/standard/fsock.c @@ -23,6 +23,28 @@ #include "php_network.h" #include "file.h" +static size_t php_fsockopen_format_host_port(char **message, const char *prefix, size_t prefix_len, + const char *host, size_t host_len, zend_long port) +{ + char portbuf[32]; + int portlen = snprintf(portbuf, sizeof(portbuf), ":" ZEND_LONG_FMT, port); + size_t total_len = prefix_len + host_len + portlen; + + char *result = emalloc(total_len + 1); + + if (prefix_len > 0) { + memcpy(result, prefix, prefix_len); + } + memcpy(result + prefix_len, host, host_len); + memcpy(result + prefix_len + host_len, portbuf, portlen); + + result[total_len] = '\0'; + + *message = result; + + return total_len; +} + /* {{{ php_fsockopen() */ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) @@ -62,11 +84,12 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) } if (persistent) { - spprintf(&hashkey, 0, "pfsockopen__%s:" ZEND_LONG_FMT, host, port); + php_fsockopen_format_host_port(&hashkey, "pfsockopen__", strlen("pfsockopen__"), host, + host_len, port); } if (port > 0) { - hostname_len = spprintf(&hostname, 0, "%s:" ZEND_LONG_FMT, host, port); + hostname_len = php_fsockopen_format_host_port(&hostname, "", 0, host, host_len, port); } else { hostname_len = host_len; hostname = host; diff --git a/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt b/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt new file mode 100644 index 000000000000..7556c3be94cc --- /dev/null +++ b/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt @@ -0,0 +1,21 @@ +--TEST-- +GHSA-3cr5-j632-f35r: Null byte termination in fsockopen() +--FILE-- + +--EXPECTF-- + +Warning: fsockopen(): Unable to connect to localhost:%d (The hostname must not contain null bytes) in %s +bool(false) diff --git a/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt b/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt new file mode 100644 index 000000000000..52f9263c99aa --- /dev/null +++ b/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt @@ -0,0 +1,26 @@ +--TEST-- +GHSA-3cr5-j632-f35r: Null byte termination in stream_socket_client() +--FILE-- + +--EXPECTF-- + +Warning: stream_socket_client(): Unable to connect to tcp://localhost\0.example.com:%d (The hostname must not contain null bytes) in %s +bool(false) diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index b17eccf2eeb1..38f11d149dea 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -581,12 +581,15 @@ static inline char *parse_ip_address_ex(const char *str, size_t str_len, int *po char *colon; char *host = NULL; -#ifdef HAVE_IPV6 - char *p; + if (memchr(str, '\0', str_len)) { + *err = ZSTR_INIT_LITERAL("The hostname must not contain null bytes", 0); + return NULL; + } +#ifdef HAVE_IPV6 if (*(str) == '[' && str_len > 1) { /* IPV6 notation to specify raw address with port (i.e. [fe80::1]:80) */ - p = memchr(str + 1, ']', str_len - 2); + char *p = memchr(str + 1, ']', str_len - 2); if (!p || *(p + 1) != ':') { if (get_err) { *err = strpprintf(0, "Failed to parse IPv6 address \"%s\"", str); From 9376aeef9f8ff81f2705b8016237ec3e30bdee44 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 4 Mar 2025 17:23:01 +0100 Subject: [PATCH 20/69] Fix GHSA-hrwm-9436-5mv3: pgsql escaping no error checks This adds error checks for escape function is pgsql and pdo_pgsql extensions. It prevents possibility of storing not properly escaped data which could potentially lead to some security issues. --- ext/pdo_pgsql/pgsql_driver.c | 10 +- ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt | 24 ++++ ext/pgsql/pgsql.c | 126 ++++++++++++++++--- ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt | 64 ++++++++++ 4 files changed, 203 insertions(+), 21 deletions(-) create mode 100644 ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt create mode 100644 ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c index f84bfba9f845..2ea29490856b 100644 --- a/ext/pdo_pgsql/pgsql_driver.c +++ b/ext/pdo_pgsql/pgsql_driver.c @@ -354,11 +354,15 @@ static zend_string* pgsql_handle_quoter(pdo_dbh_t *dbh, const zend_string *unquo zend_string *quoted_str; pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data; size_t tmp_len; + int err; switch (paramtype) { case PDO_PARAM_LOB: /* escapedlen returned by PQescapeBytea() accounts for trailing 0 */ escaped = PQescapeByteaConn(H->server, (unsigned char *)ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), &tmp_len); + if (escaped == NULL) { + return NULL; + } quotedlen = tmp_len + 1; quoted = emalloc(quotedlen + 1); memcpy(quoted+1, escaped, quotedlen-2); @@ -370,7 +374,11 @@ static zend_string* pgsql_handle_quoter(pdo_dbh_t *dbh, const zend_string *unquo default: quoted = safe_emalloc(2, ZSTR_LEN(unquoted), 3); quoted[0] = '\''; - quotedlen = PQescapeStringConn(H->server, quoted + 1, ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), NULL); + quotedlen = PQescapeStringConn(H->server, quoted + 1, ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), &err); + if (err) { + efree(quoted); + return NULL; + } quoted[quotedlen + 1] = '\''; quoted[quotedlen + 2] = '\0'; quotedlen += 2; diff --git a/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt b/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt new file mode 100644 index 000000000000..8566a26753b4 --- /dev/null +++ b/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt @@ -0,0 +1,24 @@ +--TEST-- +#GHSA-hrwm-9436-5mv3: pdo_pgsql extension does not check for errors during escaping +--EXTENSIONS-- +pdo +pdo_pgsql +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +$invalid = "ABC\xff\x30';"; +var_dump($db->quote($invalid)); + +?> +--EXPECT-- +bool(false) diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c index 9f04f2c843b9..01fb9dde3bae 100644 --- a/ext/pgsql/pgsql.c +++ b/ext/pgsql/pgsql.c @@ -3297,8 +3297,14 @@ PHP_FUNCTION(pg_escape_string) to = zend_string_safe_alloc(ZSTR_LEN(from), 2, 0, 0); if (link) { + int err; pgsql = link->conn; - ZSTR_LEN(to) = PQescapeStringConn(pgsql, ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from), NULL); + ZSTR_LEN(to) = PQescapeStringConn(pgsql, ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from), &err); + if (err) { + zend_argument_value_error(ZEND_NUM_ARGS(), "Escaping string failed"); + zend_string_efree(to); + RETURN_THROWS(); + } } else { ZSTR_LEN(to) = PQescapeString(ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from)); @@ -3341,6 +3347,10 @@ PHP_FUNCTION(pg_escape_bytea) } else { to = (char *)PQescapeBytea((unsigned char *)ZSTR_VAL(from), ZSTR_LEN(from), &to_len); } + if (to == NULL) { + zend_argument_value_error(ZEND_NUM_ARGS(), "Escape failure"); + RETURN_THROWS(); + } RETVAL_STRINGL(to, to_len-1); /* to_len includes additional '\0' */ PQfreemem(to); @@ -4257,7 +4267,7 @@ PHP_PGSQL_API zend_result php_pgsql_meta_data(PGconn *pg_link, const zend_string char *escaped; smart_str querystr = {0}; size_t new_len; - int i, num_rows; + int i, num_rows, err; zval elem; ZEND_ASSERT(ZSTR_LEN(table_name) != 0); @@ -4296,7 +4306,14 @@ PHP_PGSQL_API zend_result php_pgsql_meta_data(PGconn *pg_link, const zend_string "WHERE a.attnum > 0 AND c.relname = '"); } escaped = (char *)safe_emalloc(strlen(tmp_name2), 2, 1); - new_len = PQescapeStringConn(pg_link, escaped, tmp_name2, strlen(tmp_name2), NULL); + new_len = PQescapeStringConn(pg_link, escaped, tmp_name2, strlen(tmp_name2), &err); + if (err) { + php_error_docref(NULL, E_WARNING, "Escaping table name '%s' failed", ZSTR_VAL(table_name)); + efree(src); + efree(escaped); + smart_str_free(&querystr); + return FAILURE; + } if (new_len) { smart_str_appendl(&querystr, escaped, new_len); } @@ -4304,7 +4321,14 @@ PHP_PGSQL_API zend_result php_pgsql_meta_data(PGconn *pg_link, const zend_string smart_str_appends(&querystr, "' AND n.nspname = '"); escaped = (char *)safe_emalloc(strlen(tmp_name), 2, 1); - new_len = PQescapeStringConn(pg_link, escaped, tmp_name, strlen(tmp_name), NULL); + new_len = PQescapeStringConn(pg_link, escaped, tmp_name, strlen(tmp_name), &err); + if (err) { + php_error_docref(NULL, E_WARNING, "Escaping table namespace '%s' failed", ZSTR_VAL(table_name)); + efree(src); + efree(escaped); + smart_str_free(&querystr); + return FAILURE; + } if (new_len) { smart_str_appendl(&querystr, escaped, new_len); } @@ -4565,7 +4589,7 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * { zend_string *field = NULL; zval meta, *def, *type, *not_null, *has_default, *is_enum, *val, new_val; - int err = 0, skip_field; + int err = 0, escape_err = 0, skip_field; php_pgsql_data_type data_type; ZEND_ASSERT(pg_link != NULL); @@ -4818,8 +4842,13 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * /* PostgreSQL ignores \0 */ str = zend_string_alloc(Z_STRLEN_P(val) * 2, 0); /* better to use PGSQLescapeLiteral since PGescapeStringConn does not handle special \ */ - ZSTR_LEN(str) = PQescapeStringConn(pg_link, ZSTR_VAL(str), Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); - ZVAL_STR(&new_val, php_pgsql_add_quotes(str)); + ZSTR_LEN(str) = PQescapeStringConn(pg_link, ZSTR_VAL(str), + Z_STRVAL_P(val), Z_STRLEN_P(val), &escape_err); + if (escape_err) { + err = 1; + } else { + ZVAL_STR(&new_val, php_pgsql_add_quotes(str)); + } zend_string_release_ex(str, false); } break; @@ -4842,7 +4871,15 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * } PGSQL_CONV_CHECK_IGNORE(); if (err) { - php_error_docref(NULL, E_NOTICE, "Expects NULL, string, long or double value for PostgreSQL '%s' (%s)", Z_STRVAL_P(type), ZSTR_VAL(field)); + if (escape_err) { + php_error_docref(NULL, E_NOTICE, + "String value escaping failed for PostgreSQL '%s' (%s)", + Z_STRVAL_P(type), ZSTR_VAL(field)); + } else { + php_error_docref(NULL, E_NOTICE, + "Expects NULL, string, long or double value for PostgreSQL '%s' (%s)", + Z_STRVAL_P(type), ZSTR_VAL(field)); + } } break; @@ -5113,6 +5150,11 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * zend_string *tmp_zstr; tmp = PQescapeByteaConn(pg_link, (unsigned char *)Z_STRVAL_P(val), Z_STRLEN_P(val), &to_len); + if (tmp == NULL) { + php_error_docref(NULL, E_NOTICE, "Escaping value failed for %s field (%s)", Z_STRVAL_P(type), ZSTR_VAL(field)); + err = 1; + break; + } tmp_zstr = zend_string_init((char *)tmp, to_len - 1, false); /* PQescapeBytea's to_len includes additional '\0' */ PQfreemem(tmp); @@ -5191,6 +5233,12 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * zend_hash_update(Z_ARRVAL_P(result), field, &new_val); } else { char *escaped = PQescapeIdentifier(pg_link, ZSTR_VAL(field), ZSTR_LEN(field)); + if (escaped == NULL) { + /* This cannot fail because of invalid string but only due to failed memory allocation */ + php_error_docref(NULL, E_NOTICE, "Escaping field '%s' failed", ZSTR_VAL(field)); + err = 1; + break; + } add_assoc_zval(result, escaped, &new_val); PQfreemem(escaped); } @@ -5269,7 +5317,7 @@ static bool do_exec(smart_str *querystr, ExecStatusType expect, PGconn *pg_link, } /* }}} */ -static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const zend_string *table) /* {{{ */ +static inline zend_result build_tablename(smart_str *querystr, PGconn *pg_link, const zend_string *table) /* {{{ */ { /* schema.table should be "schema"."table" */ const char *dot = memchr(ZSTR_VAL(table), '.', ZSTR_LEN(table)); @@ -5279,6 +5327,10 @@ static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const z smart_str_appendl(querystr, ZSTR_VAL(table), len); } else { char *escaped = PQescapeIdentifier(pg_link, ZSTR_VAL(table), len); + if (escaped == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape table name '%s'", ZSTR_VAL(table)); + return FAILURE; + } smart_str_appends(querystr, escaped); PQfreemem(escaped); } @@ -5291,11 +5343,17 @@ static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const z smart_str_appendl(querystr, after_dot, len); } else { char *escaped = PQescapeIdentifier(pg_link, after_dot, len); + if (escaped == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape table name '%s'", ZSTR_VAL(table)); + return FAILURE; + } smart_str_appendc(querystr, '.'); smart_str_appends(querystr, escaped); PQfreemem(escaped); } } + + return SUCCESS; } /* }}} */ @@ -5316,7 +5374,9 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t ZVAL_UNDEF(&converted); if (zend_hash_num_elements(Z_ARRVAL_P(var_array)) == 0) { smart_str_appends(&querystr, "INSERT INTO "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " DEFAULT VALUES"); goto no_values; @@ -5332,7 +5392,9 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "INSERT INTO "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " ("); ZEND_HASH_FOREACH_STR_KEY(Z_ARRVAL_P(var_array), fld) { @@ -5342,6 +5404,10 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t } if (opt & PGSQL_DML_ESCAPE) { tmp = PQescapeIdentifier(pg_link, ZSTR_VAL(fld), ZSTR_LEN(fld) + 1); + if (tmp == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s'", ZSTR_VAL(fld)); + goto cleanup; + } smart_str_appends(&querystr, tmp); PQfreemem(tmp); } else { @@ -5353,15 +5419,19 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t smart_str_appends(&querystr, ") VALUES ("); /* make values string */ - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(var_array), val) { + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(var_array), fld, val) { /* we can avoid the key_type check here, because we tested it in the other loop */ switch (Z_TYPE_P(val)) { case IS_STRING: if (opt & PGSQL_DML_ESCAPE) { - size_t new_len; - char *tmp; - tmp = (char *)safe_emalloc(Z_STRLEN_P(val), 2, 1); - new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); + int error; + char *tmp = safe_emalloc(Z_STRLEN_P(val), 2, 1); + size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), &error); + if (error) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s' value", ZSTR_VAL(fld)); + efree(tmp); + goto cleanup; + } smart_str_appendc(&querystr, '\''); smart_str_appendl(&querystr, tmp, new_len); smart_str_appendc(&querystr, '\''); @@ -5517,6 +5587,10 @@ static inline int build_assignment_string(PGconn *pg_link, smart_str *querystr, } if (opt & PGSQL_DML_ESCAPE) { char *tmp = PQescapeIdentifier(pg_link, ZSTR_VAL(fld), ZSTR_LEN(fld) + 1); + if (tmp == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s'", ZSTR_VAL(fld)); + return -1; + } smart_str_appends(querystr, tmp); PQfreemem(tmp); } else { @@ -5532,8 +5606,14 @@ static inline int build_assignment_string(PGconn *pg_link, smart_str *querystr, switch (Z_TYPE_P(val)) { case IS_STRING: if (opt & PGSQL_DML_ESCAPE) { + int error; char *tmp = (char *)safe_emalloc(Z_STRLEN_P(val), 2, 1); - size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); + size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), &error); + if (error) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s' value", ZSTR_VAL(fld)); + efree(tmp); + return -1; + } smart_str_appendc(querystr, '\''); smart_str_appendl(querystr, tmp, new_len); smart_str_appendc(querystr, '\''); @@ -5601,7 +5681,9 @@ PHP_PGSQL_API zend_result php_pgsql_update(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "UPDATE "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " SET "); if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(var_array), 0, ",", 1, opt)) @@ -5704,7 +5786,9 @@ PHP_PGSQL_API zend_result php_pgsql_delete(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "DELETE FROM "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " WHERE "); if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(ids_array), 1, " AND ", sizeof(" AND ")-1, opt)) @@ -5844,7 +5928,9 @@ PHP_PGSQL_API zend_result php_pgsql_select(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "SELECT * FROM "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " WHERE "); if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(ids_array), 1, " AND ", sizeof(" AND ")-1, opt)) diff --git a/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt b/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt new file mode 100644 index 000000000000..c1c5e05dce62 --- /dev/null +++ b/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt @@ -0,0 +1,64 @@ +--TEST-- +#GHSA-hrwm-9436-5mv3: pgsql extension does not check for errors during escaping +--EXTENSIONS-- +pgsql +--SKIPIF-- + +--FILE-- + 'test'])); // table name str escape in php_pgsql_meta_data +var_dump(pg_insert($db, "$invalid.tbl", ['bar' => 'test'])); // schema name str escape in php_pgsql_meta_data +var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', ['bar' => $invalid])); // converted value str escape in php_pgsql_convert +var_dump(pg_insert($db, $invalid, [])); // ident escape in build_tablename +var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', [$invalid => 'foo'], $flags)); // ident escape for field php_pgsql_insert +var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', ['bar' => $invalid], $flags)); // str escape for field value in php_pgsql_insert +var_dump(pg_update($db, 'ghsa_hrmw_9436_5mv3', ['bar' => 'val'], [$invalid => 'test'], $flags)); // ident escape in build_assignment_string +var_dump(pg_update($db, 'ghsa_hrmw_9436_5mv3', ['bar' => 'val'], ['bar' => $invalid], $flags)); // invalid str escape in build_assignment_string +var_dump(pg_escape_literal($db, $invalid)); // pg_escape_literal escape +var_dump(pg_escape_identifier($db, $invalid)); // pg_escape_identifier escape + +?> +--EXPECTF-- + +Warning: pg_insert(): Escaping table name 'ABC%s';' failed in %s on line %d +bool(false) + +Warning: pg_insert(): Escaping table namespace 'ABC%s';.tbl' failed in %s on line %d +bool(false) + +Notice: pg_insert(): String value escaping failed for PostgreSQL 'text' (bar) in %s on line %d +bool(false) + +Notice: pg_insert(): Failed to escape table name 'ABC%s';' in %s on line %d +bool(false) + +Notice: pg_insert(): Failed to escape field 'ABC%s';' in %s on line %d +bool(false) + +Notice: pg_insert(): Failed to escape field 'bar' value in %s on line %d +bool(false) + +Notice: pg_update(): Failed to escape field 'ABC%s';' in %s on line %d +bool(false) + +Notice: pg_update(): Failed to escape field 'bar' value in %s on line %d +bool(false) + +Warning: pg_escape_literal(): Failed to escape in %s on line %d +bool(false) + +Warning: pg_escape_identifier(): Failed to escape in %s on line %d +bool(false) From 6233dc6210b159762a97b7759ea0883d027feac1 Mon Sep 17 00:00:00 2001 From: Shivam Mathur Date: Wed, 25 Jun 2025 01:57:07 +0530 Subject: [PATCH 21/69] Switch to windows-2022 in CI (#18927) * Switch to windows-2022 in CI windows-2019 runner will be dropped by GitHub on 2025-06-30. * xfail test cases that fail on windows-2022 --- .github/scripts/windows/build.bat | 4 +- .github/scripts/windows/find-vs-toolset.bat | 49 +++++++++++++++++++++ .github/scripts/windows/test.bat | 3 +- .github/workflows/push.yml | 2 +- .github/workflows/root.yml | 2 +- Zend/tests/bug70258.phpt | 3 ++ Zend/tests/gh11189.phpt | 3 ++ Zend/tests/gh11189_1.phpt | 3 ++ 8 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 .github/scripts/windows/find-vs-toolset.bat diff --git a/.github/scripts/windows/build.bat b/.github/scripts/windows/build.bat index ebe08c86b5ea..139cce8be816 100644 --- a/.github/scripts/windows/build.bat +++ b/.github/scripts/windows/build.bat @@ -43,7 +43,9 @@ if not exist "%SDK_RUNNER%" ( exit /b 3 ) -cmd /c %SDK_RUNNER% -t .github\scripts\windows\build_task.bat +for /f "delims=" %%T in ('call .github\scripts\windows\find-vs-toolset.bat %PHP_BUILD_CRT%') do set "VS_TOOLSET=%%T" +echo Got VS Toolset %VS_TOOLSET% +cmd /c %SDK_RUNNER% -s %VS_TOOLSET% -t .github\scripts\windows\build_task.bat if %errorlevel% neq 0 exit /b 3 exit /b 0 diff --git a/.github/scripts/windows/find-vs-toolset.bat b/.github/scripts/windows/find-vs-toolset.bat new file mode 100644 index 000000000000..2d9e68e73031 --- /dev/null +++ b/.github/scripts/windows/find-vs-toolset.bat @@ -0,0 +1,49 @@ +@echo off + +setlocal enabledelayedexpansion + +if "%~1"=="" ( + echo ERROR: Usage: %~nx0 [vc14^|vc15^|vs16^|vs17] + exit /b 1 +) + +set "toolsets_vc14=14.0" +set "toolsets_vc15=" +set "toolsets_vs16=" +set "toolsets_vs17=" + + +for /f "usebackq tokens=*" %%I in (`vswhere.exe -latest -find "VC\Tools\MSVC"`) do set "MSVCDIR=%%I" + +if not defined MSVCDIR ( + echo ERROR: could not locate VC\Tools\MSVC + exit /b 1 +) + +for /f "delims=" %%D in ('dir /b /ad "%MSVCDIR%"') do ( + for /f "tokens=1,2 delims=." %%A in ("%%D") do ( + set "maj=%%A" & set "min=%%B" + if "!maj!"=="14" ( + if !min! LEQ 9 ( + set "toolsets_vc14=%%D" + ) else if !min! LEQ 19 ( + set "toolsets_vc15=%%D" + ) else if !min! LEQ 29 ( + set "toolsets_vs16=%%D" + ) else ( + set "toolsets_vs17=%%D" + ) + ) + ) +) + +set "KEY=%~1" +set "VAR=toolsets_%KEY%" +call set "RESULT=%%%VAR%%%" +if defined RESULT ( + echo %RESULT% + exit /b 0 +) else ( + echo ERROR: no toolset found for %KEY% + exit /b 1 +) diff --git a/.github/scripts/windows/test.bat b/.github/scripts/windows/test.bat index 510e9bc78f4e..7ef60534cc78 100644 --- a/.github/scripts/windows/test.bat +++ b/.github/scripts/windows/test.bat @@ -11,7 +11,8 @@ if not exist "%SDK_RUNNER%" ( exit /b 3 ) -cmd /c %SDK_RUNNER% -t .github\scripts\windows\test_task.bat +for /f "delims=" %%T in ('call .github\scripts\windows\find-vs-toolset.bat %PHP_BUILD_CRT%') do set "VS_TOOLSET=%%T" +cmd /c %SDK_RUNNER% -s %VS_TOOLSET% -t .github\scripts\windows\test_task.bat if %errorlevel% neq 0 exit /b 3 exit /b 0 diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 214798af62b5..5353ef7d0ea4 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -147,7 +147,7 @@ jobs: WINDOWS: if: github.repository == 'php/php-src' || github.event_name == 'pull_request' name: WINDOWS_X64_ZTS - runs-on: windows-2019 + runs-on: windows-2022 env: PHP_BUILD_CACHE_BASE_DIR: C:\build-cache PHP_BUILD_OBJ_DIR: C:\obj diff --git a/.github/workflows/root.yml b/.github/workflows/root.yml index a98bb39ba0d9..78e0d47aa1d1 100644 --- a/.github/workflows/root.yml +++ b/.github/workflows/root.yml @@ -58,7 +58,7 @@ jobs: ubuntu_version: ${{ (((matrix.branch.version[0] == 8 && matrix.branch.version[1] >= 5) || matrix.branch.version[0] >= 9) && '24.04') || '22.04' }} - windows_version: ${{ ((matrix.branch.version[0] == 8 && matrix.branch.version[1] >= 4) || matrix.branch.version[0] >= 9) && '2022' || '2019' }} + windows_version: '2022' skip_laravel: ${{ matrix.branch.version[0] == 8 && matrix.branch.version[1] == 1 }} skip_symfony: ${{ matrix.branch.version[0] == 8 && matrix.branch.version[1] == 1 }} skip_wordpress: ${{ matrix.branch.version[0] == 8 && matrix.branch.version[1] == 1 }} diff --git a/Zend/tests/bug70258.phpt b/Zend/tests/bug70258.phpt index 40915a286ef9..d346dbdf3a35 100644 --- a/Zend/tests/bug70258.phpt +++ b/Zend/tests/bug70258.phpt @@ -4,6 +4,9 @@ Bug #70258 (Segfault if do_resize fails to allocated memory) memory_limit=2M --SKIPIF-- --INI-- diff --git a/Zend/tests/gh11189_1.phpt b/Zend/tests/gh11189_1.phpt index 53727908e5e2..17b9967bc318 100644 --- a/Zend/tests/gh11189_1.phpt +++ b/Zend/tests/gh11189_1.phpt @@ -2,6 +2,9 @@ GH-11189: Exceeding memory limit in zend_hash_do_resize leaves the array in an invalid state (not packed array) --SKIPIF-- --INI-- From 359a21f102b5ca9e517253281228da68a053b3ae Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:55:11 +0200 Subject: [PATCH 22/69] Fix RCN violations in array functions When the array functions perform their operation in-place, the `@refcount 1` annotation is wrong and causes a failure under `ZEND_VERIFY_FUNC_INFO`. The test file tests all functions that have the in-place optimization, even those that didn't have the refcount annotation, just to prevent future regressions. Closes GH-18929. --- NEWS | 1 + Zend/Optimizer/zend_func_infos.h | 7 --- ext/standard/basic_functions.stub.php | 7 --- ext/standard/basic_functions_arginfo.h | 2 +- ext/standard/tests/array/rcn_in_place.phpt | 57 ++++++++++++++++++++++ 5 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 ext/standard/tests/array/rcn_in_place.phpt diff --git a/NEWS b/NEWS index 34866abfb21d..624bcda9bdf4 100644 --- a/NEWS +++ b/NEWS @@ -28,6 +28,7 @@ PHP NEWS - Standard: . Fix misleading errors in printf(). (nielsdos) + . Fix RCN violations in array functions. (nielsdos) - Streams: . Fixed GH-13264 (fgets() and stream_get_line() do not return false on filter diff --git a/Zend/Optimizer/zend_func_infos.h b/Zend/Optimizer/zend_func_infos.h index 125212d2292e..c9229d638f43 100644 --- a/Zend/Optimizer/zend_func_infos.h +++ b/Zend/Optimizer/zend_func_infos.h @@ -450,8 +450,6 @@ static const func_info_t func_infos[] = { F1("compact", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), FN("array_fill", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_ANY), F1("array_fill_keys", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), - F1("array_replace", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), - F1("array_replace_recursive", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), FN("array_keys", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_LONG|MAY_BE_ARRAY_OF_STRING), FN("array_values", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), F1("array_count_values", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_LONG), @@ -460,13 +458,8 @@ static const func_info_t func_infos[] = { F1("array_flip", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_LONG|MAY_BE_ARRAY_OF_STRING), F1("array_change_key_case", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), F1("array_intersect_key", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), - F1("array_intersect_ukey", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), - F1("array_intersect", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), - F1("array_uintersect", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), F1("array_intersect_assoc", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), F1("array_uintersect_assoc", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), - F1("array_intersect_uassoc", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), - F1("array_uintersect_uassoc", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), F1("array_diff_key", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), F1("array_diff_ukey", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), F1("array_udiff", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 3bdc3827b4d3..0bbdeeb72a2c 100755 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -1678,13 +1678,11 @@ function array_merge_recursive(array ...$arrays): array {} /** * @compile-time-eval - * @refcount 1 */ function array_replace(array $array, array ...$replacements): array {} /** * @compile-time-eval - * @refcount 1 */ function array_replace_recursive(array $array, array ...$replacements): array {} @@ -1757,19 +1755,16 @@ function array_intersect_key(array $array, array ...$arrays): array {} /** * @param array|callable $rest - * @refcount 1 */ function array_intersect_ukey(array $array, ...$rest): array {} /** * @compile-time-eval - * @refcount 1 */ function array_intersect(array $array, array ...$arrays): array {} /** * @param array|callable $rest - * @refcount 1 */ function array_uintersect(array $array, ...$rest): array {} @@ -1787,13 +1782,11 @@ function array_uintersect_assoc(array $array, ...$rest): array {} /** * @param array|callable $rest - * @refcount 1 */ function array_intersect_uassoc(array $array, ...$rest): array {} /** * @param array|callable $rest - * @refcount 1 */ function array_uintersect_uassoc(array $array, ...$rest): array {} diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index 1361cf606279..23ee75fe18dc 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 2a3d8da0b92134dcca74f2ac70454bd27768f20e */ + * Stub hash: 60960e59f6310521a958b6fb0917650854d19612 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) diff --git a/ext/standard/tests/array/rcn_in_place.phpt b/ext/standard/tests/array/rcn_in_place.phpt new file mode 100644 index 000000000000..e6a7b5b6d695 --- /dev/null +++ b/ext/standard/tests/array/rcn_in_place.phpt @@ -0,0 +1,57 @@ +--TEST-- +RCN check for in-place array modifications +--FILE-- + 0)); +var_dump(array_intersect(range(0, 1), [])); +var_dump(array_uintersect(range(0, 1), [], fn () => 0)); +var_dump(array_intersect_uassoc(range(0, 1), [], fn () => 0)); +var_dump(array_uintersect_uassoc(range(0, 1), [], fn () => 0, fn () => 0)); +?> +--EXPECT-- +array(2) { + [0]=> + int(0) + [1]=> + int(1) +} +array(2) { + [0]=> + int(0) + [1]=> + int(1) +} +array(2) { + [0]=> + int(0) + [1]=> + int(1) +} +array(2) { + [0]=> + int(0) + [1]=> + int(1) +} +array(2) { + [0]=> + int(0) + [1]=> + int(1) +} +array(0) { +} +array(0) { +} +array(0) { +} +array(0) { +} +array(0) { +} From 9cb3d8d200f0c822b17bda35a2a67a97b039d3e1 Mon Sep 17 00:00:00 2001 From: Ahmed Lekssays Date: Tue, 3 Jun 2025 09:00:55 +0000 Subject: [PATCH 23/69] Fix GHSA-453j-q27h-5p8x Libxml versions prior to 2.13 cannot correctly handle a call to xmlNodeSetName() with a name longer than 2G. It will leave the node object in an invalid state with a NULL name. This later causes a NULL pointer dereference when using the name during message serialization. To solve this, implement a workaround that resets the name to the sentinel name if this situation arises. Versions of libxml of 2.13 and higher are not affected. This can be exploited if a SoapVar is created with a fully qualified name that is longer than 2G. This would be possible if some application code uses a namespace prefix from an untrusted source like from a remote SOAP service. Co-authored-by: Niels Dossche <7771979+nielsdos@users.noreply.github.com> --- ext/soap/soap.c | 6 ++-- ext/soap/tests/soap_qname_crash.phpt | 48 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 ext/soap/tests/soap_qname_crash.phpt diff --git a/ext/soap/soap.c b/ext/soap/soap.c index fbf6546beb82..3bc713ca76bd 100644 --- a/ext/soap/soap.c +++ b/ext/soap/soap.c @@ -4019,8 +4019,10 @@ static xmlNodePtr serialize_zval(zval *val, sdlParamPtr param, char *paramName, } xmlParam = master_to_xml(enc, val, style, parent); zval_ptr_dtor(&defval); - if (!strcmp((char*)xmlParam->name, "BOGUS")) { - xmlNodeSetName(xmlParam, BAD_CAST(paramName)); + if (xmlParam != NULL) { + if (xmlParam->name == NULL || strcmp((char*)xmlParam->name, "BOGUS") == 0) { + xmlNodeSetName(xmlParam, BAD_CAST(paramName)); + } } return xmlParam; } diff --git a/ext/soap/tests/soap_qname_crash.phpt b/ext/soap/tests/soap_qname_crash.phpt new file mode 100644 index 000000000000..bcf01d574fab --- /dev/null +++ b/ext/soap/tests/soap_qname_crash.phpt @@ -0,0 +1,48 @@ +--TEST-- +Test SoapClient with excessively large QName prefix in SoapVar +--EXTENSIONS-- +soap +--SKIPIF-- + +--INI-- +memory_limit=6144M +--FILE-- + 'http://127.0.0.1/', + 'uri' => 'urn:dummy', + 'trace' => 1, + 'exceptions' => true, +]; +$client = new TestSoapClient(null, $options); +$client->__soapCall("DummyFunction", [$var]); +?> +--EXPECT-- +Attempting to create SoapVar with very large QName +Attempting encoding + +value From ea6a7a97255b939539cf92d43ce4c5c8b24b3199 Mon Sep 17 00:00:00 2001 From: Shivam Mathur Date: Wed, 25 Jun 2025 03:15:10 +0530 Subject: [PATCH 24/69] Fix CI for windows-2022 This is a continuation of GH-18927 to fix CI for windows-2022 --- Zend/tests/bug40770.phpt | 3 +++ Zend/tests/gh12073.phpt | 3 +++ tests/basic/timeout_variation_0.phpt | 3 +++ tests/basic/timeout_variation_7.phpt | 3 +++ tests/func/005a.phpt | 3 +++ tests/lang/bug45392.phpt | 3 +++ 6 files changed, 18 insertions(+) diff --git a/Zend/tests/bug40770.phpt b/Zend/tests/bug40770.phpt index f37d96d5ff33..bdbae4cf8f1a 100644 --- a/Zend/tests/bug40770.phpt +++ b/Zend/tests/bug40770.phpt @@ -4,6 +4,9 @@ Bug #40770 (Apache child exits when PHP memory limit reached) memory_limit=8M --SKIPIF-- --FILE-- diff --git a/tests/basic/timeout_variation_7.phpt b/tests/basic/timeout_variation_7.phpt index 0401240ba953..3d40b540677d 100644 --- a/tests/basic/timeout_variation_7.phpt +++ b/tests/basic/timeout_variation_7.phpt @@ -2,6 +2,9 @@ Timeout within for loop --SKIPIF-- --FILE-- diff --git a/tests/func/005a.phpt b/tests/func/005a.phpt index cf1e5713770a..2f527d773adb 100644 --- a/tests/func/005a.phpt +++ b/tests/func/005a.phpt @@ -2,6 +2,9 @@ Testing register_shutdown_function() with timeout. (Bug: #21513) --SKIPIF-- --FILE-- diff --git a/tests/lang/bug45392.phpt b/tests/lang/bug45392.phpt index 692fa0cdcf6f..1a01bac3261a 100644 --- a/tests/lang/bug45392.phpt +++ b/tests/lang/bug45392.phpt @@ -2,6 +2,9 @@ Bug #45392 (ob_start()/ob_end_clean() and memory_limit) --SKIPIF-- Date: Wed, 25 Jun 2025 03:15:10 +0530 Subject: [PATCH 25/69] Fix CI for windows-2022 This is a continuation of GH-18927 to fix CI for windows-2022 --- Zend/tests/bug40770.phpt | 3 +++ Zend/tests/gh12073.phpt | 3 +++ Zend/tests/traits/bugs/gh13177.phpt | 6 ++++++ tests/basic/timeout_variation_0.phpt | 3 +++ tests/basic/timeout_variation_7.phpt | 3 +++ tests/func/005a.phpt | 3 +++ tests/lang/bug45392.phpt | 3 +++ 7 files changed, 24 insertions(+) diff --git a/Zend/tests/bug40770.phpt b/Zend/tests/bug40770.phpt index f37d96d5ff33..bdbae4cf8f1a 100644 --- a/Zend/tests/bug40770.phpt +++ b/Zend/tests/bug40770.phpt @@ -4,6 +4,9 @@ Bug #40770 (Apache child exits when PHP memory limit reached) memory_limit=8M --SKIPIF-- --FILE-- --FILE-- diff --git a/tests/basic/timeout_variation_7.phpt b/tests/basic/timeout_variation_7.phpt index 0401240ba953..3d40b540677d 100644 --- a/tests/basic/timeout_variation_7.phpt +++ b/tests/basic/timeout_variation_7.phpt @@ -2,6 +2,9 @@ Timeout within for loop --SKIPIF-- --FILE-- diff --git a/tests/func/005a.phpt b/tests/func/005a.phpt index cf1e5713770a..2f527d773adb 100644 --- a/tests/func/005a.phpt +++ b/tests/func/005a.phpt @@ -2,6 +2,9 @@ Testing register_shutdown_function() with timeout. (Bug: #21513) --SKIPIF-- --FILE-- diff --git a/tests/lang/bug45392.phpt b/tests/lang/bug45392.phpt index 692fa0cdcf6f..1a01bac3261a 100644 --- a/tests/lang/bug45392.phpt +++ b/tests/lang/bug45392.phpt @@ -2,6 +2,9 @@ Bug #45392 (ob_start()/ob_end_clean() and memory_limit) --SKIPIF-- Date: Thu, 26 Jun 2025 11:24:54 +0200 Subject: [PATCH 26/69] Update NEWS with entries for security fixes --- NEWS | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 29400e6ef5b5..8c8b28fb9818 100644 --- a/NEWS +++ b/NEWS @@ -1,8 +1,18 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| -?? ??? ????, PHP 8.1.33 +03 Jul 2025, PHP 8.1.33 +- PGSQL: + . Fixed GHSA-hrwm-9436-5mv3 (pgsql extension does not check for errors during + escaping). (CVE-2025-1735) (Jakub Zelenka) + +- SOAP: + . Fixed GHSA-453j-q27h-5p8x (NULL Pointer Dereference in PHP SOAP Extension + via Large XML Namespace Prefix). (CVE-2025-6491) (Lekssays, nielsdos) +- Standard: + . Fixed GHSA-3cr5-j632-f35r (Null byte termination in hostnames). + (CVE-2025-1220) (Jakub Zelenka) 13 Mar 2025, PHP 8.1.32 From aee1d7fb964aadfbcf9d421e416e48b3cb17190a Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 27 Jun 2025 14:02:36 +0200 Subject: [PATCH 27/69] Fix pcntl_rfork / pcntl_forkx with zend-max-execution-timers --- NEWS | 4 ++++ ext/pcntl/pcntl.c | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/NEWS b/NEWS index 624bcda9bdf4..61e1697b62dd 100644 --- a/NEWS +++ b/NEWS @@ -26,6 +26,10 @@ PHP NEWS . Fixed bug GH-14082 (Segmentation fault on unknown address 0x600000000018 in ext/opcache/jit/zend_jit.c). (nielsdos) +- PCNTL: + . Fixed bug GH-18958 (Fatal error during shutdown after pcntl_rfork() or + pcntl_forkx() with zend-max-execution-timers). (Arnaud) + - Standard: . Fix misleading errors in printf(). (nielsdos) . Fix RCN violations in array functions. (nielsdos) diff --git a/ext/pcntl/pcntl.c b/ext/pcntl/pcntl.c index adb94af2fb46..16f29d419e3d 100644 --- a/ext/pcntl/pcntl.c +++ b/ext/pcntl/pcntl.c @@ -1277,6 +1277,8 @@ PHP_FUNCTION(pcntl_rfork) default: php_error_docref(NULL, E_WARNING, "Error %d", errno); } + } else if (pid == 0) { + zend_max_execution_timer_init(); } RETURN_LONG((zend_long) pid); @@ -1320,6 +1322,8 @@ PHP_FUNCTION(pcntl_forkx) default: php_error_docref(NULL, E_WARNING, "Error %d", errno); } + } else if (pid == 0) { + zend_max_execution_timer_init(); } RETURN_LONG((zend_long) pid); From 865739e5b196390f2eb1c5aeb2a7551e31da87cb Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sun, 29 Jun 2025 13:03:43 +0100 Subject: [PATCH 28/69] Fix GH-18976: pack with h or H format string overflow. adding with its own remainder, INT_MAX overflows here (negative values are discarded). close GH-18977 --- NEWS | 2 ++ ext/standard/pack.c | 2 +- ext/standard/tests/strings/gh18976.phpt | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 ext/standard/tests/strings/gh18976.phpt diff --git a/NEWS b/NEWS index 61e1697b62dd..267681cfa265 100644 --- a/NEWS +++ b/NEWS @@ -33,6 +33,8 @@ PHP NEWS - Standard: . Fix misleading errors in printf(). (nielsdos) . Fix RCN violations in array functions. (nielsdos) + . Fixed GH-18976 pack() overflow with h/H format and INT_MAX repeater value. + (David Carlier) - Streams: . Fixed GH-13264 (fgets() and stream_get_line() do not return false on filter diff --git a/ext/standard/pack.c b/ext/standard/pack.c index 8f72164a2695..46798e7403da 100644 --- a/ext/standard/pack.c +++ b/ext/standard/pack.c @@ -388,7 +388,7 @@ PHP_FUNCTION(pack) switch ((int) code) { case 'h': case 'H': - INC_OUTPUTPOS((arg + (arg % 2)) / 2,1) /* 4 bit per arg */ + INC_OUTPUTPOS((arg / 2) + (arg % 2),1) /* 4 bit per arg */ break; case 'a': diff --git a/ext/standard/tests/strings/gh18976.phpt b/ext/standard/tests/strings/gh18976.phpt new file mode 100644 index 000000000000..aa58167f9d45 --- /dev/null +++ b/ext/standard/tests/strings/gh18976.phpt @@ -0,0 +1,14 @@ +--TEST-- +GH-18976 (pack overflow with h/H format) +--INI-- +memory_limit=-1 +--FILE-- + +--EXPECTF-- + +Warning: pack(): Type h: not enough characters in string in %s on line %d + +Warning: pack(): Type H: not enough characters in string in %s on line %d From 85522c0d4803688c957c5bcccfc4f38696392bb9 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 27 Jun 2025 09:14:54 +0200 Subject: [PATCH 29/69] Add FreeBSD ZTS nightly build Closes GH-18959 --- .github/actions/freebsd/action.yml | 8 +++++++- .github/workflows/nightly.yml | 14 +++++++++++++- .github/workflows/root.yml | 1 + 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/actions/freebsd/action.yml b/.github/actions/freebsd/action.yml index 1f7c670f2722..fd37e92bbe5f 100644 --- a/.github/actions/freebsd/action.yml +++ b/.github/actions/freebsd/action.yml @@ -1,4 +1,8 @@ name: FreeBSD +inputs: + configurationParameters: + default: '' + required: false runs: using: composite steps: @@ -79,7 +83,9 @@ runs: --with-mhash \ --with-sodium \ --with-config-file-path=/etc \ - --with-config-file-scan-dir=/etc/php.d + --with-config-file-scan-dir=/etc/php.d \ + ${{ inputs.configurationParameters }} + gmake -j2 mkdir /etc/php.d gmake install > /dev/null diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 1b1532af7f79..1167b9c5d293 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -23,6 +23,9 @@ on: run_macos_arm64: required: true type: boolean + run_freebsd_zts: + required: true + type: boolean ubuntu_version: required: true type: string @@ -1052,7 +1055,13 @@ jobs: - name: Test run: .github/scripts/windows/test.bat FREEBSD: - name: FREEBSD + strategy: + fail-fast: false + matrix: + zts: [true, false] + exclude: + - zts: ${{ !inputs.run_freebsd_zts && true || '*never*' }} + name: "FREEBSD_${{ matrix.zts && 'ZTS' || 'NTS' }}" runs-on: ubuntu-latest steps: - name: git checkout @@ -1061,3 +1070,6 @@ jobs: ref: ${{ inputs.branch }} - name: FreeBSD uses: ./.github/actions/freebsd + with: + configurationParameters: >- + --${{ matrix.zts && 'enable' || 'disable' }}-zts diff --git a/.github/workflows/root.yml b/.github/workflows/root.yml index 78e0d47aa1d1..30abc1da852b 100644 --- a/.github/workflows/root.yml +++ b/.github/workflows/root.yml @@ -55,6 +55,7 @@ jobs: run_alpine: ${{ (matrix.branch.version[0] == 8 && matrix.branch.version[1] >= 4) || matrix.branch.version[0] >= 9 }} run_linux_ppc64: ${{ (matrix.branch.version[0] == 8 && matrix.branch.version[1] >= 4) || matrix.branch.version[0] >= 9 }} run_macos_arm64: ${{ (matrix.branch.version[0] == 8 && matrix.branch.version[1] >= 4) || matrix.branch.version[0] >= 9 }} + run_freebsd_zts: ${{ (matrix.branch.version[0] == 8 && matrix.branch.version[1] >= 3) || matrix.branch.version[0] >= 9 }} ubuntu_version: ${{ (((matrix.branch.version[0] == 8 && matrix.branch.version[1] >= 5) || matrix.branch.version[0] >= 9) && '24.04') || '22.04' }} From 8ddc210bf783d6dbe1ec756996ad9a0fb2c77469 Mon Sep 17 00:00:00 2001 From: Shivam Mathur Date: Mon, 30 Jun 2025 20:00:25 +0530 Subject: [PATCH 30/69] Fix PHP_BUILD_CRT input in the nightly workflow (#18982) --- .github/workflows/nightly.yml | 5 ++++- .github/workflows/root.yml | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 1167b9c5d293..5817c647a871 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -32,6 +32,9 @@ on: windows_version: required: true type: string + vs_crt_version: + required: true + type: string skip_laravel: required: true type: boolean @@ -1034,7 +1037,7 @@ jobs: PHP_BUILD_OBJ_DIR: C:\obj PHP_BUILD_CACHE_SDK_DIR: C:\build-cache\sdk PHP_BUILD_SDK_BRANCH: php-sdk-2.3.0 - PHP_BUILD_CRT: ${{ inputs.windows_version == '2022' && 'vs17' || 'vs16' }} + PHP_BUILD_CRT: ${{ inputs.vs_crt_version }} PLATFORM: ${{ matrix.x64 && 'x64' || 'x86' }} THREAD_SAFE: "${{ matrix.zts && '1' || '0' }}" INTRINSICS: "${{ matrix.zts && 'AVX2' || '' }}" diff --git a/.github/workflows/root.yml b/.github/workflows/root.yml index 30abc1da852b..96943a8cfb2a 100644 --- a/.github/workflows/root.yml +++ b/.github/workflows/root.yml @@ -60,6 +60,7 @@ jobs: (((matrix.branch.version[0] == 8 && matrix.branch.version[1] >= 5) || matrix.branch.version[0] >= 9) && '24.04') || '22.04' }} windows_version: '2022' + vs_crt_version: ${{ ((matrix.branch.version[0] == 8 && matrix.branch.version[1] >= 4) && 'vs17') || 'vs16' }} skip_laravel: ${{ matrix.branch.version[0] == 8 && matrix.branch.version[1] == 1 }} skip_symfony: ${{ matrix.branch.version[0] == 8 && matrix.branch.version[1] == 1 }} skip_wordpress: ${{ matrix.branch.version[0] == 8 && matrix.branch.version[1] == 1 }} From 53f2aa93ae2bfbb4291a32f27cbbce78ff6b8d13 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Mon, 23 Jun 2025 23:38:41 +0200 Subject: [PATCH 31/69] Fix GH-18898: SEGV zend_jit_op_array_hot with property hooks and preloading Property hooks were not handled for JIT+trait+preloading. Split the existing functions that handle op arrays, and add iterations for property hooks. Closes GH-18923. --- NEWS | 2 + ext/opcache/jit/zend_jit.c | 31 +++++++++-- ext/opcache/tests/jit/gh18898_1.phpt | 23 ++++++++ ext/opcache/tests/jit/gh18898_2.phpt | 23 ++++++++ ext/opcache/zend_persist.c | 83 ++++++++++++++++++++-------- 5 files changed, 133 insertions(+), 29 deletions(-) create mode 100644 ext/opcache/tests/jit/gh18898_1.phpt create mode 100644 ext/opcache/tests/jit/gh18898_2.phpt diff --git a/NEWS b/NEWS index 3f4e3a0a4a34..1af910562381 100644 --- a/NEWS +++ b/NEWS @@ -28,6 +28,8 @@ PHP NEWS warning and opline is not set yet). (nielsdos) . Fixed bug GH-14082 (Segmentation fault on unknown address 0x600000000018 in ext/opcache/jit/zend_jit.c). (nielsdos) + . Fixed bug GH-18898 (SEGV zend_jit_op_array_hot with property hooks + and preloading). (nielsdos) - PCNTL: . Fixed bug GH-18958 (Fatal error during shutdown after pcntl_rfork() or diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index 67e89d3e2e66..7b451240a38b 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -3216,6 +3216,17 @@ int zend_jit_op_array(zend_op_array *op_array, zend_script *script) return FAILURE; } +static void zend_jit_link_func_info(zend_op_array *op_array) +{ + if (!ZEND_FUNC_INFO(op_array)) { + void *jit_extension = zend_shared_alloc_get_xlat_entry(op_array->opcodes); + + if (jit_extension) { + ZEND_SET_FUNC_INFO(op_array, jit_extension); + } + } +} + int zend_jit_script(zend_script *script) { void *checkpoint; @@ -3303,6 +3314,7 @@ int zend_jit_script(zend_script *script) zend_class_entry *ce; zend_op_array *op_array; zval *zv; + zend_property_info *prop; ZEND_HASH_MAP_FOREACH_VAL(&script->class_table, zv) { if (Z_TYPE_P(zv) == IS_ALIAS_PTR) { @@ -3313,14 +3325,21 @@ int zend_jit_script(zend_script *script) ZEND_ASSERT(ce->type == ZEND_USER_CLASS); ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, op_array) { - if (!ZEND_FUNC_INFO(op_array)) { - void *jit_extension = zend_shared_alloc_get_xlat_entry(op_array->opcodes); + zend_jit_link_func_info(op_array); + } ZEND_HASH_FOREACH_END(); - if (jit_extension) { - ZEND_SET_FUNC_INFO(op_array, jit_extension); + if (ce->num_hooked_props > 0) { + ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, prop) { + if (prop->hooks) { + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + if (prop->hooks[i]) { + op_array = &prop->hooks[i]->op_array; + zend_jit_link_func_info(op_array); + } + } } - } - } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); + } } ZEND_HASH_FOREACH_END(); } diff --git a/ext/opcache/tests/jit/gh18898_1.phpt b/ext/opcache/tests/jit/gh18898_1.phpt new file mode 100644 index 000000000000..6038f006f5e5 --- /dev/null +++ b/ext/opcache/tests/jit/gh18898_1.phpt @@ -0,0 +1,23 @@ +--TEST-- +GH-18898 (SEGV zend_jit_op_array_hot with property hooks and preloading) - jit 1235 +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=1235 +opcache.jit_buffer_size=16M +opcache.preload={PWD}/../gh18534_preload.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +dummyProperty2); +echo "ok"; +?> +--EXPECT-- +NULL +ok diff --git a/ext/opcache/tests/jit/gh18898_2.phpt b/ext/opcache/tests/jit/gh18898_2.phpt new file mode 100644 index 000000000000..0ce79b859a97 --- /dev/null +++ b/ext/opcache/tests/jit/gh18898_2.phpt @@ -0,0 +1,23 @@ +--TEST-- +GH-18898 (SEGV zend_jit_op_array_hot with property hooks and preloading) - jit 1233 +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=1233 +opcache.jit_buffer_size=16M +opcache.preload={PWD}/../gh18534_preload.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +dummyProperty2); +echo "ok"; +?> +--EXPECT-- +NULL +ok diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index 1c21e031a195..71e5bb540078 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -1262,6 +1262,39 @@ void zend_update_parent_ce(zend_class_entry *ce) } } +static void zend_accel_persist_jit_op_array(zend_op_array *op_array, zend_class_entry *ce) +{ + if (op_array->type == ZEND_USER_FUNCTION) { + if (op_array->scope == ce + && !(op_array->fn_flags & ZEND_ACC_ABSTRACT) + && !(op_array->fn_flags & ZEND_ACC_TRAIT_CLONE)) { + zend_jit_op_array(op_array, ZCG(current_persistent_script) ? &ZCG(current_persistent_script)->script : NULL); + for (uint32_t i = 0; i < op_array->num_dynamic_func_defs; i++) { + zend_jit_op_array(op_array->dynamic_func_defs[i], ZCG(current_persistent_script) ? &ZCG(current_persistent_script)->script : NULL); + } + } + } +} + +static void zend_accel_persist_link_func_info(zend_op_array *op_array, zend_class_entry *ce) +{ + if (op_array->type == ZEND_USER_FUNCTION + && !(op_array->fn_flags & ZEND_ACC_ABSTRACT)) { + if ((op_array->scope != ce + || (op_array->fn_flags & ZEND_ACC_TRAIT_CLONE)) + && (JIT_G(trigger) == ZEND_JIT_ON_FIRST_EXEC + || JIT_G(trigger) == ZEND_JIT_ON_PROF_REQUEST + || JIT_G(trigger) == ZEND_JIT_ON_HOT_COUNTERS + || JIT_G(trigger) == ZEND_JIT_ON_HOT_TRACE)) { + void *jit_extension = zend_shared_alloc_get_xlat_entry(op_array->opcodes); + + if (jit_extension) { + ZEND_SET_FUNC_INFO(op_array, jit_extension); + } + } + } +} + static void zend_accel_persist_class_table(HashTable *class_table) { Bucket *p; @@ -1288,44 +1321,48 @@ static void zend_accel_persist_class_table(HashTable *class_table) if (JIT_G(on) && JIT_G(opt_level) <= ZEND_JIT_LEVEL_OPT_FUNCS && !ZCG(current_persistent_script)->corrupted) { zend_op_array *op_array; + zend_property_info *prop; ZEND_HASH_MAP_FOREACH_BUCKET(class_table, p) { if (EXPECTED(Z_TYPE(p->val) != IS_ALIAS_PTR)) { ce = Z_PTR(p->val); ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, op_array) { - if (op_array->type == ZEND_USER_FUNCTION) { - if (op_array->scope == ce - && !(op_array->fn_flags & ZEND_ACC_ABSTRACT) - && !(op_array->fn_flags & ZEND_ACC_TRAIT_CLONE)) { - zend_jit_op_array(op_array, ZCG(current_persistent_script) ? &ZCG(current_persistent_script)->script : NULL); - for (uint32_t i = 0; i < op_array->num_dynamic_func_defs; i++) { - zend_jit_op_array(op_array->dynamic_func_defs[i], ZCG(current_persistent_script) ? &ZCG(current_persistent_script)->script : NULL); + zend_accel_persist_jit_op_array(op_array, ce); + } ZEND_HASH_FOREACH_END(); + + if (ce->num_hooked_props > 0) { + ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, prop) { + if (prop->hooks) { + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + if (prop->hooks[i]) { + op_array = &prop->hooks[i]->op_array; + zend_accel_persist_jit_op_array(op_array, ce); + } } } - } - } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); + } } } ZEND_HASH_FOREACH_END(); ZEND_HASH_MAP_FOREACH_BUCKET(class_table, p) { if (EXPECTED(Z_TYPE(p->val) != IS_ALIAS_PTR)) { ce = Z_PTR(p->val); ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, op_array) { - if (op_array->type == ZEND_USER_FUNCTION - && !(op_array->fn_flags & ZEND_ACC_ABSTRACT)) { - if ((op_array->scope != ce - || (op_array->fn_flags & ZEND_ACC_TRAIT_CLONE)) - && (JIT_G(trigger) == ZEND_JIT_ON_FIRST_EXEC - || JIT_G(trigger) == ZEND_JIT_ON_PROF_REQUEST - || JIT_G(trigger) == ZEND_JIT_ON_HOT_COUNTERS - || JIT_G(trigger) == ZEND_JIT_ON_HOT_TRACE)) { - void *jit_extension = zend_shared_alloc_get_xlat_entry(op_array->opcodes); - - if (jit_extension) { - ZEND_SET_FUNC_INFO(op_array, jit_extension); + zend_accel_persist_link_func_info(op_array, ce); + } ZEND_HASH_FOREACH_END(); + + if (ce->num_hooked_props > 0) { + ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, prop) { + if (prop->hooks) { + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + if (prop->hooks[i]) { + op_array = &prop->hooks[i]->op_array; + zend_accel_persist_link_func_info(op_array, ce); + } } } - } - } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); + } } } ZEND_HASH_FOREACH_END(); } From 5ef0dc76665d089ccb4b05a8542c62220b146d2c Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 10 Apr 2025 15:15:36 +0200 Subject: [PATCH 32/69] Fix GHSA-3cr5-j632-f35r: Null byte in hostnames This fixes stream_socket_client() and fsockopen(). Specifically it adds a check to parse_ip_address_ex and it also makes sure that the \0 is not ignored in fsockopen() hostname formatting. --- ext/standard/fsock.c | 27 +++++++++++++++++-- .../tests/network/ghsa-3cr5-j632-f35r.phpt | 21 +++++++++++++++ .../tests/streams/ghsa-3cr5-j632-f35r.phpt | 26 ++++++++++++++++++ main/streams/xp_socket.c | 9 ++++--- 4 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt create mode 100644 ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt diff --git a/ext/standard/fsock.c b/ext/standard/fsock.c index cb7a471e935a..2b9e00a57554 100644 --- a/ext/standard/fsock.c +++ b/ext/standard/fsock.c @@ -23,6 +23,28 @@ #include "php_network.h" #include "file.h" +static size_t php_fsockopen_format_host_port(char **message, const char *prefix, size_t prefix_len, + const char *host, size_t host_len, zend_long port) +{ + char portbuf[32]; + int portlen = snprintf(portbuf, sizeof(portbuf), ":" ZEND_LONG_FMT, port); + size_t total_len = prefix_len + host_len + portlen; + + char *result = emalloc(total_len + 1); + + if (prefix_len > 0) { + memcpy(result, prefix, prefix_len); + } + memcpy(result + prefix_len, host, host_len); + memcpy(result + prefix_len + host_len, portbuf, portlen); + + result[total_len] = '\0'; + + *message = result; + + return total_len; +} + /* {{{ php_fsockopen() */ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) @@ -62,11 +84,12 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) } if (persistent) { - spprintf(&hashkey, 0, "pfsockopen__%s:" ZEND_LONG_FMT, host, port); + php_fsockopen_format_host_port(&hashkey, "pfsockopen__", strlen("pfsockopen__"), host, + host_len, port); } if (port > 0) { - hostname_len = spprintf(&hostname, 0, "%s:" ZEND_LONG_FMT, host, port); + hostname_len = php_fsockopen_format_host_port(&hostname, "", 0, host, host_len, port); } else { hostname_len = host_len; hostname = host; diff --git a/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt b/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt new file mode 100644 index 000000000000..7556c3be94cc --- /dev/null +++ b/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt @@ -0,0 +1,21 @@ +--TEST-- +GHSA-3cr5-j632-f35r: Null byte termination in fsockopen() +--FILE-- + +--EXPECTF-- + +Warning: fsockopen(): Unable to connect to localhost:%d (The hostname must not contain null bytes) in %s +bool(false) diff --git a/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt b/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt new file mode 100644 index 000000000000..52f9263c99aa --- /dev/null +++ b/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt @@ -0,0 +1,26 @@ +--TEST-- +GHSA-3cr5-j632-f35r: Null byte termination in stream_socket_client() +--FILE-- + +--EXPECTF-- + +Warning: stream_socket_client(): Unable to connect to tcp://localhost\0.example.com:%d (The hostname must not contain null bytes) in %s +bool(false) diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index 3d035de6edb2..8623c11be004 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -620,12 +620,15 @@ static inline char *parse_ip_address_ex(const char *str, size_t str_len, int *po char *colon; char *host = NULL; -#ifdef HAVE_IPV6 - char *p; + if (memchr(str, '\0', str_len)) { + *err = ZSTR_INIT_LITERAL("The hostname must not contain null bytes", 0); + return NULL; + } +#ifdef HAVE_IPV6 if (*(str) == '[' && str_len > 1) { /* IPV6 notation to specify raw address with port (i.e. [fe80::1]:80) */ - p = memchr(str + 1, ']', str_len - 2); + char *p = memchr(str + 1, ']', str_len - 2); if (!p || *(p + 1) != ':') { if (get_err) { *err = strpprintf(0, "Failed to parse IPv6 address \"%s\"", str); From 66bd809ac922338201f4efaafdc56755afafa37a Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 4 Mar 2025 17:23:01 +0100 Subject: [PATCH 33/69] Fix GHSA-hrwm-9436-5mv3: pgsql escaping no error checks This adds error checks for escape function is pgsql and pdo_pgsql extensions. It prevents possibility of storing not properly escaped data which could potentially lead to some security issues. --- ext/pdo_pgsql/pgsql_driver.c | 10 +- ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt | 24 ++++ ext/pgsql/pgsql.c | 125 ++++++++++++++++--- ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt | 64 ++++++++++ 4 files changed, 202 insertions(+), 21 deletions(-) create mode 100644 ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt create mode 100644 ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c index 684f7798a45f..fc4b5b7c6640 100644 --- a/ext/pdo_pgsql/pgsql_driver.c +++ b/ext/pdo_pgsql/pgsql_driver.c @@ -367,11 +367,15 @@ static zend_string* pgsql_handle_quoter(pdo_dbh_t *dbh, const zend_string *unquo zend_string *quoted_str; pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data; size_t tmp_len; + int err; switch (paramtype) { case PDO_PARAM_LOB: /* escapedlen returned by PQescapeBytea() accounts for trailing 0 */ escaped = PQescapeByteaConn(H->server, (unsigned char *)ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), &tmp_len); + if (escaped == NULL) { + return NULL; + } quotedlen = tmp_len + 1; quoted = emalloc(quotedlen + 1); memcpy(quoted+1, escaped, quotedlen-2); @@ -383,7 +387,11 @@ static zend_string* pgsql_handle_quoter(pdo_dbh_t *dbh, const zend_string *unquo default: quoted = safe_emalloc(2, ZSTR_LEN(unquoted), 3); quoted[0] = '\''; - quotedlen = PQescapeStringConn(H->server, quoted + 1, ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), NULL); + quotedlen = PQescapeStringConn(H->server, quoted + 1, ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), &err); + if (err) { + efree(quoted); + return NULL; + } quoted[quotedlen + 1] = '\''; quoted[quotedlen + 2] = '\0'; quotedlen += 2; diff --git a/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt b/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt new file mode 100644 index 000000000000..8566a26753b4 --- /dev/null +++ b/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt @@ -0,0 +1,24 @@ +--TEST-- +#GHSA-hrwm-9436-5mv3: pdo_pgsql extension does not check for errors during escaping +--EXTENSIONS-- +pdo +pdo_pgsql +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +$invalid = "ABC\xff\x30';"; +var_dump($db->quote($invalid)); + +?> +--EXPECT-- +bool(false) diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c index 4e5020d8c096..7e43360e61d8 100644 --- a/ext/pgsql/pgsql.c +++ b/ext/pgsql/pgsql.c @@ -3528,8 +3528,14 @@ PHP_FUNCTION(pg_escape_string) to = zend_string_safe_alloc(ZSTR_LEN(from), 2, 0, 0); if (link) { + int err; pgsql = link->conn; - ZSTR_LEN(to) = PQescapeStringConn(pgsql, ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from), NULL); + ZSTR_LEN(to) = PQescapeStringConn(pgsql, ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from), &err); + if (err) { + zend_argument_value_error(ZEND_NUM_ARGS(), "Escaping string failed"); + zend_string_efree(to); + RETURN_THROWS(); + } } else { ZSTR_LEN(to) = PQescapeString(ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from)); @@ -3575,6 +3581,10 @@ PHP_FUNCTION(pg_escape_bytea) } else { to = (char *)PQescapeBytea((unsigned char *)ZSTR_VAL(from), ZSTR_LEN(from), &to_len); } + if (to == NULL) { + zend_argument_value_error(ZEND_NUM_ARGS(), "Escape failure"); + RETURN_THROWS(); + } RETVAL_STRINGL(to, to_len-1); /* to_len includes additional '\0' */ PQfreemem(to); @@ -4523,7 +4533,7 @@ PHP_PGSQL_API zend_result php_pgsql_meta_data(PGconn *pg_link, const zend_string char *escaped; smart_str querystr = {0}; size_t new_len, len; - int i, num_rows; + int i, num_rows, err; zval elem; ZEND_ASSERT(ZSTR_LEN(table_name) != 0); @@ -4562,7 +4572,14 @@ PHP_PGSQL_API zend_result php_pgsql_meta_data(PGconn *pg_link, const zend_string } len = strlen(tmp_name2); escaped = (char *)safe_emalloc(len, 2, 1); - new_len = PQescapeStringConn(pg_link, escaped, tmp_name2, len, NULL); + new_len = PQescapeStringConn(pg_link, escaped, tmp_name2, len, &err); + if (err) { + php_error_docref(NULL, E_WARNING, "Escaping table name '%s' failed", ZSTR_VAL(table_name)); + efree(src); + efree(escaped); + smart_str_free(&querystr); + return FAILURE; + } if (new_len) { smart_str_appendl(&querystr, escaped, new_len); } @@ -4571,7 +4588,14 @@ PHP_PGSQL_API zend_result php_pgsql_meta_data(PGconn *pg_link, const zend_string smart_str_appends(&querystr, "' AND n.nspname = '"); len = strlen(tmp_name); escaped = (char *)safe_emalloc(len, 2, 1); - new_len = PQescapeStringConn(pg_link, escaped, tmp_name, len, NULL); + new_len = PQescapeStringConn(pg_link, escaped, tmp_name, len, &err); + if (err) { + php_error_docref(NULL, E_WARNING, "Escaping table namespace '%s' failed", ZSTR_VAL(table_name)); + efree(src); + efree(escaped); + smart_str_free(&querystr); + return FAILURE; + } if (new_len) { smart_str_appendl(&querystr, escaped, new_len); } @@ -4826,7 +4850,7 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * { zend_string *field = NULL; zval meta, *def, *type, *not_null, *has_default, *is_enum, *val, new_val; - int err = 0, skip_field; + int err = 0, escape_err = 0, skip_field; php_pgsql_data_type data_type; ZEND_ASSERT(pg_link != NULL); @@ -5072,8 +5096,13 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * /* PostgreSQL ignores \0 */ str = zend_string_alloc(Z_STRLEN_P(val) * 2, 0); /* better to use PGSQLescapeLiteral since PGescapeStringConn does not handle special \ */ - ZSTR_LEN(str) = PQescapeStringConn(pg_link, ZSTR_VAL(str), Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); - ZVAL_STR(&new_val, php_pgsql_add_quotes(str)); + ZSTR_LEN(str) = PQescapeStringConn(pg_link, ZSTR_VAL(str), + Z_STRVAL_P(val), Z_STRLEN_P(val), &escape_err); + if (escape_err) { + err = 1; + } else { + ZVAL_STR(&new_val, php_pgsql_add_quotes(str)); + } zend_string_release_ex(str, false); } break; @@ -5096,7 +5125,14 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * } PGSQL_CONV_CHECK_IGNORE(); if (err) { - zend_type_error("%s(): Field \"%s\" must be of type string|null, %s given", get_active_function_name(), ZSTR_VAL(field), Z_STRVAL_P(type)); + if (escape_err) { + php_error_docref(NULL, E_NOTICE, + "String value escaping failed for PostgreSQL '%s' (%s)", + Z_STRVAL_P(type), ZSTR_VAL(field)); + } else { + zend_type_error("%s(): Field \"%s\" must be of type string|null, %s given", + get_active_function_name(), ZSTR_VAL(field), Z_STRVAL_P(type)); + } } break; @@ -5330,6 +5366,11 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * zend_string *tmp_zstr; tmp = PQescapeByteaConn(pg_link, (unsigned char *)Z_STRVAL_P(val), Z_STRLEN_P(val), &to_len); + if (tmp == NULL) { + php_error_docref(NULL, E_NOTICE, "Escaping value failed for %s field (%s)", Z_STRVAL_P(type), ZSTR_VAL(field)); + err = 1; + break; + } tmp_zstr = zend_string_init((char *)tmp, to_len - 1, false); /* PQescapeBytea's to_len includes additional '\0' */ PQfreemem(tmp); @@ -5406,6 +5447,12 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * zend_hash_update(Z_ARRVAL_P(result), field, &new_val); } else { char *escaped = PQescapeIdentifier(pg_link, ZSTR_VAL(field), ZSTR_LEN(field)); + if (escaped == NULL) { + /* This cannot fail because of invalid string but only due to failed memory allocation */ + php_error_docref(NULL, E_NOTICE, "Escaping field '%s' failed", ZSTR_VAL(field)); + err = 1; + break; + } add_assoc_zval(result, escaped, &new_val); PQfreemem(escaped); } @@ -5488,7 +5535,7 @@ static bool do_exec(smart_str *querystr, ExecStatusType expect, PGconn *pg_link, } /* }}} */ -static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const zend_string *table) /* {{{ */ +static inline zend_result build_tablename(smart_str *querystr, PGconn *pg_link, const zend_string *table) /* {{{ */ { /* schema.table should be "schema"."table" */ const char *dot = memchr(ZSTR_VAL(table), '.', ZSTR_LEN(table)); @@ -5498,6 +5545,10 @@ static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const z smart_str_appendl(querystr, ZSTR_VAL(table), len); } else { char *escaped = PQescapeIdentifier(pg_link, ZSTR_VAL(table), len); + if (escaped == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape table name '%s'", ZSTR_VAL(table)); + return FAILURE; + } smart_str_appends(querystr, escaped); PQfreemem(escaped); } @@ -5510,11 +5561,17 @@ static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const z smart_str_appendl(querystr, after_dot, len); } else { char *escaped = PQescapeIdentifier(pg_link, after_dot, len); + if (escaped == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape table name '%s'", ZSTR_VAL(table)); + return FAILURE; + } smart_str_appendc(querystr, '.'); smart_str_appends(querystr, escaped); PQfreemem(escaped); } } + + return SUCCESS; } /* }}} */ @@ -5535,7 +5592,9 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t ZVAL_UNDEF(&converted); if (zend_hash_num_elements(Z_ARRVAL_P(var_array)) == 0) { smart_str_appends(&querystr, "INSERT INTO "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " DEFAULT VALUES"); goto no_values; @@ -5551,7 +5610,9 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "INSERT INTO "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " ("); ZEND_HASH_FOREACH_STR_KEY(Z_ARRVAL_P(var_array), fld) { @@ -5561,6 +5622,10 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t } if (opt & PGSQL_DML_ESCAPE) { tmp = PQescapeIdentifier(pg_link, ZSTR_VAL(fld), ZSTR_LEN(fld) + 1); + if (tmp == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s'", ZSTR_VAL(fld)); + goto cleanup; + } smart_str_appends(&querystr, tmp); PQfreemem(tmp); } else { @@ -5572,15 +5637,19 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t smart_str_appends(&querystr, ") VALUES ("); /* make values string */ - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(var_array), val) { + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(var_array), fld, val) { /* we can avoid the key_type check here, because we tested it in the other loop */ switch (Z_TYPE_P(val)) { case IS_STRING: if (opt & PGSQL_DML_ESCAPE) { - size_t new_len; - char *tmp; - tmp = (char *)safe_emalloc(Z_STRLEN_P(val), 2, 1); - new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); + int error; + char *tmp = safe_emalloc(Z_STRLEN_P(val), 2, 1); + size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), &error); + if (error) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s' value", ZSTR_VAL(fld)); + efree(tmp); + goto cleanup; + } smart_str_appendc(&querystr, '\''); smart_str_appendl(&querystr, tmp, new_len); smart_str_appendc(&querystr, '\''); @@ -5738,6 +5807,10 @@ static inline int build_assignment_string(PGconn *pg_link, smart_str *querystr, } if (opt & PGSQL_DML_ESCAPE) { char *tmp = PQescapeIdentifier(pg_link, ZSTR_VAL(fld), ZSTR_LEN(fld) + 1); + if (tmp == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s'", ZSTR_VAL(fld)); + return -1; + } smart_str_appends(querystr, tmp); PQfreemem(tmp); } else { @@ -5753,8 +5826,14 @@ static inline int build_assignment_string(PGconn *pg_link, smart_str *querystr, switch (Z_TYPE_P(val)) { case IS_STRING: if (opt & PGSQL_DML_ESCAPE) { + int error; char *tmp = (char *)safe_emalloc(Z_STRLEN_P(val), 2, 1); - size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); + size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), &error); + if (error) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s' value", ZSTR_VAL(fld)); + efree(tmp); + return -1; + } smart_str_appendc(querystr, '\''); smart_str_appendl(querystr, tmp, new_len); smart_str_appendc(querystr, '\''); @@ -5822,7 +5901,9 @@ PHP_PGSQL_API zend_result php_pgsql_update(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "UPDATE "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " SET "); if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(var_array), 0, ",", 1, opt)) @@ -5928,7 +6009,9 @@ PHP_PGSQL_API zend_result php_pgsql_delete(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "DELETE FROM "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " WHERE "); if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(ids_array), 1, " AND ", sizeof(" AND ")-1, opt)) @@ -6072,7 +6155,9 @@ PHP_PGSQL_API zend_result php_pgsql_select(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "SELECT * FROM "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } if (is_valid_ids_array) { smart_str_appends(&querystr, " WHERE "); diff --git a/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt b/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt new file mode 100644 index 000000000000..6cbfe6d1f585 --- /dev/null +++ b/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt @@ -0,0 +1,64 @@ +--TEST-- +#GHSA-hrwm-9436-5mv3: pgsql extension does not check for errors during escaping +--EXTENSIONS-- +pgsql +--SKIPIF-- + +--FILE-- + 'test'])); // table name str escape in php_pgsql_meta_data +var_dump(pg_insert($db, "$invalid.tbl", ['bar' => 'test'])); // schema name str escape in php_pgsql_meta_data +var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', ['bar' => $invalid])); // converted value str escape in php_pgsql_convert +var_dump(pg_insert($db, $invalid, [])); // ident escape in build_tablename +var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', [$invalid => 'foo'], $flags)); // ident escape for field php_pgsql_insert +var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', ['bar' => $invalid], $flags)); // str escape for field value in php_pgsql_insert +var_dump(pg_update($db, 'ghsa_hrmw_9436_5mv3', ['bar' => 'val'], [$invalid => 'test'], $flags)); // ident escape in build_assignment_string +var_dump(pg_update($db, 'ghsa_hrmw_9436_5mv3', ['bar' => 'val'], ['bar' => $invalid], $flags)); // invalid str escape in build_assignment_string +var_dump(pg_escape_literal($db, $invalid)); // pg_escape_literal escape +var_dump(pg_escape_identifier($db, $invalid)); // pg_escape_identifier escape + +?> +--EXPECTF-- + +Warning: pg_insert(): Escaping table name 'ABC%s';' failed in %s on line %d +bool(false) + +Warning: pg_insert(): Escaping table namespace 'ABC%s';.tbl' failed in %s on line %d +bool(false) + +Notice: pg_insert(): String value escaping failed for PostgreSQL 'text' (bar) in %s on line %d +bool(false) + +Notice: pg_insert(): Failed to escape table name 'ABC%s';' in %s on line %d +bool(false) + +Notice: pg_insert(): Failed to escape field 'ABC%s';' in %s on line %d +bool(false) + +Notice: pg_insert(): Failed to escape field 'bar' value in %s on line %d +bool(false) + +Notice: pg_update(): Failed to escape field 'ABC%s';' in %s on line %d +bool(false) + +Notice: pg_update(): Failed to escape field 'bar' value in %s on line %d +bool(false) + +Warning: pg_escape_literal(): Failed to escape in %s on line %d +bool(false) + +Warning: pg_escape_identifier(): Failed to escape in %s on line %d +bool(false) From a179e39c38001af3d40a0dbd7e0e57bbd7b66814 Mon Sep 17 00:00:00 2001 From: Ahmed Lekssays Date: Tue, 3 Jun 2025 09:00:55 +0000 Subject: [PATCH 34/69] Fix GHSA-453j-q27h-5p8x Libxml versions prior to 2.13 cannot correctly handle a call to xmlNodeSetName() with a name longer than 2G. It will leave the node object in an invalid state with a NULL name. This later causes a NULL pointer dereference when using the name during message serialization. To solve this, implement a workaround that resets the name to the sentinel name if this situation arises. Versions of libxml of 2.13 and higher are not affected. This can be exploited if a SoapVar is created with a fully qualified name that is longer than 2G. This would be possible if some application code uses a namespace prefix from an untrusted source like from a remote SOAP service. Co-authored-by: Niels Dossche <7771979+nielsdos@users.noreply.github.com> --- ext/soap/soap.c | 6 ++-- ext/soap/tests/soap_qname_crash.phpt | 48 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 ext/soap/tests/soap_qname_crash.phpt diff --git a/ext/soap/soap.c b/ext/soap/soap.c index 6a718f3af14a..afd4be8c24c0 100644 --- a/ext/soap/soap.c +++ b/ext/soap/soap.c @@ -4134,8 +4134,10 @@ static xmlNodePtr serialize_zval(zval *val, sdlParamPtr param, const char *param } xmlParam = master_to_xml(enc, val, style, parent); zval_ptr_dtor(&defval); - if (!strcmp((char*)xmlParam->name, "BOGUS")) { - xmlNodeSetName(xmlParam, BAD_CAST(paramName)); + if (xmlParam != NULL) { + if (xmlParam->name == NULL || strcmp((char*)xmlParam->name, "BOGUS") == 0) { + xmlNodeSetName(xmlParam, BAD_CAST(paramName)); + } } return xmlParam; } diff --git a/ext/soap/tests/soap_qname_crash.phpt b/ext/soap/tests/soap_qname_crash.phpt new file mode 100644 index 000000000000..bcf01d574fab --- /dev/null +++ b/ext/soap/tests/soap_qname_crash.phpt @@ -0,0 +1,48 @@ +--TEST-- +Test SoapClient with excessively large QName prefix in SoapVar +--EXTENSIONS-- +soap +--SKIPIF-- + +--INI-- +memory_limit=6144M +--FILE-- + 'http://127.0.0.1/', + 'uri' => 'urn:dummy', + 'trace' => 1, + 'exceptions' => true, +]; +$client = new TestSoapClient(null, $options); +$client->__soapCall("DummyFunction", [$var]); +?> +--EXPECT-- +Attempting to create SoapVar with very large QName +Attempting encoding + +value From c57ec92eb6afc7dd89559f8c145ff47b5c2d1065 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 1 Jul 2025 11:41:24 +0200 Subject: [PATCH 35/69] Fix missing HAVE_JIT guard Closes GH-18993 --- ext/opcache/zend_persist.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index 71e5bb540078..f8e11e4d01ec 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -1262,6 +1262,7 @@ void zend_update_parent_ce(zend_class_entry *ce) } } +#ifdef HAVE_JIT static void zend_accel_persist_jit_op_array(zend_op_array *op_array, zend_class_entry *ce) { if (op_array->type == ZEND_USER_FUNCTION) { @@ -1294,6 +1295,7 @@ static void zend_accel_persist_link_func_info(zend_op_array *op_array, zend_clas } } } +#endif static void zend_accel_persist_class_table(HashTable *class_table) { From cf0c39723ee05fac979e8f11a0f0c0645e61a83a Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 10 Apr 2025 15:15:36 +0200 Subject: [PATCH 36/69] Fix GHSA-3cr5-j632-f35r: Null byte in hostnames This fixes stream_socket_client() and fsockopen(). Specifically it adds a check to parse_ip_address_ex and it also makes sure that the \0 is not ignored in fsockopen() hostname formatting. --- ext/standard/fsock.c | 27 +++++++++++++++++-- .../tests/network/ghsa-3cr5-j632-f35r.phpt | 21 +++++++++++++++ .../tests/streams/ghsa-3cr5-j632-f35r.phpt | 26 ++++++++++++++++++ main/streams/xp_socket.c | 9 ++++--- 4 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt create mode 100644 ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt diff --git a/ext/standard/fsock.c b/ext/standard/fsock.c index cb7a471e935a..2b9e00a57554 100644 --- a/ext/standard/fsock.c +++ b/ext/standard/fsock.c @@ -23,6 +23,28 @@ #include "php_network.h" #include "file.h" +static size_t php_fsockopen_format_host_port(char **message, const char *prefix, size_t prefix_len, + const char *host, size_t host_len, zend_long port) +{ + char portbuf[32]; + int portlen = snprintf(portbuf, sizeof(portbuf), ":" ZEND_LONG_FMT, port); + size_t total_len = prefix_len + host_len + portlen; + + char *result = emalloc(total_len + 1); + + if (prefix_len > 0) { + memcpy(result, prefix, prefix_len); + } + memcpy(result + prefix_len, host, host_len); + memcpy(result + prefix_len + host_len, portbuf, portlen); + + result[total_len] = '\0'; + + *message = result; + + return total_len; +} + /* {{{ php_fsockopen() */ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) @@ -62,11 +84,12 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) } if (persistent) { - spprintf(&hashkey, 0, "pfsockopen__%s:" ZEND_LONG_FMT, host, port); + php_fsockopen_format_host_port(&hashkey, "pfsockopen__", strlen("pfsockopen__"), host, + host_len, port); } if (port > 0) { - hostname_len = spprintf(&hostname, 0, "%s:" ZEND_LONG_FMT, host, port); + hostname_len = php_fsockopen_format_host_port(&hostname, "", 0, host, host_len, port); } else { hostname_len = host_len; hostname = host; diff --git a/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt b/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt new file mode 100644 index 000000000000..7556c3be94cc --- /dev/null +++ b/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt @@ -0,0 +1,21 @@ +--TEST-- +GHSA-3cr5-j632-f35r: Null byte termination in fsockopen() +--FILE-- + +--EXPECTF-- + +Warning: fsockopen(): Unable to connect to localhost:%d (The hostname must not contain null bytes) in %s +bool(false) diff --git a/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt b/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt new file mode 100644 index 000000000000..52f9263c99aa --- /dev/null +++ b/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt @@ -0,0 +1,26 @@ +--TEST-- +GHSA-3cr5-j632-f35r: Null byte termination in stream_socket_client() +--FILE-- + +--EXPECTF-- + +Warning: stream_socket_client(): Unable to connect to tcp://localhost\0.example.com:%d (The hostname must not contain null bytes) in %s +bool(false) diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index 606d1499456a..b1d89bc44cb2 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -616,12 +616,15 @@ static inline char *parse_ip_address_ex(const char *str, size_t str_len, int *po char *colon; char *host = NULL; -#ifdef HAVE_IPV6 - char *p; + if (memchr(str, '\0', str_len)) { + *err = ZSTR_INIT_LITERAL("The hostname must not contain null bytes", 0); + return NULL; + } +#ifdef HAVE_IPV6 if (*(str) == '[' && str_len > 1) { /* IPV6 notation to specify raw address with port (i.e. [fe80::1]:80) */ - p = memchr(str + 1, ']', str_len - 2); + char *p = memchr(str + 1, ']', str_len - 2); if (!p || *(p + 1) != ':') { if (get_err) { *err = strpprintf(0, "Failed to parse IPv6 address \"%s\"", str); From 545d1536d8b2c16ff9c844791d8d9d39530e0313 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 4 Mar 2025 17:23:01 +0100 Subject: [PATCH 37/69] Fix GHSA-hrwm-9436-5mv3: pgsql escaping no error checks This adds error checks for escape function is pgsql and pdo_pgsql extensions. It prevents possibility of storing not properly escaped data which could potentially lead to some security issues. --- ext/pdo_pgsql/pgsql_driver.c | 10 +- ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt | 24 ++++ ext/pgsql/pgsql.c | 125 ++++++++++++++++--- ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt | 64 ++++++++++ 4 files changed, 202 insertions(+), 21 deletions(-) create mode 100644 ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt create mode 100644 ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c index 46b3f25f4086..1cccfd2ab07a 100644 --- a/ext/pdo_pgsql/pgsql_driver.c +++ b/ext/pdo_pgsql/pgsql_driver.c @@ -353,11 +353,15 @@ static zend_string* pgsql_handle_quoter(pdo_dbh_t *dbh, const zend_string *unquo zend_string *quoted_str; pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data; size_t tmp_len; + int err; switch (paramtype) { case PDO_PARAM_LOB: /* escapedlen returned by PQescapeBytea() accounts for trailing 0 */ escaped = PQescapeByteaConn(H->server, (unsigned char *)ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), &tmp_len); + if (escaped == NULL) { + return NULL; + } quotedlen = tmp_len + 1; quoted = emalloc(quotedlen + 1); memcpy(quoted+1, escaped, quotedlen-2); @@ -369,7 +373,11 @@ static zend_string* pgsql_handle_quoter(pdo_dbh_t *dbh, const zend_string *unquo default: quoted = safe_emalloc(2, ZSTR_LEN(unquoted), 3); quoted[0] = '\''; - quotedlen = PQescapeStringConn(H->server, quoted + 1, ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), NULL); + quotedlen = PQescapeStringConn(H->server, quoted + 1, ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), &err); + if (err) { + efree(quoted); + return NULL; + } quoted[quotedlen + 1] = '\''; quoted[quotedlen + 2] = '\0'; quotedlen += 2; diff --git a/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt b/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt new file mode 100644 index 000000000000..8566a26753b4 --- /dev/null +++ b/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt @@ -0,0 +1,24 @@ +--TEST-- +#GHSA-hrwm-9436-5mv3: pdo_pgsql extension does not check for errors during escaping +--EXTENSIONS-- +pdo +pdo_pgsql +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +$invalid = "ABC\xff\x30';"; +var_dump($db->quote($invalid)); + +?> +--EXPECT-- +bool(false) diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c index 63acd26ea01f..11ce814cbec0 100644 --- a/ext/pgsql/pgsql.c +++ b/ext/pgsql/pgsql.c @@ -3269,8 +3269,14 @@ PHP_FUNCTION(pg_escape_string) to = zend_string_safe_alloc(ZSTR_LEN(from), 2, 0, 0); if (link) { + int err; pgsql = link->conn; - ZSTR_LEN(to) = PQescapeStringConn(pgsql, ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from), NULL); + ZSTR_LEN(to) = PQescapeStringConn(pgsql, ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from), &err); + if (err) { + zend_argument_value_error(ZEND_NUM_ARGS(), "Escaping string failed"); + zend_string_efree(to); + RETURN_THROWS(); + } } else { ZSTR_LEN(to) = PQescapeString(ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from)); @@ -3313,6 +3319,10 @@ PHP_FUNCTION(pg_escape_bytea) } else { to = (char *)PQescapeBytea((unsigned char *)ZSTR_VAL(from), ZSTR_LEN(from), &to_len); } + if (to == NULL) { + zend_argument_value_error(ZEND_NUM_ARGS(), "Escape failure"); + RETURN_THROWS(); + } RETVAL_STRINGL(to, to_len-1); /* to_len includes additional '\0' */ PQfreemem(to); @@ -4245,7 +4255,7 @@ PHP_PGSQL_API zend_result php_pgsql_meta_data(PGconn *pg_link, const zend_string char *escaped; smart_str querystr = {0}; size_t new_len; - int i, num_rows; + int i, num_rows, err; zval elem; ZEND_ASSERT(ZSTR_LEN(table_name) != 0); @@ -4283,7 +4293,14 @@ PHP_PGSQL_API zend_result php_pgsql_meta_data(PGconn *pg_link, const zend_string "WHERE a.attnum > 0 AND c.relname = '"); } escaped = (char *)safe_emalloc(strlen(tmp_name2), 2, 1); - new_len = PQescapeStringConn(pg_link, escaped, tmp_name2, strlen(tmp_name2), NULL); + new_len = PQescapeStringConn(pg_link, escaped, tmp_name2, strlen(tmp_name2), &err); + if (err) { + php_error_docref(NULL, E_WARNING, "Escaping table name '%s' failed", ZSTR_VAL(table_name)); + efree(src); + efree(escaped); + smart_str_free(&querystr); + return FAILURE; + } if (new_len) { smart_str_appendl(&querystr, escaped, new_len); } @@ -4291,7 +4308,14 @@ PHP_PGSQL_API zend_result php_pgsql_meta_data(PGconn *pg_link, const zend_string smart_str_appends(&querystr, "' AND n.nspname = '"); escaped = (char *)safe_emalloc(strlen(tmp_name), 2, 1); - new_len = PQescapeStringConn(pg_link, escaped, tmp_name, strlen(tmp_name), NULL); + new_len = PQescapeStringConn(pg_link, escaped, tmp_name, strlen(tmp_name), &err); + if (err) { + php_error_docref(NULL, E_WARNING, "Escaping table namespace '%s' failed", ZSTR_VAL(table_name)); + efree(src); + efree(escaped); + smart_str_free(&querystr); + return FAILURE; + } if (new_len) { smart_str_appendl(&querystr, escaped, new_len); } @@ -4552,7 +4576,7 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * { zend_string *field = NULL; zval meta, *def, *type, *not_null, *has_default, *is_enum, *val, new_val; - int err = 0, skip_field; + int err = 0, escape_err = 0, skip_field; php_pgsql_data_type data_type; ZEND_ASSERT(pg_link != NULL); @@ -4804,8 +4828,13 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * /* PostgreSQL ignores \0 */ str = zend_string_alloc(Z_STRLEN_P(val) * 2, 0); /* better to use PGSQLescapeLiteral since PGescapeStringConn does not handle special \ */ - ZSTR_LEN(str) = PQescapeStringConn(pg_link, ZSTR_VAL(str), Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); - ZVAL_STR(&new_val, php_pgsql_add_quotes(str)); + ZSTR_LEN(str) = PQescapeStringConn(pg_link, ZSTR_VAL(str), + Z_STRVAL_P(val), Z_STRLEN_P(val), &escape_err); + if (escape_err) { + err = 1; + } else { + ZVAL_STR(&new_val, php_pgsql_add_quotes(str)); + } zend_string_release_ex(str, false); } break; @@ -4828,7 +4857,14 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * } PGSQL_CONV_CHECK_IGNORE(); if (err) { - zend_type_error("%s(): Field \"%s\" must be of type string|null, %s given", get_active_function_name(), ZSTR_VAL(field), Z_STRVAL_P(type)); + if (escape_err) { + php_error_docref(NULL, E_NOTICE, + "String value escaping failed for PostgreSQL '%s' (%s)", + Z_STRVAL_P(type), ZSTR_VAL(field)); + } else { + zend_type_error("%s(): Field \"%s\" must be of type string|null, %s given", + get_active_function_name(), ZSTR_VAL(field), Z_STRVAL_P(type)); + } } break; @@ -5103,6 +5139,11 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * zend_string *tmp_zstr; tmp = PQescapeByteaConn(pg_link, (unsigned char *)Z_STRVAL_P(val), Z_STRLEN_P(val), &to_len); + if (tmp == NULL) { + php_error_docref(NULL, E_NOTICE, "Escaping value failed for %s field (%s)", Z_STRVAL_P(type), ZSTR_VAL(field)); + err = 1; + break; + } tmp_zstr = zend_string_init((char *)tmp, to_len - 1, false); /* PQescapeBytea's to_len includes additional '\0' */ PQfreemem(tmp); @@ -5181,6 +5222,12 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * zend_hash_update(Z_ARRVAL_P(result), field, &new_val); } else { char *escaped = PQescapeIdentifier(pg_link, ZSTR_VAL(field), ZSTR_LEN(field)); + if (escaped == NULL) { + /* This cannot fail because of invalid string but only due to failed memory allocation */ + php_error_docref(NULL, E_NOTICE, "Escaping field '%s' failed", ZSTR_VAL(field)); + err = 1; + break; + } add_assoc_zval(result, escaped, &new_val); PQfreemem(escaped); } @@ -5259,7 +5306,7 @@ static bool do_exec(smart_str *querystr, ExecStatusType expect, PGconn *pg_link, } /* }}} */ -static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const zend_string *table) /* {{{ */ +static inline zend_result build_tablename(smart_str *querystr, PGconn *pg_link, const zend_string *table) /* {{{ */ { /* schema.table should be "schema"."table" */ const char *dot = memchr(ZSTR_VAL(table), '.', ZSTR_LEN(table)); @@ -5269,6 +5316,10 @@ static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const z smart_str_appendl(querystr, ZSTR_VAL(table), len); } else { char *escaped = PQescapeIdentifier(pg_link, ZSTR_VAL(table), len); + if (escaped == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape table name '%s'", ZSTR_VAL(table)); + return FAILURE; + } smart_str_appends(querystr, escaped); PQfreemem(escaped); } @@ -5281,11 +5332,17 @@ static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const z smart_str_appendl(querystr, after_dot, len); } else { char *escaped = PQescapeIdentifier(pg_link, after_dot, len); + if (escaped == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape table name '%s'", ZSTR_VAL(table)); + return FAILURE; + } smart_str_appendc(querystr, '.'); smart_str_appends(querystr, escaped); PQfreemem(escaped); } } + + return SUCCESS; } /* }}} */ @@ -5306,7 +5363,9 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t ZVAL_UNDEF(&converted); if (zend_hash_num_elements(Z_ARRVAL_P(var_array)) == 0) { smart_str_appends(&querystr, "INSERT INTO "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " DEFAULT VALUES"); goto no_values; @@ -5322,7 +5381,9 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "INSERT INTO "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " ("); ZEND_HASH_FOREACH_STR_KEY(Z_ARRVAL_P(var_array), fld) { @@ -5332,6 +5393,10 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t } if (opt & PGSQL_DML_ESCAPE) { tmp = PQescapeIdentifier(pg_link, ZSTR_VAL(fld), ZSTR_LEN(fld) + 1); + if (tmp == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s'", ZSTR_VAL(fld)); + goto cleanup; + } smart_str_appends(&querystr, tmp); PQfreemem(tmp); } else { @@ -5343,15 +5408,19 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t smart_str_appends(&querystr, ") VALUES ("); /* make values string */ - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(var_array), val) { + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(var_array), fld, val) { /* we can avoid the key_type check here, because we tested it in the other loop */ switch (Z_TYPE_P(val)) { case IS_STRING: if (opt & PGSQL_DML_ESCAPE) { - size_t new_len; - char *tmp; - tmp = (char *)safe_emalloc(Z_STRLEN_P(val), 2, 1); - new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); + int error; + char *tmp = safe_emalloc(Z_STRLEN_P(val), 2, 1); + size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), &error); + if (error) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s' value", ZSTR_VAL(fld)); + efree(tmp); + goto cleanup; + } smart_str_appendc(&querystr, '\''); smart_str_appendl(&querystr, tmp, new_len); smart_str_appendc(&querystr, '\''); @@ -5507,6 +5576,10 @@ static inline int build_assignment_string(PGconn *pg_link, smart_str *querystr, } if (opt & PGSQL_DML_ESCAPE) { char *tmp = PQescapeIdentifier(pg_link, ZSTR_VAL(fld), ZSTR_LEN(fld) + 1); + if (tmp == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s'", ZSTR_VAL(fld)); + return -1; + } smart_str_appends(querystr, tmp); PQfreemem(tmp); } else { @@ -5522,8 +5595,14 @@ static inline int build_assignment_string(PGconn *pg_link, smart_str *querystr, switch (Z_TYPE_P(val)) { case IS_STRING: if (opt & PGSQL_DML_ESCAPE) { + int error; char *tmp = (char *)safe_emalloc(Z_STRLEN_P(val), 2, 1); - size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); + size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), &error); + if (error) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s' value", ZSTR_VAL(fld)); + efree(tmp); + return -1; + } smart_str_appendc(querystr, '\''); smart_str_appendl(querystr, tmp, new_len); smart_str_appendc(querystr, '\''); @@ -5591,7 +5670,9 @@ PHP_PGSQL_API zend_result php_pgsql_update(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "UPDATE "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " SET "); if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(var_array), 0, ",", 1, opt)) @@ -5694,7 +5775,9 @@ PHP_PGSQL_API zend_result php_pgsql_delete(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "DELETE FROM "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " WHERE "); if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(ids_array), 1, " AND ", sizeof(" AND ")-1, opt)) @@ -5834,7 +5917,9 @@ PHP_PGSQL_API zend_result php_pgsql_select(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "SELECT * FROM "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " WHERE "); if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(ids_array), 1, " AND ", sizeof(" AND ")-1, opt)) diff --git a/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt b/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt new file mode 100644 index 000000000000..c1c5e05dce62 --- /dev/null +++ b/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt @@ -0,0 +1,64 @@ +--TEST-- +#GHSA-hrwm-9436-5mv3: pgsql extension does not check for errors during escaping +--EXTENSIONS-- +pgsql +--SKIPIF-- + +--FILE-- + 'test'])); // table name str escape in php_pgsql_meta_data +var_dump(pg_insert($db, "$invalid.tbl", ['bar' => 'test'])); // schema name str escape in php_pgsql_meta_data +var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', ['bar' => $invalid])); // converted value str escape in php_pgsql_convert +var_dump(pg_insert($db, $invalid, [])); // ident escape in build_tablename +var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', [$invalid => 'foo'], $flags)); // ident escape for field php_pgsql_insert +var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', ['bar' => $invalid], $flags)); // str escape for field value in php_pgsql_insert +var_dump(pg_update($db, 'ghsa_hrmw_9436_5mv3', ['bar' => 'val'], [$invalid => 'test'], $flags)); // ident escape in build_assignment_string +var_dump(pg_update($db, 'ghsa_hrmw_9436_5mv3', ['bar' => 'val'], ['bar' => $invalid], $flags)); // invalid str escape in build_assignment_string +var_dump(pg_escape_literal($db, $invalid)); // pg_escape_literal escape +var_dump(pg_escape_identifier($db, $invalid)); // pg_escape_identifier escape + +?> +--EXPECTF-- + +Warning: pg_insert(): Escaping table name 'ABC%s';' failed in %s on line %d +bool(false) + +Warning: pg_insert(): Escaping table namespace 'ABC%s';.tbl' failed in %s on line %d +bool(false) + +Notice: pg_insert(): String value escaping failed for PostgreSQL 'text' (bar) in %s on line %d +bool(false) + +Notice: pg_insert(): Failed to escape table name 'ABC%s';' in %s on line %d +bool(false) + +Notice: pg_insert(): Failed to escape field 'ABC%s';' in %s on line %d +bool(false) + +Notice: pg_insert(): Failed to escape field 'bar' value in %s on line %d +bool(false) + +Notice: pg_update(): Failed to escape field 'ABC%s';' in %s on line %d +bool(false) + +Notice: pg_update(): Failed to escape field 'bar' value in %s on line %d +bool(false) + +Warning: pg_escape_literal(): Failed to escape in %s on line %d +bool(false) + +Warning: pg_escape_identifier(): Failed to escape in %s on line %d +bool(false) From dd060656d31fc2ba3fe9acd42bbc19d1c96ff914 Mon Sep 17 00:00:00 2001 From: Ahmed Lekssays Date: Tue, 3 Jun 2025 09:00:55 +0000 Subject: [PATCH 38/69] Fix GHSA-453j-q27h-5p8x Libxml versions prior to 2.13 cannot correctly handle a call to xmlNodeSetName() with a name longer than 2G. It will leave the node object in an invalid state with a NULL name. This later causes a NULL pointer dereference when using the name during message serialization. To solve this, implement a workaround that resets the name to the sentinel name if this situation arises. Versions of libxml of 2.13 and higher are not affected. This can be exploited if a SoapVar is created with a fully qualified name that is longer than 2G. This would be possible if some application code uses a namespace prefix from an untrusted source like from a remote SOAP service. Co-authored-by: Niels Dossche <7771979+nielsdos@users.noreply.github.com> --- ext/soap/soap.c | 6 ++-- ext/soap/tests/soap_qname_crash.phpt | 48 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 ext/soap/tests/soap_qname_crash.phpt diff --git a/ext/soap/soap.c b/ext/soap/soap.c index 3f8a50ba6ed2..fc51e32658f3 100644 --- a/ext/soap/soap.c +++ b/ext/soap/soap.c @@ -3980,8 +3980,10 @@ static xmlNodePtr serialize_zval(zval *val, sdlParamPtr param, char *paramName, } xmlParam = master_to_xml(enc, val, style, parent); zval_ptr_dtor(&defval); - if (!strcmp((char*)xmlParam->name, "BOGUS")) { - xmlNodeSetName(xmlParam, BAD_CAST(paramName)); + if (xmlParam != NULL) { + if (xmlParam->name == NULL || strcmp((char*)xmlParam->name, "BOGUS") == 0) { + xmlNodeSetName(xmlParam, BAD_CAST(paramName)); + } } return xmlParam; } diff --git a/ext/soap/tests/soap_qname_crash.phpt b/ext/soap/tests/soap_qname_crash.phpt new file mode 100644 index 000000000000..bcf01d574fab --- /dev/null +++ b/ext/soap/tests/soap_qname_crash.phpt @@ -0,0 +1,48 @@ +--TEST-- +Test SoapClient with excessively large QName prefix in SoapVar +--EXTENSIONS-- +soap +--SKIPIF-- + +--INI-- +memory_limit=6144M +--FILE-- + 'http://127.0.0.1/', + 'uri' => 'urn:dummy', + 'trace' => 1, + 'exceptions' => true, +]; +$client = new TestSoapClient(null, $options); +$client->__soapCall("DummyFunction", [$var]); +?> +--EXPECT-- +Attempting to create SoapVar with very large QName +Attempting encoding + +value From fc49d334496c865dd7e60d8b6b360313823162ef Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 26 Jun 2025 11:29:28 +0200 Subject: [PATCH 39/69] Update NEWS with entries for security fixes --- NEWS | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/NEWS b/NEWS index 267681cfa265..44c964099bc8 100644 --- a/NEWS +++ b/NEWS @@ -91,6 +91,8 @@ PHP NEWS - PGSQL: . Fix warning not being emitted when failure to cancel a query with pg_cancel_query(). (Girgias) + . Fixed GHSA-hrwm-9436-5mv3 (pgsql extension does not check for errors during + escaping). (CVE-2025-1735) (Jakub Zelenka) - Random: . Fix reference type confusion and leak in user random engine. @@ -102,6 +104,12 @@ PHP NEWS - Soap: . Fix memory leaks in php_http.c when call_user_function() fails. (nielsdos) + . Fixed GHSA-453j-q27h-5p8x (NULL Pointer Dereference in PHP SOAP ExtensionAdd commentMore actions + via Large XML Namespace Prefix). (CVE-2025-6491) (Lekssays, nielsdos) + +- Standard: + . Fixed GHSA-3cr5-j632-f35r (Null byte termination in hostnames). + (CVE-2025-1220) (Jakub Zelenka) - Tidy: . Fix memory leak in tidy output handler on error. (nielsdos) From 27e67cc3712ce0bc94fd4b2bfbb7155d89ec52d6 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 10 Apr 2025 15:15:36 +0200 Subject: [PATCH 40/69] Fix GHSA-3cr5-j632-f35r: Null byte in hostnames This fixes stream_socket_client() and fsockopen(). Specifically it adds a check to parse_ip_address_ex and it also makes sure that the \0 is not ignored in fsockopen() hostname formatting. --- ext/standard/fsock.c | 27 +++++++++++++++++-- .../tests/network/ghsa-3cr5-j632-f35r.phpt | 21 +++++++++++++++ .../tests/streams/ghsa-3cr5-j632-f35r.phpt | 26 ++++++++++++++++++ main/streams/xp_socket.c | 9 ++++--- 4 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt create mode 100644 ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt diff --git a/ext/standard/fsock.c b/ext/standard/fsock.c index cb7a471e935a..2b9e00a57554 100644 --- a/ext/standard/fsock.c +++ b/ext/standard/fsock.c @@ -23,6 +23,28 @@ #include "php_network.h" #include "file.h" +static size_t php_fsockopen_format_host_port(char **message, const char *prefix, size_t prefix_len, + const char *host, size_t host_len, zend_long port) +{ + char portbuf[32]; + int portlen = snprintf(portbuf, sizeof(portbuf), ":" ZEND_LONG_FMT, port); + size_t total_len = prefix_len + host_len + portlen; + + char *result = emalloc(total_len + 1); + + if (prefix_len > 0) { + memcpy(result, prefix, prefix_len); + } + memcpy(result + prefix_len, host, host_len); + memcpy(result + prefix_len + host_len, portbuf, portlen); + + result[total_len] = '\0'; + + *message = result; + + return total_len; +} + /* {{{ php_fsockopen() */ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) @@ -62,11 +84,12 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) } if (persistent) { - spprintf(&hashkey, 0, "pfsockopen__%s:" ZEND_LONG_FMT, host, port); + php_fsockopen_format_host_port(&hashkey, "pfsockopen__", strlen("pfsockopen__"), host, + host_len, port); } if (port > 0) { - hostname_len = spprintf(&hostname, 0, "%s:" ZEND_LONG_FMT, host, port); + hostname_len = php_fsockopen_format_host_port(&hostname, "", 0, host, host_len, port); } else { hostname_len = host_len; hostname = host; diff --git a/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt b/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt new file mode 100644 index 000000000000..7556c3be94cc --- /dev/null +++ b/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt @@ -0,0 +1,21 @@ +--TEST-- +GHSA-3cr5-j632-f35r: Null byte termination in fsockopen() +--FILE-- + +--EXPECTF-- + +Warning: fsockopen(): Unable to connect to localhost:%d (The hostname must not contain null bytes) in %s +bool(false) diff --git a/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt b/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt new file mode 100644 index 000000000000..52f9263c99aa --- /dev/null +++ b/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt @@ -0,0 +1,26 @@ +--TEST-- +GHSA-3cr5-j632-f35r: Null byte termination in stream_socket_client() +--FILE-- + +--EXPECTF-- + +Warning: stream_socket_client(): Unable to connect to tcp://localhost\0.example.com:%d (The hostname must not contain null bytes) in %s +bool(false) diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index 9987f871a7af..ef4fa6f86e38 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -594,12 +594,15 @@ static inline char *parse_ip_address_ex(const char *str, size_t str_len, int *po char *colon; char *host = NULL; -#ifdef HAVE_IPV6 - char *p; + if (memchr(str, '\0', str_len)) { + *err = ZSTR_INIT_LITERAL("The hostname must not contain null bytes", 0); + return NULL; + } +#ifdef HAVE_IPV6 if (*(str) == '[' && str_len > 1) { /* IPV6 notation to specify raw address with port (i.e. [fe80::1]:80) */ - p = memchr(str + 1, ']', str_len - 2); + char *p = memchr(str + 1, ']', str_len - 2); if (!p || *(p + 1) != ':') { if (get_err) { *err = strpprintf(0, "Failed to parse IPv6 address \"%s\"", str); From a2cdff5583ad2bfe9a27fba71e2b1fc423296dc0 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 4 Mar 2025 17:23:01 +0100 Subject: [PATCH 41/69] Fix GHSA-hrwm-9436-5mv3: pgsql escaping no error checks This adds error checks for escape function is pgsql and pdo_pgsql extensions. It prevents possibility of storing not properly escaped data which could potentially lead to some security issues. --- ext/pdo_pgsql/pgsql_driver.c | 10 +- ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt | 24 ++++ ext/pgsql/pgsql.c | 126 ++++++++++++++++--- ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt | 64 ++++++++++ 4 files changed, 203 insertions(+), 21 deletions(-) create mode 100644 ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt create mode 100644 ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c index ec4d5ec65866..ed3aeaf2f772 100644 --- a/ext/pdo_pgsql/pgsql_driver.c +++ b/ext/pdo_pgsql/pgsql_driver.c @@ -353,11 +353,15 @@ static zend_string* pgsql_handle_quoter(pdo_dbh_t *dbh, const zend_string *unquo zend_string *quoted_str; pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data; size_t tmp_len; + int err; switch (paramtype) { case PDO_PARAM_LOB: /* escapedlen returned by PQescapeBytea() accounts for trailing 0 */ escaped = PQescapeByteaConn(H->server, (unsigned char *)ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), &tmp_len); + if (escaped == NULL) { + return NULL; + } quotedlen = tmp_len + 1; quoted = emalloc(quotedlen + 1); memcpy(quoted+1, escaped, quotedlen-2); @@ -369,7 +373,11 @@ static zend_string* pgsql_handle_quoter(pdo_dbh_t *dbh, const zend_string *unquo default: quoted = safe_emalloc(2, ZSTR_LEN(unquoted), 3); quoted[0] = '\''; - quotedlen = PQescapeStringConn(H->server, quoted + 1, ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), NULL); + quotedlen = PQescapeStringConn(H->server, quoted + 1, ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), &err); + if (err) { + efree(quoted); + return NULL; + } quoted[quotedlen + 1] = '\''; quoted[quotedlen + 2] = '\0'; quotedlen += 2; diff --git a/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt b/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt new file mode 100644 index 000000000000..8566a26753b4 --- /dev/null +++ b/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt @@ -0,0 +1,24 @@ +--TEST-- +#GHSA-hrwm-9436-5mv3: pdo_pgsql extension does not check for errors during escaping +--EXTENSIONS-- +pdo +pdo_pgsql +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +$invalid = "ABC\xff\x30';"; +var_dump($db->quote($invalid)); + +?> +--EXPECT-- +bool(false) diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c index 6e04848bdea0..cfa915206b8e 100644 --- a/ext/pgsql/pgsql.c +++ b/ext/pgsql/pgsql.c @@ -3203,8 +3203,14 @@ PHP_FUNCTION(pg_escape_string) to = zend_string_safe_alloc(ZSTR_LEN(from), 2, 0, 0); if (link) { + int err; pgsql = link->conn; - ZSTR_LEN(to) = PQescapeStringConn(pgsql, ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from), NULL); + ZSTR_LEN(to) = PQescapeStringConn(pgsql, ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from), &err); + if (err) { + zend_argument_value_error(ZEND_NUM_ARGS(), "Escaping string failed"); + zend_string_efree(to); + RETURN_THROWS(); + } } else { ZSTR_LEN(to) = PQescapeString(ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from)); @@ -3247,6 +3253,10 @@ PHP_FUNCTION(pg_escape_bytea) } else { to = (char *)PQescapeBytea((unsigned char *)ZSTR_VAL(from), ZSTR_LEN(from), &to_len); } + if (to == NULL) { + zend_argument_value_error(ZEND_NUM_ARGS(), "Escape failure"); + RETURN_THROWS(); + } RETVAL_STRINGL(to, to_len-1); /* to_len includes additional '\0' */ PQfreemem(to); @@ -4163,7 +4173,7 @@ PHP_PGSQL_API zend_result php_pgsql_meta_data(PGconn *pg_link, const zend_string char *escaped; smart_str querystr = {0}; size_t new_len; - int i, num_rows; + int i, num_rows, err; zval elem; ZEND_ASSERT(ZSTR_LEN(table_name) != 0); @@ -4202,7 +4212,14 @@ PHP_PGSQL_API zend_result php_pgsql_meta_data(PGconn *pg_link, const zend_string "WHERE a.attnum > 0 AND c.relname = '"); } escaped = (char *)safe_emalloc(strlen(tmp_name2), 2, 1); - new_len = PQescapeStringConn(pg_link, escaped, tmp_name2, strlen(tmp_name2), NULL); + new_len = PQescapeStringConn(pg_link, escaped, tmp_name2, strlen(tmp_name2), &err); + if (err) { + php_error_docref(NULL, E_WARNING, "Escaping table name '%s' failed", ZSTR_VAL(table_name)); + efree(src); + efree(escaped); + smart_str_free(&querystr); + return FAILURE; + } if (new_len) { smart_str_appendl(&querystr, escaped, new_len); } @@ -4210,7 +4227,14 @@ PHP_PGSQL_API zend_result php_pgsql_meta_data(PGconn *pg_link, const zend_string smart_str_appends(&querystr, "' AND n.nspname = '"); escaped = (char *)safe_emalloc(strlen(tmp_name), 2, 1); - new_len = PQescapeStringConn(pg_link, escaped, tmp_name, strlen(tmp_name), NULL); + new_len = PQescapeStringConn(pg_link, escaped, tmp_name, strlen(tmp_name), &err); + if (err) { + php_error_docref(NULL, E_WARNING, "Escaping table namespace '%s' failed", ZSTR_VAL(table_name)); + efree(src); + efree(escaped); + smart_str_free(&querystr); + return FAILURE; + } if (new_len) { smart_str_appendl(&querystr, escaped, new_len); } @@ -4471,7 +4495,7 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * { zend_string *field = NULL; zval meta, *def, *type, *not_null, *has_default, *is_enum, *val, new_val; - int err = 0, skip_field; + int err = 0, escape_err = 0, skip_field; php_pgsql_data_type data_type; ZEND_ASSERT(pg_link != NULL); @@ -4724,8 +4748,13 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * /* PostgreSQL ignores \0 */ str = zend_string_alloc(Z_STRLEN_P(val) * 2, 0); /* better to use PGSQLescapeLiteral since PGescapeStringConn does not handle special \ */ - ZSTR_LEN(str) = PQescapeStringConn(pg_link, ZSTR_VAL(str), Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); - ZVAL_STR(&new_val, php_pgsql_add_quotes(str)); + ZSTR_LEN(str) = PQescapeStringConn(pg_link, ZSTR_VAL(str), + Z_STRVAL_P(val), Z_STRLEN_P(val), &escape_err); + if (escape_err) { + err = 1; + } else { + ZVAL_STR(&new_val, php_pgsql_add_quotes(str)); + } zend_string_release_ex(str, false); } break; @@ -4748,7 +4777,15 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * } PGSQL_CONV_CHECK_IGNORE(); if (err) { - php_error_docref(NULL, E_NOTICE, "Expects NULL, string, long or double value for PostgreSQL '%s' (%s)", Z_STRVAL_P(type), ZSTR_VAL(field)); + if (escape_err) { + php_error_docref(NULL, E_NOTICE, + "String value escaping failed for PostgreSQL '%s' (%s)", + Z_STRVAL_P(type), ZSTR_VAL(field)); + } else { + php_error_docref(NULL, E_NOTICE, + "Expects NULL, string, long or double value for PostgreSQL '%s' (%s)", + Z_STRVAL_P(type), ZSTR_VAL(field)); + } } break; @@ -5019,6 +5056,11 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * zend_string *tmp_zstr; tmp = PQescapeByteaConn(pg_link, (unsigned char *)Z_STRVAL_P(val), Z_STRLEN_P(val), &to_len); + if (tmp == NULL) { + php_error_docref(NULL, E_NOTICE, "Escaping value failed for %s field (%s)", Z_STRVAL_P(type), ZSTR_VAL(field)); + err = 1; + break; + } tmp_zstr = zend_string_init((char *)tmp, to_len - 1, false); /* PQescapeBytea's to_len includes additional '\0' */ PQfreemem(tmp); @@ -5097,6 +5139,12 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * zend_hash_update(Z_ARRVAL_P(result), field, &new_val); } else { char *escaped = PQescapeIdentifier(pg_link, ZSTR_VAL(field), ZSTR_LEN(field)); + if (escaped == NULL) { + /* This cannot fail because of invalid string but only due to failed memory allocation */ + php_error_docref(NULL, E_NOTICE, "Escaping field '%s' failed", ZSTR_VAL(field)); + err = 1; + break; + } add_assoc_zval(result, escaped, &new_val); PQfreemem(escaped); } @@ -5175,7 +5223,7 @@ static bool do_exec(smart_str *querystr, ExecStatusType expect, PGconn *pg_link, } /* }}} */ -static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const zend_string *table) /* {{{ */ +static inline zend_result build_tablename(smart_str *querystr, PGconn *pg_link, const zend_string *table) /* {{{ */ { /* schema.table should be "schema"."table" */ const char *dot = memchr(ZSTR_VAL(table), '.', ZSTR_LEN(table)); @@ -5185,6 +5233,10 @@ static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const z smart_str_appendl(querystr, ZSTR_VAL(table), len); } else { char *escaped = PQescapeIdentifier(pg_link, ZSTR_VAL(table), len); + if (escaped == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape table name '%s'", ZSTR_VAL(table)); + return FAILURE; + } smart_str_appends(querystr, escaped); PQfreemem(escaped); } @@ -5197,11 +5249,17 @@ static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const z smart_str_appendl(querystr, after_dot, len); } else { char *escaped = PQescapeIdentifier(pg_link, after_dot, len); + if (escaped == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape table name '%s'", ZSTR_VAL(table)); + return FAILURE; + } smart_str_appendc(querystr, '.'); smart_str_appends(querystr, escaped); PQfreemem(escaped); } } + + return SUCCESS; } /* }}} */ @@ -5222,7 +5280,9 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t ZVAL_UNDEF(&converted); if (zend_hash_num_elements(Z_ARRVAL_P(var_array)) == 0) { smart_str_appends(&querystr, "INSERT INTO "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " DEFAULT VALUES"); goto no_values; @@ -5238,7 +5298,9 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "INSERT INTO "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " ("); ZEND_HASH_FOREACH_STR_KEY(Z_ARRVAL_P(var_array), fld) { @@ -5248,6 +5310,10 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t } if (opt & PGSQL_DML_ESCAPE) { tmp = PQescapeIdentifier(pg_link, ZSTR_VAL(fld), ZSTR_LEN(fld) + 1); + if (tmp == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s'", ZSTR_VAL(fld)); + goto cleanup; + } smart_str_appends(&querystr, tmp); PQfreemem(tmp); } else { @@ -5259,15 +5325,19 @@ PHP_PGSQL_API zend_result php_pgsql_insert(PGconn *pg_link, const zend_string *t smart_str_appends(&querystr, ") VALUES ("); /* make values string */ - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(var_array), val) { + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(var_array), fld, val) { /* we can avoid the key_type check here, because we tested it in the other loop */ switch (Z_TYPE_P(val)) { case IS_STRING: if (opt & PGSQL_DML_ESCAPE) { - size_t new_len; - char *tmp; - tmp = (char *)safe_emalloc(Z_STRLEN_P(val), 2, 1); - new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); + int error; + char *tmp = safe_emalloc(Z_STRLEN_P(val), 2, 1); + size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), &error); + if (error) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s' value", ZSTR_VAL(fld)); + efree(tmp); + goto cleanup; + } smart_str_appendc(&querystr, '\''); smart_str_appendl(&querystr, tmp, new_len); smart_str_appendc(&querystr, '\''); @@ -5423,6 +5493,10 @@ static inline int build_assignment_string(PGconn *pg_link, smart_str *querystr, } if (opt & PGSQL_DML_ESCAPE) { char *tmp = PQescapeIdentifier(pg_link, ZSTR_VAL(fld), ZSTR_LEN(fld) + 1); + if (tmp == NULL) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s'", ZSTR_VAL(fld)); + return -1; + } smart_str_appends(querystr, tmp); PQfreemem(tmp); } else { @@ -5438,8 +5512,14 @@ static inline int build_assignment_string(PGconn *pg_link, smart_str *querystr, switch (Z_TYPE_P(val)) { case IS_STRING: if (opt & PGSQL_DML_ESCAPE) { + int error; char *tmp = (char *)safe_emalloc(Z_STRLEN_P(val), 2, 1); - size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); + size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), &error); + if (error) { + php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s' value", ZSTR_VAL(fld)); + efree(tmp); + return -1; + } smart_str_appendc(querystr, '\''); smart_str_appendl(querystr, tmp, new_len); smart_str_appendc(querystr, '\''); @@ -5507,7 +5587,9 @@ PHP_PGSQL_API zend_result php_pgsql_update(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "UPDATE "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " SET "); if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(var_array), 0, ",", 1, opt)) @@ -5610,7 +5692,9 @@ PHP_PGSQL_API zend_result php_pgsql_delete(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "DELETE FROM "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " WHERE "); if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(ids_array), 1, " AND ", sizeof(" AND ")-1, opt)) @@ -5750,7 +5834,9 @@ PHP_PGSQL_API zend_result php_pgsql_select(PGconn *pg_link, const zend_string *t } smart_str_appends(&querystr, "SELECT * FROM "); - build_tablename(&querystr, pg_link, table); + if (build_tablename(&querystr, pg_link, table) == FAILURE) { + goto cleanup; + } smart_str_appends(&querystr, " WHERE "); if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(ids_array), 1, " AND ", sizeof(" AND ")-1, opt)) diff --git a/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt b/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt new file mode 100644 index 000000000000..c1c5e05dce62 --- /dev/null +++ b/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt @@ -0,0 +1,64 @@ +--TEST-- +#GHSA-hrwm-9436-5mv3: pgsql extension does not check for errors during escaping +--EXTENSIONS-- +pgsql +--SKIPIF-- + +--FILE-- + 'test'])); // table name str escape in php_pgsql_meta_data +var_dump(pg_insert($db, "$invalid.tbl", ['bar' => 'test'])); // schema name str escape in php_pgsql_meta_data +var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', ['bar' => $invalid])); // converted value str escape in php_pgsql_convert +var_dump(pg_insert($db, $invalid, [])); // ident escape in build_tablename +var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', [$invalid => 'foo'], $flags)); // ident escape for field php_pgsql_insert +var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', ['bar' => $invalid], $flags)); // str escape for field value in php_pgsql_insert +var_dump(pg_update($db, 'ghsa_hrmw_9436_5mv3', ['bar' => 'val'], [$invalid => 'test'], $flags)); // ident escape in build_assignment_string +var_dump(pg_update($db, 'ghsa_hrmw_9436_5mv3', ['bar' => 'val'], ['bar' => $invalid], $flags)); // invalid str escape in build_assignment_string +var_dump(pg_escape_literal($db, $invalid)); // pg_escape_literal escape +var_dump(pg_escape_identifier($db, $invalid)); // pg_escape_identifier escape + +?> +--EXPECTF-- + +Warning: pg_insert(): Escaping table name 'ABC%s';' failed in %s on line %d +bool(false) + +Warning: pg_insert(): Escaping table namespace 'ABC%s';.tbl' failed in %s on line %d +bool(false) + +Notice: pg_insert(): String value escaping failed for PostgreSQL 'text' (bar) in %s on line %d +bool(false) + +Notice: pg_insert(): Failed to escape table name 'ABC%s';' in %s on line %d +bool(false) + +Notice: pg_insert(): Failed to escape field 'ABC%s';' in %s on line %d +bool(false) + +Notice: pg_insert(): Failed to escape field 'bar' value in %s on line %d +bool(false) + +Notice: pg_update(): Failed to escape field 'ABC%s';' in %s on line %d +bool(false) + +Notice: pg_update(): Failed to escape field 'bar' value in %s on line %d +bool(false) + +Warning: pg_escape_literal(): Failed to escape in %s on line %d +bool(false) + +Warning: pg_escape_identifier(): Failed to escape in %s on line %d +bool(false) From 0298837252fda06e0f86be3dfe91f166f45e85d4 Mon Sep 17 00:00:00 2001 From: Ahmed Lekssays Date: Tue, 3 Jun 2025 09:00:55 +0000 Subject: [PATCH 42/69] Fix GHSA-453j-q27h-5p8x Libxml versions prior to 2.13 cannot correctly handle a call to xmlNodeSetName() with a name longer than 2G. It will leave the node object in an invalid state with a NULL name. This later causes a NULL pointer dereference when using the name during message serialization. To solve this, implement a workaround that resets the name to the sentinel name if this situation arises. Versions of libxml of 2.13 and higher are not affected. This can be exploited if a SoapVar is created with a fully qualified name that is longer than 2G. This would be possible if some application code uses a namespace prefix from an untrusted source like from a remote SOAP service. Co-authored-by: Niels Dossche <7771979+nielsdos@users.noreply.github.com> --- ext/soap/soap.c | 6 ++-- ext/soap/tests/soap_qname_crash.phpt | 48 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 ext/soap/tests/soap_qname_crash.phpt diff --git a/ext/soap/soap.c b/ext/soap/soap.c index 0996927cee09..1350c0c0a344 100644 --- a/ext/soap/soap.c +++ b/ext/soap/soap.c @@ -3980,8 +3980,10 @@ static xmlNodePtr serialize_zval(zval *val, sdlParamPtr param, char *paramName, } xmlParam = master_to_xml(enc, val, style, parent); zval_ptr_dtor(&defval); - if (!strcmp((char*)xmlParam->name, "BOGUS")) { - xmlNodeSetName(xmlParam, BAD_CAST(paramName)); + if (xmlParam != NULL) { + if (xmlParam->name == NULL || strcmp((char*)xmlParam->name, "BOGUS") == 0) { + xmlNodeSetName(xmlParam, BAD_CAST(paramName)); + } } return xmlParam; } diff --git a/ext/soap/tests/soap_qname_crash.phpt b/ext/soap/tests/soap_qname_crash.phpt new file mode 100644 index 000000000000..bcf01d574fab --- /dev/null +++ b/ext/soap/tests/soap_qname_crash.phpt @@ -0,0 +1,48 @@ +--TEST-- +Test SoapClient with excessively large QName prefix in SoapVar +--EXTENSIONS-- +soap +--SKIPIF-- + +--INI-- +memory_limit=6144M +--FILE-- + 'http://127.0.0.1/', + 'uri' => 'urn:dummy', + 'trace' => 1, + 'exceptions' => true, +]; +$client = new TestSoapClient(null, $options); +$client->__soapCall("DummyFunction", [$var]); +?> +--EXPECT-- +Attempting to create SoapVar with very large QName +Attempting encoding + +value From 165e5169a945a0d7de43835436bce6361a737ffe Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 26 Jun 2025 11:27:48 +0200 Subject: [PATCH 43/69] Update NEWS with entries for security fixes --- NEWS | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 9fdb96e790d3..dbb434af8e93 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,18 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| -?? ??? ????, PHP 8.2.29 +03 Jul 2025, PHP 8.2.29 + +- PGSQL: + . Fixed GHSA-hrwm-9436-5mv3 (pgsql extension does not check for errors during + escaping). (CVE-2025-1735) (Jakub Zelenka) + +- SOAP: + . Fixed GHSA-453j-q27h-5p8x (NULL Pointer Dereference in PHP SOAP Extension + via Large XML Namespace Prefix). (CVE-2025-6491) (Lekssays, nielsdos) + +- Standard: + . Fixed GHSA-3cr5-j632-f35r (Null byte termination in hostnames). + (CVE-2025-1220) (Jakub Zelenka) 13 Mar 2025, PHP 8.2.28 From 3d8cc222d590a989646954e46343ff7228c70ecf Mon Sep 17 00:00:00 2001 From: Sergey Panteleev Date: Tue, 1 Jul 2025 19:49:50 +0300 Subject: [PATCH 44/69] PHP-8.2 is now for PHP 8.2.30-dev --- NEWS | 3 +++ Zend/zend.h | 2 +- configure.ac | 2 +- main/php_version.h | 6 +++--- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/NEWS b/NEWS index dbb434af8e93..ed786b668584 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,8 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| +?? ??? ????, PHP 8.2.30 + + 03 Jul 2025, PHP 8.2.29 - PGSQL: diff --git a/Zend/zend.h b/Zend/zend.h index 71e5908f334f..e3a416096f2a 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -20,7 +20,7 @@ #ifndef ZEND_H #define ZEND_H -#define ZEND_VERSION "4.2.29-dev" +#define ZEND_VERSION "4.2.30-dev" #define ZEND_ENGINE_3 diff --git a/configure.ac b/configure.ac index c70ce436721a..4c43bd7d330f 100644 --- a/configure.ac +++ b/configure.ac @@ -17,7 +17,7 @@ dnl Basic autoconf initialization, generation of config.nice. dnl ---------------------------------------------------------------------------- AC_PREREQ([2.68]) -AC_INIT([PHP],[8.2.29-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) +AC_INIT([PHP],[8.2.30-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) AC_CONFIG_SRCDIR([main/php_version.h]) AC_CONFIG_AUX_DIR([build]) AC_PRESERVE_HELP_ORDER diff --git a/main/php_version.h b/main/php_version.h index cd9671425f86..6251ec54ebe6 100644 --- a/main/php_version.h +++ b/main/php_version.h @@ -2,7 +2,7 @@ /* edit configure.ac to change version number */ #define PHP_MAJOR_VERSION 8 #define PHP_MINOR_VERSION 2 -#define PHP_RELEASE_VERSION 29 +#define PHP_RELEASE_VERSION 30 #define PHP_EXTRA_VERSION "-dev" -#define PHP_VERSION "8.2.29-dev" -#define PHP_VERSION_ID 80229 +#define PHP_VERSION "8.2.30-dev" +#define PHP_VERSION_ID 80230 From 91749844e629b4813d8cc24e4462be678fb03e91 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Thu, 26 Jun 2025 23:30:16 +0200 Subject: [PATCH 45/69] Fix OSS-Fuzz #427814456 The first warning may trigger an error handler, destroying the operand and its string. So we need to protect the string in that case. Care was taken to avoid unnecessary refcounts and to avoid touching the hot code path. Closes GH-18951. --- NEWS | 1 + Zend/tests/numeric_strings/oss_fuzz_427814456.phpt | 11 +++++++++++ Zend/zend_operators.c | 7 ++++++- 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 Zend/tests/numeric_strings/oss_fuzz_427814456.phpt diff --git a/NEWS b/NEWS index 44c964099bc8..259270fdfcf8 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,7 @@ PHP NEWS - Core: . Fixed bug GH-18833 (Use after free with weakmaps dependent on destruction order). (Daniil Gentili) + . Fix OSS-Fuzz #427814456. (nielsdos) - Curl: . Fix memory leaks when returning refcounted value from curl callback. diff --git a/Zend/tests/numeric_strings/oss_fuzz_427814456.phpt b/Zend/tests/numeric_strings/oss_fuzz_427814456.phpt new file mode 100644 index 000000000000..f91563385e9f --- /dev/null +++ b/Zend/tests/numeric_strings/oss_fuzz_427814456.phpt @@ -0,0 +1,11 @@ +--TEST-- +OSS-Fuzz #427814456 +--FILE-- + +--EXPECT-- +Done diff --git a/Zend/zend_operators.c b/Zend/zend_operators.c index 890c19c0ab22..38c87dfe98db 100644 --- a/Zend/zend_operators.c +++ b/Zend/zend_operators.c @@ -401,6 +401,7 @@ static zend_never_inline zend_long ZEND_FASTCALL zendi_try_get_long(const zval * zend_long lval; double dval; bool trailing_data = false; + zend_string *op_str = NULL; /* protect against error handlers */ /* For BC reasons we allow errors so that we can warn on leading numeric string */ type = is_numeric_string_ex(Z_STRVAL_P(op), Z_STRLEN_P(op), &lval, &dval, @@ -410,6 +411,9 @@ static zend_never_inline zend_long ZEND_FASTCALL zendi_try_get_long(const zval * return 0; } if (UNEXPECTED(trailing_data)) { + if (type != IS_LONG) { + op_str = zend_string_copy(Z_STR_P(op)); + } zend_error(E_WARNING, "A non-numeric value encountered"); if (UNEXPECTED(EG(exception))) { *failed = 1; @@ -425,11 +429,12 @@ static zend_never_inline zend_long ZEND_FASTCALL zendi_try_get_long(const zval * */ lval = zend_dval_to_lval_cap(dval); if (!zend_is_long_compatible(dval, lval)) { - zend_incompatible_string_to_long_error(Z_STR_P(op)); + zend_incompatible_string_to_long_error(op_str ? op_str : Z_STR_P(op)); if (UNEXPECTED(EG(exception))) { *failed = 1; } } + zend_tmp_string_release(op_str); return lval; } } From 1d5089e5740b6831485b17bddf482f4d91894305 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:48:27 +0200 Subject: [PATCH 46/69] Fix GH-18979: DOM\XMLDocument::createComment() triggers undefined behavior with null byte Closes GH-18983. --- NEWS | 4 ++++ ext/dom/tests/modern/xml/gh18979.phpt | 13 +++++++++++++ ext/dom/xml_serializer.c | 6 +++++- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 ext/dom/tests/modern/xml/gh18979.phpt diff --git a/NEWS b/NEWS index 1af910562381..6bc566be3904 100644 --- a/NEWS +++ b/NEWS @@ -14,6 +14,10 @@ PHP NEWS . Fix memory leaks when returning refcounted value from curl callback. (nielsdos) +- DOM: + . Fixed bug GH-18979 (Dom\XMLDocument::createComment() triggers undefined + behavior with null byte). (nielsdos) + - LDAP: . Fixed GH-18902 ldap_exop/ldap_exop_sync assert triggered on empty request OID. (David Carlier) diff --git a/ext/dom/tests/modern/xml/gh18979.phpt b/ext/dom/tests/modern/xml/gh18979.phpt new file mode 100644 index 000000000000..3a90bd583773 --- /dev/null +++ b/ext/dom/tests/modern/xml/gh18979.phpt @@ -0,0 +1,13 @@ +--TEST-- +GH-18979 (DOM\XMLDocument::createComment() triggers undefined behavior with null byte) +--EXTENSIONS-- +dom +--FILE-- +createElement("container"); +$container->append($dom->createComment("\0")); +var_dump($container->innerHTML); +?> +--EXPECT-- +string(7) "" diff --git a/ext/dom/xml_serializer.c b/ext/dom/xml_serializer.c index debbb41fdade..a4b46082b0ee 100644 --- a/ext/dom/xml_serializer.c +++ b/ext/dom/xml_serializer.c @@ -640,7 +640,11 @@ static int dom_xml_serialize_comment_node(xmlOutputBufferPtr out, xmlNodePtr com const xmlChar *ptr = comment->content; if (ptr != NULL) { TRY(dom_xml_check_char_production(ptr)); - if (strstr((const char *) ptr, "--") != NULL || ptr[strlen((const char *) ptr) - 1] == '-') { + if (strstr((const char *) ptr, "--") != NULL) { + return -1; + } + size_t len = strlen((const char *) ptr); + if (len > 0 && ptr[len - 1] == '-') { return -1; } } From ca09f4dba463034a1c312426630ed1672e7d90d0 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Tue, 1 Jul 2025 15:17:40 -0500 Subject: [PATCH 47/69] PHP-8.1 is now for PHP 8.1.34-dev --- NEWS | 4 ++++ Zend/zend.h | 2 +- configure.ac | 2 +- main/php_version.h | 6 +++--- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/NEWS b/NEWS index 8c8b28fb9818..d52050e3a8e3 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,9 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| +?? ??? ????, PHP 8.1.34 + + + 03 Jul 2025, PHP 8.1.33 - PGSQL: diff --git a/Zend/zend.h b/Zend/zend.h index 6562119e8363..29bb98c85fca 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -20,7 +20,7 @@ #ifndef ZEND_H #define ZEND_H -#define ZEND_VERSION "4.1.33-dev" +#define ZEND_VERSION "4.1.34-dev" #define ZEND_ENGINE_3 diff --git a/configure.ac b/configure.ac index 0f45db92ba7d..51256f6b7c17 100644 --- a/configure.ac +++ b/configure.ac @@ -17,7 +17,7 @@ dnl Basic autoconf initialization, generation of config.nice. dnl ---------------------------------------------------------------------------- AC_PREREQ([2.68]) -AC_INIT([PHP],[8.1.33-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) +AC_INIT([PHP],[8.1.34-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) AC_CONFIG_SRCDIR([main/php_version.h]) AC_CONFIG_AUX_DIR([build]) AC_PRESERVE_HELP_ORDER diff --git a/main/php_version.h b/main/php_version.h index 2aee6f19d16a..96bd615c0738 100644 --- a/main/php_version.h +++ b/main/php_version.h @@ -2,7 +2,7 @@ /* edit configure.ac to change version number */ #define PHP_MAJOR_VERSION 8 #define PHP_MINOR_VERSION 1 -#define PHP_RELEASE_VERSION 33 +#define PHP_RELEASE_VERSION 34 #define PHP_EXTRA_VERSION "-dev" -#define PHP_VERSION "8.1.33-dev" -#define PHP_VERSION_ID 80133 +#define PHP_VERSION "8.1.34-dev" +#define PHP_VERSION_ID 80134 From d5fe1bce63a2446472a5217bc5f02b49106d26e1 Mon Sep 17 00:00:00 2001 From: Saki Takamachi Date: Wed, 2 Jul 2025 11:39:33 +0900 Subject: [PATCH 48/69] PHP-8.4 is now for PHP 8.4.11-dev --- NEWS | 4 ++-- Zend/zend.h | 2 +- configure.ac | 2 +- main/php_version.h | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/NEWS b/NEWS index 93be6f405c09..9ed0c7259699 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,6 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| -?? ??? ????, PHP 8.4.10 +?? ??? ????, PHP 8.4.11 - Calendar: . Fixed jewishtojd overflow on year argument. (David Carlier) @@ -50,7 +50,7 @@ PHP NEWS . Fixed GH-13264 (fgets() and stream_get_line() do not return false on filter fatal error). (Jakub Zelenka) -03 Jul 2025, PHP 8.4.9 +03 Jul 2025, PHP 8.4.10 - BcMath: . Fixed bug GH-18641 (Accessing a BcMath\Number property by ref crashes). diff --git a/Zend/zend.h b/Zend/zend.h index 15a9b3d8189a..682dc813b5a6 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -20,7 +20,7 @@ #ifndef ZEND_H #define ZEND_H -#define ZEND_VERSION "4.4.10-dev" +#define ZEND_VERSION "4.4.11-dev" #define ZEND_ENGINE_3 diff --git a/configure.ac b/configure.ac index c94038e6ca5d..53481b10c056 100644 --- a/configure.ac +++ b/configure.ac @@ -17,7 +17,7 @@ dnl Basic autoconf initialization, generation of config.nice. dnl ---------------------------------------------------------------------------- AC_PREREQ([2.68]) -AC_INIT([PHP],[8.4.10-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) +AC_INIT([PHP],[8.4.11-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) AC_CONFIG_SRCDIR([main/php_version.h]) AC_CONFIG_AUX_DIR([build]) AC_PRESERVE_HELP_ORDER diff --git a/main/php_version.h b/main/php_version.h index da01e82826df..e518de26b2f0 100644 --- a/main/php_version.h +++ b/main/php_version.h @@ -2,7 +2,7 @@ /* edit configure.ac to change version number */ #define PHP_MAJOR_VERSION 8 #define PHP_MINOR_VERSION 4 -#define PHP_RELEASE_VERSION 10 +#define PHP_RELEASE_VERSION 11 #define PHP_EXTRA_VERSION "-dev" -#define PHP_VERSION "8.4.10-dev" -#define PHP_VERSION_ID 80410 +#define PHP_VERSION "8.4.11-dev" +#define PHP_VERSION_ID 80411 From 11ea995ff3966ae2c5bf2bceb5a6853ca68401d5 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Tue, 1 Jul 2025 00:44:50 +0200 Subject: [PATCH 49/69] curl: Remove incorrect string release on error The string is owned by the caller, and the caller releases it. Closes GH-18989. --- NEWS | 1 + ext/curl/interface.c | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 259270fdfcf8..22f667e57446 100644 --- a/NEWS +++ b/NEWS @@ -13,6 +13,7 @@ PHP NEWS - Curl: . Fix memory leaks when returning refcounted value from curl callback. (nielsdos) + . Remove incorrect string release. (nielsdos) - LDAP: . Fixed GH-18902 ldap_exop/ldap_exop_sync assert triggered on empty diff --git a/ext/curl/interface.c b/ext/curl/interface.c index 6c480907b762..b3139422cffa 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -1369,7 +1369,6 @@ static inline CURLcode add_simple_field(struct HttpPost **first, struct HttpPost part = curl_mime_addpart(mime); if (part == NULL) { zend_tmp_string_release(tmp_postval); - zend_string_release_ex(string_key, 0); return CURLE_OUT_OF_MEMORY; } if ((form_error = curl_mime_name(part, ZSTR_VAL(string_key))) != CURLE_OK From 09c223de00af9b312e49db7bbc915aefaca5dbf8 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Tue, 1 Jul 2025 21:10:33 +0200 Subject: [PATCH 50/69] Fix leak when path is too long in ZipArchive::extractTo() I did not find an easy way to trigger this branch without also triggering some other error conditions earlier. Closes GH-19002. --- NEWS | 3 +++ ext/zip/php_zip.c | 1 + 2 files changed, 4 insertions(+) diff --git a/NEWS b/NEWS index 22f667e57446..5df4c88e972b 100644 --- a/NEWS +++ b/NEWS @@ -42,6 +42,9 @@ PHP NEWS . Fixed GH-13264 (fgets() and stream_get_line() do not return false on filter fatal error). (Jakub Zelenka) +- Zip: + . Fix leak when path is too long in ZipArchive::extractTo(). (nielsdos) + 03 Jul 2025, PHP 8.3.23 - Core: diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c index 62f51ce9f35f..3710b304c351 100644 --- a/ext/zip/php_zip.c +++ b/ext/zip/php_zip.c @@ -218,6 +218,7 @@ static int php_zip_extract_file(struct zip * za, char *dest, const char *file, s return 0; } else if (len > MAXPATHLEN) { php_error_docref(NULL, E_WARNING, "Full extraction path exceed MAXPATHLEN (%i)", MAXPATHLEN); + efree(fullpath); efree(file_dirname_fullpath); zend_string_release_ex(file_basename, 0); CWD_STATE_FREE(new_state.cwd); From 69328ba304f2a47e0e9b3ba872db5681982efb96 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Tue, 1 Jul 2025 19:50:52 +0200 Subject: [PATCH 51/69] Fix GH-18990, bug #81029, bug #47314: SOAP HTTP socket not closing on object destruction Currently the resource is attached to the object and its refcount is increased. This means that the refcount to the resource is 2 instead of 1 as expected. A refcount of 2 is necessary in the current code because of how the error handling works: by using convert_to_null() the resource actually goes to rc_dtor_func(), dropping its refcount to 1. So on error the refcount is correct. To solve the issue, let `stream` conceptually be a borrow of the resource with refcount 1, and just use ZVAL_NULL() to prevent calling rc_dtor_func() on the resource. Closes GH-19001. --- NEWS | 4 +++ ext/soap/php_http.c | 21 +++++------- ext/soap/tests/bugs/gh18990.phpt | 58 ++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 ext/soap/tests/bugs/gh18990.phpt diff --git a/NEWS b/NEWS index 5df4c88e972b..08be236abbdd 100644 --- a/NEWS +++ b/NEWS @@ -32,6 +32,10 @@ PHP NEWS . Fixed bug GH-18958 (Fatal error during shutdown after pcntl_rfork() or pcntl_forkx() with zend-max-execution-timers). (Arnaud) +- SOAP: + . Fixed bug GH-18990, bug #81029, bug #47314 (SOAP HTTP socket not closing + on object destruction). (nielsdos) + - Standard: . Fix misleading errors in printf(). (nielsdos) . Fix RCN violations in array functions. (nielsdos) diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c index c908bb4d8ff1..3dfafda4f957 100644 --- a/ext/soap/php_http.c +++ b/ext/soap/php_http.c @@ -511,9 +511,9 @@ int make_http_soap_request(zval *this_ptr, zend_string_equals(orig->host, phpurl->host) && orig->port == phpurl->port))) { } else { + ZVAL_NULL(Z_CLIENT_HTTPSOCKET_P(this_ptr)); php_stream_close(stream); convert_to_null(Z_CLIENT_HTTPURL_P(this_ptr)); - convert_to_null(Z_CLIENT_HTTPSOCKET_P(this_ptr)); convert_to_null(Z_CLIENT_USE_PROXY_P(this_ptr)); stream = NULL; use_proxy = 0; @@ -522,9 +522,9 @@ int make_http_soap_request(zval *this_ptr, /* Check if keep-alive connection is still opened */ if (stream != NULL && php_stream_eof(stream)) { + ZVAL_NULL(Z_CLIENT_HTTPSOCKET_P(this_ptr)); php_stream_close(stream); convert_to_null(Z_CLIENT_HTTPURL_P(this_ptr)); - convert_to_null(Z_CLIENT_HTTPSOCKET_P(this_ptr)); convert_to_null(Z_CLIENT_USE_PROXY_P(this_ptr)); stream = NULL; use_proxy = 0; @@ -533,9 +533,7 @@ int make_http_soap_request(zval *this_ptr, if (!stream) { stream = http_connect(this_ptr, phpurl, use_ssl, context, &use_proxy); if (stream) { - php_stream_auto_cleanup(stream); - ZVAL_RES(Z_CLIENT_HTTPSOCKET_P(this_ptr), stream->res); - GC_ADDREF(stream->res); + php_stream_to_zval(stream, Z_CLIENT_HTTPSOCKET_P(this_ptr)); ZVAL_LONG(Z_CLIENT_USE_PROXY_P(this_ptr), use_proxy); } else { php_url_free(phpurl); @@ -555,7 +553,6 @@ int make_http_soap_request(zval *this_ptr, zval *cookies, *login, *password; zend_resource *ret = zend_register_resource(phpurl, le_url); ZVAL_RES(Z_CLIENT_HTTPURL_P(this_ptr), ret); - GC_ADDREF(ret); if (context && (tmp = php_stream_context_get_option(context, "http", "protocol_version")) != NULL && @@ -683,9 +680,9 @@ int make_http_soap_request(zval *this_ptr, if (UNEXPECTED(php_random_bytes_throw(&nonce, sizeof(nonce)) != SUCCESS)) { ZEND_ASSERT(EG(exception)); + ZVAL_NULL(Z_CLIENT_HTTPSOCKET_P(this_ptr)); php_stream_close(stream); convert_to_null(Z_CLIENT_HTTPURL_P(this_ptr)); - convert_to_null(Z_CLIENT_HTTPSOCKET_P(this_ptr)); convert_to_null(Z_CLIENT_USE_PROXY_P(this_ptr)); smart_str_free(&soap_headers_z); smart_str_free(&soap_headers); @@ -901,9 +898,9 @@ int make_http_soap_request(zval *this_ptr, if (request != buf) { zend_string_release_ex(request, 0); } + ZVAL_NULL(Z_CLIENT_HTTPSOCKET_P(this_ptr)); php_stream_close(stream); convert_to_null(Z_CLIENT_HTTPURL_P(this_ptr)); - convert_to_null(Z_CLIENT_HTTPSOCKET_P(this_ptr)); convert_to_null(Z_CLIENT_USE_PROXY_P(this_ptr)); add_soap_fault(this_ptr, "HTTP", "Failed Sending HTTP SOAP request", NULL, NULL); smart_str_free(&soap_headers_z); @@ -919,8 +916,8 @@ int make_http_soap_request(zval *this_ptr, } if (!return_value) { + ZVAL_NULL(Z_CLIENT_HTTPSOCKET_P(this_ptr)); php_stream_close(stream); - convert_to_null(Z_CLIENT_HTTPSOCKET_P(this_ptr)); convert_to_null(Z_CLIENT_USE_PROXY_P(this_ptr)); smart_str_free(&soap_headers_z); efree(http_msg); @@ -933,8 +930,8 @@ int make_http_soap_request(zval *this_ptr, if (request != buf) { zend_string_release_ex(request, 0); } + ZVAL_NULL(Z_CLIENT_HTTPSOCKET_P(this_ptr)); php_stream_close(stream); - convert_to_null(Z_CLIENT_HTTPSOCKET_P(this_ptr)); convert_to_null(Z_CLIENT_USE_PROXY_P(this_ptr)); add_soap_fault(this_ptr, "HTTP", "Error Fetching http headers", NULL, NULL); smart_str_free(&soap_headers_z); @@ -1102,9 +1099,9 @@ int make_http_soap_request(zval *this_ptr, if (request != buf) { zend_string_release_ex(request, 0); } + ZVAL_NULL(Z_CLIENT_HTTPSOCKET_P(this_ptr)); php_stream_close(stream); zend_string_release_ex(http_headers, 0); - convert_to_null(Z_CLIENT_HTTPSOCKET_P(this_ptr)); convert_to_null(Z_CLIENT_USE_PROXY_P(this_ptr)); add_soap_fault(this_ptr, "HTTP", "Error Fetching http body, No Content-Length, connection closed or chunked data", NULL, NULL); if (http_msg) { @@ -1119,8 +1116,8 @@ int make_http_soap_request(zval *this_ptr, } if (http_close) { + ZVAL_NULL(Z_CLIENT_HTTPSOCKET_P(this_ptr)); php_stream_close(stream); - convert_to_null(Z_CLIENT_HTTPSOCKET_P(this_ptr)); convert_to_null(Z_CLIENT_USE_PROXY_P(this_ptr)); stream = NULL; } diff --git a/ext/soap/tests/bugs/gh18990.phpt b/ext/soap/tests/bugs/gh18990.phpt new file mode 100644 index 000000000000..30dbc0fe8b75 --- /dev/null +++ b/ext/soap/tests/bugs/gh18990.phpt @@ -0,0 +1,58 @@ +--TEST-- +GH-18990 (SOAP HTTP socket not closing on object destruction) +--INI-- +soap.wsdl_cache_enabled=0 +--EXTENSIONS-- +soap +--SKIPIF-- + +text0text1text2text3text4text5text6text7text8text9 +EOF; + +$responses = [ + "data://text/plain,HTTP/1.1 200 OK\r\n". + "Content-Type: text/xml;charset=utf-8\r\n". + "Connection: Keep-Alive\r\n". + "Content-Length: ".strlen($wsdl)."\r\n". + "\r\n". + $wsdl, + + "data://text/plain,HTTP/1.1 200 OK\r\n". + "Content-Type: text/xml;charset=utf-8\r\n". + "Connection: Keep-Alive\r\n". + "Content-Length: ".strlen($soap)."\r\n". + "\r\n". + $soap, +]; + +['pid' => $pid, 'uri' => $uri] = http_server($responses); + +$options = [ + 'trace' => false, + 'location' => $uri, +]; + +$cnt = count(get_resources()); + +$client = new SoapClient($uri, $options); + +var_dump(count($client->getItems())); + +http_server_kill($pid); + +unset($client); +var_dump(count(get_resources()) - $cnt); +?> +--EXPECT-- +int(10) +int(0) From c161bb0c18f6012795cc7763ac06733b29ae64b0 Mon Sep 17 00:00:00 2001 From: SakiTakamachi Date: Fri, 27 Jun 2025 20:34:09 +0900 Subject: [PATCH 52/69] Fix GH-18873 - Free column->descid appropriately (#18957) fixes #18873 closes #18957 --- NEWS | 4 ++++ ext/oci8/oci8.c | 8 ++------ ext/oci8/tests/gh18873.phpt | 38 +++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 ext/oci8/tests/gh18873.phpt diff --git a/NEWS b/NEWS index 08be236abbdd..814c4692f3d6 100644 --- a/NEWS +++ b/NEWS @@ -22,6 +22,10 @@ PHP NEWS - MbString: . Fixed bug GH-18901 (integer overflow mb_split). (nielsdos) +- OCI8: + . Fixed bug GH-18873 (OCI_RETURN_LOBS flag causes oci8 to leak memory). + (Saki Takamachi) + - Opcache: . Fixed bug GH-18639 (Internal class aliases can break preloading + JIT). (nielsdos) diff --git a/ext/oci8/oci8.c b/ext/oci8/oci8.c index 01cb1c8ad927..b13843e86666 100644 --- a/ext/oci8/oci8.c +++ b/ext/oci8/oci8.c @@ -573,12 +573,8 @@ void php_oci_column_hash_dtor(zval *data) zend_list_close(column->stmtid); } - if (column->descid) { - if (GC_REFCOUNT(column->descid) == 1) - zend_list_close(column->descid); - else { - GC_DELREF(column->descid); - } + if (column->descid && !GC_DELREF(column->descid)) { + zend_list_free(column->descid); } if (column->data) { diff --git a/ext/oci8/tests/gh18873.phpt b/ext/oci8/tests/gh18873.phpt new file mode 100644 index 000000000000..acd88facb57c --- /dev/null +++ b/ext/oci8/tests/gh18873.phpt @@ -0,0 +1,38 @@ +--TEST-- +GH-18873 (OCI_RETURN_LOBS flag causes oci8 to leak memory) +--EXTENSIONS-- +oci8 +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done! From b6660634b4ff951d1ca98d02381853645faa57af Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 15 Feb 2024 10:24:41 +0100 Subject: [PATCH 53/69] Disable JIT on Apple Silicon + ZTS Apple Silicon has stricter rules about rwx mmap regions. They need to be created using the MAP_JIT flag. However, the MAP_JIT seems to be incompatible with MAP_SHARED. ZTS requires MAP_SHARED so that some threads may execute code from a page while another writes/appends to it. We did not find another solution, other than completely disabling JIT for Apple Silicon + ZTS. See discussion in https://github.com/php/php-src/pull/13351. Co-authored-by: Peter Kokot Fixes GH-13400 Closes GH-13396 --- ext/opcache/config.m4 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ext/opcache/config.m4 b/ext/opcache/config.m4 index 0b923206282c..d35efbc689ed 100644 --- a/ext/opcache/config.m4 +++ b/ext/opcache/config.m4 @@ -36,6 +36,10 @@ if test "$PHP_OPCACHE" != "no"; then PHP_OPCACHE_JIT=no ;; esac + if test "$host_vendor" = "apple" && test "$host_cpu" = "aarch64" && test "$PHP_THREAD_SAFETY" = "yes"; then + AC_MSG_WARN([JIT not supported on Apple Silicon with ZTS]) + PHP_OPCACHE_JIT=no + fi fi if test "$PHP_OPCACHE_JIT" = "yes"; then From 4aac98f1456069b69ffd701dadb31be014a8e90c Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Wed, 2 Jul 2025 20:29:35 +0200 Subject: [PATCH 54/69] Fix OSS-Fuzz #428983568 and #428760800 Both these issues have the same root cause, their reproducer is extremely similar so I don't duplicate the test. If the parser invokes the lexer, and the lexer fails, it could've allocated a string which must be freed when the parser backs up. The `%destructor` list is responsible for this but did not have an entry for `fallback` yet. Solve the issue by adding such an entry. Closes GH-19012. --- NEWS | 1 + Zend/tests/zend_ini/oss_fuzz_428983568.phpt | 14 ++++++++++++++ Zend/zend_ini_parser.y | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 Zend/tests/zend_ini/oss_fuzz_428983568.phpt diff --git a/NEWS b/NEWS index 814c4692f3d6..11b5146bc33f 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,7 @@ PHP NEWS . Fixed bug GH-18833 (Use after free with weakmaps dependent on destruction order). (Daniil Gentili) . Fix OSS-Fuzz #427814456. (nielsdos) + . Fix OSS-Fuzz #428983568 and #428760800. (nielsdos) - Curl: . Fix memory leaks when returning refcounted value from curl callback. diff --git a/Zend/tests/zend_ini/oss_fuzz_428983568.phpt b/Zend/tests/zend_ini/oss_fuzz_428983568.phpt new file mode 100644 index 000000000000..80310fbd9287 --- /dev/null +++ b/Zend/tests/zend_ini/oss_fuzz_428983568.phpt @@ -0,0 +1,14 @@ +--TEST-- +OSS-Fuzz #428983568 +--FILE-- + +--EXPECTF-- +Warning: syntax error, unexpected end of file, expecting '}' in Unknown on line 1 + in %s on line %d +bool(false) diff --git a/Zend/zend_ini_parser.y b/Zend/zend_ini_parser.y index 352f2eb3eec2..370493d54e1b 100644 --- a/Zend/zend_ini_parser.y +++ b/Zend/zend_ini_parser.y @@ -353,7 +353,7 @@ static void normalize_value(zval *zv) %left '|' '&' '^' %precedence '~' '!' -%destructor { zval_ini_dtor(&$$); } TC_RAW TC_CONSTANT TC_NUMBER TC_STRING TC_WHITESPACE TC_LABEL TC_OFFSET TC_VARNAME BOOL_TRUE BOOL_FALSE NULL_NULL cfg_var_ref constant_literal constant_string encapsed_list expr option_offset section_string_or_value string_or_value var_string_list var_string_list_section +%destructor { zval_ini_dtor(&$$); } TC_RAW TC_CONSTANT TC_NUMBER TC_STRING TC_WHITESPACE TC_LABEL TC_OFFSET TC_VARNAME BOOL_TRUE BOOL_FALSE NULL_NULL cfg_var_ref constant_literal constant_string encapsed_list expr fallback option_offset section_string_or_value string_or_value var_string_list var_string_list_section %% From 32344c4dc463affe588496976eadf55ba6658178 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Fri, 27 Jun 2025 11:05:33 +0800 Subject: [PATCH 55/69] Fix stream double free in phar The copy function does two things wrong: - The error recovery logic is a hack that temporarily moves the fp pointer to cfp, even though it's not compressed. The respective error recovery it talks about is not present in the code, nor is it necessary. This is the direct cause of the double free in the original reproducer. Fixing this makes it crash in another location though. - The link following logic is inconsistent and illogical. It cannot be a link at this point. The root cause, after fixing the above issues, is that the file pointers are not reset properly for the copy. The file pointer need to be the original ones to perform the copy from the right source, but after that they need to be set properly to NULL (because fp_type == PHAR_FP). Closes GH-19035. Co-authored-by: Yun Dou --- NEWS | 3 +++ ext/phar/phar_object.c | 23 ++++++++------------- ext/phar/tests/gh18953.phpt | 40 +++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 15 deletions(-) create mode 100644 ext/phar/tests/gh18953.phpt diff --git a/NEWS b/NEWS index 11b5146bc33f..df26a2d9823d 100644 --- a/NEWS +++ b/NEWS @@ -37,6 +37,9 @@ PHP NEWS . Fixed bug GH-18958 (Fatal error during shutdown after pcntl_rfork() or pcntl_forkx() with zend-max-execution-timers). (Arnaud) +- Phar: + . Fix stream double free in phar. (nielsdos, dixyes) + - SOAP: . Fixed bug GH-18990, bug #81029, bug #47314 (SOAP HTTP socket not closing on object destruction). (nielsdos) diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index bd724b103694..cb032095c392 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -1926,7 +1926,8 @@ static int phar_copy_file_contents(phar_entry_info *entry, php_stream *fp) /* {{ { char *error; zend_off_t offset; - phar_entry_info *link; + + ZEND_ASSERT(!entry->link); if (FAILURE == phar_open_entry_fp(entry, &error, 1)) { if (error) { @@ -1941,26 +1942,14 @@ static int phar_copy_file_contents(phar_entry_info *entry, php_stream *fp) /* {{ } /* copy old contents in entirety */ - phar_seek_efp(entry, 0, SEEK_SET, 0, 1); + phar_seek_efp(entry, 0, SEEK_SET, 0, 0); offset = php_stream_tell(fp); - link = phar_get_link_source(entry); - - if (!link) { - link = entry; - } - - if (SUCCESS != php_stream_copy_to_stream_ex(phar_get_efp(link, 0), fp, link->uncompressed_filesize, NULL)) { + if (SUCCESS != php_stream_copy_to_stream_ex(phar_get_efp(entry, 0), fp, entry->uncompressed_filesize, NULL)) { zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Cannot convert phar archive \"%s\", unable to copy entry \"%s\" contents", entry->phar->fname, entry->filename); return FAILURE; } - if (entry->fp_type == PHAR_MOD) { - /* save for potential restore on error */ - entry->cfp = entry->fp; - entry->fp = NULL; - } - /* set new location of file contents */ entry->fp_type = PHAR_FP; entry->offset = offset; @@ -2299,6 +2288,10 @@ static zend_object *phar_convert_to_other(phar_archive_data *source, int convert return NULL; } no_copy: + /* Reset file pointers, they have to be reset here such that if a copy happens the original + * source fp can be accessed. */ + newentry.fp = NULL; + newentry.cfp = NULL; newentry.filename = estrndup(newentry.filename, newentry.filename_len); phar_metadata_tracker_clone(&newentry.metadata_tracker); diff --git a/ext/phar/tests/gh18953.phpt b/ext/phar/tests/gh18953.phpt new file mode 100644 index 000000000000..52bbace2406d --- /dev/null +++ b/ext/phar/tests/gh18953.phpt @@ -0,0 +1,40 @@ +--TEST-- +GH-18953 (Phar: Stream double free) +--EXTENSIONS-- +phar +--INI-- +phar.readonly=0 +--FILE-- +addFromString("file", str_repeat("123", random_int(1, 1))); +$phar->addEmptyDir("dir"); +$phar2 = $phar->compress(Phar::GZ); + +var_dump($phar["dir"]); +var_dump($phar2["dir"]); +var_dump($phar["file"]->openFile()->fread(100)); +var_dump($phar2["file"]->openFile()->fread(100)); + +?> +--CLEAN-- + +--EXPECTF-- +object(PharFileInfo)#%d (2) { + ["pathName":"SplFileInfo":private]=> + string(%d) "%sphar%sdir" + ["fileName":"SplFileInfo":private]=> + string(3) "dir" +} +object(PharFileInfo)#%d (2) { + ["pathName":"SplFileInfo":private]=> + string(%d) "%sphar.gz%sdir" + ["fileName":"SplFileInfo":private]=> + string(3) "dir" +} +string(3) "123" +string(3) "123" From 405be1c940af576e18633add4774cfc5f4326657 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sat, 5 Jul 2025 12:07:12 +0200 Subject: [PATCH 56/69] Fix phar crash and file corruption with SplFileObject There are two bugfixes here. The first was a crash that I discovered while working on GH-19035. The check for when a file pointer was still occupied was wrong, leading to a UAF. Strangely, zip got this right. The second issue was that even after fixing the first one, the file contents were garbage. This is because the file write offset for the phar stream was wrong. Closes GH-19038. --- NEWS | 1 + ext/phar/phar.c | 2 +- ext/phar/stream.c | 4 ++-- ext/phar/tar.c | 2 +- ext/phar/tests/gh19038.phpt | 25 +++++++++++++++++++++++++ 5 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 ext/phar/tests/gh19038.phpt diff --git a/NEWS b/NEWS index df26a2d9823d..84add23a52e0 100644 --- a/NEWS +++ b/NEWS @@ -39,6 +39,7 @@ PHP NEWS - Phar: . Fix stream double free in phar. (nielsdos, dixyes) + . Fix phar crash and file corruption with SplFileObject. (nielsdos) - SOAP: . Fixed bug GH-18990, bug #81029, bug #47314 (SOAP HTTP socket not closing diff --git a/ext/phar/phar.c b/ext/phar/phar.c index 125fc8470361..ab5460887c3f 100644 --- a/ext/phar/phar.c +++ b/ext/phar/phar.c @@ -2737,7 +2737,7 @@ int phar_flush(phar_archive_data *phar, char *user_stub, zend_long len, int conv /* remove this from the new phar */ continue; } - if (!entry->is_modified && entry->fp_refcount) { + if (entry->fp_refcount) { /* open file pointers refer to this fp, do not free the stream */ switch (entry->fp_type) { case PHAR_FP: diff --git a/ext/phar/stream.c b/ext/phar/stream.c index fee100cc31a1..e68f07bddd0c 100644 --- a/ext/phar/stream.c +++ b/ext/phar/stream.c @@ -450,12 +450,12 @@ static ssize_t phar_stream_write(php_stream *stream, const char *buf, size_t cou { phar_entry_data *data = (phar_entry_data *) stream->abstract; - php_stream_seek(data->fp, data->position, SEEK_SET); + php_stream_seek(data->fp, data->position + data->zero, SEEK_SET); if (count != php_stream_write(data->fp, buf, count)) { php_stream_wrapper_log_error(stream->wrapper, stream->flags, "phar error: Could not write %d characters to \"%s\" in phar \"%s\"", (int) count, data->internal_file->filename, data->phar->fname); return -1; } - data->position = php_stream_tell(data->fp); + data->position = php_stream_tell(data->fp) - data->zero; if (data->position > (zend_off_t)data->internal_file->uncompressed_filesize) { data->internal_file->uncompressed_filesize = data->position; } diff --git a/ext/phar/tar.c b/ext/phar/tar.c index e259fb6f3dec..38bd52c86093 100644 --- a/ext/phar/tar.c +++ b/ext/phar/tar.c @@ -832,7 +832,7 @@ static int phar_tar_writeheaders_int(phar_entry_info *entry, void *argument) /* php_stream_write(fp->new, padding, ((entry->uncompressed_filesize +511)&~511) - entry->uncompressed_filesize); } - if (!entry->is_modified && entry->fp_refcount) { + if (entry->fp_refcount) { /* open file pointers refer to this fp, do not free the stream */ switch (entry->fp_type) { case PHAR_FP: diff --git a/ext/phar/tests/gh19038.phpt b/ext/phar/tests/gh19038.phpt new file mode 100644 index 000000000000..34eba44c1dcd --- /dev/null +++ b/ext/phar/tests/gh19038.phpt @@ -0,0 +1,25 @@ +--TEST-- +GH-19038 (Phar crash and data corruption with SplFileObject) +--EXTENSIONS-- +phar +--INI-- +phar.readonly=0 +--FILE-- +addFromString("file", "123"); +$file = $phar["file"]->openFile(); +$file->fseek(3); +var_dump($file->fwrite("456", 3)); +$file->fseek(0); +echo $file->fread(100); + +?> +--CLEAN-- + +--EXPECT-- +int(3) +123456 From 258fbd6bf958a7743c479152fcc4806379d0a6d1 Mon Sep 17 00:00:00 2001 From: Peter Kokot Date: Mon, 7 Jul 2025 09:51:25 +0200 Subject: [PATCH 57/69] Fix -Wuseless-escape warnings emitted by re2c (#19050) re2c version 4 enabled some warnings by default. This fixes re2c code for the `-Wuseless-escape` warnings. There are two same issues reported. Issue: GH-17523 Closes: GH-17204 --- NEWS | 1 + Zend/zend_ini_scanner.l | 6 +++--- Zend/zend_language_scanner.l | 4 ++-- sapi/phpdbg/phpdbg_lexer.l | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/NEWS b/NEWS index 84add23a52e0..d0d8c25df532 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,7 @@ PHP NEWS order). (Daniil Gentili) . Fix OSS-Fuzz #427814456. (nielsdos) . Fix OSS-Fuzz #428983568 and #428760800. (nielsdos) + . Fixed bug GH-17204 -Wuseless-escape warnings emitted by re2c. (Peter Kokot) - Curl: . Fix memory leaks when returning refcounted value from curl callback. diff --git a/Zend/zend_ini_scanner.l b/Zend/zend_ini_scanner.l index 44159297a04e..b87f4e33cc8f 100644 --- a/Zend/zend_ini_scanner.l +++ b/Zend/zend_ini_scanner.l @@ -352,16 +352,16 @@ restart: /*!re2c re2c:yyfill:check = 0; LNUM [0-9]+ -DNUM ([0-9]*[\.][0-9]+)|([0-9]+[\.][0-9]*) +DNUM ([0-9]*[.][0-9]+)|([0-9]+[.][0-9]*) NUMBER [-]?{LNUM}|{DNUM} ANY_CHAR (.|[\n\t]) NEWLINE ("\r"|"\n"|"\r\n") TABS_AND_SPACES [ \t] WHITESPACE [ \t]+ CONSTANT [a-zA-Z_][a-zA-Z0-9_]* -LABEL_CHAR [^=\n\r\t;&|^$~(){}!"\[\]\x00] +LABEL_CHAR [^=\n\r\t;&|^$~(){}!"[\]\x00] LABEL ({LABEL_CHAR}+) -TOKENS [:,.\[\]"'()&|^+-/*=%$!~<>?@{}] +TOKENS [:,.[\]"'()&|^+-/*=%$!~<>?@{}] OPERATORS [&|^~()!] DOLLAR_CURLY "${" diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l index 25e1ecbca6f5..3ea51fe7c9d0 100644 --- a/Zend/zend_language_scanner.l +++ b/Zend/zend_language_scanner.l @@ -1801,11 +1801,11 @@ OPTIONAL_WHITESPACE_OR_COMMENTS ({WHITESPACE}|{MULTI_LINE_COMMENT}|{SINGLE_LINE_ RETURN_TOKEN(T_MUL_EQUAL); } -"*\*" { +"**" { RETURN_TOKEN(T_POW); } -"*\*=" { +"**=" { RETURN_TOKEN(T_POW_EQUAL); } diff --git a/sapi/phpdbg/phpdbg_lexer.l b/sapi/phpdbg/phpdbg_lexer.l index 60d995526ea2..ba1423c5a4ec 100644 --- a/sapi/phpdbg/phpdbg_lexer.l +++ b/sapi/phpdbg/phpdbg_lexer.l @@ -77,7 +77,7 @@ T_IF 'if' T_RUN 'run' T_RUN_SHORT "r" WS [ \r\t]+ -DIGITS [-]?[0-9\.]+ +DIGITS [-]?[0-9.]+ ID [^ \r\n\t:#\000]+ GENERIC_ID ([^ \r\n\t:#\000"']|":\\")+|["]([^\n\000"\\]|"\\\\"|"\\"["])+["]|[']([^\n\000'\\]|"\\\\"|"\\"['])+['] ADDR [0][x][a-fA-F0-9]+ From e8ae27bf8ae9e4b2a4fbb338c9e436e581370010 Mon Sep 17 00:00:00 2001 From: Dmitry Stogov Date: Mon, 7 Jul 2025 14:03:11 +0300 Subject: [PATCH 58/69] Update IR IR commit: af6dc83bcd91c3123f40efcdcbeba8794b9b2abf --- ext/opcache/jit/ir/ir.c | 4 +- ext/opcache/jit/ir/ir.h | 5 +- ext/opcache/jit/ir/ir_aarch64.dasc | 41 ++++- ext/opcache/jit/ir/ir_cfg.c | 232 ++++++++++++++++++++--------- ext/opcache/jit/ir/ir_check.c | 9 +- ext/opcache/jit/ir/ir_emit.c | 4 +- ext/opcache/jit/ir/ir_fold.h | 4 +- ext/opcache/jit/ir/ir_gcm.c | 181 ++++++++++++++++++++-- ext/opcache/jit/ir/ir_private.h | 2 +- ext/opcache/jit/ir/ir_save.c | 3 + ext/opcache/jit/ir/ir_sccp.c | 15 +- ext/opcache/jit/ir/ir_x86.dasc | 97 ++++++++++-- 12 files changed, 483 insertions(+), 114 deletions(-) diff --git a/ext/opcache/jit/ir/ir.c b/ext/opcache/jit/ir/ir.c index a9f55cc0e466..e90a5e80bf04 100644 --- a/ext/opcache/jit/ir/ir.c +++ b/ext/opcache/jit/ir/ir.c @@ -803,7 +803,9 @@ ir_ref ir_proto(ir_ctx *ctx, uint8_t flags, ir_type ret_type, uint32_t params_co proto->flags = flags; proto->ret_type = ret_type; proto->params_count = params_count; - memcpy(proto->param_types, param_types, params_count); + if (params_count) { + memcpy(proto->param_types, param_types, params_count); + } return ir_strl(ctx, (const char *)proto, offsetof(ir_proto_t, param_types) + params_count); } diff --git a/ext/opcache/jit/ir/ir.h b/ext/opcache/jit/ir/ir.h index ec5e57129c9a..be8779e01941 100644 --- a/ext/opcache/jit/ir/ir.h +++ b/ext/opcache/jit/ir/ir.h @@ -854,6 +854,9 @@ void ir_gdb_unregister_all(void); bool ir_gdb_present(void); /* IR load API (implementation in ir_load.c) */ +#define IR_RESOLVE_SYM_ADD_THUNK (1<<0) +#define IR_RESOLVE_SYM_SILENT (1<<1) + struct _ir_loader { uint32_t default_func_flags; bool (*init_module) (ir_loader *loader, const char *name, const char *filename, const char *target); @@ -870,7 +873,7 @@ struct _ir_loader { bool (*sym_data_end) (ir_loader *loader, uint32_t flags); bool (*func_init) (ir_loader *loader, ir_ctx *ctx, const char *name); bool (*func_process) (ir_loader *loader, ir_ctx *ctx, const char *name); - void*(*resolve_sym_name) (ir_loader *loader, const char *name, bool add_thunk); + void*(*resolve_sym_name) (ir_loader *loader, const char *name, uint32_t flags); bool (*has_sym) (ir_loader *loader, const char *name); bool (*add_sym) (ir_loader *loader, const char *name, void *addr); }; diff --git a/ext/opcache/jit/ir/ir_aarch64.dasc b/ext/opcache/jit/ir/ir_aarch64.dasc index 772eea7a5d78..1d927cc8c726 100644 --- a/ext/opcache/jit/ir/ir_aarch64.dasc +++ b/ext/opcache/jit/ir/ir_aarch64.dasc @@ -4366,11 +4366,15 @@ static void ir_emit_va_arg(ir_ctx *ctx, ir_ref def, ir_insn *insn) ir_backend_data *data = ctx->data; dasm_State **Dst = &data->dasm_state; ir_type type = insn->type; - ir_reg def_reg = ctx->regs[def][0]; + ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]); ir_reg op2_reg = ctx->regs[def][2]; ir_reg tmp_reg = ctx->regs[def][3]; int32_t offset; + if (ctx->use_lists[def].count == 1) { + /* dead load */ + return; + } IR_ASSERT(def_reg != IR_REG_NONE && tmp_reg != IR_REG_NONE); if (op2_reg != IR_REG_NONE) { if (IR_REG_SPILLED(op2_reg)) { @@ -4394,11 +4398,15 @@ static void ir_emit_va_arg(ir_ctx *ctx, ir_ref def, ir_insn *insn) ir_backend_data *data = ctx->data; dasm_State **Dst = &data->dasm_state; ir_type type = insn->type; - ir_reg def_reg = ctx->regs[def][0]; + ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]); ir_reg op2_reg = ctx->regs[def][2]; ir_reg tmp_reg = ctx->regs[def][3]; int32_t offset; + if (ctx->use_lists[def].count == 1) { + /* dead load */ + return; + } IR_ASSERT(def_reg != IR_REG_NONE && tmp_reg != IR_REG_NONE); if (op2_reg != IR_REG_NONE) { if (IR_REG_SPILLED(op2_reg)) { @@ -4935,6 +4943,28 @@ static void ir_emit_tailcall(ir_ctx *ctx, ir_ref def, ir_insn *insn) return; } + /* Move op2 to a tmp register before epilogue if it's in + * used_preserved_regs, because it will be overridden. */ + + ir_reg op2_reg = IR_REG_NONE; + if (!IR_IS_CONST_REF(insn->op2)) { + op2_reg = ctx->regs[def][2]; + IR_ASSERT(op2_reg != IR_REG_NONE); + + if (IR_REG_SPILLED(op2_reg)) { + op2_reg = IR_REG_INT_TMP; + ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2); + } else if (IR_REGSET_IN((ir_regset)ctx->used_preserved_regs, IR_REG_NUM(op2_reg))) { + ir_reg orig_op2_reg = op2_reg; + op2_reg = IR_REG_INT_TMP; + + ir_type type = ctx->ir_base[insn->op2].type; + | ASM_REG_REG_OP mov, type, op2_reg, IR_REG_NUM(orig_op2_reg) + } else { + op2_reg = IR_REG_NUM(op2_reg); + } + } + ir_emit_epilogue(ctx); if (IR_IS_CONST_REF(insn->op2)) { @@ -4947,13 +4977,8 @@ static void ir_emit_tailcall(ir_ctx *ctx, ir_ref def, ir_insn *insn) | br Rx(IR_REG_INT_TMP) } } else { - ir_reg op2_reg = ctx->regs[def][2]; - IR_ASSERT(op2_reg != IR_REG_NONE); - if (IR_REG_SPILLED(op2_reg)) { - op2_reg = IR_REG_NUM(op2_reg); - ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2); - } + IR_ASSERT(!IR_REGSET_IN((ir_regset)ctx->used_preserved_regs, op2_reg)); | br Rx(op2_reg) } } diff --git a/ext/opcache/jit/ir/ir_cfg.c b/ext/opcache/jit/ir/ir_cfg.c index 01532c8ea3e3..34375b0a3b55 100644 --- a/ext/opcache/jit/ir/ir_cfg.c +++ b/ext/opcache/jit/ir/ir_cfg.c @@ -244,7 +244,6 @@ int ir_build_cfg(ir_ctx *ctx) _blocks[start] = b; _blocks[end] = b; IR_ASSERT(IR_IS_BB_START(insn->op)); - IR_ASSERT(end > start); bb->start = start; bb->end = end; bb->successors = count; @@ -583,7 +582,6 @@ static int ir_remove_unreachable_blocks(ir_ctx *ctx) return 1; } -#if 0 static void compute_postnum(const ir_ctx *ctx, uint32_t *cur, uint32_t b) { uint32_t i, *p; @@ -607,34 +605,42 @@ static void compute_postnum(const ir_ctx *ctx, uint32_t *cur, uint32_t b) /* Computes dominator tree using algorithm from "A Simple, Fast Dominance Algorithm" by * Cooper, Harvey and Kennedy. */ -int ir_build_dominators_tree(ir_ctx *ctx) +static int ir_build_dominators_tree_slow(ir_ctx *ctx) { uint32_t blocks_count, b, postnum; ir_block *blocks, *bb; uint32_t *edges; bool changed; + blocks = ctx->cfg_blocks; + edges = ctx->cfg_edges; + blocks_count = ctx->cfg_blocks_count; + + /* Clear the dominators tree */ + for (b = 0, bb = &blocks[0]; b <= blocks_count; b++, bb++) { + bb->idom = 0; + bb->dom_depth = 0; + bb->dom_child = 0; + bb->dom_next_child = 0; + } + ctx->flags2 &= ~IR_NO_LOOPS; postnum = 1; compute_postnum(ctx, &postnum, 1); - /* Find immediate dominators */ - blocks = ctx->cfg_blocks; - edges = ctx->cfg_edges; - blocks_count = ctx->cfg_blocks_count; + /* Find immediate dominators by iterative fixed-point algorithm */ blocks[1].idom = 1; do { changed = 0; /* Iterating in Reverse Post Order */ for (b = 2, bb = &blocks[2]; b <= blocks_count; b++, bb++) { IR_ASSERT(!(bb->flags & IR_BB_UNREACHABLE)); + IR_ASSERT(bb->predecessors_count > 0); if (bb->predecessors_count == 1) { uint32_t pred_b = edges[bb->predecessors]; - if (blocks[pred_b].idom <= 0) { - //IR_ASSERT("Wrong blocks order: BB is before its single predecessor"); - } else if (bb->idom != pred_b) { + if (blocks[pred_b].idom > 0 && bb->idom != pred_b) { bb->idom = pred_b; changed = 1; } @@ -680,39 +686,37 @@ int ir_build_dominators_tree(ir_ctx *ctx) } } } while (changed); + + /* Build dominators tree */ blocks[1].idom = 0; blocks[1].dom_depth = 0; - - /* Construct dominators tree */ for (b = 2, bb = &blocks[2]; b <= blocks_count; b++, bb++) { - IR_ASSERT(!(bb->flags & IR_BB_UNREACHABLE)); - if (bb->idom > 0) { - ir_block *idom_bb = &blocks[bb->idom]; - - bb->dom_depth = idom_bb->dom_depth + 1; - /* Sort by block number to traverse children in pre-order */ - if (idom_bb->dom_child == 0) { - idom_bb->dom_child = b; - } else if (b < idom_bb->dom_child) { - bb->dom_next_child = idom_bb->dom_child; - idom_bb->dom_child = b; - } else { - int child = idom_bb->dom_child; - ir_block *child_bb = &blocks[child]; + uint32_t idom = bb->idom; + ir_block *idom_bb = &blocks[idom]; - while (child_bb->dom_next_child > 0 && b > child_bb->dom_next_child) { - child = child_bb->dom_next_child; - child_bb = &blocks[child]; - } - bb->dom_next_child = child_bb->dom_next_child; - child_bb->dom_next_child = b; + bb->dom_depth = idom_bb->dom_depth + 1; + /* Sort by block number to traverse children in pre-order */ + if (idom_bb->dom_child == 0) { + idom_bb->dom_child = b; + } else if (b < idom_bb->dom_child) { + bb->dom_next_child = idom_bb->dom_child; + idom_bb->dom_child = b; + } else { + int child = idom_bb->dom_child; + ir_block *child_bb = &blocks[child]; + + while (child_bb->dom_next_child > 0 && b > child_bb->dom_next_child) { + child = child_bb->dom_next_child; + child_bb = &blocks[child]; } + bb->dom_next_child = child_bb->dom_next_child; + child_bb->dom_next_child = b; } } return 1; } -#else + /* A single pass modification of "A Simple, Fast Dominance Algorithm" by * Cooper, Harvey and Kennedy, that relays on IR block ordering. * It may fallback to the general slow fixed-point algorithm. */ @@ -747,7 +751,11 @@ int ir_build_dominators_tree(ir_ctx *ctx) if (UNEXPECTED(idom >= b)) { /* In rare cases, LOOP_BEGIN.op1 may be a back-edge. Skip back-edges. */ ctx->flags2 &= ~IR_NO_LOOPS; - IR_ASSERT(k > 1 && "Wrong blocks order: BB is before its single predecessor"); +// IR_ASSERT(k > 1 && "Wrong blocks order: BB is before its single predecessor"); + if (UNEXPECTED(k <= 1)) { + ir_list_free(&worklist); + return ir_build_dominators_tree_slow(ctx); + } ir_list_push(&worklist, idom); while (1) { k--; @@ -942,7 +950,6 @@ static int ir_build_dominators_tree_iterative(ir_ctx *ctx) return 1; } -#endif static bool ir_dominates(const ir_block *blocks, uint32_t b1, uint32_t b2) { @@ -958,7 +965,7 @@ static bool ir_dominates(const ir_block *blocks, uint32_t b1, uint32_t b2) int ir_find_loops(ir_ctx *ctx) { - uint32_t i, j, n, count; + uint32_t b, j, n, count; uint32_t *entry_times, *exit_times, *sorted_blocks, time = 1; ir_block *blocks = ctx->cfg_blocks; uint32_t *edges = ctx->cfg_edges; @@ -983,13 +990,13 @@ int ir_find_loops(ir_ctx *ctx) int child; next: - i = ir_worklist_peek(&work); - if (!entry_times[i]) { - entry_times[i] = time++; + b = ir_worklist_peek(&work); + if (!entry_times[b]) { + entry_times[b] = time++; } - /* Visit blocks immediately dominated by i. */ - bb = &blocks[i]; + /* Visit blocks immediately dominated by "b". */ + bb = &blocks[b]; for (child = bb->dom_child; child > 0; child = blocks[child].dom_next_child) { if (ir_worklist_push(&work, child)) { goto next; @@ -999,17 +1006,17 @@ int ir_find_loops(ir_ctx *ctx) /* Visit join edges. */ if (bb->successors_count) { uint32_t *p = edges + bb->successors; - for (j = 0; j < bb->successors_count; j++,p++) { + for (j = 0; j < bb->successors_count; j++, p++) { uint32_t succ = *p; - if (blocks[succ].idom == i) { + if (blocks[succ].idom == b) { continue; } else if (ir_worklist_push(&work, succ)) { goto next; } } } - exit_times[i] = time++; + exit_times[b] = time++; ir_worklist_pop(&work); } @@ -1018,7 +1025,7 @@ int ir_find_loops(ir_ctx *ctx) j = 1; n = 2; while (j != n) { - i = j; + uint32_t i = j; j = n; for (; i < j; i++) { int child; @@ -1030,9 +1037,82 @@ int ir_find_loops(ir_ctx *ctx) count = n; /* Identify loops. See Sreedhar et al, "Identifying Loops Using DJ Graphs". */ + uint32_t prev_dom_depth = blocks[sorted_blocks[n - 1]].dom_depth; + uint32_t prev_irreducible = 0; while (n > 1) { - i = sorted_blocks[--n]; - ir_block *bb = &blocks[i]; + b = sorted_blocks[--n]; + ir_block *bb = &blocks[b]; + + IR_ASSERT(bb->dom_depth <= prev_dom_depth); + if (UNEXPECTED(prev_irreducible) && bb->dom_depth != prev_dom_depth) { + /* process delyed irreducible loops */ + do { + b = sorted_blocks[prev_irreducible]; + bb = &blocks[b]; + if ((bb->flags & IR_BB_IRREDUCIBLE_LOOP) && !bb->loop_depth) { + /* process irreducible loop */ + uint32_t hdr = b; + + bb->loop_depth = 1; + if (ctx->ir_base[bb->start].op == IR_MERGE) { + ctx->ir_base[bb->start].op = IR_LOOP_BEGIN; + } + + /* find the closing edge(s) of the irreucible loop */ + IR_ASSERT(bb->predecessors_count > 1); + uint32_t *p = &edges[bb->predecessors]; + j = bb->predecessors_count; + do { + uint32_t pred = *p; + + if (entry_times[pred] > entry_times[b] && exit_times[pred] < exit_times[b]) { + if (!ir_worklist_len(&work)) { + ir_bitset_clear(work.visited, ir_bitset_len(ir_worklist_capasity(&work))); + } + blocks[pred].loop_header = 0; /* support for merged loops */ + ir_worklist_push(&work, pred); + } + p++; + } while (--j); + IR_ASSERT(ir_worklist_len(&work) != 0); + + /* collect members of the irreducible loop */ + while (ir_worklist_len(&work)) { + b = ir_worklist_pop(&work); + if (b != hdr) { + ir_block *bb = &blocks[b]; + bb->loop_header = hdr; + if (bb->predecessors_count) { + uint32_t *p = &edges[bb->predecessors]; + uint32_t n = bb->predecessors_count; + do { + uint32_t pred = *p; + while (blocks[pred].loop_header > 0) { + pred = blocks[pred].loop_header; + } + if (pred != hdr) { + if (entry_times[pred] > entry_times[hdr] && exit_times[pred] < exit_times[hdr]) { + /* "pred" is a descendant of "hdr" */ + ir_worklist_push(&work, pred); + } else { + /* another entry to the irreducible loop */ + bb->flags |= IR_BB_IRREDUCIBLE_LOOP; + if (ctx->ir_base[bb->start].op == IR_MERGE) { + ctx->ir_base[bb->start].op = IR_LOOP_BEGIN; + } + } + } + p++; + } while (--n); + } + } + } + } + } while (--prev_irreducible != n); + prev_irreducible = 0; + b = sorted_blocks[n]; + bb = &blocks[b]; + } if (bb->predecessors_count > 1) { bool irreducible = 0; @@ -1047,7 +1127,7 @@ int ir_find_loops(ir_ctx *ctx) if (bb->idom != pred) { /* In a loop back-edge (back-join edge), the successor dominates the predecessor. */ - if (ir_dominates(blocks, i, pred)) { + if (ir_dominates(blocks, b, pred)) { if (!ir_worklist_len(&work)) { ir_bitset_clear(work.visited, ir_bitset_len(ir_worklist_capasity(&work))); } @@ -1056,8 +1136,9 @@ int ir_find_loops(ir_ctx *ctx) } else { /* Otherwise it's a cross-join edge. See if it's a branch to an ancestor on the DJ spanning tree. */ - if (entry_times[pred] > entry_times[i] && exit_times[pred] < exit_times[i]) { + if (entry_times[pred] > entry_times[b] && exit_times[pred] < exit_times[b]) { irreducible = 1; + break; } } } @@ -1065,46 +1146,55 @@ int ir_find_loops(ir_ctx *ctx) } while (--j); if (UNEXPECTED(irreducible)) { - // TODO: Support for irreducible loops ??? - bb->flags |= IR_BB_IRREDUCIBLE_LOOP; - ctx->flags2 |= IR_IRREDUCIBLE_CFG; - while (ir_worklist_len(&work)) { - ir_worklist_pop(&work); + bb->flags |= IR_BB_LOOP_HEADER | IR_BB_IRREDUCIBLE_LOOP; + ctx->flags2 |= IR_CFG_HAS_LOOPS | IR_IRREDUCIBLE_CFG; + /* Remember the position of the first irreducible loop to process all the irreducible loops + * after the reducible loops with the same dominator tree depth + */ + if (!prev_irreducible) { + prev_irreducible = n; } + ir_list_clear(&work.l); } else if (ir_worklist_len(&work)) { + /* collect members of the reducible loop */ + uint32_t hdr = b; + bb->flags |= IR_BB_LOOP_HEADER; ctx->flags2 |= IR_CFG_HAS_LOOPS; bb->loop_depth = 1; + if (ctx->ir_base[bb->start].op == IR_MERGE) { + ctx->ir_base[bb->start].op = IR_LOOP_BEGIN; + } while (ir_worklist_len(&work)) { - j = ir_worklist_pop(&work); - while (blocks[j].loop_header > 0) { - j = blocks[j].loop_header; - } - if (j != i) { - ir_block *bb = &blocks[j]; - if (bb->idom == 0 && j != 1) { - /* Ignore blocks that are unreachable or only abnormally reachable. */ - continue; - } - bb->loop_header = i; + b = ir_worklist_pop(&work); + if (b != hdr) { + ir_block *bb = &blocks[b]; + bb->loop_header = hdr; if (bb->predecessors_count) { uint32_t *p = &edges[bb->predecessors]; - j = bb->predecessors_count; + uint32_t n = bb->predecessors_count; do { - ir_worklist_push(&work, *p); + uint32_t pred = *p; + while (blocks[pred].loop_header > 0) { + pred = blocks[pred].loop_header; + } + if (pred != hdr) { + ir_worklist_push(&work, pred); + } p++; - } while (--j); + } while (--n); } } } } } } + IR_ASSERT(!prev_irreducible); if (ctx->flags2 & IR_CFG_HAS_LOOPS) { for (n = 1; n < count; n++) { - i = sorted_blocks[n]; - ir_block *bb = &blocks[i]; + b = sorted_blocks[n]; + ir_block *bb = &blocks[b]; if (bb->loop_header > 0) { ir_block *loop = &blocks[bb->loop_header]; uint32_t loop_depth = loop->loop_depth; @@ -1389,7 +1479,7 @@ static int ir_schedule_blocks_bottom_up(ir_ctx *ctx) goto restart; } } else if (b != predecessor && ctx->cfg_blocks[predecessor].loop_header != b) { - ir_dump_cfg(ctx, stderr); + /* not a loop back-edge */ IR_ASSERT(b == predecessor || ctx->cfg_blocks[predecessor].loop_header == b); } } diff --git a/ext/opcache/jit/ir/ir_check.c b/ext/opcache/jit/ir/ir_check.c index f12b4776fa1e..a791baef5db9 100644 --- a/ext/opcache/jit/ir/ir_check.c +++ b/ext/opcache/jit/ir/ir_check.c @@ -213,13 +213,18 @@ bool ir_check(const ir_ctx *ctx) ok = 0; } } - break; - case IR_OPND_CONTROL_DEP: if ((ctx->flags2 & IR_LINEAR) && use >= i && !(insn->op == IR_LOOP_BEGIN)) { fprintf(stderr, "ir_base[%d].ops[%d] invalid forward reference (%d)\n", i, j, use); ok = 0; + } + break; + case IR_OPND_CONTROL_DEP: + if ((ctx->flags2 & IR_LINEAR) + && use >= i) { + fprintf(stderr, "ir_base[%d].ops[%d] invalid forward reference (%d)\n", i, j, use); + ok = 0; } else if (insn->op == IR_PHI) { ir_insn *merge_insn = &ctx->ir_base[insn->op1]; if (merge_insn->op != IR_MERGE && merge_insn->op != IR_LOOP_BEGIN) { diff --git a/ext/opcache/jit/ir/ir_emit.c b/ext/opcache/jit/ir/ir_emit.c index c82655daf48d..fab9f56228d8 100644 --- a/ext/opcache/jit/ir/ir_emit.c +++ b/ext/opcache/jit/ir/ir_emit.c @@ -309,7 +309,7 @@ static void* ir_sym_addr(ir_ctx *ctx, const ir_insn *addr_insn) { const char *name = ir_get_str(ctx, addr_insn->val.name); void *addr = (ctx->loader && ctx->loader->resolve_sym_name) ? - ctx->loader->resolve_sym_name(ctx->loader, name, 0) : + ctx->loader->resolve_sym_name(ctx->loader, name, IR_RESOLVE_SYM_SILENT) : ir_resolve_sym_name(name); return addr; @@ -320,7 +320,7 @@ static void* ir_sym_val(ir_ctx *ctx, const ir_insn *addr_insn) { const char *name = ir_get_str(ctx, addr_insn->val.name); void *addr = (ctx->loader && ctx->loader->resolve_sym_name) ? - ctx->loader->resolve_sym_name(ctx->loader, name, addr_insn->op == IR_FUNC) : + ctx->loader->resolve_sym_name(ctx->loader, name, addr_insn->op == IR_FUNC ? IR_RESOLVE_SYM_ADD_THUNK : 0) : ir_resolve_sym_name(name); IR_ASSERT(addr); diff --git a/ext/opcache/jit/ir/ir_fold.h b/ext/opcache/jit/ir/ir_fold.h index 88539e52ab08..90112214d0c8 100644 --- a/ext/opcache/jit/ir/ir_fold.h +++ b/ext/opcache/jit/ir/ir_fold.h @@ -1909,7 +1909,9 @@ IR_FOLD(SUB(_, SUB)) IR_FOLD(SUB(ADD, ADD)) { if (IR_IS_TYPE_INT(IR_OPT_TYPE(opt))) { - if (op1_insn->op1 == op2_insn->op1) { + if (op1 == op2) { + IR_FOLD_CONST_U(0); + } else if (op1_insn->op1 == op2_insn->op1) { /* (a + b) - (a + c) => b - c */ op1 = op1_insn->op2; op2 = op2_insn->op2; diff --git a/ext/opcache/jit/ir/ir_gcm.c b/ext/opcache/jit/ir/ir_gcm.c index 8bd6be5d10aa..0d8a6c2d760b 100644 --- a/ext/opcache/jit/ir/ir_gcm.c +++ b/ext/opcache/jit/ir/ir_gcm.c @@ -785,6 +785,139 @@ IR_ALWAYS_INLINE ir_ref ir_count_constant(ir_ref *_xlat, ir_ref ref) return 0; } +IR_ALWAYS_INLINE bool ir_is_good_bb_order(ir_ctx *ctx, uint32_t b, ir_block *bb, ir_ref start) +{ + ir_insn *insn = &ctx->ir_base[start]; + uint32_t n = insn->inputs_count; + ir_ref *p = insn->ops + 1; + + if (n == 1) { + return *p < start; + } else { + IR_ASSERT(n > 1); + for (; n > 0; p++, n--) { + ir_ref input = *p; + if (input < start) { + /* ordered */ + } else if ((bb->flags & IR_BB_LOOP_HEADER) + && (ctx->cfg_map[input] == b || ctx->cfg_blocks[ctx->cfg_map[input]].loop_header == b)) { + /* back-edge of reducible loop */ + } else if ((bb->flags & IR_BB_IRREDUCIBLE_LOOP) + && (ctx->cfg_blocks[ctx->cfg_map[input]].loop_header == ctx->cfg_blocks[b].loop_header)) { + /* closing edge of irreducible loop */ + } else { + return 0; + } + } + return 1; + } +} + +static IR_NEVER_INLINE void ir_fix_bb_order(ir_ctx *ctx, ir_ref *_prev, ir_ref *_next) +{ + uint32_t b, succ, count, *q, *xlat; + ir_block *bb; + ir_ref ref, n, prev; + ir_worklist worklist; + ir_block *new_blocks; + +#if 0 + for (b = 1, bb = ctx->cfg_blocks + 1; b <= ctx->cfg_blocks_count; b++, bb++) { + if (!ir_is_good_bb_order(ctx, b, bb, bb->start)) { + goto fix; + } + } + return; + +fix: +#endif + count = ctx->cfg_blocks_count + 1; + new_blocks = ir_mem_malloc(count * sizeof(ir_block)); + xlat = ir_mem_malloc(count * sizeof(uint32_t)); + ir_worklist_init(&worklist, count); + ir_worklist_push(&worklist, 1); + while (ir_worklist_len(&worklist) != 0) { +next: + b = ir_worklist_peek(&worklist); + bb = &ctx->cfg_blocks[b]; + n = bb->successors_count; + if (n == 1) { + succ = ctx->cfg_edges[bb->successors]; + if (ir_worklist_push(&worklist, succ)) { + goto next; + } + } else if (n > 1) { + uint32_t best = 0; + uint32_t best_loop_depth = 0; + + q = ctx->cfg_edges + bb->successors + n; + do { + q--; + succ = *q; + if (ir_bitset_in(worklist.visited, succ)) { + /* already processed */ + } else if ((ctx->cfg_blocks[succ].flags & IR_BB_LOOP_HEADER) + && (succ == b || ctx->cfg_blocks[b].loop_header == succ)) { + /* back-edge of reducible loop */ + } else if ((ctx->cfg_blocks[succ].flags & IR_BB_IRREDUCIBLE_LOOP) + && (ctx->cfg_blocks[succ].loop_header == ctx->cfg_blocks[b].loop_header)) { + /* closing edge of irreducible loop */ + } else if (!best) { + best = succ; + best_loop_depth = ctx->cfg_blocks[best].loop_depth; + } else if (ctx->cfg_blocks[succ].loop_depth < best_loop_depth) { + /* prefer deeper loop */ + best = succ; + best_loop_depth = ctx->cfg_blocks[best].loop_depth; + } + n--; + } while (n > 0); + if (best) { + ir_worklist_push(&worklist, best); + goto next; + } + } + ir_worklist_pop(&worklist); + count--; + new_blocks[count] = *bb; + xlat[b] = count; + } + IR_ASSERT(count == 1); + xlat[0] = 0; + ir_worklist_free(&worklist); + + prev = 0; + for (b = 1, bb = new_blocks + 1; b <= ctx->cfg_blocks_count; b++, bb++) { + bb->idom = xlat[bb->idom]; + bb->loop_header = xlat[bb->loop_header]; + n = bb->successors_count; + if (n > 0) { + for (q = ctx->cfg_edges + bb->successors; n > 0; q++, n--) { + *q = xlat[*q]; + } + } + n = bb->predecessors_count; + if (n > 0) { + for (q = ctx->cfg_edges + bb->predecessors; n > 0; q++, n--) { + *q = xlat[*q]; + } + } + _next[prev] = bb->start; + _prev[bb->start] = prev; + prev = bb->end; + } + _next[0] = 0; + _next[prev] = 0; + + for (ref = 2; ref < ctx->insns_count; ref++) { + ctx->cfg_map[ref] = xlat[ctx->cfg_map[ref]]; + } + ir_mem_free(xlat); + + ir_mem_free(ctx->cfg_blocks); + ctx->cfg_blocks = new_blocks; +} + int ir_schedule(ir_ctx *ctx) { ir_ctx new_ctx; @@ -800,6 +933,7 @@ int ir_schedule(ir_ctx *ctx) ir_block *bb; ir_insn *insn, *new_insn; ir_use_list *lists, *use_list, *new_list; + bool bad_bb_order = 0; /* Create a double-linked list of nodes ordered by BB, respecting BB->start and BB->end */ IR_ASSERT(_blocks[1] == 1); @@ -818,27 +952,50 @@ int ir_schedule(ir_ctx *ctx) } else if (b > prev_b) { bb = &ctx->cfg_blocks[b]; if (i == bb->start) { - IR_ASSERT(bb->end > bb->start); - prev_b = b; - prev_b_end = bb->end; - _prev[bb->end] = 0; + if (bb->end > bb->start) { + prev_b = b; + prev_b_end = bb->end; + /* add to the end of the list */ + _next[j] = i; + _prev[i] = j; + j = i; + } else { + prev_b = 0; + prev_b_end = 0; + k = bb->end; + while (_blocks[_prev[k]] == b) { + k = _prev[k]; + } + /* insert before "k" */ + _prev[i] = _prev[k]; + _next[i] = k; + _next[_prev[k]] = i; + _prev[k] = i; + } + if (!ir_is_good_bb_order(ctx, b, bb, i)) { + bad_bb_order = 1; + } + } else if (i != bb->end) { + /* move down late (see the following loop) */ + _next[i] = _move_down; + _move_down = i; + } else { + IR_ASSERT(bb->start > bb->end); + prev_b = 0; + prev_b_end = 0; /* add to the end of the list */ _next[j] = i; _prev[i] = j; j = i; - } else { - IR_ASSERT(i != bb->end); - /* move down late (see the following loop) */ - _next[i] = _move_down; - _move_down = i; } } else if (b) { bb = &ctx->cfg_blocks[b]; IR_ASSERT(i != bb->start); - if (_prev[bb->end]) { + if (i > bb->end) { /* move up, insert before the end of the already scheduled BB */ k = bb->end; } else { + IR_ASSERT(i > bb->start); /* move up, insert at the end of the block */ k = ctx->cfg_blocks[b + 1].start; } @@ -883,6 +1040,10 @@ int ir_schedule(ir_ctx *ctx) } #endif + if (bad_bb_order) { + ir_fix_bb_order(ctx, _prev, _next); + } + _xlat = ir_mem_calloc((ctx->consts_count + ctx->insns_count), sizeof(ir_ref)); _xlat += ctx->consts_count; _xlat[IR_TRUE] = IR_TRUE; diff --git a/ext/opcache/jit/ir/ir_private.h b/ext/opcache/jit/ir/ir_private.h index 69a0101d24ee..ac952e402f52 100644 --- a/ext/opcache/jit/ir/ir_private.h +++ b/ext/opcache/jit/ir/ir_private.h @@ -62,7 +62,7 @@ #define IR_MAX(a, b) (((a) > (b)) ? (a) : (b)) #define IR_MIN(a, b) (((a) < (b)) ? (a) : (b)) -#define IR_IS_POWER_OF_TWO(x) (!((x) & ((x) - 1))) +#define IR_IS_POWER_OF_TWO(x) ((x) && (!((x) & ((x) - 1)))) #define IR_LOG2(x) ir_ntzl(x) diff --git a/ext/opcache/jit/ir/ir_save.c b/ext/opcache/jit/ir/ir_save.c index b12cc267af60..595f2d9d6a21 100644 --- a/ext/opcache/jit/ir/ir_save.c +++ b/ext/opcache/jit/ir/ir_save.c @@ -140,6 +140,9 @@ void ir_save(const ir_ctx *ctx, uint32_t save_flags, FILE *f) fprintf(f, ", loop=BB%d(%d)", bb->loop_header, bb->loop_depth); } } + if (bb->flags & IR_BB_IRREDUCIBLE_LOOP) { + fprintf(f, ", IRREDUCIBLE"); + } if (bb->predecessors_count) { uint32_t i; diff --git a/ext/opcache/jit/ir/ir_sccp.c b/ext/opcache/jit/ir/ir_sccp.c index 2e006516df81..221a86a5ad8a 100644 --- a/ext/opcache/jit/ir/ir_sccp.c +++ b/ext/opcache/jit/ir/ir_sccp.c @@ -1732,7 +1732,20 @@ static ir_ref ir_promote_i2i(ir_ctx *ctx, ir_type type, ir_ref ref, ir_ref use, ir_ref *p, n, input; if (IR_IS_CONST_REF(ref)) { - return ir_const(ctx, insn->val, type); + ir_val val; + + switch (type) { + case IR_I8: val.i64 = insn->val.i8; break; + case IR_U8: val.u64 = insn->val.u8; break; + case IR_I16: val.i64 = insn->val.i16; break; + case IR_U16: val.u64 = insn->val.u16; break; + case IR_I32: val.i64 = insn->val.i32; break; + case IR_U32: val.u64 = insn->val.u32; break; + case IR_CHAR:val.i64 = insn->val.i8; break; + case IR_BOOL:val.u64 = insn->val.u8 != 0; break; + default: IR_ASSERT(0); val.u64 = 0; + } + return ir_const(ctx, val, type); } else { ir_bitqueue_add(worklist, ref); switch (insn->op) { diff --git a/ext/opcache/jit/ir/ir_x86.dasc b/ext/opcache/jit/ir/ir_x86.dasc index 76602c2b4bcf..d56cb8645e18 100644 --- a/ext/opcache/jit/ir/ir_x86.dasc +++ b/ext/opcache/jit/ir/ir_x86.dasc @@ -6868,7 +6868,24 @@ static void ir_emit_return_fp(ir_ctx *ctx, ir_ref ref, ir_insn *insn) ir_backend_data *data = ctx->data; dasm_State **Dst = &data->dasm_state; - if (op2_reg == IR_REG_NONE || IR_REG_SPILLED(op2_reg)) { + if (IR_IS_CONST_REF(insn->op2)) { + ir_insn *value = &ctx->ir_base[insn->op2]; + + if ((type == IR_FLOAT && value->val.f == 0.0) || (type == IR_DOUBLE && value->val.d == 0.0)) { + | fldz + } else if ((type == IR_FLOAT && value->val.f == 1.0) || (type == IR_DOUBLE && value->val.d == 1.0)) { + | fld1 + } else { + int label = ir_const_label(ctx, insn->op2); + + if (type == IR_DOUBLE) { + | fld qword [=>label] + } else { + IR_ASSERT(type == IR_FLOAT); + | fld dword [=>label] + } + } + } else if (op2_reg == IR_REG_NONE || IR_REG_SPILLED(op2_reg)) { ir_reg fp; int32_t offset = ir_ref_spill_slot_offset(ctx, insn->op2, &fp); @@ -8442,11 +8459,15 @@ static void ir_emit_va_arg(ir_ctx *ctx, ir_ref def, ir_insn *insn) ir_backend_data *data = ctx->data; dasm_State **Dst = &data->dasm_state; ir_type type = insn->type; - ir_reg def_reg = ctx->regs[def][0]; + ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]); ir_reg op2_reg = ctx->regs[def][2]; ir_reg tmp_reg = ctx->regs[def][3]; int32_t offset; + if (ctx->use_lists[def].count == 1) { + /* dead load */ + return; + } IR_ASSERT(def_reg != IR_REG_NONE && tmp_reg != IR_REG_NONE); if (op2_reg != IR_REG_NONE) { if (IR_REG_SPILLED(op2_reg)) { @@ -8471,11 +8492,15 @@ static void ir_emit_va_arg(ir_ctx *ctx, ir_ref def, ir_insn *insn) ir_backend_data *data = ctx->data; dasm_State **Dst = &data->dasm_state; ir_type type = insn->type; - ir_reg def_reg = ctx->regs[def][0]; + ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]); ir_reg op2_reg = ctx->regs[def][2]; ir_reg tmp_reg = ctx->regs[def][3]; int32_t offset; + if (ctx->use_lists[def].count == 1) { + /* dead load */ + return; + } IR_ASSERT(def_reg != IR_REG_NONE&& tmp_reg != IR_REG_NONE); if (op2_reg != IR_REG_NONE) { if (IR_REG_SPILLED(op2_reg)) { @@ -9221,6 +9246,58 @@ static void ir_emit_tailcall(ir_ctx *ctx, ir_ref def, ir_insn *insn) return; } + /* Move op2 to a tmp register before epilogue if it's in + * used_preserved_regs, because it will be overridden. */ + + ir_reg op2_reg = IR_REG_NONE; + ir_mem mem = IR_MEM_B(IR_REG_NONE); + if (!IR_IS_CONST_REF(insn->op2)) { + op2_reg = ctx->regs[def][2]; + + ir_regset preserved_regs = (ir_regset)ctx->used_preserved_regs | IR_REGSET(IR_REG_STACK_POINTER); + if (ctx->flags & IR_USE_FRAME_POINTER) { + preserved_regs |= IR_REGSET(IR_REG_FRAME_POINTER); + } + + bool is_spill_slot = op2_reg != IR_REG_NONE + && IR_REG_SPILLED(op2_reg) + && ctx->vregs[insn->op2]; + + if (op2_reg != IR_REG_NONE && !is_spill_slot) { + if (IR_REGSET_IN(preserved_regs, IR_REG_NUM(op2_reg))) { + ir_ref orig_op2_reg = op2_reg; + op2_reg = IR_REG_RAX; + + if (IR_REG_SPILLED(orig_op2_reg)) { + ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2); + } else { + ir_type type = ctx->ir_base[insn->op2].type; + | ASM_REG_REG_OP mov, type, op2_reg, IR_REG_NUM(orig_op2_reg) + } + } else { + op2_reg = IR_REG_NUM(op2_reg); + } + } else { + if (ir_rule(ctx, insn->op2) & IR_FUSED) { + IR_ASSERT(op2_reg == IR_REG_NONE); + mem = ir_fuse_load(ctx, def, insn->op2); + } else { + mem = ir_ref_spill_slot(ctx, insn->op2); + } + ir_reg base = IR_MEM_BASE(mem); + ir_reg index = IR_MEM_INDEX(mem); + if ((base != IR_REG_NONE && IR_REGSET_IN(preserved_regs, base)) || + (index != IR_REG_NONE && IR_REGSET_IN(preserved_regs, index))) { + op2_reg = IR_REG_RAX; + + ir_type type = ctx->ir_base[insn->op2].type; + ir_emit_load_mem_int(ctx, type, op2_reg, mem); + } else { + op2_reg = IR_REG_NONE; + } + } + } + ir_emit_epilogue(ctx); if (IR_IS_CONST_REF(insn->op2)) { @@ -9246,22 +9323,10 @@ static void ir_emit_tailcall(ir_ctx *ctx, ir_ref def, ir_insn *insn) |.endif } } else { - ir_reg op2_reg = ctx->regs[def][2]; - if (op2_reg != IR_REG_NONE) { - if (IR_REG_SPILLED(op2_reg)) { - op2_reg = IR_REG_NUM(op2_reg); - ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2); - } + IR_ASSERT(!IR_REGSET_IN((ir_regset)ctx->used_preserved_regs, op2_reg)); | jmp Ra(op2_reg) } else { - ir_mem mem; - - if (ir_rule(ctx, insn->op2) & IR_FUSED) { - mem = ir_fuse_load(ctx, def, insn->op2); - } else { - mem = ir_ref_spill_slot(ctx, insn->op2); - } | ASM_TMEM_OP jmp, aword, mem } } From faa78313d9cc7b89c1df610e4bfabfd38c171245 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Mon, 7 Jul 2025 16:28:18 +0200 Subject: [PATCH 59/69] [skip ci] Add missing zlib dep to phar compression test --- ext/phar/tests/gh18953.phpt | 1 + 1 file changed, 1 insertion(+) diff --git a/ext/phar/tests/gh18953.phpt b/ext/phar/tests/gh18953.phpt index 52bbace2406d..ff48e457cc9f 100644 --- a/ext/phar/tests/gh18953.phpt +++ b/ext/phar/tests/gh18953.phpt @@ -2,6 +2,7 @@ GH-18953 (Phar: Stream double free) --EXTENSIONS-- phar +zlib --INI-- phar.readonly=0 --FILE-- From 2be3aa86f04be760cfc826f11c4d11dedd060ff9 Mon Sep 17 00:00:00 2001 From: Demon Date: Tue, 8 Jul 2025 13:51:03 +0800 Subject: [PATCH 60/69] Zend: fix undefined symbol 'execute_ex' on Windows ARM64 #19064; ext/gd: fix emmintrin.h not found on Windows ARM64 --- Zend/zend_vm_execute.h | 2 +- Zend/zend_vm_execute.skl | 2 +- ext/gd/libgd/gd_interpolation.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 25e537dc1c07..96bdc01746ae 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -55088,7 +55088,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_NULL_HANDLER(ZEND_OPCODE_HANDL # pragma GCC optimize("no-gcse") # pragma GCC optimize("no-ivopts") #endif -#ifdef _WIN64 +#if defined(_WIN64) && defined(_M_X64) /* See save_xmm_x86_64_ms_masm.asm */ void execute_ex_real(zend_execute_data *ex) #else diff --git a/Zend/zend_vm_execute.skl b/Zend/zend_vm_execute.skl index 5b4799cd67c2..b1e26d002604 100644 --- a/Zend/zend_vm_execute.skl +++ b/Zend/zend_vm_execute.skl @@ -5,7 +5,7 @@ # pragma GCC optimize("no-gcse") # pragma GCC optimize("no-ivopts") #endif -#ifdef _WIN64 +#if defined(_WIN64) && defined(_M_X64) /* See save_xmm_x86_64_ms_masm.asm */ void {%EXECUTOR_NAME%}_ex_real(zend_execute_data *ex) #else diff --git a/ext/gd/libgd/gd_interpolation.c b/ext/gd/libgd/gd_interpolation.c index 5481fa2a16b0..4aa2b79b7a7c 100644 --- a/ext/gd/libgd/gd_interpolation.c +++ b/ext/gd/libgd/gd_interpolation.c @@ -62,7 +62,7 @@ #include "gdhelpers.h" #include "gd_intern.h" -#ifdef _MSC_VER +#if defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64)) # pragma optimize("t", on) # include #endif From 12fa8c637f9801f7b9574f152f31845403345d9a Mon Sep 17 00:00:00 2001 From: Demon Date: Thu, 10 Jul 2025 09:52:59 +0800 Subject: [PATCH 61/69] ext/gd: Drop useless and doubtful MSVC specific code (libgd/libgd@f1480ab) --- ext/gd/libgd/gd_interpolation.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ext/gd/libgd/gd_interpolation.c b/ext/gd/libgd/gd_interpolation.c index 4aa2b79b7a7c..b3f391e6d20e 100644 --- a/ext/gd/libgd/gd_interpolation.c +++ b/ext/gd/libgd/gd_interpolation.c @@ -62,11 +62,6 @@ #include "gdhelpers.h" #include "gd_intern.h" -#if defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64)) -# pragma optimize("t", on) -# include -#endif - #ifndef MIN #define MIN(a,b) ((a)<(b)?(a):(b)) #endif From 2fa0e554500d6568f18eef08b34b6d5fd88fb6e9 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Thu, 10 Jul 2025 22:14:38 +0200 Subject: [PATCH 62/69] Update NEWS for GH-19068 --- NEWS | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index d4354c12b08d..2061e6d53382 100644 --- a/NEWS +++ b/NEWS @@ -11,7 +11,9 @@ PHP NEWS . Fixed bug GH-18907 (Leak when creating cycle in hook). (ilutov) . Fix OSS-Fuzz #427814456. (nielsdos) . Fix OSS-Fuzz #428983568 and #428760800. (nielsdos) - . Fixed bug GH-17204 -Wuseless-escape warnings emitted by re2c. (Peter Kokot) + . Fixed bug GH-17204 (-Wuseless-escape warnings emitted by re2c). (Peter Kokot) + . Fixed bug GH-19064 (Undefined symbol 'execute_ex' on Windows ARM64). + (Demon) - Curl: . Fix memory leaks when returning refcounted value from curl callback. From 974526b244e59eb38a66bd6b44030a7c6bd5e792 Mon Sep 17 00:00:00 2001 From: Demon Date: Thu, 10 Jul 2025 09:52:59 +0800 Subject: [PATCH 63/69] ext/gd: Drop useless and doubtful MSVC specific code (libgd/libgd@f1480ab) --- ext/gd/libgd/gd_interpolation.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ext/gd/libgd/gd_interpolation.c b/ext/gd/libgd/gd_interpolation.c index bc1b3d344e69..5d9e991127c4 100644 --- a/ext/gd/libgd/gd_interpolation.c +++ b/ext/gd/libgd/gd_interpolation.c @@ -62,11 +62,6 @@ #include "gdhelpers.h" #include "gd_intern.h" -#ifdef _MSC_VER -# pragma optimize("t", on) -# include -#endif - #ifndef HAVE_FLOORF # define HAVE_FLOORF 0 #endif From 85a49d4198e55274ed222335a0e95d67d58ccf70 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Mon, 7 Jul 2025 14:58:47 +0100 Subject: [PATCH 64/69] ext/soap/php_http.c: Fix memory leak of header value --- NEWS | 1 + ext/soap/php_http.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index d0d8c25df532..c653d946b90a 100644 --- a/NEWS +++ b/NEWS @@ -45,6 +45,7 @@ PHP NEWS - SOAP: . Fixed bug GH-18990, bug #81029, bug #47314 (SOAP HTTP socket not closing on object destruction). (nielsdos) + . Fix memory leak when URL parsing fails in redirect. (Girgias) - Standard: . Fix misleading errors in printf(). (nielsdos) diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c index 3dfafda4f957..65ce38db3872 100644 --- a/ext/soap/php_http.c +++ b/ext/soap/php_http.c @@ -1128,11 +1128,11 @@ int make_http_soap_request(zval *this_ptr, if ((loc = get_http_header_value(ZSTR_VAL(http_headers), "Location: ")) != NULL) { php_url *new_url = php_url_parse(loc); + efree(loc); if (new_url != NULL) { zend_string_release_ex(http_headers, 0); zend_string_release_ex(http_body, 0); - efree(loc); if (new_url->scheme == NULL && new_url->path != NULL) { new_url->scheme = phpurl->scheme ? zend_string_copy(phpurl->scheme) : NULL; new_url->host = phpurl->host ? zend_string_copy(phpurl->host) : NULL; From 71472268c08a6e5c2e4eeb0def56b7ef9732bfaf Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:02:43 +0200 Subject: [PATCH 65/69] Fix GH-19094: Attaching class with no Iterator implementation to MultipleIterator causes crash Closes GH-19097. --- NEWS | 4 +++ ext/spl/spl_observer.c | 21 +++++++++++++- ext/spl/tests/gh19094.phpt | 56 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 ext/spl/tests/gh19094.phpt diff --git a/NEWS b/NEWS index c653d946b90a..3e45adc3d9f4 100644 --- a/NEWS +++ b/NEWS @@ -47,6 +47,10 @@ PHP NEWS on object destruction). (nielsdos) . Fix memory leak when URL parsing fails in redirect. (Girgias) +- SPL: + . Fixed bug GH-19094 (Attaching class with no Iterator implementation to + MultipleIterator causes crash). (nielsdos) + - Standard: . Fix misleading errors in printf(). (nielsdos) . Fix RCN violations in array functions. (nielsdos) diff --git a/ext/spl/spl_observer.c b/ext/spl/spl_observer.c index 83889c430b7a..1f9c17c129a2 100644 --- a/ext/spl/spl_observer.c +++ b/ext/spl/spl_observer.c @@ -43,6 +43,7 @@ PHPAPI zend_class_entry *spl_ce_SplObjectStorage; PHPAPI zend_class_entry *spl_ce_MultipleIterator; PHPAPI zend_object_handlers spl_handler_SplObjectStorage; +static zend_object_handlers spl_handler_MultipleIterator; /* TODO: make public ? */ /* Bit flags for marking internal functionality overridden by SplObjectStorage subclasses. */ #define SOS_OVERRIDDEN_READ_DIMENSION 1 @@ -493,6 +494,20 @@ static void spl_object_storage_write_dimension(zend_object *object, zval *offset spl_object_storage_attach_handle(intern, Z_OBJ_P(offset), inf); } +static void spl_multiple_iterator_write_dimension(zend_object *object, zval *offset, zval *inf) +{ + spl_SplObjectStorage *intern = spl_object_storage_from_obj(object); + if (UNEXPECTED(offset == NULL || Z_TYPE_P(offset) != IS_OBJECT || (intern->flags & SOS_OVERRIDDEN_WRITE_DIMENSION))) { + zend_std_write_dimension(object, offset, inf); + return; + } + if (UNEXPECTED(!Z_OBJCE_P(offset)->iterator_funcs_ptr || !Z_OBJCE_P(offset)->iterator_funcs_ptr->zf_valid)) { + zend_type_error("Can only attach objects that implement the Iterator interface"); + return; + } + spl_object_storage_attach_handle(intern, Z_OBJ_P(offset), inf); +} + static void spl_object_storage_unset_dimension(zend_object *object, zval *offset) { spl_SplObjectStorage *intern = spl_object_storage_from_obj(object); @@ -1352,9 +1367,13 @@ PHP_MINIT_FUNCTION(spl_observer) spl_handler_SplObjectStorage.has_dimension = spl_object_storage_has_dimension; spl_handler_SplObjectStorage.unset_dimension = spl_object_storage_unset_dimension; + memcpy(&spl_handler_MultipleIterator, &spl_handler_SplObjectStorage, sizeof(zend_object_handlers)); + + spl_handler_MultipleIterator.write_dimension = spl_multiple_iterator_write_dimension; + spl_ce_MultipleIterator = register_class_MultipleIterator(zend_ce_iterator); spl_ce_MultipleIterator->create_object = spl_SplObjectStorage_new; - spl_ce_MultipleIterator->default_object_handlers = &spl_handler_SplObjectStorage; + spl_ce_MultipleIterator->default_object_handlers = &spl_handler_MultipleIterator; return SUCCESS; } diff --git a/ext/spl/tests/gh19094.phpt b/ext/spl/tests/gh19094.phpt new file mode 100644 index 000000000000..d0665c3e8f06 --- /dev/null +++ b/ext/spl/tests/gh19094.phpt @@ -0,0 +1,56 @@ +--TEST-- +GH-19094 (Attaching class with no Iterator implementation to MultipleIterator causes crash) +--FILE-- +getMessage(), "\n"; +} +try { + $cls[new MyAggregate] = 1; +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +$cls[new MyIterator] = 1; +try { + $cls->key(); +} catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +Can only attach objects that implement the Iterator interface +Can only attach objects that implement the Iterator interface +Called key() with non valid sub iterator From 9abb0fb0c4cce9c889211790c0bee9083e824991 Mon Sep 17 00:00:00 2001 From: Dmitry Stogov Date: Mon, 14 Jul 2025 14:27:05 +0300 Subject: [PATCH 66/69] Revert "Update IR" This reverts commit e8ae27bf8ae9e4b2a4fbb338c9e436e581370010. Something wrong in irrducable loops habdling that causes ir_find_loop() to stuck. See https://github.com/php/php-src/issues/19104 --- ext/opcache/jit/ir/ir.c | 4 +- ext/opcache/jit/ir/ir.h | 5 +- ext/opcache/jit/ir/ir_aarch64.dasc | 41 +---- ext/opcache/jit/ir/ir_cfg.c | 232 +++++++++-------------------- ext/opcache/jit/ir/ir_check.c | 9 +- ext/opcache/jit/ir/ir_emit.c | 4 +- ext/opcache/jit/ir/ir_fold.h | 4 +- ext/opcache/jit/ir/ir_gcm.c | 181 ++-------------------- ext/opcache/jit/ir/ir_private.h | 2 +- ext/opcache/jit/ir/ir_save.c | 3 - ext/opcache/jit/ir/ir_sccp.c | 15 +- ext/opcache/jit/ir/ir_x86.dasc | 97 ++---------- 12 files changed, 114 insertions(+), 483 deletions(-) diff --git a/ext/opcache/jit/ir/ir.c b/ext/opcache/jit/ir/ir.c index e90a5e80bf04..a9f55cc0e466 100644 --- a/ext/opcache/jit/ir/ir.c +++ b/ext/opcache/jit/ir/ir.c @@ -803,9 +803,7 @@ ir_ref ir_proto(ir_ctx *ctx, uint8_t flags, ir_type ret_type, uint32_t params_co proto->flags = flags; proto->ret_type = ret_type; proto->params_count = params_count; - if (params_count) { - memcpy(proto->param_types, param_types, params_count); - } + memcpy(proto->param_types, param_types, params_count); return ir_strl(ctx, (const char *)proto, offsetof(ir_proto_t, param_types) + params_count); } diff --git a/ext/opcache/jit/ir/ir.h b/ext/opcache/jit/ir/ir.h index be8779e01941..ec5e57129c9a 100644 --- a/ext/opcache/jit/ir/ir.h +++ b/ext/opcache/jit/ir/ir.h @@ -854,9 +854,6 @@ void ir_gdb_unregister_all(void); bool ir_gdb_present(void); /* IR load API (implementation in ir_load.c) */ -#define IR_RESOLVE_SYM_ADD_THUNK (1<<0) -#define IR_RESOLVE_SYM_SILENT (1<<1) - struct _ir_loader { uint32_t default_func_flags; bool (*init_module) (ir_loader *loader, const char *name, const char *filename, const char *target); @@ -873,7 +870,7 @@ struct _ir_loader { bool (*sym_data_end) (ir_loader *loader, uint32_t flags); bool (*func_init) (ir_loader *loader, ir_ctx *ctx, const char *name); bool (*func_process) (ir_loader *loader, ir_ctx *ctx, const char *name); - void*(*resolve_sym_name) (ir_loader *loader, const char *name, uint32_t flags); + void*(*resolve_sym_name) (ir_loader *loader, const char *name, bool add_thunk); bool (*has_sym) (ir_loader *loader, const char *name); bool (*add_sym) (ir_loader *loader, const char *name, void *addr); }; diff --git a/ext/opcache/jit/ir/ir_aarch64.dasc b/ext/opcache/jit/ir/ir_aarch64.dasc index 1d927cc8c726..772eea7a5d78 100644 --- a/ext/opcache/jit/ir/ir_aarch64.dasc +++ b/ext/opcache/jit/ir/ir_aarch64.dasc @@ -4366,15 +4366,11 @@ static void ir_emit_va_arg(ir_ctx *ctx, ir_ref def, ir_insn *insn) ir_backend_data *data = ctx->data; dasm_State **Dst = &data->dasm_state; ir_type type = insn->type; - ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]); + ir_reg def_reg = ctx->regs[def][0]; ir_reg op2_reg = ctx->regs[def][2]; ir_reg tmp_reg = ctx->regs[def][3]; int32_t offset; - if (ctx->use_lists[def].count == 1) { - /* dead load */ - return; - } IR_ASSERT(def_reg != IR_REG_NONE && tmp_reg != IR_REG_NONE); if (op2_reg != IR_REG_NONE) { if (IR_REG_SPILLED(op2_reg)) { @@ -4398,15 +4394,11 @@ static void ir_emit_va_arg(ir_ctx *ctx, ir_ref def, ir_insn *insn) ir_backend_data *data = ctx->data; dasm_State **Dst = &data->dasm_state; ir_type type = insn->type; - ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]); + ir_reg def_reg = ctx->regs[def][0]; ir_reg op2_reg = ctx->regs[def][2]; ir_reg tmp_reg = ctx->regs[def][3]; int32_t offset; - if (ctx->use_lists[def].count == 1) { - /* dead load */ - return; - } IR_ASSERT(def_reg != IR_REG_NONE && tmp_reg != IR_REG_NONE); if (op2_reg != IR_REG_NONE) { if (IR_REG_SPILLED(op2_reg)) { @@ -4943,28 +4935,6 @@ static void ir_emit_tailcall(ir_ctx *ctx, ir_ref def, ir_insn *insn) return; } - /* Move op2 to a tmp register before epilogue if it's in - * used_preserved_regs, because it will be overridden. */ - - ir_reg op2_reg = IR_REG_NONE; - if (!IR_IS_CONST_REF(insn->op2)) { - op2_reg = ctx->regs[def][2]; - IR_ASSERT(op2_reg != IR_REG_NONE); - - if (IR_REG_SPILLED(op2_reg)) { - op2_reg = IR_REG_INT_TMP; - ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2); - } else if (IR_REGSET_IN((ir_regset)ctx->used_preserved_regs, IR_REG_NUM(op2_reg))) { - ir_reg orig_op2_reg = op2_reg; - op2_reg = IR_REG_INT_TMP; - - ir_type type = ctx->ir_base[insn->op2].type; - | ASM_REG_REG_OP mov, type, op2_reg, IR_REG_NUM(orig_op2_reg) - } else { - op2_reg = IR_REG_NUM(op2_reg); - } - } - ir_emit_epilogue(ctx); if (IR_IS_CONST_REF(insn->op2)) { @@ -4977,8 +4947,13 @@ static void ir_emit_tailcall(ir_ctx *ctx, ir_ref def, ir_insn *insn) | br Rx(IR_REG_INT_TMP) } } else { + ir_reg op2_reg = ctx->regs[def][2]; + IR_ASSERT(op2_reg != IR_REG_NONE); - IR_ASSERT(!IR_REGSET_IN((ir_regset)ctx->used_preserved_regs, op2_reg)); + if (IR_REG_SPILLED(op2_reg)) { + op2_reg = IR_REG_NUM(op2_reg); + ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2); + } | br Rx(op2_reg) } } diff --git a/ext/opcache/jit/ir/ir_cfg.c b/ext/opcache/jit/ir/ir_cfg.c index 34375b0a3b55..01532c8ea3e3 100644 --- a/ext/opcache/jit/ir/ir_cfg.c +++ b/ext/opcache/jit/ir/ir_cfg.c @@ -244,6 +244,7 @@ int ir_build_cfg(ir_ctx *ctx) _blocks[start] = b; _blocks[end] = b; IR_ASSERT(IR_IS_BB_START(insn->op)); + IR_ASSERT(end > start); bb->start = start; bb->end = end; bb->successors = count; @@ -582,6 +583,7 @@ static int ir_remove_unreachable_blocks(ir_ctx *ctx) return 1; } +#if 0 static void compute_postnum(const ir_ctx *ctx, uint32_t *cur, uint32_t b) { uint32_t i, *p; @@ -605,42 +607,34 @@ static void compute_postnum(const ir_ctx *ctx, uint32_t *cur, uint32_t b) /* Computes dominator tree using algorithm from "A Simple, Fast Dominance Algorithm" by * Cooper, Harvey and Kennedy. */ -static int ir_build_dominators_tree_slow(ir_ctx *ctx) +int ir_build_dominators_tree(ir_ctx *ctx) { uint32_t blocks_count, b, postnum; ir_block *blocks, *bb; uint32_t *edges; bool changed; - blocks = ctx->cfg_blocks; - edges = ctx->cfg_edges; - blocks_count = ctx->cfg_blocks_count; - - /* Clear the dominators tree */ - for (b = 0, bb = &blocks[0]; b <= blocks_count; b++, bb++) { - bb->idom = 0; - bb->dom_depth = 0; - bb->dom_child = 0; - bb->dom_next_child = 0; - } - ctx->flags2 &= ~IR_NO_LOOPS; postnum = 1; compute_postnum(ctx, &postnum, 1); - /* Find immediate dominators by iterative fixed-point algorithm */ + /* Find immediate dominators */ + blocks = ctx->cfg_blocks; + edges = ctx->cfg_edges; + blocks_count = ctx->cfg_blocks_count; blocks[1].idom = 1; do { changed = 0; /* Iterating in Reverse Post Order */ for (b = 2, bb = &blocks[2]; b <= blocks_count; b++, bb++) { IR_ASSERT(!(bb->flags & IR_BB_UNREACHABLE)); - IR_ASSERT(bb->predecessors_count > 0); if (bb->predecessors_count == 1) { uint32_t pred_b = edges[bb->predecessors]; - if (blocks[pred_b].idom > 0 && bb->idom != pred_b) { + if (blocks[pred_b].idom <= 0) { + //IR_ASSERT("Wrong blocks order: BB is before its single predecessor"); + } else if (bb->idom != pred_b) { bb->idom = pred_b; changed = 1; } @@ -686,37 +680,39 @@ static int ir_build_dominators_tree_slow(ir_ctx *ctx) } } } while (changed); - - /* Build dominators tree */ blocks[1].idom = 0; blocks[1].dom_depth = 0; - for (b = 2, bb = &blocks[2]; b <= blocks_count; b++, bb++) { - uint32_t idom = bb->idom; - ir_block *idom_bb = &blocks[idom]; - bb->dom_depth = idom_bb->dom_depth + 1; - /* Sort by block number to traverse children in pre-order */ - if (idom_bb->dom_child == 0) { - idom_bb->dom_child = b; - } else if (b < idom_bb->dom_child) { - bb->dom_next_child = idom_bb->dom_child; - idom_bb->dom_child = b; - } else { - int child = idom_bb->dom_child; - ir_block *child_bb = &blocks[child]; + /* Construct dominators tree */ + for (b = 2, bb = &blocks[2]; b <= blocks_count; b++, bb++) { + IR_ASSERT(!(bb->flags & IR_BB_UNREACHABLE)); + if (bb->idom > 0) { + ir_block *idom_bb = &blocks[bb->idom]; + + bb->dom_depth = idom_bb->dom_depth + 1; + /* Sort by block number to traverse children in pre-order */ + if (idom_bb->dom_child == 0) { + idom_bb->dom_child = b; + } else if (b < idom_bb->dom_child) { + bb->dom_next_child = idom_bb->dom_child; + idom_bb->dom_child = b; + } else { + int child = idom_bb->dom_child; + ir_block *child_bb = &blocks[child]; - while (child_bb->dom_next_child > 0 && b > child_bb->dom_next_child) { - child = child_bb->dom_next_child; - child_bb = &blocks[child]; + while (child_bb->dom_next_child > 0 && b > child_bb->dom_next_child) { + child = child_bb->dom_next_child; + child_bb = &blocks[child]; + } + bb->dom_next_child = child_bb->dom_next_child; + child_bb->dom_next_child = b; } - bb->dom_next_child = child_bb->dom_next_child; - child_bb->dom_next_child = b; } } return 1; } - +#else /* A single pass modification of "A Simple, Fast Dominance Algorithm" by * Cooper, Harvey and Kennedy, that relays on IR block ordering. * It may fallback to the general slow fixed-point algorithm. */ @@ -751,11 +747,7 @@ int ir_build_dominators_tree(ir_ctx *ctx) if (UNEXPECTED(idom >= b)) { /* In rare cases, LOOP_BEGIN.op1 may be a back-edge. Skip back-edges. */ ctx->flags2 &= ~IR_NO_LOOPS; -// IR_ASSERT(k > 1 && "Wrong blocks order: BB is before its single predecessor"); - if (UNEXPECTED(k <= 1)) { - ir_list_free(&worklist); - return ir_build_dominators_tree_slow(ctx); - } + IR_ASSERT(k > 1 && "Wrong blocks order: BB is before its single predecessor"); ir_list_push(&worklist, idom); while (1) { k--; @@ -950,6 +942,7 @@ static int ir_build_dominators_tree_iterative(ir_ctx *ctx) return 1; } +#endif static bool ir_dominates(const ir_block *blocks, uint32_t b1, uint32_t b2) { @@ -965,7 +958,7 @@ static bool ir_dominates(const ir_block *blocks, uint32_t b1, uint32_t b2) int ir_find_loops(ir_ctx *ctx) { - uint32_t b, j, n, count; + uint32_t i, j, n, count; uint32_t *entry_times, *exit_times, *sorted_blocks, time = 1; ir_block *blocks = ctx->cfg_blocks; uint32_t *edges = ctx->cfg_edges; @@ -990,13 +983,13 @@ int ir_find_loops(ir_ctx *ctx) int child; next: - b = ir_worklist_peek(&work); - if (!entry_times[b]) { - entry_times[b] = time++; + i = ir_worklist_peek(&work); + if (!entry_times[i]) { + entry_times[i] = time++; } - /* Visit blocks immediately dominated by "b". */ - bb = &blocks[b]; + /* Visit blocks immediately dominated by i. */ + bb = &blocks[i]; for (child = bb->dom_child; child > 0; child = blocks[child].dom_next_child) { if (ir_worklist_push(&work, child)) { goto next; @@ -1006,17 +999,17 @@ int ir_find_loops(ir_ctx *ctx) /* Visit join edges. */ if (bb->successors_count) { uint32_t *p = edges + bb->successors; - for (j = 0; j < bb->successors_count; j++, p++) { + for (j = 0; j < bb->successors_count; j++,p++) { uint32_t succ = *p; - if (blocks[succ].idom == b) { + if (blocks[succ].idom == i) { continue; } else if (ir_worklist_push(&work, succ)) { goto next; } } } - exit_times[b] = time++; + exit_times[i] = time++; ir_worklist_pop(&work); } @@ -1025,7 +1018,7 @@ int ir_find_loops(ir_ctx *ctx) j = 1; n = 2; while (j != n) { - uint32_t i = j; + i = j; j = n; for (; i < j; i++) { int child; @@ -1037,82 +1030,9 @@ int ir_find_loops(ir_ctx *ctx) count = n; /* Identify loops. See Sreedhar et al, "Identifying Loops Using DJ Graphs". */ - uint32_t prev_dom_depth = blocks[sorted_blocks[n - 1]].dom_depth; - uint32_t prev_irreducible = 0; while (n > 1) { - b = sorted_blocks[--n]; - ir_block *bb = &blocks[b]; - - IR_ASSERT(bb->dom_depth <= prev_dom_depth); - if (UNEXPECTED(prev_irreducible) && bb->dom_depth != prev_dom_depth) { - /* process delyed irreducible loops */ - do { - b = sorted_blocks[prev_irreducible]; - bb = &blocks[b]; - if ((bb->flags & IR_BB_IRREDUCIBLE_LOOP) && !bb->loop_depth) { - /* process irreducible loop */ - uint32_t hdr = b; - - bb->loop_depth = 1; - if (ctx->ir_base[bb->start].op == IR_MERGE) { - ctx->ir_base[bb->start].op = IR_LOOP_BEGIN; - } - - /* find the closing edge(s) of the irreucible loop */ - IR_ASSERT(bb->predecessors_count > 1); - uint32_t *p = &edges[bb->predecessors]; - j = bb->predecessors_count; - do { - uint32_t pred = *p; - - if (entry_times[pred] > entry_times[b] && exit_times[pred] < exit_times[b]) { - if (!ir_worklist_len(&work)) { - ir_bitset_clear(work.visited, ir_bitset_len(ir_worklist_capasity(&work))); - } - blocks[pred].loop_header = 0; /* support for merged loops */ - ir_worklist_push(&work, pred); - } - p++; - } while (--j); - IR_ASSERT(ir_worklist_len(&work) != 0); - - /* collect members of the irreducible loop */ - while (ir_worklist_len(&work)) { - b = ir_worklist_pop(&work); - if (b != hdr) { - ir_block *bb = &blocks[b]; - bb->loop_header = hdr; - if (bb->predecessors_count) { - uint32_t *p = &edges[bb->predecessors]; - uint32_t n = bb->predecessors_count; - do { - uint32_t pred = *p; - while (blocks[pred].loop_header > 0) { - pred = blocks[pred].loop_header; - } - if (pred != hdr) { - if (entry_times[pred] > entry_times[hdr] && exit_times[pred] < exit_times[hdr]) { - /* "pred" is a descendant of "hdr" */ - ir_worklist_push(&work, pred); - } else { - /* another entry to the irreducible loop */ - bb->flags |= IR_BB_IRREDUCIBLE_LOOP; - if (ctx->ir_base[bb->start].op == IR_MERGE) { - ctx->ir_base[bb->start].op = IR_LOOP_BEGIN; - } - } - } - p++; - } while (--n); - } - } - } - } - } while (--prev_irreducible != n); - prev_irreducible = 0; - b = sorted_blocks[n]; - bb = &blocks[b]; - } + i = sorted_blocks[--n]; + ir_block *bb = &blocks[i]; if (bb->predecessors_count > 1) { bool irreducible = 0; @@ -1127,7 +1047,7 @@ int ir_find_loops(ir_ctx *ctx) if (bb->idom != pred) { /* In a loop back-edge (back-join edge), the successor dominates the predecessor. */ - if (ir_dominates(blocks, b, pred)) { + if (ir_dominates(blocks, i, pred)) { if (!ir_worklist_len(&work)) { ir_bitset_clear(work.visited, ir_bitset_len(ir_worklist_capasity(&work))); } @@ -1136,9 +1056,8 @@ int ir_find_loops(ir_ctx *ctx) } else { /* Otherwise it's a cross-join edge. See if it's a branch to an ancestor on the DJ spanning tree. */ - if (entry_times[pred] > entry_times[b] && exit_times[pred] < exit_times[b]) { + if (entry_times[pred] > entry_times[i] && exit_times[pred] < exit_times[i]) { irreducible = 1; - break; } } } @@ -1146,55 +1065,46 @@ int ir_find_loops(ir_ctx *ctx) } while (--j); if (UNEXPECTED(irreducible)) { - bb->flags |= IR_BB_LOOP_HEADER | IR_BB_IRREDUCIBLE_LOOP; - ctx->flags2 |= IR_CFG_HAS_LOOPS | IR_IRREDUCIBLE_CFG; - /* Remember the position of the first irreducible loop to process all the irreducible loops - * after the reducible loops with the same dominator tree depth - */ - if (!prev_irreducible) { - prev_irreducible = n; + // TODO: Support for irreducible loops ??? + bb->flags |= IR_BB_IRREDUCIBLE_LOOP; + ctx->flags2 |= IR_IRREDUCIBLE_CFG; + while (ir_worklist_len(&work)) { + ir_worklist_pop(&work); } - ir_list_clear(&work.l); } else if (ir_worklist_len(&work)) { - /* collect members of the reducible loop */ - uint32_t hdr = b; - bb->flags |= IR_BB_LOOP_HEADER; ctx->flags2 |= IR_CFG_HAS_LOOPS; bb->loop_depth = 1; - if (ctx->ir_base[bb->start].op == IR_MERGE) { - ctx->ir_base[bb->start].op = IR_LOOP_BEGIN; - } while (ir_worklist_len(&work)) { - b = ir_worklist_pop(&work); - if (b != hdr) { - ir_block *bb = &blocks[b]; - bb->loop_header = hdr; + j = ir_worklist_pop(&work); + while (blocks[j].loop_header > 0) { + j = blocks[j].loop_header; + } + if (j != i) { + ir_block *bb = &blocks[j]; + if (bb->idom == 0 && j != 1) { + /* Ignore blocks that are unreachable or only abnormally reachable. */ + continue; + } + bb->loop_header = i; if (bb->predecessors_count) { uint32_t *p = &edges[bb->predecessors]; - uint32_t n = bb->predecessors_count; + j = bb->predecessors_count; do { - uint32_t pred = *p; - while (blocks[pred].loop_header > 0) { - pred = blocks[pred].loop_header; - } - if (pred != hdr) { - ir_worklist_push(&work, pred); - } + ir_worklist_push(&work, *p); p++; - } while (--n); + } while (--j); } } } } } } - IR_ASSERT(!prev_irreducible); if (ctx->flags2 & IR_CFG_HAS_LOOPS) { for (n = 1; n < count; n++) { - b = sorted_blocks[n]; - ir_block *bb = &blocks[b]; + i = sorted_blocks[n]; + ir_block *bb = &blocks[i]; if (bb->loop_header > 0) { ir_block *loop = &blocks[bb->loop_header]; uint32_t loop_depth = loop->loop_depth; @@ -1479,7 +1389,7 @@ static int ir_schedule_blocks_bottom_up(ir_ctx *ctx) goto restart; } } else if (b != predecessor && ctx->cfg_blocks[predecessor].loop_header != b) { - /* not a loop back-edge */ + ir_dump_cfg(ctx, stderr); IR_ASSERT(b == predecessor || ctx->cfg_blocks[predecessor].loop_header == b); } } diff --git a/ext/opcache/jit/ir/ir_check.c b/ext/opcache/jit/ir/ir_check.c index a791baef5db9..f12b4776fa1e 100644 --- a/ext/opcache/jit/ir/ir_check.c +++ b/ext/opcache/jit/ir/ir_check.c @@ -213,16 +213,11 @@ bool ir_check(const ir_ctx *ctx) ok = 0; } } - if ((ctx->flags2 & IR_LINEAR) - && use >= i - && !(insn->op == IR_LOOP_BEGIN)) { - fprintf(stderr, "ir_base[%d].ops[%d] invalid forward reference (%d)\n", i, j, use); - ok = 0; - } break; case IR_OPND_CONTROL_DEP: if ((ctx->flags2 & IR_LINEAR) - && use >= i) { + && use >= i + && !(insn->op == IR_LOOP_BEGIN)) { fprintf(stderr, "ir_base[%d].ops[%d] invalid forward reference (%d)\n", i, j, use); ok = 0; } else if (insn->op == IR_PHI) { diff --git a/ext/opcache/jit/ir/ir_emit.c b/ext/opcache/jit/ir/ir_emit.c index fab9f56228d8..c82655daf48d 100644 --- a/ext/opcache/jit/ir/ir_emit.c +++ b/ext/opcache/jit/ir/ir_emit.c @@ -309,7 +309,7 @@ static void* ir_sym_addr(ir_ctx *ctx, const ir_insn *addr_insn) { const char *name = ir_get_str(ctx, addr_insn->val.name); void *addr = (ctx->loader && ctx->loader->resolve_sym_name) ? - ctx->loader->resolve_sym_name(ctx->loader, name, IR_RESOLVE_SYM_SILENT) : + ctx->loader->resolve_sym_name(ctx->loader, name, 0) : ir_resolve_sym_name(name); return addr; @@ -320,7 +320,7 @@ static void* ir_sym_val(ir_ctx *ctx, const ir_insn *addr_insn) { const char *name = ir_get_str(ctx, addr_insn->val.name); void *addr = (ctx->loader && ctx->loader->resolve_sym_name) ? - ctx->loader->resolve_sym_name(ctx->loader, name, addr_insn->op == IR_FUNC ? IR_RESOLVE_SYM_ADD_THUNK : 0) : + ctx->loader->resolve_sym_name(ctx->loader, name, addr_insn->op == IR_FUNC) : ir_resolve_sym_name(name); IR_ASSERT(addr); diff --git a/ext/opcache/jit/ir/ir_fold.h b/ext/opcache/jit/ir/ir_fold.h index 90112214d0c8..88539e52ab08 100644 --- a/ext/opcache/jit/ir/ir_fold.h +++ b/ext/opcache/jit/ir/ir_fold.h @@ -1909,9 +1909,7 @@ IR_FOLD(SUB(_, SUB)) IR_FOLD(SUB(ADD, ADD)) { if (IR_IS_TYPE_INT(IR_OPT_TYPE(opt))) { - if (op1 == op2) { - IR_FOLD_CONST_U(0); - } else if (op1_insn->op1 == op2_insn->op1) { + if (op1_insn->op1 == op2_insn->op1) { /* (a + b) - (a + c) => b - c */ op1 = op1_insn->op2; op2 = op2_insn->op2; diff --git a/ext/opcache/jit/ir/ir_gcm.c b/ext/opcache/jit/ir/ir_gcm.c index 0d8a6c2d760b..8bd6be5d10aa 100644 --- a/ext/opcache/jit/ir/ir_gcm.c +++ b/ext/opcache/jit/ir/ir_gcm.c @@ -785,139 +785,6 @@ IR_ALWAYS_INLINE ir_ref ir_count_constant(ir_ref *_xlat, ir_ref ref) return 0; } -IR_ALWAYS_INLINE bool ir_is_good_bb_order(ir_ctx *ctx, uint32_t b, ir_block *bb, ir_ref start) -{ - ir_insn *insn = &ctx->ir_base[start]; - uint32_t n = insn->inputs_count; - ir_ref *p = insn->ops + 1; - - if (n == 1) { - return *p < start; - } else { - IR_ASSERT(n > 1); - for (; n > 0; p++, n--) { - ir_ref input = *p; - if (input < start) { - /* ordered */ - } else if ((bb->flags & IR_BB_LOOP_HEADER) - && (ctx->cfg_map[input] == b || ctx->cfg_blocks[ctx->cfg_map[input]].loop_header == b)) { - /* back-edge of reducible loop */ - } else if ((bb->flags & IR_BB_IRREDUCIBLE_LOOP) - && (ctx->cfg_blocks[ctx->cfg_map[input]].loop_header == ctx->cfg_blocks[b].loop_header)) { - /* closing edge of irreducible loop */ - } else { - return 0; - } - } - return 1; - } -} - -static IR_NEVER_INLINE void ir_fix_bb_order(ir_ctx *ctx, ir_ref *_prev, ir_ref *_next) -{ - uint32_t b, succ, count, *q, *xlat; - ir_block *bb; - ir_ref ref, n, prev; - ir_worklist worklist; - ir_block *new_blocks; - -#if 0 - for (b = 1, bb = ctx->cfg_blocks + 1; b <= ctx->cfg_blocks_count; b++, bb++) { - if (!ir_is_good_bb_order(ctx, b, bb, bb->start)) { - goto fix; - } - } - return; - -fix: -#endif - count = ctx->cfg_blocks_count + 1; - new_blocks = ir_mem_malloc(count * sizeof(ir_block)); - xlat = ir_mem_malloc(count * sizeof(uint32_t)); - ir_worklist_init(&worklist, count); - ir_worklist_push(&worklist, 1); - while (ir_worklist_len(&worklist) != 0) { -next: - b = ir_worklist_peek(&worklist); - bb = &ctx->cfg_blocks[b]; - n = bb->successors_count; - if (n == 1) { - succ = ctx->cfg_edges[bb->successors]; - if (ir_worklist_push(&worklist, succ)) { - goto next; - } - } else if (n > 1) { - uint32_t best = 0; - uint32_t best_loop_depth = 0; - - q = ctx->cfg_edges + bb->successors + n; - do { - q--; - succ = *q; - if (ir_bitset_in(worklist.visited, succ)) { - /* already processed */ - } else if ((ctx->cfg_blocks[succ].flags & IR_BB_LOOP_HEADER) - && (succ == b || ctx->cfg_blocks[b].loop_header == succ)) { - /* back-edge of reducible loop */ - } else if ((ctx->cfg_blocks[succ].flags & IR_BB_IRREDUCIBLE_LOOP) - && (ctx->cfg_blocks[succ].loop_header == ctx->cfg_blocks[b].loop_header)) { - /* closing edge of irreducible loop */ - } else if (!best) { - best = succ; - best_loop_depth = ctx->cfg_blocks[best].loop_depth; - } else if (ctx->cfg_blocks[succ].loop_depth < best_loop_depth) { - /* prefer deeper loop */ - best = succ; - best_loop_depth = ctx->cfg_blocks[best].loop_depth; - } - n--; - } while (n > 0); - if (best) { - ir_worklist_push(&worklist, best); - goto next; - } - } - ir_worklist_pop(&worklist); - count--; - new_blocks[count] = *bb; - xlat[b] = count; - } - IR_ASSERT(count == 1); - xlat[0] = 0; - ir_worklist_free(&worklist); - - prev = 0; - for (b = 1, bb = new_blocks + 1; b <= ctx->cfg_blocks_count; b++, bb++) { - bb->idom = xlat[bb->idom]; - bb->loop_header = xlat[bb->loop_header]; - n = bb->successors_count; - if (n > 0) { - for (q = ctx->cfg_edges + bb->successors; n > 0; q++, n--) { - *q = xlat[*q]; - } - } - n = bb->predecessors_count; - if (n > 0) { - for (q = ctx->cfg_edges + bb->predecessors; n > 0; q++, n--) { - *q = xlat[*q]; - } - } - _next[prev] = bb->start; - _prev[bb->start] = prev; - prev = bb->end; - } - _next[0] = 0; - _next[prev] = 0; - - for (ref = 2; ref < ctx->insns_count; ref++) { - ctx->cfg_map[ref] = xlat[ctx->cfg_map[ref]]; - } - ir_mem_free(xlat); - - ir_mem_free(ctx->cfg_blocks); - ctx->cfg_blocks = new_blocks; -} - int ir_schedule(ir_ctx *ctx) { ir_ctx new_ctx; @@ -933,7 +800,6 @@ int ir_schedule(ir_ctx *ctx) ir_block *bb; ir_insn *insn, *new_insn; ir_use_list *lists, *use_list, *new_list; - bool bad_bb_order = 0; /* Create a double-linked list of nodes ordered by BB, respecting BB->start and BB->end */ IR_ASSERT(_blocks[1] == 1); @@ -952,50 +818,27 @@ int ir_schedule(ir_ctx *ctx) } else if (b > prev_b) { bb = &ctx->cfg_blocks[b]; if (i == bb->start) { - if (bb->end > bb->start) { - prev_b = b; - prev_b_end = bb->end; - /* add to the end of the list */ - _next[j] = i; - _prev[i] = j; - j = i; - } else { - prev_b = 0; - prev_b_end = 0; - k = bb->end; - while (_blocks[_prev[k]] == b) { - k = _prev[k]; - } - /* insert before "k" */ - _prev[i] = _prev[k]; - _next[i] = k; - _next[_prev[k]] = i; - _prev[k] = i; - } - if (!ir_is_good_bb_order(ctx, b, bb, i)) { - bad_bb_order = 1; - } - } else if (i != bb->end) { - /* move down late (see the following loop) */ - _next[i] = _move_down; - _move_down = i; - } else { - IR_ASSERT(bb->start > bb->end); - prev_b = 0; - prev_b_end = 0; + IR_ASSERT(bb->end > bb->start); + prev_b = b; + prev_b_end = bb->end; + _prev[bb->end] = 0; /* add to the end of the list */ _next[j] = i; _prev[i] = j; j = i; + } else { + IR_ASSERT(i != bb->end); + /* move down late (see the following loop) */ + _next[i] = _move_down; + _move_down = i; } } else if (b) { bb = &ctx->cfg_blocks[b]; IR_ASSERT(i != bb->start); - if (i > bb->end) { + if (_prev[bb->end]) { /* move up, insert before the end of the already scheduled BB */ k = bb->end; } else { - IR_ASSERT(i > bb->start); /* move up, insert at the end of the block */ k = ctx->cfg_blocks[b + 1].start; } @@ -1040,10 +883,6 @@ int ir_schedule(ir_ctx *ctx) } #endif - if (bad_bb_order) { - ir_fix_bb_order(ctx, _prev, _next); - } - _xlat = ir_mem_calloc((ctx->consts_count + ctx->insns_count), sizeof(ir_ref)); _xlat += ctx->consts_count; _xlat[IR_TRUE] = IR_TRUE; diff --git a/ext/opcache/jit/ir/ir_private.h b/ext/opcache/jit/ir/ir_private.h index ac952e402f52..69a0101d24ee 100644 --- a/ext/opcache/jit/ir/ir_private.h +++ b/ext/opcache/jit/ir/ir_private.h @@ -62,7 +62,7 @@ #define IR_MAX(a, b) (((a) > (b)) ? (a) : (b)) #define IR_MIN(a, b) (((a) < (b)) ? (a) : (b)) -#define IR_IS_POWER_OF_TWO(x) ((x) && (!((x) & ((x) - 1)))) +#define IR_IS_POWER_OF_TWO(x) (!((x) & ((x) - 1))) #define IR_LOG2(x) ir_ntzl(x) diff --git a/ext/opcache/jit/ir/ir_save.c b/ext/opcache/jit/ir/ir_save.c index 595f2d9d6a21..b12cc267af60 100644 --- a/ext/opcache/jit/ir/ir_save.c +++ b/ext/opcache/jit/ir/ir_save.c @@ -140,9 +140,6 @@ void ir_save(const ir_ctx *ctx, uint32_t save_flags, FILE *f) fprintf(f, ", loop=BB%d(%d)", bb->loop_header, bb->loop_depth); } } - if (bb->flags & IR_BB_IRREDUCIBLE_LOOP) { - fprintf(f, ", IRREDUCIBLE"); - } if (bb->predecessors_count) { uint32_t i; diff --git a/ext/opcache/jit/ir/ir_sccp.c b/ext/opcache/jit/ir/ir_sccp.c index 221a86a5ad8a..2e006516df81 100644 --- a/ext/opcache/jit/ir/ir_sccp.c +++ b/ext/opcache/jit/ir/ir_sccp.c @@ -1732,20 +1732,7 @@ static ir_ref ir_promote_i2i(ir_ctx *ctx, ir_type type, ir_ref ref, ir_ref use, ir_ref *p, n, input; if (IR_IS_CONST_REF(ref)) { - ir_val val; - - switch (type) { - case IR_I8: val.i64 = insn->val.i8; break; - case IR_U8: val.u64 = insn->val.u8; break; - case IR_I16: val.i64 = insn->val.i16; break; - case IR_U16: val.u64 = insn->val.u16; break; - case IR_I32: val.i64 = insn->val.i32; break; - case IR_U32: val.u64 = insn->val.u32; break; - case IR_CHAR:val.i64 = insn->val.i8; break; - case IR_BOOL:val.u64 = insn->val.u8 != 0; break; - default: IR_ASSERT(0); val.u64 = 0; - } - return ir_const(ctx, val, type); + return ir_const(ctx, insn->val, type); } else { ir_bitqueue_add(worklist, ref); switch (insn->op) { diff --git a/ext/opcache/jit/ir/ir_x86.dasc b/ext/opcache/jit/ir/ir_x86.dasc index d56cb8645e18..76602c2b4bcf 100644 --- a/ext/opcache/jit/ir/ir_x86.dasc +++ b/ext/opcache/jit/ir/ir_x86.dasc @@ -6868,24 +6868,7 @@ static void ir_emit_return_fp(ir_ctx *ctx, ir_ref ref, ir_insn *insn) ir_backend_data *data = ctx->data; dasm_State **Dst = &data->dasm_state; - if (IR_IS_CONST_REF(insn->op2)) { - ir_insn *value = &ctx->ir_base[insn->op2]; - - if ((type == IR_FLOAT && value->val.f == 0.0) || (type == IR_DOUBLE && value->val.d == 0.0)) { - | fldz - } else if ((type == IR_FLOAT && value->val.f == 1.0) || (type == IR_DOUBLE && value->val.d == 1.0)) { - | fld1 - } else { - int label = ir_const_label(ctx, insn->op2); - - if (type == IR_DOUBLE) { - | fld qword [=>label] - } else { - IR_ASSERT(type == IR_FLOAT); - | fld dword [=>label] - } - } - } else if (op2_reg == IR_REG_NONE || IR_REG_SPILLED(op2_reg)) { + if (op2_reg == IR_REG_NONE || IR_REG_SPILLED(op2_reg)) { ir_reg fp; int32_t offset = ir_ref_spill_slot_offset(ctx, insn->op2, &fp); @@ -8459,15 +8442,11 @@ static void ir_emit_va_arg(ir_ctx *ctx, ir_ref def, ir_insn *insn) ir_backend_data *data = ctx->data; dasm_State **Dst = &data->dasm_state; ir_type type = insn->type; - ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]); + ir_reg def_reg = ctx->regs[def][0]; ir_reg op2_reg = ctx->regs[def][2]; ir_reg tmp_reg = ctx->regs[def][3]; int32_t offset; - if (ctx->use_lists[def].count == 1) { - /* dead load */ - return; - } IR_ASSERT(def_reg != IR_REG_NONE && tmp_reg != IR_REG_NONE); if (op2_reg != IR_REG_NONE) { if (IR_REG_SPILLED(op2_reg)) { @@ -8492,15 +8471,11 @@ static void ir_emit_va_arg(ir_ctx *ctx, ir_ref def, ir_insn *insn) ir_backend_data *data = ctx->data; dasm_State **Dst = &data->dasm_state; ir_type type = insn->type; - ir_reg def_reg = IR_REG_NUM(ctx->regs[def][0]); + ir_reg def_reg = ctx->regs[def][0]; ir_reg op2_reg = ctx->regs[def][2]; ir_reg tmp_reg = ctx->regs[def][3]; int32_t offset; - if (ctx->use_lists[def].count == 1) { - /* dead load */ - return; - } IR_ASSERT(def_reg != IR_REG_NONE&& tmp_reg != IR_REG_NONE); if (op2_reg != IR_REG_NONE) { if (IR_REG_SPILLED(op2_reg)) { @@ -9246,58 +9221,6 @@ static void ir_emit_tailcall(ir_ctx *ctx, ir_ref def, ir_insn *insn) return; } - /* Move op2 to a tmp register before epilogue if it's in - * used_preserved_regs, because it will be overridden. */ - - ir_reg op2_reg = IR_REG_NONE; - ir_mem mem = IR_MEM_B(IR_REG_NONE); - if (!IR_IS_CONST_REF(insn->op2)) { - op2_reg = ctx->regs[def][2]; - - ir_regset preserved_regs = (ir_regset)ctx->used_preserved_regs | IR_REGSET(IR_REG_STACK_POINTER); - if (ctx->flags & IR_USE_FRAME_POINTER) { - preserved_regs |= IR_REGSET(IR_REG_FRAME_POINTER); - } - - bool is_spill_slot = op2_reg != IR_REG_NONE - && IR_REG_SPILLED(op2_reg) - && ctx->vregs[insn->op2]; - - if (op2_reg != IR_REG_NONE && !is_spill_slot) { - if (IR_REGSET_IN(preserved_regs, IR_REG_NUM(op2_reg))) { - ir_ref orig_op2_reg = op2_reg; - op2_reg = IR_REG_RAX; - - if (IR_REG_SPILLED(orig_op2_reg)) { - ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2); - } else { - ir_type type = ctx->ir_base[insn->op2].type; - | ASM_REG_REG_OP mov, type, op2_reg, IR_REG_NUM(orig_op2_reg) - } - } else { - op2_reg = IR_REG_NUM(op2_reg); - } - } else { - if (ir_rule(ctx, insn->op2) & IR_FUSED) { - IR_ASSERT(op2_reg == IR_REG_NONE); - mem = ir_fuse_load(ctx, def, insn->op2); - } else { - mem = ir_ref_spill_slot(ctx, insn->op2); - } - ir_reg base = IR_MEM_BASE(mem); - ir_reg index = IR_MEM_INDEX(mem); - if ((base != IR_REG_NONE && IR_REGSET_IN(preserved_regs, base)) || - (index != IR_REG_NONE && IR_REGSET_IN(preserved_regs, index))) { - op2_reg = IR_REG_RAX; - - ir_type type = ctx->ir_base[insn->op2].type; - ir_emit_load_mem_int(ctx, type, op2_reg, mem); - } else { - op2_reg = IR_REG_NONE; - } - } - } - ir_emit_epilogue(ctx); if (IR_IS_CONST_REF(insn->op2)) { @@ -9323,10 +9246,22 @@ static void ir_emit_tailcall(ir_ctx *ctx, ir_ref def, ir_insn *insn) |.endif } } else { + ir_reg op2_reg = ctx->regs[def][2]; + if (op2_reg != IR_REG_NONE) { - IR_ASSERT(!IR_REGSET_IN((ir_regset)ctx->used_preserved_regs, op2_reg)); + if (IR_REG_SPILLED(op2_reg)) { + op2_reg = IR_REG_NUM(op2_reg); + ir_emit_load(ctx, IR_ADDR, op2_reg, insn->op2); + } | jmp Ra(op2_reg) } else { + ir_mem mem; + + if (ir_rule(ctx, insn->op2) & IR_FUSED) { + mem = ir_fuse_load(ctx, def, insn->op2); + } else { + mem = ir_ref_spill_slot(ctx, insn->op2); + } | ASM_TMEM_OP jmp, aword, mem } } From 6b2b60f683afbdb649862273c8de570e963c1b17 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 21 Jun 2025 19:48:51 +0200 Subject: [PATCH 67/69] Fix bug #80770: openssl cafile not used in SNI SSL_CTX The issue is about not being able to connect as cafile for SNI is not used in its SSL context. This sets it up so it is possible to capture the client certificate which is only possible when verify_peer is true. Closes GH-18893 --- NEWS | 4 ++ ext/openssl/tests/bug80770.phpt | 83 +++++++++++++++++++++++++++++++++ ext/openssl/xp_ssl.c | 20 ++++++-- 3 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 ext/openssl/tests/bug80770.phpt diff --git a/NEWS b/NEWS index 3e45adc3d9f4..c395d0dcd3b5 100644 --- a/NEWS +++ b/NEWS @@ -34,6 +34,10 @@ PHP NEWS . Fixed bug GH-14082 (Segmentation fault on unknown address 0x600000000018 in ext/opcache/jit/zend_jit.c). (nielsdos) +- OpenSSL: + . Fixed bug #80770 (It is not possible to get client peer certificate with + stream_socket_server). (Jakub Zelenka) + - PCNTL: . Fixed bug GH-18958 (Fatal error during shutdown after pcntl_rfork() or pcntl_forkx() with zend-max-execution-timers). (Arnaud) diff --git a/ext/openssl/tests/bug80770.phpt b/ext/openssl/tests/bug80770.phpt new file mode 100644 index 000000000000..9100aaa5aa18 --- /dev/null +++ b/ext/openssl/tests/bug80770.phpt @@ -0,0 +1,83 @@ +--TEST-- +Bug #80770: SNI_server_certs does not inherit peer verification options +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'SNI_server_certs' => [ + "cs.php.net" => __DIR__ . "/sni_server_cs.pem", + "uk.php.net" => __DIR__ . "/sni_server_uk.pem", + "us.php.net" => __DIR__ . "/sni_server_us.pem" + ], + 'verify_peer' => true, + 'cafile' => '%s', + 'capture_peer_cert' => true, + 'verify_peer_name' => false, + 'security_level' => 0, + ]]); + $server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + $client = stream_socket_accept($server, 30); + if ($client) { + $success = stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_SERVER); + if ($success) { + $options = stream_context_get_options($client); + $hasCert = isset($options['ssl']['peer_certificate']); + phpt_notify(message: $hasCert ? "CLIENT_CERT_CAPTURED" : "NO_CLIENT_CERT"); + } else { + phpt_notify(message: "TLS_HANDSHAKE_FAILED"); + } + } else { + phpt_notify(message: "ACCEPT_FAILED"); + } +CODE; +$serverCode = sprintf($serverCode, $caCertFile); + +$clientCode = <<<'CODE' + $flags = STREAM_CLIENT_CONNECT; + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'local_cert' => '%s', + 'peer_name' => 'cs.php.net', + 'security_level' => 0, + ]]); + $client = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + if ($client) { + stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); + } + + $result = phpt_wait(); + echo trim($result); +CODE; +$clientCode = sprintf($clientCode, $clientCertFile); + +include 'CertificateGenerator.inc'; + +// Generate CA and client certificate signed by that CA +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveCaCert($caCertFile); +$certificateGenerator->saveNewCertAsFileWithKey('Bug80770 Test Client', $clientCertFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +CLIENT_CERT_CAPTURED diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 1c7aabefa1bc..37ab9c4012d8 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -1443,7 +1443,8 @@ static SSL_CTX *php_openssl_create_sni_server_ctx(char *cert_path, char *key_pat } /* }}} */ -static zend_result php_openssl_enable_server_sni(php_stream *stream, php_openssl_netstream_data_t *sslsock) /* {{{ */ +static zend_result php_openssl_enable_server_sni( + php_stream *stream, php_openssl_netstream_data_t *sslsock, bool verify_peer) { zval *val; zval *current; @@ -1564,6 +1565,12 @@ static zend_result php_openssl_enable_server_sni(php_stream *stream, php_openssl return FAILURE; } + if (!verify_peer) { + php_openssl_disable_peer_verification(ctx, stream); + } else if (FAILURE == php_openssl_enable_peer_verification(ctx, stream)) { + return FAILURE; + } + sslsock->sni_certs[i].name = pestrdup(ZSTR_VAL(key), php_stream_is_persistent(stream)); sslsock->sni_certs[i].ctx = ctx; ++i; @@ -1574,7 +1581,6 @@ static zend_result php_openssl_enable_server_sni(php_stream *stream, php_openssl return SUCCESS; } -/* }}} */ static void php_openssl_enable_client_sni(php_stream *stream, php_openssl_netstream_data_t *sslsock) /* {{{ */ { @@ -1666,6 +1672,7 @@ zend_result php_openssl_setup_crypto(php_stream *stream, char *cipherlist = NULL; char *alpn_protocols = NULL; zval *val; + bool verify_peer = false; if (sslsock->ssl_handle) { if (sslsock->s.is_blocked) { @@ -1717,8 +1724,11 @@ zend_result php_openssl_setup_crypto(php_stream *stream, if (GET_VER_OPT("verify_peer") && !zend_is_true(val)) { php_openssl_disable_peer_verification(sslsock->ctx, stream); - } else if (FAILURE == php_openssl_enable_peer_verification(sslsock->ctx, stream)) { - return FAILURE; + } else { + verify_peer = true; + if (FAILURE == php_openssl_enable_peer_verification(sslsock->ctx, stream)) { + return FAILURE; + } } /* callback for the passphrase (for localcert) */ @@ -1819,7 +1829,7 @@ zend_result php_openssl_setup_crypto(php_stream *stream, #ifdef HAVE_TLS_SNI /* Enable server-side SNI */ - if (!sslsock->is_client && php_openssl_enable_server_sni(stream, sslsock) == FAILURE) { + if (!sslsock->is_client && php_openssl_enable_server_sni(stream, sslsock, verify_peer) == FAILURE) { return FAILURE; } #endif From c077787a531d9dadf4c6d888e5c81cb3ff8bedb8 Mon Sep 17 00:00:00 2001 From: Calvin Buckley Date: Tue, 29 Jul 2025 12:27:36 -0300 Subject: [PATCH 68/69] Update versions for PHP 8.4.11 --- Zend/zend.h | 2 +- configure.ac | 2 +- main/php_version.h | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Zend/zend.h b/Zend/zend.h index 682dc813b5a6..98b55cb46bae 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -20,7 +20,7 @@ #ifndef ZEND_H #define ZEND_H -#define ZEND_VERSION "4.4.11-dev" +#define ZEND_VERSION "4.4.11" #define ZEND_ENGINE_3 diff --git a/configure.ac b/configure.ac index 53481b10c056..49d7ffefe504 100644 --- a/configure.ac +++ b/configure.ac @@ -17,7 +17,7 @@ dnl Basic autoconf initialization, generation of config.nice. dnl ---------------------------------------------------------------------------- AC_PREREQ([2.68]) -AC_INIT([PHP],[8.4.11-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) +AC_INIT([PHP],[8.4.11],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) AC_CONFIG_SRCDIR([main/php_version.h]) AC_CONFIG_AUX_DIR([build]) AC_PRESERVE_HELP_ORDER diff --git a/main/php_version.h b/main/php_version.h index e518de26b2f0..06724496b667 100644 --- a/main/php_version.h +++ b/main/php_version.h @@ -3,6 +3,6 @@ #define PHP_MAJOR_VERSION 8 #define PHP_MINOR_VERSION 4 #define PHP_RELEASE_VERSION 11 -#define PHP_EXTRA_VERSION "-dev" -#define PHP_VERSION "8.4.11-dev" +#define PHP_EXTRA_VERSION "" +#define PHP_VERSION "8.4.11" #define PHP_VERSION_ID 80411 From a42bbd343f41ee56c173d9c92333ed4f7ba24f97 Mon Sep 17 00:00:00 2001 From: Calvin Buckley Date: Tue, 29 Jul 2025 12:30:21 -0300 Subject: [PATCH 69/69] Update NEWS for PHP 8.4.11 --- NEWS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 5190b9b563a8..0aa9939f77e6 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,6 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| -?? ??? ????, PHP 8.4.11 +31 Jul 2025, PHP 8.4.11 - Calendar: . Fixed jewishtojd overflow on year argument. (David Carlier)