From df1250dc64f53db849503618d2f911645cd43e3a Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 20 Jun 2026 20:50:06 -0400 Subject: [PATCH 01/21] ext/gd: fix out-of-bounds write reading font header on short reads imageloadfont() read the font header with `(char*)&font[b]`, which scales the byte counter b by sizeof(gdFont) rather than advancing one byte, so a short php_stream_read() (deliverable by a user stream wrapper) makes the loop write hdr_size-b bytes past the emalloc(sizeof(gdFont)) buffer. Index the destination by bytes, matching the body read a few lines below. Closes GH-22380 --- ext/gd/gd.c | 2 +- ext/gd/tests/imageloadfont_short_read.phpt | 65 ++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 ext/gd/tests/imageloadfont_short_read.phpt diff --git a/ext/gd/gd.c b/ext/gd/gd.c index 16e871f6bc30..311900ba7bb3 100644 --- a/ext/gd/gd.c +++ b/ext/gd/gd.c @@ -556,7 +556,7 @@ PHP_FUNCTION(imageloadfont) */ font = (gdFontPtr) emalloc(sizeof(gdFont)); b = 0; - while (b < hdr_size && (n = php_stream_read(stream, (char*)&font[b], hdr_size - b)) > 0) { + while (b < hdr_size && (n = php_stream_read(stream, (char *) font + b, hdr_size - b)) > 0) { b += n; } diff --git a/ext/gd/tests/imageloadfont_short_read.phpt b/ext/gd/tests/imageloadfont_short_read.phpt new file mode 100644 index 000000000000..5a7f6a14c9bb --- /dev/null +++ b/ext/gd/tests/imageloadfont_short_read.phpt @@ -0,0 +1,65 @@ +--TEST-- +imageloadfont(): header read must stay in bounds on short reads +--EXTENSIONS-- +gd +--FILE-- +data = pack('V4', 1, 32, 1, 1) . "\x00"; + return true; + } + + public function stream_read($count): string + { + return $this->pos < strlen($this->data) ? $this->data[$this->pos++] : ''; + } + + public function stream_eof(): bool + { + return $this->pos >= strlen($this->data); + } + + public function stream_stat() + { + return []; + } + + public function stream_tell(): int + { + return $this->pos; + } + + public function stream_seek($offset, $whence): bool + { + if ($whence === SEEK_CUR) { + $this->pos += $offset; + } elseif ($whence === SEEK_END) { + $this->pos = strlen($this->data) + $offset; + } else { + $this->pos = $offset; + } + return true; + } + + public function stream_set_option($option, $arg1, $arg2): bool + { + return false; + } +} + +stream_wrapper_register('drip', drip::class); +var_dump(imageloadfont('drip://font') instanceof GdFont); +?> +--EXPECT-- +bool(true) From ca4561cda685da615f9d1e1a6db0c8251bb20428 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 20 Jun 2026 21:05:06 -0400 Subject: [PATCH 02/21] ext/com_dotnet: release the held IUnknown in com_get_active_object() The cleanup block guarded on `unk` but released `obj`. On a successful GetActiveObject() this released the IDispatch proxy twice and leaked the IUnknown; on a QueryInterface failure `obj` is still NULL while `unk` is live, so the same line released NULL through a NULL vtable and crashed instead of throwing. Release `unk` so each interface pointer is released exactly once and the failure path no longer crashes. Closes GH-22378 --- ext/com_dotnet/com_com.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/com_dotnet/com_com.c b/ext/com_dotnet/com_com.c index fd2d1ee9a439..cfc343405e0e 100644 --- a/ext/com_dotnet/com_com.c +++ b/ext/com_dotnet/com_com.c @@ -321,7 +321,7 @@ PHP_FUNCTION(com_get_active_object) IDispatch_Release(obj); } if (unk) { - IUnknown_Release(obj); + IUnknown_Release(unk); } efree(module); } From df7fd972127bb4d0070d4297c0ccd45419be966a Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 20 Jun 2026 21:52:25 -0400 Subject: [PATCH 03/21] Throw on below-minimum opslimit/memlimit in sodium pwhash The four sodium pwhash functions queued a zend_argument_error for an opslimit or memlimit below the documented minimum but fell through to the KDF instead of returning. When libsodium rejects the value the precise argument error is clobbered by a generic "internal error"; when it accepts the value the full KDF runs before the queued error surfaces, defeating the minimum-cost gate. Add the missing RETURN_THROWS() so each lower-bound check returns like its sibling upper-bound branches. Closes GH-22383 --- ext/sodium/libsodium.c | 7 +++++ .../tests/pwhash_memlimit_below_min.phpt | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 ext/sodium/tests/pwhash_memlimit_below_min.phpt diff --git a/ext/sodium/libsodium.c b/ext/sodium/libsodium.c index 7cabc2b325f6..57a34d5ec841 100644 --- a/ext/sodium/libsodium.c +++ b/ext/sodium/libsodium.c @@ -1473,6 +1473,7 @@ PHP_FUNCTION(sodium_crypto_pwhash) } if (memlimit < crypto_pwhash_MEMLIMIT_MIN) { zend_argument_error(sodium_exception_ce, 5, "must be greater than or equal to %d", crypto_pwhash_MEMLIMIT_MIN); + RETURN_THROWS(); } hash = zend_string_alloc((size_t) hash_len, 0); ret = -1; @@ -1532,9 +1533,11 @@ PHP_FUNCTION(sodium_crypto_pwhash_str) } if (opslimit < crypto_pwhash_OPSLIMIT_MIN) { zend_argument_error(sodium_exception_ce, 2, "must be greater than or equal to %d", crypto_pwhash_OPSLIMIT_MIN); + RETURN_THROWS(); } if (memlimit < crypto_pwhash_MEMLIMIT_MIN) { zend_argument_error(sodium_exception_ce, 3, "must be greater than or equal to %d", crypto_pwhash_MEMLIMIT_MIN); + RETURN_THROWS(); } hash_str = zend_string_alloc(crypto_pwhash_STRBYTES - 1, 0); if (crypto_pwhash_str @@ -1640,9 +1643,11 @@ PHP_FUNCTION(sodium_crypto_pwhash_scryptsalsa208sha256) } if (opslimit < crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_INTERACTIVE) { zend_argument_error(sodium_exception_ce, 4, "must be greater than or equal to %d", crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_INTERACTIVE); + RETURN_THROWS(); } if (memlimit < crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_INTERACTIVE) { zend_argument_error(sodium_exception_ce, 5, "must be greater than or equal to %d", crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_INTERACTIVE); + RETURN_THROWS(); } hash = zend_string_alloc((size_t) hash_len, 0); if (crypto_pwhash_scryptsalsa208sha256 @@ -1685,9 +1690,11 @@ PHP_FUNCTION(sodium_crypto_pwhash_scryptsalsa208sha256_str) } if (opslimit < crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_INTERACTIVE) { zend_argument_error(sodium_exception_ce, 2, "must be greater than or equal to %d", crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_INTERACTIVE); + RETURN_THROWS(); } if (memlimit < crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_INTERACTIVE) { zend_argument_error(sodium_exception_ce, 3, "must be greater than or equal to %d", crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_INTERACTIVE); + RETURN_THROWS(); } hash_str = zend_string_alloc (crypto_pwhash_scryptsalsa208sha256_STRBYTES - 1, 0); diff --git a/ext/sodium/tests/pwhash_memlimit_below_min.phpt b/ext/sodium/tests/pwhash_memlimit_below_min.phpt new file mode 100644 index 000000000000..63bf4443939b --- /dev/null +++ b/ext/sodium/tests/pwhash_memlimit_below_min.phpt @@ -0,0 +1,27 @@ +--TEST-- +sodium_crypto_pwhash(): a below-minimum memlimit reports a precise argument error +--EXTENSIONS-- +sodium +--SKIPIF-- + +--FILE-- +getMessage(), "\n"; +} + +try { + sodium_crypto_pwhash_str("password", SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, 1); +} catch (SodiumException $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +sodium_crypto_pwhash(): Argument #5 ($memlimit) must be greater than or equal to %d +sodium_crypto_pwhash_str(): Argument #3 ($memlimit) must be greater than or equal to %d From bd8a9bd3b15e5959fe5b23343475f0efa0bda2e7 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 20 Jun 2026 05:01:24 +0100 Subject: [PATCH 04/21] Fix GH-22360: convert.base64-encode corruption on incremental flush. Fix #22360 close GH-22368 --- NEWS | 4 ++++ ext/standard/filters.c | 2 +- ext/standard/tests/filters/gh22360.phpt | 24 ++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 ext/standard/tests/filters/gh22360.phpt diff --git a/NEWS b/NEWS index 9a1b6d35d1f7..1b6809def6c4 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,10 @@ PHP NEWS . Fixed bug GH-22324 (Ignore leading namespace separator in ReflectionParameter::__construct()). (jorgsowa) +- Standard: + . Fixed bug GH-22360 (convert.base64-encode corruption on + incremental flush). (David Carlier) + 02 Jul 2026, PHP 8.4.23 - Core: diff --git a/ext/standard/filters.c b/ext/standard/filters.c index d0fdd0f1f68b..a7c0a035a239 100644 --- a/ext/standard/filters.c +++ b/ext/standard/filters.c @@ -1519,7 +1519,7 @@ static php_stream_filter_status_t strfilter_convert_filter( php_stream_bucket_delref(bucket); } - if (flags != PSFS_FLAG_NORMAL) { + if (flags & PSFS_FLAG_FLUSH_CLOSE) { if (strfilter_convert_append_bucket(inst, stream, thisfilter, buckets_out, NULL, 0, &consumed, php_stream_is_persistent(stream)) != SUCCESS) { diff --git a/ext/standard/tests/filters/gh22360.phpt b/ext/standard/tests/filters/gh22360.phpt new file mode 100644 index 000000000000..b23483b22b92 --- /dev/null +++ b/ext/standard/tests/filters/gh22360.phpt @@ -0,0 +1,24 @@ +--TEST-- +GH-22360 (convert.base64-encode emits padding on incremental flush) +--FILE-- + +--CLEAN-- + +--EXPECT-- +string(4) "YWJj" +YWJj From cad6ed2a388d0d85b913d1b44e298b536b61e1c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Sun, 21 Jun 2026 21:50:52 +0200 Subject: [PATCH 05/21] zend_ast: Surround function by parens when exporting calls to function stored in property (#22376) * zend_ast: Surround function by parens when exporting calls to function stored in property The extra parentheses are needed to disambiguate method calls from calls to a function stored in a property. Fixes php/php-src#22373. * zend_ast: Avoid needless indirection through `zend_ast_export_ns_name()` --- NEWS | 2 ++ Zend/zend_ast.c | 18 ++++++++----- ext/standard/tests/assert/gh22373.phpt | 36 ++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 ext/standard/tests/assert/gh22373.phpt diff --git a/NEWS b/NEWS index c7645ca27b8a..858ca396189a 100644 --- a/NEWS +++ b/NEWS @@ -28,6 +28,8 @@ PHP NEWS invalid variable names). (timwolla) . Fixed bug GH-22291 (AST pretty printing does not correctly handle braces in string interpolation). (timwolla) + . Fixed bug GH-22373 (AST pretty-printing drops meaningful parentheses + surrounding property access). (timwolla) - BCMath: . Added NUL-byte validation to BCMath functions. (jorgsowa) diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index f495c4c8e3bb..57faedc06f9b 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2535,12 +2535,18 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio break; case ZEND_AST_CALL: { zend_ast *left = ast->child[0]; - if (left->kind == ZEND_AST_ARROW_FUNC || left->kind == ZEND_AST_CLOSURE) { - smart_str_appendc(str, '('); - zend_ast_export_ns_name(str, left, 0, indent); - smart_str_appendc(str, ')'); - } else { - zend_ast_export_ns_name(str, left, 0, indent); + switch (left->kind) { + /* ZEND_AST_ZVAL is a regular function call. */ + case ZEND_AST_ZVAL: + /* ZEND_AST_VAR ($foo()) is unambiguous without parens. */ + case ZEND_AST_VAR: + zend_ast_export_ns_name(str, left, 0, indent); + break; + default: + smart_str_appendc(str, '('); + zend_ast_export_ex(str, left, 0, indent); + smart_str_appendc(str, ')'); + break; } smart_str_appendc(str, '('); zend_ast_export_ex(str, ast->child[1], 0, indent); diff --git a/ext/standard/tests/assert/gh22373.phpt b/ext/standard/tests/assert/gh22373.phpt new file mode 100644 index 000000000000..8c26f77f490b --- /dev/null +++ b/ext/standard/tests/assert/gh22373.phpt @@ -0,0 +1,36 @@ +--TEST-- +GH-22373: AST pretty-printing drops meaningful parentheses surrounding property access +--FILE-- +f)('abc') !== 'cba'); + } catch (Error $e) { + echo $e->getMessage(), PHP_EOL; + } + try { + assert(($this?->f)('abc') !== 'cba'); + } catch (Error $e) { + echo $e->getMessage(), PHP_EOL; + } + try { + assert((self::$sf)('abc') !== 'cba'); + } catch (Error $e) { + echo $e->getMessage(), PHP_EOL; + } + } +} + +new Foo(); + +?> +--EXPECT-- +assert(($this->f)('abc') !== 'cba') +assert(($this?->f)('abc') !== 'cba') +assert((self::$sf)('abc') !== 'cba') From 34a9a4390435c6bb27094419cd68bbcda605a7cd Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 20 Jun 2026 20:58:24 -0400 Subject: [PATCH 06/21] ext/ftp: fix off-by-one terminator write in ftp_readline() The bug80901 fix (09696eee9d43) terminates an over-long response with *data = 0, but when the line fills the whole FTP_BUFSIZE inbuf without a CR/LF, data points at inbuf[FTP_BUFSIZE] and the terminator is written one byte past the buffer, into the adjacent ftpbuf_t::extra field. Reserve the final byte for the terminator so it always lands inside inbuf. A buffer-filling response loses its last character (bug80901's SYST reply is now 4095 visible chars, with the terminator taking the 4096th slot). Closes GH-22377 --- ext/ftp/ftp.c | 2 +- ext/ftp/tests/bug80901.phpt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/ftp/ftp.c b/ext/ftp/ftp.c index ca5e05ead81b..17a3e10e0d61 100644 --- a/ext/ftp/ftp.c +++ b/ext/ftp/ftp.c @@ -1359,7 +1359,7 @@ ftp_readline(ftpbuf_t *ftp) } data = eol; - if ((rcvd = my_recv(ftp, ftp->fd, data, size)) < 1) { + if (size < 2 || (rcvd = my_recv(ftp, ftp->fd, data, size - 1)) < 1) { *data = 0; return 0; } diff --git a/ext/ftp/tests/bug80901.phpt b/ext/ftp/tests/bug80901.phpt index e2a58fa0668a..a1c0e479c6ae 100644 --- a/ext/ftp/tests/bug80901.phpt +++ b/ext/ftp/tests/bug80901.phpt @@ -16,4 +16,4 @@ ftp_systype($ftp); --EXPECTF-- bool(true) -Warning: ftp_systype(): **************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************** in %s on line %d +Warning: ftp_systype(): *************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************** in %s on line %d From cb64c16917426a305d1d76a0dc36886b1201d536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Sun, 21 Jun 2026 23:51:32 +0200 Subject: [PATCH 07/21] zend_ast: Remove duplicated code when exporting arrays (#22392) Following php/php-src#22350. --- Zend/zend_ast.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 57faedc06f9b..cb27d9b7459c 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -1940,11 +1940,10 @@ static ZEND_COLD void zend_ast_export_zval(smart_str *str, const zval *zv, int p } if (key) { zend_ast_export_quoted_str(str, key); - smart_str_appends(str, " => "); } else { smart_str_append_long(str, idx); - smart_str_appends(str, " => "); } + smart_str_appends(str, " => "); zend_ast_export_zval(str, val, 0, indent); } ZEND_HASH_FOREACH_END(); smart_str_appendc(str, ']'); From a7a3a5f1d2530191554ea249f5bdf1277501eb43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 22 Jun 2026 00:30:58 +0200 Subject: [PATCH 08/21] zend_ast: Clean up `zend_ast_export_quoted_str()` (#22393) * zend_ast: Make `s` a `const zend_string*` in `zend_ast_export_quoted_str()` Following php/php-src#22350. * zend_ast: Reduce variable scope in `zend_ast_export_*str()` --- Zend/zend_ast.c | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index cb27d9b7459c..d3ce419c737e 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -1578,9 +1578,7 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio static ZEND_COLD void zend_ast_export_str(smart_str *str, const zend_string *s) { - size_t i; - - for (i = 0; i < ZSTR_LEN(s); i++) { + for (size_t i = 0; i < ZSTR_LEN(s); i++) { unsigned char c = ZSTR_VAL(s)[i]; if (c == '\'' || c == '\\') { smart_str_appendc(str, '\\'); @@ -1593,9 +1591,7 @@ static ZEND_COLD void zend_ast_export_str(smart_str *str, const zend_string *s) static ZEND_COLD void zend_ast_export_qstr(smart_str *str, char quote, const zend_string *s) { - size_t i; - - for (i = 0; i < ZSTR_LEN(s); i++) { + for (size_t i = 0; i < ZSTR_LEN(s); i++) { unsigned char c = ZSTR_VAL(s)[i]; if (c < ' ') { switch (c) { @@ -1636,12 +1632,11 @@ static ZEND_COLD void zend_ast_export_qstr(smart_str *str, char quote, const zen } } -static ZEND_COLD void zend_ast_export_quoted_str(smart_str *str, zend_string *s) +static ZEND_COLD void zend_ast_export_quoted_str(smart_str *str, const zend_string *s) { - size_t i; - - for (i = 0; i < ZSTR_LEN(s); i++) { - if ((unsigned char) ZSTR_VAL(s)[i] < ' ') { + for (size_t i = 0; i < ZSTR_LEN(s); i++) { + unsigned char c = ZSTR_VAL(s)[i]; + if (c < ' ') { smart_str_appendc(str, '"'); zend_ast_export_qstr(str, '"', s); smart_str_appendc(str, '"'); From 9a6aadf0d52bba44ce3977de44b5dbb68a5471e9 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Mon, 22 Jun 2026 13:07:02 +0800 Subject: [PATCH 09/21] ext/phar: Optimize temporary string handling in Phar directory streams (#22374) Reduced temporary allocations when iterating Phar directories. --- UPGRADING | 3 +++ ext/phar/dirstream.c | 16 ++++------------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/UPGRADING b/UPGRADING index c77bfbfa4e02..9f7917301b59 100644 --- a/UPGRADING +++ b/UPGRADING @@ -481,6 +481,9 @@ PHP 8.6 UPGRADE NOTES . Improved performance of indentation generation in json_encode() when using PHP_JSON_PRETTY_PRINT. +- Phar: + . Reduced temporary allocations when iterating Phar directories. + - Standard: . Improved performance of array_fill_keys(). . Improved performance of array_map() with multiple arrays passed. diff --git a/ext/phar/dirstream.c b/ext/phar/dirstream.c index b1c8c4747cc6..f700cc3d84b3 100644 --- a/ext/phar/dirstream.c +++ b/ext/phar/dirstream.c @@ -149,7 +149,7 @@ static int phar_compare_dir_name(Bucket *f, Bucket *s) /* {{{ */ static php_stream *phar_make_dirstream(const char *dir, size_t dirlen, const HashTable *manifest) /* {{{ */ { HashTable *data; - char *entry; + const char *entry; ALLOC_HASHTABLE(data); zend_hash_init(data, 64, NULL, NULL, 0); @@ -181,9 +181,7 @@ static php_stream *phar_make_dirstream(const char *dir, size_t dirlen, const Has /* the entry has a path separator and is a subdirectory */ keylen = has_slash - ZSTR_VAL(str_key); } - entry = safe_emalloc(keylen, 1, 1); - memcpy(entry, ZSTR_VAL(str_key), keylen); - entry[keylen] = '\0'; + entry = ZSTR_VAL(str_key); } else { if (0 != memcmp(ZSTR_VAL(str_key), dir, dirlen)) { /* entry in directory not found */ @@ -201,16 +199,12 @@ static php_stream *phar_make_dirstream(const char *dir, size_t dirlen, const Has if (has_slash) { /* is subdirectory */ save -= dirlen + 1; - entry = safe_emalloc(has_slash - save + dirlen, 1, 1); - memcpy(entry, save + dirlen + 1, has_slash - save - dirlen - 1); keylen = has_slash - save - dirlen - 1; - entry[keylen] = '\0'; + entry = save + dirlen + 1; } else { /* is file */ save -= dirlen + 1; - entry = safe_emalloc(keylen - dirlen, 1, 1); - memcpy(entry, save + dirlen + 1, keylen - dirlen - 1); - entry[keylen - dirlen - 1] = '\0'; + entry = save + dirlen + 1; keylen = keylen - dirlen - 1; } } @@ -227,8 +221,6 @@ static php_stream *phar_make_dirstream(const char *dir, size_t dirlen, const Has ZVAL_NULL(&dummy); zend_hash_str_update(data, entry, keylen, &dummy); } - - efree(entry); } ZEND_HASH_FOREACH_END(); if (FAILURE != zend_hash_has_more_elements(data)) { From 02f71b68132cde1f97343e8fa5210699659e89d2 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Mon, 22 Jun 2026 13:19:09 +0800 Subject: [PATCH 10/21] ext/intl: Fix double construction leaks (#22386) Calling Collator::__construct() or Spoofchecker::__construct() on an already constructed object replaces the stored ICU handle, which leaves the previous handle unreachable and prevents it from being released during object destruction. Reject repeated construction with an Error for both classes so the existing ICU handle remains owned by the object. Add PHPT coverage for the double construction path. Closes #22386 --- NEWS | 2 ++ ext/intl/collator/collator_create.c | 4 ++++ ext/intl/spoofchecker/spoofchecker_create.c | 8 ++++++-- ext/intl/tests/collator_double_ctor.phpt | 16 ++++++++++++++++ ext/intl/tests/spoofchecker_double_ctor.phpt | 18 ++++++++++++++++++ 5 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 ext/intl/tests/collator_double_ctor.phpt create mode 100644 ext/intl/tests/spoofchecker_double_ctor.phpt diff --git a/NEWS b/NEWS index 1b6809def6c4..f880b5ca33c8 100644 --- a/NEWS +++ b/NEWS @@ -54,6 +54,8 @@ PHP NEWS and later. (Graham Campbell) . Fixed Locale::lookup() and locale_lookup() to return NULL instead of the fallback locale when a language tag cannot be canonicalized. (Weilin Du) + . Fixed memory leaks when calling Collator::__construct() or + Spoofchecker::__construct() twice. (Weilin Du) - mysqli: . Fix stmt->query leak in mysqli_execute_query() validation errors. diff --git a/ext/intl/collator/collator_create.c b/ext/intl/collator/collator_create.c index 88dacc1c1db4..ca57d5431e01 100644 --- a/ext/intl/collator/collator_create.c +++ b/ext/intl/collator/collator_create.c @@ -42,6 +42,10 @@ static int collator_ctor(INTERNAL_FUNCTION_PARAMETERS, zend_error_handling *erro INTL_CHECK_LOCALE_LEN_OR_FAILURE(locale_len); COLLATOR_METHOD_FETCH_OBJECT; + if (co->ucoll) { + zend_throw_error(NULL, "Collator object is already constructed"); + return FAILURE; + } if(locale_len == 0) { locale = (char *)intl_locale_get_default(); diff --git a/ext/intl/spoofchecker/spoofchecker_create.c b/ext/intl/spoofchecker/spoofchecker_create.c index c1cecac8412a..4614d44c3174 100644 --- a/ext/intl/spoofchecker/spoofchecker_create.c +++ b/ext/intl/spoofchecker/spoofchecker_create.c @@ -31,9 +31,13 @@ PHP_METHOD(Spoofchecker, __construct) ZEND_PARSE_PARAMETERS_NONE(); - zend_replace_error_handling(EH_THROW, IntlException_ce_ptr, &error_handling); - SPOOFCHECKER_METHOD_FETCH_OBJECT_NO_CHECK; + if (co->uspoof) { + zend_throw_error(NULL, "Spoofchecker object is already constructed"); + RETURN_THROWS(); + } + + zend_replace_error_handling(EH_THROW, IntlException_ce_ptr, &error_handling); co->uspoof = uspoof_open(SPOOFCHECKER_ERROR_CODE_P(co)); INTL_METHOD_CHECK_STATUS(co, "spoofchecker: unable to open ICU Spoof Checker"); diff --git a/ext/intl/tests/collator_double_ctor.phpt b/ext/intl/tests/collator_double_ctor.phpt new file mode 100644 index 000000000000..93b72f7392b3 --- /dev/null +++ b/ext/intl/tests/collator_double_ctor.phpt @@ -0,0 +1,16 @@ +--TEST-- +Collator double construction should not be allowed +--EXTENSIONS-- +intl +--FILE-- +__construct('en_US'); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Collator object is already constructed diff --git a/ext/intl/tests/spoofchecker_double_ctor.phpt b/ext/intl/tests/spoofchecker_double_ctor.phpt new file mode 100644 index 000000000000..01dae5ab4bc5 --- /dev/null +++ b/ext/intl/tests/spoofchecker_double_ctor.phpt @@ -0,0 +1,18 @@ +--TEST-- +Spoofchecker double construction should not be allowed +--EXTENSIONS-- +intl +--SKIPIF-- + +--FILE-- +__construct(); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Spoofchecker object is already constructed From b8278fdf56c55564d8364f35276034dee06554cc Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Mon, 22 Jun 2026 13:36:56 +0800 Subject: [PATCH 11/21] ext/intl: Fix Spoofchecker build after double construction change (#22386) PHP-8.5 no longer declares a zend_error_handling variable in Spoofchecker::__construct(). The previous merge from PHP-8.4 carried forward a zend_replace_error_handling() call that references the removed local variable. Remove the stale call so PHP-8.5 and branches merged from it build again. --- ext/intl/spoofchecker/spoofchecker_create.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/ext/intl/spoofchecker/spoofchecker_create.c b/ext/intl/spoofchecker/spoofchecker_create.c index 4305aec6f5fa..196886ad4eca 100644 --- a/ext/intl/spoofchecker/spoofchecker_create.c +++ b/ext/intl/spoofchecker/spoofchecker_create.c @@ -36,8 +36,6 @@ PHP_METHOD(Spoofchecker, __construct) RETURN_THROWS(); } - zend_replace_error_handling(EH_THROW, IntlException_ce_ptr, &error_handling); - co->uspoof = uspoof_open(SPOOFCHECKER_ERROR_CODE_P(co)); if (U_FAILURE(INTL_DATA_ERROR_CODE(co))) { zend_throw_exception(IntlException_ce_ptr, From e121cb26ec23b0b74e03a82aec177fb039fb630a Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Mon, 22 Jun 2026 14:13:44 +0800 Subject: [PATCH 12/21] [skip ci] Fix NEWS entry for several Intl fixes into the latest version section Move the Locale::lookup() and double-construction leak NEWS entries from the previous release section to the current unreleased section. These fixes landed after the previous release and should be listed under the next one. --- NEWS | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index f880b5ca33c8..8ee746eadee2 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,12 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.4.24 +- Intl: + . Fixed Locale::lookup() and locale_lookup() to return NULL instead of the + fallback locale when a language tag cannot be canonicalized. (Weilin Du) + . Fixed memory leaks when calling Collator::__construct() or + Spoofchecker::__construct() twice. (Weilin Du) + - Reflection: . Fixed bug GH-22324 (Ignore leading namespace separator in ReflectionParameter::__construct()). (jorgsowa) @@ -52,10 +58,6 @@ PHP NEWS for invalid display types. (Weilin Du) . Fixed Spoofchecker restriction-level APIs to only be exposed with ICU 53 and later. (Graham Campbell) - . Fixed Locale::lookup() and locale_lookup() to return NULL instead of the - fallback locale when a language tag cannot be canonicalized. (Weilin Du) - . Fixed memory leaks when calling Collator::__construct() or - Spoofchecker::__construct() twice. (Weilin Du) - mysqli: . Fix stmt->query leak in mysqli_execute_query() validation errors. From f0236f11ba77cf372f1709493988bf37f10c09cd Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 20 Jun 2026 22:00:28 -0400 Subject: [PATCH 13/21] pdo_pgsql: preserve the pending exception when a COPY row fails to convert pgsqlCopyFromArray() feeds each row through try_convert_to_string(). A non-stringable row throws a TypeError, but both the array and iterator branches then called pdo_pgsql_error() and recorded a fabricated PGRES_FATAL_ERROR, leaving PDO::errorInfo() reporting "HY000" for what is a client-side type error. Return through the pending exception instead of overwriting the driver error state. Closes GH-22384 --- ext/pdo_pgsql/pgsql_driver.c | 6 +++ .../tests/copy_from_array_non_stringable.phpt | 49 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 ext/pdo_pgsql/tests/copy_from_array_non_stringable.phpt diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c index db640f2a1b53..54b2e25f72f6 100644 --- a/ext/pdo_pgsql/pgsql_driver.c +++ b/ext/pdo_pgsql/pgsql_driver.c @@ -720,6 +720,9 @@ void pgsqlCopyFromArray_internal(INTERNAL_FUNCTION_PARAMETERS) if (Z_TYPE_P(pg_rows) == IS_ARRAY) { ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(pg_rows), tmp) { if (!_pdo_pgsql_send_copy_data(H, tmp)) { + if (EG(exception)) { + RETURN_THROWS(); + } pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL); PDO_HANDLE_DBH_ERR(); RETURN_FALSE; @@ -742,6 +745,9 @@ void pgsqlCopyFromArray_internal(INTERNAL_FUNCTION_PARAMETERS) tmp = iter->funcs->get_current_data(iter); if (!_pdo_pgsql_send_copy_data(H, tmp)) { zend_iterator_dtor(iter); + if (EG(exception)) { + RETURN_THROWS(); + } pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL); PDO_HANDLE_DBH_ERR(); RETURN_FALSE; diff --git a/ext/pdo_pgsql/tests/copy_from_array_non_stringable.phpt b/ext/pdo_pgsql/tests/copy_from_array_non_stringable.phpt new file mode 100644 index 000000000000..65edb30d6671 --- /dev/null +++ b/ext/pdo_pgsql/tests/copy_from_array_non_stringable.phpt @@ -0,0 +1,49 @@ +--TEST-- +PDO PgSQL pgsqlCopyFromArray()/copyFromArray() with a non-stringable row throws without polluting errorInfo +--EXTENSIONS-- +pdo_pgsql +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); + +$db->exec('CREATE TABLE test_copy_non_stringable (v text)'); + +try { + $db->pgsqlCopyFromArray('test_copy_non_stringable', [new stdClass()]); +} catch (\Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +var_dump($db->errorInfo()[0]); + +$pgsql = PDOTest::test_factory(__DIR__ . '/common.phpt', Pdo\Pgsql::class, true); +$pgsql->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); + +try { + $pgsql->copyFromArray('test_copy_non_stringable', [new stdClass()]); +} catch (\Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +var_dump($pgsql->errorInfo()[0]); +?> +--CLEAN-- +query('DROP TABLE IF EXISTS test_copy_non_stringable CASCADE'); +?> +--EXPECTF-- +Deprecated: Method PDO::pgsqlCopyFromArray() is deprecated since 8.5, use Pdo\Pgsql::copyFromArray() instead in %s on line %d +Object of class stdClass could not be converted to string +string(5) "00000" +Object of class stdClass could not be converted to string +string(5) "00000" From aa216258a44955268b84754abedb775dc60dfeb9 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Tue, 23 Jun 2026 01:51:24 +0800 Subject: [PATCH 14/21] ext/Intl: Fix IntlListFormatter double construction leak (#22394) IntlListFormatter stores a UListFormatter pointer. Calling the constructor again on an already initialized object overwrote the existing pointer and leaked the previous formatter. Follow up to the double construction fixes from GH-22386 by rejecting repeated IntlListFormatter::__construct() calls. Closes #22394 --- NEWS | 2 ++ ext/intl/listformatter/listformatter_class.c | 5 +++++ .../listformatter/listformatter_double_ctor.phpt | 16 ++++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 ext/intl/tests/listformatter/listformatter_double_ctor.phpt diff --git a/NEWS b/NEWS index cb2a8846fcba..2d5316b6a2ac 100644 --- a/NEWS +++ b/NEWS @@ -18,6 +18,8 @@ PHP NEWS fallback locale when a language tag cannot be canonicalized. (Weilin Du) . Fixed memory leaks when calling Collator::__construct() or Spoofchecker::__construct() twice. (Weilin Du) + . Fixed memory leak when calling IntlListFormatter::__construct() twice. + (Weilin Du) - Reflection: . Fixed bug GH-22324 (Ignore leading namespace separator in diff --git a/ext/intl/listformatter/listformatter_class.c b/ext/intl/listformatter/listformatter_class.c index e4f8b18d7dd6..d8c9c792e036 100644 --- a/ext/intl/listformatter/listformatter_class.c +++ b/ext/intl/listformatter/listformatter_class.c @@ -67,6 +67,11 @@ PHP_METHOD(IntlListFormatter, __construct) Z_PARAM_LONG(width) ZEND_PARSE_PARAMETERS_END(); + if (LISTFORMATTER_OBJECT(obj)) { + zend_throw_error(NULL, "IntlListFormatter object is already constructed"); + RETURN_THROWS(); + } + if(locale_len == 0) { locale = (char *)intl_locale_get_default(); } diff --git a/ext/intl/tests/listformatter/listformatter_double_ctor.phpt b/ext/intl/tests/listformatter/listformatter_double_ctor.phpt new file mode 100644 index 000000000000..f8b0ca1e1633 --- /dev/null +++ b/ext/intl/tests/listformatter/listformatter_double_ctor.phpt @@ -0,0 +1,16 @@ +--TEST-- +IntlListFormatter double construction should not be allowed +--EXTENSIONS-- +intl +--FILE-- +__construct('en_US'); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +IntlListFormatter object is already constructed From e0113cd1d640144715059189ddae874a829f1669 Mon Sep 17 00:00:00 2001 From: Calvin Buckley Date: Mon, 22 Jun 2026 19:07:21 -0300 Subject: [PATCH 15/21] gen_stub: Fix handling of escape sequences in generated C strings (#22273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When handling sequences like this in a stub: ```php expr has all its PHP constants replaced by C constants $prettyPrinter = new Standard; $expr = $prettyPrinter->prettyPrintExpr($this->expr); - // PHP single-quote to C double-quote string if ($this->type->isString()) { - $expr = preg_replace("/(^'|'$)/", '"', $expr); + // The string in $expr has had the octal, hex, and unicode + // backslash sequences already applied for double-quoted strings, + // but not the other sequences. + // + // PHP has single quote strings, C doesn't (they're one char). + // Single-quoted strings need handling to replace their escapes + // with the double-quoted equivalent; namely single quote escapes. + // + // Double-quoted strings have similar escape sequences as C does, + // so we can pass them through directly. However, C does *not* + // support the \$ escape sequence (in ""), so strip that. Variable + // interpolation shouldn't be possible in a stub, so we don't need + // to worry about mangling such a case. + if (preg_match("/(^'|'$)/", $expr)) { + $expr = substr($expr, 1, -1); // strip quotes, readd later + $expr = str_replace("\\'", "'", $expr); + $expr = addcslashes($expr, "\\\""); + $expr = "\"$expr\""; + } else { + $expr = str_replace('\$', "$", $expr); + } } return $expr[0] == '"' ? $expr : preg_replace('(\bnull\b)', 'NULL', str_replace('\\', '', $expr)); } diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php index 489d7d0a260b..a4562368735e 100644 --- a/ext/zend_test/test.stub.php +++ b/ext/zend_test/test.stub.php @@ -57,6 +57,12 @@ class _ZendTestClass implements _ZendTestInterface { public static $_StaticProp; public static int $staticIntProp = 123; + /* If there's a problem with escapes in quotes in generated headers, + * the generated header won't compile. (tests/gh22169.phpt) */ + public static string $doubleQuoteEscaped = "BEGIN \n\r\t\v\e\f\\\$\"\101\x41\u{41} END"; + public static string $singleQuoteEscaped = 'BEGIN \n\r\t\v\e\f\\\$\"\101\x41\u{41} END'; + public static string $escapeInterpolated = "begin \$ \\$ end"; + public int $intProp = 123; public ?stdClass $classProp = null; public stdClass|Iterator|null $classUnionProp = null; diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h index 94f75cdb3601..bd6548e7bffa 100644 --- a/ext/zend_test/test_arginfo.h +++ b/ext/zend_test/test_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit test.stub.php instead. - * Stub hash: 4bb5b467b9d62c0e0c6a7c1e069e8755403a0af9 + * Stub hash: 8ca2fc33013d5a1c325bf5f0090cc6416a242297 * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_trigger_bailout, 0, 0, IS_NEVER, 0) @@ -795,6 +795,27 @@ static zend_class_entry *register_class__ZendTestClass(zend_class_entry *class_e zend_declare_typed_property(class_entry, property_staticIntProp_name, &property_staticIntProp_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); zend_string_release_ex(property_staticIntProp_name, true); + zval property_doubleQuoteEscaped_default_value; + zend_string *property_doubleQuoteEscaped_default_value_str = zend_string_init("BEGIN \n\r\t\v\x1b\f\\$\"AAA END", strlen("BEGIN \n\r\t\v\x1b\f\\$\"AAA END"), 1); + ZVAL_STR(&property_doubleQuoteEscaped_default_value, property_doubleQuoteEscaped_default_value_str); + zend_string *property_doubleQuoteEscaped_name = zend_string_init("doubleQuoteEscaped", sizeof("doubleQuoteEscaped") - 1, true); + zend_declare_typed_property(class_entry, property_doubleQuoteEscaped_name, &property_doubleQuoteEscaped_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release_ex(property_doubleQuoteEscaped_name, true); + + zval property_singleQuoteEscaped_default_value; + zend_string *property_singleQuoteEscaped_default_value_str = zend_string_init("BEGIN \\n\\r\\t\\v\\e\\f\\\\\\\\$\\\"\\101\\x41\\u{41} END", strlen("BEGIN \\n\\r\\t\\v\\e\\f\\\\\\\\$\\\"\\101\\x41\\u{41} END"), 1); + ZVAL_STR(&property_singleQuoteEscaped_default_value, property_singleQuoteEscaped_default_value_str); + zend_string *property_singleQuoteEscaped_name = zend_string_init("singleQuoteEscaped", sizeof("singleQuoteEscaped") - 1, true); + zend_declare_typed_property(class_entry, property_singleQuoteEscaped_name, &property_singleQuoteEscaped_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release_ex(property_singleQuoteEscaped_name, true); + + zval property_escapeInterpolated_default_value; + zend_string *property_escapeInterpolated_default_value_str = zend_string_init("begin $ \\$ end", strlen("begin $ \\$ end"), 1); + ZVAL_STR(&property_escapeInterpolated_default_value, property_escapeInterpolated_default_value_str); + zend_string *property_escapeInterpolated_name = zend_string_init("escapeInterpolated", sizeof("escapeInterpolated") - 1, true); + zend_declare_typed_property(class_entry, property_escapeInterpolated_name, &property_escapeInterpolated_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release_ex(property_escapeInterpolated_name, true); + zval property_intProp_default_value; ZVAL_LONG(&property_intProp_default_value, 123); zend_string *property_intProp_name = zend_string_init("intProp", sizeof("intProp") - 1, true); diff --git a/ext/zend_test/test_decl.h b/ext/zend_test/test_decl.h index 4a6babbe12b9..2561000f4b60 100644 --- a/ext/zend_test/test_decl.h +++ b/ext/zend_test/test_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit test.stub.php instead. - * Stub hash: 4bb5b467b9d62c0e0c6a7c1e069e8755403a0af9 */ + * Stub hash: 8ca2fc33013d5a1c325bf5f0090cc6416a242297 */ -#ifndef ZEND_TEST_DECL_4bb5b467b9d62c0e0c6a7c1e069e8755403a0af9_H -#define ZEND_TEST_DECL_4bb5b467b9d62c0e0c6a7c1e069e8755403a0af9_H +#ifndef ZEND_TEST_DECL_8ca2fc33013d5a1c325bf5f0090cc6416a242297_H +#define ZEND_TEST_DECL_8ca2fc33013d5a1c325bf5f0090cc6416a242297_H typedef enum zend_enum_ZendTestUnitEnum { ZEND_ENUM_ZendTestUnitEnum_Foo = 1, @@ -27,4 +27,4 @@ typedef enum zend_enum_ZendTestEnumWithInterface { ZEND_ENUM_ZendTestEnumWithInterface_Bar = 2, } zend_enum_ZendTestEnumWithInterface; -#endif /* ZEND_TEST_DECL_4bb5b467b9d62c0e0c6a7c1e069e8755403a0af9_H */ +#endif /* ZEND_TEST_DECL_8ca2fc33013d5a1c325bf5f0090cc6416a242297_H */ diff --git a/ext/zend_test/test_legacy_arginfo.h b/ext/zend_test/test_legacy_arginfo.h index a4c1ae3f2c96..a254a637e07d 100644 --- a/ext/zend_test/test_legacy_arginfo.h +++ b/ext/zend_test/test_legacy_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit test.stub.php instead. - * Stub hash: 4bb5b467b9d62c0e0c6a7c1e069e8755403a0af9 + * Stub hash: 8ca2fc33013d5a1c325bf5f0090cc6416a242297 * Has decl header: yes */ ZEND_BEGIN_ARG_INFO_EX(arginfo_zend_trigger_bailout, 0, 0, 0) @@ -650,6 +650,27 @@ static zend_class_entry *register_class__ZendTestClass(zend_class_entry *class_e zend_declare_property_ex(class_entry, property_staticIntProp_name, &property_staticIntProp_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL); zend_string_release_ex(property_staticIntProp_name, true); + zval property_doubleQuoteEscaped_default_value; + zend_string *property_doubleQuoteEscaped_default_value_str = zend_string_init("BEGIN \n\r\t\v\x1b\f\\$\"AAA END", strlen("BEGIN \n\r\t\v\x1b\f\\$\"AAA END"), 1); + ZVAL_STR(&property_doubleQuoteEscaped_default_value, property_doubleQuoteEscaped_default_value_str); + zend_string *property_doubleQuoteEscaped_name = zend_string_init("doubleQuoteEscaped", sizeof("doubleQuoteEscaped") - 1, true); + zend_declare_property_ex(class_entry, property_doubleQuoteEscaped_name, &property_doubleQuoteEscaped_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL); + zend_string_release_ex(property_doubleQuoteEscaped_name, true); + + zval property_singleQuoteEscaped_default_value; + zend_string *property_singleQuoteEscaped_default_value_str = zend_string_init("BEGIN \\n\\r\\t\\v\\e\\f\\\\\\\\$\\\"\\101\\x41\\u{41} END", strlen("BEGIN \\n\\r\\t\\v\\e\\f\\\\\\\\$\\\"\\101\\x41\\u{41} END"), 1); + ZVAL_STR(&property_singleQuoteEscaped_default_value, property_singleQuoteEscaped_default_value_str); + zend_string *property_singleQuoteEscaped_name = zend_string_init("singleQuoteEscaped", sizeof("singleQuoteEscaped") - 1, true); + zend_declare_property_ex(class_entry, property_singleQuoteEscaped_name, &property_singleQuoteEscaped_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL); + zend_string_release_ex(property_singleQuoteEscaped_name, true); + + zval property_escapeInterpolated_default_value; + zend_string *property_escapeInterpolated_default_value_str = zend_string_init("begin $ \\$ end", strlen("begin $ \\$ end"), 1); + ZVAL_STR(&property_escapeInterpolated_default_value, property_escapeInterpolated_default_value_str); + zend_string *property_escapeInterpolated_name = zend_string_init("escapeInterpolated", sizeof("escapeInterpolated") - 1, true); + zend_declare_property_ex(class_entry, property_escapeInterpolated_name, &property_escapeInterpolated_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL); + zend_string_release_ex(property_escapeInterpolated_name, true); + zval property_intProp_default_value; ZVAL_LONG(&property_intProp_default_value, 123); zend_string *property_intProp_name = zend_string_init("intProp", sizeof("intProp") - 1, true); diff --git a/ext/zend_test/tests/gh22169.phpt b/ext/zend_test/tests/gh22169.phpt new file mode 100644 index 000000000000..5ed2ab22fa21 --- /dev/null +++ b/ext/zend_test/tests/gh22169.phpt @@ -0,0 +1,22 @@ +--TEST-- +GH-22169: Ensure escaped strings in stubs are valid +--EXTENSIONS-- +zend_test +--FILE-- + +--EXPECT-- +string(44) "424547494e200a0d090b1b0c5c242241414120454e44" +string(43) "BEGIN \n\r\t\v\e\f\\\\$\"\101\x41\u{41} END" +string(14) "begin $ \$ end" + From b15a786fea67961edd921256c5cbef06af454b53 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Tue, 23 Jun 2026 06:10:07 -0400 Subject: [PATCH 16/21] ext/sockets/tests/gh21161.phpt: skip if no ipv6 (#22403) If AF_INET6 is not defined, we get Fatal error: Uncaught Error: Undefined constant "AF_INET6" ... before the _expected_ error. --- ext/sockets/tests/gh21161.phpt | 1 + 1 file changed, 1 insertion(+) diff --git a/ext/sockets/tests/gh21161.phpt b/ext/sockets/tests/gh21161.phpt index 8a3958a583d9..f8dbe909c8d6 100644 --- a/ext/sockets/tests/gh21161.phpt +++ b/ext/sockets/tests/gh21161.phpt @@ -7,6 +7,7 @@ sockets if (substr(PHP_OS, 0, 3) == 'WIN') { die('skip.. Not valid for Windows'); } +require 'ipv6_skipif.inc'; ?> --FILE-- Date: Sat, 20 Jun 2026 21:47:27 -0400 Subject: [PATCH 17/21] Fix session save-handler argv leak on recursive rejection ps_call_handler() returned on the recursive-call rejection branch before reaching the argv cleanup loop, leaking one ref per owned argument. The read/write/destroy/validate_sid/update_timestamp callers copy the session id and data into argv and rely on ps_call_handler() to release them, so a handler that re-enters session machinery (for example calling session_destroy() from within a write handler) leaks those strings. Fold the handler call into an else branch so the cleanup loop always runs. Closes GH-22382 --- ext/session/mod_user.c | 18 +++++------ .../recursive_handler_argv_leak.phpt | 31 +++++++++++++++++++ 2 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 ext/session/tests/user_session_module/recursive_handler_argv_leak.phpt diff --git a/ext/session/mod_user.c b/ext/session/mod_user.c index 78fb6260540a..71b18612683d 100644 --- a/ext/session/mod_user.c +++ b/ext/session/mod_user.c @@ -30,16 +30,16 @@ static void ps_call_handler(zval *func, int argc, zval *argv, zval *retval) PS(in_save_handler) = 0; ZVAL_UNDEF(retval); php_error_docref(NULL, E_WARNING, "Cannot call session save handler in a recursive manner"); - return; - } - PS(in_save_handler) = 1; - if (call_user_function(NULL, NULL, func, retval, argc, argv) == FAILURE) { - zval_ptr_dtor(retval); - ZVAL_UNDEF(retval); - } else if (Z_ISUNDEF_P(retval)) { - ZVAL_NULL(retval); + } else { + PS(in_save_handler) = 1; + if (call_user_function(NULL, NULL, func, retval, argc, argv) == FAILURE) { + zval_ptr_dtor(retval); + ZVAL_UNDEF(retval); + } else if (Z_ISUNDEF_P(retval)) { + ZVAL_NULL(retval); + } + PS(in_save_handler) = 0; } - PS(in_save_handler) = 0; for (i = 0; i < argc; i++) { zval_ptr_dtor(&argv[i]); } diff --git a/ext/session/tests/user_session_module/recursive_handler_argv_leak.phpt b/ext/session/tests/user_session_module/recursive_handler_argv_leak.phpt new file mode 100644 index 000000000000..2b954494e87c --- /dev/null +++ b/ext/session/tests/user_session_module/recursive_handler_argv_leak.phpt @@ -0,0 +1,31 @@ +--TEST-- +ps_call_handler() releases argv when a recursive save-handler call is rejected +--INI-- +session.save_path= +session.name=PHPSESSID +--EXTENSIONS-- +session +--FILE-- +tripped) { + $this->tripped = true; + session_destroy(); + } + return true; + } +} + +session_set_save_handler(new H, true); +session_start(); +$_SESSION['x'] = 1; +session_write_close(); +echo "done\n"; +?> +--EXPECTF-- +Warning: session_destroy(): Cannot call session save handler in a recursive manner in %s on line %d + +Warning: session_destroy(): Session object destruction failed in %s on line %d +done From a3bcc86564251a697b0986670fe447a9cb02bdd8 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Tue, 23 Jun 2026 23:12:12 +0800 Subject: [PATCH 18/21] Fix GH-22395: Avoid truncating base_convert() output at 64 characters (#22406) By using `ZEND_DOUBLE_MAX_LENGTH` instead of `(sizeof(double) << 3) + 1` we can fix the bug that base_convert() truncates output at 64 characters. Fixes #22395 --- NEWS | 2 ++ ext/standard/math.c | 3 ++- ext/standard/tests/math/gh22395.phpt | 11 +++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 ext/standard/tests/math/gh22395.phpt diff --git a/NEWS b/NEWS index 8ee746eadee2..df17e31c61f6 100644 --- a/NEWS +++ b/NEWS @@ -15,6 +15,8 @@ PHP NEWS - Standard: . Fixed bug GH-22360 (convert.base64-encode corruption on incremental flush). (David Carlier) + . Fixed bug GH-22395 (base_convert() outputs at most 64 characters). + (Weilin Du) 02 Jul 2026, PHP 8.4.23 diff --git a/ext/standard/math.c b/ext/standard/math.c index 2a9cf5d4a6ff..758b352f90ff 100644 --- a/ext/standard/math.c +++ b/ext/standard/math.c @@ -24,6 +24,7 @@ #include "zend_exceptions.h" #include "zend_multiply.h" #include "zend_portability.h" +#include "zend_strtod.h" #include #include @@ -949,7 +950,7 @@ PHPAPI zend_string * _php_math_zvaltobase(zval *arg, int base) if (Z_TYPE_P(arg) == IS_DOUBLE) { double fvalue = floor(Z_DVAL_P(arg)); /* floor it just in case */ char *ptr, *end; - char buf[(sizeof(double) << 3) + 1]; + char buf[ZEND_DOUBLE_MAX_LENGTH]; /* Don't try to convert +/- infinity */ if (fvalue == ZEND_INFINITY || fvalue == -ZEND_INFINITY) { diff --git a/ext/standard/tests/math/gh22395.phpt b/ext/standard/tests/math/gh22395.phpt new file mode 100644 index 000000000000..73c2c66da199 --- /dev/null +++ b/ext/standard/tests/math/gh22395.phpt @@ -0,0 +1,11 @@ +--TEST-- +GH-22395 (base_convert outputs at most 64 characters) +--FILE-- + +--EXPECT-- +int(78) +string(13) "4b61b5e0639ff" From 2a339c24d0c536a5716aa2061047537574354651 Mon Sep 17 00:00:00 2001 From: Weilin Du <108666168+LamentXU123@users.noreply.github.com> Date: Tue, 23 Jun 2026 23:37:17 +0800 Subject: [PATCH 19/21] ext/phar: Fix .phar-prefixed non-magic directory handling (#22372) Use a shared helper for checking whether a path refers to the magic .phar directory. Treat .phar itself and paths below it as magic, while allowing non-magic entries that merely use a .phar-prefixed name such as .pharx. Apply the same check across creation, copying, ArrayAccess, stream lookup, directory iteration, extraction, and mounts so these paths are handled consistently. Co-authored-by: Gina Peter Banyard Closes #22372 --- NEWS | 7 ++ ext/phar/dirstream.c | 4 +- ext/phar/phar_internal.h | 25 +++++++ ext/phar/phar_object.c | 45 ++++--------- ext/phar/tests/phar_magic_dir_prefix.phpt | 80 +++++++++++++++++++++++ ext/phar/util.c | 4 +- 6 files changed, 130 insertions(+), 35 deletions(-) create mode 100644 ext/phar/tests/phar_magic_dir_prefix.phpt diff --git a/NEWS b/NEWS index df17e31c61f6..060dc91e80a4 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,13 @@ PHP NEWS . Fixed memory leaks when calling Collator::__construct() or Spoofchecker::__construct() twice. (Weilin Du) +- Phar: + . Fixed inconsistent handling of the magic ".phar" directory. Paths such as + "/.phar" remain protected, while non-magic paths that merely start with + ".phar" are handled consistently across file and directory creation, + copying, ArrayAccess, stream lookup, directory iteration and extraction. + (Weilin Du) + - Reflection: . Fixed bug GH-22324 (Ignore leading namespace separator in ReflectionParameter::__construct()). (jorgsowa) diff --git a/ext/phar/dirstream.c b/ext/phar/dirstream.c index 4fe61db412a4..e28f4f977278 100644 --- a/ext/phar/dirstream.c +++ b/ext/phar/dirstream.c @@ -178,7 +178,7 @@ static php_stream *phar_make_dirstream(char *dir, HashTable *manifest) /* {{{ */ ALLOC_HASHTABLE(data); zend_hash_init(data, 64, NULL, NULL, 0); - if ((*dir == '/' && dirlen == 1 && (manifest->nNumOfElements == 0)) || (dirlen >= sizeof(".phar")-1 && !memcmp(dir, ".phar", sizeof(".phar")-1))) { + if ((*dir == '/' && dirlen == 1 && (manifest->nNumOfElements == 0)) || phar_path_is_magic_phar_ex(dir, dirlen)) { /* make empty root directory for empty phar */ /* make empty directory for .phar magic directory */ efree(dir); @@ -204,7 +204,7 @@ static php_stream *phar_make_dirstream(char *dir, HashTable *manifest) /* {{{ */ if (*dir == '/') { /* root directory */ - if (keylen >= sizeof(".phar")-1 && !memcmp(ZSTR_VAL(str_key), ".phar", sizeof(".phar")-1)) { + if (phar_is_magic_phar(str_key)) { /* do not add any magic entries to this directory */ if (SUCCESS != zend_hash_move_forward(manifest)) { break; diff --git a/ext/phar/phar_internal.h b/ext/phar/phar_internal.h index e9c34ad9117e..39f489ad439f 100644 --- a/ext/phar/phar_internal.h +++ b/ext/phar/phar_internal.h @@ -381,6 +381,31 @@ static inline bool phar_validate_alias(const char *alias, size_t alias_len) /* { } /* }}} */ +static inline bool phar_path_is_magic_phar_ex(const char *path, size_t path_len) /* {{{ */ +{ + if (path_len > 0 && path[0] == '/') { + path++; + path_len--; + } + + if (path_len < sizeof(".phar") - 1 || memcmp(path, ".phar", sizeof(".phar") - 1) != 0) { + return false; + } + + if (path_len == sizeof(".phar") - 1) { + return true; + } + + return path[sizeof(".phar") - 1] == '/' || path[sizeof(".phar") - 1] == '\\'; +} +/* }}} */ + +static inline bool phar_is_magic_phar(const zend_string *path) /* {{{ */ +{ + return phar_path_is_magic_phar_ex(ZSTR_VAL(path), ZSTR_LEN(path)); +} +/* }}} */ + static inline void phar_set_inode(phar_entry_info *entry) /* {{{ */ { char tmp[MAXPATHLEN]; diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index a2d70a1a000f..a1736f221e27 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -1619,7 +1619,7 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ return ZEND_HASH_APPLY_STOP; } after_open_fp: - if (str_key_len >= sizeof(".phar")-1 && !memcmp(str_key, ".phar", sizeof(".phar")-1)) { + if (phar_path_is_magic_phar_ex(str_key, str_key_len)) { /* silently skip any files that would be added to the magic .phar directory */ if (save) { efree(save); @@ -3468,14 +3468,14 @@ PHP_METHOD(Phar, copy) RETURN_THROWS(); } - if (zend_string_starts_with_literal(old_file, ".phar")) { + if (phar_is_magic_phar(old_file)) { /* can't copy a meta file */ zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "file \"%s\" cannot be copied to file \"%s\", cannot copy Phar meta-file in %s", ZSTR_VAL(old_file), ZSTR_VAL(new_file), phar_obj->archive->fname); RETURN_THROWS(); } - if (zend_string_starts_with_literal(new_file, ".phar")) { + if (phar_is_magic_phar(new_file)) { /* can't copy a meta file */ zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "file \"%s\" cannot be copied to file \"%s\", cannot copy to Phar meta-file in %s", ZSTR_VAL(old_file), ZSTR_VAL(new_file), phar_obj->archive->fname); @@ -3562,11 +3562,8 @@ PHP_METHOD(Phar, offsetExists) } } - if (zend_string_starts_with_literal(file_name, ".phar")) { - /* none of these are real files, so they don't exist */ - RETURN_FALSE; - } - RETURN_TRUE; + /* none of these are real files, so they don't exist */ + RETURN_BOOL(!phar_is_magic_phar(file_name)); } else { /* If the info class is not based on PharFileInfo, directories are not directly instantiable */ if (UNEXPECTED(!instanceof_function(phar_obj->spl.info_class, phar_ce_entry))) { @@ -3609,7 +3606,7 @@ PHP_METHOD(Phar, offsetGet) RETURN_THROWS(); } - if (zend_string_starts_with_literal(file_name, ".phar")) { + if (phar_is_magic_phar(file_name)) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot directly get any files or directories in magic \".phar\" directory"); RETURN_THROWS(); } @@ -3640,16 +3637,9 @@ static void phar_add_file(phar_archive_data **pphar, zend_string *file_name, con ALLOCA_FLAG(filename_use_heap) #endif - if ( - zend_string_starts_with_literal(file_name, ".phar") - || zend_string_starts_with_literal(file_name, "/.phar") - ) { - size_t prefix_len = (ZSTR_VAL(file_name)[0] == '/') + sizeof(".phar")-1; - char next_char = ZSTR_VAL(file_name)[prefix_len]; - if (next_char == '/' || next_char == '\\' || next_char == '\0') { - zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create any files in magic \".phar\" directory"); - return; - } + if (phar_is_magic_phar(file_name)) { + zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create any files in magic \".phar\" directory"); + return; } /* TODO How to handle Windows path normalisation with zend_string ? */ @@ -3796,7 +3786,7 @@ PHP_METHOD(Phar, offsetSet) RETURN_THROWS(); } - if (zend_string_starts_with_literal(file_name, ".phar")) { + if (phar_is_magic_phar(file_name)) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot set any files or directories in magic \".phar\" directory"); RETURN_THROWS(); } @@ -3863,16 +3853,9 @@ PHP_METHOD(Phar, addEmptyDir) PHAR_ARCHIVE_OBJECT(); - if ( - zend_string_starts_with_literal(dir_name, ".phar") - || zend_string_starts_with_literal(dir_name, "/.phar") - ) { - size_t prefix_len = (ZSTR_VAL(dir_name)[0] == '/') + sizeof(".phar")-1; - char next_char = ZSTR_VAL(dir_name)[prefix_len]; - if (next_char == '/' || next_char == '\\' || next_char == '\0') { - zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create a directory in magic \".phar\" directory"); - RETURN_THROWS(); - } + if (phar_is_magic_phar(dir_name)) { + zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create a directory in magic \".phar\" directory"); + RETURN_THROWS(); } phar_mkdir(&phar_obj->archive, dir_name); @@ -4178,7 +4161,7 @@ static zend_result phar_extract_file(bool overwrite, phar_entry_info *entry, cha return SUCCESS; } - if (entry->filename_len >= sizeof(".phar")-1 && !memcmp(entry->filename, ".phar", sizeof(".phar")-1)) { + if (phar_path_is_magic_phar_ex(entry->filename, entry->filename_len)) { return SUCCESS; } /* strip .. from path and restrict it to be under dest directory */ diff --git a/ext/phar/tests/phar_magic_dir_prefix.phpt b/ext/phar/tests/phar_magic_dir_prefix.phpt new file mode 100644 index 000000000000..e1f1c517632e --- /dev/null +++ b/ext/phar/tests/phar_magic_dir_prefix.phpt @@ -0,0 +1,80 @@ +--TEST-- +Phar: .phar-prefixed non-magic directories are accessible +--EXTENSIONS-- +phar +--INI-- +phar.readonly=0 +phar.require_hash=0 +--FILE-- +addFromString('.pharx/from-string.txt', 'from-string'); +$phar->addFromString('/.phary/leading.txt', 'leading'); +$phar->copy('.pharx/array.txt', '.pharx/copy.txt'); + +var_dump(isset($phar['.pharx/array.txt'])); +echo $phar['.pharx/array.txt']->getContent(), "\n"; +echo file_get_contents($pname . '/.pharx/from-string.txt'), "\n"; +echo file_get_contents($pname . '/.phary/leading.txt'), "\n"; +echo file_get_contents($pname . '/.pharx/copy.txt'), "\n"; + +$root = []; +$dh = opendir($pname . '/'); +while (false !== ($entry = readdir($dh))) { + $root[] = $entry; +} +closedir($dh); +sort($root); +var_dump($root); + +$subdir = []; +$dh = opendir($pname . '/.pharx'); +while (false !== ($entry = readdir($dh))) { + $subdir[] = $entry; +} +closedir($dh); +sort($subdir); +var_dump($subdir); + +try { + $phar->addFromString('.phar/still-magic.txt', 'no'); +} catch (Throwable $e) { + echo $e->getMessage(), "\n"; +} + +try { + $phar->addEmptyDir('/.phar'); +} catch (Throwable $e) { + echo $e->getMessage(), "\n"; +} +?> +--CLEAN-- + +--EXPECT-- +bool(true) +array +from-string +leading +array +array(2) { + [0]=> + string(6) ".pharx" + [1]=> + string(6) ".phary" +} +array(3) { + [0]=> + string(9) "array.txt" + [1]=> + string(8) "copy.txt" + [2]=> + string(15) "from-string.txt" +} +Cannot create any files in magic ".phar" directory +Cannot create a directory in magic ".phar" directory diff --git a/ext/phar/util.c b/ext/phar/util.c index e2d1076921f2..2c896c6f6588 100644 --- a/ext/phar/util.c +++ b/ext/phar/util.c @@ -208,7 +208,7 @@ zend_result phar_mount_entry(phar_archive_data *phar, char *filename, size_t fil return FAILURE; } - if (path_len >= sizeof(".phar")-1 && !memcmp(path, ".phar", sizeof(".phar")-1)) { + if (phar_path_is_magic_phar_ex(path, path_len)) { /* no creating magic phar files by mounting them */ return FAILURE; } @@ -1290,7 +1290,7 @@ phar_entry_info *phar_get_entry_info_dir(phar_archive_data *phar, char *path, si *error = NULL; } - if (security && path_len >= sizeof(".phar")-1 && !memcmp(path, ".phar", sizeof(".phar")-1)) { + if (security && phar_path_is_magic_phar_ex(path, path_len)) { if (error) { spprintf(error, 4096, "phar error: cannot directly access magic \".phar\" directory or files within it"); } From 08a6ebfe3116e149d13b572385bac345a301ef21 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 5 Mar 2026 12:15:21 +0000 Subject: [PATCH 20/21] ext/spl: ArrayObject improve ZPP test for usort() methods --- .../arrayObject_uasort_error1.phpt | 20 +++++-------------- .../arrayObject_uksort_error1.phpt | 20 +++++-------------- 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/ext/spl/tests/ArrayObject/arrayObject_uasort_error1.phpt b/ext/spl/tests/ArrayObject/arrayObject_uasort_error1.phpt index d4c853245127..32defd35e6fc 100644 --- a/ext/spl/tests/ArrayObject/arrayObject_uasort_error1.phpt +++ b/ext/spl/tests/ArrayObject/arrayObject_uasort_error1.phpt @@ -1,26 +1,16 @@ --TEST-- -Test ArrayObject::uasort() function : wrong arg count +ArrayObject::uasort() function: non callable error --FILE-- uasort(); -} catch (ArgumentCountError $e) { - echo $e->getMessage() . "\n"; + $ao->uasort('not_a_valid_function'); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; } -try { - $ao->uasort(1,2); -} catch (ArgumentCountError $e) { - echo $e->getMessage() . "\n"; -} ?> --EXPECT-- -ArrayObject::uasort() expects exactly 1 argument, 0 given -ArrayObject::uasort() expects exactly 1 argument, 2 given +TypeError: uasort(): Argument #2 ($callback) must be a valid callback, function "not_a_valid_function" not found or invalid function name diff --git a/ext/spl/tests/ArrayObject/arrayObject_uksort_error1.phpt b/ext/spl/tests/ArrayObject/arrayObject_uksort_error1.phpt index 71164383e41b..11b40aae8c8f 100644 --- a/ext/spl/tests/ArrayObject/arrayObject_uksort_error1.phpt +++ b/ext/spl/tests/ArrayObject/arrayObject_uksort_error1.phpt @@ -1,26 +1,16 @@ --TEST-- -Test ArrayObject::uksort() function : wrong arg count +ArrayObject::uksort() function: non callable error --FILE-- uksort(); -} catch (ArgumentCountError $e) { - echo $e->getMessage() . "\n"; + $ao->uksort('not_a_valid_function'); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; } -try { - $ao->uksort(1,2); -} catch (ArgumentCountError $e) { - echo $e->getMessage() . "\n"; -} ?> --EXPECT-- -ArrayObject::uksort() expects exactly 1 argument, 0 given -ArrayObject::uksort() expects exactly 1 argument, 2 given +TypeError: uksort(): Argument #2 ($callback) must be a valid callback, function "not_a_valid_function" not found or invalid function name From 6f0aa11d23a350e01f4d14112333281eb2b0d77a Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 5 Mar 2026 12:42:20 +0000 Subject: [PATCH 21/21] ext/spl/ArrayObject: handle ZPP for each sort methods directly --- ext/spl/spl_array.c | 125 ++++++++++++++++++++++++-------------------- 1 file changed, 68 insertions(+), 57 deletions(-) diff --git a/ext/spl/spl_array.c b/ext/spl/spl_array.c index 39d27bc40105..1976192e7b06 100644 --- a/ext/spl/spl_array.c +++ b/ext/spl/spl_array.c @@ -1153,18 +1153,12 @@ PHP_METHOD(ArrayObject, count) RETURN_LONG(spl_array_object_count_elements_helper(intern)); } /* }}} */ -enum spl_array_object_sort_methods { - SPL_NAT_SORT, - SPL_CALLBACK_SORT, - SPL_OPTIONAL_FLAG_SORT -}; - -static void spl_array_method(INTERNAL_FUNCTION_PARAMETERS, const char *fname, size_t fname_len, enum spl_array_object_sort_methods use_arg) /* {{{ */ +static void spl_array_method(zval *return_value, spl_array_object *intern, const char *fname, size_t fname_len, const zval *extra_arg) /* {{{ */ { - spl_array_object *intern = Z_SPLARRAY_P(ZEND_THIS); HashTable **ht_ptr = spl_array_get_hash_table_ptr(intern); HashTable *aht = *ht_ptr; - zval params[2], *arg = NULL; + zval params[2]; + uint32_t param_num = 1; zend_function *fn = zend_hash_str_find_ptr(EG(function_table), fname, fname_len); if (UNEXPECTED(fn == NULL)) { @@ -1176,68 +1170,85 @@ static void spl_array_method(INTERNAL_FUNCTION_PARAMETERS, const char *fname, si ZVAL_ARR(Z_REFVAL(params[0]), aht); GC_ADDREF(aht); - if (use_arg == SPL_NAT_SORT) { - if (zend_parse_parameters_none() == FAILURE) { - goto exit; - } - - intern->nApplyCount++; - zend_call_known_function(fn, NULL, NULL, return_value, 1, params, NULL); - intern->nApplyCount--; - } else if (use_arg == SPL_OPTIONAL_FLAG_SORT) { - zend_long sort_flags = 0; - if (zend_parse_parameters(ZEND_NUM_ARGS(), "|l", &sort_flags) == FAILURE) { - goto exit; - } - ZVAL_LONG(¶ms[1], sort_flags); - intern->nApplyCount++; - zend_call_known_function(fn, NULL, NULL, return_value, 2, params, NULL); - intern->nApplyCount--; - } else { - ZEND_ASSERT(use_arg == SPL_CALLBACK_SORT); - if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &arg) == FAILURE) { - goto exit; - } - ZVAL_COPY_VALUE(¶ms[1], arg); - intern->nApplyCount++; - zend_call_known_function(fn, NULL, NULL, return_value, 2, params, NULL); - intern->nApplyCount--; + if (extra_arg) { + param_num = 2; + ZVAL_COPY_VALUE(¶ms[1], extra_arg); } + intern->nApplyCount++; + zend_call_known_function(fn, NULL, NULL, return_value, param_num, params, NULL); + intern->nApplyCount--; -exit: - { - zval *ht_zv = Z_REFVAL(params[0]); - zend_array_release(*ht_ptr); - SEPARATE_ARRAY(ht_zv); - *ht_ptr = Z_ARRVAL_P(ht_zv); - ZVAL_NULL(ht_zv); - zval_ptr_dtor(¶ms[0]); - } + zval *ht_zv = Z_REFVAL(params[0]); + zend_array_release(*ht_ptr); + SEPARATE_ARRAY(ht_zv); + *ht_ptr = Z_ARRVAL_P(ht_zv); + ZVAL_NULL(ht_zv); + zval_ptr_dtor(¶ms[0]); } /* }}} */ -#define SPL_ARRAY_METHOD(cname, fname, use_arg) \ -PHP_METHOD(cname, fname) \ -{ \ - spl_array_method(INTERNAL_FUNCTION_PARAM_PASSTHRU, #fname, sizeof(#fname)-1, use_arg); \ -} - /* Sort the entries by values. */ -SPL_ARRAY_METHOD(ArrayObject, asort, SPL_OPTIONAL_FLAG_SORT) +PHP_METHOD(ArrayObject, asort) +{ + zend_long sort_flags = 0; + if (zend_parse_parameters(ZEND_NUM_ARGS(), "|l", &sort_flags) == FAILURE) { + RETURN_THROWS(); + } + zval sort_flag_param; + ZVAL_LONG(&sort_flag_param, sort_flags); + + spl_array_method(return_value, Z_SPLARRAY_P(ZEND_THIS), ZEND_STRL("asort"), &sort_flag_param); +} /* Sort the entries by key. */ -SPL_ARRAY_METHOD(ArrayObject, ksort, SPL_OPTIONAL_FLAG_SORT) +PHP_METHOD(ArrayObject, ksort) +{ + zend_long sort_flags = 0; + if (zend_parse_parameters(ZEND_NUM_ARGS(), "|l", &sort_flags) == FAILURE) { + RETURN_THROWS(); + } + zval sort_flag_param; + ZVAL_LONG(&sort_flag_param, sort_flags); + + spl_array_method(return_value, Z_SPLARRAY_P(ZEND_THIS), ZEND_STRL("ksort"), &sort_flag_param); +} /* Sort the entries by values user defined function. */ -SPL_ARRAY_METHOD(ArrayObject, uasort, SPL_CALLBACK_SORT) +PHP_METHOD(ArrayObject, uasort) +{ + zval *callback = NULL; + /* TODO: Should check variable is callable */ + if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &callback) == FAILURE) { + RETURN_THROWS(); + } + spl_array_method(return_value, Z_SPLARRAY_P(ZEND_THIS), ZEND_STRL("uasort"), callback); +} /* Sort the entries by key using user defined function. */ -SPL_ARRAY_METHOD(ArrayObject, uksort, SPL_CALLBACK_SORT) +PHP_METHOD(ArrayObject, uksort) +{ + zval *callback = NULL; + /* TODO: Should check variable is callable */ + if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &callback) == FAILURE) { + RETURN_THROWS(); + } + spl_array_method(return_value, Z_SPLARRAY_P(ZEND_THIS), ZEND_STRL("uksort"), callback); +} /* Sort the entries by values using "natural order" algorithm. */ -SPL_ARRAY_METHOD(ArrayObject, natsort, SPL_NAT_SORT) +PHP_METHOD(ArrayObject, natsort) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + spl_array_method(return_value, Z_SPLARRAY_P(ZEND_THIS), ZEND_STRL("natsort"), NULL); +} -/* {{{ Sort the entries by key using case-insensitive "natural order" algorithm. */ -SPL_ARRAY_METHOD(ArrayObject, natcasesort, SPL_NAT_SORT) +/* Sort the entries by key using case-insensitive "natural order" algorithm. */ +PHP_METHOD(ArrayObject, natcasesort) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + spl_array_method(return_value, Z_SPLARRAY_P(ZEND_THIS), ZEND_STRL("natcasesort"), NULL); +} /* {{{ Serialize the object */ PHP_METHOD(ArrayObject, serialize)