[php-src] PHP-8.5: Merge branch 'PHP-8.4' into PHP-8.5
Ilija Tovilo <[email protected]> Tue, 28 Jul 2026 10:00:14 +0000
| Newsgroups | gmane.comp.php.cvs.general |
|---|---|
| Message-ID | <[email protected]> |
Author: Ilija Tovilo (iluuu1994)
Date: 2026-07-28T11:51:44+02:00
Commit: https://github.com/php/php-src/commit/e5c3b4b083354d24f7202ccdb82f00a61bb16fe8
Raw diff: https://github.com/php/php-src/commit/e5c3b4b083354d24f7202ccdb82f00a61bb16fe8.diff
Merge branch 'PHP-8.4' into PHP-8.5
* PHP-8.4:
Fix out-of-bounds write in ext-bcmath bccomp() via bc_str2num()
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/bcmath/tests/GHSA-x692-q9x7-8c3f.phpt
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/bcmath/libbcmath/src/str2num.c
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 25c5af392c80..28a4ef939ea9 100644
--- a/NEWS
+++ b/NEWS
@@ -45,6 +45,10 @@ PHP NEWS
. Lock unmodified readonly properties for modification after clone-with.
(NickSdot)
+- 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)
@@ -71,6 +75,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)
@@ -112,12 +119,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/bcmath/libbcmath/src/str2num.c b/ext/bcmath/libbcmath/src/str2num.c
index 11d71ae492ad..eaaf6a11547e 100644
--- a/ext/bcmath/libbcmath/src/str2num.c
+++ b/ext/bcmath/libbcmath/src/str2num.c
@@ -179,6 +179,7 @@ bool bc_str2num(bc_num *num, const char *str, const char *end, size_t scale, siz
if (str_scale > 0) {
const char *fractional_new_end = bc_skip_zero_reverse(fractional_end, fractional_ptr);
str_scale -= fractional_end - fractional_new_end; /* fractional_end >= fractional_new_end */
+ fractional_end = fractional_new_end;
}
}
} else {
diff --git a/ext/bcmath/tests/GHSA-x692-q9x7-8c3f.phpt b/ext/bcmath/tests/GHSA-x692-q9x7-8c3f.phpt
new file mode 100644
index 000000000000..b735958bd07a
--- /dev/null
+++ b/ext/bcmath/tests/GHSA-x692-q9x7-8c3f.phpt
@@ -0,0 +1,13 @@
+--TEST--
+GHSA-x692-q9x7-8c3f: bccomp() out-of-bounds write
+--CREDITS--
+Recep Asan (recepasan)
+--FILE--
+<?php
+
+$n = '1.' . '9' . str_repeat('0', 300) . '1';
+var_dump(bccomp($n, '0', 300));
+
+?>
+--EXPECT--
+int(1)
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 a800d795372e..0106b074fd4f 100644
--- a/ext/pgsql/pgsql.c
+++ b/ext/pgsql/pgsql.c
@@ -4890,7 +4890,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("'"));
}
/* }}} */
@@ -5161,7 +5161,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 ebc2905a3243..7e245fe7585d 100644
--- a/ext/phar/util.c
+++ b/ext/phar/util.c
@@ -62,40 +62,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)
{