[php-src] PHP-8.4: Merge branch 'PHP-8.3' into PHP-8.4

From: Date: Tue, 28 Jul 2026 10:00:10 +0000
Subject: [php-src] PHP-8.4: Merge branch 'PHP-8.3' into PHP-8.4
Groups: php.cvs 
Request: Send a blank email to php-cvs+get-140253@lists.php.net to get a copy of this message
Author: Ilija Tovilo (iluuu1994) Date: 2026-07-28T11:50:34+02:00 Commit: https://github.com/php/php-src/commit/d3f361c3b7ff7d9fd428f9ee5112885898cb4e43 Raw diff: https://github.com/php/php-src/commit/d3f361c3b7ff7d9fd428f9ee5112885898cb4e43.diff Merge branch 'PHP-8.3' into PHP-8.4 * PHP-8.3: Add NEWS entries Fix GHSA-vc5h-9ppw-p5f3: phar circular symlink crash Fix SQL injection in ext-pgsql via E'...' backslash breakout libgd patch for CVE-2026-9672 Changed paths: A ext/pgsql/tests/GHSA-7qpv-r5mr-78m4.phpt A ext/phar/tests/tar/files/circular_symlinks.tar A ext/phar/tests/tar/files/circular_symlinks_long.tar A ext/phar/tests/tar/files/circular_symlinks_rho.tar A ext/phar/tests/tar/ghsa-vc5h-9ppw-p5f3-symlink-circular.phpt M NEWS M ext/gd/libgd/gd_gif_in.c M ext/pgsql/pgsql.c M ext/pgsql/tests/10pg_convert_9.phpt M ext/pgsql/tests/10pg_convert_json_array.phpt M ext/pgsql/tests/12pg_insert_9.phpt M ext/pgsql/tests/14pg_update_9.phpt M ext/pgsql/tests/bug64609.phpt M ext/pgsql/tests/bug68638.phpt M ext/phar/util.c Diff: diff --git a/NEWS b/NEWS index 1dc666e361b5..54dea5289ff9 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,6 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| -?? ??? ????, PHP 8.4.24 +?? ??? ????, PHP 8.4.25 - Date: . Fixed leak on double DatePeriod::__construct() call. (ilutov) @@ -29,6 +29,10 @@ PHP NEWS 30 Jul 2026, PHP 8.4.24 +- BCMath: + . Fixed GHSA-x692-q9x7-8c3f (Out-of-bounds write in bccomp()). + (CVE-2026-17544) (Recep Asan) + - Calendar: . Fixed bug GH-22602 (gregoriantojd() and juliantojd() integer overflow with INT_MAX year). (arshidkv12) @@ -53,6 +57,9 @@ PHP NEWS . Fixed bug GH-11020 (exif_read_data() emits a spurious "Illegal IFD size" warning when an IFD is not followed by a next-IFD offset). (Eyüp Can Akman) +- GD: + . Upgrade libgd. (CVE-2026-9672) (Pierre Joye) + - Hash: . Fixed bug GH-18173 (ext/hash relies on implementation-defined malloc alignment). (iliaal) @@ -83,12 +90,18 @@ PHP NEWS . Fixed bug GH-22665 (Out-of-bounds write when the ODBC driver reports a diagnostic message length beyond the error buffer). (iliaal) +- PGSQL: + . Fixed GHSA-7qpv-r5mr-78m4 (SQL injection via E'...' backslash breakout). + (CVE-2026-17543) (ilutov) + - 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) + . Fixed GHSA-vc5h-9ppw-p5f3 (Crash via recursive symlinks). (CVE-2026-7260) + (Jakub Zelenka) - PHPDBG: . Fixed bug GH-17387 (Trivial crash in phpdbg lexer). (iliaal) diff --git a/ext/gd/libgd/gd_gif_in.c b/ext/gd/libgd/gd_gif_in.c index 0204c4158db4..14c27f3293cc 100644 --- a/ext/gd/libgd/gd_gif_in.c +++ b/ext/gd/libgd/gd_gif_in.c @@ -452,7 +452,7 @@ LWZReadByte_(gdIOCtx *fd, LZW_STATIC_DATA *sd, char flag, int input_code_size, i sd->table[1][i] = i; } for (; i < (1<<MAX_LWZ_BITS); ++i) - sd->table[0][i] = sd->table[1][0] = 0; + sd->table[0][i] = sd->table[1][i] = 0; sd->sp = sd->stack; @@ -496,6 +496,8 @@ LWZReadByte_(gdIOCtx *fd, LZW_STATIC_DATA *sd, char flag, int input_code_size, i if (count != 0) return -2; + + return -2; } incode = code; @@ -562,7 +564,7 @@ ReadImage(gdImagePtr im, gdIOCtx *fd, int len, int height, unsigned char (*cmap) int v; int xpos = 0, ypos = 0, pass = 0; int i; - LZW_STATIC_DATA sd; + LZW_STATIC_DATA sd = {0}; /* diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c index 99f2e3cf563f..92da18b2b4ac 100644 --- a/ext/pgsql/pgsql.c +++ b/ext/pgsql/pgsql.c @@ -4841,7 +4841,7 @@ static int php_pgsql_convert_match(const zend_string *str, zend_string *regex) */ static zend_string *php_pgsql_add_quotes(zend_string *src) { - return zend_string_concat3("E'", strlen("E'"), ZSTR_VAL(src), ZSTR_LEN(src), "'", strlen("'")); + return zend_string_concat3("'", strlen("'"), ZSTR_VAL(src), ZSTR_LEN(src), "'", strlen("'")); } /* }}} */ @@ -5112,7 +5112,6 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string * zend_string *str; /* 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), &escape_err); if (escape_err) { diff --git a/ext/pgsql/tests/10pg_convert_9.phpt b/ext/pgsql/tests/10pg_convert_9.phpt index d733b888fe03..2ef325dbc970 100644 --- a/ext/pgsql/tests/10pg_convert_9.phpt +++ b/ext/pgsql/tests/10pg_convert_9.phpt @@ -24,6 +24,8 @@ $converted = pg_convert($db, $table_name, $fields); var_dump($converted); +var_dump(pg_convert($db, $table_name, ['str' => "\\' OR 1=1"])); + /* Invalid values */ try { $converted = pg_convert($db, $table_name, [5 => 'AAA']); @@ -51,6 +53,10 @@ try { } catch (\TypeError $e) { echo $e->getMessage(), \PHP_EOL; } + +/* standard_conforming_strings = 1 */ +pg_query($db, "SET standard_conforming_strings = 1"); +var_dump(pg_convert($db, $table_name, ['str' => "\\' OR 1=1"])); ?> --CLEAN-- <?php @@ -65,12 +71,20 @@ array(3) { [""num""]=> string(4) "1234" [""str""]=> - string(6) "E'AAA'" + string(5) "'AAA'" [""bin""]=> - string(12) "E'\\x424242'" + string(11) "'\\x424242'" +} +array(1) { + [""str""]=> + string(13) "'\\'' OR 1=1'" } Array of values must be an associative array with string keys Array of values must be an associative array with string keys Values must be of type string|int|float|bool|null, array given Values must be of type string|int|float|bool|null, stdClass given Values must be of type string|int|float|bool|null, PgSql\Connection given +array(1) { + [""str""]=> + string(12) "'\'' OR 1=1'" +} diff --git a/ext/pgsql/tests/10pg_convert_json_array.phpt b/ext/pgsql/tests/10pg_convert_json_array.phpt index d1acc686d890..b8f3514b5ed3 100644 --- a/ext/pgsql/tests/10pg_convert_json_array.phpt +++ b/ext/pgsql/tests/10pg_convert_json_array.phpt @@ -42,8 +42,8 @@ pg_query($db, "DROP TABLE IF EXISTS {$table_name_92}"); --EXPECT-- array(2) { [""textary""]=> - string(51) "E'{"meeting", "lunch", "training", "presentation"}'" + string(50) "'{"meeting", "lunch", "training", "presentation"}'" [""jsn""]=> - string(22) "E'{"f1":1,"f2":"foo"}'" + string(21) "'{"f1":1,"f2":"foo"}'" } OK diff --git a/ext/pgsql/tests/12pg_insert_9.phpt b/ext/pgsql/tests/12pg_insert_9.phpt index b986d14e15dc..7782c93ef7aa 100644 --- a/ext/pgsql/tests/12pg_insert_9.phpt +++ b/ext/pgsql/tests/12pg_insert_9.phpt @@ -65,7 +65,7 @@ $db = pg_connect($conn_str); pg_query($db, "DROP TABLE IF EXISTS {$table_name}"); ?> --EXPECTF-- -INSERT INTO "table_12pg_insert_9" ("num","str","bin") VALUES (1234,E'AAA',E'\\x424242'); +INSERT INTO "table_12pg_insert_9" ("num","str","bin") VALUES (1234,'AAA','\\x424242'); INSERT INTO "table_12pg_insert_9" ("num","str","bin") VALUES ('1234','AAA','BBB'); object(PgSql\Result)#%d (0) { } diff --git a/ext/pgsql/tests/14pg_update_9.phpt b/ext/pgsql/tests/14pg_update_9.phpt index 602019c2fb62..6bcea5121501 100644 --- a/ext/pgsql/tests/14pg_update_9.phpt +++ b/ext/pgsql/tests/14pg_update_9.phpt @@ -39,6 +39,6 @@ $db = pg_connect($conn_str); pg_query($db, "DROP TABLE IF EXISTS {$table_name}"); ?> --EXPECT-- -UPDATE "table_14pg_update_9" SET "num"=1234,"str"=E'ABC',"bin"=E'\\x58595a' WHERE "num"=1234; +UPDATE "table_14pg_update_9" SET "num"=1234,"str"='ABC',"bin"='\\x58595a' WHERE "num"=1234; UPDATE "table_14pg_update_9" SET "num"='1234',"str"='ABC',"bin"='XYZ' WHERE "num"='1234'; Ok diff --git a/ext/pgsql/tests/GHSA-7qpv-r5mr-78m4.phpt b/ext/pgsql/tests/GHSA-7qpv-r5mr-78m4.phpt new file mode 100644 index 000000000000..93f6376bb1cf --- /dev/null +++ b/ext/pgsql/tests/GHSA-7qpv-r5mr-78m4.phpt @@ -0,0 +1,58 @@ +--TEST-- +GHSA-7qpv-r5mr-78m4: SQL injection via E'...' backslash breakout +--CREDITS-- +expatch.llc +--EXTENSIONS-- +pgsql +--SKIPIF-- +<?php include("inc/skipif.inc"); ?> +--FILE-- +<?php +include 'inc/config.inc'; + +$db = pg_connect($conn_str); + +pg_query($db, "SET standard_conforming_strings = 1"); + +pg_query($db, "DROP TABLE IF EXISTS ghsa_7qpv_r5mr_78m4"); +pg_query($db, "CREATE TABLE ghsa_7qpv_r5mr_78m4 (id serial primary key, name text, admin boolean)"); +pg_query($db, "INSERT INTO ghsa_7qpv_r5mr_78m4 (name, admin) VALUES ('alice', false), ('bob', false)"); + +$params = ['name' => "zzz' OR 1=1 --"]; +echo pg_select($db, 'ghsa_7qpv_r5mr_78m4', $params, PGSQL_DML_STRING) . "\n"; +printf("returned: %d\n\n", count(pg_select($db, 'ghsa_7qpv_r5mr_78m4', $params))); + +$params = ['name' => "zzz\\' OR 1=1 --"]; +echo pg_select($db, 'ghsa_7qpv_r5mr_78m4', $params, PGSQL_DML_STRING) . "\n"; +printf("returned: %d\n\n", count(pg_select($db, 'ghsa_7qpv_r5mr_78m4', $params))); + +$params = ['name' => "john\\', true) --", 'admin' => 'false']; +echo pg_insert($db, 'ghsa_7qpv_r5mr_78m4', $params, PGSQL_DML_STRING) . "\n"; +pg_insert($db, 'ghsa_7qpv_r5mr_78m4', $params); +var_dump(pg_select($db, 'ghsa_7qpv_r5mr_78m4', ['id' => 3])[0]['admin']); +echo "\n"; + +$params = ['name' => "jake\\', true) --", 'admin' => 'f']; +echo pg_insert($db, 'ghsa_7qpv_r5mr_78m4', $params, PGSQL_DML_ESCAPE|PGSQL_DML_STRING) . "\n"; +pg_insert($db, 'ghsa_7qpv_r5mr_78m4', $params, PGSQL_DML_EXEC|PGSQL_DML_ESCAPE); +var_dump(pg_select($db, 'ghsa_7qpv_r5mr_78m4', ['id' => 4])[0]['admin']); + +?> +--EXPECT-- +SELECT * FROM "ghsa_7qpv_r5mr_78m4" WHERE "name"='zzz'' OR 1=1 --'; +returned: 0 + +SELECT * FROM "ghsa_7qpv_r5mr_78m4" WHERE "name"='zzz\'' OR 1=1 --'; +returned: 0 + +INSERT INTO "ghsa_7qpv_r5mr_78m4" ("name","admin") VALUES ('john\'', true) --','f'); +string(1) "f" + +INSERT INTO "ghsa_7qpv_r5mr_78m4" ("name","admin") VALUES ('jake\'', true) --','f'); +string(1) "f" +--CLEAN-- +<?php +include('inc/config.inc'); +$db = pg_connect($conn_str); +pg_query($db, "DROP TABLE IF EXISTS ghsa_7qpv_r5mr_78m4"); +?> diff --git a/ext/pgsql/tests/bug64609.phpt b/ext/pgsql/tests/bug64609.phpt index 67e10a3986cb..f6fbac26f616 100644 --- a/ext/pgsql/tests/bug64609.phpt +++ b/ext/pgsql/tests/bug64609.phpt @@ -30,5 +30,5 @@ var_dump($converted); --EXPECT-- array(1) { [""a""]=> - string(5) "E'ok'" + string(4) "'ok'" } diff --git a/ext/pgsql/tests/bug68638.phpt b/ext/pgsql/tests/bug68638.phpt index 957e9a33abf5..267423a22264 100644 --- a/ext/pgsql/tests/bug68638.phpt +++ b/ext/pgsql/tests/bug68638.phpt @@ -41,7 +41,7 @@ $table='test_68638'; pg_query($conn, "DROP TABLE IF EXISTS $table"); ?> --EXPECT-- -string(52) "UPDATE "test_68638" SET "value"=E'inf' WHERE "id"=1;" +string(51) "UPDATE "test_68638" SET "value"='inf' WHERE "id"=1;" array(2) { ["id"]=> string(1) "1" diff --git a/ext/phar/tests/tar/files/circular_symlinks.tar b/ext/phar/tests/tar/files/circular_symlinks.tar new file mode 100644 index 000000000000..2af7bb8c2384 Binary files /dev/null and b/ext/phar/tests/tar/files/circular_symlinks.tar differ diff --git a/ext/phar/tests/tar/files/circular_symlinks_long.tar b/ext/phar/tests/tar/files/circular_symlinks_long.tar new file mode 100644 index 000000000000..b2b4c1cb095c Binary files /dev/null and b/ext/phar/tests/tar/files/circular_symlinks_long.tar differ diff --git a/ext/phar/tests/tar/files/circular_symlinks_rho.tar b/ext/phar/tests/tar/files/circular_symlinks_rho.tar new file mode 100644 index 000000000000..2ccbf17c8c63 Binary files /dev/null and b/ext/phar/tests/tar/files/circular_symlinks_rho.tar differ diff --git a/ext/phar/tests/tar/ghsa-vc5h-9ppw-p5f3-symlink-circular.phpt b/ext/phar/tests/tar/ghsa-vc5h-9ppw-p5f3-symlink-circular.phpt new file mode 100644 index 000000000000..cf0048c0fcd9 --- /dev/null +++ b/ext/phar/tests/tar/ghsa-vc5h-9ppw-p5f3-symlink-circular.phpt @@ -0,0 +1,27 @@ +--TEST-- +GHSA-vc5h-9ppw-p5f3 (circular symlinks in tar should not cause stack overflow) +--CREDITS-- +Calvin Young - eWalker Consulting (HK) Limited +Enoch Chow - Isomorph Cyber +--EXTENSIONS-- +phar +--FILE-- +<?php +$base = dirname(__FILE__); + +// simple 2-cycle +$phar = new PharData($base . '/files/circular_symlinks.tar'); +var_dump($phar['file_a']->getContent() === ''); + +// rho-shaped cycle (tail leading into a loop) +$phar = new PharData($base . '/files/circular_symlinks_rho.tar'); +var_dump($phar['file_a']->getContent() === ''); + +// long cycle (400 entries) +$phar = new PharData($base . '/files/circular_symlinks_long.tar'); +var_dump($phar['link_0']->getContent() === ''); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) diff --git a/ext/phar/util.c b/ext/phar/util.c index 2c896c6f6588..d3bdf3d52a78 100644 --- a/ext/phar/util.c +++ b/ext/phar/util.c @@ -60,40 +60,59 @@ static char *phar_get_link_location(phar_entry_info *entry) /* {{{ */ } /* }}} */ -phar_entry_info *phar_get_link_source(phar_entry_info *entry) /* {{{ */ +static phar_entry_info *phar_follow_one_link(phar_entry_info *entry) { phar_entry_info *link_entry; char *link; - uint32_t depth = 0, max_depth; + + link = phar_get_link_location(entry); + if (NULL != (link_entry = zend_hash_str_find_ptr(&(entry->phar->manifest), entry->link, strlen(entry->link))) || + NULL != (link_entry = zend_hash_str_find_ptr(&(entry->phar->manifest), link, strlen(link)))) { + if (link != entry->link) { + efree(link); + } + return link_entry; + } + + if (link != entry->link) { + efree(link); + } + return NULL; +} + +phar_entry_info *phar_get_link_source(phar_entry_info *entry) +{ + phar_entry_info *slow, *fast; if (!entry->link) { return entry; } - max_depth = zend_hash_num_elements(&(entry->phar->manifest)); - - while (entry->link) { - if (UNEXPECTED(++depth > max_depth)) { - return NULL; + /* + * Use Floyd's cycle detection algorithm to follow the symlink chain without unbounded + * recursion. Each entry has at most one outgoing link, so if a cycle exists the fast pointer + * will eventually meet the slow one. Otherwise the fast pointer reaches the end first. + */ + slow = fast = entry; + while (1) { + fast = phar_follow_one_link(fast); + if (!fast || !fast->link) { + return fast; + } + fast = phar_follow_one_link(fast); + if (!fast || !fast->link) { + return fast; } - link = phar_get_link_location(entry); - if (NULL != (link_entry = zend_hash_str_find_ptr(&(entry->phar->manifest), entry->link, strlen(entry->link))) || - NULL != (link_entry = zend_hash_str_find_ptr(&(entry->phar->manifest), link, strlen(link)))) { - if (link != entry->link) { - efree(link); - } - entry = link_entry; - } else { - if (link != entry->link) { - efree(link); - } + /* no need to check slow as it's always behind */ + slow = phar_follow_one_link(slow); + + if (slow == fast) { + /* circular symlink chain */ return NULL; } } - return entry; } -/* }}} */ static php_stream *phar_get_entrypufp(const phar_entry_info *entry) {

« previous php.cvs (#140253) next »