Skip to content

Commit 8f04cd2

Browse files
authored
Merge branch 'PHP-8.4' into fix/gh-23113-array-replace-recursive-stack-limit
2 parents 5a1d824 + 69d5f1b commit 8f04cd2

27 files changed

Lines changed: 2104 additions & 354 deletions

‎.github/workflows/test-suite.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,7 @@ jobs:
852852
PHP_BUILD_CACHE_BASE_DIR: C:\build-cache
853853
PHP_BUILD_OBJ_DIR: C:\obj
854854
PHP_BUILD_CACHE_SDK_DIR: C:\build-cache\sdk
855-
PHP_BUILD_SDK_BRANCH: php-sdk-2.7.1
855+
PHP_BUILD_SDK_BRANCH: php-sdk-2.8.2
856856
PHP_BUILD_CRT: ${{ fromJson(inputs.branch).jobs.WINDOWS.config.vs_crt_version }}
857857
PLATFORM: ${{ matrix.x64 && 'x64' || 'x86' }}
858858
THREAD_SAFE: "${{ matrix.zts && '1' || '0' }}"

‎NEWS‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,28 @@ PHP NEWS
22
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
33
?? ??? ????, PHP 8.4.25
44

5+
- Core:
6+
. Fixed bug GH-23088 (Stack overflow when comparing deeply nested arrays).
7+
(Lazizbek Ergashev)
8+
59
- Date:
610
. Fixed leak on double DatePeriod::__construct() call. (ilutov)
711

812
- DOM:
13+
. Fixed bug GH-23116 (Stack overflow when normalizing a deeply nested
14+
DOMDocument). (Lazizbek Ergashev)
15+
. Fixed bug GH-23117 (Stack overflow when normalizing a deeply nested
16+
Dom\XMLDocument). (Lazizbek Ergashev)
917
. Fixed bug GH-22825 (DOMElement::setAttribute() fails silently when the DTD
1018
declares a default value for the attribute). (iliaal)
1119
. Fixed bug GH-22447 (UAF at dom_objects_free_storage when setting an
1220
attribute node that collides by local name with a namespaced
1321
attribute). (David Carlier)
22+
. Fixed bug GH-23120 (Stack overflow when comparing deeply nested DOM nodes
23+
with DOMNode::isEqualNode()). (Weilin Du)
24+
25+
- LibXML:
26+
. Fixed bug GH-22752 (Build failure with libxml 2.15). (David Carlier)
1427

1528
- MBString:
1629
. Fixed bug GH-22779 (mb_strrpos() returns the wrong position for a negative
@@ -46,13 +59,19 @@ PHP NEWS
4659
. Fixed bug GH-23043 (broken session id code can cause zend_mm_heap
4760
corrupted). (ndossche)
4861

62+
- SimpleXML:
63+
. Fixed integer element offsets that cannot resolve aliasing an existing
64+
element. (iliaal)
65+
4966
- Sockets:
5067
. Fixed various memory related issues in ext/sockets. (David Carlier)
5168

5269
- SQLite:
5370
. Fix leak when trying to close db if blob stream is still open. (ndossche)
5471

5572
- Standard:
73+
. Fixed bug GH-23111 (Stack overflow in array_walk_recursive() with deeply
74+
nested arrays). (Lazizbek Ergashev)
5675
. Fixed bug GH-23113 (Stack overflow in array_replace_recursive() with deeply
5776
nested arrays). (Lazizbek Ergashev)
5877

@@ -64,6 +83,10 @@ PHP NEWS
6483
. Fixed out-of-bounds write when shm_attach() opens an existing segment with
6584
a size larger than the segment actually is. (David Carlier)
6685

86+
- Zip:
87+
. Fixed ZipArchive::addGlob() and ZipArchive::addPattern() ignoring their
88+
default options when no options array is given. (David Carlier)
89+
6790
30 Jul 2026, PHP 8.4.24
6891

6992
- BCMath:

‎Zend/tests/gh18572.phpt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,4 @@ try {
3636
}
3737
?>
3838
--EXPECTREGEX--
39-
(Maximum call stack size reached during object comparison|Nesting level too deep - recursive dependency\?)
39+
(Maximum call stack size reached during (object )?comparison|Nesting level too deep - recursive dependency\?)

‎Zend/tests/gh23088.phpt‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
--TEST--
2+
GH-23088 (Stack overflow when comparing deeply nested arrays)
3+
--SKIPIF--
4+
<?php
5+
if (ini_get('zend.max_allowed_stack_size') === false) {
6+
die('skip No stack limit support');
7+
}
8+
if (getenv('SKIP_ASAN')) {
9+
die('skip ASAN needs different stack limit setting due to more stack space usage');
10+
}
11+
?>
12+
--INI--
13+
zend.max_allowed_stack_size=256K
14+
--FILE--
15+
<?php
16+
17+
$a = [];
18+
$b = [];
19+
20+
for ($i = 0; $i < 20000; $i++) {
21+
$a = [$a];
22+
$b = [$b];
23+
}
24+
25+
try {
26+
var_dump($a == $b);
27+
} catch (Error $e) {
28+
echo $e->getMessage(), PHP_EOL;
29+
}
30+
31+
try {
32+
var_dump($a === $b);
33+
} catch (Error $e) {
34+
echo $e->getMessage(), PHP_EOL;
35+
}
36+
37+
?>
38+
--EXPECT--
39+
Maximum call stack size reached during comparison
40+
Maximum call stack size reached during comparison

‎Zend/zend_hash.c‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3214,6 +3214,13 @@ ZEND_API int zend_hash_compare(HashTable *ht1, HashTable *ht2, compare_func_t co
32143214
return 0;
32153215
}
32163216

3217+
#ifdef ZEND_CHECK_STACK_LIMIT
3218+
if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) {
3219+
zend_throw_error(NULL, "Maximum call stack size reached during comparison");
3220+
return ZEND_UNCOMPARABLE;
3221+
}
3222+
#endif
3223+
32173224
/* It's enough to protect only one of the arrays.
32183225
* The second one may be referenced from the first and this may cause
32193226
* false recursion detection.

‎ext/dom/node.c‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1730,11 +1730,27 @@ static bool php_dom_is_equal_attr(const xmlAttr *this_attr, const xmlAttr *other
17301730
&& php_dom_node_is_content_equal((const xmlNode *) this_attr, (const xmlNode *) other_attr);
17311731
}
17321732

1733+
static zend_always_inline bool php_dom_node_is_equal_node_check_stack_limit(void)
1734+
{
1735+
#ifdef ZEND_CHECK_STACK_LIMIT
1736+
return zend_call_stack_overflowed(EG(stack_limit));
1737+
#else
1738+
return false;
1739+
#endif
1740+
}
1741+
17331742
static bool php_dom_node_is_equal_node(const xmlNode *this, const xmlNode *other, bool spec_compliant)
17341743
{
17351744
ZEND_ASSERT(this != NULL);
17361745
ZEND_ASSERT(other != NULL);
17371746

1747+
if (UNEXPECTED(php_dom_node_is_equal_node_check_stack_limit())) {
1748+
if (!EG(exception)) {
1749+
zend_throw_error(NULL, "Maximum call stack size reached.");
1750+
}
1751+
return false;
1752+
}
1753+
17381754
if (this->type != other->type) {
17391755
return false;
17401756
}
@@ -1795,6 +1811,7 @@ static void dom_node_is_equal_node_common(INTERNAL_FUNCTION_PARAMETERS, bool mod
17951811
zval *id, *node;
17961812
xmlNodePtr otherp, nodep;
17971813
dom_object *intern;
1814+
bool result;
17981815

17991816
id = ZEND_THIS;
18001817
ZEND_PARSE_PARAMETERS_START(1, 1)
@@ -1817,7 +1834,11 @@ static void dom_node_is_equal_node_common(INTERNAL_FUNCTION_PARAMETERS, bool mod
18171834
RETURN_BOOL(nodep == NULL && otherp == NULL);
18181835
}
18191836

1820-
RETURN_BOOL(php_dom_node_is_equal_node(nodep, otherp, modern));
1837+
result = php_dom_node_is_equal_node(nodep, otherp, modern);
1838+
if (UNEXPECTED(EG(exception))) {
1839+
RETURN_THROWS();
1840+
}
1841+
RETURN_BOOL(result);
18211842
}
18221843

18231844
PHP_METHOD(DOMNode, isEqualNode)

‎ext/dom/php_dom.c‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1966,9 +1966,26 @@ static void dom_merge_adjacent_exclusive_text_nodes(xmlNodePtr node)
19661966
}
19671967
}
19681968

1969+
static zend_always_inline bool dom_normalize_check_stack_limit(void)
1970+
{
1971+
#ifdef ZEND_CHECK_STACK_LIMIT
1972+
if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) {
1973+
if (!EG(exception)) {
1974+
zend_throw_error(NULL, "Maximum call stack size reached. Infinite recursion?");
1975+
}
1976+
return true;
1977+
}
1978+
#endif
1979+
return false;
1980+
}
1981+
19691982
/* {{{ void php_dom_normalize_legacy(xmlNodePtr nodep) */
19701983
void php_dom_normalize_legacy(xmlNodePtr nodep)
19711984
{
1985+
if (UNEXPECTED(dom_normalize_check_stack_limit())) {
1986+
return;
1987+
}
1988+
19721989
xmlNodePtr child = nodep->children;
19731990
while(child != NULL) {
19741991
switch (child->type) {
@@ -2001,6 +2018,10 @@ void php_dom_normalize_legacy(xmlNodePtr nodep)
20012018
/* https://dom.spec.whatwg.org/#dom-node-normalize */
20022019
void php_dom_normalize_modern(xmlNodePtr this)
20032020
{
2021+
if (UNEXPECTED(dom_normalize_check_stack_limit())) {
2022+
return;
2023+
}
2024+
20042025
/* for each descendant exclusive Text node node of this: */
20052026
xmlNodePtr node = this->children;
20062027
while (node != NULL) {

‎ext/dom/tests/DOMDocument_relaxNGValidate_error2.phpt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ var_dump($result);
2222
--EXPECTF--
2323
Warning: DOMDocument::relaxNGValidate(): I/O %s : failed to load %s
2424

25-
Warning: DOMDocument::relaxNGValidate(): xmlRelaxNGParse: could not load %s/foo.rng in %s on line %d
25+
Warning: DOMDocument::relaxNGValidate(): xmlRelaxNGParse: could not load %sfoo.rng in %s on line %d
2626

2727
Warning: DOMDocument::relaxNGValidate(): Invalid RelaxNG in %s on line %d
2828
bool(false)

‎ext/dom/tests/gh23116.phpt‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
--TEST--
2+
GH-23116 (Stack overflow when normalizing a deeply nested DOMDocument)
3+
--EXTENSIONS--
4+
dom
5+
--SKIPIF--
6+
<?php
7+
if (ini_get('zend.max_allowed_stack_size') === false) {
8+
die('skip No stack limit support');
9+
}
10+
if (getenv('SKIP_ASAN')) {
11+
die('skip ASAN needs different stack limit setting due to more stack space usage');
12+
}
13+
?>
14+
--INI--
15+
zend.max_allowed_stack_size=256K
16+
--FILE--
17+
<?php
18+
// Build bottom-up so the insertion cycle-check stays O(1); top-down is O(n^2).
19+
$doc = new DOMDocument();
20+
$root = $doc->createElement('root');
21+
for ($s = 0; $s < 2; $s++) {
22+
$node = $doc->createElement('a');
23+
for ($i = 0; $i < 25000; $i++) {
24+
$parent = $doc->createElement('a');
25+
$parent->appendChild($node);
26+
$node = $parent;
27+
}
28+
$root->appendChild($node);
29+
}
30+
$doc->appendChild($root);
31+
32+
try {
33+
$doc->normalize();
34+
} catch (\Error $e) {
35+
echo "normalize: ", $e::class, ": ", $e->getMessage(), "\n";
36+
var_dump($e->getPrevious());
37+
}
38+
39+
try {
40+
$doc->normalizeDocument();
41+
} catch (\Error $e) {
42+
echo "normalizeDocument: ", $e::class, ": ", $e->getMessage(), "\n";
43+
var_dump($e->getPrevious());
44+
}
45+
?>
46+
--EXPECT--
47+
normalize: Error: Maximum call stack size reached. Infinite recursion?
48+
NULL
49+
normalizeDocument: Error: Maximum call stack size reached. Infinite recursion?
50+
NULL

‎ext/dom/tests/gh23120.phpt‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
--TEST--
2+
GH-23120 (Stack overflow when comparing deeply nested DOM nodes)
3+
--EXTENSIONS--
4+
dom
5+
--SKIPIF--
6+
<?php
7+
if (ini_get('zend.max_allowed_stack_size') === false) {
8+
die('skip No stack limit support');
9+
}
10+
if (getenv('SKIP_ASAN')) {
11+
die('skip ASAN needs different stack limit setting due to more stack space usage');
12+
}
13+
?>
14+
--INI--
15+
zend.max_allowed_stack_size=256K
16+
--FILE--
17+
<?php
18+
function create_deep_document(): DOMDocument {
19+
$doc = new DOMDocument();
20+
$node = $doc->createElement('leaf', 'x');
21+
22+
for ($i = 0; $i < 10000; $i++) {
23+
$parent = $doc->createElement('a');
24+
$parent->appendChild($node);
25+
$node = $parent;
26+
}
27+
28+
$doc->appendChild($node);
29+
return $doc;
30+
}
31+
32+
$doc1 = create_deep_document();
33+
$doc2 = create_deep_document();
34+
35+
try {
36+
var_dump($doc1->documentElement->isEqualNode($doc2->documentElement));
37+
} catch (\Error $e) {
38+
echo $e::class, ": ", $e->getMessage(), "\n";
39+
}
40+
?>
41+
--EXPECT--
42+
Error: Maximum call stack size reached.

0 commit comments

Comments
 (0)