From df1250dc64f53db849503618d2f911645cd43e3a Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 20 Jun 2026 20:50:06 -0400 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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')