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

Ilija Tovilo <[email protected]> Tue, 28 Jul 2026 10:00:08 +0000
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilija Tovilo (iluuu1994)
Date: 2026-07-28T11:41:07+02:00

Commit: https://github.com/php/php-src/commit/416985389d0a5c7ad97161c911a53f0e6df7bbbe
Raw diff: https://github.com/php/php-src/commit/416985389d0a5c7ad97161c911a53f0e6df7bbbe.diff

Merge branch 'PHP-8.2' into PHP-8.3

* PHP-8.2:
  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 03a3f4f89f7a..875982a3eb9a 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,17 @@ PHP                                                                        NEWS
 - Date:
   . Fixed leak on double DatePeriod::__construct() call. (ilutov)
 
+- GD:
+  . Upgrade libgd. (CVE-2026-9672) (Pierre Joye)
+
+- PGSQL:
+  . Fixed GHSA-7qpv-r5mr-78m4 (SQL injection via E'...' backslash breakout).
+    (CVE-2026-17543) (ilutov)
+
+- Phar:
+  . Fixed GHSA-vc5h-9ppw-p5f3 (Crash via recursive symlinks). (CVE-2026-7260)
+    (Jakub Zelenka)
+
 02 Jul 2026, PHP 8.3.32
 
 - Streams:
diff --git a/ext/gd/libgd/gd_gif_in.c b/ext/gd/libgd/gd_gif_in.c
index 1f697236107f..e055f9024455 100644
--- a/ext/gd/libgd/gd_gif_in.c
+++ b/ext/gd/libgd/gd_gif_in.c
@@ -450,7 +450,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;
 
@@ -494,6 +494,8 @@ LWZReadByte_(gdIOCtx *fd, LZW_STATIC_DATA *sd, char flag, int input_code_size, i
 
 			if (count != 0)
 				return -2;
+
+			return -2;
 		}
 
 		incode = code;
@@ -560,7 +562,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 96a3cc7e7073..115c6997a89f 100644
--- a/ext/pgsql/pgsql.c
+++ b/ext/pgsql/pgsql.c
@@ -4561,7 +4561,7 @@ static int php_pgsql_convert_match(const zend_string *str, const char *regex , s
  */
 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("'"));
 }
 /* }}} */
 
@@ -4838,7 +4838,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 f4bfaf5e3955..fb36e5801c82 100644
--- a/ext/pgsql/tests/10pg_convert_9.phpt
+++ b/ext/pgsql/tests/10pg_convert_9.phpt
@@ -21,6 +21,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']);
@@ -48,18 +50,30 @@ 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"]));
 ?>
 --EXPECT--
 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 918765db06c5..5353e04dac5e 100644
--- a/ext/pgsql/tests/10pg_convert_json_array.phpt
+++ b/ext/pgsql/tests/10pg_convert_json_array.phpt
@@ -32,8 +32,8 @@ if (!pg_insert($db, $table_name_92, $fields)) {
 --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 892494fe6f0a..699426c89b21 100644
--- a/ext/pgsql/tests/12pg_insert_9.phpt
+++ b/ext/pgsql/tests/12pg_insert_9.phpt
@@ -54,7 +54,7 @@ try {
 echo "Ok\n";
 ?>
 --EXPECTF--
-INSERT INTO "php_pgsql_test" ("num","str","bin") VALUES (1234,E'AAA',E'\\x424242');
+INSERT INTO "php_pgsql_test" ("num","str","bin") VALUES (1234,'AAA','\\x424242');
 INSERT INTO "php_pgsql_test" ("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 d34f05216ec4..64493075490a 100644
--- a/ext/pgsql/tests/14pg_update_9.phpt
+++ b/ext/pgsql/tests/14pg_update_9.phpt
@@ -26,6 +26,6 @@ echo pg_update($db, $table_name, $fields, $ids, PGSQL_DML_STRING|PGSQL_DML_ESCAP
 echo "Ok\n";
 ?>
 --EXPECT--
-UPDATE "php_pgsql_test" SET "num"=1234,"str"=E'ABC',"bin"=E'\\x58595a' WHERE "num"=1234;
+UPDATE "php_pgsql_test" SET "num"=1234,"str"='ABC',"bin"='\\x58595a' WHERE "num"=1234;
 UPDATE "php_pgsql_test" 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..88b37a8432fe
--- /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("skipif.inc"); ?>
+--FILE--
+<?php
+include '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('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 ebf878dbeed9..ba27091ecf38 100644
--- a/ext/pgsql/tests/bug64609.phpt
+++ b/ext/pgsql/tests/bug64609.phpt
@@ -28,5 +28,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 d95fa1e44125..2a1025cad8d8 100644
--- a/ext/pgsql/tests/bug68638.phpt
+++ b/ext/pgsql/tests/bug68638.phpt
@@ -34,7 +34,7 @@ pg_query($conn, "DROP TABLE $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 4b9ff1cb58ba..4df7060c1d3f 100644
--- a/ext/phar/util.c
+++ b/ext/phar/util.c
@@ -57,30 +57,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;
 
-	if (!entry->link) {
-		return entry;
-	}
-
 	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 phar_get_link_source(link_entry);
-	} else {
-		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;
+	}
+
+	/*
+	 * 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;
+		}
+
+		/* 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 NULL;
 	}
 }
-/* }}} */
 
 /* retrieve a phar_entry_info's current file pointer for reading contents */
 php_stream *phar_get_efp(phar_entry_info *entry, int follow_links) /* {{{ */